diff --git a/ci/build.yml b/ci/build.yml index 0164cc8ab..288d2c5ad 100644 --- a/ci/build.yml +++ b/ci/build.yml @@ -133,6 +133,37 @@ core fw btconly production build: - firmware-T2T1-btconly-production-*.*.*-$CI_COMMIT_SHORT_SHA.bin expire_in: 1 week +core fw R debug build: + stage: build + <<: *gitlab_caching + needs: [] + variables: + TREZOR_MODEL: "R" + PYOPT: "0" + script: + - nix-shell --run "poetry run make -C core build_firmware" + - cp core/build/firmware/firmware.bin trezor-fw-debug-tr-$CORE_VERSION-$CI_COMMIT_SHORT_SHA.bin + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA" + paths: + - trezor-fw-*.*.*-$CI_COMMIT_SHORT_SHA.bin + expire_in: 1 week + +core fw R build: + stage: build + <<: *gitlab_caching + needs: [] + variables: + TREZOR_MODEL: "R" + script: + - nix-shell --run "poetry run make -C core build_firmware" + - cp core/build/firmware/firmware.bin trezor-fw-tr-$CORE_VERSION-$CI_COMMIT_SHORT_SHA.bin + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA" + paths: + - trezor-fw-*.*.*-$CI_COMMIT_SHORT_SHA.bin + expire_in: 1 week + # Non-frozen emulator build. This means you still need Python files # present which get interpreted. core unix regular build: @@ -235,6 +266,38 @@ core unix frozen debug build: untracked: true expire_in: 1 week +core unix frozen R debug build: + stage: build + <<: *gitlab_caching + needs: [] + variables: + PYOPT: "0" + TREZOR_MODEL: "R" + script: + - nix-shell --run "poetry run make -C core build_unix_frozen" + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA" + untracked: true + expire_in: 10 weeks + +core unix frozen R debug build arm: + image: nixos/nix + stage: build + <<: *gitlab_caching + needs: [] + variables: + PYOPT: "0" + TREZOR_MODEL: "R" + script: + - nix-shell --run "poetry run make -C core build_unix_frozen" + - mv core/build/unix/trezor-emu-core core/build/unix/trezor-emu-core-arm + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA" + untracked: true + expire_in: 10 weeks + tags: + - docker_darwin_arm + core unix frozen debug asan build: stage: build <<: *gitlab_caching diff --git a/ci/deploy.yml b/ci/deploy.yml index 9194b1b37..5df786f00 100644 --- a/ci/deploy.yml +++ b/ci/deploy.yml @@ -238,6 +238,7 @@ ui tests fixtures deploy: - core device test - core persistence test - legacy device test + - core device R test script: - echo "Deploying to $DEPLOY_PATH" - rsync --delete -va ci/ui_test_records/* "$DEPLOY_PATH" diff --git a/ci/test.yml b/ci/test.yml index c37e1d4d8..7b300f08f 100644 --- a/ci/test.yml +++ b/ci/test.yml @@ -81,6 +81,37 @@ core device test: reports: junit: tests/junit.xml +core device R test: + stage: test + <<: *gitlab_caching + needs: + - core unix frozen R debug build + variables: + TREZOR_PROFILING: "1" + TREZOR_MODEL: "R" + script: + - nix-shell --run "poetry run make -C core test_emu_ui | ts -s" + after_script: + - mv tests/ui_tests/reporting/reports/test/ test_ui_report + - nix-shell --run "poetry run python ci/prepare_ui_artifacts.py TR | ts -s" + - diff tests/ui_tests/fixtures.json tests/ui_tests/fixtures.suggestion.json + - nix-shell --run "cd tests/ui_tests ; poetry run python reporting/report_master_diff.py TR_" + - mv tests/ui_tests/reporting/reports/master_diff/ . + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA" + paths: + - ci/ui_test_records/ + - test_ui_report + - tests/ui_tests/screens/ + - tests/ui_tests/fixtures.suggestion.json + - tests/junit.xml + - tests/trezor.log + - master_diff + when: always + expire_in: 4 weeks + reports: + junit: tests/junit.xml + core device asan test: stage: test <<: *gitlab_caching @@ -293,6 +324,36 @@ core click test: expire_in: 1 week when: always +# Click tests. +# See [docs/tests/click-tests](../tests/click-tests.md) for more info. +core click R test: + stage: test + <<: *gitlab_caching + needs: + - core unix frozen R debug build + variables: + TREZOR_PROFILING: 1 + script: + - nix-shell --run "poetry run make -C core test_emu_click_ui | ts -s" + after_script: + - mv core/src/.coverage core/.coverage.test_click + - mv tests/ui_tests/reports/test/ test_ui_report + - nix-shell --run "poetry run python ci/prepare_ui_artifacts.py | ts -s" + - diff -u tests/ui_tests/fixtures.json tests/ui_tests/fixtures.suggestion.json + artifacts: + name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA" + paths: + - ci/ui_test_records/ + - test_ui_report + - tests/ui_tests/screens/ + - tests/ui_tests/fixtures.suggestion.json + - tests/trezor.log + - tests/junit.xml + reports: + junit: tests/junit.xml + expire_in: 1 week + when: always + core click asan test: stage: test <<: *gitlab_caching diff --git a/common/protob/messages-debug.proto b/common/protob/messages-debug.proto index 2ea26233c..d508311f9 100644 --- a/common/protob/messages-debug.proto +++ b/common/protob/messages-debug.proto @@ -39,10 +39,21 @@ message DebugLinkDecision { INFO = 2; } - optional uint32 x = 4; // touch X coordinate - optional uint32 y = 5; // touch Y coordinate - optional bool wait = 6; // wait for layout change - optional uint32 hold_ms = 7; // touch hold duration + /** + * Structure representing model R button presses + */ + // TODO: probably delete the middle_btn as it is not a physical one + enum DebugPhysicalButton { + LEFT_BTN = 0; + MIDDLE_BTN = 1; + RIGHT_BTN = 2; + } + + optional uint32 x = 4; // touch X coordinate + optional uint32 y = 5; // touch Y coordinate + optional bool wait = 6; // wait for layout change + optional uint32 hold_ms = 7; // touch hold duration + optional DebugPhysicalButton physical_button = 8; // physical button press } /** diff --git a/common/protob/messages-management.proto b/common/protob/messages-management.proto index cc87c9338..e31e67362 100644 --- a/common/protob/messages-management.proto +++ b/common/protob/messages-management.proto @@ -502,3 +502,11 @@ message UnlockPath { message UnlockedPathRequest { optional bytes mac = 1; // authentication code for future UnlockPath calls } + +/** + * Request: Show tutorial screens on the device + * @start + * @next Success + */ +message ShowDeviceTutorial { +} diff --git a/common/protob/messages.proto b/common/protob/messages.proto index 0c3e7f6b7..dd8119141 100644 --- a/common/protob/messages.proto +++ b/common/protob/messages.proto @@ -120,6 +120,7 @@ enum MessageType { reserved 90 to 92; MessageType_UnlockPath = 93 [(bitcoin_only) = true, (wire_in) = true]; MessageType_UnlockedPathRequest = 94 [(bitcoin_only) = true, (wire_out) = true]; + MessageType_ShowDeviceTutorial = 95 [(bitcoin_only) = true, (wire_in) = true]; MessageType_SetU2FCounter = 63 [(wire_in) = true]; MessageType_GetNextU2FCounter = 80 [(wire_in) = true]; diff --git a/core/.changelog.d/2610.added b/core/.changelog.d/2610.added new file mode 100644 index 000000000..beb5505e3 --- /dev/null +++ b/core/.changelog.d/2610.added @@ -0,0 +1 @@ +Implement UI for Model R diff --git a/core/SConscript.firmware b/core/SConscript.firmware index 2010228b5..30cb32f2e 100644 --- a/core/SConscript.firmware +++ b/core/SConscript.firmware @@ -29,9 +29,9 @@ SOURCE_MOD = [] PYOPT = ARGUMENTS.get('PYOPT', '1') FROZEN = True -if TREZOR_MODEL in ('1', 'R'): - FONT_NORMAL='Font_PixelOperator_Regular_8' - FONT_DEMIBOLD=None +if TREZOR_MODEL in ('R',): + FONT_NORMAL='Font_Unifont_Regular_16' + FONT_DEMIBOLD='Font_Unifont_Bold_16' FONT_BOLD='Font_PixelOperator_Bold_8' FONT_MONO='Font_PixelOperatorMono_Regular_8' if TREZOR_MODEL in ('T', ): @@ -571,11 +571,13 @@ if FROZEN: SOURCE_PY_DIR = 'src/' SOURCE_PY = Glob(SOURCE_PY_DIR + '*.py') - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/*.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/*.py', + exclude=[ + SOURCE_PY_DIR + 'trezor/sdcard.py', + ] if TREZOR_MODEL not in ('T',) else [] + )) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/crypto/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/*.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/*.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/common/*.py')) # UI layouts - common files and then model-specific. Exclude FIDO when BTC-only. SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/*.py', @@ -589,14 +591,22 @@ if FROZEN: SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/fido.py', ] if not EVERYTHING else [] )) - elif TREZOR_MODEL in ('1', 'R'): - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/__init__.py')) + elif TREZOR_MODEL in ('R'): + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/*.py', + exclude=[ + SOURCE_PY_DIR + 'trezor/ui/layouts/tr/fido.py', + ] if not EVERYTHING else [] + )) else: raise ValueError('Unknown Trezor model') SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/wire/*.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'storage/*.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'storage/*.py', + exclude=[ + SOURCE_PY_DIR + 'storage/sd_salt.py', + ] if TREZOR_MODEL not in ('T',) else [] + )) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/messages/__init__.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/*.py', @@ -616,8 +626,11 @@ if FROZEN: ) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/*.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/common/*.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/common/*/*.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/common/*.py', + exclude=[ + SOURCE_PY_DIR + 'apps/common/sdcard.py', + ] if TREZOR_MODEL not in ('T',) else [] + )) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/debug/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/management/*.py', diff --git a/core/SConscript.unix b/core/SConscript.unix index 2d9e02a12..b530d43a3 100644 --- a/core/SConscript.unix +++ b/core/SConscript.unix @@ -25,9 +25,9 @@ PYOPT = ARGUMENTS.get('PYOPT', '1') FROZEN = ARGUMENTS.get('TREZOR_EMULATOR_FROZEN', 0) RASPI = os.getenv('TREZOR_EMULATOR_RASPI') == '1' -if TREZOR_MODEL in ('1', 'R'): - FONT_NORMAL='Font_PixelOperator_Regular_8' - FONT_DEMIBOLD='Font_PixelOperator_Regular_8' +if TREZOR_MODEL in ('R',): + FONT_NORMAL='Font_Unifont_Regular_16' + FONT_DEMIBOLD='Font_Unifont_Bold_16' FONT_BOLD='Font_PixelOperator_Bold_8' FONT_MONO='Font_PixelOperatorMono_Regular_8' if TREZOR_MODEL in ('T', ): @@ -353,7 +353,7 @@ SOURCE_UNIX = [ 'vendor/micropython/ports/unix/input.c', 'vendor/micropython/ports/unix/unix_mphal.c', ] -if TREZOR_MODEL in ('T',): +if TREZOR_MODEL in ('T', 'R'): SOURCE_UNIX += [ 'embed/unix/sbu.c', 'embed/unix/sdcard.c', @@ -542,11 +542,13 @@ if FROZEN: SOURCE_PY_DIR = 'src/' SOURCE_PY = Glob(SOURCE_PY_DIR + '*.py') - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/*.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/*.py', + exclude=[ + SOURCE_PY_DIR + 'trezor/sdcard.py', + ] if TREZOR_MODEL not in ('T',) else [] + )) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/crypto/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/*.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/*.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/common/*.py')) # UI layouts - common files and then model-specific. Exclude FIDO when BTC-only. SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/*.py', @@ -560,14 +562,22 @@ if FROZEN: SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/fido.py', ] if not EVERYTHING else [] )) - elif TREZOR_MODEL in ('1', 'R'): - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/__init__.py')) + elif TREZOR_MODEL in ('R'): + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/*.py', + exclude=[ + SOURCE_PY_DIR + 'trezor/ui/layouts/tr/fido.py', + ] if not EVERYTHING else [] + )) else: raise ValueError('Unknown Trezor model') SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/wire/*.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'storage/*.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'storage/*.py', + exclude=[ + SOURCE_PY_DIR + 'storage/sd_salt.py', + ] if TREZOR_MODEL not in ('T',) else [] + )) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/messages/__init__.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/*.py', @@ -587,8 +597,11 @@ if FROZEN: ) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/*.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/common/*.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/common/*/*.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/common/*.py', + exclude=[ + SOURCE_PY_DIR + 'apps/common/sdcard.py', + ] if TREZOR_MODEL not in ('T',) else [] + )) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/debug/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/management/*.py', diff --git a/core/assets/model_r/amount.png b/core/assets/model_r/amount.png new file mode 100644 index 000000000..4ffc24edb Binary files /dev/null and b/core/assets/model_r/amount.png differ diff --git a/core/assets/model_r/amount_smaller.png b/core/assets/model_r/amount_smaller.png new file mode 100644 index 000000000..aeef73c16 Binary files /dev/null and b/core/assets/model_r/amount_smaller.png differ diff --git a/core/assets/model_r/back_up_arrow.png b/core/assets/model_r/back_up_arrow.png new file mode 100644 index 000000000..46670dafa Binary files /dev/null and b/core/assets/model_r/back_up_arrow.png differ diff --git a/core/assets/model_r/bin.png b/core/assets/model_r/bin.png new file mode 100644 index 000000000..3dc2c3c4b Binary files /dev/null and b/core/assets/model_r/bin.png differ diff --git a/core/assets/model_r/cancel_for_outline.png b/core/assets/model_r/cancel_for_outline.png new file mode 100644 index 000000000..b6fff7a40 Binary files /dev/null and b/core/assets/model_r/cancel_for_outline.png differ diff --git a/core/assets/model_r/cancel_no_outline.png b/core/assets/model_r/cancel_no_outline.png new file mode 100644 index 000000000..eb92d0e83 Binary files /dev/null and b/core/assets/model_r/cancel_no_outline.png differ diff --git a/core/assets/model_r/delete.png b/core/assets/model_r/delete.png new file mode 100644 index 000000000..f480bc2bc Binary files /dev/null and b/core/assets/model_r/delete.png differ diff --git a/core/assets/model_r/down_arrow.png b/core/assets/model_r/down_arrow.png new file mode 100644 index 000000000..2533409df Binary files /dev/null and b/core/assets/model_r/down_arrow.png differ diff --git a/core/assets/model_r/eye.png b/core/assets/model_r/eye.png new file mode 100644 index 000000000..c3068447a Binary files /dev/null and b/core/assets/model_r/eye.png differ diff --git a/core/assets/model_r/eye_round.png b/core/assets/model_r/eye_round.png new file mode 100644 index 000000000..78c8c4374 Binary files /dev/null and b/core/assets/model_r/eye_round.png differ diff --git a/core/assets/model_r/homescreen.png b/core/assets/model_r/homescreen.png new file mode 100644 index 000000000..29c15a199 Binary files /dev/null and b/core/assets/model_r/homescreen.png differ diff --git a/core/assets/model_r/left_arm.png b/core/assets/model_r/left_arm.png new file mode 100644 index 000000000..de6e65574 Binary files /dev/null and b/core/assets/model_r/left_arm.png differ diff --git a/core/assets/model_r/left_arrow.png b/core/assets/model_r/left_arrow.png new file mode 100644 index 000000000..c2f2c85ca Binary files /dev/null and b/core/assets/model_r/left_arrow.png differ diff --git a/core/assets/model_r/lock.png b/core/assets/model_r/lock.png new file mode 100644 index 000000000..1ac4f5088 Binary files /dev/null and b/core/assets/model_r/lock.png differ diff --git a/core/assets/model_r/logo_22_33.png b/core/assets/model_r/logo_22_33.png new file mode 100644 index 000000000..ea0497048 Binary files /dev/null and b/core/assets/model_r/logo_22_33.png differ diff --git a/core/assets/model_r/next_page.png b/core/assets/model_r/next_page.png new file mode 100644 index 000000000..d02c3fcf1 Binary files /dev/null and b/core/assets/model_r/next_page.png differ diff --git a/core/assets/model_r/param.png b/core/assets/model_r/param.png new file mode 100644 index 000000000..a69ec43f1 Binary files /dev/null and b/core/assets/model_r/param.png differ diff --git a/core/assets/model_r/param_smaller.png b/core/assets/model_r/param_smaller.png new file mode 100644 index 000000000..bec578ebd Binary files /dev/null and b/core/assets/model_r/param_smaller.png differ diff --git a/core/assets/model_r/prev_page.png b/core/assets/model_r/prev_page.png new file mode 100644 index 000000000..f9419fe49 Binary files /dev/null and b/core/assets/model_r/prev_page.png differ diff --git a/core/assets/model_r/right_arm.png b/core/assets/model_r/right_arm.png new file mode 100644 index 000000000..abb3117db Binary files /dev/null and b/core/assets/model_r/right_arm.png differ diff --git a/core/assets/model_r/right_arrow.png b/core/assets/model_r/right_arrow.png new file mode 100644 index 000000000..24d28fd0e Binary files /dev/null and b/core/assets/model_r/right_arrow.png differ diff --git a/core/assets/model_r/right_arrow_fat.png b/core/assets/model_r/right_arrow_fat.png new file mode 100644 index 000000000..8e8a37265 Binary files /dev/null and b/core/assets/model_r/right_arrow_fat.png differ diff --git a/core/assets/model_r/space.png b/core/assets/model_r/space.png new file mode 100644 index 000000000..ec3b47168 Binary files /dev/null and b/core/assets/model_r/space.png differ diff --git a/core/assets/model_r/tick.png b/core/assets/model_r/tick.png new file mode 100644 index 000000000..cec7f4402 Binary files /dev/null and b/core/assets/model_r/tick.png differ diff --git a/core/assets/model_r/tick_fat.png b/core/assets/model_r/tick_fat.png new file mode 100644 index 000000000..03e386c41 Binary files /dev/null and b/core/assets/model_r/tick_fat.png differ diff --git a/core/assets/model_r/up_arrow.png b/core/assets/model_r/up_arrow.png new file mode 100644 index 000000000..c7007d097 Binary files /dev/null and b/core/assets/model_r/up_arrow.png differ diff --git a/core/assets/model_r/user.png b/core/assets/model_r/user.png new file mode 100644 index 000000000..fbd51f8ec Binary files /dev/null and b/core/assets/model_r/user.png differ diff --git a/core/assets/model_r/user_smaller.png b/core/assets/model_r/user_smaller.png new file mode 100644 index 000000000..df61c6c36 Binary files /dev/null and b/core/assets/model_r/user_smaller.png differ diff --git a/core/assets/model_r/wallet.png b/core/assets/model_r/wallet.png new file mode 100644 index 000000000..505515f85 Binary files /dev/null and b/core/assets/model_r/wallet.png differ diff --git a/core/assets/model_r/warning_left.png b/core/assets/model_r/warning_left.png new file mode 100644 index 000000000..18dcfd565 Binary files /dev/null and b/core/assets/model_r/warning_left.png differ diff --git a/core/assets/model_r/warning_right.png b/core/assets/model_r/warning_right.png new file mode 100644 index 000000000..1705d4b3e Binary files /dev/null and b/core/assets/model_r/warning_right.png differ diff --git a/core/embed/lib/fonts/font_pixeloperatormono_regular_8.c b/core/embed/lib/fonts/font_pixeloperatormono_regular_8.c index ab72c6e2f..fcc5747f2 100644 --- a/core/embed/lib/fonts/font_pixeloperatormono_regular_8.c +++ b/core/embed/lib/fonts/font_pixeloperatormono_regular_8.c @@ -6,7 +6,7 @@ // - the third, fourth and fifth bytes are advance, bearingX and bearingY of the horizontal metrics of the glyph // - the rest is packed 1-bit glyph data -/* */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_32[] = { 0, 0, 8, 0, 0 }; +/* */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_32[] = { 0, 0, 7, 0, 0 }; // width hand-changed from 8 to 7 to have 9px space between words /* ! */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_33[] = { 1, 7, 7, 2, 7, 250 }; /* " */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_34[] = { 3, 3, 7, 1, 7, 182, 128 }; /* # */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_35[] = { 6, 6, 7, 0, 6, 75, 244, 146, 253, 32 }; diff --git a/core/embed/lib/fonts/font_unifont_bold_16.c b/core/embed/lib/fonts/font_unifont_bold_16.c new file mode 100644 index 000000000..ad4097cae --- /dev/null +++ b/core/embed/lib/fonts/font_unifont_bold_16.c @@ -0,0 +1,203 @@ +#include + +// clang-format off + +// - the first two bytes are width and height of the glyph +// - the third, fourth and fifth bytes are advance, bearingX and bearingY of the horizontal metrics of the glyph +// - the rest is packed 1-bit glyph data + +/* */ static const uint8_t Font_Unifont_Bold_16_glyph_32[] = { 0, 0, 8, 0, 0 }; +/* ! */ static const uint8_t Font_Unifont_Bold_16_glyph_33[] = { 2, 10, 7, 2, 10, 255, 252, 240 }; +/* " */ static const uint8_t Font_Unifont_Bold_16_glyph_34[] = { 6, 4, 7, 0, 12, 207, 60, 209, 0 }; +/* # */ static const uint8_t Font_Unifont_Bold_16_glyph_35[] = { 7, 10, 8, 0, 10, 54, 108, 223, 246, 205, 191, 236, 217, 176 }; +/* $ */ static const uint8_t Font_Unifont_Bold_16_glyph_36[] = { 7, 10, 8, 0, 10, 16, 251, 94, 183, 3, 181, 235, 124, 32 }; +/* % */ static const uint8_t Font_Unifont_Bold_16_glyph_37[] = { 7, 10, 8, 0, 10, 97, 102, 211, 65, 2, 11, 43, 150, 24 }; +/* & */ static const uint8_t Font_Unifont_Bold_16_glyph_38[] = { 8, 10, 8, 0, 10, 56, 108, 108, 104, 48, 107, 206, 204, 206, 122, 0 }; +/* ' */ static const uint8_t Font_Unifont_Bold_16_glyph_39[] = { 2, 4, 7, 2, 12, 253, 0 }; +/* ( */ static const uint8_t Font_Unifont_Bold_16_glyph_40[] = { 4, 12, 7, 2, 11, 54, 108, 204, 204, 198, 99, 0 }; +/* ) */ static const uint8_t Font_Unifont_Bold_16_glyph_41[] = { 4, 12, 7, 0, 11, 198, 99, 51, 51, 54, 108, 0 }; +/* * */ static const uint8_t Font_Unifont_Bold_16_glyph_42[] = { 7, 7, 8, 0, 8, 17, 37, 81, 197, 82, 68, 0 }; +/* + */ static const uint8_t Font_Unifont_Bold_16_glyph_43[] = { 7, 7, 8, 0, 8, 16, 32, 71, 241, 2, 4, 0 }; +/* , */ static const uint8_t Font_Unifont_Bold_16_glyph_44[] = { 3, 4, 7, 1, 2, 237, 224 }; +/* - */ static const uint8_t Font_Unifont_Bold_16_glyph_45[] = { 5, 1, 7, 0, 5, 248 }; +/* . */ static const uint8_t Font_Unifont_Bold_16_glyph_46[] = { 2, 2, 7, 2, 2, 240 }; +/* / */ static const uint8_t Font_Unifont_Bold_16_glyph_47[] = { 6, 10, 7, 0, 10, 12, 49, 132, 48, 194, 24, 195, 0 }; +/* 0 */ static const uint8_t Font_Unifont_Bold_16_glyph_48[] = { 7, 10, 8, 0, 10, 56, 219, 30, 125, 122, 249, 227, 108, 112 }; +/* 1 */ static const uint8_t Font_Unifont_Bold_16_glyph_49[] = { 6, 10, 7, 0, 10, 51, 195, 12, 48, 195, 12, 51, 240 }; +/* 2 */ static const uint8_t Font_Unifont_Bold_16_glyph_50[] = { 7, 10, 8, 0, 10, 125, 143, 24, 48, 195, 12, 112, 193, 252 }; +/* 3 */ static const uint8_t Font_Unifont_Bold_16_glyph_51[] = { 7, 10, 8, 0, 10, 125, 143, 24, 49, 192, 193, 227, 198, 248 }; +/* 4 */ static const uint8_t Font_Unifont_Bold_16_glyph_52[] = { 7, 10, 8, 0, 10, 12, 120, 179, 100, 217, 191, 134, 12, 24 }; +/* 5 */ static const uint8_t Font_Unifont_Bold_16_glyph_53[] = { 7, 10, 8, 0, 10, 255, 131, 6, 15, 193, 193, 227, 198, 248 }; +/* 6 */ static const uint8_t Font_Unifont_Bold_16_glyph_54[] = { 7, 10, 8, 0, 10, 60, 195, 6, 15, 216, 241, 227, 198, 248 }; +/* 7 */ static const uint8_t Font_Unifont_Bold_16_glyph_55[] = { 6, 10, 7, 0, 10, 252, 48, 195, 24, 99, 12, 97, 128 }; +/* 8 */ static const uint8_t Font_Unifont_Bold_16_glyph_56[] = { 7, 10, 8, 0, 10, 125, 143, 30, 55, 216, 241, 227, 198, 248 }; +/* 9 */ static const uint8_t Font_Unifont_Bold_16_glyph_57[] = { 7, 10, 8, 0, 10, 125, 143, 30, 60, 111, 193, 131, 12, 240 }; +/* : */ static const uint8_t Font_Unifont_Bold_16_glyph_58[] = { 2, 7, 7, 2, 8, 240, 60 }; +/* ; */ static const uint8_t Font_Unifont_Bold_16_glyph_59[] = { 2, 9, 7, 2, 8, 240, 61, 128 }; +/* < */ static const uint8_t Font_Unifont_Bold_16_glyph_60[] = { 6, 9, 7, 0, 9, 12, 99, 24, 193, 131, 6, 12 }; +/* = */ static const uint8_t Font_Unifont_Bold_16_glyph_61[] = { 6, 5, 7, 0, 7, 252, 0, 0, 252 }; +/* > */ static const uint8_t Font_Unifont_Bold_16_glyph_62[] = { 6, 9, 7, 0, 9, 193, 131, 6, 12, 99, 24, 192 }; +/* ? */ static const uint8_t Font_Unifont_Bold_16_glyph_63[] = { 7, 10, 8, 0, 10, 125, 143, 24, 48, 195, 6, 0, 24, 48 }; +/* @ */ static const uint8_t Font_Unifont_Bold_16_glyph_64[] = { 7, 10, 8, 0, 10, 60, 134, 109, 187, 118, 237, 205, 64, 124 }; +/* A */ static const uint8_t Font_Unifont_Bold_16_glyph_65[] = { 7, 10, 8, 0, 10, 56, 249, 182, 60, 120, 255, 227, 199, 140 }; +/* B */ static const uint8_t Font_Unifont_Bold_16_glyph_66[] = { 7, 10, 8, 0, 10, 253, 143, 30, 63, 216, 241, 227, 199, 248 }; +/* C */ static const uint8_t Font_Unifont_Bold_16_glyph_67[] = { 7, 10, 8, 0, 10, 125, 143, 30, 12, 24, 48, 99, 198, 248 }; +/* D */ static const uint8_t Font_Unifont_Bold_16_glyph_68[] = { 7, 10, 8, 0, 10, 241, 155, 30, 60, 120, 241, 227, 205, 224 }; +/* E */ static const uint8_t Font_Unifont_Bold_16_glyph_69[] = { 6, 10, 7, 0, 10, 255, 12, 48, 251, 12, 48, 195, 240 }; +/* F */ static const uint8_t Font_Unifont_Bold_16_glyph_70[] = { 6, 10, 7, 0, 10, 255, 12, 48, 195, 236, 48, 195, 0 }; +/* G */ static const uint8_t Font_Unifont_Bold_16_glyph_71[] = { 7, 10, 8, 0, 10, 125, 143, 30, 12, 27, 241, 227, 206, 236 }; +/* H */ static const uint8_t Font_Unifont_Bold_16_glyph_72[] = { 7, 10, 8, 0, 10, 199, 143, 30, 63, 248, 241, 227, 199, 140 }; +/* I */ static const uint8_t Font_Unifont_Bold_16_glyph_73[] = { 6, 10, 7, 0, 10, 252, 195, 12, 48, 195, 12, 51, 240 }; +/* J */ static const uint8_t Font_Unifont_Bold_16_glyph_74[] = { 7, 10, 8, 0, 10, 62, 24, 48, 96, 193, 131, 102, 204, 240 }; +/* K */ static const uint8_t Font_Unifont_Bold_16_glyph_75[] = { 7, 10, 8, 0, 10, 199, 143, 54, 207, 28, 62, 110, 207, 140 }; +/* L */ static const uint8_t Font_Unifont_Bold_16_glyph_76[] = { 6, 10, 7, 0, 10, 195, 12, 48, 195, 12, 48, 195, 240 }; +/* M */ static const uint8_t Font_Unifont_Bold_16_glyph_77[] = { 7, 10, 8, 0, 10, 131, 143, 31, 127, 250, 245, 227, 199, 140 }; +/* N */ static const uint8_t Font_Unifont_Bold_16_glyph_78[] = { 7, 10, 8, 0, 10, 199, 207, 158, 189, 122, 245, 231, 207, 140 }; +/* O */ static const uint8_t Font_Unifont_Bold_16_glyph_79[] = { 7, 10, 8, 0, 10, 125, 143, 30, 60, 120, 241, 227, 198, 248 }; +/* P */ static const uint8_t Font_Unifont_Bold_16_glyph_80[] = { 7, 10, 8, 0, 10, 253, 143, 30, 60, 127, 176, 96, 193, 128 }; +/* Q */ static const uint8_t Font_Unifont_Bold_16_glyph_81[] = { 7, 11, 8, 0, 10, 125, 143, 30, 60, 120, 241, 235, 238, 112, 24 }; +/* R */ static const uint8_t Font_Unifont_Bold_16_glyph_82[] = { 7, 10, 8, 0, 10, 253, 143, 30, 60, 127, 182, 102, 199, 140 }; +/* S */ static const uint8_t Font_Unifont_Bold_16_glyph_83[] = { 7, 10, 8, 0, 10, 125, 143, 31, 7, 135, 131, 227, 198, 248 }; +/* T */ static const uint8_t Font_Unifont_Bold_16_glyph_84[] = { 7, 10, 8, 0, 10, 254, 48, 96, 193, 131, 6, 12, 24, 48 }; +/* U */ static const uint8_t Font_Unifont_Bold_16_glyph_85[] = { 7, 10, 8, 0, 10, 199, 143, 30, 60, 120, 241, 227, 238, 248 }; +/* V */ static const uint8_t Font_Unifont_Bold_16_glyph_86[] = { 7, 10, 8, 0, 10, 199, 143, 26, 38, 205, 155, 20, 40, 112 }; +/* W */ static const uint8_t Font_Unifont_Bold_16_glyph_87[] = { 7, 10, 8, 0, 10, 199, 143, 30, 189, 122, 245, 255, 238, 136 }; +/* X */ static const uint8_t Font_Unifont_Bold_16_glyph_88[] = { 7, 10, 8, 0, 10, 199, 141, 179, 99, 135, 27, 54, 199, 140 }; +/* Y */ static const uint8_t Font_Unifont_Bold_16_glyph_89[] = { 6, 10, 7, 0, 10, 207, 60, 243, 73, 227, 12, 48, 192 }; +/* Z */ static const uint8_t Font_Unifont_Bold_16_glyph_90[] = { 7, 10, 8, 0, 10, 254, 12, 56, 225, 135, 28, 112, 193, 252 }; +/* [ */ static const uint8_t Font_Unifont_Bold_16_glyph_91[] = { 4, 12, 7, 2, 11, 252, 204, 204, 204, 204, 207, 0 }; +/* \ */ static const uint8_t Font_Unifont_Bold_16_glyph_92[] = { 6, 10, 7, 0, 10, 195, 6, 8, 48, 193, 6, 12, 48 }; +/* ] */ static const uint8_t Font_Unifont_Bold_16_glyph_93[] = { 4, 12, 7, 0, 11, 243, 51, 51, 51, 51, 63, 0 }; +/* ^ */ static const uint8_t Font_Unifont_Bold_16_glyph_94[] = { 7, 3, 8, 0, 12, 56, 219, 24 }; +/* _ */ static const uint8_t Font_Unifont_Bold_16_glyph_95[] = { 7, 1, 7, 0, 0, 254 }; +/* ` */ static const uint8_t Font_Unifont_Bold_16_glyph_96[] = { 4, 3, 7, 0, 13, 198, 48 }; +/* a */ static const uint8_t Font_Unifont_Bold_16_glyph_97[] = { 7, 8, 8, 0, 8, 125, 140, 27, 252, 120, 243, 187, 0 }; +/* b */ static const uint8_t Font_Unifont_Bold_16_glyph_98[] = { 7, 11, 8, 0, 11, 193, 131, 6, 238, 120, 241, 227, 199, 207, 112 }; +/* c */ static const uint8_t Font_Unifont_Bold_16_glyph_99[] = { 7, 8, 8, 0, 8, 125, 143, 30, 12, 24, 241, 190, 0 }; +/* d */ static const uint8_t Font_Unifont_Bold_16_glyph_100[] = { 7, 11, 8, 0, 11, 6, 12, 25, 188, 248, 241, 227, 199, 156, 216 }; +/* e */ static const uint8_t Font_Unifont_Bold_16_glyph_101[] = { 7, 8, 8, 0, 8, 125, 143, 31, 252, 24, 241, 190, 0 }; +/* f */ static const uint8_t Font_Unifont_Bold_16_glyph_102[] = { 7, 11, 8, 0, 11, 30, 96, 193, 143, 230, 12, 24, 48, 97, 240 }; +/* g */ static const uint8_t Font_Unifont_Bold_16_glyph_103[] = { 7, 11, 8, 0, 9, 2, 247, 54, 108, 207, 8, 62, 207, 141, 240 }; +/* h */ static const uint8_t Font_Unifont_Bold_16_glyph_104[] = { 7, 11, 8, 0, 11, 193, 131, 6, 238, 120, 241, 227, 199, 143, 24 }; +/* i */ static const uint8_t Font_Unifont_Bold_16_glyph_105[] = { 6, 11, 7, 0, 11, 48, 192, 60, 48, 195, 12, 48, 207, 192 }; +/* j */ static const uint8_t Font_Unifont_Bold_16_glyph_106[] = { 6, 13, 8, 0, 11, 24, 96, 31, 12, 48, 195, 12, 60, 246, 112 }; +/* k */ static const uint8_t Font_Unifont_Bold_16_glyph_107[] = { 7, 11, 8, 0, 11, 193, 131, 6, 60, 251, 60, 120, 217, 159, 24 }; +/* l */ static const uint8_t Font_Unifont_Bold_16_glyph_108[] = { 6, 11, 7, 0, 11, 240, 195, 12, 48, 195, 12, 48, 207, 192 }; +/* m */ static const uint8_t Font_Unifont_Bold_16_glyph_109[] = { 7, 8, 8, 0, 8, 237, 175, 94, 189, 122, 245, 235, 0 }; +/* n */ static const uint8_t Font_Unifont_Bold_16_glyph_110[] = { 7, 8, 8, 0, 8, 221, 207, 30, 60, 120, 241, 227, 0 }; +/* o */ static const uint8_t Font_Unifont_Bold_16_glyph_111[] = { 7, 8, 8, 0, 8, 125, 143, 30, 60, 120, 241, 190, 0 }; +/* p */ static const uint8_t Font_Unifont_Bold_16_glyph_112[] = { 7, 10, 8, 0, 8, 221, 207, 30, 60, 120, 249, 238, 193, 128 }; +/* q */ static const uint8_t Font_Unifont_Bold_16_glyph_113[] = { 7, 10, 8, 0, 8, 119, 159, 30, 60, 120, 243, 187, 6, 12 }; +/* r */ static const uint8_t Font_Unifont_Bold_16_glyph_114[] = { 7, 8, 8, 0, 8, 221, 207, 30, 12, 24, 48, 96, 0 }; +/* s */ static const uint8_t Font_Unifont_Bold_16_glyph_115[] = { 7, 8, 8, 0, 8, 125, 143, 27, 129, 216, 241, 190, 0 }; +/* t */ static const uint8_t Font_Unifont_Bold_16_glyph_116[] = { 7, 10, 8, 0, 10, 48, 96, 199, 243, 6, 12, 24, 48, 60 }; +/* u */ static const uint8_t Font_Unifont_Bold_16_glyph_117[] = { 7, 8, 8, 0, 8, 199, 143, 30, 60, 120, 243, 187, 0 }; +/* v */ static const uint8_t Font_Unifont_Bold_16_glyph_118[] = { 7, 8, 8, 0, 8, 199, 143, 26, 38, 205, 142, 28, 0 }; +/* w */ static const uint8_t Font_Unifont_Bold_16_glyph_119[] = { 7, 8, 8, 0, 8, 199, 175, 94, 189, 122, 245, 182, 0 }; +/* x */ static const uint8_t Font_Unifont_Bold_16_glyph_120[] = { 7, 8, 8, 0, 8, 199, 141, 177, 195, 141, 177, 227, 0 }; +/* y */ static const uint8_t Font_Unifont_Bold_16_glyph_121[] = { 7, 10, 8, 0, 8, 199, 143, 30, 60, 109, 205, 131, 6, 248 }; +/* z */ static const uint8_t Font_Unifont_Bold_16_glyph_122[] = { 7, 8, 8, 0, 8, 254, 12, 56, 227, 142, 56, 127, 0 }; +/* { */ static const uint8_t Font_Unifont_Bold_16_glyph_123[] = { 5, 13, 7, 1, 11, 59, 24, 99, 51, 12, 49, 152, 195, 128 }; +/* | */ static const uint8_t Font_Unifont_Bold_16_glyph_124[] = { 2, 14, 7, 2, 12, 255, 255, 255, 240 }; +/* } */ static const uint8_t Font_Unifont_Bold_16_glyph_125[] = { 5, 13, 7, 0, 11, 225, 140, 198, 24, 102, 99, 12, 110, 0 }; +/* ~ */ static const uint8_t Font_Unifont_Bold_16_glyph_126[] = { 7, 3, 8, 0, 11, 99, 118, 48 }; + +const uint8_t Font_Unifont_Bold_16_glyph_nonprintable[] = { 7, 10, 8, 0, 10, 130, 112, 231, 207, 60, 249, 255, 231, 207 }; + +const uint8_t * const Font_Unifont_Bold_16[126 + 1 - 32] = { + Font_Unifont_Bold_16_glyph_32, + Font_Unifont_Bold_16_glyph_33, + Font_Unifont_Bold_16_glyph_34, + Font_Unifont_Bold_16_glyph_35, + Font_Unifont_Bold_16_glyph_36, + Font_Unifont_Bold_16_glyph_37, + Font_Unifont_Bold_16_glyph_38, + Font_Unifont_Bold_16_glyph_39, + Font_Unifont_Bold_16_glyph_40, + Font_Unifont_Bold_16_glyph_41, + Font_Unifont_Bold_16_glyph_42, + Font_Unifont_Bold_16_glyph_43, + Font_Unifont_Bold_16_glyph_44, + Font_Unifont_Bold_16_glyph_45, + Font_Unifont_Bold_16_glyph_46, + Font_Unifont_Bold_16_glyph_47, + Font_Unifont_Bold_16_glyph_48, + Font_Unifont_Bold_16_glyph_49, + Font_Unifont_Bold_16_glyph_50, + Font_Unifont_Bold_16_glyph_51, + Font_Unifont_Bold_16_glyph_52, + Font_Unifont_Bold_16_glyph_53, + Font_Unifont_Bold_16_glyph_54, + Font_Unifont_Bold_16_glyph_55, + Font_Unifont_Bold_16_glyph_56, + Font_Unifont_Bold_16_glyph_57, + Font_Unifont_Bold_16_glyph_58, + Font_Unifont_Bold_16_glyph_59, + Font_Unifont_Bold_16_glyph_60, + Font_Unifont_Bold_16_glyph_61, + Font_Unifont_Bold_16_glyph_62, + Font_Unifont_Bold_16_glyph_63, + Font_Unifont_Bold_16_glyph_64, + Font_Unifont_Bold_16_glyph_65, + Font_Unifont_Bold_16_glyph_66, + Font_Unifont_Bold_16_glyph_67, + Font_Unifont_Bold_16_glyph_68, + Font_Unifont_Bold_16_glyph_69, + Font_Unifont_Bold_16_glyph_70, + Font_Unifont_Bold_16_glyph_71, + Font_Unifont_Bold_16_glyph_72, + Font_Unifont_Bold_16_glyph_73, + Font_Unifont_Bold_16_glyph_74, + Font_Unifont_Bold_16_glyph_75, + Font_Unifont_Bold_16_glyph_76, + Font_Unifont_Bold_16_glyph_77, + Font_Unifont_Bold_16_glyph_78, + Font_Unifont_Bold_16_glyph_79, + Font_Unifont_Bold_16_glyph_80, + Font_Unifont_Bold_16_glyph_81, + Font_Unifont_Bold_16_glyph_82, + Font_Unifont_Bold_16_glyph_83, + Font_Unifont_Bold_16_glyph_84, + Font_Unifont_Bold_16_glyph_85, + Font_Unifont_Bold_16_glyph_86, + Font_Unifont_Bold_16_glyph_87, + Font_Unifont_Bold_16_glyph_88, + Font_Unifont_Bold_16_glyph_89, + Font_Unifont_Bold_16_glyph_90, + Font_Unifont_Bold_16_glyph_91, + Font_Unifont_Bold_16_glyph_92, + Font_Unifont_Bold_16_glyph_93, + Font_Unifont_Bold_16_glyph_94, + Font_Unifont_Bold_16_glyph_95, + Font_Unifont_Bold_16_glyph_96, + Font_Unifont_Bold_16_glyph_97, + Font_Unifont_Bold_16_glyph_98, + Font_Unifont_Bold_16_glyph_99, + Font_Unifont_Bold_16_glyph_100, + Font_Unifont_Bold_16_glyph_101, + Font_Unifont_Bold_16_glyph_102, + Font_Unifont_Bold_16_glyph_103, + Font_Unifont_Bold_16_glyph_104, + Font_Unifont_Bold_16_glyph_105, + Font_Unifont_Bold_16_glyph_106, + Font_Unifont_Bold_16_glyph_107, + Font_Unifont_Bold_16_glyph_108, + Font_Unifont_Bold_16_glyph_109, + Font_Unifont_Bold_16_glyph_110, + Font_Unifont_Bold_16_glyph_111, + Font_Unifont_Bold_16_glyph_112, + Font_Unifont_Bold_16_glyph_113, + Font_Unifont_Bold_16_glyph_114, + Font_Unifont_Bold_16_glyph_115, + Font_Unifont_Bold_16_glyph_116, + Font_Unifont_Bold_16_glyph_117, + Font_Unifont_Bold_16_glyph_118, + Font_Unifont_Bold_16_glyph_119, + Font_Unifont_Bold_16_glyph_120, + Font_Unifont_Bold_16_glyph_121, + Font_Unifont_Bold_16_glyph_122, + Font_Unifont_Bold_16_glyph_123, + Font_Unifont_Bold_16_glyph_124, + Font_Unifont_Bold_16_glyph_125, + Font_Unifont_Bold_16_glyph_126, +}; diff --git a/core/embed/lib/fonts/font_unifont_bold_16.h b/core/embed/lib/fonts/font_unifont_bold_16.h new file mode 100644 index 000000000..3dbac99a0 --- /dev/null +++ b/core/embed/lib/fonts/font_unifont_bold_16.h @@ -0,0 +1,10 @@ +#include + +#if TREZOR_FONT_BPP != 1 +#error Wrong TREZOR_FONT_BPP (expected 1) +#endif +#define Font_Unifont_Bold_16_HEIGHT 12 // <--- 12 from 16 +#define Font_Unifont_Bold_16_MAX_HEIGHT 12 // <--- 12 from 15 +#define Font_Unifont_Bold_16_BASELINE 2 +extern const uint8_t* const Font_Unifont_Bold_16[126 + 1 - 32]; +extern const uint8_t Font_Unifont_Bold_16_glyph_nonprintable[]; diff --git a/core/embed/lib/fonts/font_unifont_regular_16.c b/core/embed/lib/fonts/font_unifont_regular_16.c new file mode 100644 index 000000000..93eaadc25 --- /dev/null +++ b/core/embed/lib/fonts/font_unifont_regular_16.c @@ -0,0 +1,207 @@ +#include + +// clang-format off + +// - the first two bytes are width and height of the glyph +// - the third, fourth and fifth bytes are advance, bearingX and bearingY of the horizontal metrics of the glyph +// - the rest is packed 1-bit glyph data + +// MANUAL CHANGES! +// In cases where the width and advance were the same (usually 7 and 7), increasing +// the advance to 8, so that these wide letters do not collide with the following one. + +/* */ static const uint8_t Font_Unifont_Regular_16_glyph_32[] = { 0, 0, 8, 0, 0 }; +/* ! */ static const uint8_t Font_Unifont_Regular_16_glyph_33[] = { 1, 10, 7, 3, 10, 254, 192 }; +/* " */ static const uint8_t Font_Unifont_Regular_16_glyph_34[] = { 5, 4, 7, 1, 12, 140, 99, 16 }; +/* # */ static const uint8_t Font_Unifont_Regular_16_glyph_35[] = { 6, 10, 7, 0, 10, 36, 146, 127, 73, 47, 228, 146, 64 }; +/* $ */ static const uint8_t Font_Unifont_Regular_16_glyph_36[] = { 7, 10, 8, 0, 10, 16, 250, 76, 135, 3, 132, 201, 124, 32 }; // < --- advanced changed from 7 to 8 +/* % */ static const uint8_t Font_Unifont_Regular_16_glyph_37[] = { 7, 10, 8, 0, 10, 99, 42, 83, 65, 2, 11, 41, 83, 24 }; // < --- advanced changed from 7 to 8 +/* & */ static const uint8_t Font_Unifont_Regular_16_glyph_38[] = { 7, 10, 8, 0, 10, 56, 137, 17, 67, 10, 98, 194, 140, 228 }; // < --- advanced changed from 7 to 8 +/* ' */ static const uint8_t Font_Unifont_Regular_16_glyph_39[] = { 1, 4, 7, 3, 12, 240 }; +/* ( */ static const uint8_t Font_Unifont_Regular_16_glyph_40[] = { 3, 12, 7, 2, 11, 41, 73, 36, 137, 16 }; +/* ) */ static const uint8_t Font_Unifont_Regular_16_glyph_41[] = { 3, 12, 7, 1, 11, 137, 18, 73, 41, 64 }; +/* * */ static const uint8_t Font_Unifont_Regular_16_glyph_42[] = { 7, 7, 8, 0, 8, 17, 37, 81, 197, 82, 68, 0 }; // < --- advanced changed from 7 to 8 +/* + */ static const uint8_t Font_Unifont_Regular_16_glyph_43[] = { 7, 7, 8, 0, 8, 16, 32, 71, 241, 2, 4, 0 }; // < --- advanced changed from 7 to 8 +/* , */ static const uint8_t Font_Unifont_Regular_16_glyph_44[] = { 2, 4, 7, 2, 2, 214, 0 }; +/* - */ static const uint8_t Font_Unifont_Regular_16_glyph_45[] = { 4, 1, 7, 1, 5, 240 }; +/* . */ static const uint8_t Font_Unifont_Regular_16_glyph_46[] = { 2, 2, 7, 2, 2, 240 }; +/* / */ static const uint8_t Font_Unifont_Regular_16_glyph_47[] = { 6, 10, 7, 0, 10, 4, 16, 132, 16, 130, 16, 130, 0 }; +/* 0 */ static const uint8_t Font_Unifont_Regular_16_glyph_48[] = { 6, 10, 7, 0, 10, 49, 40, 99, 150, 156, 97, 72, 192 }; +/* 1 */ static const uint8_t Font_Unifont_Regular_16_glyph_49[] = { 5, 10, 7, 1, 10, 35, 40, 66, 16, 132, 39, 192 }; +/* 2 */ static const uint8_t Font_Unifont_Regular_16_glyph_50[] = { 6, 10, 7, 0, 10, 122, 24, 65, 24, 132, 32, 131, 240 }; +/* 3 */ static const uint8_t Font_Unifont_Regular_16_glyph_51[] = { 6, 10, 7, 0, 10, 122, 24, 65, 56, 16, 97, 133, 224 }; +/* 4 */ static const uint8_t Font_Unifont_Regular_16_glyph_52[] = { 6, 10, 7, 0, 10, 8, 98, 146, 138, 47, 194, 8, 32 }; +/* 5 */ static const uint8_t Font_Unifont_Regular_16_glyph_53[] = { 6, 10, 7, 0, 10, 254, 8, 32, 248, 16, 65, 133, 224 }; +/* 6 */ static const uint8_t Font_Unifont_Regular_16_glyph_54[] = { 6, 10, 7, 0, 10, 57, 8, 32, 250, 24, 97, 133, 224 }; +/* 7 */ static const uint8_t Font_Unifont_Regular_16_glyph_55[] = { 6, 10, 7, 0, 10, 252, 16, 66, 8, 33, 4, 16, 64 }; +/* 8 */ static const uint8_t Font_Unifont_Regular_16_glyph_56[] = { 6, 10, 7, 0, 10, 122, 24, 97, 122, 24, 97, 133, 224 }; +/* 9 */ static const uint8_t Font_Unifont_Regular_16_glyph_57[] = { 6, 10, 7, 0, 10, 122, 24, 97, 124, 16, 65, 9, 192 }; +/* : */ static const uint8_t Font_Unifont_Regular_16_glyph_58[] = { 2, 7, 7, 2, 8, 240, 60 }; +/* ; */ static const uint8_t Font_Unifont_Regular_16_glyph_59[] = { 2, 9, 7, 2, 8, 240, 53, 128 }; +/* < */ static const uint8_t Font_Unifont_Regular_16_glyph_60[] = { 5, 9, 7, 1, 9, 8, 136, 136, 32, 130, 8 }; +/* = */ static const uint8_t Font_Unifont_Regular_16_glyph_61[] = { 6, 5, 7, 0, 7, 252, 0, 0, 252 }; +/* > */ static const uint8_t Font_Unifont_Regular_16_glyph_62[] = { 5, 9, 7, 0, 9, 130, 8, 32, 136, 136, 128 }; +/* ? */ static const uint8_t Font_Unifont_Regular_16_glyph_63[] = { 6, 10, 7, 0, 10, 122, 24, 65, 8, 65, 0, 16, 64 }; +/* @ */ static const uint8_t Font_Unifont_Regular_16_glyph_64[] = { 6, 10, 7, 0, 10, 57, 25, 107, 166, 154, 103, 64, 240 }; +/* A */ static const uint8_t Font_Unifont_Regular_16_glyph_65[] = { 6, 10, 7, 0, 10, 49, 36, 161, 135, 248, 97, 134, 16 }; +/* B */ static const uint8_t Font_Unifont_Regular_16_glyph_66[] = { 6, 10, 7, 0, 10, 250, 24, 97, 250, 24, 97, 135, 224 }; +/* C */ static const uint8_t Font_Unifont_Regular_16_glyph_67[] = { 6, 10, 7, 0, 10, 122, 24, 96, 130, 8, 33, 133, 224 }; +/* D */ static const uint8_t Font_Unifont_Regular_16_glyph_68[] = { 6, 10, 7, 0, 10, 242, 40, 97, 134, 24, 97, 139, 192 }; +/* E */ static const uint8_t Font_Unifont_Regular_16_glyph_69[] = { 6, 10, 7, 0, 10, 254, 8, 32, 250, 8, 32, 131, 240 }; +/* F */ static const uint8_t Font_Unifont_Regular_16_glyph_70[] = { 6, 10, 7, 0, 10, 254, 8, 32, 250, 8, 32, 130, 0 }; +/* G */ static const uint8_t Font_Unifont_Regular_16_glyph_71[] = { 6, 10, 7, 0, 10, 122, 24, 96, 130, 120, 97, 141, 208 }; +/* H */ static const uint8_t Font_Unifont_Regular_16_glyph_72[] = { 6, 10, 7, 0, 10, 134, 24, 97, 254, 24, 97, 134, 16 }; +/* I */ static const uint8_t Font_Unifont_Regular_16_glyph_73[] = { 5, 10, 7, 1, 10, 249, 8, 66, 16, 132, 39, 192 }; +/* J */ static const uint8_t Font_Unifont_Regular_16_glyph_74[] = { 7, 10, 8, 0, 10, 62, 16, 32, 64, 129, 2, 68, 136, 224 }; // < --- advanced changed from 7 to 8 +/* K */ static const uint8_t Font_Unifont_Regular_16_glyph_75[] = { 6, 10, 7, 0, 10, 134, 41, 40, 195, 10, 36, 138, 16 }; +/* L */ static const uint8_t Font_Unifont_Regular_16_glyph_76[] = { 6, 10, 7, 0, 10, 130, 8, 32, 130, 8, 32, 131, 240 }; +/* M */ static const uint8_t Font_Unifont_Regular_16_glyph_77[] = { 6, 10, 7, 0, 10, 134, 28, 243, 182, 216, 97, 134, 16 }; +/* N */ static const uint8_t Font_Unifont_Regular_16_glyph_78[] = { 6, 10, 7, 0, 10, 135, 28, 105, 166, 89, 99, 142, 16 }; +/* O */ static const uint8_t Font_Unifont_Regular_16_glyph_79[] = { 6, 10, 7, 0, 10, 122, 24, 97, 134, 24, 97, 133, 224 }; +/* P */ static const uint8_t Font_Unifont_Regular_16_glyph_80[] = { 6, 10, 7, 0, 10, 250, 24, 97, 250, 8, 32, 130, 0 }; +/* Q */ static const uint8_t Font_Unifont_Regular_16_glyph_81[] = { 7, 11, 8, 0, 10, 121, 10, 20, 40, 80, 161, 90, 204, 240, 24 }; // < --- advanced changed from 7 to 8 +/* R */ static const uint8_t Font_Unifont_Regular_16_glyph_82[] = { 6, 10, 7, 0, 10, 250, 24, 97, 250, 72, 162, 134, 16 }; +/* S */ static const uint8_t Font_Unifont_Regular_16_glyph_83[] = { 6, 10, 7, 0, 10, 122, 24, 96, 96, 96, 97, 133, 224 }; +/* T */ static const uint8_t Font_Unifont_Regular_16_glyph_84[] = { 7, 10, 8, 0, 10, 254, 32, 64, 129, 2, 4, 8, 16, 32 }; // < --- advanced changed from 7 to 8 +/* U */ static const uint8_t Font_Unifont_Regular_16_glyph_85[] = { 6, 10, 7, 0, 10, 134, 24, 97, 134, 24, 97, 133, 224 }; +/* V */ static const uint8_t Font_Unifont_Regular_16_glyph_86[] = { 7, 10, 8, 0, 10, 131, 6, 10, 36, 72, 138, 20, 16, 32 }; // < --- advanced changed from 7 to 8 +/* W */ static const uint8_t Font_Unifont_Regular_16_glyph_87[] = { 6, 10, 7, 0, 10, 134, 24, 97, 182, 220, 243, 134, 16 }; +/* X */ static const uint8_t Font_Unifont_Regular_16_glyph_88[] = { 6, 10, 7, 0, 10, 134, 20, 146, 48, 196, 146, 134, 16 }; +/* Y */ static const uint8_t Font_Unifont_Regular_16_glyph_89[] = { 7, 10, 8, 0, 10, 131, 5, 18, 34, 130, 4, 8, 16, 32 }; // < --- advanced changed from 7 to 8 +/* Z */ static const uint8_t Font_Unifont_Regular_16_glyph_90[] = { 6, 10, 7, 0, 10, 252, 16, 66, 16, 132, 32, 131, 240 }; +/* [ */ static const uint8_t Font_Unifont_Regular_16_glyph_91[] = { 3, 12, 7, 3, 11, 242, 73, 36, 146, 112 }; +/* \ */ static const uint8_t Font_Unifont_Regular_16_glyph_92[] = { 6, 10, 7, 0, 10, 130, 4, 8, 32, 65, 2, 4, 16 }; +/* ] */ static const uint8_t Font_Unifont_Regular_16_glyph_93[] = { 3, 12, 7, 0, 11, 228, 146, 73, 36, 240 }; +/* ^ */ static const uint8_t Font_Unifont_Regular_16_glyph_94[] = { 6, 3, 7, 0, 12, 49, 40, 64 }; +/* _ */ static const uint8_t Font_Unifont_Regular_16_glyph_95[] = { 7, 1, 8, 0, 0, 254 }; // < --- advanced changed from 7 to 8 +/* ` */ static const uint8_t Font_Unifont_Regular_16_glyph_96[] = { 3, 3, 7, 1, 13, 136, 128 }; +/* a */ static const uint8_t Font_Unifont_Regular_16_glyph_97[] = { 6, 8, 7, 0, 8, 122, 16, 95, 134, 24, 221, 0 }; +/* b */ static const uint8_t Font_Unifont_Regular_16_glyph_98[] = { 6, 11, 7, 0, 11, 130, 8, 46, 198, 24, 97, 135, 27, 128 }; +/* c */ static const uint8_t Font_Unifont_Regular_16_glyph_99[] = { 6, 8, 7, 0, 8, 122, 24, 32, 130, 8, 94, 0 }; +/* d */ static const uint8_t Font_Unifont_Regular_16_glyph_100[] = { 6, 11, 7, 0, 11, 4, 16, 93, 142, 24, 97, 134, 55, 64 }; +/* e */ static const uint8_t Font_Unifont_Regular_16_glyph_101[] = { 6, 8, 7, 0, 8, 122, 24, 127, 130, 8, 94, 0 }; +/* f */ static const uint8_t Font_Unifont_Regular_16_glyph_102[] = { 5, 11, 7, 0, 11, 25, 8, 79, 144, 132, 33, 8 }; +/* g */ static const uint8_t Font_Unifont_Regular_16_glyph_103[] = { 6, 11, 7, 0, 9, 5, 216, 162, 137, 196, 30, 134, 23, 128 }; +/* h */ static const uint8_t Font_Unifont_Regular_16_glyph_104[] = { 6, 11, 7, 0, 11, 130, 8, 46, 198, 24, 97, 134, 24, 64 }; +/* i */ static const uint8_t Font_Unifont_Regular_16_glyph_105[] = { 5, 11, 7, 1, 11, 33, 0, 194, 16, 132, 33, 62 }; +/* j */ static const uint8_t Font_Unifont_Regular_16_glyph_106[] = { 5, 13, 7, 0, 11, 8, 64, 48, 132, 33, 8, 67, 38, 0 }; +/* k */ static const uint8_t Font_Unifont_Regular_16_glyph_107[] = { 6, 11, 7, 0, 11, 130, 8, 34, 146, 140, 40, 146, 40, 64 }; +/* l */ static const uint8_t Font_Unifont_Regular_16_glyph_108[] = { 5, 11, 7, 1, 11, 97, 8, 66, 16, 132, 33, 62 }; +/* m */ static const uint8_t Font_Unifont_Regular_16_glyph_109[] = { 7, 8, 8, 0, 8, 237, 38, 76, 153, 50, 100, 201, 0 }; // < --- advanced changed from 7 to 8 +/* n */ static const uint8_t Font_Unifont_Regular_16_glyph_110[] = { 6, 8, 7, 0, 8, 187, 24, 97, 134, 24, 97, 0 }; +/* o */ static const uint8_t Font_Unifont_Regular_16_glyph_111[] = { 6, 8, 7, 0, 8, 122, 24, 97, 134, 24, 94, 0 }; +/* p */ static const uint8_t Font_Unifont_Regular_16_glyph_112[] = { 6, 10, 7, 0, 8, 187, 24, 97, 134, 28, 110, 130, 0 }; +/* q */ static const uint8_t Font_Unifont_Regular_16_glyph_113[] = { 6, 10, 7, 0, 8, 118, 56, 97, 134, 24, 221, 4, 16 }; +/* r */ static const uint8_t Font_Unifont_Regular_16_glyph_114[] = { 6, 8, 7, 0, 8, 187, 24, 96, 130, 8, 32, 0 }; +/* s */ static const uint8_t Font_Unifont_Regular_16_glyph_115[] = { 6, 8, 7, 0, 8, 122, 24, 24, 24, 24, 94, 0 }; +/* t */ static const uint8_t Font_Unifont_Regular_16_glyph_116[] = { 5, 10, 7, 0, 10, 33, 9, 242, 16, 132, 32, 192 }; +/* u */ static const uint8_t Font_Unifont_Regular_16_glyph_117[] = { 6, 8, 7, 0, 8, 134, 24, 97, 134, 24, 221, 0 }; +/* v */ static const uint8_t Font_Unifont_Regular_16_glyph_118[] = { 6, 8, 7, 0, 8, 134, 24, 82, 73, 35, 12, 0 }; +/* w */ static const uint8_t Font_Unifont_Regular_16_glyph_119[] = { 7, 8, 8, 0, 8, 131, 38, 76, 153, 50, 100, 182, 0 }; // < --- advanced changed from 7 to 8 +/* x */ static const uint8_t Font_Unifont_Regular_16_glyph_120[] = { 6, 8, 7, 0, 8, 134, 20, 140, 49, 40, 97, 0 }; +/* y */ static const uint8_t Font_Unifont_Regular_16_glyph_121[] = { 6, 10, 7, 0, 8, 134, 24, 97, 133, 51, 65, 5, 224 }; +/* z */ static const uint8_t Font_Unifont_Regular_16_glyph_122[] = { 6, 8, 7, 0, 8, 252, 16, 132, 33, 8, 63, 0 }; +/* { */ static const uint8_t Font_Unifont_Regular_16_glyph_123[] = { 4, 13, 7, 1, 11, 52, 66, 36, 132, 34, 68, 48 }; +/* | */ static const uint8_t Font_Unifont_Regular_16_glyph_124[] = { 1, 14, 7, 3, 12, 255, 252 }; +/* } */ static const uint8_t Font_Unifont_Regular_16_glyph_125[] = { 4, 13, 7, 1, 11, 194, 36, 66, 18, 68, 34, 192 }; +/* ~ */ static const uint8_t Font_Unifont_Regular_16_glyph_126[] = { 7, 3, 8, 0, 11, 99, 38, 48 }; // < --- advanced changed from 7 to 8 + +const uint8_t Font_Unifont_Regular_16_glyph_nonprintable[] = { 6, 10, 7, 0, 10, 133, 231, 190, 247, 190, 255, 239, 191 }; + +const uint8_t * const Font_Unifont_Regular_16[126 + 1 - 32] = { + Font_Unifont_Regular_16_glyph_32, + Font_Unifont_Regular_16_glyph_33, + Font_Unifont_Regular_16_glyph_34, + Font_Unifont_Regular_16_glyph_35, + Font_Unifont_Regular_16_glyph_36, + Font_Unifont_Regular_16_glyph_37, + Font_Unifont_Regular_16_glyph_38, + Font_Unifont_Regular_16_glyph_39, + Font_Unifont_Regular_16_glyph_40, + Font_Unifont_Regular_16_glyph_41, + Font_Unifont_Regular_16_glyph_42, + Font_Unifont_Regular_16_glyph_43, + Font_Unifont_Regular_16_glyph_44, + Font_Unifont_Regular_16_glyph_45, + Font_Unifont_Regular_16_glyph_46, + Font_Unifont_Regular_16_glyph_47, + Font_Unifont_Regular_16_glyph_48, + Font_Unifont_Regular_16_glyph_49, + Font_Unifont_Regular_16_glyph_50, + Font_Unifont_Regular_16_glyph_51, + Font_Unifont_Regular_16_glyph_52, + Font_Unifont_Regular_16_glyph_53, + Font_Unifont_Regular_16_glyph_54, + Font_Unifont_Regular_16_glyph_55, + Font_Unifont_Regular_16_glyph_56, + Font_Unifont_Regular_16_glyph_57, + Font_Unifont_Regular_16_glyph_58, + Font_Unifont_Regular_16_glyph_59, + Font_Unifont_Regular_16_glyph_60, + Font_Unifont_Regular_16_glyph_61, + Font_Unifont_Regular_16_glyph_62, + Font_Unifont_Regular_16_glyph_63, + Font_Unifont_Regular_16_glyph_64, + Font_Unifont_Regular_16_glyph_65, + Font_Unifont_Regular_16_glyph_66, + Font_Unifont_Regular_16_glyph_67, + Font_Unifont_Regular_16_glyph_68, + Font_Unifont_Regular_16_glyph_69, + Font_Unifont_Regular_16_glyph_70, + Font_Unifont_Regular_16_glyph_71, + Font_Unifont_Regular_16_glyph_72, + Font_Unifont_Regular_16_glyph_73, + Font_Unifont_Regular_16_glyph_74, + Font_Unifont_Regular_16_glyph_75, + Font_Unifont_Regular_16_glyph_76, + Font_Unifont_Regular_16_glyph_77, + Font_Unifont_Regular_16_glyph_78, + Font_Unifont_Regular_16_glyph_79, + Font_Unifont_Regular_16_glyph_80, + Font_Unifont_Regular_16_glyph_81, + Font_Unifont_Regular_16_glyph_82, + Font_Unifont_Regular_16_glyph_83, + Font_Unifont_Regular_16_glyph_84, + Font_Unifont_Regular_16_glyph_85, + Font_Unifont_Regular_16_glyph_86, + Font_Unifont_Regular_16_glyph_87, + Font_Unifont_Regular_16_glyph_88, + Font_Unifont_Regular_16_glyph_89, + Font_Unifont_Regular_16_glyph_90, + Font_Unifont_Regular_16_glyph_91, + Font_Unifont_Regular_16_glyph_92, + Font_Unifont_Regular_16_glyph_93, + Font_Unifont_Regular_16_glyph_94, + Font_Unifont_Regular_16_glyph_95, + Font_Unifont_Regular_16_glyph_96, + Font_Unifont_Regular_16_glyph_97, + Font_Unifont_Regular_16_glyph_98, + Font_Unifont_Regular_16_glyph_99, + Font_Unifont_Regular_16_glyph_100, + Font_Unifont_Regular_16_glyph_101, + Font_Unifont_Regular_16_glyph_102, + Font_Unifont_Regular_16_glyph_103, + Font_Unifont_Regular_16_glyph_104, + Font_Unifont_Regular_16_glyph_105, + Font_Unifont_Regular_16_glyph_106, + Font_Unifont_Regular_16_glyph_107, + Font_Unifont_Regular_16_glyph_108, + Font_Unifont_Regular_16_glyph_109, + Font_Unifont_Regular_16_glyph_110, + Font_Unifont_Regular_16_glyph_111, + Font_Unifont_Regular_16_glyph_112, + Font_Unifont_Regular_16_glyph_113, + Font_Unifont_Regular_16_glyph_114, + Font_Unifont_Regular_16_glyph_115, + Font_Unifont_Regular_16_glyph_116, + Font_Unifont_Regular_16_glyph_117, + Font_Unifont_Regular_16_glyph_118, + Font_Unifont_Regular_16_glyph_119, + Font_Unifont_Regular_16_glyph_120, + Font_Unifont_Regular_16_glyph_121, + Font_Unifont_Regular_16_glyph_122, + Font_Unifont_Regular_16_glyph_123, + Font_Unifont_Regular_16_glyph_124, + Font_Unifont_Regular_16_glyph_125, + Font_Unifont_Regular_16_glyph_126, +}; diff --git a/core/embed/lib/fonts/font_unifont_regular_16.h b/core/embed/lib/fonts/font_unifont_regular_16.h new file mode 100644 index 000000000..2ecaa9d8f --- /dev/null +++ b/core/embed/lib/fonts/font_unifont_regular_16.h @@ -0,0 +1,10 @@ +#include + +#if TREZOR_FONT_BPP != 1 +#error Wrong TREZOR_FONT_BPP (expected 1) +#endif +#define Font_Unifont_Regular_16_HEIGHT 12 // <--- 12 from 16 +#define Font_Unifont_Regular_16_MAX_HEIGHT 12 // <--- 12 from 15 +#define Font_Unifont_Regular_16_BASELINE 2 +extern const uint8_t* const Font_Unifont_Regular_16[126 + 1 - 32]; +extern const uint8_t Font_Unifont_Regular_16_glyph_nonprintable[]; diff --git a/core/embed/rust/Cargo.toml b/core/embed/rust/Cargo.toml index f8fe0d092..40c54cb48 100644 --- a/core/embed/rust/Cargo.toml +++ b/core/embed/rust/Cargo.toml @@ -21,13 +21,23 @@ button = [] touch = [] clippy = [] jpeg = [] -disp_i8080_8bit_dw = [] # write pixels directly to peripheral -disp_i8080_16bit_dw = [] # write pixels directly to peripheral +disp_i8080_8bit_dw = [] # write pixels directly to peripheral +disp_i8080_16bit_dw = [] # write pixels directly to peripheral debug = ["ui_debug"] sbu = [] sd_card = [] rgb_led = [] -test = ["cc", "glob", "micropython", "protobuf", "ui", "ui_debug", "dma2d", "touch"] +test = [ + "button", + "cc", + "glob", + "micropython", + "protobuf", + "ui", + "ui_debug", + "dma2d", + "touch", +] [lib] crate-type = ["staticlib"] diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index f51f020f7..807e31fda 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -20,9 +20,12 @@ static void _librust_qstrs(void) { MP_QSTR_action; MP_QSTR_active; MP_QSTR_address; + MP_QSTR_address_title; MP_QSTR_allow_cancel; + MP_QSTR_amount; MP_QSTR_amount_change; MP_QSTR_amount_new; + MP_QSTR_amount_title; MP_QSTR_app_name; MP_QSTR_attach_timer_fn; MP_QSTR_bootscreen; @@ -36,13 +39,14 @@ static void _librust_qstrs(void) { MP_QSTR_confirm_coinjoin; MP_QSTR_confirm_fido; MP_QSTR_confirm_homescreen; + MP_QSTR_confirm_joint_total; MP_QSTR_confirm_modify_fee; MP_QSTR_confirm_modify_output; MP_QSTR_confirm_more; + MP_QSTR_confirm_output; MP_QSTR_confirm_properties; MP_QSTR_confirm_recovery; MP_QSTR_confirm_reset_device; - MP_QSTR_confirm_text; MP_QSTR_confirm_total; MP_QSTR_confirm_value; MP_QSTR_confirm_with_info; @@ -53,6 +57,8 @@ static void _librust_qstrs(void) { MP_QSTR_draw_welcome_screen; MP_QSTR_dry_run; MP_QSTR_extra; + MP_QSTR_fee_amount; + MP_QSTR_fee_label; MP_QSTR_fee_rate; MP_QSTR_fee_rate_amount; MP_QSTR_hold; @@ -89,6 +95,7 @@ static void _librust_qstrs(void) { MP_QSTR_reverse; MP_QSTR_select_word; MP_QSTR_select_word_count; + MP_QSTR_share_words; MP_QSTR_show_address_details; MP_QSTR_show_checklist; MP_QSTR_show_error; @@ -107,15 +114,20 @@ static void _librust_qstrs(void) { MP_QSTR_show_warning; MP_QSTR_sign; MP_QSTR_skip_first_paint; + MP_QSTR_spending_amount; MP_QSTR_subprompt; MP_QSTR_subtitle; MP_QSTR_time_ms; MP_QSTR_timer; MP_QSTR_title; + MP_QSTR_toif_info; + MP_QSTR_total_amount; MP_QSTR_total_fee_new; + MP_QSTR_total_label; MP_QSTR_touch_event; MP_QSTR_trace; MP_QSTR_trezorui2; + MP_QSTR_tutorial; MP_QSTR_usb_event; MP_QSTR_user_fee_change; MP_QSTR_value; diff --git a/core/embed/rust/src/docs/ButtonsTR.drawio.png b/core/embed/rust/src/docs/ButtonsTR.drawio.png new file mode 100644 index 000000000..58e2316f1 Binary files /dev/null and b/core/embed/rust/src/docs/ButtonsTR.drawio.png differ diff --git a/core/embed/rust/src/error.rs b/core/embed/rust/src/error.rs index d3e26e91b..044adf517 100644 --- a/core/embed/rust/src/error.rs +++ b/core/embed/rust/src/error.rs @@ -26,6 +26,13 @@ pub enum Error { ValueErrorParam(&'static CStr, Obj), } +#[macro_export] +macro_rules! value_error { + ($msg:expr) => { + Error::ValueError(cstr!($msg)) + }; +} + #[cfg(feature = "micropython")] impl Error { /// Create an exception instance matching the error code. The result of this diff --git a/core/embed/rust/src/lib.rs b/core/embed/rust/src/lib.rs index 49b874fd0..48de63523 100644 --- a/core/embed/rust/src/lib.rs +++ b/core/embed/rust/src/lib.rs @@ -11,6 +11,7 @@ #[macro_use] extern crate num_derive; +#[macro_use] mod error; // use trezorhal for its macros early #[macro_use] diff --git a/core/embed/rust/src/micropython/buffer.rs b/core/embed/rust/src/micropython/buffer.rs index debd6e431..9cf330ad1 100644 --- a/core/embed/rust/src/micropython/buffer.rs +++ b/core/embed/rust/src/micropython/buffer.rs @@ -1,6 +1,10 @@ use core::{convert::TryFrom, ops::Deref, ptr, slice, str}; -use crate::{error::Error, micropython::obj::Obj, strutil::hexlify}; +use crate::{ + error::Error, + micropython::obj::Obj, + strutil::{hexlify, SkipPrefix}, +}; use super::ffi; @@ -20,7 +24,7 @@ use super::ffi; /// The `off` field represents offset from the `ptr` and allows us to do /// substring slices while keeping the head pointer as required by GC. #[repr(C)] -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct StrBuffer { ptr: *const u8, len: u16, @@ -89,8 +93,10 @@ impl StrBuffer { unsafe { slice::from_raw_parts(self.ptr.add(self.off.into()), self.len.into()) } } } +} - pub fn offset(&self, skip_bytes: usize) -> Self { +impl SkipPrefix for StrBuffer { + fn skip_prefix(&self, skip_bytes: usize) -> Self { let off: u16 = unwrap!(skip_bytes.try_into()); assert!(off <= self.len); assert!(self.as_ref().is_char_boundary(skip_bytes)); @@ -251,5 +257,5 @@ pub fn hexlify_bytes(obj: Obj, offset: usize, max_len: usize) -> Result for Obj { } } +impl TryFrom<(Obj, Obj, Obj)> for Obj { + type Error = Error; + + fn try_from(val: (Obj, Obj, Obj)) -> Result { + // SAFETY: + // - Should work with any micropython objects. + // EXCEPTION: Will raise if allocation fails. + let values = [val.0, val.1, val.2]; + let obj = catch_exception(|| unsafe { ffi::mp_obj_new_tuple(3, values.as_ptr()) })?; + if obj.is_null() { + Err(Error::AllocationFailed) + } else { + Ok(obj) + } + } +} + // // # Additional conversions based on the methods above. // diff --git a/core/embed/rust/src/protobuf/error.rs b/core/embed/rust/src/protobuf/error.rs index 68d8950d3..78449fc33 100644 --- a/core/embed/rust/src/protobuf/error.rs +++ b/core/embed/rust/src/protobuf/error.rs @@ -1,31 +1,25 @@ -use cstr_core::CStr; +use cstr_core::cstr; use crate::{error::Error, micropython::qstr::Qstr}; // XXX const version of `from_bytes_with_nul_unchecked` is nightly-only. pub fn experimental_not_enabled() -> Error { - let msg = - unsafe { CStr::from_bytes_with_nul_unchecked(b"Experimental features are disabled.\0") }; - Error::ValueError(msg) + value_error!("Experimental features are disabled.") } pub fn unknown_field_type() -> Error { - let msg = unsafe { CStr::from_bytes_with_nul_unchecked(b"Unknown field type.\0") }; - Error::ValueError(msg) + value_error!("Unknown field type.") } pub fn missing_required_field(field: Qstr) -> Error { - let msg = unsafe { CStr::from_bytes_with_nul_unchecked(b"Missing required field\0") }; - Error::ValueErrorParam(msg, field.into()) + Error::ValueErrorParam(cstr!("Missing required field."), field.into()) } pub fn invalid_value(field: Qstr) -> Error { - let msg = unsafe { CStr::from_bytes_with_nul_unchecked(b"Invalid value for field\0") }; - Error::ValueErrorParam(msg, field.into()) + Error::ValueErrorParam(cstr!("Invalid value for field."), field.into()) } pub fn end_of_buffer() -> Error { - let msg = unsafe { CStr::from_bytes_with_nul_unchecked(b"End of buffer.\0") }; - Error::ValueError(msg) + value_error!("End of buffer.") } diff --git a/core/embed/rust/src/storage/mod.rs b/core/embed/rust/src/storage/mod.rs index a9a123ddc..0bb147b0e 100644 --- a/core/embed/rust/src/storage/mod.rs +++ b/core/embed/rust/src/storage/mod.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] -use crate::trezorhal::storage::{get, get_length}; +use crate::trezorhal::storage::{get, get_length, StorageResult}; pub const HOMESCREEN_MAX_SIZE: usize = 16384; @@ -36,26 +36,13 @@ const INITIALIZED: u16 = FLAG_PUBLIC | APP_DEVICE | 0x0013; const SAFETY_CHECK_LEVEL: u16 = APP_DEVICE | 0x0014; const EXPERIMENTAL_FEATURES: u16 = APP_DEVICE | 0x0015; -pub fn get_avatar_len() -> Result { - let avatar_len_res = get_length(HOMESCREEN); - if let Ok(len) = avatar_len_res { - Ok(len) - } else { - Err(()) - } +pub fn get_avatar_len() -> StorageResult { + get_length(HOMESCREEN) } -pub fn get_avatar(buffer: &mut [u8]) -> Result { - let avatar_len_res = get_length(HOMESCREEN); - - if let Ok(len) = avatar_len_res { - if len <= buffer.len() { - unwrap!(get(HOMESCREEN, buffer)); - Ok(len) - } else { - Err(()) - } - } else { - Err(()) - } +pub fn load_avatar(dest: &mut [u8]) -> StorageResult<()> { + let dest_len = dest.len(); + let result = get(HOMESCREEN, dest)?; + ensure!(dest_len == result.len(), "Internal error in load_avatar"); + Ok(()) } diff --git a/core/embed/rust/src/strutil.rs b/core/embed/rust/src/strutil.rs index 5e48f464e..6b2c43f37 100644 --- a/core/embed/rust/src/strutil.rs +++ b/core/embed/rust/src/strutil.rs @@ -1,3 +1,35 @@ +use heapless::String; + +/// Trait for slicing off string prefix by a specified number of bytes. +/// See `StringType` for deeper explanation. +pub trait SkipPrefix { + fn skip_prefix(&self, bytes: usize) -> Self; +} + +// XXX only implemented in bootloader, as we don't want &str to satisfy +// StringType in the main firmware. This is because we want to avoid duplication +// of every StringType-parametrized component. +#[cfg(feature = "bootloader")] +impl SkipPrefix for &str { + fn skip_prefix(&self, chars: usize) -> Self { + &self[chars..] + } +} + +/// Trait for internal representation of strings. +/// Exists so that we can support `StrBuffer` as well as `&str` in the UI +/// components. Implies the following operations: +/// - dereference into a short-lived `&str` reference (AsRef) +/// - create a new string by skipping some number of bytes (SkipPrefix) - used +/// when rendering continuations of long strings +/// - create a new string from a string literal (From<&'static str>) +pub trait StringType: AsRef + From<&'static str> + SkipPrefix {} + +impl StringType for T where T: AsRef + From<&'static str> + SkipPrefix {} + +/// Unified-length String type, long enough for most simple use-cases. +pub type ShortString = String<50>; + pub fn hexlify(data: &[u8], buffer: &mut [u8]) { const HEX_LOWER: [u8; 16] = *b"0123456789abcdef"; let mut i: usize = 0; diff --git a/core/embed/rust/src/trace.rs b/core/embed/rust/src/trace.rs index 5c844f163..23bfd5c60 100644 --- a/core/embed/rust/src/trace.rs +++ b/core/embed/rust/src/trace.rs @@ -5,6 +5,7 @@ pub trait Tracer { fn int(&mut self, key: &str, i: i64); fn string(&mut self, key: &str, s: &str); fn bool(&mut self, key: &str, b: bool); + fn null(&mut self, key: &str); fn in_child(&mut self, key: &str, block: &dyn Fn(&mut dyn Tracer)); fn in_list(&mut self, key: &str, block: &dyn Fn(&mut dyn ListTracer)); @@ -169,6 +170,11 @@ impl Tracer for JsonTracer { (self.write_fn)(if b { "true" } else { "false" }); } + fn null(&mut self, key: &str) { + self.key(key); + (self.write_fn)("null"); + } + fn in_child(&mut self, key: &str, block: &dyn Fn(&mut dyn Tracer)) { self.key(key); (self.write_fn)("{"); diff --git a/core/embed/rust/src/trezorhal/random.rs b/core/embed/rust/src/trezorhal/random.rs index 8f64b0773..340e48ce5 100644 --- a/core/embed/rust/src/trezorhal/random.rs +++ b/core/embed/rust/src/trezorhal/random.rs @@ -9,3 +9,42 @@ pub fn shuffle(slice: &mut [T]) { slice.swap(i, j); } } + +/// Returns a random number in the range [min, max]. +pub fn uniform_between(min: u32, max: u32) -> u32 { + assert!(max > min); + uniform(max - min + 1) + min +} + +/// Returns a random number in the range [min, max] except one `except` number. +pub fn uniform_between_except(min: u32, max: u32, except: u32) -> u32 { + assert!(max > min); + // Generate uniform_between as long as it is not except + loop { + let rand = uniform_between(min, max); + if rand != except { + return rand; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn uniform_between_test() { + for _ in 0..10 { + assert!((10..=11).contains(&uniform_between(10, 11))); + assert!((10..=12).contains(&uniform_between(10, 12))); + assert!((256..=512).contains(&uniform_between(256, 512))); + } + } + + #[test] + fn uniform_between_except_test() { + for _ in 0..10 { + assert!(uniform_between_except(10, 12, 11) != 11); + } + } +} diff --git a/core/embed/rust/src/trezorhal/storage.rs b/core/embed/rust/src/trezorhal/storage.rs index c05621c78..d4cd14f4d 100644 --- a/core/embed/rust/src/trezorhal/storage.rs +++ b/core/embed/rust/src/trezorhal/storage.rs @@ -64,12 +64,12 @@ pub enum StorageError { impl From for Error { fn from(err: StorageError) -> Self { match err { - StorageError::InvalidData => Error::ValueError(cstr!("Invalid data for storage")), - StorageError::WriteFailed => Error::ValueError(cstr!("Storage write failed")), - StorageError::ReadFailed => Error::ValueError(cstr!("Storage read failed")), - StorageError::DeleteFailed => Error::ValueError(cstr!("Storage delete failed")), + StorageError::InvalidData => value_error!("Invalid data for storage"), + StorageError::WriteFailed => value_error!("Storage write failed"), + StorageError::ReadFailed => value_error!("Storage read failed"), + StorageError::DeleteFailed => value_error!("Storage delete failed"), StorageError::CounterFailed => { - Error::ValueError(cstr!("Retrieving counter value failed")) + value_error!("Retrieving counter value failed") } } } diff --git a/core/embed/rust/src/trezorhal/wordlist.rs b/core/embed/rust/src/trezorhal/wordlist.rs index 44d91aea6..ecfd497e5 100644 --- a/core/embed/rust/src/trezorhal/wordlist.rs +++ b/core/embed/rust/src/trezorhal/wordlist.rs @@ -4,56 +4,76 @@ use cstr_core::CStr; /// Holds all the possible words with the possibility to interact /// with the "list" - filtering it further, getting their count, etc. -pub struct Wordlist(&'static [*const cty::c_char]); +pub struct Wordlist { + words: &'static [*const cty::c_char], + /// Holds the length of prefix which was used to filter the list + /// (how many beginning characters are common for all words). + prefix_len: usize, +} impl Wordlist { + pub fn new(words: &'static [*const cty::c_char], prefix_len: usize) -> Self { + Self { words, prefix_len } + } + /// Initialize BIP39 wordlist. pub fn bip39() -> Self { - Self(unsafe { &ffi::BIP39_WORDLIST_ENGLISH }) + Self::new(unsafe { &ffi::BIP39_WORDLIST_ENGLISH }, 0) } /// Initialize SLIP39 wordlist. pub fn slip39() -> Self { - Self(unsafe { &ffi::SLIP39_WORDLIST }) + Self::new(unsafe { &ffi::SLIP39_WORDLIST }, 0) + } + + /// Returns all possible letters from current wordlist that form a valid + /// word. Alphabetically sorted. + pub fn get_available_letters(&self) -> impl Iterator { + let mut prev_char = '\0'; + let prefix_len = self.prefix_len; + self.iter().filter_map(move |word| { + if word.len() <= prefix_len { + return None; + } + let following_char = unwrap!(word.chars().nth(prefix_len)); + if following_char != prev_char { + prev_char = following_char; + Some(following_char) + } else { + None + } + }) } /// Only leaves words that have a specified prefix. Throw away others. pub fn filter_prefix(&self, prefix: &str) -> Self { - let mut start = 0usize; - let mut end = self.0.len(); - for (i, word) in self.0.iter().enumerate() { - // SAFETY: We assume our slice is an array of 0-terminated strings. - match unsafe { prefix_cmp(prefix, *word) } { - Ordering::Less => { - start = i + 1; - } - Ordering::Greater => { - end = i; - break; - } - _ => {} - } - } - Self(&self.0[start..end]) + // SAFETY: We assume our slice is an array of 0-terminated strings. + let start = self + .words + .partition_point(|&word| matches!(unsafe { prefix_cmp(prefix, word) }, Ordering::Less)); + let end = self.words.partition_point(|&word| { + !matches!(unsafe { prefix_cmp(prefix, word) }, Ordering::Greater) + }); + Self::new(&self.words[start..end], prefix.len()) } /// Get a word at the certain position. pub fn get(&self, index: usize) -> Option<&'static str> { // SAFETY: we assume every word in the wordlist is a valid 0-terminated UTF-8 // string. - self.0 + self.words .get(index) .map(|word| unsafe { from_utf8_unchecked(*word) }) } /// How many words are currently in the list. pub const fn len(&self) -> usize { - self.0.len() + self.words.len() } /// Iterator of all current words. pub fn iter(&self) -> impl Iterator { - self.0 + self.words .iter() .map(|word| unsafe { from_utf8_unchecked(*word) }) } @@ -158,4 +178,36 @@ mod tests { .collect::>(); assert_eq!(result, expected_result); } + #[test] + fn test_get_available_letters() { + let result = Wordlist::bip39() + .filter_prefix("ab") + .get_available_letters() + .collect::>(); + let expected_result = vec!['a', 'i', 'l', 'o', 's', 'u']; + assert_eq!(result, expected_result); + + let result = Wordlist::bip39() + .filter_prefix("str") + .get_available_letters() + .collect::>(); + let expected_result = vec!['a', 'e', 'i', 'o', 'u']; + assert_eq!(result, expected_result); + + let result = Wordlist::bip39() + .filter_prefix("zoo") + .get_available_letters() + .collect::>(); + let expected_result = vec![]; + assert_eq!(result, expected_result); + + let result = Wordlist::bip39() + .get_available_letters() + .collect::>(); + let expected_result = vec![ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', + 'r', 's', 't', 'u', 'v', 'w', 'y', 'z', + ]; + assert_eq!(result, expected_result); + } } diff --git a/core/embed/rust/src/ui/component/base.rs b/core/embed/rust/src/ui/component/base.rs index cb108699f..b65828fd7 100644 --- a/core/embed/rust/src/ui/component/base.rs +++ b/core/embed/rust/src/ui/component/base.rs @@ -17,6 +17,8 @@ use crate::ui::event::ButtonEvent; use crate::ui::event::TouchEvent; use crate::ui::event::USBEvent; +use super::Paginate; + /// Type used by components that do not return any messages. /// /// Alternative to the yet-unstable `!`-type. @@ -153,6 +155,16 @@ where } } +impl Paginate for Child { + fn page_count(&mut self) -> usize { + self.component.page_count() + } + + fn change_page(&mut self, active_page: usize) { + self.component.change_page(active_page); + } +} + impl PaintOverlapping for Child where T: PaintOverlapping, diff --git a/core/embed/rust/src/ui/component/label.rs b/core/embed/rust/src/ui/component/label.rs index f79a9e5f7..f7cefc7ae 100644 --- a/core/embed/rust/src/ui/component/label.rs +++ b/core/embed/rust/src/ui/component/label.rs @@ -73,6 +73,21 @@ where .fit_text(self.text.as_ref()) .height() } + + pub fn text_area(&self) -> Rect { + // XXX only works on single-line labels + assert!(self.layout.bounds.height() <= self.font().text_max_height()); + let available_width = self.layout.bounds.width(); + let width = self.font().text_width(self.text.as_ref()); + let height = self.font().text_height(); + let cursor = self.layout.initial_cursor(); + let baseline = match self.alignment() { + Alignment::Start => cursor, + Alignment::Center => cursor + Offset::x(available_width / 2) - Offset::x(width / 2), + Alignment::End => cursor + Offset::x(available_width) - Offset::x(width), + }; + Rect::from_bottom_left_and_size(baseline, Offset::new(width, height)) + } } impl Component for Label diff --git a/core/embed/rust/src/ui/component/marquee.rs b/core/embed/rust/src/ui/component/marquee.rs index 0b4292373..774881094 100644 --- a/core/embed/rust/src/ui/component/marquee.rs +++ b/core/embed/rust/src/ui/component/marquee.rs @@ -11,6 +11,8 @@ use crate::{ }; const MILLIS_PER_LETTER_M: u32 = 300; +const ANIMATION_DURATION_MS: u32 = 2000; +const PAUSE_DURATION_MS: u32 = 1000; enum State { Initial, @@ -49,11 +51,15 @@ where font, fg, bg, - duration: Duration::from_millis(2000), - pause: Duration::from_millis(1000), + duration: Duration::from_millis(ANIMATION_DURATION_MS), + pause: Duration::from_millis(PAUSE_DURATION_MS), } } + pub fn set_text(&mut self, text: T) { + self.text = text; + } + pub fn start(&mut self, ctx: &mut EventCtx, now: Instant) { // Not starting if animations are disabled. if animation_disabled() { diff --git a/core/embed/rust/src/ui/component/mod.rs b/core/embed/rust/src/ui/component/mod.rs index c8559edcf..6f85fe167 100644 --- a/core/embed/rust/src/ui/component/mod.rs +++ b/core/embed/rust/src/ui/component/mod.rs @@ -32,4 +32,4 @@ pub use text::{ formatted::FormattedText, layout::{LineBreaking, PageBreaking, TextLayout}, }; -pub use timeout::{Timeout, TimeoutMsg}; +pub use timeout::Timeout; diff --git a/core/embed/rust/src/ui/component/paginated.rs b/core/embed/rust/src/ui/component/paginated.rs index db0a516e0..3bdbe2dab 100644 --- a/core/embed/rust/src/ui/component/paginated.rs +++ b/core/embed/rust/src/ui/component/paginated.rs @@ -1,8 +1,3 @@ -use crate::ui::component::{ - text::layout::{LayoutFit, TextNoOp}, - FormattedText, -}; - pub enum AuxPageMsg { /// Page component was instantiated with BACK button on every page and it /// was pressed. @@ -34,61 +29,3 @@ pub trait Paginate { /// Navigate to the given page. fn change_page(&mut self, active_page: usize); } - -impl Paginate for FormattedText -where - F: AsRef, - T: AsRef, -{ - fn page_count(&mut self) -> usize { - let mut page_count = 1; // There's always at least one page. - let mut char_offset = 0; - - loop { - let fit = self.layout_content(&mut TextNoOp); - match fit { - LayoutFit::Fitting { .. } => { - break; // TODO: We should consider if there's more content - // to render. - } - LayoutFit::OutOfBounds { - processed_chars, .. - } => { - page_count += 1; - char_offset += processed_chars; - self.set_char_offset(char_offset); - } - } - } - - // Reset the char offset back to the beginning. - self.set_char_offset(0); - - page_count - } - - fn change_page(&mut self, to_page: usize) { - let mut active_page = 0; - let mut char_offset = 0; - - // Make sure we're starting from the beginning. - self.set_char_offset(char_offset); - - while active_page < to_page { - let fit = self.layout_content(&mut TextNoOp); - match fit { - LayoutFit::Fitting { .. } => { - break; // TODO: We should consider if there's more content - // to render. - } - LayoutFit::OutOfBounds { - processed_chars, .. - } => { - active_page += 1; - char_offset += processed_chars; - self.set_char_offset(char_offset); - } - } - } - } -} diff --git a/core/embed/rust/src/ui/component/text/formatted.rs b/core/embed/rust/src/ui/component/text/formatted.rs index 911bc0976..96183df59 100644 --- a/core/embed/rust/src/ui/component/text/formatted.rs +++ b/core/embed/rust/src/ui/component/text/formatted.rs @@ -1,133 +1,103 @@ -use core::{ - iter::{Enumerate, Peekable}, - slice, +use crate::{ + strutil::StringType, + ui::{ + component::{Component, Event, EventCtx, Never, Paginate}, + geometry::Rect, + }, }; -use heapless::LinearMap; - -use crate::ui::{ - component::{Component, Event, EventCtx, Never}, - display::{Color, Font}, - geometry::Rect, -}; - -use super::layout::{ - LayoutFit, LayoutSink, LineBreaking, Op, PageBreaking, TextLayout, TextRenderer, TextStyle, +use super::{ + layout::{LayoutFit, LayoutSink, TextNoOp, TextRenderer}, + op::OpTextLayout, }; -pub const MAX_ARGUMENTS: usize = 6; - -pub struct FormattedText { - layout: TextLayout, - fonts: FormattedFonts, - format: F, - args: LinearMap<&'static str, T, MAX_ARGUMENTS>, +#[derive(Clone)] +pub struct FormattedText { + op_layout: OpTextLayout, char_offset: usize, } -pub struct FormattedFonts { - /// Font used to format `{normal}`. - pub normal: Font, - /// Font used to format `{demibold}`. - pub demibold: Font, - /// Font used to format `{bold}`. - pub bold: Font, - /// Font used to format `{mono}`. - pub mono: Font, -} - -impl FormattedText { - pub fn new(style: TextStyle, fonts: FormattedFonts, format: F) -> Self { +impl FormattedText { + pub fn new(op_layout: OpTextLayout) -> Self { Self { - format, - fonts, - layout: TextLayout::new(style), - args: LinearMap::new(), + op_layout, char_offset: 0, } } - pub fn with(mut self, key: &'static str, value: T) -> Self { - if self.args.insert(key, value).is_err() { - #[cfg(feature = "ui_debug")] - panic!("text args map is full"); - } - self - } - - pub fn with_format(mut self, format: F) -> Self { - self.format = format; - self + fn layout_content(&mut self, sink: &mut dyn LayoutSink) -> LayoutFit { + self.op_layout.layout_content(self.char_offset, sink) } +} - pub fn with_text_font(mut self, text_font: Font) -> Self { - self.layout.style.text_font = text_font; - self - } +// Pagination +impl Paginate for FormattedText { + fn page_count(&mut self) -> usize { + let mut page_count = 1; // There's always at least one page. + let mut char_offset = 0; - pub fn with_text_color(mut self, text_color: Color) -> Self { - self.layout.style.text_color = text_color; - self - } + // Make sure we're starting from the beginning. + self.char_offset = char_offset; - pub fn with_line_breaking(mut self, line_breaking: LineBreaking) -> Self { - self.layout.style.line_breaking = line_breaking; - self - } + // Looping through the content and counting pages + // until we finally fit. + loop { + let fit = self.layout_content(&mut TextNoOp); + match fit { + LayoutFit::Fitting { .. } => { + break; // TODO: We should consider if there's more content + // to render. + } + LayoutFit::OutOfBounds { + processed_chars, .. + } => { + page_count += 1; + char_offset += processed_chars; + self.char_offset = char_offset; + } + } + } - pub fn with_page_breaking(mut self, page_breaking: PageBreaking) -> Self { - self.layout.style.page_breaking = page_breaking; - self - } + // Reset the char offset back to the beginning. + self.char_offset = 0; - pub fn set_char_offset(&mut self, char_offset: usize) { - self.char_offset = char_offset; + page_count } - pub fn char_offset(&mut self) -> usize { - self.char_offset - } + fn change_page(&mut self, to_page: usize) { + let mut active_page = 0; + let mut char_offset = 0; - pub fn layout_mut(&mut self) -> &mut TextLayout { - &mut self.layout - } -} + // Make sure we're starting from the beginning. + self.char_offset = char_offset; -impl FormattedText -where - F: AsRef, - T: AsRef, -{ - pub fn layout_content(&self, sink: &mut dyn LayoutSink) -> LayoutFit { - let mut cursor = self.layout.initial_cursor(); - let mut ops = Op::skip_n_text_bytes( - Tokenizer::new(self.format.as_ref()).flat_map(|arg| match arg { - Token::Literal(literal) => Some(Op::Text(literal)), - Token::Argument("mono") => Some(Op::Font(self.fonts.mono)), - Token::Argument("bold") => Some(Op::Font(self.fonts.bold)), - Token::Argument("normal") => Some(Op::Font(self.fonts.normal)), - Token::Argument("demibold") => Some(Op::Font(self.fonts.demibold)), - Token::Argument(argument) => self - .args - .get(argument) - .map(|value| Op::Text(value.as_ref())), - }), - self.char_offset, - ); - self.layout.layout_ops(&mut ops, &mut cursor, sink) + // Looping through the content until we arrive at + // the wanted page. + while active_page < to_page { + let fit = self.layout_content(&mut TextNoOp); + match fit { + LayoutFit::Fitting { .. } => { + break; // TODO: We should consider if there's more content + // to render. + } + LayoutFit::OutOfBounds { + processed_chars, .. + } => { + active_page += 1; + char_offset += processed_chars; + self.char_offset = char_offset; + } + } + } } } -impl Component for FormattedText -where - F: AsRef, - T: AsRef, -{ +impl Component for FormattedText { type Msg = Never; fn place(&mut self, bounds: Rect) -> Rect { - self.layout.bounds = bounds; - self.layout.bounds + self.op_layout.place(bounds); + bounds } fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { @@ -140,133 +110,37 @@ where #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { - sink(self.layout.bounds) + sink(self.op_layout.layout.bounds) } } +// DEBUG-ONLY SECTION BELOW + #[cfg(feature = "ui_debug")] -impl crate::trace::Trace for FormattedText -where - F: AsRef, - T: AsRef, -{ +impl FormattedText { + /// Is the same as layout_content, but does not use `&mut self` + /// to be compatible with `trace`. + /// Therefore it has to do the `clone` of `op_layout`. + pub fn layout_content_debug(&self, sink: &mut dyn LayoutSink) -> LayoutFit { + // TODO: how to solve it "properly", without the `clone`? + // (changing `trace` to `&mut self` had some other isses...) + self.op_layout + .clone() + .layout_content(self.char_offset, sink) + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for FormattedText { fn trace(&self, t: &mut dyn crate::trace::Tracer) { use crate::ui::component::text::layout::trace::TraceSink; use core::cell::Cell; let fit: Cell> = Cell::new(None); t.component("FormattedText"); t.in_list("text", &|l| { - let result = self.layout_content(&mut TraceSink(l)); + let result = self.layout_content_debug(&mut TraceSink(l)); fit.set(Some(result)); }); t.bool("fits", matches!(fit.get(), Some(LayoutFit::Fitting { .. }))); } } - -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum Token<'a> { - /// Process literal text content. - Literal(&'a str), - /// Process argument with specified descriptor. - Argument(&'a str), -} - -/// Processes a format string into an iterator of `Token`s. -/// -/// # Example -/// -/// ``` -/// let parser = Tokenizer::new("Nice to meet {you}, where you been?"); -/// assert!(matches!(parser.next(), Some(Token::Literal("Nice to meet ")))); -/// assert!(matches!(parser.next(), Some(Token::Argument("you")))); -/// assert!(matches!(parser.next(), Some(Token::Literal(", where you been?")))); -/// ``` -pub struct Tokenizer<'a> { - input: &'a str, - inner: Peekable>>, -} - -impl<'a> Tokenizer<'a> { - /// Create a new tokenizer for bytes of a formatting string `input`, - /// returning an iterator. - pub fn new(input: &'a str) -> Self { - assert!(input.is_ascii()); - Self { - input, - inner: input.as_bytes().iter().enumerate().peekable(), - } - } -} - -impl<'a> Iterator for Tokenizer<'a> { - type Item = Token<'a>; - - fn next(&mut self) -> Option { - const ASCII_OPEN_BRACE: u8 = b'{'; - const ASCII_CLOSED_BRACE: u8 = b'}'; - - match self.inner.next() { - // Argument token is starting. Read until we find '}', then parse the content between - // the braces and return the token. If we encounter the end of string before the closing - // brace, quit. - Some((open, &ASCII_OPEN_BRACE)) => loop { - match self.inner.next() { - Some((close, &ASCII_CLOSED_BRACE)) => { - break Some(Token::Argument(&self.input[open + 1..close])); - } - None => { - break None; - } - _ => {} - } - }, - // Literal token is starting. Read until we find '{' or the end of string, and return - // the token. Use `peek()` for matching the opening brace, se we can keep it - // in the iterator for the above code. - Some((start, _)) => loop { - match self.inner.peek() { - Some(&(open, &ASCII_OPEN_BRACE)) => { - break Some(Token::Literal(&self.input[start..open])); - } - None => { - break Some(Token::Literal(&self.input[start..])); - } - _ => { - self.inner.next(); - } - } - }, - None => None, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn tokenizer_yields_expected_tokens() { - assert!(Tokenizer::new("").eq([])); - assert!(Tokenizer::new("x").eq([Token::Literal("x")])); - assert!(Tokenizer::new("x\0y").eq([Token::Literal("x\0y")])); - assert!(Tokenizer::new("{").eq([])); - assert!(Tokenizer::new("x{").eq([Token::Literal("x")])); - assert!(Tokenizer::new("x{y").eq([Token::Literal("x")])); - assert!(Tokenizer::new("{}").eq([Token::Argument("")])); - assert!(Tokenizer::new("x{}y{").eq([ - Token::Literal("x"), - Token::Argument(""), - Token::Literal("y"), - ])); - assert!(Tokenizer::new("{\0}").eq([Token::Argument("\0"),])); - assert!(Tokenizer::new("{{y}").eq([Token::Argument("{y"),])); - assert!(Tokenizer::new("{{{{xyz").eq([])); - assert!(Tokenizer::new("x{}{{}}}}").eq([ - Token::Literal("x"), - Token::Argument(""), - Token::Argument("{"), - Token::Literal("}}}"), - ])); - } -} diff --git a/core/embed/rust/src/ui/component/text/layout.rs b/core/embed/rust/src/ui/component/text/layout.rs index 2f19a936d..87d535ce4 100644 --- a/core/embed/rust/src/ui/component/text/layout.rs +++ b/core/embed/rust/src/ui/component/text/layout.rs @@ -175,51 +175,8 @@ impl TextLayout { } /// Draw as much text as possible on the current screen. - pub fn render_text(&self, text: &str) { - self.layout_text(text, &mut self.initial_cursor(), &mut TextRenderer); - } - - pub fn layout_ops( - mut self, - ops: &mut dyn Iterator>, - cursor: &mut Point, - sink: &mut dyn LayoutSink, - ) -> LayoutFit { - let init_cursor = *cursor; - let mut total_processed_chars = 0; - - for op in ops { - match op { - Op::Color(color) => { - self.style.text_color = color; - } - Op::Font(font) => { - self.style.text_font = font; - } - Op::Text(text) => match self.layout_text(text, cursor, sink) { - LayoutFit::Fitting { - processed_chars, .. - } => { - total_processed_chars += processed_chars; - } - LayoutFit::OutOfBounds { - processed_chars, .. - } => { - total_processed_chars += processed_chars; - - return LayoutFit::OutOfBounds { - processed_chars: total_processed_chars, - height: self.layout_height(init_cursor, *cursor), - }; - } - }, - } - } - - LayoutFit::Fitting { - processed_chars: total_processed_chars, - height: self.layout_height(init_cursor, *cursor), - } + pub fn render_text(&self, text: &str) -> LayoutFit { + self.layout_text(text, &mut self.initial_cursor(), &mut TextRenderer) } /// Loop through the `text` and try to fit it on the current screen, @@ -343,7 +300,7 @@ impl TextLayout { } /// Overall height of the content, including paddings. - fn layout_height(&self, init_cursor: Point, end_cursor: Point) -> i16 { + pub fn layout_height(&self, init_cursor: Point, end_cursor: Point) -> i16 { self.padding_top + self.style.text_font.text_height() + (end_cursor.y - init_cursor.y) @@ -507,38 +464,6 @@ pub mod trace { } } -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum Op<'a> { - /// Render text with current color and font. - Text(&'a str), - /// Set current text color. - Color(Color), - /// Set currently used font. - Font(Font), -} - -impl<'a> Op<'a> { - pub fn skip_n_text_bytes( - ops: impl Iterator>, - skip_bytes: usize, - ) -> impl Iterator> { - let mut skipped = 0; - - ops.filter_map(move |op| match op { - Op::Text(text) if skipped < skip_bytes => { - skipped = skipped.saturating_add(text.len()); - if skipped > skip_bytes { - let leave_bytes = skipped - skip_bytes; - Some(Op::Text(&text[text.len() - leave_bytes..])) - } else { - None - } - } - op_to_pass_through => Some(op_to_pass_through), - }) - } -} - /// Carries info about the content that was processed /// on the current line. #[derive(Debug, PartialEq, Eq)] @@ -557,7 +482,7 @@ struct Span { } impl Span { - fn fit_horizontally( + pub fn fit_horizontally( text: &str, max_width: i16, text_font: impl GlyphMetrics, diff --git a/core/embed/rust/src/ui/component/text/mod.rs b/core/embed/rust/src/ui/component/text/mod.rs index 6cec1a680..c879e38c0 100644 --- a/core/embed/rust/src/ui/component/text/mod.rs +++ b/core/embed/rust/src/ui/component/text/mod.rs @@ -1,6 +1,8 @@ pub mod common; pub mod formatted; pub mod layout; +pub mod op; pub mod paragraphs; +pub mod util; pub use layout::{LineBreaking, PageBreaking, TextStyle}; diff --git a/core/embed/rust/src/ui/component/text/op.rs b/core/embed/rust/src/ui/component/text/op.rs new file mode 100644 index 000000000..aef23220d --- /dev/null +++ b/core/embed/rust/src/ui/component/text/op.rs @@ -0,0 +1,240 @@ +use crate::{ + strutil::StringType, + ui::{ + display::{Color, Font}, + geometry::{Alignment, Offset, Rect}, + util::ResultExt, + }, +}; + +use super::{ + layout::{LayoutFit, LayoutSink, TextLayout}, + LineBreaking, TextStyle, +}; + +use heapless::Vec; + +// So that there is only one implementation, and not multiple generic ones +// as would be via `const N: usize` generics. +const MAX_OPS: usize = 15; + +/// To account for operations that are not made of characters +/// but need to be accounted for somehow. +/// Number of processed characters will be increased by this +/// to account for the operation. +const PROCESSED_CHARS_ONE: usize = 1; + +#[derive(Clone)] +/// Extension of TextLayout, allowing for Op-based operations +pub struct OpTextLayout { + pub layout: TextLayout, + ops: Vec, MAX_OPS>, +} + +impl<'a, T: StringType + Clone + 'a> OpTextLayout { + pub fn new(style: TextStyle) -> Self { + Self { + layout: TextLayout::new(style), + ops: Vec::new(), + } + } + + pub fn place(&mut self, bounds: Rect) -> Rect { + self.layout.bounds = bounds; + bounds + } + + /// Send the layout's content into a sink. + pub fn layout_content(&mut self, skip_bytes: usize, sink: &mut dyn LayoutSink) -> LayoutFit { + self.layout_ops(skip_bytes, sink) + } + + /// Perform some operations defined on `Op` for a list of those `Op`s + /// - e.g. changing the color, changing the font or rendering the text. + fn layout_ops(&mut self, skip_bytes: usize, sink: &mut dyn LayoutSink) -> LayoutFit { + // TODO: make sure it is called when we have the current font (not sooner) + let mut cursor = &mut self.layout.initial_cursor(); + let init_cursor = *cursor; + let mut total_processed_chars = 0; + + // Do something when it was not skipped + for op in Self::filter_skipped_ops(self.ops.iter(), skip_bytes) { + match op { + // Changing color + Op::Color(color) => { + self.layout.style.text_color = color; + } + // Changing font + Op::Font(font) => { + self.layout.style.text_font = font; + } + // Changing line/text alignment + Op::Alignment(line_alignment) => { + self.layout.align = line_alignment; + } + // Changing line breaking + Op::LineBreaking(line_breaking) => { + self.layout.style.line_breaking = line_breaking; + } + // Moving the cursor + Op::CursorOffset(offset) => { + cursor.x += offset.x; + cursor.y += offset.y; + } + // Moving to the next page + Op::NextPage => { + // Pretending that nothing more fits on current page to force + // continuing on the next one + total_processed_chars += PROCESSED_CHARS_ONE; + return LayoutFit::OutOfBounds { + processed_chars: total_processed_chars, + height: self.layout.layout_height(init_cursor, *cursor), + }; + } + // Drawing text + Op::Text(text) => { + // Try to fit text on the current page and if they do not fit, + // return the appropriate OutOfBounds message + + let fit = self.layout.layout_text(text.as_ref(), cursor, sink); + + match fit { + LayoutFit::Fitting { + processed_chars, .. + } => { + total_processed_chars += processed_chars; + } + LayoutFit::OutOfBounds { + processed_chars, .. + } => { + total_processed_chars += processed_chars; + + return LayoutFit::OutOfBounds { + processed_chars: total_processed_chars, + height: self.layout.layout_height(init_cursor, *cursor), + }; + } + } + } + } + } + + LayoutFit::Fitting { + processed_chars: total_processed_chars, + height: self.layout.layout_height(init_cursor, *cursor), + } + } + + /// Gets rid of all action-Ops that are before the `skip_bytes` threshold. + /// (Not removing the style changes, e.g. Font or Color, because they need + /// to be correctly set for future Text operations.) + fn filter_skipped_ops<'b, I>(ops_iter: I, skip_bytes: usize) -> impl Iterator> + 'b + where + I: Iterator> + 'b, + 'a: 'b, + { + let mut skipped = 0; + ops_iter.filter_map(move |op| { + match op { + Op::Text(text) if skipped < skip_bytes => { + let skip_text_bytes_if_fits_partially = skip_bytes - skipped; + skipped = skipped.saturating_add(text.as_ref().len()); + if skipped > skip_bytes { + // Fits partially + // Skipping some bytes at the beginning, leaving rest + Some(Op::Text( + text.skip_prefix(skip_text_bytes_if_fits_partially), + )) + } else { + // Does not fit at all + None + } + } + Op::NextPage if skipped < skip_bytes => { + skipped = skipped.saturating_add(PROCESSED_CHARS_ONE); + None + } + Op::CursorOffset(_) if skipped < skip_bytes => { + // Skip any offsets + None + } + op_to_pass_through => Some(op_to_pass_through.clone()), + } + }) + } +} + +// Op-adding operations +impl OpTextLayout { + pub fn with_new_item(mut self, item: Op) -> Self { + self.ops + .push(item) + .assert_if_debugging_ui("Could not push to self.ops - increase MAX_OPS."); + self + } + + pub fn text(self, text: T) -> Self { + self.with_new_item(Op::Text(text)) + } + + pub fn newline(self) -> Self { + self.with_new_item(Op::Text("\n".into())) + } + + pub fn newline_half(self) -> Self { + self.with_new_item(Op::Text("\r".into())) + } + + pub fn next_page(self) -> Self { + self.with_new_item(Op::NextPage) + } + + pub fn font(self, font: Font) -> Self { + self.with_new_item(Op::Font(font)) + } + + pub fn offset(self, offset: Offset) -> Self { + self.with_new_item(Op::CursorOffset(offset)) + } + + pub fn alignment(self, alignment: Alignment) -> Self { + self.with_new_item(Op::Alignment(alignment)) + } + + pub fn line_breaking(self, line_breaking: LineBreaking) -> Self { + self.with_new_item(Op::LineBreaking(line_breaking)) + } +} + +// Op-adding aggregation operations +impl OpTextLayout { + pub fn text_normal(self, text: T) -> Self { + self.font(Font::NORMAL).text(text) + } + + pub fn text_mono(self, text: T) -> Self { + self.font(Font::MONO).text(text) + } + + pub fn text_bold(self, text: T) -> Self { + self.font(Font::BOLD).text(text) + } +} + +#[derive(Clone)] +pub enum Op { + /// Render text with current color and font. + Text(T), + /// Set current text color. + Color(Color), + /// Set currently used font. + Font(Font), + /// Set currently used line alignment. + Alignment(Alignment), + /// Set currently used line breaking algorithm. + LineBreaking(LineBreaking), + /// Move the current cursor by specified Offset. + CursorOffset(Offset), + /// Force continuing on the next page. + NextPage, +} diff --git a/core/embed/rust/src/ui/component/text/paragraphs.rs b/core/embed/rust/src/ui/component/text/paragraphs.rs index 52671232a..ee0479a94 100644 --- a/core/embed/rust/src/ui/component/text/paragraphs.rs +++ b/core/embed/rust/src/ui/component/text/paragraphs.rs @@ -1,9 +1,12 @@ use heapless::Vec; -use crate::ui::{ - component::{Component, Event, EventCtx, Never, Paginate}, - display::toif::Icon, - geometry::{Alignment, Insets, LinearPlacement, Offset, Point, Rect, TOP_LEFT}, +use crate::{ + strutil::StringType, + ui::{ + component::{Component, Event, EventCtx, Never, Paginate}, + display::toif::Icon, + geometry::{Alignment, Insets, LinearPlacement, Offset, Point, Rect, TOP_LEFT}, + }, }; use super::layout::{LayoutFit, TextLayout, TextStyle}; @@ -24,28 +27,9 @@ pub const PARAGRAPH_BOTTOM_SPACE: i16 = 5; pub type ParagraphVecLong = Vec, 32>; pub type ParagraphVecShort = Vec, 8>; -/// Trait for internal representation of strings, which need to support -/// converting to short-lived &str reference as well as creating a new string by -/// skipping some number of bytes. Exists so that we can support `StrBuffer` as -/// well as `&'static str`. -/// -/// NOTE: do not implement this trait for `&'static str` in firmware. We always -/// use StrBuffer because using multiple internal representations results in -/// multiple copies of the code in flash memory. -pub trait ParagraphStrType: AsRef { - fn skip_prefix(&self, bytes: usize) -> Self; -} - -#[cfg(feature = "bootloader")] -impl ParagraphStrType for &str { - fn skip_prefix(&self, chars: usize) -> Self { - &self[chars..] - } -} - pub trait ParagraphSource { /// Determines the output type produced. - type StrType: ParagraphStrType; + type StrType: StringType; /// Return text and associated style for given paragraph index and character /// offset within the paragraph. @@ -115,7 +99,7 @@ where /// Helper for `change_offset` which should not get monomorphized as it /// doesn't refer to T or Self. - fn dyn_change_offset( + fn dyn_change_offset( mut area: Rect, mut offset: PageOffset, source: &dyn ParagraphSource, @@ -149,7 +133,7 @@ where /// Iterate over visible layouts (bounding box, style) together /// with corresponding string content. Should not get monomorphized. - fn foreach_visible<'a, S: ParagraphStrType>( + fn foreach_visible<'a, S: StringType>( source: &'a dyn ParagraphSource, visible: &'a [TextLayout], offset: PageOffset, @@ -366,7 +350,7 @@ impl PageOffset { /// /// If the returned remaining area is not None then it holds that /// `next_offset.par == self.par + 1`. - fn advance( + fn advance( mut self, area: Rect, source: &dyn ParagraphSource, @@ -432,7 +416,7 @@ impl PageOffset { ) } - fn should_place_pair_on_next_page( + fn should_place_pair_on_next_page( this_paragraph: &Paragraph, next_paragraph: &Paragraph, area: Rect, @@ -483,7 +467,7 @@ struct PageBreakIterator<'a, T> { } impl PageBreakIterator<'_, T> { - fn dyn_next( + fn dyn_next( mut area: Rect, paragraphs: &dyn ParagraphSource, mut offset: PageOffset, @@ -629,6 +613,17 @@ where } } +impl Paginate for Checklist +where + T: ParagraphSource, +{ + fn page_count(&mut self) -> usize { + 1 + } + + fn change_page(&mut self, _to_page: usize) {} +} + #[cfg(feature = "ui_debug")] impl crate::trace::Trace for Checklist { fn trace(&self, t: &mut dyn crate::trace::Tracer) { @@ -658,7 +653,7 @@ where } } -impl ParagraphSource for Vec, N> { +impl ParagraphSource for Vec, N> { type StrType = T; fn at(&self, index: usize, offset: usize) -> Paragraph { @@ -671,7 +666,7 @@ impl ParagraphSource for Vec, } } -impl ParagraphSource for [Paragraph; N] { +impl ParagraphSource for [Paragraph; N] { type StrType = T; fn at(&self, index: usize, offset: usize) -> Paragraph { @@ -684,7 +679,7 @@ impl ParagraphSource for [Paragraph; N] } } -impl ParagraphSource for Paragraph { +impl ParagraphSource for Paragraph { type StrType = T; fn at(&self, index: usize, offset: usize) -> Paragraph { diff --git a/core/embed/rust/src/ui/component/text/util.rs b/core/embed/rust/src/ui/component/text/util.rs new file mode 100644 index 000000000..d875d5ea4 --- /dev/null +++ b/core/embed/rust/src/ui/component/text/util.rs @@ -0,0 +1,35 @@ +use crate::ui::{ + display::{Color, Font}, + geometry::{Alignment, Rect}, +}; + +use super::{ + layout::{LayoutFit, TextLayout}, + TextStyle, +}; + +/// Draws longer multiline texts inside an area. +/// Splits lines on word boundaries/whitespace. +/// When a word is too long to fit one line, splitting +/// it on multiple lines with "-" at the line-ends. +/// +/// If it fits, returns the rest of the area. +/// If it does not fit, returns `None`. +pub fn text_multiline_split_words( + area: Rect, + text: &str, + font: Font, + fg_color: Color, + bg_color: Color, + alignment: Alignment, +) -> Option { + let text_style = TextStyle::new(font, fg_color, bg_color, fg_color, fg_color); + let text_layout = TextLayout::new(text_style) + .with_bounds(area) + .with_align(alignment); + let layout_fit = text_layout.render_text(text); + match layout_fit { + LayoutFit::Fitting { height, .. } => Some(area.split_top(height).1), + LayoutFit::OutOfBounds { .. } => None, + } +} diff --git a/core/embed/rust/src/ui/component/timeout.rs b/core/embed/rust/src/ui/component/timeout.rs index 155295694..67d29c750 100644 --- a/core/embed/rust/src/ui/component/timeout.rs +++ b/core/embed/rust/src/ui/component/timeout.rs @@ -11,10 +11,6 @@ pub struct Timeout { timer: Option, } -pub enum TimeoutMsg { - TimedOut, -} - impl Timeout { pub fn new(time_ms: u32) -> Self { Self { @@ -25,7 +21,7 @@ impl Timeout { } impl Component for Timeout { - type Msg = TimeoutMsg; + type Msg = (); fn place(&mut self, _bounds: Rect) -> Rect { Rect::zero() @@ -41,7 +37,7 @@ impl Component for Timeout { // Fire. Event::Timer(token) if Some(token) == self.timer => { self.timer = None; - Some(TimeoutMsg::TimedOut) + Some(()) } _ => None, } diff --git a/core/embed/rust/src/ui/display/font.rs b/core/embed/rust/src/ui/display/font.rs index a4b1983a6..8b929a002 100644 --- a/core/embed/rust/src/ui/display/font.rs +++ b/core/embed/rust/src/ui/display/font.rs @@ -9,11 +9,19 @@ use core::slice; use super::{get_color_table, get_offset, pixeldata, set_window, Color}; +/// Representation of a single glyph. +/// We use standard typographic terms. For a nice explanation, see, e.g., +/// the FreeType docs at https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html pub struct Glyph { + /// Total width of the glyph itself pub width: i16, + /// Total height of the glyph itself pub height: i16, + /// Advance - how much to move the cursor after drawing this glyph pub adv: i16, + /// Left-side horizontal bearing pub bearing_x: i16, + /// Top-side vertical bearing pub bearing_y: i16, data: &'static [u8], } @@ -52,6 +60,12 @@ impl Glyph { } } + /// Space between the right edge of the glyph and the left edge of the next + /// bounding box. + pub const fn right_side_bearing(&self) -> i16 { + self.adv - self.width - self.bearing_x + } + pub fn print(&self, pos: Point, colortable: [Color; 16]) -> i16 { let bearing = Offset::new(self.bearing_x, -self.bearing_y); let size = Offset::new(self.width, self.height); @@ -129,6 +143,36 @@ impl Font { display::text_width(text, self.into()) } + /// Width of the text that is visible. + /// Not including the spaces before the first and after the last character. + pub fn visible_text_width(self, text: &str) -> i16 { + if text.is_empty() { + // No text, no width. + return 0; + } + + let first_char = unwrap!(text.chars().next()); + let first_char_glyph = unwrap!(self.get_glyph(first_char as u8)); + + let last_char = unwrap!(text.chars().last()); + let last_char_glyph = unwrap!(self.get_glyph(last_char as u8)); + + // Strip leftmost and rightmost spaces/bearings/margins. + self.text_width(text) - first_char_glyph.bearing_x - last_char_glyph.right_side_bearing() + } + + /// Returning the x-bearing (offset) of the first character. + /// Useful to enforce that the text is positioned correctly (e.g. centered). + pub fn start_x_bearing(self, text: &str) -> i16 { + if text.is_empty() { + return 0; + } + + let first_char = unwrap!(text.chars().next()); + let first_char_glyph = unwrap!(self.get_glyph(first_char as u8)); + first_char_glyph.bearing_x + } + pub fn char_width(self, ch: char) -> i16 { display::char_width(ch, self.into()) } diff --git a/core/embed/rust/src/ui/display/mod.rs b/core/embed/rust/src/ui/display/mod.rs index 1de71c38c..efcbd2c1e 100644 --- a/core/embed/rust/src/ui/display/mod.rs +++ b/core/embed/rust/src/ui/display/mod.rs @@ -126,16 +126,16 @@ pub fn rect_fill_corners(r: Rect, fg_color: Color) { } #[derive(Copy, Clone, PartialEq, Eq)] -pub struct TextOverlay<'a> { +pub struct TextOverlay { area: Rect, - text: &'a str, + text: T, font: Font, max_height: i16, baseline: i16, } -impl<'a> TextOverlay<'a> { - pub fn new(text: &'a str, font: Font) -> Self { +impl> TextOverlay { + pub fn new(text: T, font: Font) -> Self { let area = Rect::zero(); Self { @@ -147,8 +147,17 @@ impl<'a> TextOverlay<'a> { } } + pub fn set_text(&mut self, text: T) { + self.text = text; + } + + pub fn get_text(&self) -> &T { + &self.text + } + + // baseline relative to the underlying render area pub fn place(&mut self, baseline: Point) { - let text_width = self.font.text_width(self.text); + let text_width = self.font.text_width(self.text.as_ref()); let text_height = self.font.text_height(); let text_area_start = baseline + Offset::new(-(text_width / 2), -text_height); @@ -167,7 +176,12 @@ impl<'a> TextOverlay<'a> { let p_rel = Point::new(p.x - self.area.x0, p.y - self.area.y0); - for g in self.text.bytes().filter_map(|c| self.font.get_glyph(c)) { + for g in self + .text + .as_ref() + .bytes() + .filter_map(|c| self.font.get_glyph(c)) + { let top = self.max_height - self.baseline - g.bearing_y; let char_area = Rect::new( Point::new(tot_adv + g.bearing_x, top), @@ -755,9 +769,9 @@ fn rect_rounded2_get_pixel( /// Optionally draws a text inside the rectangle and adjusts its color to match /// the fill. The coordinates of the text are specified in the TextOverlay /// struct. -pub fn bar_with_text_and_fill( +pub fn bar_with_text_and_fill>( area: Rect, - overlay: Option, + overlay: Option<&TextOverlay>, fg_color: Color, bg_color: Color, fill_from: i16, @@ -855,7 +869,7 @@ pub fn text_center(baseline: Point, text: &str, font: Font, fg_color: Color, bg_ ); } -/// Display text right-alligned to a certain Point +/// Display text right-aligned to a certain Point pub fn text_right(baseline: Point, text: &str, font: Font, fg_color: Color, bg_color: Color) { let w = font.text_width(text); display::text( @@ -869,7 +883,6 @@ pub fn text_right(baseline: Point, text: &str, font: Font, fg_color: Color, bg_c } pub fn text_top_left(position: Point, text: &str, font: Font, fg_color: Color, bg_color: Color) { - // let w = font.text_width(text); let h = font.text_height(); display::text( position.x, diff --git a/core/embed/rust/src/ui/display/toif.rs b/core/embed/rust/src/ui/display/toif.rs index b4da30a2c..c831319e3 100644 --- a/core/embed/rust/src/ui/display/toif.rs +++ b/core/embed/rust/src/ui/display/toif.rs @@ -25,8 +25,12 @@ use super::Color; const TOIF_HEADER_LENGTH: usize = 12; -pub fn icon(icon: &Icon, center: Point, fg_color: Color, bg_color: Color) { - let r = Rect::from_center_and_size(center, icon.toif.size()); +pub fn render_icon(icon: &Icon, center: Point, fg_color: Color, bg_color: Color) { + render_toif(&icon.toif, center, fg_color, bg_color); +} + +pub fn render_toif(toif: &Toif, center: Point, fg_color: Color, bg_color: Color) { + let r = Rect::from_center_and_size(center, toif.size()); let area = r.translate(get_offset()); let clamped = area.clamp(constant::screen()); let colortable = get_color_table(fg_color, bg_color); @@ -36,7 +40,7 @@ pub fn icon(icon: &Icon, center: Point, fg_color: Color, bg_color: Color) { let mut dest = [0_u8; 1]; let mut window = [0; UZLIB_WINDOW_SIZE]; - let mut ctx = icon.toif.decompression_context(Some(&mut window)); + let mut ctx = toif.decompression_context(Some(&mut window)); for py in area.y0..area.y1 { for px in area.x0..area.x1 { @@ -177,6 +181,13 @@ impl<'i> Toif<'i> { } } + pub const fn is_grayscale(&self) -> bool { + matches!( + self.format(), + ToifFormat::GrayScaleOH | ToifFormat::GrayScaleEH + ) + } + pub const fn width(&self) -> i16 { u16::from_le_bytes([self.data[4], self.data[5]]) as i16 } @@ -204,11 +215,20 @@ impl<'i> Toif<'i> { ) -> UzlibContext { UzlibContext::new(self.zdata(), window) } + + /// Display the data with baseline Point, aligned according to the + /// `alignment` argument. + pub fn draw(&self, baseline: Point, alignment: Alignment2D, fg_color: Color, bg_color: Color) { + let r = Rect::snap(baseline, self.size(), alignment); + render_toif(self, r.center(), fg_color, bg_color); + } } #[derive(PartialEq, Eq, Clone, Copy)] pub struct Icon { pub toif: Toif<'static>, + #[cfg(feature = "ui_debug")] + pub name: &'static str, } impl Icon { @@ -218,13 +238,27 @@ impl Icon { None => panic!("Invalid image."), }; assert!(matches!(toif.format(), ToifFormat::GrayScaleEH)); - Self { toif } + Self { + toif, + #[cfg(feature = "ui_debug")] + name: "", + } + } + + /// Create a named icon. + /// The name is only stored in debug builds. + pub const fn debug_named(data: &'static [u8], name: &'static str) -> Self { + Self { + #[cfg(feature = "ui_debug")] + name, + ..Self::new(data) + } } /// Display the icon with baseline Point, aligned according to the /// `alignment` argument. pub fn draw(&self, baseline: Point, alignment: Alignment2D, fg_color: Color, bg_color: Color) { let r = Rect::snap(baseline, self.toif.size(), alignment); - icon(self, r.center(), fg_color, bg_color); + render_icon(self, r.center(), fg_color, bg_color); } } diff --git a/core/embed/rust/src/ui/event.rs b/core/embed/rust/src/ui/event.rs index 3c273f996..fd1f55554 100644 --- a/core/embed/rust/src/ui/event.rs +++ b/core/embed/rust/src/ui/event.rs @@ -9,8 +9,14 @@ pub enum PhysicalButton { #[derive(Copy, Clone, PartialEq, Eq)] pub enum ButtonEvent { + /// Button pressed down. + /// â–¼ * | * â–¼ ButtonPressed(PhysicalButton), + /// Button released up. + /// â–² * | * â–² ButtonReleased(PhysicalButton), + HoldStarted, + HoldEnded, } impl ButtonEvent { diff --git a/core/embed/rust/src/ui/geometry.rs b/core/embed/rust/src/ui/geometry.rs index e873c4369..9a84ea8cd 100644 --- a/core/embed/rust/src/ui/geometry.rs +++ b/core/embed/rust/src/ui/geometry.rs @@ -241,6 +241,21 @@ impl Rect { } } + pub const fn from_top_right_and_size(p0: Point, size: Offset) -> Self { + let top_left = Point::new(p0.x - size.x, p0.y); + Self::from_top_left_and_size(top_left, size) + } + + pub const fn from_bottom_left_and_size(p0: Point, size: Offset) -> Self { + let top_left = Point::new(p0.x, p0.y - size.y); + Self::from_top_left_and_size(top_left, size) + } + + pub const fn from_bottom_right_and_size(p0: Point, size: Offset) -> Self { + let top_left = Point::new(p0.x - size.x, p0.y - size.y); + Self::from_top_left_and_size(top_left, size) + } + pub const fn from_center_and_size(p: Point, size: Offset) -> Self { let x0 = p.x - size.x / 2; let y0 = p.y - size.y / 2; @@ -306,6 +321,14 @@ impl Rect { self.bottom_left().center(self.bottom_right()) } + pub const fn left_center(&self) -> Point { + self.bottom_left().center(self.top_left()) + } + + pub const fn right_center(&self) -> Point { + self.bottom_right().center(self.top_right()) + } + /// Whether a `Point` is inside the `Rect`. pub const fn contains(&self, point: Point) -> bool { point.x >= self.x0 && point.x < self.x1 && point.y >= self.y0 && point.y < self.y1 @@ -341,6 +364,11 @@ impl Rect { } } + /// Move all the sides further from the center by the same distance. + pub const fn expand(&self, size: i16) -> Self { + self.outset(Insets::uniform(size)) + } + /// Move all the sides closer to the center by the same distance. pub const fn shrink(&self, size: i16) -> Self { self.inset(Insets::uniform(size)) @@ -386,6 +414,17 @@ impl Rect { self.split_left(self.width() - width) } + /// Split `Rect` into left, center and right, given the center one's + /// `width`. Center element is symmetric, left and right have the same + /// size. In case left and right cannot be the same size, right is 1px + /// wider. + pub const fn split_center(self, width: i16) -> (Self, Self, Self) { + let left_right_width = (self.width() - width) / 2; + let (left, center_right) = self.split_left(left_right_width); + let (center, right) = center_right.split_left(width); + (left, center, right) + } + pub const fn clamp(self, limit: Rect) -> Self { Self { x0: max(self.x0, limit.x0), diff --git a/core/embed/rust/src/ui/layout/util.rs b/core/embed/rust/src/ui/layout/util.rs index 71c68582e..800cf44bb 100644 --- a/core/embed/rust/src/ui/layout/util.rs +++ b/core/embed/rust/src/ui/layout/util.rs @@ -8,9 +8,11 @@ use crate::{ obj::Obj, util::try_or_raise, }, + storage::{get_avatar_len, load_avatar}, + strutil::SkipPrefix, ui::{ component::text::{ - paragraphs::{Paragraph, ParagraphSource, ParagraphStrType}, + paragraphs::{Paragraph, ParagraphSource}, TextStyle, }, util::set_animation_disabled, @@ -20,37 +22,32 @@ use cstr_core::cstr; use heapless::Vec; #[cfg(feature = "jpeg")] -use crate::{ - micropython::{ - buffer::get_buffer, - ffi::{mp_obj_new_int, mp_obj_new_tuple}, - }, - ui::display::tjpgd::{jpeg_info, jpeg_test}, -}; +use crate::ui::display::tjpgd::{jpeg_info, jpeg_test}; +use crate::{micropython::buffer::get_buffer, ui::display::toif::Toif}; -pub fn iter_into_objs(iterable: Obj) -> Result<[Obj; N], Error> { - let err = Error::ValueError(cstr!("Invalid iterable length")); - let mut vec = Vec::::new(); - let mut iter_buf = IterBuf::new(); - for item in Iter::try_from_obj_with_buf(iterable, &mut iter_buf)? { - vec.push(item).map_err(|_| err)?; - } +pub fn iter_into_array(iterable: Obj) -> Result<[T; N], Error> +where + T: TryFrom, + Error: From, +{ + let vec: Vec = iter_into_vec(iterable)?; // Returns error if array.len() != N - vec.into_array().map_err(|_| err) + vec.into_array() + .map_err(|_| value_error!("Invalid iterable length")) } -pub fn iter_into_array(iterable: Obj) -> Result<[T; N], Error> +pub fn iter_into_vec(iterable: Obj) -> Result, Error> where - T: TryFrom, + T: TryFrom, + Error: From, { - let err = Error::ValueError(cstr!("Invalid iterable length")); let mut vec = Vec::::new(); let mut iter_buf = IterBuf::new(); for item in Iter::try_from_obj_with_buf(iterable, &mut iter_buf)? { - vec.push(item.try_into()?).map_err(|_| err)?; + vec.push(item.try_into()?) + .map_err(|_| value_error!("Invalid iterable length"))?; } - // Returns error if array.len() != N - vec.into_array().map_err(|_| err) + Ok(vec) } /// Maximum number of characters that can be displayed on screen at once. Used @@ -143,7 +140,7 @@ impl ParagraphSource for PropsList { fn at(&self, index: usize, offset: usize) -> Paragraph { let block = move || { let entry = self.items.get(index / 2)?; - let [key, value, value_is_mono]: [Obj; 3] = iter_into_objs(entry)?; + let [key, value, value_is_mono]: [Obj; 3] = iter_into_array(entry)?; let value_is_mono: bool = bool::try_from(value_is_mono)?; let obj: Obj; let style: &TextStyle; @@ -197,12 +194,6 @@ impl ParagraphSource for PropsList { } } -impl ParagraphStrType for StrBuffer { - fn skip_prefix(&self, chars: usize) -> Self { - self.offset(chars) - } -} - pub extern "C" fn upy_disable_animation(disable: Obj) -> Obj { let block = || { set_animation_disabled(disable.try_into()?); @@ -214,27 +205,32 @@ pub extern "C" fn upy_disable_animation(disable: Obj) -> Obj { #[cfg(feature = "jpeg")] pub extern "C" fn upy_jpeg_info(data: Obj) -> Obj { let block = || { - let buffer = unsafe { get_buffer(data) }; - - if let Ok(buffer) = buffer { - let info = jpeg_info(buffer); - - if let Some(info) = info { - let obj = unsafe { - let values = [ - mp_obj_new_int(info.0.x as _), - mp_obj_new_int(info.0.y as _), - mp_obj_new_int(info.1 as _), - ]; - mp_obj_new_tuple(3, values.as_ptr()) - }; - - Ok(obj) - } else { - Err(Error::ValueError(cstr!("Invalid image format."))) - } + let buffer = unsafe { get_buffer(data) }?; + + if let Some(info) = jpeg_info(buffer) { + let w = info.0.x as u16; + let h = info.0.y as u16; + let mcu_h = info.1 as u16; + (w.into(), h.into(), mcu_h.into()).try_into() } else { - Err(Error::ValueError(cstr!("Buffer error."))) + Err(value_error!("Invalid image format.")) + } + }; + + unsafe { try_or_raise(block) } +} + +pub extern "C" fn upy_toif_info(data: Obj) -> Obj { + let block = || { + let buffer = unsafe { get_buffer(data) }?; + + if let Some(toif) = Toif::new(buffer) { + let w = toif.width() as u16; + let h = toif.height() as u16; + let is_grayscale = toif.is_grayscale(); + (w.into(), h.into(), is_grayscale.into()).try_into() + } else { + Err(value_error!("Invalid image format.")) } }; @@ -244,11 +240,18 @@ pub extern "C" fn upy_jpeg_info(data: Obj) -> Obj { #[cfg(feature = "jpeg")] pub extern "C" fn upy_jpeg_test(data: Obj) -> Obj { let block = || { - let buffer = - unsafe { get_buffer(data) }.map_err(|_| Error::ValueError(cstr!("Buffer error.")))?; + let buffer = unsafe { get_buffer(data) }?; let result = jpeg_test(buffer); Ok(result.into()) }; unsafe { try_or_raise(block) } } + +pub fn get_user_custom_image() -> Result, Error> { + let len = get_avatar_len()?; + let mut data = Gc::<[u8]>::new_slice(len)?; + // SAFETY: buffer is freshly allocated so nobody else has it. + load_avatar(unsafe { Gc::<[u8]>::as_mut(&mut data) })?; + Ok(data) +} diff --git a/core/embed/rust/src/ui/macros.rs b/core/embed/rust/src/ui/macros.rs index c0e239e9e..50398399a 100644 --- a/core/embed/rust/src/ui/macros.rs +++ b/core/embed/rust/src/ui/macros.rs @@ -5,7 +5,6 @@ macro_rules! include_res { }; } -#[allow(unused_macros)] // Only used in TR so far. /// Concatenates arbitrary amount of slices into a String. macro_rules! build_string { ($max:expr, $($string:expr),+) => { @@ -17,7 +16,6 @@ macro_rules! build_string { } } -#[cfg(feature = "ui_debug")] /// Transforms integer into string slice. For example for printing. macro_rules! inttostr { ($int:expr) => {{ diff --git a/core/embed/rust/src/ui/mod.rs b/core/embed/rust/src/ui/mod.rs index c44b24dd5..0b57e0306 100644 --- a/core/embed/rust/src/ui/mod.rs +++ b/core/embed/rust/src/ui/mod.rs @@ -9,6 +9,7 @@ pub mod event; pub mod geometry; pub mod lerp; pub mod screens; +#[macro_use] pub mod util; #[cfg(feature = "micropython")] diff --git a/core/embed/rust/src/ui/model_tr/bootloader/confirm.rs b/core/embed/rust/src/ui/model_tr/bootloader/confirm.rs index af5189e55..5aee31f56 100644 --- a/core/embed/rust/src/ui/model_tr/bootloader/confirm.rs +++ b/core/embed/rust/src/ui/model_tr/bootloader/confirm.rs @@ -6,15 +6,17 @@ use crate::ui::{ constant::screen, display::{Color, Icon}, geometry::{Point, Rect, CENTER}, - model_tr::{ - component::{Button, ButtonMsg::Clicked}, +}; + +use super::{ + super::{ + component::Button, constant::{HEIGHT, WIDTH}, theme::WHITE, }, + ReturnToC, }; -use super::ReturnToC; - #[derive(Copy, Clone)] pub enum ConfirmMsg { Cancel = 1, @@ -76,21 +78,22 @@ impl Component for Confirm { bounds } - fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - if let Some(Clicked) = self.left.event(ctx, event) { - return if self.confirm_left { - Some(Self::Msg::Confirm) - } else { - Some(Self::Msg::Cancel) - }; - }; - if let Some(Clicked) = self.right.event(ctx, event) { - return if self.confirm_left { - Some(Self::Msg::Cancel) - } else { - Some(Self::Msg::Confirm) - }; - }; + fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { + // TODO: to be fixed in bootloader branch + // if let Some(Clicked) = self.left.event(ctx, event) { + // return if self.confirm_left { + // Some(Self::Msg::Confirm) + // } else { + // Some(Self::Msg::Cancel) + // }; + // }; + // if let Some(Clicked) = self.right.event(ctx, event) { + // return if self.confirm_left { + // Some(Self::Msg::Cancel) + // } else { + // Some(Self::Msg::Confirm) + // }; + // }; None } @@ -111,6 +114,7 @@ impl Component for Confirm { self.right.paint(); } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.left.bounds(sink); self.right.bounds(sink); diff --git a/core/embed/rust/src/ui/model_tr/bootloader/intro.rs b/core/embed/rust/src/ui/model_tr/bootloader/intro.rs index 0e23ebdd7..4ed36bc18 100644 --- a/core/embed/rust/src/ui/model_tr/bootloader/intro.rs +++ b/core/embed/rust/src/ui/model_tr/bootloader/intro.rs @@ -4,18 +4,14 @@ use crate::ui::{ Child, Component, Event, EventCtx, Pad, }, geometry::{LinearPlacement, Point, Rect}, - model_tr::{ - bootloader::{ - theme::{BLD_BG, TEXT_NORMAL}, - title::Title, - ReturnToC, - }, - component::ButtonMsg::Clicked, - }, }; -use crate::ui::model_tr::{ - bootloader::theme::bld_button_default, +use super::super::{ + bootloader::{ + theme::{bld_button_default, BLD_BG, TEXT_NORMAL}, + title::Title, + ReturnToC, + }, component::{Button, ButtonPos}, constant::{HEIGHT, WIDTH}, }; @@ -89,13 +85,14 @@ impl Component for Intro { bounds } - fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - if let Some(Clicked) = self.menu.event(ctx, event) { - return Some(Self::Msg::Menu); - }; - if let Some(Clicked) = self.host.event(ctx, event) { - return Some(Self::Msg::Host); - }; + fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { + // TODO: to be fixed in bootloader branch + // if let Some(Clicked) = self.menu.event(ctx, event) { + // return Some(Self::Msg::Menu); + // }; + // if let Some(Clicked) = self.host.event(ctx, event) { + // return Some(Self::Msg::Host); + // }; None } @@ -107,6 +104,7 @@ impl Component for Intro { self.menu.paint(); } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.title.bounds(sink); self.text.bounds(sink); diff --git a/core/embed/rust/src/ui/model_tr/bootloader/menu.rs b/core/embed/rust/src/ui/model_tr/bootloader/menu.rs index f210c5724..15677d0db 100644 --- a/core/embed/rust/src/ui/model_tr/bootloader/menu.rs +++ b/core/embed/rust/src/ui/model_tr/bootloader/menu.rs @@ -1,24 +1,12 @@ use crate::ui::{ - component::{Child, Component, Event, EventCtx, Pad}, + component::{Child, Component, Event, EventCtx, Never, Pad}, geometry::{Point, Rect}, - model_tr::{ - bootloader::{theme::BLD_BG, title::Title, ReturnToC}, - constant::{HEIGHT, WIDTH}, - }, }; -#[repr(u32)] -#[derive(Copy, Clone)] -pub enum MenuMsg { - Close = 1, - Reboot = 2, - FactoryReset = 3, -} -impl ReturnToC for MenuMsg { - fn return_to_c(self) -> u32 { - self as u32 - } -} +use super::super::{ + bootloader::{theme::BLD_BG, title::Title}, + constant::{HEIGHT, WIDTH}, +}; pub struct Menu { bg: Pad, @@ -37,7 +25,7 @@ impl Menu { } impl Component for Menu { - type Msg = MenuMsg; + type Msg = Never; fn place(&mut self, bounds: Rect) -> Rect { self.bg diff --git a/core/embed/rust/src/ui/model_tr/bootloader/mod.rs b/core/embed/rust/src/ui/model_tr/bootloader/mod.rs index bd650810d..cc30e51d2 100644 --- a/core/embed/rust/src/ui/model_tr/bootloader/mod.rs +++ b/core/embed/rust/src/ui/model_tr/bootloader/mod.rs @@ -4,7 +4,6 @@ use crate::{ component::{Component, Never}, display::{self, Font}, geometry::Point, - model_tr::constant, }, }; @@ -20,22 +19,23 @@ use crate::ui::{ Event, EventCtx, }, constant::{screen, WIDTH}, - display::{fade_backlight_duration, Color, Icon, TextOverlay}, + display::{Color, Icon, TextOverlay}, event::ButtonEvent, geometry::{LinearPlacement, Offset, Rect, CENTER}, - model_tr::{ - bootloader::{ - confirm::Confirm, - intro::Intro, - menu::Menu, - theme::{bld_button_cancel, bld_button_default, BLD_BG, BLD_FG}, - }, - component::{Button, ButtonPos, ResultScreen}, - theme::{BACKLIGHT_NORMAL, ICON_FAIL, ICON_SUCCESS, LOGO_EMPTY}, - }, util::{from_c_array, from_c_str}, }; +use super::{ + component::{Button, ButtonPos, ResultScreen}, + constant, + theme::{ICON_FAIL, ICON_SUCCESS, LOGO_EMPTY}, +}; + +use confirm::Confirm; +use intro::Intro; +use menu::Menu; +use theme::{bld_button_cancel, bld_button_default, BLD_BG, BLD_FG}; + const SCREEN_ADJ: Rect = screen().split_top(64).0; pub trait ReturnToC { @@ -78,7 +78,6 @@ where { frame.place(SCREEN_ADJ); frame.paint(); - fade_backlight_duration(BACKLIGHT_NORMAL as _, 500); while button_eval().is_some() {} @@ -139,6 +138,7 @@ extern "C" fn screen_install_confirm( message.add(Paragraph::new(&theme::TEXT_BOLD, "Seed will be erased!").centered()); } + // TODO: this relies on StrBuffer support for bootloader, decide what to do let left = Button::with_text(ButtonPos::Left, "CANCEL", bld_button_cancel()); let right = Button::with_text(ButtonPos::Right, "INSTALL", bld_button_default()); @@ -172,6 +172,7 @@ extern "C" fn screen_wipe_confirm() -> u32 { let message = Paragraphs::new(messages).with_placement(LinearPlacement::vertical().align_at_center()); + // TODO: this relies on StrBuffer support for bootloader, decide what to do let left = Button::with_text(ButtonPos::Left, "WIPE", bld_button_default()); let right = Button::with_text(ButtonPos::Right, "CANCEL", bld_button_cancel()); @@ -220,12 +221,17 @@ fn screen_progress( let fill_to = (loader_area.width() as u32 * progress as u32) / 1000; - display::bar_with_text_and_fill(loader_area, Some(text), fg_color, bg_color, 0, fill_to as _); + display::bar_with_text_and_fill( + loader_area, + Some(&text), + fg_color, + bg_color, + 0, + fill_to as _, + ); display::refresh(); } -const INITIAL_INSTALL_LOADER_COLOR: Color = Color::rgb(0x4A, 0x90, 0xE2); - #[no_mangle] extern "C" fn screen_install_progress(progress: u16, initialize: bool, _initial_setup: bool) { screen_progress( @@ -278,14 +284,7 @@ extern "C" fn screen_wipe_success() { let m_bottom = Paragraphs::new(messages).with_placement(LinearPlacement::vertical().align_at_center()); - let mut frame = ResultScreen::new( - BLD_FG, - BLD_BG, - Icon::new(ICON_SUCCESS), - m_top, - m_bottom, - true, - ); + let mut frame = ResultScreen::new(BLD_FG, BLD_BG, ICON_SUCCESS, m_top, m_bottom, true); show(&mut frame); } @@ -305,7 +304,7 @@ extern "C" fn screen_wipe_fail() { let m_bottom = Paragraphs::new(messages).with_placement(LinearPlacement::vertical().align_at_center()); - let mut frame = ResultScreen::new(BLD_FG, BLD_BG, Icon::new(ICON_FAIL), m_top, m_bottom, true); + let mut frame = ResultScreen::new(BLD_FG, BLD_BG, ICON_FAIL, m_top, m_bottom, true); show(&mut frame); } @@ -329,7 +328,7 @@ extern "C" fn screen_install_fail() { let m_bottom = Paragraphs::new(messages).with_placement(LinearPlacement::vertical().align_at_center()); - let mut frame = ResultScreen::new(BLD_FG, BLD_BG, Icon::new(ICON_FAIL), m_top, m_bottom, true); + let mut frame = ResultScreen::new(BLD_FG, BLD_BG, ICON_FAIL, m_top, m_bottom, true); show(&mut frame); } @@ -347,14 +346,7 @@ fn screen_install_success_bld(msg: &'static str, complete_draw: bool) { let m_bottom = Paragraphs::new(messages).with_placement(LinearPlacement::vertical().align_at_center()); - let mut frame = ResultScreen::new( - BLD_FG, - BLD_BG, - Icon::new(ICON_SUCCESS), - m_top, - m_bottom, - complete_draw, - ); + let mut frame = ResultScreen::new(BLD_FG, BLD_BG, ICON_SUCCESS, m_top, m_bottom, complete_draw); show(&mut frame); } @@ -372,14 +364,7 @@ fn screen_install_success_initial(msg: &'static str, complete_draw: bool) { let m_bottom = Paragraphs::new(messages).with_placement(LinearPlacement::vertical().align_at_center()); - let mut frame = ResultScreen::new( - BLD_FG, - BLD_BG, - Icon::new(ICON_SUCCESS), - m_top, - m_bottom, - complete_draw, - ); + let mut frame = ResultScreen::new(BLD_FG, BLD_BG, ICON_SUCCESS, m_top, m_bottom, complete_draw); show(&mut frame); } diff --git a/core/embed/rust/src/ui/model_tr/bootloader/theme.rs b/core/embed/rust/src/ui/model_tr/bootloader/theme.rs index 33335ec83..d40449354 100644 --- a/core/embed/rust/src/ui/model_tr/bootloader/theme.rs +++ b/core/embed/rust/src/ui/model_tr/bootloader/theme.rs @@ -1,49 +1,23 @@ use crate::ui::{ component::text::TextStyle, display::{Color, Font}, - model_tr::{ - component::{ButtonStyle, ButtonStyleSheet}, - theme::{BG, BLACK, FG, WHITE}, - }, + geometry::Offset, +}; + +use super::super::{ + component::ButtonStyleSheet, + theme::{BG, BLACK, FG, WHITE}, }; pub const BLD_BG: Color = BLACK; pub const BLD_FG: Color = WHITE; -// Commonly used corner radius (i.e. for buttons). -pub const RADIUS: u8 = 2; - -// Size of icons in the UI (i.e. inside buttons). -pub const ICON_SIZE: i32 = 16; - pub fn bld_button_default() -> ButtonStyleSheet { - ButtonStyleSheet { - normal: &ButtonStyle { - font: Font::NORMAL, - text_color: BG, - border_horiz: true, - }, - active: &ButtonStyle { - font: Font::NORMAL, - text_color: FG, - border_horiz: true, - }, - } + ButtonStyleSheet::new(BG, FG, false, false, None, Offset::zero()) } pub fn bld_button_cancel() -> ButtonStyleSheet { - ButtonStyleSheet { - normal: &ButtonStyle { - font: Font::NORMAL, - text_color: FG, - border_horiz: false, - }, - active: &ButtonStyle { - font: Font::NORMAL, - text_color: BG, - border_horiz: false, - }, - } + ButtonStyleSheet::new(FG, BG, false, false, None, Offset::zero()) } pub const TEXT_NORMAL: TextStyle = TextStyle::new(Font::NORMAL, BLD_FG, BLD_BG, BLD_FG, BLD_FG); diff --git a/core/embed/rust/src/ui/model_tr/bootloader/title.rs b/core/embed/rust/src/ui/model_tr/bootloader/title.rs index 64f5f0d56..fe01e7605 100644 --- a/core/embed/rust/src/ui/model_tr/bootloader/title.rs +++ b/core/embed/rust/src/ui/model_tr/bootloader/title.rs @@ -2,9 +2,10 @@ use crate::ui::{ component::{Component, Event, EventCtx, Never}, display::{self, Font}, geometry::{Point, Rect}, - model_tr::bootloader::theme::{BLD_BG, BLD_FG}, }; +use super::theme::{BLD_BG, BLD_FG}; + pub struct Title { version: &'static str, area: Rect, @@ -48,5 +49,6 @@ impl Component for Title { ); } + #[cfg(feature = "ui_bounds")] fn bounds(&self, _sink: &mut dyn FnMut(Rect)) {} } diff --git a/core/embed/rust/src/ui/model_tr/component/address_details.rs b/core/embed/rust/src/ui/model_tr/component/address_details.rs new file mode 100644 index 000000000..65ef446f0 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/address_details.rs @@ -0,0 +1,280 @@ +use heapless::Vec; + +use crate::{ + error::Error, + strutil::StringType, + ui::{ + component::{ + text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs, VecExt}, + Child, Component, Event, EventCtx, Pad, Paginate, Qr, + }, + geometry::Rect, + }, +}; + +use super::{ + theme, ButtonController, ButtonControllerMsg, ButtonDetails, ButtonLayout, ButtonPos, Frame, +}; + +const MAX_XPUBS: usize = 16; +const QR_BORDER: i16 = 3; + +pub struct AddressDetails +where + T: StringType, +{ + qr_code: Qr, + details_view: Paragraphs>, + xpub_view: Frame>, T>, + xpubs: Vec<(T, T), MAX_XPUBS>, + current_page: usize, + current_subpage: usize, + area: Rect, + pad: Pad, + buttons: Child>, +} + +impl AddressDetails +where + T: StringType + Clone, +{ + pub fn new( + qr_address: T, + case_sensitive: bool, + account: Option, + path: Option, + ) -> Result { + let qr_code = Qr::new(qr_address, case_sensitive)?.with_border(QR_BORDER); + let details_view = { + let mut para = ParagraphVecShort::new(); + if let Some(account) = account { + para.add(Paragraph::new(&theme::TEXT_BOLD, "Account:".into())); + para.add(Paragraph::new(&theme::TEXT_MONO, account)); + } + if let Some(path) = path { + para.add(Paragraph::new(&theme::TEXT_BOLD, "Derivation path:".into())); + para.add(Paragraph::new(&theme::TEXT_MONO, path)); + } + Paragraphs::new(para) + }; + let xpub_view = Frame::new( + "".into(), + Paragraph::new(&theme::TEXT_MONO_DATA, "".into()).into_paragraphs(), + ); + + let result = Self { + qr_code, + details_view, + xpub_view, + xpubs: Vec::new(), + area: Rect::zero(), + current_page: 0, + current_subpage: 0, + pad: Pad::with_background(theme::BG).with_clear(), + buttons: Child::new(ButtonController::new(ButtonLayout::arrow_none_arrow())), + }; + Ok(result) + } + + pub fn add_xpub(&mut self, title: T, xpub: T) -> Result<(), Error> { + self.xpubs + .push((title, xpub)) + .map_err(|_| Error::OutOfRange) + } + + fn is_in_subpage(&self) -> bool { + self.current_subpage > 0 + } + + fn is_xpub_page(&self) -> bool { + self.current_page > 1 + } + + fn is_last_page(&self) -> bool { + self.current_page == self.page_count() - 1 + } + + fn is_last_subpage(&mut self) -> bool { + self.current_subpage == self.subpages_in_current_page() - 1 + } + + fn subpages_in_current_page(&mut self) -> usize { + if self.is_xpub_page() { + self.xpub_view.page_count() + } else { + 1 + } + } + + /// Button layout for the current page. + /// Normally there are arrows everywhere, apart from the right side of the + /// last page. On xpub pages there is VIEW FULL middle button when it + /// cannot fit one page. On xpub subpages there are wide arrows to + /// scroll. + fn get_button_layout(&mut self) -> ButtonLayout { + let (left, middle, right) = if self.is_in_subpage() { + let left = Some(ButtonDetails::up_arrow_icon_wide()); + let right = if self.is_last_subpage() { + None + } else { + Some(ButtonDetails::down_arrow_icon_wide()) + }; + (left, None, right) + } else { + let left = Some(ButtonDetails::left_arrow_icon()); + let middle = if self.is_xpub_page() && self.subpages_in_current_page() > 1 { + Some(ButtonDetails::armed_text("VIEW FULL".into())) + } else { + None + }; + let right = if self.is_last_page() { + None + } else { + Some(ButtonDetails::right_arrow_icon()) + }; + (left, middle, right) + }; + ButtonLayout::new(left, middle, right) + } + + /// Reflecting the current page in the buttons. + fn update_buttons(&mut self, ctx: &mut EventCtx) { + let btn_layout = self.get_button_layout(); + self.buttons.mutate(ctx, |_ctx, buttons| { + buttons.set(btn_layout); + }); + } + + fn page_count(&self) -> usize { + 2 + self.xpubs.len() + } + + fn fill_xpub_page(&mut self, ctx: &mut EventCtx) { + let i = self.current_page - 2; + self.xpub_view.update_title(ctx, self.xpubs[i].0.clone()); + self.xpub_view.update_content(ctx, |p| { + p.inner_mut().update(self.xpubs[i].1.clone()); + p.change_page(0) + }); + } + + fn change_page(&mut self, ctx: &mut EventCtx) { + if self.is_xpub_page() { + self.fill_xpub_page(ctx); + } + self.pad.clear(); + self.current_subpage = 0; + } + + fn change_subpage(&mut self, ctx: &mut EventCtx) { + if self.is_xpub_page() { + self.xpub_view + .update_content(ctx, |p| p.change_page(self.current_subpage)); + self.pad.clear(); + } + } +} + +impl Component for AddressDetails +where + T: StringType + Clone, +{ + type Msg = (); + + fn place(&mut self, bounds: Rect) -> Rect { + // QR code is being placed on the whole bounds, so it can be as big as possible + // (it will not collide with the buttons, they are narrow and on the sides). + // Therefore, also placing pad on the whole bounds. + self.qr_code.place(bounds); + self.pad.place(bounds); + let (content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT); + self.details_view.place(content_area); + self.xpub_view.place(content_area); + self.buttons.place(button_area); + self.area = content_area; + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + // Possibly update the components that have e.g. marquee + match self.current_page { + 0 => self.qr_code.event(ctx, event), + 1 => self.details_view.event(ctx, event), + _ => self.xpub_view.event(ctx, event), + }; + + let button_event = self.buttons.event(ctx, event); + if let Some(ButtonControllerMsg::Triggered(button)) = button_event { + if self.is_in_subpage() { + match button { + ButtonPos::Left => { + // Going back + self.current_subpage -= 1; + } + ButtonPos::Right => { + // Going next + self.current_subpage += 1; + } + _ => unreachable!(), + } + self.change_subpage(ctx); + self.update_buttons(ctx); + return None; + } else { + match button { + ButtonPos::Left => { + // Cancelling or going back + if self.current_page == 0 { + return Some(()); + } + self.current_page -= 1; + self.change_page(ctx); + } + ButtonPos::Right => { + // Going to the next page + self.current_page += 1; + self.change_page(ctx); + } + ButtonPos::Middle => { + // Going into subpage + self.current_subpage = 1; + self.change_subpage(ctx); + } + } + self.update_buttons(ctx); + return None; + } + } + None + } + + fn paint(&mut self) { + self.pad.paint(); + self.buttons.paint(); + match self.current_page { + 0 => self.qr_code.paint(), + 1 => self.details_view.paint(), + _ => self.xpub_view.paint(), + } + } + + #[cfg(feature = "ui_bounds")] + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + sink(self.area) + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for AddressDetails +where + T: StringType + Clone, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("AddressDetails"); + match self.current_page { + 0 => t.child("qr_code", &self.qr_code), + 1 => t.child("details_view", &self.details_view), + _ => t.child("xpub_view", &self.xpub_view), + } + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/button.rs b/core/embed/rust/src/ui/model_tr/component/button.rs index 3fbcdc794..6417bbd2f 100644 --- a/core/embed/rust/src/ui/model_tr/component/button.rs +++ b/core/embed/rust/src/ui/model_tr/component/button.rs @@ -1,57 +1,66 @@ -use crate::ui::{ - component::{Component, Event, EventCtx}, - display::{self, Color, Font}, - event::{ButtonEvent, PhysicalButton}, - geometry::{Offset, Point, Rect}, +use crate::{ + strutil::{ShortString, StringType}, + time::Duration, + ui::{ + component::{Component, Event, EventCtx, Never}, + constant, + display::{self, Color, Font, Icon}, + geometry::{ + Insets, Offset, Point, Rect, BOTTOM_LEFT, BOTTOM_RIGHT, CENTER, TOP_LEFT, TOP_RIGHT, + }, + }, }; -use super::theme; +use super::{loader::DEFAULT_DURATION_MS, theme}; -pub enum ButtonMsg { - Clicked, -} +const HALF_SCREEN_BUTTON_WIDTH: i16 = constant::WIDTH / 2 - 1; #[derive(Copy, Clone)] pub enum ButtonPos { Left, + Middle, Right, } -impl ButtonPos { - pub fn hit(&self, b: &PhysicalButton) -> bool { - matches!( - (self, b), - (Self::Left, PhysicalButton::Left) | (Self::Right, PhysicalButton::Right) - ) - } -} - -pub struct Button { - area: Rect, +pub struct Button +where + T: StringType, +{ + bounds: Rect, pos: ButtonPos, - baseline: Point, content: ButtonContent, styles: ButtonStyleSheet, state: State, } -impl> Button { +impl Button +where + T: StringType, +{ pub fn new(pos: ButtonPos, content: ButtonContent, styles: ButtonStyleSheet) -> Self { Self { pos, content, styles, - baseline: Point::zero(), - area: Rect::zero(), + bounds: Rect::zero(), state: State::Released, } } + pub fn from_button_details(pos: ButtonPos, btn_details: ButtonDetails) -> Self { + // Deciding between text and icon + let style = btn_details.style(); + match btn_details.content { + ButtonContent::Text(text) => Self::with_text(pos, text, style), + ButtonContent::Icon(icon) => Self::with_icon(pos, icon, style), + } + } + pub fn with_text(pos: ButtonPos, text: T, styles: ButtonStyleSheet) -> Self { Self::new(pos, ButtonContent::Text(text), styles) } - pub fn with_icon(pos: ButtonPos, image: &'static [u8], styles: ButtonStyleSheet) -> Self { + pub fn with_icon(pos: ButtonPos, image: Icon, styles: ButtonStyleSheet) -> Self { Self::new(pos, ButtonContent::Icon(image), styles) } @@ -61,11 +70,27 @@ impl> Button { fn style(&self) -> &ButtonStyle { match self.state { - State::Released => self.styles.normal, - State::Pressed => self.styles.active, + State::Released => &self.styles.normal, + State::Pressed => &self.styles.active, } } + /// Changing the icon content of the button. + pub fn set_icon(&mut self, image: Icon) { + self.content = ButtonContent::Icon(image); + } + + /// Changing the text content of the button. + pub fn set_text(&mut self, text: T) { + self.content = ButtonContent::Text(text); + } + + /// Changing the style of the button. + pub fn set_style(&mut self, styles: ButtonStyleSheet) { + self.styles = styles; + } + + // Setting the visual state of the button. fn set(&mut self, ctx: &mut EventCtx, state: State) { if self.state != state { self.state = state; @@ -73,117 +98,833 @@ impl> Button { } } - fn placement( - area: Rect, - pos: ButtonPos, - content: &ButtonContent, - styles: &ButtonStyleSheet, - ) -> (Rect, Point) { - let border_width = if styles.normal.border_horiz { 2 } else { 0 }; - let content_width = match content { - ButtonContent::Text(text) => styles.normal.font.text_width(text.as_ref()) - 1, - ButtonContent::Icon(_icon) => todo!(), + // Setting the visual state of the button. + pub fn set_pressed(&mut self, ctx: &mut EventCtx, is_pressed: bool) { + let new_state = if is_pressed { + State::Pressed + } else { + State::Released + }; + self.set(ctx, new_state); + } + + /// Return the full area of the button according + /// to its current style, content and position. + fn get_current_area(&self) -> Rect { + let style = self.style(); + + // Button width may be forced. Otherwise calculate it. + let button_width = if let Some(width) = style.fixed_width { + width + } else { + let outline = if style.with_outline { + theme::BUTTON_OUTLINE + } else { + 0 + }; + let content_width = match &self.content { + ButtonContent::Text(text) => style.font.visible_text_width(text.as_ref()), + ButtonContent::Icon(icon) => icon.toif.width() - 1, + }; + content_width + 2 * outline + }; + + // Button height may be adjusted for the icon without outline + // Done to avoid highlighting bigger area than necessary when + // drawing the icon in active (black on white) state + let button_height = match &self.content { + ButtonContent::Text(_) => theme::BUTTON_HEIGHT, + ButtonContent::Icon(icon) => { + if style.with_outline { + theme::BUTTON_HEIGHT + } else { + icon.toif.height() + } + } + }; + + let button_bounds = self.bounds.split_bottom(button_height).1; + let area = match self.pos { + ButtonPos::Left => button_bounds.split_left(button_width).0, + ButtonPos::Right => button_bounds.split_right(button_width).1, + ButtonPos::Middle => button_bounds.split_center(button_width).1, }; - let button_width = content_width + 2 * border_width; - let area = match pos { - ButtonPos::Left => area.split_left(button_width).0, - ButtonPos::Right => area.split_right(button_width).1, + + // Allowing for possible offset of the area from current style + area.translate(style.offset) + } + + /// Determine baseline point for the text. + fn get_text_baseline(&self, style: &ButtonStyle) -> Point { + // Arms and outline require the text to be elevated. + let offset_y = if style.with_arms { + theme::BUTTON_ARMS + } else if style.with_outline { + theme::BUTTON_OUTLINE + } else { + 0 }; - let start_of_baseline = area.bottom_left() + Offset::new(border_width, -2); + let offset_x = if style.with_outline { + theme::BUTTON_OUTLINE + } else { + 0 + }; - (area, start_of_baseline) + self.get_current_area().bottom_left() + Offset::new(offset_x, -offset_y) } } impl Component for Button where - T: AsRef, + T: StringType, { - type Msg = ButtonMsg; + type Msg = Never; fn place(&mut self, bounds: Rect) -> Rect { - let (area, baseline) = Self::placement(bounds, self.pos, &self.content, &self.styles); - self.area = area; - self.baseline = baseline; - self.area + self.bounds = bounds; + self.get_current_area() } - fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - match event { - Event::Button(ButtonEvent::ButtonPressed(which)) if self.pos.hit(&which) => { - self.set(ctx, State::Pressed); - } - Event::Button(ButtonEvent::ButtonReleased(which)) if self.pos.hit(&which) => { - if matches!(self.state, State::Pressed) { - self.set(ctx, State::Released); - return Some(ButtonMsg::Clicked); - } - } - _ => {} - }; + fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { + // Events are handled by `ButtonController` None } fn paint(&mut self) { let style = self.style(); + let text_color = style.text_color; + let background_color = text_color.negate(); + let mut area = self.get_current_area(); + + // Optionally display "arms" at both sides of content, or create + // a nice rounded outline around it. + // By default just fill the content background. + if style.with_arms { + const ARM_WIDTH: i16 = 15; + area = area.translate(Offset::y(1)); + + // Prepare space for both the arms and content with BG color. + // Arms are icons 10*6 pixels. + let area_to_fill = area.outset(Insets::sides(ARM_WIDTH)); + display::rect_fill(area_to_fill, background_color); + display::rect_fill_corners(area_to_fill, theme::BG); + + // Paint both arms. + // Baselines are adjusted to give space between text and icon. + // 2 px because 1px might lead to odd coordinate which can't be render + theme::ICON_ARM_LEFT.draw( + area.left_center() + Offset::x(-2), + TOP_RIGHT, + text_color, + background_color, + ); + theme::ICON_ARM_RIGHT.draw( + area.right_center() + Offset::x(2), + TOP_LEFT, + text_color, + background_color, + ); + } else if style.with_outline { + if background_color == theme::BG { + display::rect_outline_rounded(area, text_color, background_color, 2); + } else { + // With inverse colors having just radius of one, `rect_outline_rounded` + // is not suitable for inverse colors. + display::rect_fill(area, background_color); + display::rect_fill_corners(area, theme::BG); + } + } else { + display::rect_fill(area, background_color); + } match &self.content { ButtonContent::Text(text) => { - let background_color = style.text_color.negate(); - if style.border_horiz { - display::rect_fill_rounded(self.area, background_color, theme::BG, 1); - } else { - display::rect_fill(self.area, background_color) - } - display::text_left( - self.baseline, + self.get_text_baseline(style), text.as_ref(), style.font, - style.text_color, + text_color, background_color, ); } - ButtonContent::Icon(_image) => { - todo!(); + ButtonContent::Icon(icon) => { + if style.with_outline { + // Accounting for the 8*8 icon with empty left column and bottom row + // (which fits the outline nicely and symmetrically) + let center = area.center() + Offset::uniform(1); + icon.draw(center, CENTER, text_color, background_color); + } else { + // Positioning the icon in the corresponding corner/center + match self.pos { + ButtonPos::Left => icon.draw( + area.bottom_left(), + BOTTOM_LEFT, + text_color, + background_color, + ), + ButtonPos::Right => icon.draw( + area.bottom_right(), + BOTTOM_RIGHT, + text_color, + background_color, + ), + ButtonPos::Middle => { + icon.draw(area.center(), CENTER, text_color, background_color) + } + } + } } } } } -#[cfg(feature = "ui_debug")] -impl crate::trace::Trace for Button -where - T: AsRef, -{ - fn trace(&self, t: &mut dyn crate::trace::Tracer) { - t.component("Button"); - match &self.content { - ButtonContent::Text(text) => t.string("text", text.as_ref()), - ButtonContent::Icon(_) => t.bool("icon", true), - } - } -} - #[derive(PartialEq, Eq)] enum State { Released, Pressed, } +#[derive(Clone)] pub enum ButtonContent { Text(T), - Icon(&'static [u8]), + Icon(Icon), } pub struct ButtonStyleSheet { - pub normal: &'static ButtonStyle, - pub active: &'static ButtonStyle, + pub normal: ButtonStyle, + pub active: ButtonStyle, } pub struct ButtonStyle { pub font: Font, pub text_color: Color, - pub border_horiz: bool, + pub with_outline: bool, + pub with_arms: bool, + pub fixed_width: Option, + pub offset: Offset, +} + +impl ButtonStyleSheet { + pub fn new( + normal_color: Color, + active_color: Color, + with_outline: bool, + with_arms: bool, + fixed_width: Option, + offset: Offset, + ) -> Self { + Self { + normal: ButtonStyle { + font: theme::FONT_BUTTON, + text_color: normal_color, + with_outline, + with_arms, + fixed_width, + offset, + }, + active: ButtonStyle { + font: theme::FONT_BUTTON, + text_color: active_color, + with_outline, + with_arms, + fixed_width, + offset, + }, + } + } + + // White text in normal mode. + pub fn default( + with_outline: bool, + with_arms: bool, + fixed_width: Option, + offset: Offset, + ) -> Self { + Self::new( + theme::FG, + theme::BG, + with_outline, + with_arms, + fixed_width, + offset, + ) + } +} + +/// Describing the button on the screen - only visuals. +#[derive(Clone)] +pub struct ButtonDetails { + pub content: ButtonContent, + pub duration: Option, + with_outline: bool, + with_arms: bool, + fixed_width: Option, + offset: Offset, +} + +impl ButtonDetails { + /// Text button. + pub fn text(text: T) -> Self { + Self { + content: ButtonContent::Text(text), + duration: None, + with_outline: true, + with_arms: false, + fixed_width: None, + offset: Offset::zero(), + } + } + + /// Icon button. + pub fn icon(icon: Icon) -> Self { + Self { + content: ButtonContent::Icon(icon), + duration: None, + with_outline: true, + with_arms: false, + fixed_width: None, + offset: Offset::zero(), + } + } + + /// Text with arms signalling double press. + pub fn armed_text(text: T) -> Self { + Self::text(text).with_arms() + } + + /// Cross-style-icon cancel button with no outline. + pub fn cancel_icon() -> Self { + Self::icon(theme::ICON_CANCEL) + .with_no_outline() + .with_offset(Offset::new(2, -2)) + } + + /// Left arrow to signal going back. No outline. + pub fn left_arrow_icon() -> Self { + Self::icon(theme::ICON_ARROW_LEFT) + .with_no_outline() + .with_offset(Offset::new(1, -1)) + } + + /// Right arrow to signal going forward. No outline. + pub fn right_arrow_icon() -> Self { + Self::icon(theme::ICON_ARROW_RIGHT) + .with_no_outline() + .with_offset(Offset::new(-1, -1)) + } + + /// Up arrow to signal paginating back. No outline. Offsetted little right + /// to not be on the boundary. + pub fn up_arrow_icon() -> Self { + Self::icon(theme::ICON_ARROW_UP) + .with_no_outline() + .with_offset(Offset::new(2, -3)) + } + + /// Down arrow to signal paginating forward. Takes half the screen's width + pub fn down_arrow_icon_wide() -> Self { + Self::icon(theme::ICON_ARROW_DOWN).fixed_width(HALF_SCREEN_BUTTON_WIDTH) + } + + /// Up arrow to signal paginating back. Takes half the screen's width + pub fn up_arrow_icon_wide() -> Self { + Self::icon(theme::ICON_ARROW_UP).fixed_width(HALF_SCREEN_BUTTON_WIDTH) + } + + /// Icon of a bin to signal deleting. + pub fn bin_icon() -> Self { + Self::icon(theme::ICON_BIN).with_no_outline() + } + + /// No outline around the button. + pub fn with_no_outline(mut self) -> Self { + self.with_outline = false; + self + } + + /// Positioning the icon precisely where we want. + /// Buttons are by default placed exactly in the corners (left/right) + /// or in the center in case of center button. The offset can change it. + pub fn with_offset(mut self, offset: Offset) -> Self { + self.offset = offset; + self + } + + /// Left and right "arms" around the button. + /// Automatically disabling the outline. + pub fn with_arms(mut self) -> Self { + self.with_arms = true; + self.with_outline = false; + self + } + + /// Default duration of the hold-to-confirm - 1 second. + pub fn with_default_duration(mut self) -> Self { + self.duration = Some(Duration::from_millis(DEFAULT_DURATION_MS)); + self + } + + /// Specific duration of the hold-to-confirm. + pub fn with_duration(mut self, duration: Duration) -> Self { + self.duration = Some(duration); + self + } + + /// Width of the button. + pub fn fixed_width(mut self, width: i16) -> Self { + self.fixed_width = Some(width); + self + } + + /// Button style that should be applied. + pub fn style(&self) -> ButtonStyleSheet { + ButtonStyleSheet::default( + self.with_outline, + self.with_arms, + self.fixed_width, + self.offset, + ) + } +} + +/// Holding the button details for all three possible buttons. +#[derive(Clone)] +pub struct ButtonLayout { + pub btn_left: Option>, + pub btn_middle: Option>, + pub btn_right: Option>, +} + +impl ButtonLayout +where + T: StringType, +{ + pub fn new( + btn_left: Option>, + btn_middle: Option>, + btn_right: Option>, + ) -> Self { + Self { + btn_left, + btn_middle, + btn_right, + } + } + + /// Empty layout for when we cannot yet tell which buttons + /// should be on the screen. + pub fn empty() -> Self { + Self::new(None, None, None) + } + + /// Default button layout for all three buttons - icons. + pub fn default_three_icons() -> Self { + Self::arrow_armed_arrow("SELECT".into()) + } + + /// Special middle text for default icon layout. + pub fn arrow_armed_arrow(text: T) -> Self { + Self::new( + Some(ButtonDetails::left_arrow_icon()), + Some(ButtonDetails::armed_text(text)), + Some(ButtonDetails::right_arrow_icon()), + ) + } + + /// Left cancel, armed text and next right arrow. + pub fn cancel_armed_arrow(text: T) -> Self { + Self::new( + Some(ButtonDetails::cancel_icon()), + Some(ButtonDetails::armed_text(text)), + Some(ButtonDetails::right_arrow_icon()), + ) + } + + /// Middle armed text and next right arrow. + pub fn none_armed_arrow(text: T) -> Self { + Self::new( + None, + Some(ButtonDetails::armed_text(text)), + Some(ButtonDetails::right_arrow_icon()), + ) + } + + /// Left cancel, armed text and right text. + pub fn cancel_armed_text(middle: T, right: T) -> Self { + Self::new( + Some(ButtonDetails::cancel_icon()), + Some(ButtonDetails::armed_text(middle)), + Some(ButtonDetails::text(right)), + ) + } + + /// Left back arrow and middle armed text. + pub fn arrow_armed_none(text: T) -> Self { + Self::new( + Some(ButtonDetails::left_arrow_icon()), + Some(ButtonDetails::armed_text(text)), + None, + ) + } + + /// Left and right texts. + pub fn text_none_text(left: T, right: T) -> Self { + Self::new( + Some(ButtonDetails::text(left)), + None, + Some(ButtonDetails::text(right)), + ) + } + + /// Left text and right arrow. + pub fn text_none_arrow(text: T) -> Self { + Self::new( + Some(ButtonDetails::text(text)), + None, + Some(ButtonDetails::right_arrow_icon()), + ) + } + + /// Only right text. + pub fn none_none_text(text: T) -> Self { + Self::new(None, None, Some(ButtonDetails::text(text))) + } + + /// Left and right arrow icons for navigation. + pub fn arrow_none_arrow() -> Self { + Self::new( + Some(ButtonDetails::left_arrow_icon()), + None, + Some(ButtonDetails::right_arrow_icon()), + ) + } + + /// Left arrow and right text. + pub fn arrow_none_text(text: T) -> Self { + Self::new( + Some(ButtonDetails::left_arrow_icon()), + None, + Some(ButtonDetails::text(text)), + ) + } + + /// Up arrow left and right text. + pub fn up_arrow_none_text(text: T) -> Self { + Self::new( + Some(ButtonDetails::up_arrow_icon()), + None, + Some(ButtonDetails::text(text)), + ) + } + + /// Cancel cross on left and right arrow. + pub fn cancel_none_arrow() -> Self { + Self::new( + Some(ButtonDetails::cancel_icon()), + None, + Some(ButtonDetails::right_arrow_icon()), + ) + } + + /// Cancel cross on left and right arrow facing down. + pub fn cancel_none_arrow_wide() -> Self { + Self::new( + Some(ButtonDetails::cancel_icon()), + None, + Some(ButtonDetails::down_arrow_icon_wide()), + ) + } + + /// Cancel cross on left and right arrow facing down. + pub fn cancel_none_arrow_down() -> Self { + Self::new( + Some(ButtonDetails::cancel_icon()), + None, + Some(ButtonDetails::down_arrow_icon_wide()), + ) + } + + /// Cancel cross on left and text on the right. + pub fn cancel_none_text(text: T) -> Self { + Self::new( + Some(ButtonDetails::cancel_icon()), + None, + Some(ButtonDetails::text(text)), + ) + } + + /// Cancel cross on left and hold-to-confirm text on the right. + pub fn cancel_none_htc(text: T) -> Self { + Self::new( + Some(ButtonDetails::cancel_icon()), + None, + Some(ButtonDetails::text(text).with_default_duration()), + ) + } + + /// Arrow back on left and hold-to-confirm text on the right. + pub fn arrow_none_htc(text: T) -> Self { + Self::new( + Some(ButtonDetails::left_arrow_icon()), + None, + Some(ButtonDetails::text(text).with_default_duration()), + ) + } + + /// Only armed text in the middle. + pub fn none_armed_none(text: T) -> Self { + Self::new(None, Some(ButtonDetails::armed_text(text)), None) + } + + /// HTC on both sides. + pub fn htc_none_htc(left: T, right: T) -> Self { + Self::new( + Some(ButtonDetails::text(left).with_default_duration()), + None, + Some(ButtonDetails::text(right).with_default_duration()), + ) + } + + /// Only left arrow. + pub fn arrow_none_none() -> Self { + Self::new(Some(ButtonDetails::left_arrow_icon()), None, None) + } + + /// Only right arrow facing down. + pub fn none_none_arrow_wide() -> Self { + Self::new(None, None, Some(ButtonDetails::down_arrow_icon_wide())) + } +} + +/// What happens when a button is triggered. +/// Theoretically any action can be connected +/// with any button. +#[derive(Clone, PartialEq, Eq, Copy)] +pub enum ButtonAction { + /// Go to the next page of this flow + NextPage, + /// Go to the previous page of this flow + PrevPage, + /// Go to the first page of this flow + FirstPage, + /// Go to the last page of this flow + LastPage, + /// Cancel the whole layout - send Msg::Cancelled + Cancel, + /// Confirm the whole layout - send Msg::Confirmed + Confirm, + /// Send INFO message from layout - send Msg::Info + Info, +} + +/// Storing actions for all three possible buttons. +#[derive(Clone, Copy)] +pub struct ButtonActions { + pub left: Option, + pub middle: Option, + pub right: Option, +} + +impl ButtonActions { + pub const fn new( + left: Option, + middle: Option, + right: Option, + ) -> Self { + Self { + left, + middle, + right, + } + } + + /// Going back with left, going further with right + pub fn prev_none_next() -> Self { + Self::new( + Some(ButtonAction::PrevPage), + None, + Some(ButtonAction::NextPage), + ) + } + + /// Going back with left, going further with middle + pub fn prev_next_none() -> Self { + Self::new( + Some(ButtonAction::PrevPage), + Some(ButtonAction::NextPage), + None, + ) + } + + /// Previous with left, confirming with right + pub fn prev_none_confirm() -> Self { + Self::new( + Some(ButtonAction::PrevPage), + None, + Some(ButtonAction::Confirm), + ) + } + + /// Previous with left, confirming with middle + pub fn prev_confirm_none() -> Self { + Self::new( + Some(ButtonAction::PrevPage), + Some(ButtonAction::Confirm), + None, + ) + } + + /// Going to last page with left, to the next page with right + pub fn last_none_next() -> Self { + Self::new( + Some(ButtonAction::LastPage), + None, + Some(ButtonAction::NextPage), + ) + } + + /// Going to last page with left, to the next page with right and confirm + /// with middle + pub fn last_confirm_next() -> Self { + Self::new( + Some(ButtonAction::LastPage), + Some(ButtonAction::Confirm), + Some(ButtonAction::NextPage), + ) + } + + /// Going to previous page with left, to the next page with right and + /// confirm with middle + pub fn prev_confirm_next() -> Self { + Self::new( + Some(ButtonAction::PrevPage), + Some(ButtonAction::Confirm), + Some(ButtonAction::NextPage), + ) + } + + /// Cancelling with left, going to the next page with right + pub fn cancel_none_next() -> Self { + Self::new( + Some(ButtonAction::Cancel), + None, + Some(ButtonAction::NextPage), + ) + } + + /// Only going to the next page with right + pub fn none_none_next() -> Self { + Self::new(None, None, Some(ButtonAction::NextPage)) + } + + /// Only going to the prev page with left + pub fn prev_none_none() -> Self { + Self::new(Some(ButtonAction::PrevPage), None, None) + } + + /// Cancelling with left, confirming with right + pub fn cancel_none_confirm() -> Self { + Self::new( + Some(ButtonAction::Cancel), + None, + Some(ButtonAction::Confirm), + ) + } + + /// Cancelling with left, confirming with middle and next with right + pub fn cancel_confirm_next() -> Self { + Self::new( + Some(ButtonAction::Cancel), + Some(ButtonAction::Confirm), + Some(ButtonAction::NextPage), + ) + } + + /// Cancelling with left, confirming with middle and info with right + pub fn cancel_confirm_info() -> Self { + Self::new( + Some(ButtonAction::Cancel), + Some(ButtonAction::Confirm), + Some(ButtonAction::Info), + ) + } + + /// Going to the beginning with left, confirming with right + pub fn beginning_none_confirm() -> Self { + Self::new( + Some(ButtonAction::FirstPage), + None, + Some(ButtonAction::Confirm), + ) + } + + /// Going to the beginning with left, cancelling with right + pub fn beginning_none_cancel() -> Self { + Self::new( + Some(ButtonAction::FirstPage), + None, + Some(ButtonAction::Cancel), + ) + } + + /// Having access to appropriate action based on the `ButtonPos` + pub fn get_action(&self, pos: ButtonPos) -> Option { + match pos { + ButtonPos::Left => self.left, + ButtonPos::Middle => self.middle, + ButtonPos::Right => self.right, + } + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for Button { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("Button"); + match &self.content { + ButtonContent::Text(text) => t.string("text", text.as_ref()), + ButtonContent::Icon(icon) => { + t.null("text"); + t.string("icon", icon.name); + } + } + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for ButtonDetails { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("ButtonDetails"); + match &self.content { + ButtonContent::Text(text) => { + t.string("text", text.as_ref()); + } + ButtonContent::Icon(icon) => { + t.null("text"); + t.string("icon", icon.name); + } + } + if let Some(duration) = &self.duration { + t.int("hold_to_confirm", duration.to_millis() as i64); + } + } +} + +#[cfg(feature = "ui_debug")] +impl ButtonAction { + /// Describing the action as a string. Debug-only. + pub fn string(&self) -> ShortString { + match self { + ButtonAction::NextPage => "Next".into(), + ButtonAction::PrevPage => "Prev".into(), + ButtonAction::FirstPage => "First".into(), + ButtonAction::LastPage => "Last".into(), + ButtonAction::Cancel => "Cancel".into(), + ButtonAction::Confirm => "Confirm".into(), + ButtonAction::Info => "Info".into(), + } + } } diff --git a/core/embed/rust/src/ui/model_tr/component/button_controller.rs b/core/embed/rust/src/ui/model_tr/component/button_controller.rs new file mode 100644 index 000000000..638b1284c --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/button_controller.rs @@ -0,0 +1,445 @@ +use super::{ + theme, Button, ButtonDetails, ButtonLayout, ButtonPos, HoldToConfirm, HoldToConfirmMsg, +}; +use crate::{ + strutil::StringType, + ui::{ + component::{base::Event, Component, EventCtx, Pad}, + event::{ButtonEvent, PhysicalButton}, + geometry::Rect, + }, +}; + +/// All possible states buttons (left and right) can be at. +#[derive(Copy, Clone, PartialEq, Eq)] +enum ButtonState { + /// Both buttons are in untouched state. + /// _ _ + /// NEXT: OneDown + Nothing, + /// One Button is down when previously nothing was. + /// _ _ ... ↓ _ | _ ↓ + /// NEXT: Nothing, BothDown, HTCNeedsRelease + OneDown(PhysicalButton), + /// Both buttons are down ("middle-click"). + /// ↓ _ | _ ↓ ... ↓ ↓ + /// NEXT: OneReleased + BothDown, + /// One button is down when previously both were. + /// Happens when "middle-click" is performed. + /// ↓ ↓ ... ↓ _ | _ ↓ + /// NEXT: Nothing, BothDown + OneReleased(PhysicalButton), + /// One button is down after it triggered a HoldToConfirm event. + /// Needed so that we can cleanly reset the state. + /// ↓ _ | _ ↓ ... ↓ _ | _ ↓ + /// NEXT: Nothing + HTCNeedsRelease(PhysicalButton), +} + +pub enum ButtonControllerMsg { + Pressed(ButtonPos), + Triggered(ButtonPos), +} + +/// Defines what kind of button should be currently used. +pub enum ButtonType +where + T: StringType, +{ + Button(Button), + HoldToConfirm(HoldToConfirm), + Nothing, +} + +impl ButtonType +where + T: StringType, +{ + pub fn from_button_details(pos: ButtonPos, btn_details: Option>) -> Self { + if let Some(btn_details) = btn_details { + if btn_details.duration.is_some() { + Self::HoldToConfirm(HoldToConfirm::from_button_details(pos, btn_details)) + } else { + Self::Button(Button::from_button_details(pos, btn_details)) + } + } else { + Self::Nothing + } + } + + pub fn place(&mut self, button_area: Rect) { + match self { + Self::Button(button) => { + button.place(button_area); + } + Self::HoldToConfirm(htc) => { + htc.place(button_area); + } + Self::Nothing => {} + } + } + + pub fn paint(&mut self) { + match self { + Self::Button(button) => { + button.paint(); + } + Self::HoldToConfirm(htc) => { + htc.paint(); + } + Self::Nothing => {} + } + } +} + +/// Wrapping a button and its state, so that it can be easily +/// controlled from outside. +/// +/// Users have a choice of a normal button or Hold-to-confirm button. +/// `button_type` specified what from those two is used, if anything. +pub struct ButtonContainer +where + T: StringType, +{ + pos: ButtonPos, + button_type: ButtonType, +} + +impl ButtonContainer +where + T: StringType, +{ + /// Supplying `None` as `btn_details` marks the button inactive + /// (it can be later activated in `set()`). + pub fn new(pos: ButtonPos, btn_details: Option>) -> Self { + Self { + pos, + button_type: ButtonType::from_button_details(pos, btn_details), + } + } + + /// Changing the state of the button. + /// + /// Passing `None` as `btn_details` will mark the button as inactive. + pub fn set(&mut self, btn_details: Option>, button_area: Rect) { + self.button_type = ButtonType::from_button_details(self.pos, btn_details); + self.button_type.place(button_area); + } + + /// Placing the possible component. + pub fn place(&mut self, bounds: Rect) { + self.button_type.place(bounds); + } + + /// Painting the component that should be currently visible, if any. + pub fn paint(&mut self) { + self.button_type.paint(); + } + + /// Setting the visual state of the button - released/pressed. + pub fn set_pressed(&mut self, ctx: &mut EventCtx, is_pressed: bool) { + if let ButtonType::Button(btn) = &mut self.button_type { + btn.set_pressed(ctx, is_pressed); + } + } + + /// Trigger an action or end hold. + /// Called when the button is released. If it is a simple button, it returns + /// a Triggered message. If it is a hold-to-confirm button, it ends the + /// hold. + pub fn maybe_trigger(&mut self, ctx: &mut EventCtx) -> Option { + match self.button_type { + ButtonType::Button(_) => Some(ButtonControllerMsg::Triggered(self.pos)), + _ => { + self.hold_ended(ctx); + None + } + } + } + + /// Find out whether hold-to-confirm was triggered. + pub fn htc_got_triggered(&mut self, ctx: &mut EventCtx, event: Event) -> bool { + if let ButtonType::HoldToConfirm(htc) = &mut self.button_type { + if matches!(htc.event(ctx, event), Some(HoldToConfirmMsg::Confirmed)) { + return true; + } + } + false + } + + /// Registering hold event. + pub fn hold_started(&mut self, ctx: &mut EventCtx) { + if let ButtonType::HoldToConfirm(htc) = &mut self.button_type { + htc.event(ctx, Event::Button(ButtonEvent::HoldStarted)); + } + } + + /// Cancelling hold event. + pub fn hold_ended(&mut self, ctx: &mut EventCtx) { + if let ButtonType::HoldToConfirm(htc) = &mut self.button_type { + htc.event(ctx, Event::Button(ButtonEvent::HoldEnded)); + } + } +} + +/// Component responsible for handling buttons. +/// +/// Acts as a state-machine of `ButtonState`. +/// +/// Storing all three possible buttons - left, middle and right - +/// and handling their placement, painting and returning +/// appropriate events when they are triggered. +/// +/// Buttons can be interactively changed by clients by `set()`. +/// +/// Only "final" button events are returned in `ButtonControllerMsg::Triggered`, +/// based upon the buttons being long-press or not. +pub struct ButtonController +where + T: StringType, +{ + pad: Pad, + left_btn: ButtonContainer, + middle_btn: ButtonContainer, + right_btn: ButtonContainer, + state: ButtonState, + // Button area is needed so the buttons + // can be "re-placed" after their text is changed + // Will be set in `place` + button_area: Rect, +} + +impl ButtonController +where + T: StringType, +{ + pub fn new(btn_layout: ButtonLayout) -> Self { + Self { + pad: Pad::with_background(theme::BG).with_clear(), + left_btn: ButtonContainer::new(ButtonPos::Left, btn_layout.btn_left), + middle_btn: ButtonContainer::new(ButtonPos::Middle, btn_layout.btn_middle), + right_btn: ButtonContainer::new(ButtonPos::Right, btn_layout.btn_right), + state: ButtonState::Nothing, + button_area: Rect::zero(), + } + } + + /// Updating all the three buttons to the wanted states. + pub fn set(&mut self, btn_layout: ButtonLayout) { + self.pad.clear(); + self.left_btn.set(btn_layout.btn_left, self.button_area); + self.middle_btn.set(btn_layout.btn_middle, self.button_area); + self.right_btn.set(btn_layout.btn_right, self.button_area); + } + + /// Setting the pressed state for all three buttons by boolean flags. + fn set_pressed(&mut self, ctx: &mut EventCtx, left: bool, mid: bool, right: bool) { + self.left_btn.set_pressed(ctx, left); + self.middle_btn.set_pressed(ctx, mid); + self.right_btn.set_pressed(ctx, right); + } + + /// Handle middle button hold-to-confirm start. + /// We need to cancel possible holds in both other buttons. + fn middle_hold_started(&mut self, ctx: &mut EventCtx) { + self.left_btn.hold_ended(ctx); + self.middle_btn.hold_started(ctx); + self.right_btn.hold_ended(ctx); + } + + /// Handling the expiration of HTC elements. + /// Finding out if they have been triggered and sending event + /// for the appropriate button. + /// Setting the state to wait for the appropriate release event + /// from the pressed button. Also resetting visible state. + fn handle_htc_expiration( + &mut self, + ctx: &mut EventCtx, + event: Event, + ) -> Option { + if self.left_btn.htc_got_triggered(ctx, event) { + self.state = ButtonState::HTCNeedsRelease(PhysicalButton::Left); + self.set_pressed(ctx, false, false, false); + return Some(ButtonControllerMsg::Triggered(ButtonPos::Left)); + } else if self.middle_btn.htc_got_triggered(ctx, event) { + self.state = ButtonState::Nothing; + self.set_pressed(ctx, false, false, false); + return Some(ButtonControllerMsg::Triggered(ButtonPos::Middle)); + } else if self.right_btn.htc_got_triggered(ctx, event) { + self.state = ButtonState::HTCNeedsRelease(PhysicalButton::Right); + self.set_pressed(ctx, false, false, false); + return Some(ButtonControllerMsg::Triggered(ButtonPos::Right)); + } + None + } +} + +impl Component for ButtonController +where + T: StringType, +{ + type Msg = ButtonControllerMsg; + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + // State machine for the ButtonController + // We are matching event with `Event::Button` for a button action + // and `Event::Timer` for getting the expiration of HTC. + match event { + Event::Button(button_event) => { + let (new_state, event) = match self.state { + // _ _ + ButtonState::Nothing => match button_event { + // â–¼ * | * â–¼ + ButtonEvent::ButtonPressed(which) => ( + // ↓ _ | _ ↓ + ButtonState::OneDown(which), + match which { + // â–¼ * + PhysicalButton::Left => { + self.left_btn.hold_started(ctx); + Some(ButtonControllerMsg::Pressed(ButtonPos::Left)) + } + // * â–¼ + PhysicalButton::Right => { + self.right_btn.hold_started(ctx); + Some(ButtonControllerMsg::Pressed(ButtonPos::Right)) + } + }, + ), + _ => (self.state, None), + }, + // ↓ _ | _ ↓ + ButtonState::OneDown(which_down) => match button_event { + // â–² * | * â–² + ButtonEvent::ButtonReleased(b) if b == which_down => match which_down { + // â–² * + PhysicalButton::Left => { + // _ _ + (ButtonState::Nothing, self.left_btn.maybe_trigger(ctx)) + } + // * â–² + PhysicalButton::Right => { + // _ _ + (ButtonState::Nothing, self.right_btn.maybe_trigger(ctx)) + } + }, + // * â–¼ | â–¼ * + ButtonEvent::ButtonPressed(b) if b != which_down => { + self.middle_hold_started(ctx); + ( + // ↓ ↓ + ButtonState::BothDown, + Some(ButtonControllerMsg::Pressed(ButtonPos::Middle)), + ) + } + _ => (self.state, None), + }, + // ↓ ↓ + ButtonState::BothDown => match button_event { + // â–² * | * â–² + ButtonEvent::ButtonReleased(b) => { + self.middle_btn.hold_ended(ctx); + // _ ↓ | ↓ _ + (ButtonState::OneReleased(b), None) + } + _ => (self.state, None), + }, + // ↓ _ | _ ↓ + ButtonState::OneReleased(which_up) => match button_event { + // * â–¼ | â–¼ * + ButtonEvent::ButtonPressed(b) if b == which_up => { + self.middle_hold_started(ctx); + // ↓ ↓ + (ButtonState::BothDown, None) + } + // â–² * | * â–² + ButtonEvent::ButtonReleased(b) if b != which_up => { + // _ _ + (ButtonState::Nothing, self.middle_btn.maybe_trigger(ctx)) + } + _ => (self.state, None), + }, + // ↓ _ | _ ↓ + ButtonState::HTCNeedsRelease(needs_release) => match button_event { + // Only going out of this state if correct button was released + // â–² * | * â–² + ButtonEvent::ButtonReleased(released) if needs_release == released => { + // _ _ + (ButtonState::Nothing, None) + } + _ => (self.state, None), + }, + }; + + // Updating the visual feedback for the buttons + match new_state { + // Not showing anything also when we wait for a release + ButtonState::Nothing | ButtonState::HTCNeedsRelease(_) => { + self.set_pressed(ctx, false, false, false); + } + ButtonState::OneDown(down_button) => match down_button { + PhysicalButton::Left => { + self.set_pressed(ctx, true, false, false); + } + PhysicalButton::Right => { + self.set_pressed(ctx, false, false, true); + } + }, + ButtonState::BothDown | ButtonState::OneReleased(_) => { + self.set_pressed(ctx, false, true, false); + } + }; + + self.state = new_state; + event + } + // HoldToConfirm expiration + Event::Timer(_) => self.handle_htc_expiration(ctx, event), + _ => None, + } + } + + fn paint(&mut self) { + self.pad.paint(); + self.left_btn.paint(); + self.middle_btn.paint(); + self.right_btn.paint(); + } + + fn place(&mut self, bounds: Rect) -> Rect { + // Saving button area so that we can re-place the buttons + // when they get updated + self.button_area = bounds; + + self.pad.place(bounds); + self.left_btn.place(bounds); + self.middle_btn.place(bounds); + self.right_btn.place(bounds); + + bounds + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for ButtonContainer { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + if let ButtonType::Button(btn) = &self.button_type { + btn.trace(t); + } else if let ButtonType::HoldToConfirm(htc) = &self.button_type { + htc.trace(t); + } + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for ButtonController { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("ButtonController"); + t.child("left_btn", &self.left_btn); + t.child("middle_btn", &self.middle_btn); + t.child("right_btn", &self.right_btn); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/changing_text.rs b/core/embed/rust/src/ui/model_tr/component/changing_text.rs new file mode 100644 index 000000000..a56238caf --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/changing_text.rs @@ -0,0 +1,149 @@ +use crate::ui::{ + component::{Component, Event, EventCtx, Never, Pad}, + display::Font, + geometry::{Alignment, Point, Rect}, + util::long_line_content_with_ellipsis, +}; + +use super::{common, theme}; + +/// Component that allows for "allocating" a standalone line of text anywhere +/// on the screen and updating it arbitrarily - without affecting the rest +/// and without being affected by other components. +pub struct ChangingTextLine { + pad: Pad, + text: T, + font: Font, + /// Whether to show the text. Can be disabled. + show_content: bool, + alignment: Alignment, +} + +impl ChangingTextLine +where + T: AsRef, +{ + pub fn new(text: T, font: Font, alignment: Alignment) -> Self { + Self { + pad: Pad::with_background(theme::BG), + text, + font, + show_content: true, + alignment, + } + } + + pub fn center_mono(text: T) -> Self { + Self::new(text, Font::MONO, Alignment::Center) + } + + pub fn center_bold(text: T) -> Self { + Self::new(text, Font::BOLD, Alignment::Center) + } + + /// Update the text to be displayed in the line. + pub fn update_text(&mut self, text: T) { + self.text = text; + } + + /// Get current text. + pub fn get_text(&self) -> &T { + &self.text + } + + /// Whether we should display the text content. + /// If not, the whole area (Pad) will still be cleared. + /// Is valid until this function is called again. + pub fn show_or_not(&mut self, show: bool) { + self.show_content = show; + } + + /// Gets the height that is needed for this line to fit perfectly + /// without affecting the rest of the screen. + /// (Accounting for letters that go below the baseline (y, j, ...).) + pub fn needed_height(&self) -> i16 { + self.font.line_height() + 2 + } + + /// Y coordinate of text baseline, is the same for all paints. + fn y_baseline(&self) -> i16 { + self.pad.area.y0 + self.font.line_height() + } + + /// Whether the whole text can be painted in the available space + fn text_fits_completely(&self) -> bool { + self.font.text_width(self.text.as_ref()) <= self.pad.area.width() + } + + fn paint_left(&self) { + let baseline = Point::new(self.pad.area.x0, self.y_baseline()); + common::display(baseline, &self.text, self.font); + } + + fn paint_center(&self) { + let baseline = Point::new(self.pad.area.bottom_center().x, self.y_baseline()); + common::display_center(baseline, &self.text, self.font); + } + + fn paint_right(&self) { + let baseline = Point::new(self.pad.area.x1, self.y_baseline()); + common::display_right(baseline, &self.text, self.font); + } + + fn paint_long_content_with_ellipsis(&self) { + let text_to_display = long_line_content_with_ellipsis( + self.text.as_ref(), + "...", + self.font, + self.pad.area.width(), + ); + + // Creating the notion of motion by shifting the text left and right with + // each new text character. + // (So that it is apparent for the user that the text is changing.) + let x_offset = if self.text.as_ref().len() % 2 == 0 { + 0 + } else { + 2 + }; + + let baseline = Point::new(self.pad.area.x0 + x_offset, self.y_baseline()); + common::display(baseline, &text_to_display, self.font); + } +} + +impl Component for ChangingTextLine +where + T: AsRef, +{ + type Msg = Never; + + fn place(&mut self, bounds: Rect) -> Rect { + self.pad.place(bounds); + bounds + } + + fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { + None + } + + fn paint(&mut self) { + // Always re-painting from scratch. + // Effectively clearing the line completely + // when `self.show_content` is set to `false`. + self.pad.clear(); + self.pad.paint(); + if self.show_content { + // In the case text cannot fit, show ellipsis and its right part + if !self.text_fits_completely() { + self.paint_long_content_with_ellipsis(); + } else { + match self.alignment { + Alignment::Start => self.paint_left(), + Alignment::Center => self.paint_center(), + Alignment::End => self.paint_right(), + } + } + } + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/coinjoin_progress.rs b/core/embed/rust/src/ui/model_tr/component/coinjoin_progress.rs new file mode 100644 index 000000000..2151844ef --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/coinjoin_progress.rs @@ -0,0 +1,96 @@ +use crate::{ + strutil::StringType, + ui::{ + component::{ + base::Never, text::util::text_multiline_split_words, Component, Event, EventCtx, + }, + display::Font, + geometry::{Alignment, Rect}, + }, +}; + +use super::theme; + +const HEADER: &str = "COINJOIN IN PROGRESS"; +const FOOTER: &str = "Don't disconnect your Trezor"; + +pub struct CoinJoinProgress { + text: T, + area: Rect, +} + +impl CoinJoinProgress +where + T: StringType, +{ + pub fn new(text: T, _indeterminate: bool) -> Self { + Self { + text, + area: Rect::zero(), + } + } +} + +impl Component for CoinJoinProgress +where + T: StringType, +{ + type Msg = Never; + + fn place(&mut self, bounds: Rect) -> Rect { + self.area = bounds; + bounds + } + + fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { + None + } + + fn paint(&mut self) { + // Trying to paint all three parts into the area, stopping if any of them + // doesn't fit. + let mut possible_rest = text_multiline_split_words( + self.area, + HEADER, + Font::BOLD, + theme::FG, + theme::BG, + Alignment::Center, + ); + if let Some(rest) = possible_rest { + possible_rest = text_multiline_split_words( + rest, + self.text.as_ref(), + Font::MONO, + theme::FG, + theme::BG, + Alignment::Center, + ); + } else { + return; + } + if let Some(rest) = possible_rest { + text_multiline_split_words( + rest, + FOOTER, + Font::BOLD, + theme::FG, + theme::BG, + Alignment::Center, + ); + } + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for CoinJoinProgress +where + T: StringType, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("CoinJoinProgress"); + t.string("header", HEADER); + t.string("text", self.text.as_ref()); + t.string("footer", FOOTER); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/common.rs b/core/embed/rust/src/ui/model_tr/component/common.rs new file mode 100644 index 000000000..9f628b1d5 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/common.rs @@ -0,0 +1,28 @@ +use crate::ui::{ + display::{self, Font}, + geometry::Point, +}; + +use super::theme; + +/// Display white text on black background +pub fn display>(baseline: Point, text: &T, font: Font) { + display::text_left(baseline, text.as_ref(), font, theme::FG, theme::BG); +} + +/// Display black text on white background +pub fn display_inverse>(baseline: Point, text: &T, font: Font) { + display::text_left(baseline, text.as_ref(), font, theme::BG, theme::FG); +} + +/// Display white text on black background, +/// centered around a baseline Point +pub fn display_center>(baseline: Point, text: &T, font: Font) { + display::text_center(baseline, text.as_ref(), font, theme::FG, theme::BG); +} + +/// Display white text on black background, +/// with right boundary at a baseline Point +pub fn display_right>(baseline: Point, text: &T, font: Font) { + display::text_right(baseline, text.as_ref(), font, theme::FG, theme::BG); +} diff --git a/core/embed/rust/src/ui/model_tr/component/confirm.rs b/core/embed/rust/src/ui/model_tr/component/confirm.rs deleted file mode 100644 index 69d53a591..000000000 --- a/core/embed/rust/src/ui/model_tr/component/confirm.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::{ - time::Instant, - ui::{ - component::{Component, Event, EventCtx}, - event::ButtonEvent, - geometry::{Point, Rect}, - model_tr::component::{loader::Loader, ButtonPos, LoaderMsg, LoaderStyleSheet}, - }, -}; - -pub enum HoldToConfirmMsg { - Confirmed, - FailedToConfirm, -} - -pub struct HoldToConfirm { - area: Rect, - pos: ButtonPos, - loader: Loader, - baseline: Point, - text_width: i16, -} - -impl HoldToConfirm { - pub fn new(pos: ButtonPos, text: &'static str, styles: LoaderStyleSheet) -> Self { - let text_width = styles.normal.font.text_width(text.as_ref()); - Self { - area: Rect::zero(), - pos, - loader: Loader::new(text, styles), - baseline: Point::zero(), - text_width, - } - } - - fn placement(&mut self, area: Rect, pos: ButtonPos) -> Rect { - let button_width = self.text_width + 7; - match pos { - ButtonPos::Left => area.split_left(button_width).0, - ButtonPos::Right => area.split_right(button_width).1, - } - } -} - -impl Component for HoldToConfirm { - type Msg = HoldToConfirmMsg; - - fn place(&mut self, bounds: Rect) -> Rect { - let loader_area = self.placement(bounds, self.pos); - self.loader.place(loader_area) - } - - fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - match event { - Event::Button(ButtonEvent::ButtonPressed(which)) if self.pos.hit(&which) => { - self.loader.start_growing(ctx, Instant::now()); - } - Event::Button(ButtonEvent::ButtonReleased(which)) if self.pos.hit(&which) => { - if self.loader.is_animating() { - self.loader.start_shrinking(ctx, Instant::now()); - } - } - _ => {} - }; - - let msg = self.loader.event(ctx, event); - - if let Some(LoaderMsg::GrownCompletely) = msg { - return Some(HoldToConfirmMsg::Confirmed); - } - if let Some(LoaderMsg::ShrunkCompletely) = msg { - return Some(HoldToConfirmMsg::FailedToConfirm); - } - - None - } - - fn paint(&mut self) { - self.loader.paint(); - } -} - -#[cfg(feature = "ui_debug")] -impl crate::trace::Trace for HoldToConfirm { - fn trace(&self, t: &mut dyn crate::trace::Tracer) { - t.component("HoldToConfirm"); - t.child("loader", &self.loader); - } -} diff --git a/core/embed/rust/src/ui/model_tr/component/dialog.rs b/core/embed/rust/src/ui/model_tr/component/dialog.rs deleted file mode 100644 index e1c6f3921..000000000 --- a/core/embed/rust/src/ui/model_tr/component/dialog.rs +++ /dev/null @@ -1,93 +0,0 @@ -use super::button::{Button, ButtonMsg::Clicked}; -use crate::ui::{ - component::{Child, Component, Event, EventCtx}, - display::Font, - geometry::Rect, -}; - -pub enum DialogMsg { - Content(T), - LeftClicked, - RightClicked, -} - -pub struct Dialog { - content: Child, - left_btn: Option>>, - right_btn: Option>>, -} - -impl Dialog -where - T: Component, - U: AsRef, -{ - pub fn new(content: T, left: Option>, right: Option>) -> Self { - Self { - content: Child::new(content), - left_btn: left.map(Child::new), - right_btn: right.map(Child::new), - } - } - - pub fn inner(&self) -> &T { - self.content.inner() - } -} - -impl Component for Dialog -where - T: Component, - U: AsRef, -{ - type Msg = DialogMsg; - - fn place(&mut self, bounds: Rect) -> Rect { - let button_height = Font::BOLD.line_height() + 2; - let (content_area, button_area) = bounds.split_bottom(button_height); - self.content.place(content_area); - self.left_btn.as_mut().map(|b| b.place(button_area)); - self.right_btn.as_mut().map(|b| b.place(button_area)); - bounds - } - - fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - if let Some(msg) = self.content.event(ctx, event) { - Some(DialogMsg::Content(msg)) - } else if let Some(Clicked) = self.left_btn.as_mut().and_then(|b| b.event(ctx, event)) { - Some(DialogMsg::LeftClicked) - } else if let Some(Clicked) = self.right_btn.as_mut().and_then(|b| b.event(ctx, event)) { - Some(DialogMsg::RightClicked) - } else { - None - } - } - - fn paint(&mut self) { - self.content.paint(); - if let Some(b) = self.left_btn.as_mut() { - b.paint(); - } - if let Some(b) = self.right_btn.as_mut() { - b.paint(); - } - } -} - -#[cfg(feature = "ui_debug")] -impl crate::trace::Trace for Dialog -where - T: crate::trace::Trace, - U: crate::trace::Trace + AsRef, -{ - fn trace(&self, t: &mut dyn crate::trace::Tracer) { - t.component("Dialog"); - t.child("content", &self.content); - if let Some(b) = self.left_btn.as_ref() { - t.child("left", b) - } - if let Some(b) = self.right_btn.as_ref() { - t.child("right", b) - } - } -} diff --git a/core/embed/rust/src/ui/model_tr/component/flow.rs b/core/embed/rust/src/ui/model_tr/component/flow.rs new file mode 100644 index 000000000..888a88f7c --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/flow.rs @@ -0,0 +1,302 @@ +use crate::{ + strutil::StringType, + ui::{ + component::{Child, Component, ComponentExt, Event, EventCtx, Pad, Paginate}, + geometry::Rect, + }, +}; + +use super::{ + scrollbar::SCROLLBAR_SPACE, theme, title::Title, ButtonAction, ButtonController, + ButtonControllerMsg, ButtonLayout, ButtonPos, CancelInfoConfirmMsg, FlowPages, Page, ScrollBar, +}; + +pub struct Flow +where + F: Fn(usize) -> Page, + T: StringType + Clone, +{ + /// Function to get pages from + pages: FlowPages, + /// Instance of the current Page + current_page: Page, + /// Title being shown at the top in bold + title: Option>, + scrollbar: Child, + content_area: Rect, + title_area: Rect, + pad: Pad, + buttons: Child>, + page_counter: usize, + return_confirmed_index: bool, +} + +impl Flow +where + F: Fn(usize) -> Page, + T: StringType + Clone, +{ + pub fn new(pages: FlowPages) -> Self { + let current_page = pages.get(0); + Self { + pages, + current_page, + title: None, + content_area: Rect::zero(), + title_area: Rect::zero(), + scrollbar: Child::new(ScrollBar::to_be_filled_later()), + pad: Pad::with_background(theme::BG), + // Setting empty layout for now, we do not yet know how many sub-pages the first page + // has. Initial button layout will be set in `place()` after we can call + // `content.page_count()`. + buttons: Child::new(ButtonController::new(ButtonLayout::empty())), + page_counter: 0, + return_confirmed_index: false, + } + } + + /// Adding a common title to all pages. The title will not be colliding + /// with the page content, as the content will be offset. + pub fn with_common_title(mut self, title: T) -> Self { + self.title = Some(Title::new(title)); + self + } + + /// Causing the Flow to return the index of the page that was confirmed. + pub fn with_return_confirmed_index(mut self) -> Self { + self.return_confirmed_index = true; + self + } + + pub fn confirmed_index(&self) -> Option { + self.return_confirmed_index.then_some(self.page_counter) + } + + /// Getting new current page according to page counter. + /// Also updating the possible title and moving the scrollbar to correct + /// position. + fn change_current_page(&mut self, ctx: &mut EventCtx) { + self.current_page = self.pages.get(self.page_counter); + if self.title.is_some() { + if let Some(title) = self.current_page.title() { + self.title = Some(Title::new(title)); + self.title.place(self.title_area); + } + } + let scrollbar_active_index = self + .pages + .scrollbar_page_index(self.content_area, self.page_counter); + self.scrollbar.mutate(ctx, |_ctx, scrollbar| { + scrollbar.change_page(scrollbar_active_index); + }); + } + + /// Placing current page, setting current buttons and clearing. + fn update(&mut self, ctx: &mut EventCtx, get_new_page: bool) { + if get_new_page { + self.change_current_page(ctx); + } + self.current_page.place(self.content_area); + self.set_buttons(ctx); + self.scrollbar.request_complete_repaint(ctx); + self.clear_and_repaint(ctx); + } + + /// Clearing the whole area and requesting repaint. + fn clear_and_repaint(&mut self, ctx: &mut EventCtx) { + self.pad.clear(); + ctx.request_paint(); + } + + /// Going to the previous page. + fn go_to_prev_page(&mut self, ctx: &mut EventCtx) { + self.page_counter -= 1; + self.update(ctx, true); + } + + /// Going to the next page. + fn go_to_next_page(&mut self, ctx: &mut EventCtx) { + self.page_counter += 1; + self.update(ctx, true); + } + + /// Going to the first page. + fn go_to_first_page(&mut self, ctx: &mut EventCtx) { + self.page_counter = 0; + self.update(ctx, true); + } + + /// Going to the first page. + fn go_to_last_page(&mut self, ctx: &mut EventCtx) { + self.page_counter = self.pages.count() - 1; + self.update(ctx, true); + } + + /// Jumping to another page relative to the current one. + fn go_to_page_relative(&mut self, jump: i16, ctx: &mut EventCtx) { + self.page_counter += jump as usize; + self.update(ctx, true); + } + + /// Updating the visual state of the buttons after each event. + /// All three buttons are handled based upon the current choice. + /// If defined in the current choice, setting their text, + /// whether they are long-pressed, and painting them. + fn set_buttons(&mut self, ctx: &mut EventCtx) { + let btn_layout = self.current_page.btn_layout(); + self.buttons.mutate(ctx, |_ctx, buttons| { + buttons.set(btn_layout); + }); + } + + /// Current choice is still the same, only its inner state has changed + /// (its sub-page changed). + fn update_after_current_choice_inner_change(&mut self, ctx: &mut EventCtx) { + let inner_page = self.current_page.get_current_page(); + self.scrollbar.mutate(ctx, |ctx, scrollbar| { + scrollbar.change_page(self.page_counter + inner_page); + scrollbar.request_complete_repaint(ctx); + }); + self.update(ctx, false); + } + + /// When current choice contains paginated content, it may use the button + /// event to just paginate itself. + fn event_consumed_by_current_choice(&mut self, ctx: &mut EventCtx, pos: ButtonPos) -> bool { + if matches!(pos, ButtonPos::Left) && self.current_page.has_prev_page() { + self.current_page.go_to_prev_page(); + self.update_after_current_choice_inner_change(ctx); + true + } else if matches!(pos, ButtonPos::Right) && self.current_page.has_next_page() { + self.current_page.go_to_next_page(); + self.update_after_current_choice_inner_change(ctx); + true + } else { + false + } + } +} + +impl Component for Flow +where + F: Fn(usize) -> Page, + T: StringType + Clone, +{ + type Msg = CancelInfoConfirmMsg; + + fn place(&mut self, bounds: Rect) -> Rect { + let (title_content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT); + // Accounting for possible title + let (title_area, content_area) = if self.title.is_some() { + title_content_area.split_top(theme::FONT_HEADER.line_height()) + } else { + (Rect::zero(), title_content_area) + }; + self.content_area = content_area; + + // Finding out the total amount of pages in this flow + let complete_page_count = self.pages.scrollbar_page_count(content_area); + // Redefining scrollbar now when we have its page_count + self.scrollbar = Child::new(ScrollBar::new(complete_page_count)); + + // Placing a title and scrollbar in case the title is there + // (scrollbar will be active - counting pages - even when not placed and + // painted) + if self.title.is_some() { + let (title_area, scrollbar_area) = + title_area.split_right(self.scrollbar.inner().overall_width() + SCROLLBAR_SPACE); + + self.title.place(title_area); + self.title_area = title_area; + self.scrollbar.place(scrollbar_area); + } + + // We finally found how long is the first page, and can set its button layout. + self.current_page.place(content_area); + self.buttons = Child::new(ButtonController::new(self.current_page.btn_layout())); + + self.pad.place(title_content_area); + self.buttons.place(button_area); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + ctx.set_page_count(self.pages.scrollbar_page_count(self.content_area)); + self.title.event(ctx, event); + let button_event = self.buttons.event(ctx, event); + + // Do something when a button was triggered + // and we have some action connected with it + if let Some(ButtonControllerMsg::Triggered(pos)) = button_event { + // When there is a previous or next screen in the current flow, + // handle that first and in case it triggers, then do not continue + if self.event_consumed_by_current_choice(ctx, pos) { + return None; + } + + let actions = self.current_page.btn_actions(); + let action = actions.get_action(pos); + if let Some(action) = action { + match action { + ButtonAction::PrevPage => { + self.go_to_prev_page(ctx); + return None; + } + ButtonAction::NextPage => { + self.go_to_next_page(ctx); + return None; + } + ButtonAction::FirstPage => { + self.go_to_first_page(ctx); + return None; + } + ButtonAction::LastPage => { + self.go_to_last_page(ctx); + return None; + } + ButtonAction::Cancel => return Some(CancelInfoConfirmMsg::Cancelled), + ButtonAction::Confirm => return Some(CancelInfoConfirmMsg::Confirmed), + ButtonAction::Info => return Some(CancelInfoConfirmMsg::Info), + } + } + }; + None + } + + fn paint(&mut self) { + self.pad.paint(); + // Scrollbars are painted only with a title + if self.title.is_some() { + self.scrollbar.paint(); + self.title.paint(); + } + self.buttons.paint(); + // On purpose painting current page at the end, after buttons, + // because we sometimes (in the case of QR code) need to use the + // whole height of the display for showing the content + // (and painting buttons last would cover the lower part). + self.current_page.paint(); + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for Flow +where + F: Fn(usize) -> Page, + T: StringType + Clone, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("Flow"); + t.int("flow_page", self.page_counter as i64); + t.int("flow_page_count", self.pages.count() as i64); + + if let Some(title) = &self.title { + t.child("title", title); + } + t.child("scrollbar", &self.scrollbar); + t.child("buttons", &self.buttons); + t.child("flow_page", &self.current_page); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/flow_pages.rs b/core/embed/rust/src/ui/model_tr/component/flow_pages.rs new file mode 100644 index 000000000..2e712ac32 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/flow_pages.rs @@ -0,0 +1,224 @@ +use crate::{ + strutil::StringType, + ui::{ + component::{base::Component, text::layout::LayoutFit, FormattedText, Paginate}, + geometry::Rect, + }, +}; + +use super::{ButtonActions, ButtonDetails, ButtonLayout}; + +// So that there is only one implementation, and not multiple generic ones +// as would be via `const N: usize` generics. +const MAX_OPS_PER_PAGE: usize = 15; + +/// Holding specific workflows that are created in `layout.rs`. +/// Is returning a `Page` (page/screen) on demand +/// based on the current page in `Flow`. +/// Before, when `layout.rs` was defining a `heapless::Vec` of `Page`s, +/// it was a very stack-expensive operation and StackOverflow was encountered. +/// With this "lazy-loading" approach (creating each page on demand) we can +/// have theoretically unlimited number of pages without triggering SO. +/// (Currently only the current page is stored on stack - in +/// `Flow::current_page`.) +pub struct FlowPages +where + T: StringType + Clone, + F: Fn(usize) -> Page, +{ + /// Function/closure that will return appropriate page on demand. + get_page: F, + /// Number of pages in the flow. + page_count: usize, +} + +impl FlowPages +where + F: Fn(usize) -> Page, + T: StringType + Clone, +{ + pub fn new(get_page: F, page_count: usize) -> Self { + Self { + get_page, + page_count, + } + } + + /// Returns a page on demand on a specified index. + pub fn get(&self, page_index: usize) -> Page { + (self.get_page)(page_index) + } + + /// Total amount of pages. + pub fn count(&self) -> usize { + self.page_count + } + + /// How many scrollable pages are there in the flow + /// (each page can have arbitrary number of "sub-pages"). + pub fn scrollbar_page_count(&self, bounds: Rect) -> usize { + self.scrollbar_page_index(bounds, self.page_count) + } + + /// Active scrollbar position connected with the beginning of a specific + /// page index. + pub fn scrollbar_page_index(&self, bounds: Rect, page_index: usize) -> usize { + let mut page_count = 0; + for i in 0..page_index { + let mut current_page = self.get(i); + current_page.place(bounds); + page_count += current_page.page_count; + } + page_count + } +} + +#[derive(Clone)] +pub struct Page +where + T: StringType + Clone, +{ + formatted: FormattedText, + btn_layout: ButtonLayout, + btn_actions: ButtonActions, + current_page: usize, + page_count: usize, + title: Option, +} + +// For `layout.rs` +impl Page +where + T: StringType + Clone, +{ + pub fn new( + btn_layout: ButtonLayout, + btn_actions: ButtonActions, + formatted: FormattedText, + ) -> Self { + Self { + formatted, + btn_layout, + btn_actions, + current_page: 0, + page_count: 1, + title: None, + } + } +} + +// For `flow.rs` +impl Page +where + T: StringType + Clone, +{ + /// Adding title. + pub fn with_title(mut self, title: T) -> Self { + self.title = Some(title); + self + } + + pub fn paint(&mut self) { + self.change_page(self.current_page); + self.formatted.paint(); + } + + pub fn place(&mut self, bounds: Rect) -> Rect { + self.formatted.place(bounds); + self.page_count = self.page_count(); + bounds + } + + pub fn btn_layout(&self) -> ButtonLayout { + // When we are in pagination inside this flow, + // show the up and down arrows on appropriate sides. + let current = self.btn_layout.clone(); + + // On the last page showing only the narrow arrow, so the right + // button with possibly long text has enough space. + let btn_left = if self.has_prev_page() && !self.has_next_page() { + Some(ButtonDetails::up_arrow_icon()) + } else if self.has_prev_page() { + Some(ButtonDetails::up_arrow_icon_wide()) + } else { + current.btn_left + }; + + // Middle button should be shown only on the last page, not to collide + // with the fat right button. + let (btn_middle, btn_right) = if self.has_next_page() { + (None, Some(ButtonDetails::down_arrow_icon_wide())) + } else { + (current.btn_middle, current.btn_right) + }; + + ButtonLayout::new(btn_left, btn_middle, btn_right) + } + + pub fn btn_actions(&self) -> ButtonActions { + self.btn_actions + } + + pub fn title(&self) -> Option { + self.title.clone() + } + + pub fn has_prev_page(&self) -> bool { + self.current_page > 0 + } + + pub fn has_next_page(&self) -> bool { + self.current_page < self.page_count - 1 + } + + pub fn go_to_prev_page(&mut self) { + self.current_page -= 1; + } + + pub fn go_to_next_page(&mut self) { + self.current_page += 1; + } + + pub fn get_current_page(&self) -> usize { + self.current_page + } +} + +// Pagination +impl Paginate for Page +where + T: StringType + Clone, +{ + fn page_count(&mut self) -> usize { + self.formatted.page_count() + } + + fn change_page(&mut self, to_page: usize) { + self.formatted.change_page(to_page) + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for Page +where + T: StringType + Clone, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + use crate::ui::component::text::layout::trace::TraceSink; + use core::cell::Cell; + let fit: Cell> = Cell::new(None); + t.component("Page"); + if let Some(title) = &self.title { + // Not calling it "title" as that is already traced by FlowPage + t.string("page_title", title.as_ref()); + } + t.int("active_page", self.current_page as i64); + t.int("page_count", self.page_count as i64); + t.in_list("text", &|l| { + let result = self.formatted.layout_content_debug(&mut TraceSink(l)); + fit.set(Some(result)); + }); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/frame.rs b/core/embed/rust/src/ui/model_tr/component/frame.rs index 4151ead74..0cdce56ec 100644 --- a/core/embed/rust/src/ui/model_tr/component/frame.rs +++ b/core/embed/rust/src/ui/model_tr/component/frame.rs @@ -1,78 +1,238 @@ -use super::theme; -use crate::ui::{ - component::{Child, Component, Event, EventCtx}, - display::{self, Font}, - geometry::{Insets, Offset, Rect}, +use crate::{ + strutil::StringType, + ui::{ + component::{Child, Component, ComponentExt, Event, EventCtx, Paginate}, + geometry::{Insets, Rect}, + }, }; -pub struct Frame { - area: Rect, - title: U, +use super::{super::constant, scrollbar::SCROLLBAR_SPACE, theme, title::Title, ScrollBar}; + +/// Component for holding another component and displaying a title. +pub struct Frame +where + T: Component, + U: StringType, +{ + title: Title, content: Child, } impl Frame where T: Component, - U: AsRef, + U: StringType + Clone, { pub fn new(title: U, content: T) -> Self { Self { - title, - area: Rect::zero(), + title: Title::new(title), content: Child::new(content), } } + /// Aligning the title to the center, instead of the left. + pub fn with_title_centered(mut self) -> Self { + self.title = self.title.with_centered(); + self + } + pub fn inner(&self) -> &T { self.content.inner() } + + pub fn update_title(&mut self, ctx: &mut EventCtx, new_title: U) { + self.title.set_text(ctx, new_title); + } + + pub fn update_content(&mut self, ctx: &mut EventCtx, update_fn: F) -> R + where + F: Fn(&mut T) -> R, + { + self.content.mutate(ctx, |ctx, c| { + let res = update_fn(c); + c.request_complete_repaint(ctx); + res + }) + } } impl Component for Frame where T: Component, - U: AsRef, + U: StringType + Clone, { type Msg = T::Msg; fn place(&mut self, bounds: Rect) -> Rect { - const TITLE_SPACE: i16 = 4; + const TITLE_SPACE: i16 = 2; - let (title_area, content_area) = bounds.split_top(Font::BOLD.line_height()); + let (title_area, content_area) = bounds.split_top(theme::FONT_HEADER.line_height()); let content_area = content_area.inset(Insets::top(TITLE_SPACE)); - self.area = title_area; + self.title.place(title_area); self.content.place(content_area); bounds } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + self.title.event(ctx, event); self.content.event(ctx, event) } fn paint(&mut self) { - display::text_left( - self.area.bottom_left() - Offset::y(2), - self.title.as_ref(), - Font::BOLD, - theme::FG, - theme::BG, - ); - display::dotted_line(self.area.bottom_left(), self.area.width(), theme::FG); + self.title.paint(); + self.content.paint(); + } +} + +impl Paginate for Frame +where + T: Component + Paginate, + U: StringType + Clone, +{ + fn page_count(&mut self) -> usize { + self.content.page_count() + } + + fn change_page(&mut self, active_page: usize) { + self.content.change_page(active_page); + } +} + +pub trait ScrollableContent { + fn page_count(&self) -> usize; + fn active_page(&self) -> usize; +} + +/// Component for holding another component and displaying a title. +/// Also is allocating space for a scrollbar. +pub struct ScrollableFrame +where + T: Component + ScrollableContent, + U: StringType + Clone, +{ + title: Option>>, + scrollbar: ScrollBar, + content: Child, +} + +impl ScrollableFrame +where + T: Component + ScrollableContent, + U: StringType + Clone, +{ + pub fn new(content: T) -> Self { + Self { + title: None, + scrollbar: ScrollBar::to_be_filled_later(), + content: Child::new(content), + } + } + + pub fn inner(&self) -> &T { + self.content.inner() + } + + pub fn with_title(mut self, title: U) -> Self { + self.title = Some(Child::new(Title::new(title))); + self + } +} + +impl Component for ScrollableFrame +where + T: Component + ScrollableContent, + U: StringType + Clone, +{ + type Msg = T::Msg; + + fn place(&mut self, bounds: Rect) -> Rect { + // Depending whether there is a title or not + let (content_area, scrollbar_area, title_area) = if self.title.is_none() { + // When the content fits on one page, no need for allocating place for scrollbar + self.content.place(bounds); + let page_count = self.content.inner().page_count(); + self.scrollbar.set_page_count(page_count); + if page_count == 1 { + (bounds, Rect::zero(), Rect::zero()) + } else { + let (scrollbar_area, content_area) = + bounds.split_top(ScrollBar::MAX_DOT_SIZE + constant::LINE_SPACE); + (content_area, scrollbar_area, Rect::zero()) + } + } else { + const TITLE_SPACE: i16 = 2; + + let (title_and_scrollbar_area, content_area) = + bounds.split_top(theme::FONT_HEADER.line_height()); + let content_area = content_area.inset(Insets::top(TITLE_SPACE)); + + // When there is only one page, do not allocate anything for scrollbar, + // which would reduce the space for title + self.content.place(content_area); + let page_count = self.content.inner().page_count(); + self.scrollbar.set_page_count(page_count); + let (title_area, scrollbar_area) = if page_count == 1 { + (title_and_scrollbar_area, Rect::zero()) + } else { + title_and_scrollbar_area + .split_right(self.scrollbar.overall_width() + SCROLLBAR_SPACE) + }; + + (content_area, scrollbar_area, title_area) + }; + + self.content.place(content_area); + self.scrollbar.place(scrollbar_area); + self.title.place(title_area); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + let msg = self.content.event(ctx, event); + let content_active_page = self.content.inner().active_page(); + if self.scrollbar.active_page != content_active_page { + self.scrollbar.change_page(content_active_page); + self.scrollbar.request_complete_repaint(ctx); + } + self.title.event(ctx, event); + msg + } + + fn paint(&mut self) { + self.title.paint(); + self.scrollbar.paint(); self.content.paint(); } } +// DEBUG-ONLY SECTION BELOW + #[cfg(feature = "ui_debug")] impl crate::trace::Trace for Frame where - T: crate::trace::Trace, - U: AsRef, + T: crate::trace::Trace + Component, + U: StringType + Clone, { fn trace(&self, t: &mut dyn crate::trace::Tracer) { t.component("Frame"); - t.string("title", self.title.as_ref()); + t.child("title", &self.title); + t.child("content", &self.content); + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for ScrollableFrame +where + T: crate::trace::Trace + Component + ScrollableContent, + U: StringType + Clone, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("ScrollableFrame"); + if let Some(title) = &self.title { + t.child("title", title); + } + t.child("scrollbar", &self.scrollbar); t.child("content", &self.content); } } diff --git a/core/embed/rust/src/ui/model_tr/component/hold_to_confirm.rs b/core/embed/rust/src/ui/model_tr/component/hold_to_confirm.rs new file mode 100644 index 000000000..d25f07ba8 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/hold_to_confirm.rs @@ -0,0 +1,140 @@ +use crate::{ + strutil::StringType, + time::{Duration, Instant}, + ui::{ + component::{Component, Event, EventCtx}, + event::ButtonEvent, + geometry::Rect, + }, +}; + +use super::{ + loader::{Loader, DEFAULT_DURATION_MS}, + theme, ButtonContent, ButtonDetails, ButtonPos, LoaderMsg, LoaderStyleSheet, +}; + +pub enum HoldToConfirmMsg { + Confirmed, + FailedToConfirm, +} + +pub struct HoldToConfirm +where + T: StringType, +{ + pos: ButtonPos, + loader: Loader, + text_width: i16, +} + +impl HoldToConfirm +where + T: StringType, +{ + pub fn text(pos: ButtonPos, text: T, styles: LoaderStyleSheet, duration: Duration) -> Self { + let text_width = styles.normal.font.visible_text_width(text.as_ref()); + Self { + pos, + loader: Loader::text(text, styles).with_growing_duration(duration), + text_width, + } + } + + pub fn from_button_details(pos: ButtonPos, btn_details: ButtonDetails) -> Self { + let duration = btn_details + .duration + .unwrap_or_else(|| Duration::from_millis(DEFAULT_DURATION_MS)); + match btn_details.content { + ButtonContent::Text(text) => { + Self::text(pos, text, LoaderStyleSheet::default_loader(), duration) + } + ButtonContent::Icon(_) => panic!("Icon is not supported"), + } + } + + /// Updating the text of the component and re-placing it. + pub fn set_text(&mut self, text: T, button_area: Rect) { + self.text_width = self.loader.get_text_width(&text); + self.loader.set_text(text); + self.place(button_area); + } + + pub fn reset(&mut self) { + self.loader.reset(); + } + + pub fn set_duration(&mut self, duration: Duration) { + self.loader.set_duration(duration); + } + + pub fn get_duration(&self) -> Duration { + self.loader.get_duration() + } + + pub fn get_text(&self) -> &T { + self.loader.get_text() + } + + fn placement(&mut self, area: Rect, pos: ButtonPos) -> Rect { + let button_width = self.text_width + 2 * theme::BUTTON_OUTLINE; + match pos { + ButtonPos::Left => area.split_left(button_width).0, + ButtonPos::Right => area.split_right(button_width).1, + ButtonPos::Middle => area.split_center(button_width).1, + } + } +} + +impl Component for HoldToConfirm +where + T: StringType, +{ + type Msg = HoldToConfirmMsg; + + fn place(&mut self, bounds: Rect) -> Rect { + let loader_area = self.placement(bounds, self.pos); + self.loader.place(loader_area) + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + match event { + Event::Button(ButtonEvent::HoldStarted) => { + self.loader.start_growing(ctx, Instant::now()); + } + Event::Button(ButtonEvent::HoldEnded) => { + if self.loader.is_animating() { + self.loader.start_shrinking(ctx, Instant::now()); + } + } + _ => {} + }; + + let msg = self.loader.event(ctx, event); + + if let Some(LoaderMsg::GrownCompletely) = msg { + return Some(HoldToConfirmMsg::Confirmed); + } + if let Some(LoaderMsg::ShrunkCompletely) = msg { + return Some(HoldToConfirmMsg::FailedToConfirm); + } + + None + } + + fn paint(&mut self) { + self.loader.paint(); + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for HoldToConfirm +where + T: StringType, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("HoldToConfirm"); + t.child("loader", &self.loader); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/homescreen.rs b/core/embed/rust/src/ui/model_tr/component/homescreen.rs new file mode 100644 index 000000000..19a397c0b --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/homescreen.rs @@ -0,0 +1,229 @@ +use crate::{ + strutil::StringType, + trezorhal::usb::usb_configured, + ui::{ + component::{Child, Component, Event, EventCtx, Label}, + display::{rect_fill, toif::Toif, Font}, + event::USBEvent, + geometry::{self, Insets, Offset, Point, Rect}, + layout::util::get_user_custom_image, + }, +}; + +use super::{ + super::constant, common::display_center, theme, ButtonController, ButtonControllerMsg, + ButtonLayout, +}; + +const AREA: Rect = constant::screen(); +const TOP_CENTER: Point = AREA.top_center(); +const LABEL_Y: i16 = constant::HEIGHT - 15; +const LABEL_AREA: Rect = AREA.split_top(LABEL_Y).1; +const LOCKED_INSTRUCTION_Y: i16 = 27; +const LOCKED_INSTRUCTION_AREA: Rect = AREA.split_top(LOCKED_INSTRUCTION_Y).1; +const LOGO_ICON_TOP_MARGIN: i16 = 12; +const LOCK_ICON_TOP_MARGIN: i16 = 12; +const NOTIFICATION_HEIGHT: i16 = 12; +const LABEL_OUTSET: i16 = 3; + +pub struct Homescreen +where + T: StringType, +{ + // TODO label should be a Child in theory, but the homescreen image is not, so it is + // always painted, so we need to always paint the label too + label: Label, + notification: Option<(T, u8)>, + /// Used for HTC functionality to lock device from homescreen + invisible_buttons: Child>, +} + +impl Homescreen +where + T: StringType + Clone, +{ + pub fn new(label: T, notification: Option<(T, u8)>) -> Self { + let invisible_btn_layout = ButtonLayout::htc_none_htc("".into(), "".into()); + Self { + label: Label::centered(label, theme::TEXT_NORMAL), + notification, + invisible_buttons: Child::new(ButtonController::new(invisible_btn_layout)), + } + } + + fn paint_homescreen_image(&self) { + if let Ok(user_custom_image) = get_user_custom_image() { + let toif_data = unwrap!(Toif::new(user_custom_image.as_ref())); + toif_data.draw(TOP_CENTER, geometry::TOP_CENTER, theme::FG, theme::BG); + } else { + theme::ICON_LOGO.draw( + TOP_CENTER + Offset::y(LOGO_ICON_TOP_MARGIN), + geometry::TOP_CENTER, + theme::FG, + theme::BG, + ); + } + } + + fn paint_notification(&self) { + let baseline = TOP_CENTER + Offset::y(Font::MONO.line_height()); + if !usb_configured() { + self.fill_notification_background(); + // TODO: fill warning icons here as well? + display_center(baseline, &"NO USB CONNECTION", Font::MONO); + } else if let Some((notification, _level)) = &self.notification { + self.fill_notification_background(); + // TODO: what if the notification text is so long it collides with icons? + self.paint_warning_icons_in_top_corners(); + display_center(baseline, ¬ification.as_ref(), Font::MONO); + } + } + + fn paint_label(&mut self) { + // paint black background to place the label + let mut outset = Insets::uniform(LABEL_OUTSET); + // the margin at top is bigger (caused by text-height vs line-height?) + // compensate by shrinking the outset + outset.top -= 1; + rect_fill(self.label.text_area().outset(outset), theme::BG); + self.label.paint(); + } + + /// So that notification is well visible even on homescreen image + fn fill_notification_background(&self) { + rect_fill(AREA.split_top(NOTIFICATION_HEIGHT).0, theme::BG); + } + + fn paint_warning_icons_in_top_corners(&self) { + let warning_icon = theme::ICON_WARNING; + warning_icon.draw(AREA.top_left(), geometry::TOP_LEFT, theme::FG, theme::BG); + // Needs x+1 Offset to compensate for empty right column (icon needs to be + // even-wide) + warning_icon.draw( + AREA.top_right() + Offset::x(1), + geometry::TOP_RIGHT, + theme::FG, + theme::BG, + ); + } + + fn event_usb(&mut self, ctx: &mut EventCtx, event: Event) { + if let Event::USB(USBEvent::Connected(_)) = event { + ctx.request_paint(); + } + } +} + +impl Component for Homescreen +where + T: StringType + Clone, +{ + type Msg = (); + + fn place(&mut self, bounds: Rect) -> Rect { + self.label.place(LABEL_AREA); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + Self::event_usb(self, ctx, event); + // HTC press of any button will lock the device + if let Some(ButtonControllerMsg::Triggered(_)) = self.invisible_buttons.event(ctx, event) { + return Some(()); + } + None + } + + fn paint(&mut self) { + // Painting the homescreen image first, as the notification and label + // should be "on top of it" + self.paint_homescreen_image(); + self.paint_notification(); + self.paint_label(); + } +} + +pub struct Lockscreen +where + T: StringType, +{ + label: Child>, + instruction: Child>, + /// Used for unlocking the device from lockscreen + invisible_buttons: Child>, +} + +impl Lockscreen +where + T: StringType + Clone, +{ + pub fn new(label: T, bootscreen: bool) -> Self { + let invisible_btn_layout = ButtonLayout::text_none_text("".into(), "".into()); + let instruction_str = if bootscreen { + "Click to Connect" + } else { + "Click to Unlock" + }; + Lockscreen { + label: Child::new(Label::centered(label, theme::TEXT_NORMAL)), + instruction: Child::new(Label::centered(instruction_str.into(), theme::TEXT_MONO)), + invisible_buttons: Child::new(ButtonController::new(invisible_btn_layout)), + } + } +} + +impl Component for Lockscreen +where + T: StringType + Clone, +{ + type Msg = (); + + fn place(&mut self, bounds: Rect) -> Rect { + self.label.place(LABEL_AREA); + self.instruction.place(LOCKED_INSTRUCTION_AREA); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + // Press of any button will unlock the device + if let Some(ButtonControllerMsg::Triggered(_)) = self.invisible_buttons.event(ctx, event) { + return Some(()); + } + None + } + + fn paint(&mut self) { + theme::ICON_LOCK.draw( + TOP_CENTER + Offset::y(LOCK_ICON_TOP_MARGIN), + geometry::TOP_CENTER, + theme::FG, + theme::BG, + ); + self.instruction.paint(); + self.label.paint(); + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for Homescreen +where + T: StringType, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("Homescreen"); + t.child("label", &self.label); + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for Lockscreen +where + T: StringType, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("Lockscreen"); + t.child("label", &self.label); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/choice.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/choice.rs new file mode 100644 index 000000000..95825ca36 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/choice.rs @@ -0,0 +1,464 @@ +use crate::{ + strutil::StringType, + ui::{ + component::{Child, Component, Event, EventCtx, Pad}, + geometry::{Offset, Rect}, + }, +}; + +use super::{ + super::{theme, ButtonController, ButtonControllerMsg, ButtonLayout, ButtonPos}, + choice_item::ChoiceItem, +}; + +const DEFAULT_ITEMS_DISTANCE: i16 = 10; +const DEFAULT_Y_BASELINE: i16 = 20; + +pub trait Choice { + fn paint_center(&self, area: Rect, inverse: bool); + fn width_center(&self) -> i16; + + fn paint_side(&self, area: Rect); + fn width_side(&self) -> i16; + + fn btn_layout(&self) -> ButtonLayout { + ButtonLayout::default_three_icons() + } +} + +/// Interface for a specific component efficiently giving +/// `ChoicePage` all the information it needs to render +/// all the choice pages. +/// +/// It avoids the need to store the whole sequence of +/// items in `heapless::Vec` (which caused StackOverflow), +/// but offers a "lazy-loading" way of requesting the +/// items only when they are needed, one-by-one. +/// This way, no more than one item is stored in memory at any time. +pub trait ChoiceFactory { + type Action; + + fn count(&self) -> usize; + fn get(&self, index: usize) -> (ChoiceItem, Self::Action); +} + +/// General component displaying a set of items on the screen +/// and allowing the user to select one of them. +/// +/// To be used by other more specific components that will +/// supply a set of `Choice`s (through `ChoiceFactory`) +/// and will receive back the index of the selected choice. +/// +/// Each `Choice` is responsible for setting the screen - +/// choosing the button text, their duration, text displayed +/// on screen etc. +/// +/// `is_carousel` can be used to make the choice page "infinite" - +/// after reaching one end, users will appear at the other end. +pub struct ChoicePage +where + F: ChoiceFactory, + T: StringType, +{ + choices: F, + pad: Pad, + buttons: Child>, + page_counter: usize, + /// How many pixels from top should we render the items. + y_baseline: i16, + /// How many pixels are between the items. + items_distance: i16, + /// Whether the choice page is "infinite" (carousel). + is_carousel: bool, + /// Whether we should show items on left/right even when they cannot + /// be painted entirely (they would be cut off). + show_incomplete: bool, + /// Whether to show only the currently selected item, nothing left/right. + show_only_one_item: bool, + /// Whether the middle selected item should be painted with + /// inverse colors - black on white. + inverse_selected_item: bool, +} + +impl ChoicePage +where + F: ChoiceFactory, + T: StringType + Clone, +{ + pub fn new(choices: F) -> Self { + let initial_btn_layout = choices.get(0).0.btn_layout(); + + Self { + choices, + pad: Pad::with_background(theme::BG), + buttons: Child::new(ButtonController::new(initial_btn_layout)), + page_counter: 0, + y_baseline: DEFAULT_Y_BASELINE, + items_distance: DEFAULT_ITEMS_DISTANCE, + is_carousel: false, + show_incomplete: false, + show_only_one_item: false, + inverse_selected_item: false, + } + } + + /// Set the page counter at the very beginning. + /// Need to update the initial button layout. + pub fn with_initial_page_counter(mut self, page_counter: usize) -> Self { + self.page_counter = page_counter; + let initial_btn_layout = self.get_current_choice().0.btn_layout(); + self.buttons = Child::new(ButtonController::new(initial_btn_layout)); + self + } + + /// Enabling the carousel mode. + pub fn with_carousel(mut self, carousel: bool) -> Self { + self.is_carousel = carousel; + self + } + + /// Show incomplete items, even when they cannot render in their entirety. + pub fn with_incomplete(mut self, show_incomplete: bool) -> Self { + self.show_incomplete = show_incomplete; + self + } + + /// Show only the currently selected item, nothing left/right. + pub fn with_only_one_item(mut self, only_one_item: bool) -> Self { + self.show_only_one_item = only_one_item; + self + } + + /// Adjust the horizontal baseline from the top of placement. + pub fn with_y_baseline(mut self, y_baseline: i16) -> Self { + self.y_baseline = y_baseline; + self + } + + /// Adjust the distance between the items. + pub fn with_items_distance(mut self, items_distance: i16) -> Self { + self.items_distance = items_distance; + self + } + + /// Resetting the component, which enables reusing the same instance + /// for multiple choice categories. + /// + /// Used for example in passphrase, where there are multiple categories of + /// characters. + pub fn reset( + &mut self, + ctx: &mut EventCtx, + new_choices: F, + new_page_counter: Option, + is_carousel: bool, + ) { + self.choices = new_choices; + if let Some(new_counter) = new_page_counter { + self.page_counter = new_counter; + } + self.is_carousel = is_carousel; + self.update(ctx); + } + + /// Navigating to the chosen page index. + pub fn set_page_counter(&mut self, ctx: &mut EventCtx, page_counter: usize) { + self.page_counter = page_counter; + self.update(ctx); + } + + /// Display current, previous and next choices according to + /// the current ChoiceItem. + fn paint_choices(&mut self) { + let available_area = self.pad.area.split_top(self.y_baseline).0; + + // Drawing the current item in the middle. + self.show_current_choice(available_area); + + // Not drawing the rest when not wanted + if self.show_only_one_item { + return; + } + + // Getting the remaining left and right areas. + let center_width = self.get_current_choice().0.width_center(); + let (left_area, _center_area, right_area) = available_area.split_center(center_width); + + // Possibly drawing on the left side. + if self.has_previous_choice() || self.is_carousel { + self.show_left_choices(left_area); + } + + // Possibly drawing on the right side. + if self.has_next_choice() || self.is_carousel { + self.show_right_choices(right_area); + } + } + + /// Setting current buttons, and clearing. + fn update(&mut self, ctx: &mut EventCtx) { + self.set_buttons(ctx); + self.clear_and_repaint(ctx); + } + + /// Clearing the whole area and requesting repaint. + fn clear_and_repaint(&mut self, ctx: &mut EventCtx) { + self.pad.clear(); + ctx.request_paint(); + } + + /// Index of the last page. + fn last_page_index(&self) -> usize { + self.choices.count() - 1 + } + + /// Whether there is a previous choice (on the left). + pub fn has_previous_choice(&self) -> bool { + self.page_counter > 0 + } + + /// Whether there is a next choice (on the right). + pub fn has_next_choice(&self) -> bool { + self.page_counter < self.last_page_index() + } + + /// Getting the choice on the current index + pub fn get_current_choice(&self) -> (ChoiceItem, A) { + self.choices.get(self.page_counter) + } + + /// Display the current choice in the middle. + fn show_current_choice(&mut self, area: Rect) { + self.get_current_choice() + .0 + .paint_center(area, self.inverse_selected_item); + + // Color inversion is just one-time thing. + if self.inverse_selected_item { + self.inverse_selected_item = false; + } + } + + /// Display all the choices fitting on the left side. + /// Going as far as possible. + fn show_left_choices(&self, area: Rect) { + // NOTE: page index can get negative here, so having it as i16 instead of usize + let mut page_index = self.page_counter as i16 - 1; + let mut current_area = area.split_right(self.items_distance).0; + while current_area.width() > 0 { + // Breaking out of the loop if we exhausted left items + // and the carousel mode is not enabled. + if page_index < 0 { + if self.is_carousel { + // Moving to the last page. + page_index = self.last_page_index() as i16; + } else { + break; + } + } + + let (choice, _) = self.choices.get(page_index as usize); + let choice_width = choice.width_side(); + + if current_area.width() <= choice_width && !self.show_incomplete { + // early break for an item that will not fit the remaining space + break; + } + + // We need to calculate the area explicitly because we want to allow it + // to exceed the bounds of the original area. + let choice_area = Rect::from_top_right_and_size( + current_area.top_right(), + Offset::new(choice_width, current_area.height()), + ); + choice.paint_side(choice_area); + + // Updating loop variables. + current_area = current_area + .split_right(choice_width + self.items_distance) + .0; + page_index -= 1; + } + } + + /// Display all the choices fitting on the right side. + /// Going as far as possible. + fn show_right_choices(&self, area: Rect) { + let mut page_index = self.page_counter + 1; + // start with a little offset to account for the middle highlight + let mut current_area = area.split_left(self.items_distance + 3).1; + while current_area.width() > 0 { + // Breaking out of the loop if we exhausted right items + // and the carousel mode is not enabled. + if page_index > self.last_page_index() { + if self.is_carousel { + // Moving to the first page. + page_index = 0; + } else { + break; + } + } + + let (choice, _) = self.choices.get(page_index); + let choice_width = choice.width_side(); + + if current_area.width() <= choice_width && !self.show_incomplete { + // early break for an item that will not fit the remaining space + break; + } + + // We need to calculate the area explicitly because we want to allow it + // to exceed the bounds of the original area. + let choice_area = Rect::from_top_left_and_size( + current_area.top_left(), + Offset::new(choice_width, current_area.height()), + ); + choice.paint_side(choice_area); + + // Updating loop variables. + current_area = current_area + .split_left(choice_width + self.items_distance) + .1; + page_index += 1; + } + } + + /// Decrease the page counter to the previous page. + fn decrease_page_counter(&mut self) { + self.page_counter -= 1; + } + + /// Advance page counter to the next page. + fn increase_page_counter(&mut self) { + self.page_counter += 1; + } + + /// Set page to the first one. + fn page_counter_to_zero(&mut self) { + self.page_counter = 0; + } + + /// Set page to the last one. + fn page_counter_to_max(&mut self) { + self.page_counter = self.last_page_index(); + } + + /// Get current page counter. + pub fn page_index(&self) -> usize { + self.page_counter + } + + /// Updating the visual state of the buttons after each event. + /// All three buttons are handled based upon the current choice. + /// If defined in the current choice, setting their text, + /// whether they are long-pressed, and painting them. + fn set_buttons(&mut self, ctx: &mut EventCtx) { + let btn_layout = self.get_current_choice().0.btn_layout(); + self.buttons.mutate(ctx, |_ctx, buttons| { + buttons.set(btn_layout); + }); + } + + pub fn choice_factory(&self) -> &F { + &self.choices + } +} + +impl Component for ChoicePage +where + F: ChoiceFactory, + T: StringType + Clone, +{ + type Msg = A; + + fn place(&mut self, bounds: Rect) -> Rect { + let (content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT); + self.pad.place(content_area); + self.buttons.place(button_area); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + let button_event = self.buttons.event(ctx, event); + + // Button was "triggered" - released. Doing the appropriate action. + if let Some(ButtonControllerMsg::Triggered(pos)) = button_event { + match pos { + ButtonPos::Left => { + if self.has_previous_choice() { + // Clicked BACK. Decrease the page counter. + self.decrease_page_counter(); + self.update(ctx); + } else if self.is_carousel { + // In case of carousel going to the right end. + self.page_counter_to_max(); + self.update(ctx); + } + } + ButtonPos::Right => { + if self.has_next_choice() { + // Clicked NEXT. Increase the page counter. + self.increase_page_counter(); + self.update(ctx); + } else if self.is_carousel { + // In case of carousel going to the left end. + self.page_counter_to_zero(); + self.update(ctx); + } + } + ButtonPos::Middle => { + // Clicked SELECT. Send current choice index + self.clear_and_repaint(ctx); + return Some(self.get_current_choice().1); + } + } + }; + // The middle button was "pressed", highlighting the current choice by color + // inversion. + if let Some(ButtonControllerMsg::Pressed(ButtonPos::Middle)) = button_event { + self.inverse_selected_item = true; + self.clear_and_repaint(ctx); + }; + None + } + + fn paint(&mut self) { + self.pad.paint(); + self.buttons.paint(); + self.paint_choices(); + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for ChoicePage +where + F: ChoiceFactory, + T: StringType + Clone, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("ChoicePage"); + t.int("active_page", self.page_counter as i64); + t.int("page_count", self.choices.count() as i64); + t.bool("is_carousel", self.is_carousel); + + if self.has_previous_choice() { + t.child("prev_choice", &self.choices.get(self.page_counter - 1).0); + } else if self.is_carousel { + // In case of carousel going to the left end. + t.child("prev_choice", &self.choices.get(self.last_page_index()).0); + } + + t.child("current_choice", &self.choices.get(self.page_counter).0); + + if self.has_next_choice() { + t.child("next_choice", &self.choices.get(self.page_counter + 1).0); + } else if self.is_carousel { + // In case of carousel going to the very left. + t.child("next_choice", &self.choices.get(0).0); + } + + t.child("buttons", &self.buttons); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/choice_item.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/choice_item.rs new file mode 100644 index 000000000..ebcd35841 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/choice_item.rs @@ -0,0 +1,183 @@ +use crate::{ + strutil::{ShortString, StringType}, + ui::{ + display::{self, rect_fill, rect_fill_corners, rect_outline_rounded, Font, Icon}, + geometry::{Offset, Rect, BOTTOM_LEFT}, + }, +}; + +use heapless::String; + +use super::super::{theme, ButtonDetails, ButtonLayout, Choice}; + +const ICON_RIGHT_PADDING: i16 = 2; + +/// Simple string component used as a choice item. +#[derive(Clone)] +pub struct ChoiceItem { + text: ShortString, + icon: Option, + btn_layout: ButtonLayout, + font: Font, +} + +impl ChoiceItem { + pub fn new>(text: U, btn_layout: ButtonLayout) -> Self { + Self { + text: String::from(text.as_ref()), + icon: None, + btn_layout, + font: theme::FONT_CHOICE_ITEMS, + } + } + + /// Allows to add the icon. + pub fn with_icon(mut self, icon: Icon) -> Self { + self.icon = Some(icon); + self + } + + /// Allows to change the font. + pub fn with_font(mut self, font: Font) -> Self { + self.font = font; + self + } + + /// Setting left button. + pub fn set_left_btn(&mut self, btn_left: Option>) { + self.btn_layout.btn_left = btn_left; + } + + /// Setting middle button. + pub fn set_middle_btn(&mut self, btn_middle: Option>) { + self.btn_layout.btn_middle = btn_middle; + } + + /// Setting right button. + pub fn set_right_btn(&mut self, btn_right: Option>) { + self.btn_layout.btn_right = btn_right; + } + + /// Changing the text. + pub fn set_text(&mut self, text: ShortString) { + self.text = text; + } + + fn side_text(&self) -> Option<&str> { + if self.icon.is_some() { + None + } else { + Some(self.text.as_ref()) + } + } + + pub fn content(&self) -> &str { + self.text.as_ref() + } +} + +impl Choice for ChoiceItem +where + T: StringType + Clone, +{ + /// Painting the item as the main choice in the middle. + /// Showing both the icon and text, if the icon is available. + fn paint_center(&self, area: Rect, inverse: bool) { + let width = text_icon_width(Some(self.text.as_ref()), self.icon, self.font); + paint_rounded_highlight(area, Offset::new(width, self.font.text_height()), inverse); + paint_text_icon( + area, + width, + Some(self.text.as_ref()), + self.icon, + self.font, + inverse, + ); + } + + /// Getting the overall width in pixels when displayed in center. + /// That means both the icon and text will be shown. + fn width_center(&self) -> i16 { + text_icon_width(Some(self.text.as_ref()), self.icon, self.font) + } + + /// Getting the non-central width in pixels. + /// It will show an icon if defined, otherwise the text, not both. + fn width_side(&self) -> i16 { + text_icon_width(self.side_text(), self.icon, self.font) + } + + /// Painting smaller version of the item on the side. + fn paint_side(&self, area: Rect) { + let width = text_icon_width(self.side_text(), self.icon, self.font); + paint_text_icon(area, width, self.side_text(), self.icon, self.font, false); + } + + /// Getting current button layout. + fn btn_layout(&self) -> ButtonLayout { + self.btn_layout.clone() + } +} + +fn paint_rounded_highlight(area: Rect, size: Offset, inverse: bool) { + let bound = theme::BUTTON_OUTLINE; + let left_bottom = area.bottom_center() + Offset::new(-size.x / 2 - bound, bound + 1); + let x_size = size.x + 2 * bound; + let y_size = size.y + 2 * bound; + let outline_size = Offset::new(x_size, y_size); + let outline = Rect::from_bottom_left_and_size(left_bottom, outline_size); + if inverse { + rect_fill(outline, theme::FG); + rect_fill_corners(outline, theme::BG); + } else { + rect_outline_rounded(outline, theme::FG, theme::BG, 1); + } +} + +fn text_icon_width(text: Option<&str>, icon: Option, font: Font) -> i16 { + match (text, icon) { + (Some(text), Some(icon)) => { + icon.toif.width() + ICON_RIGHT_PADDING + font.visible_text_width(text) + } + (Some(text), None) => font.visible_text_width(text), + (None, Some(icon)) => icon.toif.width(), + (None, None) => 0, + } +} + +fn paint_text_icon( + area: Rect, + width: i16, + text: Option<&str>, + icon: Option, + font: Font, + inverse: bool, +) { + let fg_color = if inverse { theme::BG } else { theme::FG }; + let bg_color = if inverse { theme::FG } else { theme::BG }; + + let mut baseline = area.bottom_center() - Offset::x(width / 2); + if let Some(icon) = icon { + let height_diff = font.text_height() - icon.toif.height(); + let vertical_offset = Offset::y(-height_diff / 2); + icon.draw(baseline + vertical_offset, BOTTOM_LEFT, fg_color, bg_color); + baseline = baseline + Offset::x(icon.toif.width() + ICON_RIGHT_PADDING); + } + + if let Some(text) = text { + // Possibly shifting the baseline left, when there is a text bearing. + // This is to center the text properly. + baseline = baseline - Offset::x(font.start_x_bearing(text)); + display::text_left(baseline, text, font, fg_color, bg_color); + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for ChoiceItem { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("ChoiceItem"); + t.string("content", self.text.as_ref()); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/mod.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/mod.rs new file mode 100644 index 000000000..e3c8cc649 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/mod.rs @@ -0,0 +1,8 @@ +pub mod choice; +pub mod choice_item; + +pub mod number_input; +pub mod passphrase; +pub mod pin; +pub mod simple_choice; +pub mod wordlist; diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/number_input.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/number_input.rs new file mode 100644 index 000000000..07ba36dc9 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/number_input.rs @@ -0,0 +1,99 @@ +use crate::{ + strutil::StringType, + ui::{ + component::{Component, Event, EventCtx}, + geometry::Rect, + }, +}; + +use super::super::{ButtonLayout, ChoiceFactory, ChoiceItem, ChoicePage}; +use heapless::String; + +struct ChoiceFactoryNumberInput { + min: u32, + max: u32, +} + +impl ChoiceFactoryNumberInput { + fn new(min: u32, max: u32) -> Self { + Self { min, max } + } +} + +impl ChoiceFactory for ChoiceFactoryNumberInput { + type Action = u32; + + fn count(&self) -> usize { + (self.max - self.min + 1) as usize + } + + fn get(&self, choice_index: usize) -> (ChoiceItem, Self::Action) { + let num = self.min + choice_index as u32; + let text: String<10> = String::from(num); + let mut choice_item = ChoiceItem::new(text, ButtonLayout::default_three_icons()); + + // Disabling prev/next buttons for the first/last choice. + // (could be done to the same button if there is only one) + if choice_index == 0 { + choice_item.set_left_btn(None); + } + if choice_index == >::count(self) - 1 { + choice_item.set_right_btn(None); + } + + (choice_item, num) + } +} + +/// Simple wrapper around `ChoicePage` that allows for +/// inputting a list of values and receiving the chosen one. +pub struct NumberInput { + choice_page: ChoicePage, + min: u32, +} + +impl NumberInput +where + T: StringType + Clone, +{ + pub fn new(min: u32, max: u32, init_value: u32) -> Self { + let choices = ChoiceFactoryNumberInput::new(min, max); + let initial_page = init_value - min; + Self { + min, + choice_page: ChoicePage::new(choices).with_initial_page_counter(initial_page as usize), + } + } +} + +impl Component for NumberInput +where + T: StringType + Clone, +{ + type Msg = u32; + + fn place(&mut self, bounds: Rect) -> Rect { + self.choice_page.place(bounds) + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + self.choice_page.event(ctx, event) + } + + fn paint(&mut self) { + self.choice_page.paint(); + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for NumberInput +where + T: StringType + Clone, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("NumberInput"); + t.child("choice_page", &self.choice_page); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/passphrase.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/passphrase.rs new file mode 100644 index 000000000..728cb0398 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/passphrase.rs @@ -0,0 +1,406 @@ +use crate::{ + strutil::StringType, + ui::{ + component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx}, + display::Icon, + geometry::Rect, + model_tr::layout::CancelConfirmMsg, + util::char_to_string, + }, +}; + +use heapless::String; + +use super::super::{ + theme, ButtonDetails, ButtonLayout, ChangingTextLine, ChoiceFactory, ChoiceItem, ChoicePage, +}; + +/// Defines the choices currently available on the screen +#[derive(PartialEq, Clone, Copy)] +enum ChoiceCategory { + Menu, + LowercaseLetter, + UppercaseLetter, + Digit, + SpecialSymbol, +} + +const MAX_PASSPHRASE_LENGTH: usize = 50; + +const DIGITS: &str = "0123456789"; +const LOWERCASE_LETTERS: &str = "abcdefghijklmnopqrstuvwxyz"; +const UPPERCASE_LETTERS: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +const SPECIAL_SYMBOLS: &str = "_<>.:@/|\\!()+%&-[]?{},\'`\"~$^=*#"; + +const MENU_LENGTH: usize = 8; +const SHOW_INDEX: usize = 0; +const CANCEL_DELETE_INDEX: usize = 1; +const ENTER_INDEX: usize = 2; +const LOWERCASE_INDEX: usize = 3; +const UPPERCASE_INDEX: usize = 4; +const DIGITS_INDEX: usize = 5; +const SPECIAL_INDEX: usize = 6; +const SPACE_INDEX: usize = 7; + +// Menu text, action, icon data, middle button with CONFIRM +const MENU: [(&str, PassphraseAction, Option, bool); MENU_LENGTH] = [ + ("SHOW", PassphraseAction::Show, Some(theme::ICON_EYE), true), + ( + "CANCEL_OR_DELETE", // will be chosen dynamically + PassphraseAction::CancelOrDelete, + None, + true, + ), + ( + "ENTER", + PassphraseAction::Enter, + Some(theme::ICON_TICK), + true, + ), + ( + "abc", + PassphraseAction::Category(ChoiceCategory::LowercaseLetter), + None, + false, + ), + ( + "ABC", + PassphraseAction::Category(ChoiceCategory::UppercaseLetter), + None, + false, + ), + ( + "123", + PassphraseAction::Category(ChoiceCategory::Digit), + None, + false, + ), + ( + "#$!", + PassphraseAction::Category(ChoiceCategory::SpecialSymbol), + None, + false, + ), + ( + "SPACE", + PassphraseAction::Character(' '), + Some(theme::ICON_SPACE), + false, + ), +]; + +#[derive(Clone, Copy)] +enum PassphraseAction { + Menu, + Show, + CancelOrDelete, + Enter, + Category(ChoiceCategory), + Character(char), +} + +/// Get a character at a specified index for a specified category. +fn get_char(current_category: &ChoiceCategory, index: usize) -> char { + let group = match current_category { + ChoiceCategory::LowercaseLetter => LOWERCASE_LETTERS, + ChoiceCategory::UppercaseLetter => UPPERCASE_LETTERS, + ChoiceCategory::Digit => DIGITS, + ChoiceCategory::SpecialSymbol => SPECIAL_SYMBOLS, + ChoiceCategory::Menu => unreachable!(), + }; + unwrap!(group.chars().nth(index)) +} + +/// Return category from menu based on page index. +fn get_category_from_menu(page_index: usize) -> ChoiceCategory { + match page_index { + LOWERCASE_INDEX => ChoiceCategory::LowercaseLetter, + UPPERCASE_INDEX => ChoiceCategory::UppercaseLetter, + DIGITS_INDEX => ChoiceCategory::Digit, + SPECIAL_INDEX => ChoiceCategory::SpecialSymbol, + _ => unreachable!(), + } +} + +/// How many choices are available for a specified category. +/// (does not count the extra MENU choice for characters) +fn get_category_length(current_category: &ChoiceCategory) -> usize { + match current_category { + ChoiceCategory::LowercaseLetter => LOWERCASE_LETTERS.len(), + ChoiceCategory::UppercaseLetter => UPPERCASE_LETTERS.len(), + ChoiceCategory::Digit => DIGITS.len(), + ChoiceCategory::SpecialSymbol => SPECIAL_SYMBOLS.len(), + ChoiceCategory::Menu => MENU.len(), + } +} + +/// Whether this index is the MENU index - the last one in the list. +fn is_menu_choice(current_category: &ChoiceCategory, page_index: usize) -> bool { + if let ChoiceCategory::Menu = current_category { + unreachable!() + } + let category_length = get_category_length(current_category); + page_index == category_length +} + +struct ChoiceFactoryPassphrase { + current_category: ChoiceCategory, + /// Used to either show DELETE or CANCEL + is_empty: bool, +} + +impl ChoiceFactoryPassphrase { + fn new(current_category: ChoiceCategory, is_empty: bool) -> Self { + Self { + current_category, + is_empty, + } + } + + /// MENU choices with accept and cancel hold-to-confirm side buttons. + fn get_menu_item( + &self, + choice_index: usize, + ) -> (ChoiceItem, PassphraseAction) { + // More options for CANCEL/DELETE button + let (mut text, action, mut icon, show_confirm) = MENU[choice_index]; + if matches!(action, PassphraseAction::CancelOrDelete) { + if self.is_empty { + text = "CANCEL"; + icon = Some(theme::ICON_CANCEL); + } else { + text = "DELETE"; + icon = Some(theme::ICON_DELETE); + } + } + + let mut menu_item = ChoiceItem::new(text, ButtonLayout::default_three_icons()); + + // Action buttons have different middle button text + if show_confirm { + let confirm_btn = ButtonDetails::armed_text("CONFIRM".into()); + menu_item.set_middle_btn(Some(confirm_btn)); + } + + if let Some(icon) = icon { + menu_item = menu_item.with_icon(icon); + } + (menu_item, action) + } + + /// Character choices with a BACK to MENU choice at the end (visible from + /// start) to return back + fn get_character_item( + &self, + choice_index: usize, + ) -> (ChoiceItem, PassphraseAction) { + if is_menu_choice(&self.current_category, choice_index) { + ( + ChoiceItem::new("BACK", ButtonLayout::arrow_armed_arrow("RETURN".into())) + .with_icon(theme::ICON_ARROW_BACK_UP), + PassphraseAction::Menu, + ) + } else { + let ch = get_char(&self.current_category, choice_index); + ( + ChoiceItem::new(char_to_string(ch), ButtonLayout::default_three_icons()), + PassphraseAction::Character(ch), + ) + } + } +} + +impl ChoiceFactory for ChoiceFactoryPassphrase { + type Action = PassphraseAction; + + fn count(&self) -> usize { + let length = get_category_length(&self.current_category); + // All non-MENU categories have an extra item for returning back to MENU + match self.current_category { + ChoiceCategory::Menu => length, + _ => length + 1, + } + } + fn get(&self, choice_index: usize) -> (ChoiceItem, Self::Action) { + match self.current_category { + ChoiceCategory::Menu => self.get_menu_item(choice_index), + _ => self.get_character_item(choice_index), + } + } +} + +/// Component for entering a passphrase. +pub struct PassphraseEntry { + choice_page: ChoicePage, + passphrase_dots: Child>>, + show_plain_passphrase: bool, + textbox: TextBox, + current_category: ChoiceCategory, + menu_position: usize, // position in the menu so we can return back +} + +impl PassphraseEntry +where + T: StringType + Clone, +{ + pub fn new() -> Self { + Self { + choice_page: ChoicePage::new(ChoiceFactoryPassphrase::new(ChoiceCategory::Menu, true)) + .with_carousel(true) + .with_initial_page_counter(LOWERCASE_INDEX), + passphrase_dots: Child::new(ChangingTextLine::center_mono(String::new())), + show_plain_passphrase: false, + textbox: TextBox::empty(), + current_category: ChoiceCategory::Menu, + menu_position: 0, + } + } + + fn update_passphrase_dots(&mut self, ctx: &mut EventCtx) { + let text_to_show = if self.show_plain_passphrase { + String::from(self.passphrase()) + } else { + let mut dots: String = String::new(); + for _ in 0..self.textbox.len() { + unwrap!(dots.push_str("*")); + } + dots + }; + self.passphrase_dots.mutate(ctx, |ctx, passphrase_dots| { + passphrase_dots.update_text(text_to_show); + passphrase_dots.request_complete_repaint(ctx); + }); + } + + fn append_char(&mut self, ctx: &mut EventCtx, ch: char) { + self.textbox.append(ctx, ch); + } + + fn delete_last_digit(&mut self, ctx: &mut EventCtx) { + self.textbox.delete_last(ctx); + } + + /// Displaying the MENU + fn show_menu_page(&mut self, ctx: &mut EventCtx) { + let menu_choices = ChoiceFactoryPassphrase::new(ChoiceCategory::Menu, self.is_empty()); + // Going back to the last MENU position before showing the MENU + self.choice_page + .reset(ctx, menu_choices, Some(self.menu_position), true); + } + + /// Displaying the character category + fn show_category_page(&mut self, ctx: &mut EventCtx) { + let category_choices = ChoiceFactoryPassphrase::new(self.current_category, self.is_empty()); + self.choice_page.reset(ctx, category_choices, Some(0), true); + } + + pub fn passphrase(&self) -> &str { + self.textbox.content() + } + + fn is_empty(&self) -> bool { + self.textbox.is_empty() + } + + fn is_full(&self) -> bool { + self.textbox.is_full() + } +} + +impl Component for PassphraseEntry +where + T: StringType + Clone, +{ + type Msg = CancelConfirmMsg; + + fn place(&mut self, bounds: Rect) -> Rect { + let passphrase_area_height = self.passphrase_dots.inner().needed_height(); + let (passphrase_area, choice_area) = bounds.split_top(passphrase_area_height); + self.passphrase_dots.place(passphrase_area); + self.choice_page.place(choice_area); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + // Any event when showing real passphrase should hide it + if self.show_plain_passphrase { + self.show_plain_passphrase = false; + self.update_passphrase_dots(ctx); + } + + if let Some(action) = self.choice_page.event(ctx, event) { + match action { + PassphraseAction::CancelOrDelete => { + if self.is_empty() { + return Some(CancelConfirmMsg::Cancelled); + } else { + self.delete_last_digit(ctx); + self.update_passphrase_dots(ctx); + if self.is_empty() { + // Allowing for DELETE/CANCEL change + self.menu_position = CANCEL_DELETE_INDEX; + self.show_menu_page(ctx); + } + ctx.request_paint(); + } + } + PassphraseAction::Enter => { + return Some(CancelConfirmMsg::Confirmed); + } + PassphraseAction::Show => { + self.show_plain_passphrase = true; + self.update_passphrase_dots(ctx); + ctx.request_paint(); + } + PassphraseAction::Category(category) => { + self.menu_position = self.choice_page.page_index(); + self.current_category = category; + self.show_category_page(ctx); + ctx.request_paint(); + } + PassphraseAction::Menu => { + self.current_category = ChoiceCategory::Menu; + self.show_menu_page(ctx); + ctx.request_paint(); + } + PassphraseAction::Character(ch) if !self.is_full() => { + self.append_char(ctx, ch); + self.update_passphrase_dots(ctx); + ctx.request_paint(); + } + _ => {} + } + } + + None + } + + fn paint(&mut self) { + self.passphrase_dots.paint(); + self.choice_page.paint(); + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for PassphraseEntry +where + T: StringType + Clone, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("PassphraseKeyboard"); + t.string("passphrase", self.textbox.content()); + t.string( + "current_category", + match self.current_category { + ChoiceCategory::Menu => "MENU", + ChoiceCategory::LowercaseLetter => MENU[LOWERCASE_INDEX].0, + ChoiceCategory::UppercaseLetter => MENU[UPPERCASE_INDEX].0, + ChoiceCategory::Digit => MENU[DIGITS_INDEX].0, + ChoiceCategory::SpecialSymbol => MENU[SPECIAL_INDEX].0, + }, + ); + t.child("choice_page", &self.choice_page); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/pin.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/pin.rs new file mode 100644 index 000000000..53deea419 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/pin.rs @@ -0,0 +1,233 @@ +use crate::{ + strutil::StringType, + trezorhal::random, + ui::{ + component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx}, + display::Icon, + geometry::Rect, + model_tr::layout::CancelConfirmMsg, + }, +}; + +use super::super::{ + theme, ButtonDetails, ButtonLayout, ChangingTextLine, ChoiceFactory, ChoiceItem, ChoicePage, +}; +use heapless::String; + +#[derive(Clone, Copy)] +enum PinAction { + Delete, + Show, + Enter, + Digit(char), +} + +const MAX_PIN_LENGTH: usize = 50; + +const CHOICE_LENGTH: usize = 13; +const NUMBER_START_INDEX: usize = 3; +const CHOICES: [(&str, PinAction, Option); CHOICE_LENGTH] = [ + ("DELETE", PinAction::Delete, Some(theme::ICON_DELETE)), + ("SHOW", PinAction::Show, Some(theme::ICON_EYE)), + ("ENTER", PinAction::Enter, Some(theme::ICON_TICK)), + ("0", PinAction::Digit('0'), None), + ("1", PinAction::Digit('1'), None), + ("2", PinAction::Digit('2'), None), + ("3", PinAction::Digit('3'), None), + ("4", PinAction::Digit('4'), None), + ("5", PinAction::Digit('5'), None), + ("6", PinAction::Digit('6'), None), + ("7", PinAction::Digit('7'), None), + ("8", PinAction::Digit('8'), None), + ("9", PinAction::Digit('9'), None), +]; + +struct ChoiceFactoryPIN; + +impl ChoiceFactory for ChoiceFactoryPIN { + type Action = PinAction; + + fn get(&self, choice_index: usize) -> (ChoiceItem, Self::Action) { + let (choice_str, action, icon) = CHOICES[choice_index]; + + let mut choice_item = ChoiceItem::new(choice_str, ButtonLayout::default_three_icons()); + + // Action buttons have different middle button text + if !matches!(action, PinAction::Digit(_)) { + let confirm_btn = ButtonDetails::armed_text("CONFIRM".into()); + choice_item.set_middle_btn(Some(confirm_btn)); + } + + // Adding icons for appropriate items + if let Some(icon) = icon { + choice_item = choice_item.with_icon(icon); + } + + (choice_item, action) + } + + fn count(&self) -> usize { + CHOICE_LENGTH + } +} + +/// Component for entering a PIN. +pub struct PinEntry { + choice_page: ChoicePage, + pin_line: Child>>, + subprompt_line: Child>, + prompt: T, + show_real_pin: bool, + textbox: TextBox, +} + +impl PinEntry +where + T: StringType + Clone, +{ + pub fn new(prompt: T, subprompt: T) -> Self { + let choices = ChoiceFactoryPIN; + + Self { + // Starting at the digit 0 + choice_page: ChoicePage::new(choices) + .with_initial_page_counter(NUMBER_START_INDEX) + .with_carousel(true), + pin_line: Child::new(ChangingTextLine::center_bold(String::from(prompt.as_ref()))), + subprompt_line: Child::new(ChangingTextLine::center_mono(subprompt)), + prompt, + show_real_pin: false, + textbox: TextBox::empty(), + } + } + + /// Performs overall update of the screen. + fn update(&mut self, ctx: &mut EventCtx) { + self.update_header_info(ctx); + ctx.request_paint(); + } + + /// Update the header information - (sub)prompt and visible PIN. + /// If PIN is empty, showing prompt in `pin_line` and sub-prompt in the + /// `subprompt_line`. Otherwise disabling the `subprompt_line` and showing + /// the PIN - either in real numbers or masked in asterisks. + fn update_header_info(&mut self, ctx: &mut EventCtx) { + let show_prompts = self.is_empty(); + + let text = if show_prompts { + String::from(self.prompt.as_ref()) + } else if self.show_real_pin { + String::from(self.pin()) + } else { + let mut dots: String = String::new(); + for _ in 0..self.textbox.len() { + unwrap!(dots.push_str("*")); + } + dots + }; + + // Force repaint of the whole header. + // Putting the current text into the PIN line. + self.pin_line.mutate(ctx, |ctx, pin_line| { + pin_line.update_text(text); + pin_line.request_complete_repaint(ctx); + }); + // Showing subprompt only conditionally. + self.subprompt_line.mutate(ctx, |ctx, subprompt_line| { + subprompt_line.show_or_not(show_prompts); + subprompt_line.request_complete_repaint(ctx); + }); + } + + pub fn pin(&self) -> &str { + self.textbox.content() + } + + fn is_full(&self) -> bool { + self.textbox.is_full() + } + + fn is_empty(&self) -> bool { + self.textbox.is_empty() + } +} + +impl Component for PinEntry +where + T: StringType + Clone, +{ + type Msg = CancelConfirmMsg; + + fn place(&mut self, bounds: Rect) -> Rect { + let pin_height = self.pin_line.inner().needed_height(); + let subtitle_height = self.subprompt_line.inner().needed_height(); + let (title_area, subtitle_and_choice_area) = bounds.split_top(pin_height); + let (subtitle_area, choice_area) = subtitle_and_choice_area.split_top(subtitle_height); + self.pin_line.place(title_area); + self.subprompt_line.place(subtitle_area); + self.choice_page.place(choice_area); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + // Any event when showing real PIN should hide it + if self.show_real_pin { + self.show_real_pin = false; + self.update(ctx) + } + + match self.choice_page.event(ctx, event) { + Some(PinAction::Delete) => { + self.textbox.delete_last(ctx); + self.update(ctx); + None + } + Some(PinAction::Show) => { + self.show_real_pin = true; + self.update(ctx); + None + } + Some(PinAction::Enter) => Some(CancelConfirmMsg::Confirmed), + Some(PinAction::Digit(ch)) if !self.is_full() => { + self.textbox.append(ctx, ch); + // Choosing random digit to be shown next, but different + // from the current choice. + let new_page_counter = random::uniform_between_except( + NUMBER_START_INDEX as u32, + (CHOICE_LENGTH - 1) as u32, + self.choice_page.page_index() as u32, + ); + self.choice_page + .set_page_counter(ctx, new_page_counter as usize); + self.update(ctx); + None + } + _ => None, + } + } + + fn paint(&mut self) { + self.pin_line.paint(); + self.subprompt_line.paint(); + self.choice_page.paint(); + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for PinEntry +where + T: StringType + Clone, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("PinKeyboard"); + t.string("prompt", self.prompt.as_ref()); + let subprompt = self.subprompt_line.inner().get_text(); + if !subprompt.as_ref().is_empty() { + t.string("subprompt", subprompt.as_ref()); + } + t.string("pin", self.textbox.content()); + t.child("choice_page", &self.choice_page); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/simple_choice.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/simple_choice.rs new file mode 100644 index 000000000..a4ec486a1 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/simple_choice.rs @@ -0,0 +1,133 @@ +use crate::{ + strutil::StringType, + ui::{ + component::{Component, Event, EventCtx}, + geometry::Rect, + }, +}; + +use super::super::{ButtonLayout, ChoiceFactory, ChoiceItem, ChoicePage}; +use heapless::Vec; + +// So that there is only one implementation, and not multiple generic ones +// as would be via `const N: usize` generics. +const MAX_LENGTH: usize = 5; + +struct ChoiceFactorySimple { + choices: Vec, + carousel: bool, +} + +impl ChoiceFactorySimple { + fn new(choices: Vec, carousel: bool) -> Self { + Self { choices, carousel } + } + + fn get_string(&self, choice_index: usize) -> &str { + self.choices[choice_index].as_ref() + } +} + +impl ChoiceFactory for ChoiceFactorySimple { + type Action = usize; + + fn count(&self) -> usize { + self.choices.len() + } + + fn get(&self, choice_index: usize) -> (ChoiceItem, Self::Action) { + let text = &self.choices[choice_index]; + let mut choice_item = ChoiceItem::new(text, ButtonLayout::default_three_icons()); + + // Disabling prev/next buttons for the first/last choice when not in carousel. + // (could be done to the same button if there is only one) + if !self.carousel { + if choice_index == 0 { + choice_item.set_left_btn(None); + } + if choice_index == self.count() - 1 { + choice_item.set_right_btn(None); + } + } + + (choice_item, choice_index) + } +} + +/// Simple wrapper around `ChoicePage` that allows for +/// inputting a list of values and receiving the chosen one. +pub struct SimpleChoice +where + T: StringType, +{ + choice_page: ChoicePage, T, usize>, + pub return_index: bool, +} + +impl SimpleChoice +where + T: StringType + Clone, +{ + pub fn new(str_choices: Vec, carousel: bool) -> Self { + let choices = ChoiceFactorySimple::new(str_choices, carousel); + Self { + choice_page: ChoicePage::new(choices).with_carousel(carousel), + return_index: false, + } + } + + /// Show only the currently selected item, nothing left/right. + pub fn with_only_one_item(mut self) -> Self { + self.choice_page = self.choice_page.with_only_one_item(true); + self + } + + /// Show choices even when they do not fit entirely. + pub fn with_show_incomplete(mut self) -> Self { + self.choice_page = self.choice_page.with_incomplete(true); + self + } + + /// Returning chosen page index instead of the string result. + pub fn with_return_index(mut self) -> Self { + self.return_index = true; + self + } + + /// Translating the resulting index into actual string choice. + pub fn result_by_index(&self, index: usize) -> &str { + self.choice_page.choice_factory().get_string(index) + } +} + +impl Component for SimpleChoice +where + T: StringType + Clone, +{ + type Msg = usize; + + fn place(&mut self, bounds: Rect) -> Rect { + self.choice_page.place(bounds) + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + self.choice_page.event(ctx, event) + } + + fn paint(&mut self) { + self.choice_page.paint(); + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for SimpleChoice +where + T: StringType + Clone, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("SimpleChoice"); + t.child("choice_page", &self.choice_page); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/wordlist.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/wordlist.rs new file mode 100644 index 000000000..dcbbac2ce --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/wordlist.rs @@ -0,0 +1,223 @@ +use crate::{ + strutil::StringType, + trezorhal::wordlist::Wordlist, + ui::{ + component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx}, + geometry::Rect, + util::char_to_string, + }, +}; + +use super::super::{theme, ButtonLayout, ChangingTextLine, ChoiceFactory, ChoiceItem, ChoicePage}; +use heapless::String; + +enum WordlistAction { + Letter(char), + Word(&'static str), + Delete, +} + +const MAX_WORD_LENGTH: usize = 10; +const MAX_LETTERS_LENGTH: usize = 26; + +/// Offer words when there will be fewer of them than this +const OFFER_WORDS_THRESHOLD: usize = 10; + +/// Where will be the DELETE option - at the first position +const DELETE_INDEX: usize = 0; +/// Which index will be used at the beginning. +/// (Accounts for DELETE to be at index 0) +const INITIAL_PAGE_COUNTER: usize = DELETE_INDEX + 1; + +const PROMPT: &str = "_"; + +/// Type of the wordlist, deciding the list of words to be used +#[derive(Clone, Copy)] +pub enum WordlistType { + Bip39, + Slip39, +} + +struct ChoiceFactoryWordlist { + wordlist: Wordlist, + offer_words: bool, +} + +impl ChoiceFactoryWordlist { + pub fn new(wordlist_type: WordlistType, prefix: &str) -> Self { + let wordlist = match wordlist_type { + WordlistType::Bip39 => Wordlist::bip39(), + WordlistType::Slip39 => Wordlist::slip39(), + } + .filter_prefix(prefix); + let offer_words = wordlist.len() < OFFER_WORDS_THRESHOLD; + Self { + wordlist, + offer_words, + } + } +} + +impl ChoiceFactory for ChoiceFactoryWordlist { + type Action = WordlistAction; + + fn count(&self) -> usize { + // Accounting for the DELETE option (+1) + 1 + if self.offer_words { + self.wordlist.len() + } else { + self.wordlist.get_available_letters().count() + } + } + + fn get(&self, choice_index: usize) -> (ChoiceItem, Self::Action) { + // Putting DELETE as the first option in both cases + // (is a requirement for WORDS, doing it for LETTERS as well to unite it) + if choice_index == DELETE_INDEX { + return ( + ChoiceItem::new("DELETE", ButtonLayout::arrow_armed_arrow("CONFIRM".into())) + .with_icon(theme::ICON_DELETE), + WordlistAction::Delete, + ); + } + if self.offer_words { + let word = self.wordlist.get(choice_index - 1).unwrap_or_default(); + ( + ChoiceItem::new(word, ButtonLayout::default_three_icons()), + WordlistAction::Word(word), + ) + } else { + let letter = self + .wordlist + .get_available_letters() + .nth(choice_index - 1) + .unwrap_or_default(); + ( + ChoiceItem::new(char_to_string(letter), ButtonLayout::default_three_icons()), + WordlistAction::Letter(letter), + ) + } + } +} + +/// Component for entering a mnemonic from a wordlist - BIP39 or SLIP39. +pub struct WordlistEntry { + choice_page: ChoicePage, + chosen_letters: Child>>, + textbox: TextBox, + offer_words: bool, + wordlist_type: WordlistType, +} + +impl WordlistEntry +where + T: StringType + Clone, +{ + pub fn new(wordlist_type: WordlistType) -> Self { + let choices = ChoiceFactoryWordlist::new(wordlist_type, ""); + Self { + // Starting at second page because of DELETE option + choice_page: ChoicePage::new(choices) + .with_incomplete(true) + .with_carousel(true) + .with_initial_page_counter(INITIAL_PAGE_COUNTER), + chosen_letters: Child::new(ChangingTextLine::center_mono(String::from(PROMPT))), + textbox: TextBox::empty(), + offer_words: false, + wordlist_type, + } + } + + /// Gets up-to-date choices for letters or words. + fn get_current_choices(&mut self) -> ChoiceFactoryWordlist { + // Narrowing the word list + ChoiceFactoryWordlist::new(self.wordlist_type, self.textbox.content()) + } + + /// Updates the whole page. + fn update(&mut self, ctx: &mut EventCtx) { + self.update_chosen_letters(ctx); + let new_choices = self.get_current_choices(); + self.offer_words = new_choices.offer_words; + // Not using carousel in case of words, as that looks weird in case + // there is only one word to choose from. + self.choice_page.reset( + ctx, + new_choices, + Some(INITIAL_PAGE_COUNTER), + !self.offer_words, + ); + ctx.request_paint(); + } + + /// Reflects currently chosen letters in the textbox. + fn update_chosen_letters(&mut self, ctx: &mut EventCtx) { + let text = build_string!({ MAX_WORD_LENGTH + 1 }, self.textbox.content(), PROMPT); + self.chosen_letters.mutate(ctx, |ctx, chosen_letters| { + chosen_letters.update_text(text); + chosen_letters.request_complete_repaint(ctx); + }); + } +} + +impl Component for WordlistEntry +where + T: StringType + Clone, +{ + type Msg = &'static str; + + fn place(&mut self, bounds: Rect) -> Rect { + let letters_area_height = self.chosen_letters.inner().needed_height(); + let (letters_area, choice_area) = bounds.split_top(letters_area_height); + self.chosen_letters.place(letters_area); + self.choice_page.place(choice_area); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + match self.choice_page.event(ctx, event) { + Some(WordlistAction::Delete) => { + self.textbox.delete_last(ctx); + self.update(ctx); + } + Some(WordlistAction::Letter(letter)) => { + self.textbox.append(ctx, letter); + self.update(ctx); + } + Some(WordlistAction::Word(word)) => { + return Some(word); + } + _ => {} + } + None + } + + fn paint(&mut self) { + self.chosen_letters.paint(); + self.choice_page.paint(); + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for WordlistEntry +where + T: StringType + Clone, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + match self.wordlist_type { + WordlistType::Bip39 => t.component("Bip39Entry"), + WordlistType::Slip39 => t.component("Slip39Entry"), + } + t.string("textbox", self.textbox.content()); + + if self.offer_words { + t.bool("word_choices", true); + } else { + t.bool("letter_choices", true); + } + + t.child("choice_page", &self.choice_page); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/loader.rs b/core/embed/rust/src/ui/model_tr/component/loader.rs index 7e16e2581..5846a4c1d 100644 --- a/core/embed/rust/src/ui/model_tr/component/loader.rs +++ b/core/embed/rust/src/ui/model_tr/component/loader.rs @@ -1,13 +1,20 @@ use crate::{ + strutil::StringType, time::{Duration, Instant}, ui::{ animation::Animation, component::{Component, Event, EventCtx}, display::{self, Color, Font}, geometry::{Offset, Rect}, + util::animation_disabled, }, }; +use super::theme; + +pub const DEFAULT_DURATION_MS: u32 = 1000; +pub const SHRINKING_DURATION_MS: u32 = 500; + pub enum LoaderMsg { GrownCompletely, ShrunkCompletely, @@ -20,31 +27,69 @@ enum State { Grown, } -pub struct Loader { +pub struct Loader +where + T: StringType, +{ area: Rect, state: State, growing_duration: Duration, shrinking_duration: Duration, - text: display::TextOverlay<'static>, + text_overlay: display::TextOverlay, styles: LoaderStyleSheet, } -impl Loader { +impl Loader +where + T: StringType, +{ pub const SIZE: Offset = Offset::new(120, 120); - pub fn new(text: &'static str, styles: LoaderStyleSheet) -> Self { - let overlay = display::TextOverlay::new(text, styles.normal.font); - + pub fn new(text_overlay: display::TextOverlay, styles: LoaderStyleSheet) -> Self { Self { area: Rect::zero(), state: State::Initial, - growing_duration: Duration::from_millis(1000), - shrinking_duration: Duration::from_millis(500), - text: overlay, + growing_duration: Duration::from_millis(DEFAULT_DURATION_MS), + shrinking_duration: Duration::from_millis(SHRINKING_DURATION_MS), + text_overlay, styles, } } + pub fn text(text: T, styles: LoaderStyleSheet) -> Self { + let text_overlay = display::TextOverlay::new(text, styles.normal.font); + + Self::new(text_overlay, styles) + } + + pub fn with_growing_duration(mut self, growing_duration: Duration) -> Self { + self.growing_duration = growing_duration; + self + } + + /// Change the duration of the loader. + pub fn set_duration(&mut self, growing_duration: Duration) { + self.growing_duration = growing_duration; + } + + pub fn get_duration(&self) -> Duration { + self.growing_duration + } + + pub fn get_text(&self) -> &T { + self.text_overlay.get_text() + } + + /// Change the text of the loader. + pub fn set_text(&mut self, text: T) { + self.text_overlay.set_text(text); + } + + /// Return width of given text according to current style. + pub fn get_text_width(&self, text: &T) -> i16 { + self.styles.normal.font.text_width(text.as_ref()) + } + pub fn start_growing(&mut self, ctx: &mut EventCtx, now: Instant) { let mut anim = Animation::new( display::LOADER_MIN, @@ -111,27 +156,32 @@ impl Loader { matches!(self.progress(now), Some(display::LOADER_MIN)) } - pub fn paint_loader(&mut self, style: &LoaderStyle, done: i16) { - let invert_from = ((self.area.width() + 1) * done) / (display::LOADER_MAX as i16); + pub fn paint_loader(&mut self, style: &LoaderStyle, done: i32) { + // NOTE: need to calculate this in `i32`, it would overflow using `i16` + let invert_from = ((self.area.width() as i32 + 1) * done) / (display::LOADER_MAX as i32); display::bar_with_text_and_fill( self.area, - Some(self.text), + Some(&self.text_overlay), style.fg_color, style.bg_color, -1, - invert_from, + invert_from as i16, ); } } -impl Component for Loader { +impl Component for Loader +where + T: StringType, +{ type Msg = LoaderMsg; fn place(&mut self, bounds: Rect) -> Rect { self.area = bounds; - let baseline = bounds.bottom_center() + Offset::new(1, -1); - self.text.place(baseline); + // Centering the text in the loader rectangle. + let baseline = bounds.bottom_center() + Offset::new(1, -2); + self.text_overlay.place(baseline); self.area } @@ -140,18 +190,21 @@ impl Component for Loader { if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event { if self.is_animating() { - // We have something to paint, so request to be painted in the next pass. - ctx.request_paint(); - if self.is_completely_grown(now) { self.state = State::Grown; + ctx.request_paint(); return Some(LoaderMsg::GrownCompletely); } else if self.is_completely_shrunk(now) { self.state = State::Initial; + ctx.request_paint(); return Some(LoaderMsg::ShrunkCompletely); } else { // There is further progress in the animation, request an animation frame event. ctx.request_anim_frame(); + // We have something to paint, so request to be painted in the next pass. + if !animation_disabled() { + ctx.request_paint(); + } } } } @@ -169,11 +222,11 @@ impl Component for Loader { if let State::Initial = self.state { self.paint_loader(self.styles.normal, 0); } else if let State::Grown = self.state { - self.paint_loader(self.styles.normal, display::LOADER_MAX as i16); + self.paint_loader(self.styles.normal, display::LOADER_MAX as i32); } else { let progress = self.progress(now); if let Some(done) = progress { - self.paint_loader(self.styles.normal, done as i16); + self.paint_loader(self.styles.normal, done as i32); } else { self.paint_loader(self.styles.normal, 0); } @@ -191,9 +244,28 @@ pub struct LoaderStyle { pub bg_color: Color, } +impl LoaderStyleSheet { + pub fn default_loader() -> Self { + Self { + normal: &LoaderStyle { + font: theme::FONT_BUTTON, + fg_color: theme::FG, + bg_color: theme::BG, + }, + } + } +} + +// DEBUG-ONLY SECTION BELOW + #[cfg(feature = "ui_debug")] -impl crate::trace::Trace for Loader { +impl crate::trace::Trace for Loader +where + T: StringType, +{ fn trace(&self, t: &mut dyn crate::trace::Tracer) { t.component("Loader"); + t.string("text", self.get_text().as_ref()); + t.int("duration", self.get_duration().to_millis() as i64); } } diff --git a/core/embed/rust/src/ui/model_tr/component/mod.rs b/core/embed/rust/src/ui/model_tr/component/mod.rs index acab4a282..9d669d3f5 100644 --- a/core/embed/rust/src/ui/model_tr/component/mod.rs +++ b/core/embed/rust/src/ui/model_tr/component/mod.rs @@ -1,21 +1,62 @@ mod button; -mod confirm; -mod dialog; -mod frame; +mod button_controller; +mod common; +mod hold_to_confirm; +mod input_methods; mod loader; -mod page; mod result; +mod welcome_screen; + +use super::{constant, theme}; +pub use button::{ + Button, ButtonAction, ButtonActions, ButtonContent, ButtonDetails, ButtonLayout, ButtonPos, + ButtonStyle, ButtonStyleSheet, +}; +pub use button_controller::{ButtonController, ButtonControllerMsg}; +pub use hold_to_confirm::{HoldToConfirm, HoldToConfirmMsg}; +pub use input_methods::{ + choice::{Choice, ChoiceFactory, ChoicePage}, + choice_item::ChoiceItem, +}; +pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet}; +pub use result::ResultScreen; +pub use welcome_screen::WelcomeScreen; + +mod address_details; +mod changing_text; +mod coinjoin_progress; +mod flow; +mod flow_pages; +mod frame; +mod homescreen; +mod page; +mod progress; mod result_anim; mod result_popup; +mod scrollbar; +mod share_words; +mod show_more; +mod title; -use super::theme; +pub use address_details::AddressDetails; -pub use button::{Button, ButtonContent, ButtonMsg, ButtonPos, ButtonStyle, ButtonStyleSheet}; -pub use confirm::{HoldToConfirm, HoldToConfirmMsg}; -pub use dialog::{Dialog, DialogMsg}; -pub use frame::Frame; -pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet}; +pub use changing_text::ChangingTextLine; +pub use coinjoin_progress::CoinJoinProgress; +pub use flow::Flow; +pub use flow_pages::{FlowPages, Page}; +pub use frame::{Frame, ScrollableContent, ScrollableFrame}; +pub use homescreen::{Homescreen, Lockscreen}; +pub use input_methods::{ + number_input::NumberInput, + passphrase::PassphraseEntry, + pin::PinEntry, + simple_choice::SimpleChoice, + wordlist::{WordlistEntry, WordlistType}, +}; pub use page::ButtonPage; -pub use result::ResultScreen; +pub use progress::Progress; pub use result_anim::{ResultAnim, ResultAnimMsg}; pub use result_popup::{ResultPopup, ResultPopupMsg}; +pub use scrollbar::ScrollBar; +pub use share_words::ShareWords; +pub use show_more::{CancelInfoConfirmMsg, ShowMore}; diff --git a/core/embed/rust/src/ui/model_tr/component/page.rs b/core/embed/rust/src/ui/model_tr/component/page.rs index 8cb3a45a8..4d60b780c 100644 --- a/core/embed/rust/src/ui/model_tr/component/page.rs +++ b/core/embed/rust/src/ui/model_tr/component/page.rs @@ -1,153 +1,80 @@ -use crate::ui::{ - component::{Component, ComponentExt, Event, EventCtx, Never, Pad, PageMsg, Paginate}, - display::{self, Color, Font}, - geometry::{Insets, Offset, Point, Rect}, +use crate::{ + strutil::StringType, + ui::{ + component::{Child, Component, ComponentExt, Event, EventCtx, Pad, PageMsg, Paginate}, + display::Color, + geometry::{Insets, Rect}, + }, }; -use super::{theme, Button, ButtonMsg, ButtonPos}; +use super::{ + constant, frame::ScrollableContent, theme, ButtonController, ButtonControllerMsg, + ButtonDetails, ButtonLayout, ButtonPos, +}; -pub struct ButtonPage { - content: T, - scrollbar: ScrollBar, +pub struct ButtonPage +where + T: Component + Paginate, + U: StringType, +{ + page_count: usize, + active_page: usize, + content: Child, pad: Pad, - prev: Button<&'static str>, - next: Button<&'static str>, - cancel: Button<&'static str>, - confirm: Button<&'static str>, + /// Left button of the first screen + cancel_btn_details: Option>, + /// Right button of the last screen + confirm_btn_details: Option>, + /// Left button of the last page + last_back_btn_details: Option>, + /// Left button of every screen in the middle + back_btn_details: Option>, + /// Right button of every screen apart the last one + next_btn_details: Option>, + buttons: Child>, } -impl ButtonPage +impl ButtonPage where - T: Paginate, - T: Component, + T: Component + Paginate, + U: StringType + Clone, { pub fn new(content: T, background: Color) -> Self { Self { - content, - scrollbar: ScrollBar::vertical(), - pad: Pad::with_background(background), - prev: Button::with_text(ButtonPos::Left, "BACK", theme::button_cancel()), - next: Button::with_text(ButtonPos::Right, "NEXT", theme::button_default()), - cancel: Button::with_text(ButtonPos::Left, "CANCEL", theme::button_cancel()), - confirm: Button::with_text(ButtonPos::Right, "CONFIRM", theme::button_default()), - } - } - - fn change_page(&mut self, ctx: &mut EventCtx, page: usize) { - // Change the page in the content, clear the background under it and make sure - // it gets completely repainted. - self.content.change_page(page); - self.content.request_complete_repaint(ctx); - self.pad.clear(); - } -} - -impl Component for ButtonPage -where - T: Component, - T: Paginate, -{ - type Msg = PageMsg; - - fn place(&mut self, bounds: Rect) -> Rect { - let button_height = Font::BOLD.line_height() + 2; - let (content_area, button_area) = bounds.split_bottom(button_height); - let (content_area, scrollbar_area) = content_area.split_right(ScrollBar::WIDTH); - let content_area = content_area.inset(Insets::top(1)); - self.pad.place(bounds); - self.content.place(content_area); - let page_count = self.content.page_count(); - self.scrollbar.set_count_and_active_page(page_count, 0); - self.scrollbar.place(scrollbar_area); - self.prev.place(button_area); - self.next.place(button_area); - self.cancel.place(button_area); - self.confirm.place(button_area); - bounds - } - - fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - ctx.set_page_count(self.scrollbar.page_count); - if self.scrollbar.has_previous_page() { - if let Some(ButtonMsg::Clicked) = self.prev.event(ctx, event) { - // Scroll up. - self.scrollbar.go_to_previous_page(); - self.change_page(ctx, self.scrollbar.active_page); - return None; - } - } else if let Some(ButtonMsg::Clicked) = self.cancel.event(ctx, event) { - return Some(PageMsg::Controls(false)); - } - - if self.scrollbar.has_next_page() { - if let Some(ButtonMsg::Clicked) = self.next.event(ctx, event) { - // Scroll down. - self.scrollbar.go_to_next_page(); - self.change_page(ctx, self.scrollbar.active_page); - return None; - } - } else if let Some(ButtonMsg::Clicked) = self.confirm.event(ctx, event) { - return Some(PageMsg::Controls(true)); - } - - if let Some(msg) = self.content.event(ctx, event) { - return Some(PageMsg::Content(msg)); + page_count: 0, // will be set in place() + active_page: 0, + content: Child::new(content), + pad: Pad::with_background(background).with_clear(), + cancel_btn_details: Some(ButtonDetails::cancel_icon()), + confirm_btn_details: Some(ButtonDetails::text("CONFIRM".into())), + back_btn_details: Some(ButtonDetails::up_arrow_icon_wide()), + last_back_btn_details: Some(ButtonDetails::up_arrow_icon()), + next_btn_details: Some(ButtonDetails::down_arrow_icon_wide()), + // Setting empty layout for now, we do not yet know the page count. + // Initial button layout will be set in `place()` after we can call + // `content.page_count()`. + buttons: Child::new(ButtonController::new(ButtonLayout::empty())), } - None } - fn paint(&mut self) { - self.pad.paint(); - self.content.paint(); - self.scrollbar.paint(); - if self.scrollbar.has_previous_page() { - self.prev.paint(); - } else { - self.cancel.paint(); - } - if self.scrollbar.has_next_page() { - self.next.paint(); - } else { - self.confirm.paint(); - } + pub fn with_cancel_btn(mut self, btn_details: Option>) -> Self { + self.cancel_btn_details = btn_details; + self } -} -#[cfg(feature = "ui_debug")] -impl crate::trace::Trace for ButtonPage -where - T: crate::trace::Trace, -{ - fn trace(&self, t: &mut dyn crate::trace::Tracer) { - t.component("ButtonPage"); - t.int("active_page", self.scrollbar.active_page as i64); - t.int("page_count", self.scrollbar.page_count as i64); - t.child("content", &self.content); + pub fn with_confirm_btn(mut self, btn_details: Option>) -> Self { + self.confirm_btn_details = btn_details; + self } -} - -pub struct ScrollBar { - area: Rect, - page_count: usize, - active_page: usize, -} - -impl ScrollBar { - pub const WIDTH: i16 = 8; - pub const DOT_SIZE: Offset = Offset::new(4, 4); - pub const DOT_INTERVAL: i16 = 6; - pub fn vertical() -> Self { - Self { - area: Rect::zero(), - page_count: 0, - active_page: 0, - } + pub fn with_back_btn(mut self, btn_details: Option>) -> Self { + self.back_btn_details = btn_details; + self } - pub fn set_count_and_active_page(&mut self, page_count: usize, active_page: usize) { - self.page_count = page_count; - self.active_page = active_page; + pub fn with_next_btn(mut self, btn_details: Option>) -> Self { + self.next_btn_details = btn_details; + self } pub fn has_next_page(&self) -> bool { @@ -166,61 +93,153 @@ impl ScrollBar { self.active_page = self.active_page.saturating_sub(1); } - fn paint_dot(&self, active: bool, top_left: Point) { - let sides = [ - Rect::from_top_left_and_size(top_left + Offset::x(1), Offset::new(2, 1)), - Rect::from_top_left_and_size(top_left + Offset::y(1), Offset::new(1, 2)), - Rect::from_top_left_and_size( - top_left + Offset::new(1, Self::DOT_SIZE.y - 1), - Offset::new(2, 1), - ), - Rect::from_top_left_and_size( - top_left + Offset::new(Self::DOT_SIZE.x - 1, 1), - Offset::new(1, 2), - ), - ]; - for side in sides { - display::rect_fill(side, theme::FG) + /// Basically just determining whether the right button for + /// initial page should be "NEXT" or "CONFIRM". + /// Can only be called when we know the final page_count. + fn set_buttons_for_initial_page(&mut self, page_count: usize) { + let btn_layout = self.get_button_layout(false, page_count > 1); + self.buttons = Child::new(ButtonController::new(btn_layout)); + } + + /// Called when user pressed "BACK" or "NEXT". + /// Change the page in the content, clear the background under it and make + /// sure it gets completely repainted. Also updating the buttons. + fn change_page(&mut self, ctx: &mut EventCtx) { + self.content.mutate(ctx, |ctx, content| { + content.change_page(self.active_page); + content.request_complete_repaint(ctx); + }); + self.update_buttons(ctx); + self.pad.clear(); + } + + /// Reflecting the current page in the buttons. + fn update_buttons(&mut self, ctx: &mut EventCtx) { + let btn_layout = self.get_button_layout(self.has_previous_page(), self.has_next_page()); + self.buttons.mutate(ctx, |_ctx, buttons| { + buttons.set(btn_layout); + }); + } + + fn get_button_layout(&self, has_prev: bool, has_next: bool) -> ButtonLayout { + let btn_left = self.get_left_button_details(!has_prev, !has_next); + let btn_right = self.get_right_button_details(has_next); + ButtonLayout::new(btn_left, None, btn_right) + } + + /// Get the left button details, depending whether the page is first, last, + /// or in the middle. + fn get_left_button_details(&self, is_first: bool, is_last: bool) -> Option> { + if is_first { + self.cancel_btn_details.clone() + } else if is_last { + self.last_back_btn_details.clone() + } else { + self.back_btn_details.clone() } - if active { - display::rect_fill( - Rect::from_top_left_and_size(top_left, Self::DOT_SIZE).inset(Insets::uniform(1)), - theme::FG, - ) + } + + /// Get the right button details, depending on whether there is a next + /// page. + fn get_right_button_details(&self, has_next_page: bool) -> Option> { + if has_next_page { + self.next_btn_details.clone() + } else { + self.confirm_btn_details.clone() } } } -impl Component for ScrollBar { - type Msg = Never; +impl ScrollableContent for ButtonPage +where + T: Component + Paginate, + U: StringType, +{ + fn page_count(&self) -> usize { + self.page_count + } + fn active_page(&self) -> usize { + self.active_page + } +} + +impl Component for ButtonPage +where + T: Component + Paginate, + U: StringType + Clone, +{ + type Msg = PageMsg; fn place(&mut self, bounds: Rect) -> Rect { - self.area = bounds; - self.area + let (content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT); + // Pad only the content, buttons handle it themselves. + self.pad.place(content_area); + // Moving the content LINE_SPACE pixels down, otherwise the top would not be + // padded correctly + self.content + .place(content_area.inset(Insets::top(constant::LINE_SPACE))); + // Need to be called here, only after content is placed + // and we can calculate the page count. + self.page_count = self.content.page_count(); + self.set_buttons_for_initial_page(self.page_count); + self.buttons.place(button_area); + bounds } - fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + ctx.set_page_count(self.page_count()); + if let Some(ButtonControllerMsg::Triggered(pos)) = self.buttons.event(ctx, event) { + match pos { + ButtonPos::Left => { + if self.has_previous_page() { + // Clicked BACK. Scroll up. + self.go_to_previous_page(); + self.change_page(ctx); + } else { + // Clicked CANCEL. Send result. + return Some(PageMsg::Controls(false)); + } + } + ButtonPos::Right => { + if self.has_next_page() { + // Clicked NEXT. Scroll down. + self.go_to_next_page(); + self.change_page(ctx); + } else { + // Clicked CONFIRM. Send result. + return Some(PageMsg::Controls(true)); + } + } + _ => {} + } + } + + if let Some(msg) = self.content.event(ctx, event) { + return Some(PageMsg::Content(msg)); + } None } fn paint(&mut self) { - let count = self.page_count as i16; - let interval = { - let available_height = self.area.height(); - let naive_height = count * Self::DOT_INTERVAL; - if naive_height > available_height { - available_height / count - } else { - Self::DOT_INTERVAL - } - }; - let mut dot = Point::new( - self.area.center().x - Self::DOT_SIZE.x / 2, - self.area.center().y - (count / 2) * interval, - ); - for i in 0..self.page_count { - self.paint_dot(i == self.active_page, dot); - dot.y += interval - } + self.pad.paint(); + self.content.paint(); + self.buttons.paint(); + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for ButtonPage +where + T: crate::trace::Trace + Paginate + Component, + U: StringType + Clone, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("ButtonPage"); + t.int("active_page", self.active_page as i64); + t.int("page_count", self.page_count as i64); + t.child("buttons", &self.buttons); + t.child("content", &self.content); } } diff --git a/core/embed/rust/src/ui/model_tr/component/progress.rs b/core/embed/rust/src/ui/model_tr/component/progress.rs new file mode 100644 index 000000000..01594c585 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/progress.rs @@ -0,0 +1,142 @@ +use core::mem; + +use crate::{ + error::Error, + strutil::StringType, + ui::{ + component::{ + base::ComponentExt, + paginated::Paginate, + text::paragraphs::{Paragraph, Paragraphs}, + Child, Component, Event, EventCtx, Label, Never, Pad, + }, + display::{self, Font}, + geometry::Rect, + util::animation_disabled, + }, +}; + +use super::super::{constant, theme}; + +pub struct Progress +where + T: StringType, +{ + title: Child>, + value: u16, + loader_y_offset: i16, + indeterminate: bool, + description: Child>>, + description_pad: Pad, + update_description: fn(&str) -> Result, +} + +impl Progress +where + T: StringType, +{ + const AREA: Rect = constant::screen(); + + pub fn new( + title: T, + indeterminate: bool, + description: T, + update_description: fn(&str) -> Result, + ) -> Self { + Self { + title: Label::centered(title, theme::TEXT_BOLD).into_child(), + value: 0, + loader_y_offset: 0, + indeterminate, + description: Paragraphs::new( + Paragraph::new(&theme::TEXT_NORMAL, description).centered(), + ) + .into_child(), + description_pad: Pad::with_background(theme::BG), + update_description, + } + } +} + +impl Component for Progress +where + T: StringType, +{ + type Msg = Never; + + fn place(&mut self, _bounds: Rect) -> Rect { + let description_lines = 1 + self + .description + .inner() + .inner() + .content() + .as_ref() + .chars() + .filter(|c| *c == '\n') + .count() as i16; + let (title, rest) = Self::AREA.split_top(self.title.inner().max_size().y); + let (loader, description) = + rest.split_bottom(Font::NORMAL.line_height() * description_lines); + self.title.place(title); + self.loader_y_offset = loader.center().y - constant::screen().center().y; + self.description.place(description); + self.description_pad.place(description); + Self::AREA + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + if let Event::Progress(new_value, new_description) = event { + if mem::replace(&mut self.value, new_value) != new_value { + if !animation_disabled() { + ctx.request_paint(); + } + self.description.mutate(ctx, |ctx, para| { + if para.inner_mut().content().as_ref() != new_description { + let new_description = unwrap!((self.update_description)(new_description)); + para.inner_mut().update(new_description); + para.change_page(0); // Recompute bounding box. + ctx.request_paint(); + self.description_pad.clear(); + } + }); + } + } + None + } + + fn paint(&mut self) { + self.title.paint(); + if self.indeterminate { + display::loader_indeterminate( + self.value, + self.loader_y_offset, + theme::FG, + theme::BG, + None, + ); + } else { + display::loader(self.value, self.loader_y_offset, theme::FG, theme::BG, None); + } + self.description_pad.paint(); + self.description.paint(); + } + + #[cfg(feature = "ui_bounds")] + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + sink(Self::AREA); + self.title.bounds(sink); + self.description.bounds(sink); + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for Progress +where + T: StringType, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("Progress"); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/result.rs b/core/embed/rust/src/ui/model_tr/component/result.rs index 1bf69bbf3..e1a0cae88 100644 --- a/core/embed/rust/src/ui/model_tr/component/result.rs +++ b/core/embed/rust/src/ui/model_tr/component/result.rs @@ -1,11 +1,14 @@ -use crate::ui::{ - component::{ - text::paragraphs::{ParagraphStrType, ParagraphVecShort, Paragraphs}, - Child, Component, Event, EventCtx, Never, Pad, +use crate::{ + strutil::StringType, + ui::{ + component::{ + text::paragraphs::{ParagraphVecShort, Paragraphs}, + Child, Component, Event, EventCtx, Never, Pad, + }, + constant::{screen, HEIGHT, WIDTH}, + display::{Color, Icon}, + geometry::{Offset, Point, Rect, CENTER}, }, - constant::{screen, HEIGHT, WIDTH}, - display::{Color, Icon}, - geometry::{Offset, Point, Rect, CENTER}, }; pub struct ResultScreen { @@ -18,7 +21,7 @@ pub struct ResultScreen { message_bottom: Child>>, } -impl ResultScreen { +impl ResultScreen { pub fn new( fg_color: Color, bg_color: Color, @@ -46,7 +49,7 @@ impl ResultScreen { } } -impl Component for ResultScreen { +impl Component for ResultScreen { type Msg = Never; fn place(&mut self, bounds: Rect) -> Rect { diff --git a/core/embed/rust/src/ui/model_tr/component/result_anim.rs b/core/embed/rust/src/ui/model_tr/component/result_anim.rs index d0b109858..ff15da7ee 100644 --- a/core/embed/rust/src/ui/model_tr/component/result_anim.rs +++ b/core/embed/rust/src/ui/model_tr/component/result_anim.rs @@ -6,10 +6,11 @@ use crate::{ display, display::toif::Icon, geometry::Rect, - model_tr::theme, }, }; +use super::super::theme; + pub enum ResultAnimMsg { FullyGrown, } @@ -141,6 +142,8 @@ impl Component for ResultAnim { } } +// DEBUG-ONLY SECTION BELOW + #[cfg(feature = "ui_debug")] impl crate::trace::Trace for ResultAnim { fn trace(&self, t: &mut dyn crate::trace::Tracer) { diff --git a/core/embed/rust/src/ui/model_tr/component/result_popup.rs b/core/embed/rust/src/ui/model_tr/component/result_popup.rs index 4a2b18ece..08c46dab6 100644 --- a/core/embed/rust/src/ui/model_tr/component/result_popup.rs +++ b/core/embed/rust/src/ui/model_tr/component/result_popup.rs @@ -1,58 +1,61 @@ use crate::{ + strutil::StringType, time::Instant, ui::{ component::{ - text::paragraphs::{Paragraph, ParagraphStrType, Paragraphs}, + text::paragraphs::{Paragraph, Paragraphs}, Child, Component, ComponentExt, Event, EventCtx, Label, Pad, }, constant::screen, display::toif::Icon, - geometry::{Alignment, Insets, LinearPlacement, Point, Rect}, - model_tr::{ - component::{Button, ButtonMsg, ButtonPos, ResultAnim, ResultAnimMsg}, - theme, - }, + geometry::{Alignment, Insets, LinearPlacement, Rect}, }, }; +use super::{ + super::theme, ButtonController, ButtonControllerMsg, ButtonLayout, ButtonPos, ResultAnim, + ResultAnimMsg, +}; + pub enum ResultPopupMsg { Confirmed, } -pub struct ResultPopup { +pub struct ResultPopup +where + T: StringType, +{ area: Rect, pad: Pad, result_anim: Child, - headline_baseline: Point, headline: Option>, - text: Child>>, - button: Option>>, + text: Child>>, + buttons: Option>>, autoclose: bool, } const ANIM_SIZE: i16 = 18; -const BUTTON_HEIGHT: i16 = 13; const ANIM_SPACE: i16 = 11; const ANIM_POS: i16 = 32; const ANIM_POS_ADJ_HEADLINE: i16 = 10; const ANIM_POS_ADJ_BUTTON: i16 = 6; -impl ResultPopup { +impl ResultPopup +where + T: StringType + Clone, +{ pub fn new( icon: Icon, - text: S, + text: T, headline: Option<&'static str>, button_text: Option<&'static str>, ) -> Self { let p1 = Paragraphs::new(Paragraph::new(&theme::TEXT_NORMAL, text)) .with_placement(LinearPlacement::vertical().align_at_center()); - let button = button_text.map(|t| { - Child::new(Button::with_text( - ButtonPos::Right, - t, - theme::button_default(), - )) + let buttons = button_text.map(|text| { + let btn_layout = ButtonLayout::none_none_text(text.into()); + Child::new(ButtonController::new(btn_layout)) }); let mut pad = Pad::with_background(theme::BG); @@ -63,9 +66,8 @@ impl ResultPopup { pad, result_anim: Child::new(ResultAnim::new(icon)), headline: headline.map(|a| Label::new(a, Alignment::Center, theme::TEXT_BOLD)), - headline_baseline: Point::zero(), text: Child::new(p1), - button, + buttons, autoclose: false, } } @@ -78,7 +80,7 @@ impl ResultPopup { pub fn start(&mut self, ctx: &mut EventCtx) { self.text.request_complete_repaint(ctx); self.headline.request_complete_repaint(ctx); - self.button.request_complete_repaint(ctx); + self.buttons.request_complete_repaint(ctx); self.result_anim.mutate(ctx, |ctx, c| { let now = Instant::now(); c.start_growing(ctx, now); @@ -87,7 +89,10 @@ impl ResultPopup { } } -impl Component for ResultPopup { +impl Component for ResultPopup +where + T: StringType + Clone, +{ type Msg = ResultPopupMsg; fn place(&mut self, bounds: Rect) -> Rect { @@ -102,8 +107,8 @@ impl Component for ResultPopup { headline_height = h.max_size().y; anim_adjust += ANIM_POS_ADJ_HEADLINE; } - if self.button.is_some() { - button_height = BUTTON_HEIGHT; + if self.buttons.is_some() { + button_height = theme::BUTTON_HEIGHT; anim_adjust += ANIM_POS_ADJ_BUTTON; } @@ -114,7 +119,7 @@ impl Component for ResultPopup { let (text, buttons) = rest.split_bottom(button_height); self.pad.place(bounds); - self.button.place(buttons); + self.buttons.place(buttons); self.headline.place(headline); self.text.place(text); self.result_anim @@ -129,12 +134,14 @@ impl Component for ResultPopup { self.text.event(ctx, event); self.headline.event(ctx, event); - if let Some(ButtonMsg::Clicked) = self.button.event(ctx, event) { + if let Some(ButtonControllerMsg::Triggered(ButtonPos::Right)) = + self.buttons.event(ctx, event) + { button_confirmed = true; } if let Some(ResultAnimMsg::FullyGrown) = self.result_anim.event(ctx, event) { - if self.button.is_none() || self.autoclose { + if self.buttons.is_none() || self.autoclose { return Some(ResultPopupMsg::Confirmed); } } @@ -149,22 +156,27 @@ impl Component for ResultPopup { fn paint(&mut self) { self.pad.paint(); self.text.paint(); - self.button.paint(); + self.buttons.paint(); self.headline.paint(); self.result_anim.paint(); } } +// DEBUG-ONLY SECTION BELOW + #[cfg(feature = "ui_debug")] -impl crate::trace::Trace for ResultPopup { +impl crate::trace::Trace for ResultPopup +where + T: StringType + Clone, +{ fn trace(&self, t: &mut dyn crate::trace::Tracer) { t.component("ResultPopup"); t.child("text", &self.text); - if let Some(b) = self.button.as_ref() { - t.child("button", b) + if let Some(button) = &self.buttons { + t.child("button", button); } - if let Some(h) = self.headline.as_ref() { - t.child("headline", h) + if let Some(headline) = &self.headline { + t.child("headline", headline); } t.child("result_anim", &self.result_anim); } diff --git a/core/embed/rust/src/ui/model_tr/component/scrollbar.rs b/core/embed/rust/src/ui/model_tr/component/scrollbar.rs new file mode 100644 index 000000000..d394da7be --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/scrollbar.rs @@ -0,0 +1,255 @@ +use crate::ui::{ + component::{Component, Event, EventCtx, Never, Pad, Paginate}, + display, + geometry::{Offset, Point, Rect}, +}; + +use super::super::theme; + +use heapless::Vec; + +/// Scrollbar to be painted horizontally at the top right of the screen. +pub struct ScrollBar { + pad: Pad, + page_count: usize, + pub active_page: usize, +} + +/// Carrying the appearance of the scrollbar dot. +#[derive(Debug)] +enum DotType { + BigFull, // * + Big, // O + Middle, // o + Small, // . +} + +pub const SCROLLBAR_SPACE: i16 = 5; + +/// How many dots at most will there be +const MAX_DOTS: usize = 5; + +impl ScrollBar { + /// Maximum size (width/height) of a dot + pub const MAX_DOT_SIZE: i16 = 5; + /// Distance between two dots + pub const DOTS_DISTANCE: i16 = 2; + pub const DOTS_INTERVAL: i16 = Self::MAX_DOT_SIZE + Self::DOTS_DISTANCE; + pub const MAX_WIDTH: i16 = Self::dots_width(MAX_DOTS); + + pub fn new(page_count: usize) -> Self { + Self { + pad: Pad::with_background(theme::BG), + page_count, + active_page: 0, + } + } + + /// Page count will be given later as it is not available yet. + pub fn to_be_filled_later() -> Self { + Self::new(0) + } + + pub const fn dots_width(dots_shown: usize) -> i16 { + Self::DOTS_INTERVAL * dots_shown as i16 - Self::DOTS_DISTANCE + } + + /// The width the scrollbar will really occupy. + pub fn overall_width(&self) -> i16 { + let dots_shown = self.page_count.min(MAX_DOTS); + Self::dots_width(dots_shown) + } + + pub fn set_page_count(&mut self, page_count: usize) { + self.page_count = page_count; + } + + /// Create a (seemingly circular) dot given its top left point. + /// Make it full when it is active, otherwise paint just the perimeter and + /// leave center empty. + fn paint_dot(&self, dot_type: &DotType, top_right: Point) { + let full_square = + Rect::from_top_right_and_size(top_right, Offset::uniform(Self::MAX_DOT_SIZE)); + + match dot_type { + DotType::BigFull | DotType::Big => { + // FG - painting the full square + display::rect_fill(full_square, theme::FG); + + // BG - erase four corners + display::rect_fill_corners(full_square, theme::BG); + + // BG - erasing the middle when not full + if matches!(dot_type, DotType::Big) { + display::rect_fill(full_square.shrink(1), theme::BG) + } + } + DotType::Middle => { + let middle_square = full_square.shrink(1); + + // FG - painting the middle square + display::rect_fill(middle_square, theme::FG); + + // BG - erase four corners + display::rect_fill_corners(middle_square, theme::BG); + + // BG - erasing the middle + display::rect_fill(middle_square.shrink(1), theme::BG) + } + DotType::Small => { + // FG - painting the small square + display::rect_fill(full_square.shrink(2), theme::FG) + } + } + } + + /// Get a sequence of dots to be drawn, with specifying their appearance. + /// Painting only big dots in case of 2 and 3 pages, + /// three big and 1 middle in case of 4 pages, + /// and three big, one middle and one small in case of 5 and more pages. + fn get_drawable_dots(&self) -> Vec { + let mut dots = Vec::new(); + + match self.page_count { + 0..=3 => { + // *OO + // O*O + // OO* + for i in 0..self.page_count { + if i == self.active_page { + unwrap!(dots.push(DotType::BigFull)); + } else { + unwrap!(dots.push(DotType::Big)); + } + } + } + 4 => { + // *OOo + // O*Oo + // oO*O + // oOO* + match self.active_page { + 0 => unwrap!(dots.push(DotType::BigFull)), + 1 => unwrap!(dots.push(DotType::Big)), + _ => unwrap!(dots.push(DotType::Middle)), + }; + match self.active_page { + 1 => unwrap!(dots.push(DotType::BigFull)), + _ => unwrap!(dots.push(DotType::Big)), + }; + match self.active_page { + 2 => unwrap!(dots.push(DotType::BigFull)), + _ => unwrap!(dots.push(DotType::Big)), + }; + match self.active_page { + 3 => unwrap!(dots.push(DotType::BigFull)), + 2 => unwrap!(dots.push(DotType::Big)), + _ => unwrap!(dots.push(DotType::Middle)), + }; + } + _ => { + // *OOo. + // O*Oo. + // oO*Oo + // ... + // oO*Oo + // .oO*O + // .oOO* + let full_dot_index = match self.active_page { + 0 => 0, + 1 => 1, + last_but_one if last_but_one == self.page_count - 2 => 3, + last if last == self.page_count - 1 => 4, + _ => 2, + }; + match full_dot_index { + 0 => unwrap!(dots.push(DotType::BigFull)), + 1 => unwrap!(dots.push(DotType::Big)), + 2 => unwrap!(dots.push(DotType::Middle)), + _ => unwrap!(dots.push(DotType::Small)), + }; + match full_dot_index { + 0 => unwrap!(dots.push(DotType::Big)), + 1 => unwrap!(dots.push(DotType::BigFull)), + 2 => unwrap!(dots.push(DotType::Big)), + _ => unwrap!(dots.push(DotType::Middle)), + }; + match full_dot_index { + 2 => unwrap!(dots.push(DotType::BigFull)), + _ => unwrap!(dots.push(DotType::Big)), + }; + match full_dot_index { + 0 | 1 => unwrap!(dots.push(DotType::Middle)), + 3 => unwrap!(dots.push(DotType::BigFull)), + _ => unwrap!(dots.push(DotType::Big)), + }; + match full_dot_index { + 0 | 1 => unwrap!(dots.push(DotType::Small)), + 2 => unwrap!(dots.push(DotType::Middle)), + 3 => unwrap!(dots.push(DotType::Big)), + _ => unwrap!(dots.push(DotType::BigFull)), + }; + } + } + dots + } + + /// Drawing the dots horizontally and aligning to the right. + fn paint_horizontal(&mut self) { + let mut top_right = self.pad.area.top_right(); + for dot in self.get_drawable_dots().iter().rev() { + self.paint_dot(dot, top_right); + top_right.x -= Self::DOTS_INTERVAL; + } + } +} + +impl Component for ScrollBar { + type Msg = Never; + + fn place(&mut self, bounds: Rect) -> Rect { + // Occupying as little space as possible (according to the number of pages), + // aligning to the right. + let scrollbar_area = Rect::from_top_right_and_size( + bounds.top_right() + Offset::y(1), // offset for centering vertically + Offset::new(self.overall_width(), Self::MAX_DOT_SIZE), + ); + self.pad.place(scrollbar_area); + scrollbar_area + } + + fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { + None + } + + /// Displaying one dot for each page. + fn paint(&mut self) { + // Not showing the scrollbar dot when there is only one page + if self.page_count <= 1 { + return; + } + + self.pad.clear(); + self.pad.paint(); + self.paint_horizontal(); + } +} + +impl Paginate for ScrollBar { + fn page_count(&mut self) -> usize { + self.page_count + } + + fn change_page(&mut self, active_page: usize) { + self.active_page = active_page; + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for ScrollBar { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("ScrollBar"); + t.int("scrollbar_page_count", self.page_count as i64); + t.int("scrollbar_active_page", self.active_page as i64); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/share_words.rs b/core/embed/rust/src/ui/model_tr/component/share_words.rs new file mode 100644 index 000000000..1dedd657b --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/share_words.rs @@ -0,0 +1,242 @@ +use crate::{ + strutil::StringType, + ui::{ + component::{ + text::util::text_multiline_split_words, Child, Component, Event, EventCtx, Never, + Paginate, + }, + display::Font, + geometry::{Alignment, Offset, Rect}, + }, +}; + +use heapless::{String, Vec}; + +use super::{common::display, scrollbar::SCROLLBAR_SPACE, theme, title::Title, ScrollBar}; + +const WORDS_PER_PAGE: usize = 3; +const EXTRA_LINE_HEIGHT: i16 = 2; +const NUMBER_X_OFFSET: i16 = 5; +const NUMBER_WORD_OFFSET: i16 = 20; +const NUMBER_FONT: Font = Font::DEMIBOLD; +const WORD_FONT: Font = Font::NORMAL; +const INFO_TOP_OFFSET: i16 = 15; +const MAX_WORDS: usize = 33; // super-shamir has 33 words, all other have less + +/// Showing the given share words. +pub struct ShareWords +where + T: StringType, +{ + area: Rect, + title: Child>, + scrollbar: Child, + share_words: Vec, + page_index: usize, +} + +impl ShareWords +where + T: StringType + Clone, +{ + pub fn new(title: T, share_words: Vec) -> Self { + let mut instance = Self { + area: Rect::zero(), + title: Child::new(Title::new(title)), + scrollbar: Child::new(ScrollBar::to_be_filled_later()), + share_words, + page_index: 0, + }; + let page_count = instance.total_page_count(); + let scrollbar = ScrollBar::new(page_count); + instance.scrollbar = Child::new(scrollbar); + instance + } + + fn word_index(&self) -> usize { + (self.page_index - 2) * WORDS_PER_PAGE + } + + fn is_entry_page(&self) -> bool { + self.page_index == 0 + } + + fn is_second_page(&self) -> bool { + self.page_index == 1 + } + + fn is_final_page(&self) -> bool { + self.page_index == self.total_page_count() - 1 + } + + fn total_page_count(&self) -> usize { + let word_screens = if self.share_words.len() % WORDS_PER_PAGE == 0 { + self.share_words.len() / WORDS_PER_PAGE + } else { + self.share_words.len() / WORDS_PER_PAGE + 1 + }; + // Two pages before the words, one after it + 2 + word_screens + 1 + } + + fn get_first_text(&self) -> String<100> { + build_string!( + 100, + "Write all ", + inttostr!(self.share_words.len() as u8), + " words in order on recovery seed card." + ) + } + + /// Display the first page with user information. + fn paint_entry_page(&mut self) { + self.render_text_on_screen(&self.get_first_text(), Font::BOLD); + } + + fn get_second_text(&self) -> String<100> { + build_string!(100, "Do NOT make digital copies!") + } + + /// Display the second page with user information. + fn paint_second_page(&mut self) { + self.render_text_on_screen(&self.get_second_text(), Font::MONO); + } + + fn get_final_text(&self) -> String<100> { + build_string!( + 100, + "I wrote down all ", + inttostr!(self.share_words.len() as u8), + " words in order." + ) + } + + /// Display the final page with user confirmation. + fn paint_final_page(&mut self) { + self.render_text_on_screen(&self.get_final_text(), Font::MONO); + } + + /// Shows text in the main screen area. + fn render_text_on_screen(&self, text: &str, font: Font) { + text_multiline_split_words( + self.area.split_top(INFO_TOP_OFFSET).1, + text, + font, + theme::FG, + theme::BG, + Alignment::Start, + ); + } + + /// Display current set of recovery words. + fn paint_words(&mut self) { + let mut y_offset = 0; + // Showing the word index and the words itself + for i in 0..WORDS_PER_PAGE { + y_offset += NUMBER_FONT.line_height() + EXTRA_LINE_HEIGHT; + let index = self.word_index() + i; + if index >= self.share_words.len() { + break; + } + let word = &self.share_words[index]; + let baseline = self.area.top_left() + Offset::new(NUMBER_X_OFFSET, y_offset); + display(baseline, &inttostr!(index as u8 + 1), NUMBER_FONT); + display(baseline + Offset::x(NUMBER_WORD_OFFSET), &word, WORD_FONT); + } + } +} + +impl Component for ShareWords +where + T: StringType + Clone, +{ + type Msg = Never; + + fn place(&mut self, bounds: Rect) -> Rect { + let (title_area, _) = bounds.split_top(theme::FONT_HEADER.line_height()); + + let (title_area, scrollbar_area) = + title_area.split_right(self.scrollbar.inner().overall_width() + SCROLLBAR_SPACE); + + self.title.place(title_area); + self.scrollbar.place(scrollbar_area); + + self.area = bounds; + self.area + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + self.title.event(ctx, event); + self.scrollbar.event(ctx, event); + None + } + + fn paint(&mut self) { + // Showing scrollbar in all cases + // Individual pages are responsible for not colliding with it + self.scrollbar.paint(); + if self.is_entry_page() { + self.title.paint(); + self.paint_entry_page(); + } else if self.is_second_page() { + self.paint_second_page(); + } else if self.is_final_page() { + self.paint_final_page(); + } else { + self.paint_words(); + } + } +} + +impl Paginate for ShareWords +where + T: StringType + Clone, +{ + fn page_count(&mut self) -> usize { + // Not defining the logic here, as we do not want it to be `&mut`. + self.total_page_count() + } + + fn change_page(&mut self, active_page: usize) { + self.page_index = active_page; + self.scrollbar.change_page(active_page); + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for ShareWords +where + T: StringType + Clone, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("ShareWords"); + let content = if self.is_entry_page() { + build_string!( + 100, + self.title.inner().get_text(), + "\n", + &self.get_first_text() + ) + } else if self.is_second_page() { + self.get_second_text() + } else if self.is_final_page() { + self.get_final_text() + } else { + let mut content = String::<100>::new(); + for i in 0..WORDS_PER_PAGE { + let index = self.word_index() + i; + if index >= self.share_words.len() { + break; + } + let word = &self.share_words[index]; + let current_line = + build_string!(20, inttostr!(index as u8 + 1), " ", word.as_ref(), "\n"); + unwrap!(content.push_str(¤t_line)); + } + content + }; + t.string("screen_content", &content); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/show_more.rs b/core/embed/rust/src/ui/model_tr/component/show_more.rs new file mode 100644 index 000000000..2a01412a2 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/show_more.rs @@ -0,0 +1,92 @@ +use crate::{ + strutil::StringType, + ui::{ + component::{Child, Component, Event, EventCtx}, + geometry::{Insets, Rect}, + }, +}; + +use super::{theme, ButtonController, ButtonControllerMsg, ButtonLayout, ButtonPos}; + +pub enum CancelInfoConfirmMsg { + Cancelled, + Info, + Confirmed, +} + +pub struct ShowMore +where + U: StringType, +{ + content: Child, + buttons: Child>, +} + +impl ShowMore +where + T: Component, + U: StringType + Clone, +{ + pub fn new(content: T) -> Self { + let btn_layout = ButtonLayout::cancel_armed_text("CONFIRM".into(), "i".into()); + Self { + content: Child::new(content), + buttons: Child::new(ButtonController::new(btn_layout)), + } + } +} + +impl Component for ShowMore +where + T: Component, + U: StringType + Clone, +{ + type Msg = CancelInfoConfirmMsg; + + fn place(&mut self, bounds: Rect) -> Rect { + let (content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT); + let content_area = content_area.inset(Insets::top(1)); + self.content.place(content_area); + self.buttons.place(button_area); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + let button_event = self.buttons.event(ctx, event); + + if let Some(ButtonControllerMsg::Triggered(pos)) = button_event { + match pos { + ButtonPos::Left => { + return Some(CancelInfoConfirmMsg::Cancelled); + } + ButtonPos::Middle => { + return Some(CancelInfoConfirmMsg::Confirmed); + } + ButtonPos::Right => { + return Some(CancelInfoConfirmMsg::Info); + } + } + } + None + } + + fn paint(&mut self) { + self.content.paint(); + self.buttons.paint(); + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for ShowMore +where + T: crate::trace::Trace + Component, + U: StringType + Clone, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("ShowMore"); + t.child("buttons", &self.buttons); + t.child("content", &self.content); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/title.rs b/core/embed/rust/src/ui/model_tr/component/title.rs new file mode 100644 index 000000000..0561dd42c --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/title.rs @@ -0,0 +1,132 @@ +use crate::{ + strutil::StringType, + time::Instant, + ui::{ + component::{Component, Event, EventCtx, Marquee, Never}, + display, + geometry::{Offset, Rect}, + }, +}; + +use super::super::theme; + +pub struct Title +where + T: StringType, +{ + area: Rect, + title: T, + marquee: Marquee, + needs_marquee: bool, + centered: bool, +} + +impl Title +where + T: StringType + Clone, +{ + pub fn new(title: T) -> Self { + Self { + title: title.clone(), + marquee: Marquee::new(title, theme::FONT_HEADER, theme::FG, theme::BG), + needs_marquee: false, + area: Rect::zero(), + centered: false, + } + } + + pub fn with_centered(mut self) -> Self { + self.centered = true; + self + } + + pub fn get_text(&self) -> &str { + self.title.as_ref() + } + + pub fn set_text(&mut self, ctx: &mut EventCtx, new_text: T) { + self.title = new_text.clone(); + self.marquee.set_text(new_text.clone()); + let text_width = theme::FONT_HEADER.text_width(new_text.as_ref()); + self.needs_marquee = text_width > self.area.width(); + // Resetting the marquee to the beginning and starting it when necessary. + self.marquee.reset(); + if self.needs_marquee { + self.marquee.start(ctx, Instant::now()); + } + } + + /// Display title/header at the top left of the given area. + pub fn paint_header_left(title: &T, area: Rect) { + let text_height = theme::FONT_HEADER.text_height(); + let title_baseline = area.top_left() + Offset::y(text_height - 1); + display::text_left( + title_baseline, + title.as_ref(), + theme::FONT_HEADER, + theme::FG, + theme::BG, + ); + } + + /// Display title/header centered at the top of the given area. + pub fn paint_header_centered(title: &T, area: Rect) { + let text_height = theme::FONT_HEADER.text_height(); + let title_baseline = area.top_center() + Offset::y(text_height - 1); + display::text_center( + title_baseline, + title.as_ref(), + theme::FONT_HEADER, + theme::FG, + theme::BG, + ); + } +} + +impl Component for Title +where + T: StringType + Clone, +{ + type Msg = Never; + + fn place(&mut self, bounds: Rect) -> Rect { + self.area = bounds; + self.marquee.place(bounds); + let width = theme::FONT_HEADER.text_width(self.title.as_ref()); + self.needs_marquee = width > self.area.width(); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + if self.needs_marquee { + if !self.marquee.is_animating() { + self.marquee.start(ctx, Instant::now()); + } + return self.marquee.event(ctx, event); + } + None + } + + fn paint(&mut self) { + if self.needs_marquee { + self.marquee.paint(); + } else if self.centered { + Self::paint_header_centered(&self.title, self.area); + } else { + Self::paint_header_left(&self.title, self.area); + } + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for Title +where + T: StringType + Clone, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("Title"); + t.string("text", self.title.as_ref()); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/welcome_screen.rs b/core/embed/rust/src/ui/model_tr/component/welcome_screen.rs new file mode 100644 index 000000000..f675584a7 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/welcome_screen.rs @@ -0,0 +1,59 @@ +use crate::ui::{ + component::{Component, Event, EventCtx, Never}, + constant::MODEL_NAME, + display, + geometry::{self, Offset, Rect}, +}; + +use super::super::theme; + +const TEXT_BOTTOM_MARGIN: i16 = 2; // matching the homescreen label margin +const ICON_TOP_MARGIN: i16 = 11; +const MODEL_NAME_FONT: display::Font = display::Font::NORMAL; + +pub struct WelcomeScreen { + area: Rect, +} + +impl WelcomeScreen { + pub fn new() -> Self { + Self { area: Rect::zero() } + } +} + +impl Component for WelcomeScreen { + type Msg = Never; + + fn place(&mut self, bounds: Rect) -> Rect { + self.area = bounds; + self.area + } + + fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { + None + } + + fn paint(&mut self) { + display::text_center( + self.area.bottom_center() - Offset::y(TEXT_BOTTOM_MARGIN), + MODEL_NAME, + MODEL_NAME_FONT, + theme::FG, + theme::BG, + ); + theme::ICON_LOGO.draw( + self.area.top_center() + Offset::y(ICON_TOP_MARGIN), + geometry::TOP_CENTER, + theme::FG, + theme::BG, + ); + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for WelcomeScreen { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("WelcomeScreen"); + t.string("model_name", MODEL_NAME); + } +} diff --git a/core/embed/rust/src/ui/model_tr/constant.rs b/core/embed/rust/src/ui/model_tr/constant.rs index eadb0f43a..0368eb2ae 100644 --- a/core/embed/rust/src/ui/model_tr/constant.rs +++ b/core/embed/rust/src/ui/model_tr/constant.rs @@ -1,15 +1,15 @@ use crate::ui::geometry::{Offset, Point, Rect}; pub const WIDTH: i16 = 128; -pub const HEIGHT: i16 = 128; +pub const HEIGHT: i16 = 64; pub const LINE_SPACE: i16 = 1; pub const FONT_BPP: i16 = 1; -pub const LOADER_OUTER: i32 = 32; -pub const LOADER_INNER: i32 = 18; +pub const LOADER_OUTER: i16 = 32; +pub const LOADER_INNER: i16 = 18; pub const LOADER_ICON_MAX_SIZE: i16 = 8; -pub const BACKLIGHT_NORMAL: i32 = 150; +pub const MODEL_NAME: &str = "Trezor Model R"; pub const fn size() -> Offset { Offset::new(WIDTH, HEIGHT) diff --git a/core/embed/rust/src/ui/model_tr/layout.rs b/core/embed/rust/src/ui/model_tr/layout.rs index 177dc3c9f..eeb365737 100644 --- a/core/embed/rust/src/ui/model_tr/layout.rs +++ b/core/embed/rust/src/ui/model_tr/layout.rs @@ -1,31 +1,99 @@ -use core::convert::TryInto; +use core::{cmp::Ordering, convert::TryInto}; + +use heapless::Vec; use crate::{ error::Error, - micropython::{buffer::StrBuffer, map::Map, module::Module, obj::Obj, qstr::Qstr, util}, + maybe_trace::MaybeTrace, + micropython::{ + buffer::StrBuffer, + gc::Gc, + iter::{Iter, IterBuf}, + list::List, + map::Map, + module::Module, + obj::Obj, + qstr::Qstr, + util, + }, + strutil::StringType, ui::{ component::{ base::Component, paginated::{PageMsg, Paginate}, - text::paragraphs::{Paragraph, Paragraphs}, - FormattedText, + text::{ + op::OpTextLayout, + paragraphs::{ + Checklist, Paragraph, ParagraphSource, ParagraphVecLong, ParagraphVecShort, + Paragraphs, VecExt, + }, + TextStyle, + }, + ComponentExt, FormattedText, LineBreaking, Timeout, }, + display::{self}, + geometry::Alignment, layout::{ obj::{ComponentMsgObj, LayoutObj}, - result::{CANCELLED, CONFIRMED}, - util::upy_disable_animation, + result::{CANCELLED, CONFIRMED, INFO}, + util::{ + iter_into_array, iter_into_vec, upy_disable_animation, upy_toif_info, ConfirmBlob, + }, }, }, }; use super::{ - component::{Button, ButtonPage, ButtonPos, Frame}, - theme, + component::{ + AddressDetails, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, + CancelInfoConfirmMsg, CoinJoinProgress, Flow, FlowPages, Frame, Homescreen, Lockscreen, + NumberInput, Page, PassphraseEntry, PinEntry, Progress, ScrollableContent, ScrollableFrame, + ShareWords, ShowMore, SimpleChoice, WelcomeScreen, WordlistEntry, WordlistType, + }, + constant, theme, }; -impl ComponentMsgObj for ButtonPage +pub enum CancelConfirmMsg { + Cancelled, + Confirmed, +} + +impl From for Obj { + fn from(value: CancelConfirmMsg) -> Self { + match value { + CancelConfirmMsg::Cancelled => CANCELLED.as_obj(), + CancelConfirmMsg::Confirmed => CONFIRMED.as_obj(), + } + } +} + +impl ComponentMsgObj for ShowMore +where + T: Component, + U: StringType + Clone, +{ + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + match msg { + CancelInfoConfirmMsg::Cancelled => Ok(CANCELLED.as_obj()), + CancelInfoConfirmMsg::Info => Ok(INFO.as_obj()), + CancelInfoConfirmMsg::Confirmed => Ok(CONFIRMED.as_obj()), + } + } +} + +impl ComponentMsgObj for Paragraphs +where + T: ParagraphSource, +{ + fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result { + unreachable!() + } +} + +impl ComponentMsgObj for ButtonPage where T: Component + Paginate, + U: StringType + Clone, { fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { match msg { @@ -37,47 +105,835 @@ where } } +impl ComponentMsgObj for Flow +where + F: Fn(usize) -> Page, + T: StringType + Clone, +{ + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + match msg { + CancelInfoConfirmMsg::Confirmed => { + if let Some(index) = self.confirmed_index() { + index.try_into() + } else { + Ok(CONFIRMED.as_obj()) + } + } + CancelInfoConfirmMsg::Cancelled => Ok(CANCELLED.as_obj()), + CancelInfoConfirmMsg::Info => Ok(INFO.as_obj()), + } + } +} + +impl ComponentMsgObj for PinEntry +where + T: StringType + Clone, +{ + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + match msg { + CancelConfirmMsg::Confirmed => self.pin().try_into(), + CancelConfirmMsg::Cancelled => Ok(CANCELLED.as_obj()), + } + } +} + +impl ComponentMsgObj for (Timeout, T) +where + T: Component, +{ + fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result { + Ok(CANCELLED.as_obj()) + } +} + +impl ComponentMsgObj for AddressDetails +where + T: StringType + Clone, +{ + fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result { + Ok(CANCELLED.as_obj()) + } +} + +impl ComponentMsgObj for CoinJoinProgress +where + T: StringType, +{ + fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result { + unreachable!(); + } +} + +impl ComponentMsgObj for NumberInput +where + T: StringType + Clone, +{ + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + msg.try_into() + } +} + +impl ComponentMsgObj for SimpleChoice +where + T: StringType + Clone, +{ + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + if self.return_index { + msg.try_into() + } else { + let text = self.result_by_index(msg); + text.try_into() + } + } +} + +impl ComponentMsgObj for WordlistEntry +where + T: StringType + Clone, +{ + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + msg.try_into() + } +} + +impl ComponentMsgObj for PassphraseEntry +where + T: StringType + Clone, +{ + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + match msg { + CancelConfirmMsg::Confirmed => self.passphrase().try_into(), + CancelConfirmMsg::Cancelled => Ok(CANCELLED.as_obj()), + } + } +} + impl ComponentMsgObj for Frame where T: ComponentMsgObj, - U: AsRef, + U: StringType + Clone, { fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { self.inner().msg_try_into_obj(msg) } } +impl ComponentMsgObj for ScrollableFrame +where + T: ComponentMsgObj + ScrollableContent, + U: StringType + Clone, +{ + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + self.inner().msg_try_into_obj(msg) + } +} + +impl ComponentMsgObj for Progress +where + T: StringType, +{ + fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result { + unreachable!() + } +} + +impl ComponentMsgObj for Homescreen +where + T: StringType + Clone, +{ + fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result { + Ok(CANCELLED.as_obj()) + } +} + +impl ComponentMsgObj for Lockscreen +where + T: StringType + Clone, +{ + fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result { + Ok(CANCELLED.as_obj()) + } +} + +/// Function to create and call a `ButtonPage` dialog based on paginable content +/// (e.g. `Paragraphs` or `FormattedText`). +/// Has optional title (supply empty `StrBuffer` for that) and hold-to-confirm +/// functionality. +fn content_in_button_page( + title: StrBuffer, + content: T, + verb: StrBuffer, + verb_cancel: Option, + hold: bool, +) -> Result { + // Left button - icon, text or nothing. + let cancel_btn = if let Some(verb_cancel) = verb_cancel { + if !verb_cancel.is_empty() { + Some(ButtonDetails::text(verb_cancel)) + } else { + Some(ButtonDetails::cancel_icon()) + } + } else { + None + }; + + // Right button - text or nothing. + // Optional HoldToConfirm + let mut confirm_btn = if !verb.is_empty() { + Some(ButtonDetails::text(verb)) + } else { + None + }; + if hold { + confirm_btn = confirm_btn.map(|btn| btn.with_default_duration()); + } + + let content = ButtonPage::new(content, theme::BG) + .with_cancel_btn(cancel_btn) + .with_confirm_btn(confirm_btn); + + let mut frame = ScrollableFrame::new(content); + if !title.as_ref().is_empty() { + frame = frame.with_title(title); + } + let obj = LayoutObj::new(frame)?; + + Ok(obj.into()) +} + extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = |_args: &[Obj], kwargs: &Map| { let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; let action: Option = kwargs.get(Qstr::MP_QSTR_action)?.try_into_option()?; let description: Option = kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?; - let verb: Option = kwargs.get(Qstr::MP_QSTR_verb)?.try_into_option()?; - let verb_cancel: Option = - kwargs.get(Qstr::MP_QSTR_verb_cancel)?.try_into_option()?; - let reverse: bool = kwargs.get(Qstr::MP_QSTR_reverse)?.try_into()?; - - let format = match (&action, &description, reverse) { - (Some(_), Some(_), false) => "{bold}{action}\n\r{normal}{description}", - (Some(_), Some(_), true) => "{normal}{description}\n\r{bold}{action}", - (Some(_), None, _) => "{bold}{action}", - (None, Some(_), _) => "{normal}{description}", - _ => "", + let verb: StrBuffer = kwargs.get_or(Qstr::MP_QSTR_verb, "CONFIRM".into())?; + let verb_cancel: Option = kwargs + .get(Qstr::MP_QSTR_verb_cancel) + .unwrap_or_else(|_| Obj::const_none()) + .try_into_option()?; + let reverse: bool = kwargs.get_or(Qstr::MP_QSTR_reverse, false)?; + let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?; + + let paragraphs = { + let action = action.unwrap_or_default(); + let description = description.unwrap_or_default(); + let mut paragraphs = ParagraphVecShort::new(); + if !reverse { + paragraphs + .add(Paragraph::new(&theme::TEXT_BOLD, action)) + .add(Paragraph::new(&theme::TEXT_MONO, description)); + } else { + paragraphs + .add(Paragraph::new(&theme::TEXT_MONO, description)) + .add(Paragraph::new(&theme::TEXT_BOLD, action)); + } + paragraphs.into_paragraphs() + }; + + content_in_button_page(title, paragraphs, verb, verb_cancel, hold) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_confirm_blob(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; + let data: Obj = kwargs.get(Qstr::MP_QSTR_data)?; + let description: Option = + kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?; + let extra: Option = kwargs.get(Qstr::MP_QSTR_extra)?.try_into_option()?; + let verb: StrBuffer = kwargs.get_or(Qstr::MP_QSTR_verb, "CONFIRM".into())?; + let verb_cancel: Option = kwargs + .get(Qstr::MP_QSTR_verb_cancel) + .unwrap_or_else(|_| Obj::const_none()) + .try_into_option()?; + let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?; + + let paragraphs = ConfirmBlob { + description: description.unwrap_or_else(StrBuffer::empty), + extra: extra.unwrap_or_else(StrBuffer::empty), + data: data.try_into()?, + description_font: &theme::TEXT_BOLD, + extra_font: &theme::TEXT_MONO, + data_font: &theme::TEXT_MONO_DATA, + } + .into_paragraphs(); + + content_in_button_page(title, paragraphs, verb, verb_cancel, hold) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_confirm_properties(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; + let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?; + let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?; + + let mut paragraphs = ParagraphVecLong::new(); + + let mut iter_buf = IterBuf::new(); + let iter = Iter::try_from_obj_with_buf(items, &mut iter_buf)?; + for para in iter { + let [key, value, is_data]: [Obj; 3] = iter_into_array(para)?; + let key = key.try_into_option::()?; + let value = value.try_into_option::()?; + let is_data: bool = is_data.try_into()?; + + if let Some(key) = key { + if value.is_some() { + paragraphs.add(Paragraph::new(&theme::TEXT_BOLD, key).no_break()); + } else { + paragraphs.add(Paragraph::new(&theme::TEXT_BOLD, key)); + } + } + if let Some(value) = value { + let style = if is_data { + &theme::TEXT_MONO_DATA + } else { + &theme::TEXT_MONO + }; + paragraphs.add(Paragraph::new(style, value)); + } + } + + content_in_button_page( + title, + paragraphs.into_paragraphs(), + "CONFIRM".into(), + None, + hold, + ) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_confirm_reset_device(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; + let button: StrBuffer = kwargs.get(Qstr::MP_QSTR_button)?.try_into()?; + + let ops = OpTextLayout::::new(theme::TEXT_MONO) + .text_mono("By continuing you agree to Trezor Company's terms and conditions.".into()) + .newline() + .text_mono("More info at".into()) + .newline() + .text_bold("trezor.io/tos".into()); + let formatted = FormattedText::new(ops); + + content_in_button_page(title, formatted, button, None, false) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_show_address_details(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let address: StrBuffer = kwargs.get(Qstr::MP_QSTR_address)?.try_into()?; + let case_sensitive: bool = kwargs.get(Qstr::MP_QSTR_case_sensitive)?.try_into()?; + let account: Option = kwargs.get(Qstr::MP_QSTR_account)?.try_into_option()?; + let path: Option = kwargs.get(Qstr::MP_QSTR_path)?.try_into_option()?; + + let xpubs: Obj = kwargs.get(Qstr::MP_QSTR_xpubs)?; + let mut iter_buf = IterBuf::new(); + let iter = Iter::try_from_obj_with_buf(xpubs, &mut iter_buf)?; + + let mut ad = AddressDetails::new(address, case_sensitive, account, path)?; + + for i in iter { + let [xtitle, text]: [StrBuffer; 2] = iter_into_array(i)?; + ad.add_xpub(xtitle, text)?; + } + + let obj = LayoutObj::new(ad)?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_confirm_value(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; + let description: StrBuffer = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?; + let value: StrBuffer = kwargs.get(Qstr::MP_QSTR_value)?.try_into()?; + + let verb: Option = kwargs + .get(Qstr::MP_QSTR_verb) + .unwrap_or_else(|_| Obj::const_none()) + .try_into_option()?; + let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?; + + let paragraphs = Paragraphs::new([ + Paragraph::new(&theme::TEXT_BOLD, description), + Paragraph::new(&theme::TEXT_MONO, value), + ]); + + content_in_button_page( + title, + paragraphs, + verb.unwrap_or_else(|| "CONFIRM".into()), + None, + hold, + ) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_confirm_joint_total(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let spending_amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_spending_amount)?.try_into()?; + let total_amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_total_amount)?.try_into()?; + + let paragraphs = Paragraphs::new([ + Paragraph::new(&theme::TEXT_BOLD, "You are contributing:".into()), + Paragraph::new(&theme::TEXT_MONO, spending_amount), + Paragraph::new(&theme::TEXT_BOLD, "To the total amount:".into()), + Paragraph::new(&theme::TEXT_MONO, total_amount), + ]); + + content_in_button_page( + "JOINT TRANSACTION".into(), + paragraphs, + "HOLD TO CONFIRM".into(), + None, + true, + ) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_confirm_modify_output(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let address: StrBuffer = kwargs.get(Qstr::MP_QSTR_address)?.try_into()?; + let sign: i32 = kwargs.get(Qstr::MP_QSTR_sign)?.try_into()?; + let amount_change: StrBuffer = kwargs.get(Qstr::MP_QSTR_amount_change)?.try_into()?; + let amount_new: StrBuffer = kwargs.get(Qstr::MP_QSTR_amount_new)?.try_into()?; + + let description = if sign < 0 { + "Decrease amount by:" + } else { + "Increase amount by:" }; - let _left = verb_cancel - .map(|label| Button::with_text(ButtonPos::Left, label, theme::button_cancel())); - let _right = - verb.map(|label| Button::with_text(ButtonPos::Right, label, theme::button_default())); + let paragraphs = Paragraphs::new([ + Paragraph::new(&theme::TEXT_BOLD, "Address:".into()), + Paragraph::new(&theme::TEXT_MONO, address).break_after(), + Paragraph::new(&theme::TEXT_MONO, description.into()), + Paragraph::new(&theme::TEXT_MONO, amount_change), + Paragraph::new(&theme::TEXT_BOLD, "New amount:".into()), + Paragraph::new(&theme::TEXT_MONO, amount_new), + ]); + + content_in_button_page( + "MODIFY AMOUNT".into(), + paragraphs, + "CONFIRM".into(), + None, + false, + ) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_confirm_output(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = |_args: &[Obj], kwargs: &Map| { + let address: StrBuffer = kwargs.get(Qstr::MP_QSTR_address)?.try_into()?; + let amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_amount)?.try_into()?; + let address_title: StrBuffer = kwargs.get(Qstr::MP_QSTR_address_title)?.try_into()?; + let address_title_clone = address_title.clone(); + let amount_title: StrBuffer = kwargs.get(Qstr::MP_QSTR_amount_title)?.try_into()?; + + let get_page = move |page_index| { + // Showing two screens - the recipient address and summary confirmation + match page_index { + 0 => { + // RECIPIENT + address + let btn_layout = ButtonLayout::cancel_none_text("CONFIRM".into()); + let btn_actions = ButtonActions::cancel_none_next(); + // Not putting hyphens in the address + let ops = OpTextLayout::new(theme::TEXT_MONO) + .line_breaking(LineBreaking::BreakWordsNoHyphen) + .text_mono(address.clone()); + let formatted = FormattedText::new(ops); + Page::new(btn_layout, btn_actions, formatted).with_title(address_title.clone()) + } + 1 => { + // AMOUNT + amount + let btn_layout = ButtonLayout::up_arrow_none_text("CONFIRM".into()); + let btn_actions = ButtonActions::prev_none_confirm(); + let ops = OpTextLayout::new(theme::TEXT_MONO) + .newline() + .text_mono(amount.clone()); + let formatted = FormattedText::new(ops); + Page::new(btn_layout, btn_actions, formatted).with_title(amount_title.clone()) + } + _ => unreachable!(), + } + }; + let pages = FlowPages::new(get_page, 2); + + let obj = LayoutObj::new(Flow::new(pages).with_common_title(address_title_clone))?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = |_args: &[Obj], kwargs: &Map| { + let total_amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_total_amount)?.try_into()?; + let fee_amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_fee_amount)?.try_into()?; + let fee_rate_amount: Option = kwargs + .get(Qstr::MP_QSTR_fee_rate_amount)? + .try_into_option()?; + let total_label: StrBuffer = kwargs.get(Qstr::MP_QSTR_total_label)?.try_into()?; + let fee_label: StrBuffer = kwargs.get(Qstr::MP_QSTR_fee_label)?.try_into()?; + + let get_page = move |page_index| { + // Total amount + fee + assert!(page_index == 0); + + let btn_layout = ButtonLayout::cancel_none_htc("HOLD TO CONFIRM".into()); + let btn_actions = ButtonActions::cancel_none_confirm(); + + let mut ops = OpTextLayout::new(theme::TEXT_MONO) + .text_bold(total_label.clone()) + .newline() + .text_mono(total_amount.clone()) + .newline() + .text_bold(fee_label.clone()) + .newline() + .text_mono(fee_amount.clone()); + + // Fee rate amount might not be there + if let Some(fee_rate_amount) = fee_rate_amount.clone() { + ops = ops.newline().text_mono(fee_rate_amount) + } + + let formatted = FormattedText::new(ops); + Page::new(btn_layout, btn_actions, formatted) + }; + let pages = FlowPages::new(get_page, 1); + + let obj = LayoutObj::new(Flow::new(pages))?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_confirm_address(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; + let address: StrBuffer = kwargs.get(Qstr::MP_QSTR_data)?.try_into()?; + + let get_page = move |page_index| { + assert!(page_index == 0); + + let btn_layout = ButtonLayout::cancel_armed_text("CONFIRM".into(), "i".into()); + let btn_actions = ButtonActions::cancel_confirm_info(); + let ops = OpTextLayout::new(theme::TEXT_MONO) + .line_breaking(LineBreaking::BreakWordsNoHyphen) + .text_mono(address.clone()); + let formatted = FormattedText::new(ops); + Page::new(btn_layout, btn_actions, formatted) + }; + let pages = FlowPages::new(get_page, 1); + + let obj = LayoutObj::new(Flow::new(pages).with_common_title(title))?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +/// General pattern of most tutorial screens. +/// (title, text, btn_layout, btn_actions) +fn tutorial_screen( + title: &'static str, + text: &'static str, + btn_layout: ButtonLayout, + btn_actions: ButtonActions, +) -> Page { + let mut ops = OpTextLayout::::new(theme::TEXT_MONO); + // Add title if present + if !title.is_empty() { + ops = ops.text_bold(title.into()).newline().newline_half() + } + ops = ops.text_mono(text.into()); + + let formatted = FormattedText::new(ops); + Page::new(btn_layout, btn_actions, formatted) +} + +extern "C" fn tutorial(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = |_args: &[Obj], _kwargs: &Map| { + const PAGE_COUNT: usize = 7; + + let get_page = |page_index| { + // Lazy-loaded list of screens to show, with custom content, + // buttons and actions triggered by these buttons. + // Cancelling the first screen will point to the last one, + // which asks for confirmation whether user wants to + // really cancel the tutorial. + match page_index { + // title, text, btn_layout, btn_actions + 0 => { + tutorial_screen( + "HELLO", + "Welcome to Trezor.\nPress right to continue.", + ButtonLayout::text_none_arrow("SKIP".into()), + ButtonActions::last_none_next(), + ) + }, + 1 => { + tutorial_screen( + "", + "Use Trezor by clicking left and right buttons.\n\nContinue right.", + ButtonLayout::arrow_none_arrow(), + ButtonActions::prev_none_next(), + ) + }, + 2 => { + tutorial_screen( + "HOLD TO CONFIRM", + "Press and hold right to approve important operations.", + ButtonLayout::arrow_none_htc("HOLD TO CONFIRM".into()), + ButtonActions::prev_none_next(), + ) + }, + 3 => { + tutorial_screen( + "SCREEN SCROLL", + "Press right to scroll down to read all content when text\ndoesn't fit on one screen. Press left to scroll up.", + ButtonLayout::arrow_none_text("CONTINUE".into()), + ButtonActions::prev_none_next(), + ) + }, + 4 => { + tutorial_screen( + "CONFIRM", + "Press both left and right at the same time to confirm.", + ButtonLayout::none_armed_none("CONFIRM".into()), + ButtonActions::prev_next_none(), + ) + }, + // This page is special + 5 => { + let ops = OpTextLayout::::new(theme::TEXT_MONO) + .newline() + .text_mono("Tutorial complete.".into()) + .newline() + .newline() + .alignment(Alignment::Center) + .text_bold("You're ready to\nuse Trezor.".into()); + let formatted = FormattedText::new(ops); + + Page::new( + ButtonLayout::text_none_text("AGAIN".into(), "FINISH".into()), + ButtonActions::beginning_none_confirm(), + formatted, + ) + }, + 6 => { + tutorial_screen( + "SKIP TUTORIAL", + "Are you sure you want to skip the tutorial?", + ButtonLayout::cancel_none_text("SKIP".into()), + ButtonActions::beginning_none_cancel(), + ) + }, + _ => unreachable!(), + } + }; + + let pages = FlowPages::new(get_page, PAGE_COUNT); + + let obj = LayoutObj::new(Flow::new(pages))?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_confirm_modify_fee(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let sign: i32 = kwargs.get(Qstr::MP_QSTR_sign)?.try_into()?; + let user_fee_change: StrBuffer = kwargs.get(Qstr::MP_QSTR_user_fee_change)?.try_into()?; + let total_fee_new: StrBuffer = kwargs.get(Qstr::MP_QSTR_total_fee_new)?.try_into()?; + let fee_rate_amount: Option = kwargs + .get(Qstr::MP_QSTR_fee_rate_amount)? + .try_into_option()?; + + let (description, change) = match sign { + s if s < 0 => ("Decrease fee by:", user_fee_change), + s if s > 0 => ("Increase fee by:", user_fee_change), + _ => ("Your fee did not change.", StrBuffer::empty()), + }; + + let mut paragraphs_vec = ParagraphVecShort::new(); + paragraphs_vec + .add(Paragraph::new(&theme::TEXT_BOLD, description.into())) + .add(Paragraph::new(&theme::TEXT_MONO, change)) + .add(Paragraph::new(&theme::TEXT_BOLD, "Transaction fee:".into()).no_break()) + .add(Paragraph::new(&theme::TEXT_MONO, total_fee_new)); + + if let Some(fee_rate_amount) = fee_rate_amount { + paragraphs_vec + .add(Paragraph::new(&theme::TEXT_BOLD, "Fee rate:".into()).no_break()) + .add(Paragraph::new(&theme::TEXT_MONO, fee_rate_amount)); + } + + content_in_button_page( + "MODIFY FEE".into(), + paragraphs_vec.into_paragraphs(), + "CONFIRM".into(), + Some("".into()), + false, + ) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_confirm_fido(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let app_name: StrBuffer = kwargs.get(Qstr::MP_QSTR_app_name)?.try_into()?; + let accounts: Gc = kwargs.get(Qstr::MP_QSTR_accounts)?.try_into()?; + + // Cache the page count so that we can move `accounts` into the closure. + let page_count = accounts.len(); + + let title: StrBuffer = if page_count > 1 { + "IMPORT".into() + } else { + "IMPORT CREDENTIAL".into() + }; + + // Closure to lazy-load the information on given page index. + // Done like this to allow arbitrarily many pages without + // the need of any allocation here in Rust. + let get_page = move |page_index| { + let account_obj = unwrap!(accounts.get(page_index)); + let account = account_obj.try_into().unwrap_or_else(|_| "".into()); + + let (btn_layout, btn_actions) = if page_count == 1 { + // There is only one page + ( + ButtonLayout::cancel_none_text("CONFIRM".into()), + ButtonActions::cancel_none_confirm(), + ) + } else if page_index == 0 { + // First page + ( + ButtonLayout::cancel_armed_arrow("SELECT".into()), + ButtonActions::cancel_confirm_next(), + ) + } else if page_index == page_count - 1 { + // Last page + ( + ButtonLayout::arrow_armed_none("SELECT".into()), + ButtonActions::prev_confirm_none(), + ) + } else { + // Page in the middle + ( + ButtonLayout::arrow_armed_arrow("SELECT".into()), + ButtonActions::prev_confirm_next(), + ) + }; + + let ops = OpTextLayout::new(theme::TEXT_MONO) + .newline() + .text_mono(app_name.clone()) + .newline() + .text_bold(account); + let formatted = FormattedText::new(ops); + + Page::new(btn_layout, btn_actions, formatted) + }; + + let pages = FlowPages::new(get_page, page_count); + // Returning the page index in case of confirmation. + let obj = LayoutObj::new( + Flow::new(pages) + .with_common_title(title) + .with_return_confirmed_index(), + )?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_show_info(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; + let description: StrBuffer = + kwargs.get_or(Qstr::MP_QSTR_description, StrBuffer::empty())?; + let time_ms: u32 = kwargs.get_or(Qstr::MP_QSTR_time_ms, 0)?; + + let content = Frame::new( + title, + Paragraphs::new([Paragraph::new(&theme::TEXT_MONO, description)]), + ); + let obj = if time_ms == 0 { + // No timer, used when we only want to draw the dialog once and + // then throw away the layout object. + LayoutObj::new(content)? + } else { + // Timeout. + let timeout = Timeout::new(time_ms); + LayoutObj::new((timeout, content.map(|_| None)))? + }; + + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_show_mismatch() -> Obj { + let block = move || { + let get_page = move |page_index| { + assert!(page_index == 0); + + let btn_layout = ButtonLayout::arrow_none_text("QUIT".into()); + let btn_actions = ButtonActions::cancel_none_confirm(); + let ops = OpTextLayout::::new(theme::TEXT_MONO) + .text_bold("ADDRESS MISMATCH?".into()) + .newline() + .newline_half() + .text_mono("Please contact Trezor support at".into()) + .newline() + .text_bold("trezor.io/support".into()); + let formatted = FormattedText::new(ops); + Page::new(btn_layout, btn_actions, formatted) + }; + let pages = FlowPages::new(get_page, 1); + + let obj = LayoutObj::new(Flow::new(pages))?; + Ok(obj.into()) + }; + unsafe { util::try_or_raise(block) } +} + +extern "C" fn new_confirm_with_info(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; + let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?; + + let mut paragraphs = ParagraphVecShort::new(); + + let mut iter_buf = IterBuf::new(); + let iter = Iter::try_from_obj_with_buf(items, &mut iter_buf)?; + for para in iter { + let [font, text]: [Obj; 2] = iter_into_array(para)?; + let style: &TextStyle = theme::textstyle_number_bold_or_mono(font.try_into()?); + let text: StrBuffer = text.try_into()?; + paragraphs.add(Paragraph::new(style, text)); + if paragraphs.is_full() { + break; + } + } let obj = LayoutObj::new(Frame::new( title, - ButtonPage::new( - FormattedText::new(theme::TEXT_NORMAL, theme::FORMATTED, format) - .with("action", action.unwrap_or_default()) - .with("description", description.unwrap_or_default()), - theme::BG, + ShowMore::>, StrBuffer>::new( + paragraphs.into_paragraphs(), ), ))?; Ok(obj.into()) @@ -85,28 +941,334 @@ extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut M unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } -extern "C" fn new_confirm_text(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { +extern "C" fn new_confirm_coinjoin(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let max_rounds: StrBuffer = kwargs.get(Qstr::MP_QSTR_max_rounds)?.try_into()?; + let max_feerate: StrBuffer = kwargs.get(Qstr::MP_QSTR_max_feerate)?.try_into()?; + + let paragraphs = Paragraphs::new([ + Paragraph::new(&theme::TEXT_BOLD, "Max rounds".into()), + Paragraph::new(&theme::TEXT_MONO, max_rounds), + Paragraph::new(&theme::TEXT_BOLD, "Max mining fee".into()).no_break(), + Paragraph::new(&theme::TEXT_MONO, max_feerate), + ]); + + content_in_button_page( + "AUTHORIZE COINJOIN".into(), + paragraphs, + "HOLD TO CONFIRM".into(), + None, + true, + ) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_request_pin(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = |_args: &[Obj], kwargs: &Map| { + let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?; + let subprompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_subprompt)?.try_into()?; + + let obj = LayoutObj::new(PinEntry::new(prompt, subprompt))?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_request_passphrase(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = |_args: &[Obj], kwargs: &Map| { + let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?; + + let obj = LayoutObj::new( + Frame::new(prompt, PassphraseEntry::::new()).with_title_centered(), + )?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_request_bip39(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = |_args: &[Obj], kwargs: &Map| { + let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?; + + let obj = LayoutObj::new( + Frame::new(prompt, WordlistEntry::::new(WordlistType::Bip39)) + .with_title_centered(), + )?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_request_slip39(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = |_args: &[Obj], kwargs: &Map| { + let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?; + + let obj = LayoutObj::new( + Frame::new( + prompt, + WordlistEntry::::new(WordlistType::Slip39), + ) + .with_title_centered(), + )?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_select_word(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = |_args: &[Obj], kwargs: &Map| { + // we ignore passed in `title` and use `description` in its place + let description: StrBuffer = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?; + let words_iterable: Obj = kwargs.get(Qstr::MP_QSTR_words)?; + // There are only 3 words, but SimpleChoice requires 5 elements + let words: Vec = iter_into_vec(words_iterable)?; + + // Returning the index of the selected word, not the word itself + let obj = LayoutObj::new( + Frame::new( + description, + SimpleChoice::new(words, false) + .with_show_incomplete() + .with_return_index(), + ) + .with_title_centered(), + )?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = |_args: &[Obj], kwargs: &Map| { let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; - let data: StrBuffer = kwargs.get(Qstr::MP_QSTR_data)?.try_into()?; - let description: Option = - kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?; + let share_words_obj: Obj = kwargs.get(Qstr::MP_QSTR_share_words)?; + let share_words: Vec = iter_into_vec(share_words_obj)?; - let obj = LayoutObj::new(Frame::new( - title, + let confirm_btn = Some( + ButtonDetails::::text("HOLD TO CONFIRM".into()).with_default_duration(), + ); + + let obj = LayoutObj::new( + ButtonPage::new(ShareWords::new(title, share_words), theme::BG) + .with_confirm_btn(confirm_btn), + )?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_request_number(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; + let min_count: u32 = kwargs.get(Qstr::MP_QSTR_min_count)?.try_into()?; + let max_count: u32 = kwargs.get(Qstr::MP_QSTR_max_count)?.try_into()?; + let count: u32 = kwargs.get(Qstr::MP_QSTR_count)?.try_into()?; + + let obj = LayoutObj::new( + Frame::new( + title, + NumberInput::::new(min_count, max_count, count), + ) + .with_title_centered(), + )?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_show_checklist(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let button: StrBuffer = kwargs.get(Qstr::MP_QSTR_button)?.try_into()?; + let active: usize = kwargs.get(Qstr::MP_QSTR_active)?.try_into()?; + let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?; + + let mut iter_buf = IterBuf::new(); + let mut paragraphs = ParagraphVecLong::new(); + let iter = Iter::try_from_obj_with_buf(items, &mut iter_buf)?; + for (i, item) in iter.enumerate() { + let style = match i.cmp(&active) { + Ordering::Less => &theme::TEXT_MONO, + Ordering::Equal => &theme::TEXT_BOLD, + Ordering::Greater => &theme::TEXT_MONO, + }; + let text: StrBuffer = item.try_into()?; + paragraphs.add(Paragraph::new(style, text)); + } + + let confirm_btn = Some(ButtonDetails::text(button)); + + let obj = LayoutObj::new( ButtonPage::new( - Paragraphs::new([ - Paragraph::new(&theme::TEXT_NORMAL, description.unwrap_or_default()), - Paragraph::new(&theme::TEXT_BOLD, data), - ]), + Checklist::from_paragraphs( + theme::ICON_ARROW_RIGHT_FAT, + theme::ICON_TICK_FAT, + active, + paragraphs + .into_paragraphs() + .with_spacing(theme::CHECKLIST_SPACING), + ) + .with_check_width(theme::CHECKLIST_CHECK_WIDTH) + .with_current_offset(theme::CHECKLIST_CURRENT_OFFSET), theme::BG, - ), + ) + .with_confirm_btn(confirm_btn), + )?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_confirm_recovery(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let description: StrBuffer = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?; + let button: StrBuffer = kwargs.get(Qstr::MP_QSTR_button)?.try_into()?; + let dry_run: bool = kwargs.get(Qstr::MP_QSTR_dry_run)?.try_into()?; + + let paragraphs = Paragraphs::new([Paragraph::new(&theme::TEXT_MONO, description)]); + + let title = if dry_run { + "SEED CHECK" + } else { + "WALLET RECOVERY" + }; + + content_in_button_page(title.into(), paragraphs, button, Some("".into()), false) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_select_word_count(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = |_args: &[Obj], _kwargs: &Map| { + let title: StrBuffer = "NUMBER OF WORDS".into(); + + let choices: Vec = ["12", "18", "20", "24", "33"] + .map(|num| num.into()) + .into_iter() + .collect(); + + let obj = LayoutObj::new( + Frame::new(title, SimpleChoice::new(choices, false)).with_title_centered(), + )?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_show_group_share_success( + n_args: usize, + args: *const Obj, + kwargs: *mut Map, +) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let lines_iterable: Obj = kwargs.get(Qstr::MP_QSTR_lines)?; + let lines: [StrBuffer; 4] = iter_into_array(lines_iterable)?; + + let [l0, l1, l2, l3] = lines; + + let paragraphs = Paragraphs::new([ + Paragraph::new(&theme::TEXT_MONO, l0), + Paragraph::new(&theme::TEXT_BOLD, l1), + Paragraph::new(&theme::TEXT_MONO, l2), + Paragraph::new(&theme::TEXT_BOLD, l3), + ]); + + content_in_button_page("".into(), paragraphs, "CONTINUE".into(), None, false) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_show_progress(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; + let indeterminate: bool = kwargs.get_or(Qstr::MP_QSTR_indeterminate, false)?; + let description: StrBuffer = + kwargs.get_or(Qstr::MP_QSTR_description, StrBuffer::empty())?; + + // Description updates are received as &str and we need to provide a way to + // convert them to StrBuffer. + let obj = LayoutObj::new(Progress::new( + title, + indeterminate, + description, + StrBuffer::alloc, ))?; Ok(obj.into()) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } +extern "C" fn new_show_progress_coinjoin(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; + let indeterminate: bool = kwargs.get_or(Qstr::MP_QSTR_indeterminate, false)?; + let time_ms: u32 = kwargs.get_or(Qstr::MP_QSTR_time_ms, 0)?; + let skip_first_paint: bool = kwargs.get_or(Qstr::MP_QSTR_skip_first_paint, false)?; + + // The second type parameter is actually not used in `new()` but we need to + // provide it. + let progress = CoinJoinProgress::new(title, indeterminate); + let obj = if time_ms > 0 && indeterminate { + let timeout = Timeout::new(time_ms); + LayoutObj::new((timeout, progress.map(|_msg| None)))? + } else { + LayoutObj::new(progress)? + }; + if skip_first_paint { + obj.skip_first_paint(); + } + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} +extern "C" fn new_show_homescreen(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let label: StrBuffer = kwargs + .get(Qstr::MP_QSTR_label)? + .try_into_option()? + .unwrap_or_else(|| constant::MODEL_NAME.into()); + let notification: Option = + kwargs.get(Qstr::MP_QSTR_notification)?.try_into_option()?; + let notification_level: u8 = kwargs.get_or(Qstr::MP_QSTR_notification_level, 0)?; + let skip_first_paint: bool = kwargs.get(Qstr::MP_QSTR_skip_first_paint)?.try_into()?; + + let notification = notification.map(|w| (w, notification_level)); + let obj = LayoutObj::new(Homescreen::new(label, notification))?; + if skip_first_paint { + obj.skip_first_paint(); + } + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_show_lockscreen(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let label: StrBuffer = kwargs + .get(Qstr::MP_QSTR_label)? + .try_into_option()? + .unwrap_or_else(|| constant::MODEL_NAME.into()); + let bootscreen: bool = kwargs.get(Qstr::MP_QSTR_bootscreen)?.try_into()?; + let skip_first_paint: bool = kwargs.get(Qstr::MP_QSTR_skip_first_paint)?.try_into()?; + + let obj = LayoutObj::new(Lockscreen::new(label, bootscreen))?; + if skip_first_paint { + obj.skip_first_paint(); + } + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn draw_welcome_screen() -> Obj { + // No need of util::try_or_raise, this does not allocate + let mut screen = WelcomeScreen::new(); + screen.place(constant::screen()); + display::sync(); + screen.paint(); + Obj::const_none() +} + #[no_mangle] pub static mp_module_trezorui2: Module = obj_module! { Qstr::MP_QSTR___name__ => Qstr::MP_QSTR_trezorui2.to_obj(), @@ -117,16 +1279,23 @@ pub static mp_module_trezorui2: Module = obj_module! { /// CANCELLED: object Qstr::MP_QSTR_CANCELLED => CANCELLED.as_obj(), + /// INFO: object + Qstr::MP_QSTR_INFO => INFO.as_obj(), + /// def disable_animation(disable: bool) -> None: /// """Disable animations, debug builds only.""" Qstr::MP_QSTR_disable_animation => obj_fn_1!(upy_disable_animation).as_obj(), + /// def toif_info(data: bytes) -> tuple[int, int, bool]: + /// """Get TOIF image dimensions and format (width: int, height: int, is_grayscale: bool).""" + Qstr::MP_QSTR_toif_info => obj_fn_1!(upy_toif_info).as_obj(), + /// def confirm_action( /// *, /// title: str, - /// action: str | None = None, - /// description: str | None = None, - /// verb: str | None = None, + /// action: str | None, + /// description: str | None, + /// verb: str = "CONFIRM", /// verb_cancel: str | None = None, /// hold: bool = False, /// hold_danger: bool = False, # unused on TR @@ -135,12 +1304,309 @@ pub static mp_module_trezorui2: Module = obj_module! { /// """Confirm action.""" Qstr::MP_QSTR_confirm_action => obj_fn_kw!(0, new_confirm_action).as_obj(), - /// def confirm_text( + /// def confirm_blob( /// *, /// title: str, - /// data: str, + /// data: str | bytes, /// description: str | None, + /// extra: str | None, + /// verb: str = "CONFIRM", + /// verb_cancel: str | None = None, + /// hold: bool = False, + /// ) -> object: + /// """Confirm byte sequence data.""" + Qstr::MP_QSTR_confirm_blob => obj_fn_kw!(0, new_confirm_blob).as_obj(), + + /// def confirm_address( + /// *, + /// title: str, + /// data: str, + /// description: str | None, # unused on TR + /// extra: str | None, # unused on TR + /// ) -> object: + /// """Confirm address.""" + Qstr::MP_QSTR_confirm_address => obj_fn_kw!(0, new_confirm_address).as_obj(), + + + /// def confirm_properties( + /// *, + /// title: str, + /// items: list[tuple[str | None, str | bytes | None, bool]], + /// hold: bool = False, + /// ) -> object: + /// """Confirm list of key-value pairs. The third component in the tuple should be True if + /// the value is to be rendered as binary with monospace font, False otherwise. + /// This only concerns the text style, you need to decode the value to UTF-8 in python.""" + Qstr::MP_QSTR_confirm_properties => obj_fn_kw!(0, new_confirm_properties).as_obj(), + + /// def confirm_reset_device( + /// *, + /// title: str, + /// button: str, + /// ) -> object: + /// """Confirm TOS before device setup.""" + Qstr::MP_QSTR_confirm_reset_device => obj_fn_kw!(0, new_confirm_reset_device).as_obj(), + + /// def show_address_details( + /// *, + /// address: str, + /// case_sensitive: bool, + /// account: str | None, + /// path: str | None, + /// xpubs: list[tuple[str, str]], + /// ) -> object: + /// """Show address details - QR code, account, path, cosigner xpubs.""" + Qstr::MP_QSTR_show_address_details => obj_fn_kw!(0, new_show_address_details).as_obj(), + + /// def confirm_value( + /// *, + /// title: str, + /// description: str, + /// value: str, + /// verb: str | None = None, + /// hold: bool = False, + /// ) -> object: + /// """Confirm value.""" + Qstr::MP_QSTR_confirm_value => obj_fn_kw!(0, new_confirm_value).as_obj(), + + /// def confirm_joint_total( + /// *, + /// spending_amount: str, + /// total_amount: str, + /// ) -> object: + /// """Confirm total if there are external inputs.""" + Qstr::MP_QSTR_confirm_joint_total => obj_fn_kw!(0, new_confirm_joint_total).as_obj(), + + /// def confirm_modify_output( + /// *, + /// address: str, + /// sign: int, + /// amount_change: str, + /// amount_new: str, + /// ) -> object: + /// """Decrease or increase amount for given address.""" + Qstr::MP_QSTR_confirm_modify_output => obj_fn_kw!(0, new_confirm_modify_output).as_obj(), + + /// def confirm_output( + /// *, + /// address: str, + /// amount: str, + /// address_title: str, + /// amount_title: str, + /// ) -> object: + /// """Confirm output.""" + Qstr::MP_QSTR_confirm_output => obj_fn_kw!(0, new_confirm_output).as_obj(), + + /// def confirm_total( + /// *, + /// total_amount: str, + /// fee_amount: str, + /// fee_rate_amount: str | None, + /// total_label: str, + /// fee_label: str, + /// ) -> object: + /// """Confirm summary of a transaction.""" + Qstr::MP_QSTR_confirm_total => obj_fn_kw!(0, new_confirm_total).as_obj(), + + /// def tutorial() -> object: + /// """Show user how to interact with the device.""" + Qstr::MP_QSTR_tutorial => obj_fn_kw!(0, tutorial).as_obj(), + + /// def confirm_modify_fee( + /// *, + /// sign: int, + /// user_fee_change: str, + /// total_fee_new: str, + /// fee_rate_amount: str | None, + /// ) -> object: + /// """Decrease or increase transaction fee.""" + Qstr::MP_QSTR_confirm_modify_fee => obj_fn_kw!(0, new_confirm_modify_fee).as_obj(), + + /// def confirm_fido( + /// *, + /// title: str, # unused on TR + /// app_name: str, + /// icon_name: str | None, # unused on TR + /// accounts: list[str | None], + /// ) -> int | object: + /// """FIDO confirmation. + /// + /// Returns page index in case of confirmation and CANCELLED otherwise. + /// """ + Qstr::MP_QSTR_confirm_fido => obj_fn_kw!(0, new_confirm_fido).as_obj(), + + /// def show_info( + /// *, + /// title: str, + /// description: str = "", + /// time_ms: int = 0, /// ) -> object: - /// """Confirm text.""" - Qstr::MP_QSTR_confirm_text => obj_fn_kw!(0, new_confirm_text).as_obj(), + /// """Info modal.""" + Qstr::MP_QSTR_show_info => obj_fn_kw!(0, new_show_info).as_obj(), + + /// def show_mismatch() -> object: + /// """Warning modal, receiving address mismatch.""" + Qstr::MP_QSTR_show_mismatch => obj_fn_0!(new_show_mismatch).as_obj(), + + /// def confirm_with_info( + /// *, + /// title: str, + /// button: str, # unused on TR + /// info_button: str, # unused on TR + /// items: Iterable[Tuple[int, str]], + /// ) -> object: + /// """Confirm given items but with third button. Always single page + /// without scrolling.""" + Qstr::MP_QSTR_confirm_with_info => obj_fn_kw!(0, new_confirm_with_info).as_obj(), + + /// def confirm_coinjoin( + /// *, + /// max_rounds: str, + /// max_feerate: str, + /// ) -> object: + /// """Confirm coinjoin authorization.""" + Qstr::MP_QSTR_confirm_coinjoin => obj_fn_kw!(0, new_confirm_coinjoin).as_obj(), + + /// def request_pin( + /// *, + /// prompt: str, + /// subprompt: str, + /// allow_cancel: bool = True, # unused on TR + /// wrong_pin: bool = False, # unused on TR + /// ) -> str | object: + /// """Request pin on device.""" + Qstr::MP_QSTR_request_pin => obj_fn_kw!(0, new_request_pin).as_obj(), + + /// def request_passphrase( + /// *, + /// prompt: str, + /// max_len: int, # unused on TR + /// ) -> str | object: + /// """Get passphrase.""" + Qstr::MP_QSTR_request_passphrase => obj_fn_kw!(0, new_request_passphrase).as_obj(), + + /// def request_bip39( + /// *, + /// prompt: str, + /// ) -> str: + /// """Get recovery word for BIP39.""" + Qstr::MP_QSTR_request_bip39 => obj_fn_kw!(0, new_request_bip39).as_obj(), + + /// def request_slip39( + /// *, + /// prompt: str, + /// ) -> str: + /// """SLIP39 word input keyboard.""" + Qstr::MP_QSTR_request_slip39 => obj_fn_kw!(0, new_request_slip39).as_obj(), + + /// def select_word( + /// *, + /// title: str, # unused on TR + /// description: str, + /// words: Iterable[str], + /// ) -> int: + /// """Select mnemonic word from three possibilities - seed check after backup. The + /// iterable must be of exact size. Returns index in range `0..3`.""" + Qstr::MP_QSTR_select_word => obj_fn_kw!(0, new_select_word).as_obj(), + + /// def show_share_words( + /// *, + /// title: str, + /// share_words: Iterable[str], + /// ) -> object: + /// """Shows a backup seed.""" + Qstr::MP_QSTR_show_share_words => obj_fn_kw!(0, new_show_share_words).as_obj(), + + /// def request_number( + /// *, + /// title: str, + /// count: int, + /// min_count: int, + /// max_count: int, + /// description: Callable[[int], str] | None = None, # unused on TR + /// ) -> object: + /// """Number input with + and - buttons, description, and info button.""" + Qstr::MP_QSTR_request_number => obj_fn_kw!(0, new_request_number).as_obj(), + + /// def show_checklist( + /// *, + /// title: str, # unused on TR + /// items: Iterable[str], + /// active: int, + /// button: str, + /// ) -> object: + /// """Checklist of backup steps. Active index is highlighted, previous items have check + /// mark next to them.""" + Qstr::MP_QSTR_show_checklist => obj_fn_kw!(0, new_show_checklist).as_obj(), + + /// def confirm_recovery( + /// *, + /// title: str, # unused on TR + /// description: str, + /// button: str, + /// dry_run: bool, + /// info_button: bool, # unused on TR + /// ) -> object: + /// """Device recovery homescreen.""" + Qstr::MP_QSTR_confirm_recovery => obj_fn_kw!(0, new_confirm_recovery).as_obj(), + + /// def select_word_count( + /// *, + /// dry_run: bool, # unused on TR + /// ) -> int | str: # TR returns str + /// """Select mnemonic word count from (12, 18, 20, 24, 33).""" + Qstr::MP_QSTR_select_word_count => obj_fn_kw!(0, new_select_word_count).as_obj(), + + /// def show_group_share_success( + /// *, + /// lines: Iterable[str], + /// ) -> int: + /// """Shown after successfully finishing a group.""" + Qstr::MP_QSTR_show_group_share_success => obj_fn_kw!(0, new_show_group_share_success).as_obj(), + + /// def show_progress( + /// *, + /// title: str, + /// indeterminate: bool = False, + /// description: str = "", + /// ) -> object: + /// """Show progress loader. Please note that the number of lines reserved on screen for + /// description is determined at construction time. If you want multiline descriptions + /// make sure the initial description has at least that amount of lines.""" + Qstr::MP_QSTR_show_progress => obj_fn_kw!(0, new_show_progress).as_obj(), + + /// def show_progress_coinjoin( + /// *, + /// title: str, + /// indeterminate: bool = False, + /// time_ms: int = 0, + /// skip_first_paint: bool = False, + /// ) -> object: + /// """Show progress loader for coinjoin. Returns CANCELLED after a specified time when + /// time_ms timeout is passed.""" + Qstr::MP_QSTR_show_progress_coinjoin => obj_fn_kw!(0, new_show_progress_coinjoin).as_obj(), + + /// def show_homescreen( + /// *, + /// label: str | None, + /// hold: bool, # unused on TR + /// notification: str | None, + /// notification_level: int = 0, + /// skip_first_paint: bool, + /// ) -> CANCELLED: + /// """Idle homescreen.""" + Qstr::MP_QSTR_show_homescreen => obj_fn_kw!(0, new_show_homescreen).as_obj(), + + /// def show_lockscreen( + /// *, + /// label: str | None, + /// bootscreen: bool, + /// skip_first_paint: bool, + /// ) -> CANCELLED: + /// """Homescreen for locked device.""" + Qstr::MP_QSTR_show_lockscreen => obj_fn_kw!(0, new_show_lockscreen).as_obj(), + + /// def draw_welcome_screen() -> None: + /// """Show logo icon with the model name at the bottom and return.""" + Qstr::MP_QSTR_draw_welcome_screen => obj_fn_0!(draw_welcome_screen).as_obj(), }; diff --git a/core/embed/rust/src/ui/model_tr/mod.rs b/core/embed/rust/src/ui/model_tr/mod.rs index 529781d27..63b196f24 100644 --- a/core/embed/rust/src/ui/model_tr/mod.rs +++ b/core/embed/rust/src/ui/model_tr/mod.rs @@ -2,8 +2,7 @@ pub mod bootloader; pub mod component; pub mod constant; -pub mod theme; - #[cfg(feature = "micropython")] pub mod layout; pub mod screens; +pub mod theme; diff --git a/core/embed/rust/src/ui/model_tr/res/amount.toif b/core/embed/rust/src/ui/model_tr/res/amount.toif new file mode 100644 index 000000000..6c2103feb Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/amount.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/amount_smaller.toif b/core/embed/rust/src/ui/model_tr/res/amount_smaller.toif new file mode 100644 index 000000000..68c5a2cb4 Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/amount_smaller.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/arm_left.toif b/core/embed/rust/src/ui/model_tr/res/arm_left.toif new file mode 100644 index 000000000..4f2e4c8a1 Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/arm_left.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/arm_right.toif b/core/embed/rust/src/ui/model_tr/res/arm_right.toif new file mode 100644 index 000000000..c30718791 Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/arm_right.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/arrow_back_up.toif b/core/embed/rust/src/ui/model_tr/res/arrow_back_up.toif new file mode 100644 index 000000000..b90e41b62 Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/arrow_back_up.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/arrow_down.toif b/core/embed/rust/src/ui/model_tr/res/arrow_down.toif new file mode 100644 index 000000000..166fe6b9a Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/arrow_down.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/arrow_left.toif b/core/embed/rust/src/ui/model_tr/res/arrow_left.toif new file mode 100644 index 000000000..a8c143caa Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/arrow_left.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/arrow_right.toif b/core/embed/rust/src/ui/model_tr/res/arrow_right.toif new file mode 100644 index 000000000..b69281847 Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/arrow_right.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/arrow_right_fat.toif b/core/embed/rust/src/ui/model_tr/res/arrow_right_fat.toif new file mode 100644 index 000000000..05bc52bd9 Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/arrow_right_fat.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/arrow_up.toif b/core/embed/rust/src/ui/model_tr/res/arrow_up.toif new file mode 100644 index 000000000..9e3755957 Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/arrow_up.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/bin.toif b/core/embed/rust/src/ui/model_tr/res/bin.toif new file mode 100644 index 000000000..6b8ffc8e9 Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/bin.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/cancel_for_outline.toif b/core/embed/rust/src/ui/model_tr/res/cancel_for_outline.toif new file mode 100644 index 000000000..cc4b1ba06 Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/cancel_for_outline.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/cancel_no_outline.toif b/core/embed/rust/src/ui/model_tr/res/cancel_no_outline.toif new file mode 100644 index 000000000..4dcd41a0f Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/cancel_no_outline.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/delete.toif b/core/embed/rust/src/ui/model_tr/res/delete.toif new file mode 100644 index 000000000..04cf849f9 Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/delete.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/eye.toif b/core/embed/rust/src/ui/model_tr/res/eye.toif new file mode 100644 index 000000000..3d3fe799e Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/eye.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/eye_round.toif b/core/embed/rust/src/ui/model_tr/res/eye_round.toif new file mode 100644 index 000000000..38cef9fd7 Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/eye_round.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/lock.toif b/core/embed/rust/src/ui/model_tr/res/lock.toif new file mode 100644 index 000000000..1f67f8828 Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/lock.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/logo_22_33.toif b/core/embed/rust/src/ui/model_tr/res/logo_22_33.toif new file mode 100644 index 000000000..ee81a841a Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/logo_22_33.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/next_page.toif b/core/embed/rust/src/ui/model_tr/res/next_page.toif new file mode 100644 index 000000000..8e892e408 Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/next_page.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/param.toif b/core/embed/rust/src/ui/model_tr/res/param.toif new file mode 100644 index 000000000..3b049fd0f Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/param.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/param_smaller.toif b/core/embed/rust/src/ui/model_tr/res/param_smaller.toif new file mode 100644 index 000000000..6c846e6b5 Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/param_smaller.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/prev_page.toif b/core/embed/rust/src/ui/model_tr/res/prev_page.toif new file mode 100644 index 000000000..30d704b40 Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/prev_page.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/space.toif b/core/embed/rust/src/ui/model_tr/res/space.toif new file mode 100644 index 000000000..227ebd429 Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/space.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/tick.toif b/core/embed/rust/src/ui/model_tr/res/tick.toif new file mode 100644 index 000000000..aefc96fd4 Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/tick.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/tick_fat.toif b/core/embed/rust/src/ui/model_tr/res/tick_fat.toif new file mode 100644 index 000000000..2eb9735da Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/tick_fat.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/user.toif b/core/embed/rust/src/ui/model_tr/res/user.toif new file mode 100644 index 000000000..c3fb0bfee Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/user.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/user_smaller.toif b/core/embed/rust/src/ui/model_tr/res/user_smaller.toif new file mode 100644 index 000000000..4cccc15c8 Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/user_smaller.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/wallet.toif b/core/embed/rust/src/ui/model_tr/res/wallet.toif new file mode 100644 index 000000000..277737189 Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/wallet.toif differ diff --git a/core/embed/rust/src/ui/model_tr/res/warning.toif b/core/embed/rust/src/ui/model_tr/res/warning.toif new file mode 100644 index 000000000..dc4dd35e4 Binary files /dev/null and b/core/embed/rust/src/ui/model_tr/res/warning.toif differ diff --git a/core/embed/rust/src/ui/model_tr/screens.rs b/core/embed/rust/src/ui/model_tr/screens.rs index 13c7e9128..de11cb739 100644 --- a/core/embed/rust/src/ui/model_tr/screens.rs +++ b/core/embed/rust/src/ui/model_tr/screens.rs @@ -5,13 +5,13 @@ use crate::ui::{ text::paragraphs::{Paragraph, ParagraphVecShort, Paragraphs, VecExt}, Component, }, - display::Icon, geometry::LinearPlacement, - model_tr::{ - component::ResultScreen, - constant, - theme::{BLACK, ICON_FAIL, TEXT_BOLD, TEXT_NORMAL, WHITE}, - }, +}; + +use super::{ + component::ResultScreen, + constant, + theme::{BLACK, ICON_FAIL, TEXT_BOLD, TEXT_NORMAL, WHITE}, }; #[cfg(not(feature = "micropython"))] @@ -47,7 +47,7 @@ pub fn screen_fatal_error(title: &str, msg: &str, footer: &str) { let m_bottom = Paragraphs::new(messages).with_placement(LinearPlacement::vertical().align_at_center()); - let mut frame = ResultScreen::new(WHITE, BLACK, Icon::new(ICON_FAIL), m_top, m_bottom, true); + let mut frame = ResultScreen::new(WHITE, BLACK, ICON_FAIL, m_top, m_bottom, true); frame.place(constant::screen()); frame.paint(); } diff --git a/core/embed/rust/src/ui/model_tr/theme.rs b/core/embed/rust/src/ui/model_tr/theme.rs index 1821ff258..003218be8 100644 --- a/core/embed/rust/src/ui/model_tr/theme.rs +++ b/core/embed/rust/src/ui/model_tr/theme.rs @@ -1,75 +1,89 @@ use crate::ui::{ - component::text::{formatted::FormattedFonts, TextStyle}, - display::{Color, Font}, - model_tr::component::{LoaderStyle, LoaderStyleSheet}, + component::{text::TextStyle, LineBreaking, PageBreaking}, + display::{toif::Icon, Color, Font}, + geometry::Offset, }; -use super::component::{ButtonStyle, ButtonStyleSheet}; - -// Typical backlight values. -pub const BACKLIGHT_NORMAL: u16 = 150; +use num_traits::FromPrimitive; // Color palette. -pub const WHITE: Color = Color::rgb(255, 255, 255); -pub const BLACK: Color = Color::rgb(0, 0, 0); -pub const GREY_LIGHT: Color = WHITE; // Word/page break characters. +pub const WHITE: Color = Color::white(); +pub const BLACK: Color = Color::black(); pub const FG: Color = WHITE; // Default foreground (text & icon) color. pub const BG: Color = BLACK; // Default background color. -pub const ICON_SUCCESS: &[u8] = include_res!("model_tr/res/success.toif"); -pub const ICON_FAIL: &[u8] = include_res!("model_tr/res/fail.toif"); +// Font constants. +pub const FONT_BUTTON: Font = Font::MONO; +pub const FONT_HEADER: Font = Font::BOLD; +pub const FONT_CHOICE_ITEMS: Font = Font::NORMAL; -// BLD icons -pub const LOGO_EMPTY: &[u8] = include_res!("model_tr/res/trezor_empty.toif"); +// Text constants. +pub const TEXT_NORMAL: TextStyle = TextStyle::new(Font::NORMAL, FG, BG, FG, FG); +pub const TEXT_DEMIBOLD: TextStyle = TextStyle::new(Font::DEMIBOLD, FG, BG, FG, FG); +pub const TEXT_BOLD: TextStyle = TextStyle::new(Font::BOLD, FG, BG, FG, FG) + .with_page_breaking(PageBreaking::CutAndInsertEllipsisBoth) + .with_ellipsis_icon(ICON_NEXT_PAGE, ELLIPSIS_ICON_MARGIN) + .with_prev_page_icon(ICON_PREV_PAGE, PREV_PAGE_ICON_MARGIN); +pub const TEXT_MONO: TextStyle = TextStyle::new(Font::MONO, FG, BG, FG, FG) + .with_page_breaking(PageBreaking::CutAndInsertEllipsisBoth) + .with_ellipsis_icon(ICON_NEXT_PAGE, ELLIPSIS_ICON_MARGIN) + .with_prev_page_icon(ICON_PREV_PAGE, PREV_PAGE_ICON_MARGIN); +/// Mono data text does not have hyphens +pub const TEXT_MONO_DATA: TextStyle = + TEXT_MONO.with_line_breaking(LineBreaking::BreakWordsNoHyphen); -pub fn button_default() -> ButtonStyleSheet { - ButtonStyleSheet { - normal: &ButtonStyle { - font: Font::BOLD, - text_color: BG, - border_horiz: true, - }, - active: &ButtonStyle { - font: Font::BOLD, - text_color: FG, - border_horiz: true, - }, +/// Convert Python-side numeric id to a `TextStyle`. +/// Using only BOLD or MONO fonts. +pub fn textstyle_number_bold_or_mono(num: i32) -> &'static TextStyle { + let font = Font::from_i32(-num); + match font { + Some(Font::BOLD) => &TEXT_BOLD, + Some(Font::DEMIBOLD) => &TEXT_BOLD, + _ => &TEXT_MONO, } } -pub fn button_cancel() -> ButtonStyleSheet { - ButtonStyleSheet { - normal: &ButtonStyle { - font: Font::BOLD, - text_color: FG, - border_horiz: false, - }, - active: &ButtonStyle { - font: Font::BOLD, - text_color: BG, - border_horiz: false, - }, - } -} +// BLD icons +pub const LOGO_EMPTY: &[u8] = include_res!("model_tr/res/trezor_empty.toif"); +include_icon!(ICON_FAIL, "model_tr/res/fail.toif"); -pub fn loader_default() -> LoaderStyleSheet { - LoaderStyleSheet { - normal: &LoaderStyle { - font: Font::NORMAL, - fg_color: FG, - bg_color: BG, - }, - } -} +// Firmware icons +include_icon!(ICON_ARM_LEFT, "model_tr/res/arm_left.toif"); // 6*10 +include_icon!(ICON_ARM_RIGHT, "model_tr/res/arm_right.toif"); // 6*10 +include_icon!(ICON_ARROW_LEFT, "model_tr/res/arrow_left.toif"); // 6*10 +include_icon!(ICON_ARROW_RIGHT, "model_tr/res/arrow_right.toif"); // 6*10 +include_icon!(ICON_ARROW_RIGHT_FAT, "model_tr/res/arrow_right_fat.toif"); // 4*8 +include_icon!(ICON_ARROW_UP, "model_tr/res/arrow_up.toif"); // 10*6 +include_icon!(ICON_ARROW_DOWN, "model_tr/res/arrow_down.toif"); // 10*6 +include_icon!(ICON_ARROW_BACK_UP, "model_tr/res/arrow_back_up.toif"); // 8*8 +include_icon!(ICON_BIN, "model_tr/res/bin.toif"); // 10*10 +include_icon!(ICON_CANCEL, "model_tr/res/cancel_no_outline.toif"); // 8*8 +include_icon!(ICON_DELETE, "model_tr/res/delete.toif"); // 10*7 +include_icon!(ICON_EYE, "model_tr/res/eye_round.toif"); // 12*7 +include_icon!(ICON_LOCK, "model_tr/res/lock.toif"); // 10*10 +include_icon!(ICON_LOGO, "model_tr/res/logo_22_33.toif"); // 22*33 +include_icon!(ICON_NEXT_PAGE, "model_tr/res/next_page.toif"); // 10*8 +include_icon!(ICON_PREV_PAGE, "model_tr/res/prev_page.toif"); // 8*10 +include_icon!(ICON_SPACE, "model_tr/res/space.toif"); // 12*3 +include_icon!(ICON_SUCCESS, "model_tr/res/success.toif"); +include_icon!(ICON_TICK, "model_tr/res/tick.toif"); // 8*6 +include_icon!(ICON_TICK_FAT, "model_tr/res/tick_fat.toif"); // 8*6 +include_icon!(ICON_WARNING, "model_tr/res/warning.toif"); // 12*12 -pub const TEXT_NORMAL: TextStyle = TextStyle::new(Font::NORMAL, FG, BG, FG, FG); -pub const TEXT_DEMIBOLD: TextStyle = TextStyle::new(Font::DEMIBOLD, FG, BG, FG, FG); -pub const TEXT_BOLD: TextStyle = TextStyle::new(Font::BOLD, FG, BG, FG, FG); -pub const TEXT_MONO: TextStyle = TextStyle::new(Font::MONO, FG, BG, FG, FG); +// checklist settings +pub const CHECKLIST_SPACING: i16 = 5; +pub const CHECKLIST_CHECK_WIDTH: i16 = 12; +pub const CHECKLIST_CURRENT_OFFSET: Offset = Offset::x(3); -pub const FORMATTED: FormattedFonts = FormattedFonts { - normal: Font::NORMAL, - demibold: Font::DEMIBOLD, - bold: Font::BOLD, - mono: Font::MONO, -}; +// Button height is constant for both text and icon buttons. +// It is a combination of content and (optional) outline/border. +// It is not possible to have icons 7*7, therefore having 8*8 +// with empty LEFT column and BOTTOM row. +pub const BUTTON_CONTENT_HEIGHT: i16 = 7; +pub const BUTTON_OUTLINE: i16 = 3; +pub const BUTTON_ARMS: i16 = 2; +pub const BUTTON_HEIGHT: i16 = BUTTON_CONTENT_HEIGHT + 2 * BUTTON_OUTLINE; + +// How many pixels should be between text and icons. +pub const ELLIPSIS_ICON_MARGIN: i16 = 4; +pub const PREV_PAGE_ICON_MARGIN: i16 = 6; diff --git a/core/embed/rust/src/ui/model_tt/component/address_details.rs b/core/embed/rust/src/ui/model_tt/component/address_details.rs index 2d0e7c01f..1a099527f 100644 --- a/core/embed/rust/src/ui/model_tt/component/address_details.rs +++ b/core/embed/rust/src/ui/model_tt/component/address_details.rs @@ -2,11 +2,10 @@ use heapless::Vec; use crate::{ error::Error, + strutil::StringType, ui::{ component::{ - text::paragraphs::{ - Paragraph, ParagraphSource, ParagraphStrType, ParagraphVecShort, Paragraphs, VecExt, - }, + text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs, VecExt}, Component, Event, EventCtx, Paginate, Qr, }, geometry::Rect, @@ -28,7 +27,7 @@ pub struct AddressDetails { impl AddressDetails where - T: ParagraphStrType, + T: StringType, { pub fn new( qr_address: T, @@ -124,7 +123,7 @@ where impl Paginate for AddressDetails where - T: ParagraphStrType + Clone, + T: StringType + Clone, { fn page_count(&mut self) -> usize { let total_xpub_pages: u8 = self.xpub_page_count.iter().copied().sum(); @@ -143,7 +142,7 @@ where impl Component for AddressDetails where - T: ParagraphStrType + Clone, + T: StringType + Clone, { type Msg = (); @@ -194,7 +193,7 @@ where #[cfg(feature = "ui_debug")] impl crate::trace::Trace for AddressDetails where - T: ParagraphStrType, + T: StringType, { fn trace(&self, t: &mut dyn crate::trace::Tracer) { t.component("AddressDetails"); diff --git a/core/embed/rust/src/ui/model_tt/component/button.rs b/core/embed/rust/src/ui/model_tt/component/button.rs index 3339a165f..c6f96f590 100644 --- a/core/embed/rust/src/ui/model_tt/component/button.rs +++ b/core/embed/rust/src/ui/model_tt/component/button.rs @@ -421,18 +421,18 @@ impl Button { let left = if let Some(verb) = left { left_is_small = verb.as_ref().len() <= 4; if verb.as_ref() == "^" { - Button::with_icon(Icon::new(theme::ICON_UP)) + Button::with_icon(theme::ICON_UP) } else { Button::with_text(verb) } } else { left_is_small = right.is_some(); - Button::with_icon(Icon::new(theme::ICON_CANCEL)) + Button::with_icon(theme::ICON_CANCEL) }; let right = if let Some(verb) = right { Button::with_text(verb).styled(theme::button_confirm()) } else { - Button::with_icon(Icon::new(theme::ICON_CONFIRM)).styled(theme::button_confirm()) + Button::with_icon(theme::ICON_CONFIRM).styled(theme::button_confirm()) }; Self::cancel_confirm(left, right, left_is_small) } @@ -457,7 +457,7 @@ impl Button { let top = Button::with_text(info) .styled(theme::button_moreinfo()) .map(|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Info)); - let left = Button::with_icon(Icon::new(theme::ICON_CANCEL)).map(|msg| { + let left = Button::with_icon(theme::ICON_CANCEL).map(|msg| { (matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Cancelled) }); let total_height = theme::BUTTON_HEIGHT + theme::BUTTON_SPACING + theme::INFO_BUTTON_HEIGHT; diff --git a/core/embed/rust/src/ui/model_tt/component/dialog.rs b/core/embed/rust/src/ui/model_tt/component/dialog.rs index 07b3d3d59..72f594930 100644 --- a/core/embed/rust/src/ui/model_tt/component/dialog.rs +++ b/core/embed/rust/src/ui/model_tt/component/dialog.rs @@ -1,16 +1,16 @@ -use crate::ui::{ - component::{ - image::BlendedImage, - text::{ - paragraphs::{ - Paragraph, ParagraphSource, ParagraphStrType, ParagraphVecShort, Paragraphs, VecExt, +use crate::{ + strutil::StringType, + ui::{ + component::{ + image::BlendedImage, + text::{ + paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs, VecExt}, + TextStyle, }, - TextStyle, + Child, Component, Event, EventCtx, Never, }, - Child, Component, Event, EventCtx, Never, + geometry::{Insets, LinearPlacement, Rect}, }, - display::toif::Icon, - geometry::{Insets, LinearPlacement, Rect}, }; use super::theme; @@ -99,7 +99,7 @@ pub struct IconDialog { impl IconDialog where - T: ParagraphStrType, + T: StringType, U: Component, { pub fn new(icon: BlendedImage, title: T, controls: U) -> Self { @@ -136,8 +136,8 @@ where let [l0, l1, l2, l3] = lines; Self { image: Child::new(BlendedImage::new( - Icon::new(theme::IMAGE_BG_CIRCLE), - Icon::new(theme::IMAGE_FG_SUCCESS), + theme::IMAGE_BG_CIRCLE, + theme::IMAGE_FG_SUCCESS, theme::SUCCESS_COLOR, theme::FG, theme::BG, @@ -161,7 +161,7 @@ where impl Component for IconDialog where - T: ParagraphStrType, + T: StringType, U: Component, { type Msg = DialogMsg; @@ -203,7 +203,7 @@ where #[cfg(feature = "ui_debug")] impl crate::trace::Trace for IconDialog where - T: ParagraphStrType, + T: StringType, U: crate::trace::Trace, { fn trace(&self, t: &mut dyn crate::trace::Tracer) { diff --git a/core/embed/rust/src/ui/model_tt/component/error.rs b/core/embed/rust/src/ui/model_tt/component/error.rs index 9f9a22642..e783a3ecd 100644 --- a/core/embed/rust/src/ui/model_tt/component/error.rs +++ b/core/embed/rust/src/ui/model_tt/component/error.rs @@ -1,7 +1,6 @@ use crate::ui::{ component::{Child, Component, Event, EventCtx, Label, Never, Pad}, constant::screen, - display::Icon, geometry::{Alignment::Center, Point, Rect, TOP_CENTER}, }; @@ -73,7 +72,7 @@ impl> Component for ErrorScreen<'_, T> { fn paint(&mut self) { self.bg.paint(); - let icon = Icon::new(ICON_WARNING40); + let icon = ICON_WARNING40; icon.draw( Point::new(screen().center().x, ICON_TOP), TOP_CENTER, diff --git a/core/embed/rust/src/ui/model_tt/component/frame.rs b/core/embed/rust/src/ui/model_tt/component/frame.rs index 82d1bec58..cfe4b387f 100644 --- a/core/embed/rust/src/ui/model_tt/component/frame.rs +++ b/core/embed/rust/src/ui/model_tt/component/frame.rs @@ -80,17 +80,11 @@ where } pub fn with_cancel_button(self) -> Self { - self.with_button( - Icon::new(theme::ICON_CORNER_CANCEL), - CancelInfoConfirmMsg::Cancelled, - ) + self.with_button(theme::ICON_CORNER_CANCEL, CancelInfoConfirmMsg::Cancelled) } pub fn with_info_button(self) -> Self { - self.with_button( - Icon::new(theme::ICON_CORNER_INFO), - CancelInfoConfirmMsg::Info, - ) + self.with_button(theme::ICON_CORNER_INFO, CancelInfoConfirmMsg::Info) } pub fn inner(&self) -> &T { diff --git a/core/embed/rust/src/ui/model_tt/component/hold_to_confirm.rs b/core/embed/rust/src/ui/model_tt/component/hold_to_confirm.rs index f132b32fc..6650901bb 100644 --- a/core/embed/rust/src/ui/model_tt/component/hold_to_confirm.rs +++ b/core/embed/rust/src/ui/model_tt/component/hold_to_confirm.rs @@ -2,7 +2,6 @@ use crate::{ time::Instant, ui::{ component::{Child, Component, ComponentExt, Event, EventCtx, FixedHeightBar, Pad}, - display::toif::Icon, geometry::{Grid, Insets, Rect}, util::animation_disabled, }, @@ -127,7 +126,7 @@ pub enum CancelHoldMsg { impl CancelHold { pub fn new(button_style: ButtonStyleSheet) -> FixedHeightBar { theme::button_bar(Self { - cancel: Some(Button::with_icon(Icon::new(theme::ICON_CANCEL)).into_child()), + cancel: Some(Button::with_icon(theme::ICON_CANCEL).into_child()), hold: Button::with_text("HOLD TO CONFIRM") .styled(button_style) .into_child(), diff --git a/core/embed/rust/src/ui/model_tt/component/homescreen/mod.rs b/core/embed/rust/src/ui/model_tt/component/homescreen/mod.rs index 4b66719e0..88d3ca3ac 100644 --- a/core/embed/rust/src/ui/model_tt/component/homescreen/mod.rs +++ b/core/embed/rust/src/ui/model_tt/component/homescreen/mod.rs @@ -1,8 +1,6 @@ mod render; use crate::{ - micropython::gc::Gc, - storage::{get_avatar, get_avatar_len}, time::{Duration, Instant}, trezorhal::usb::usb_configured, ui::{ @@ -10,6 +8,7 @@ use crate::{ display::{self, tjpgd::jpeg_info, toif::Icon, Color, Font}, event::{TouchEvent, USBEvent}, geometry::{Offset, Point, Rect}, + layout::util::get_user_custom_image, model_tt::{constant, theme::IMAGE_HOMESCREEN}, }, }; @@ -73,10 +72,10 @@ where fn level_to_style(level: u8) -> (Color, Icon) { match level { - 3 => (theme::YELLOW, Icon::new(theme::ICON_COINJOIN)), - 2 => (theme::VIOLET, Icon::new(theme::ICON_MAGIC)), - 1 => (theme::YELLOW, Icon::new(theme::ICON_WARN)), - _ => (theme::RED, Icon::new(theme::ICON_WARN)), + 3 => (theme::YELLOW, theme::ICON_COINJOIN), + 2 => (theme::VIOLET, theme::ICON_MAGIC), + 1 => (theme::YELLOW, theme::ICON_WARN), + _ => (theme::RED, theme::ICON_WARN), } } @@ -205,7 +204,7 @@ where let notification = self.get_notification(); - let res = get_image(); + let res = get_user_custom_image(); let mut show_default = true; if let Ok(data) = res { @@ -307,7 +306,7 @@ where text: locked, style: theme::TEXT_BOLD, offset: Offset::new(10, LOCKED_Y), - icon: Some(Icon::new(theme::ICON_LOCK)), + icon: Some(theme::ICON_LOCK), }, HomescreenText { text: tap, @@ -323,7 +322,7 @@ where }, ]; - let res = get_image(); + let res = get_user_custom_image(); let mut show_default = true; if let Ok(data) = res { @@ -352,19 +351,6 @@ where } } -fn get_image() -> Result, ()> { - if let Ok(len) = get_avatar_len() { - let result = Gc::<[u8]>::new_slice(len); - if let Ok(mut buffer) = result { - let buf = unsafe { Gc::<[u8]>::as_mut(&mut buffer) }; - if get_avatar(buf).is_ok() { - return Ok(buffer); - } - } - }; - Err(()) -} - fn is_image_jpeg(buffer: &[u8]) -> bool { let jpeg = jpeg_info(buffer); if let Some((size, mcu_height)) = jpeg { diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/bip39.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/bip39.rs index 5a6e205bd..fdd287f4e 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/bip39.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/bip39.rs @@ -3,7 +3,6 @@ use crate::{ ui::{ component::{text::common::TextBox, Component, Event, EventCtx}, display, - display::toif::Icon, geometry::{Offset, Rect, CENTER}, model_tt::{ component::{ @@ -218,14 +217,14 @@ impl Bip39Input { self.button.enable(ctx); self.button.set_stylesheet(ctx, theme::button_pin_confirm()); self.button - .set_content(ctx, ButtonContent::Icon(Icon::new(theme::ICON_LIST_CHECK))); + .set_content(ctx, ButtonContent::Icon(theme::ICON_LIST_CHECK)); } else { // Auto-complete button. self.button.enable(ctx); self.button .set_stylesheet(ctx, theme::button_pin_autocomplete()); self.button - .set_content(ctx, ButtonContent::Icon(Icon::new(theme::ICON_CLICK))); + .set_content(ctx, ButtonContent::Icon(theme::ICON_CLICK)); } } else { // Disabled button. diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/mnemonic.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/mnemonic.rs index e9511fe61..6b7425528 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/mnemonic.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/mnemonic.rs @@ -1,6 +1,5 @@ use crate::ui::{ component::{maybe::paint_overlapping, Child, Component, Event, EventCtx, Label, Maybe}, - display::toif::Icon, geometry::{Grid, Offset, Rect, CENTER}, model_tt::{ component::{Button, ButtonMsg}, @@ -39,8 +38,8 @@ where back: Child::new(Maybe::hidden( theme::BG, Button::with_icon_blend( - Icon::new(theme::IMAGE_BG_BACK_BTN_TALL), - Icon::new(theme::ICON_BACK), + theme::IMAGE_BG_BACK_BTN_TALL, + theme::ICON_BACK, Offset::new(30, 17), ) .styled(theme::button_reset()) diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs index 45ec59a43..d574cad5e 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs @@ -3,7 +3,6 @@ use crate::ui::{ base::ComponentExt, text::common::TextBox, Child, Component, Event, EventCtx, Never, }, display, - display::toif::Icon, geometry::{Grid, Offset, Rect}, model_tt::component::{ button::{Button, ButtonContent, ButtonMsg}, @@ -48,12 +47,12 @@ impl PassphraseKeyboard { Self { page_swipe: Swipe::horizontal(), input: Input::new().into_child(), - confirm: Button::with_icon(Icon::new(theme::ICON_CONFIRM)) + confirm: Button::with_icon(theme::ICON_CONFIRM) .styled(theme::button_confirm()) .into_child(), back: Button::with_icon_blend( - Icon::new(theme::IMAGE_BG_BACK_BTN), - Icon::new(theme::ICON_BACK), + theme::IMAGE_BG_BACK_BTN, + theme::ICON_BACK, Offset::new(30, 12), ) .styled(theme::button_reset()) @@ -80,7 +79,7 @@ impl PassphraseKeyboard { fn key_content(text: &'static str) -> ButtonContent<&'static str> { match text { - " " => ButtonContent::Icon(Icon::new(theme::ICON_SPACE)), + " " => ButtonContent::Icon(theme::ICON_SPACE), t => ButtonContent::Text(t), } } diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs index 74ef26d26..a6711b304 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs @@ -9,7 +9,7 @@ use crate::{ base::ComponentExt, text::TextStyle, Child, Component, Event, EventCtx, Label, Maybe, Never, Pad, TimerToken, }, - display::{self, toif::Icon, Font}, + display::{self, Font}, event::TouchEvent, geometry::{Grid, Insets, Offset, Rect, CENTER, TOP_LEFT}, model_tt::component::{ @@ -69,8 +69,8 @@ where ) -> Self { // Control buttons. let erase_btn = Button::with_icon_blend( - Icon::new(theme::IMAGE_BG_BACK_BTN), - Icon::new(theme::ICON_BACK), + theme::IMAGE_BG_BACK_BTN, + theme::ICON_BACK, Offset::new(30, 12), ) .styled(theme::button_reset()) @@ -78,8 +78,7 @@ where .initially_enabled(false); let erase_btn = Maybe::hidden(theme::BG, erase_btn).into_child(); - let cancel_btn = - Button::with_icon(Icon::new(theme::ICON_CANCEL)).styled(theme::button_cancel()); + let cancel_btn = Button::with_icon(theme::ICON_CANCEL).styled(theme::button_cancel()); let cancel_btn = Maybe::new(Pad::with_background(theme::BG), cancel_btn, allow_cancel).into_child(); @@ -95,7 +94,7 @@ where textbox_pad: Pad::with_background(theme::label_default().background_color), erase_btn, cancel_btn, - confirm_btn: Button::with_icon(Icon::new(theme::ICON_CONFIRM)) + confirm_btn: Button::with_icon(theme::ICON_CONFIRM) .styled(theme::button_confirm()) .initially_enabled(false) .into_child(), @@ -383,7 +382,7 @@ impl PinDots { // Small leftmost dot. if digits > dots_visible + 1 { - Icon::new(theme::DOT_SMALL).draw( + theme::DOT_SMALL.draw( cursor - Offset::x(2 * step), TOP_LEFT, self.style.text_color, @@ -393,7 +392,7 @@ impl PinDots { // Greyed out dot. if digits > dots_visible { - Icon::new(theme::DOT_ACTIVE).draw( + theme::DOT_ACTIVE.draw( cursor - Offset::x(step), TOP_LEFT, theme::GREY_LIGHT, @@ -403,7 +402,7 @@ impl PinDots { // Draw a dot for each PIN digit. for _ in 0..dots_visible { - Icon::new(theme::DOT_ACTIVE).draw( + theme::DOT_ACTIVE.draw( cursor, TOP_LEFT, self.style.text_color, diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/slip39.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/slip39.rs index 47ffb6fb7..45d65e9e5 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/slip39.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/slip39.rs @@ -10,7 +10,6 @@ use crate::{ Component, Event, EventCtx, }, display, - display::toif::Icon, geometry::{Offset, Rect, CENTER}, model_tt::{ component::{ @@ -230,7 +229,7 @@ impl Slip39Input { self.button.enable(ctx); self.button.set_stylesheet(ctx, theme::button_pin_confirm()); self.button - .set_content(ctx, ButtonContent::Icon(Icon::new(theme::ICON_LIST_CHECK))); + .set_content(ctx, ButtonContent::Icon(theme::ICON_LIST_CHECK)); } else { // Disabled button. self.button.disable(ctx); diff --git a/core/embed/rust/src/ui/model_tt/component/loader.rs b/core/embed/rust/src/ui/model_tt/component/loader.rs index 7e85bf209..a135154f9 100644 --- a/core/embed/rust/src/ui/model_tt/component/loader.rs +++ b/core/embed/rust/src/ui/model_tt/component/loader.rs @@ -12,6 +12,9 @@ use crate::{ use super::theme; +const GROWING_DURATION_MS: u32 = 1000; +const SHRINKING_DURATION_MS: u32 = 500; + pub enum LoaderMsg { GrownCompletely, ShrunkCompletely, @@ -38,8 +41,8 @@ impl Loader { Self { offset_y: 0, state: State::Initial, - growing_duration: Duration::from_millis(1000), - shrinking_duration: Duration::from_millis(500), + growing_duration: Duration::from_millis(GROWING_DURATION_MS), + shrinking_duration: Duration::from_millis(SHRINKING_DURATION_MS), styles: theme::loader_default(), } } diff --git a/core/embed/rust/src/ui/model_tt/component/number_input.rs b/core/embed/rust/src/ui/model_tt/component/number_input.rs index c36df7ae9..8edb37bbc 100644 --- a/core/embed/rust/src/ui/model_tt/component/number_input.rs +++ b/core/embed/rust/src/ui/model_tt/component/number_input.rs @@ -1,10 +1,10 @@ use crate::{ - strutil, + strutil::{self, StringType}, ui::{ component::{ base::ComponentExt, paginated::Paginate, - text::paragraphs::{Paragraph, ParagraphStrType, Paragraphs}, + text::paragraphs::{Paragraph, Paragraphs}, Child, Component, Event, EventCtx, Pad, }, display::{self, Font}, @@ -35,7 +35,7 @@ where impl NumberInputDialog where F: Fn(u32) -> T, - T: ParagraphStrType, + T: StringType, { pub fn new(min: u32, max: u32, init_value: u32, description_func: F) -> Self { let text = description_func(init_value); @@ -71,7 +71,7 @@ where impl Component for NumberInputDialog where - T: ParagraphStrType, + T: StringType, F: Fn(u32) -> T, { type Msg = NumberInputDialogMsg; @@ -133,7 +133,7 @@ where #[cfg(feature = "ui_debug")] impl crate::trace::Trace for NumberInputDialog where - T: ParagraphStrType, + T: StringType, F: Fn(u32) -> T, { fn trace(&self, t: &mut dyn crate::trace::Tracer) { diff --git a/core/embed/rust/src/ui/model_tt/component/page.rs b/core/embed/rust/src/ui/model_tt/component/page.rs index 23d252086..f16a81508 100644 --- a/core/embed/rust/src/ui/model_tt/component/page.rs +++ b/core/embed/rust/src/ui/model_tt/component/page.rs @@ -38,13 +38,12 @@ impl ButtonPrevCancels { } fn icon(&self, is_first_page: bool) -> Icon { - let data = match self { + match self { ButtonPrevCancels::Never => theme::ICON_UP, ButtonPrevCancels::FirstPage if is_first_page => theme::ICON_CANCEL, ButtonPrevCancels::FirstPage => theme::ICON_UP, ButtonPrevCancels::AnyPage => theme::ICON_BACK, - }; - Icon::new(data) + } } } @@ -79,8 +78,8 @@ where scrollbar: ScrollBar::vertical(), swipe: Swipe::new(), pad: Pad::with_background(background), - button_prev: Button::with_icon(Icon::new(theme::ICON_UP)).initially_enabled(false), - button_next: Button::with_icon(Icon::new(theme::ICON_DOWN)), + button_prev: Button::with_icon(theme::ICON_UP).initially_enabled(false), + button_next: Button::with_icon(theme::ICON_DOWN), button_prev_cancels: ButtonPrevCancels::Never, is_go_back: None, swipe_left: false, @@ -91,13 +90,13 @@ where pub fn with_back_button(mut self) -> Self { self.button_prev_cancels = ButtonPrevCancels::AnyPage; - self.button_prev = Button::with_icon(Icon::new(theme::ICON_BACK)).initially_enabled(true); + self.button_prev = Button::with_icon(theme::ICON_BACK).initially_enabled(true); self } pub fn with_cancel_on_first_page(mut self) -> Self { self.button_prev_cancels = ButtonPrevCancels::FirstPage; - self.button_prev = Button::with_icon(Icon::new(theme::ICON_CANCEL)).initially_enabled(true); + self.button_prev = Button::with_icon(theme::ICON_CANCEL).initially_enabled(true); self } @@ -497,10 +496,11 @@ mod tests { use serde_json; use crate::{ + strutil::SkipPrefix, trace::tests::trace, ui::{ component::{ - text::paragraphs::{Paragraph, ParagraphStrType, Paragraphs}, + text::paragraphs::{Paragraph, Paragraphs}, Empty, }, event::TouchEvent, @@ -513,7 +513,7 @@ mod tests { const SCREEN: Rect = constant::screen().inset(theme::borders()); - impl ParagraphStrType for &'static str { + impl SkipPrefix for &str { fn skip_prefix(&self, chars: usize) -> Self { &self[chars..] } diff --git a/core/embed/rust/src/ui/model_tt/component/progress.rs b/core/embed/rust/src/ui/model_tt/component/progress.rs index c27c52309..c6083ff75 100644 --- a/core/embed/rust/src/ui/model_tt/component/progress.rs +++ b/core/embed/rust/src/ui/model_tt/component/progress.rs @@ -2,11 +2,12 @@ use core::mem; use crate::{ error::Error, + strutil::StringType, ui::{ component::{ base::ComponentExt, paginated::Paginate, - text::paragraphs::{Paragraph, ParagraphStrType, Paragraphs}, + text::paragraphs::{Paragraph, Paragraphs}, Child, Component, Event, EventCtx, Label, Never, Pad, }, display::{self, Font}, @@ -30,7 +31,7 @@ pub struct Progress { impl Progress where - T: ParagraphStrType, + T: StringType, { const AREA: Rect = constant::screen().inset(theme::borders()); @@ -57,7 +58,7 @@ where impl Component for Progress where - T: ParagraphStrType, + T: StringType, { type Msg = Never; @@ -130,7 +131,7 @@ where #[cfg(feature = "ui_debug")] impl crate::trace::Trace for Progress where - T: ParagraphStrType, + T: StringType, { fn trace(&self, t: &mut dyn crate::trace::Tracer) { t.component("Progress"); diff --git a/core/embed/rust/src/ui/model_tt/component/result.rs b/core/embed/rust/src/ui/model_tt/component/result.rs index 804949e4e..71cf6956e 100644 --- a/core/embed/rust/src/ui/model_tt/component/result.rs +++ b/core/embed/rust/src/ui/model_tt/component/result.rs @@ -1,12 +1,12 @@ -use crate::ui::{ - component::{ - text::{paragraphs::ParagraphStrType, TextStyle}, - Child, Component, Event, EventCtx, Label, Never, Pad, +use crate::{ + strutil::StringType, + ui::{ + component::{text::TextStyle, Child, Component, Event, EventCtx, Label, Never, Pad}, + constant::screen, + display::{self, Color, Font, Icon}, + geometry::{Alignment::Center, Insets, Offset, Point, Rect, CENTER}, + model_tt::theme::FG, }, - constant::screen, - display::{self, Color, Font, Icon}, - geometry::{Alignment::Center, Insets, Offset, Point, Rect, CENTER}, - model_tt::theme::FG, }; use crate::ui::model_tt::{ @@ -104,7 +104,7 @@ pub struct ResultScreen<'a, T> { footer: Child>, } -impl<'a, T: ParagraphStrType> ResultScreen<'a, T> { +impl<'a, T: StringType> ResultScreen<'a, T> { pub fn new( style: &'a ResultStyle, icon: Icon, @@ -130,7 +130,7 @@ impl<'a, T: ParagraphStrType> ResultScreen<'a, T> { } } -impl Component for ResultScreen<'_, T> { +impl Component for ResultScreen<'_, T> { type Msg = Never; fn place(&mut self, _bounds: Rect) -> Rect { diff --git a/core/embed/rust/src/ui/model_tt/component/scroll.rs b/core/embed/rust/src/ui/model_tt/component/scroll.rs index 0f33c49bf..caaca7266 100644 --- a/core/embed/rust/src/ui/model_tt/component/scroll.rs +++ b/core/embed/rust/src/ui/model_tt/component/scroll.rs @@ -78,9 +78,9 @@ impl Component for ScrollBar { fn paint(&mut self) { fn dotsize(distance: usize, nhidden: usize) -> Icon { match (nhidden.saturating_sub(distance)).min(2 - distance) { - 0 => Icon::new(theme::DOT_INACTIVE), - 1 => Icon::new(theme::DOT_INACTIVE_HALF), - _ => Icon::new(theme::DOT_INACTIVE_QUARTER), + 0 => theme::DOT_INACTIVE, + 1 => theme::DOT_INACTIVE_HALF, + _ => theme::DOT_INACTIVE_QUARTER, } } @@ -100,7 +100,7 @@ impl Component for ScrollBar { ); for i in first_shown..(last_shown + 1) { let icon = if i == self.active_page { - Icon::new(theme::DOT_ACTIVE) + theme::DOT_ACTIVE } else if i <= first_shown + 1 { let before_first_shown = first_shown; dotsize(i - first_shown, before_first_shown) @@ -108,7 +108,7 @@ impl Component for ScrollBar { let after_last_shown = self.page_count - 1 - last_shown; dotsize(last_shown - i, after_last_shown) } else { - Icon::new(theme::DOT_INACTIVE) + theme::DOT_INACTIVE }; icon.draw(cursor, CENTER, theme::FG, theme::BG); cursor = cursor + Offset::on_axis(self.layout.axis, Self::DOT_INTERVAL); diff --git a/core/embed/rust/src/ui/model_tt/component/welcome_screen.rs b/core/embed/rust/src/ui/model_tt/component/welcome_screen.rs index 11737ec8e..3585f0424 100644 --- a/core/embed/rust/src/ui/model_tt/component/welcome_screen.rs +++ b/core/embed/rust/src/ui/model_tt/component/welcome_screen.rs @@ -4,6 +4,7 @@ use crate::ui::display; use crate::ui::model_tt::bootloader::theme::DEVICE_NAME; use crate::ui::{ component::{Component, Event, EventCtx, Never}, + constant::MODEL_NAME, display::Icon, geometry::{self, Offset, Rect}, model_tt::theme, @@ -11,7 +12,6 @@ use crate::ui::{ const TEXT_BOTTOM_MARGIN: i16 = 24; // matching the homescreen label margin const ICON_TOP_MARGIN: i16 = 48; -const MODEL_NAME: &str = "Trezor Model T"; #[cfg(not(feature = "bootloader"))] const MODEL_NAME_FONT: display::Font = display::Font::DEMIBOLD; @@ -38,7 +38,7 @@ impl Component for WelcomeScreen { } fn paint(&mut self) { - Icon::new(theme::ICON_LOGO).draw( + theme::ICON_LOGO.draw( self.area.top_center() + Offset::y(ICON_TOP_MARGIN), geometry::TOP_CENTER, theme::FG, diff --git a/core/embed/rust/src/ui/model_tt/constant.rs b/core/embed/rust/src/ui/model_tt/constant.rs index bf6b30c5a..f676e7d81 100644 --- a/core/embed/rust/src/ui/model_tt/constant.rs +++ b/core/embed/rust/src/ui/model_tt/constant.rs @@ -9,6 +9,8 @@ pub const LOADER_OUTER: i16 = 60; pub const LOADER_INNER: i16 = 42; pub const LOADER_ICON_MAX_SIZE: i16 = 64; +pub const MODEL_NAME: &str = "Trezor Model T"; + pub const fn size() -> Offset { Offset::new(WIDTH, HEIGHT) } diff --git a/core/embed/rust/src/ui/model_tt/layout.rs b/core/embed/rust/src/ui/model_tt/layout.rs index 3ea0fc14a..c57fb7d38 100644 --- a/core/embed/rust/src/ui/model_tt/layout.rs +++ b/core/embed/rust/src/ui/model_tt/layout.rs @@ -14,6 +14,7 @@ use crate::{ qstr::Qstr, util, }, + strutil::StringType, ui::{ component::{ base::ComponentExt, @@ -23,21 +24,21 @@ use crate::{ placed::GridPlaced, text::{ paragraphs::{ - Checklist, Paragraph, ParagraphSource, ParagraphStrType, ParagraphVecLong, - ParagraphVecShort, Paragraphs, VecExt, + Checklist, Paragraph, ParagraphSource, ParagraphVecLong, ParagraphVecShort, + Paragraphs, VecExt, }, TextStyle, }, - Border, Component, Empty, FormattedText, Never, Qr, Timeout, TimeoutMsg, + Border, Component, Empty, FormattedText, Never, Qr, Timeout, }, - display::{self, tjpgd::jpeg_info, toif::Icon}, + display::{self, tjpgd::jpeg_info}, geometry, layout::{ obj::{ComponentMsgObj, LayoutObj}, result::{CANCELLED, CONFIRMED, INFO}, util::{ - iter_into_array, iter_into_objs, upy_disable_animation, upy_jpeg_info, - upy_jpeg_test, ConfirmBlob, PropsList, + iter_into_array, upy_disable_animation, upy_jpeg_info, upy_jpeg_test, ConfirmBlob, + PropsList, }, }, }, @@ -102,7 +103,7 @@ impl TryFrom for Obj { impl ComponentMsgObj for FidoConfirm where F: Fn(usize) -> T, - T: ParagraphStrType + From<&'static str>, + T: StringType, U: Component, { fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { @@ -129,7 +130,7 @@ where impl ComponentMsgObj for IconDialog where - T: ParagraphStrType, + T: StringType, U: Component, ::Msg: TryInto, { @@ -257,6 +258,9 @@ where } } +// Clippy/compiler complains about conflicting implementations +// TODO move the common impls to a common module +#[cfg(not(feature = "clippy"))] impl ComponentMsgObj for Paragraphs where T: ParagraphSource, @@ -277,7 +281,7 @@ where impl ComponentMsgObj for NumberInputDialog where - T: ParagraphStrType, + T: StringType, F: Fn(u32) -> T, { fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { @@ -300,7 +304,7 @@ where impl ComponentMsgObj for Progress where - T: ParagraphStrType, + T: StringType, { fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result { unreachable!() @@ -329,24 +333,24 @@ where } } -impl ComponentMsgObj for (GridPlaced>, GridPlaced>) +impl ComponentMsgObj for (GridPlaced>, GridPlaced>) where T: ParagraphSource, - S: AsRef, + S: StringType + Clone, { fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result { unreachable!() } } +// Clippy/compiler complains about conflicting implementations +#[cfg(not(feature = "clippy"))] impl ComponentMsgObj for (Timeout, T) where - T: Component, + T: Component, { fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { - match msg { - TimeoutMsg::TimedOut => Ok(CANCELLED.as_obj()), - } + Ok(CANCELLED.as_obj()) } } @@ -372,7 +376,7 @@ where impl ComponentMsgObj for AddressDetails where - T: ParagraphStrType + Clone, + T: StringType + Clone, { fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result { Ok(CANCELLED.as_obj()) @@ -640,7 +644,7 @@ extern "C" fn new_confirm_homescreen(n_args: usize, args: *const Obj, kwargs: *m let size = match jpeg_info(buffer_func()) { Some(info) => info.0, - _ => return Err(Error::ValueError(cstr!("Invalid image."))), + _ => return Err(value_error!("Invalid image.")), }; let buttons = Button::cancel_confirm_text(None, Some("CONFIRM")); @@ -671,7 +675,7 @@ extern "C" fn new_confirm_reset_device(n_args: usize, args: *const Obj, kwargs: Paragraph::new(&theme::TEXT_DEMIBOLD, StrBuffer::from("trezor.io/tos")), ]); let buttons = Button::cancel_confirm( - Button::with_icon(Icon::new(theme::ICON_CANCEL)), + Button::with_icon(theme::ICON_CANCEL), Button::with_text(button).styled(theme::button_confirm()), true, ); @@ -819,7 +823,7 @@ extern "C" fn new_confirm_modify_output(n_args: usize, args: *const Obj, kwargs: ]); let buttons = Button::cancel_confirm( - Button::with_icon(Icon::new(theme::ICON_CANCEL)), + Button::with_icon(theme::ICON_CANCEL), Button::with_text("CONFIRM").styled(theme::button_confirm()), true, ); @@ -889,9 +893,7 @@ fn new_show_modal( IconDialog::new( icon, title, - Timeout::new(time_ms).map(|msg| { - (matches!(msg, TimeoutMsg::TimedOut)).then(|| CancelConfirmMsg::Confirmed) - }), + Timeout::new(time_ms).map(|_| Some(CancelConfirmMsg::Confirmed)), ) .with_description(description), )? @@ -903,7 +905,7 @@ fn new_show_modal( icon, title, Button::cancel_confirm( - Button::with_icon(Icon::new(theme::ICON_CANCEL)), + Button::with_icon(theme::ICON_CANCEL), Button::with_text(button).styled(button_style), false, ), @@ -932,8 +934,8 @@ fn new_show_modal( extern "C" fn new_show_error(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { let icon = BlendedImage::new( - Icon::new(theme::IMAGE_BG_CIRCLE), - Icon::new(theme::IMAGE_FG_ERROR), + theme::IMAGE_BG_CIRCLE, + theme::IMAGE_FG_ERROR, theme::ERROR_COLOR, theme::FG, theme::BG, @@ -961,7 +963,7 @@ extern "C" fn new_confirm_fido(n_args: usize, args: *const Obj, kwargs: *mut Map }; let controls = Button::cancel_confirm( - Button::with_icon(Icon::new(theme::ICON_CANCEL)), + Button::with_icon(theme::ICON_CANCEL), Button::with_text("CONFIRM").styled(theme::button_confirm()), true, ); @@ -977,8 +979,8 @@ extern "C" fn new_confirm_fido(n_args: usize, args: *const Obj, kwargs: *mut Map extern "C" fn new_show_warning(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { let icon = BlendedImage::new( - Icon::new(theme::IMAGE_BG_OCTAGON), - Icon::new(theme::IMAGE_FG_WARN), + theme::IMAGE_BG_OCTAGON, + theme::IMAGE_FG_WARN, theme::WARN_COLOR, theme::FG, theme::BG, @@ -991,8 +993,8 @@ extern "C" fn new_show_warning(n_args: usize, args: *const Obj, kwargs: *mut Map extern "C" fn new_show_success(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { let icon = BlendedImage::new( - Icon::new(theme::IMAGE_BG_CIRCLE), - Icon::new(theme::IMAGE_FG_SUCCESS), + theme::IMAGE_BG_CIRCLE, + theme::IMAGE_FG_SUCCESS, theme::SUCCESS_COLOR, theme::FG, theme::BG, @@ -1005,8 +1007,8 @@ extern "C" fn new_show_success(n_args: usize, args: *const Obj, kwargs: *mut Map extern "C" fn new_show_info(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { let icon = BlendedImage::new( - Icon::new(theme::IMAGE_BG_CIRCLE), - Icon::new(theme::IMAGE_FG_INFO), + theme::IMAGE_BG_CIRCLE, + theme::IMAGE_FG_INFO, theme::INFO_COLOR, theme::FG, theme::BG, @@ -1024,8 +1026,8 @@ extern "C" fn new_show_mismatch() -> Obj { let button = "QUIT"; let icon = BlendedImage::new( - Icon::new(theme::IMAGE_BG_OCTAGON), - Icon::new(theme::IMAGE_FG_WARN), + theme::IMAGE_BG_OCTAGON, + theme::IMAGE_FG_WARN, theme::WARN_COLOR, theme::FG, theme::BG, @@ -1035,7 +1037,7 @@ extern "C" fn new_show_mismatch() -> Obj { icon, title, Button::cancel_confirm( - Button::with_icon(Icon::new(theme::ICON_BACK)), + Button::with_icon(theme::ICON_BACK), Button::with_text(button).styled(theme::button_reset()), true, ), @@ -1109,7 +1111,7 @@ extern "C" fn new_confirm_with_info(n_args: usize, args: *const Obj, kwargs: *mu let mut iter_buf = IterBuf::new(); let iter = Iter::try_from_obj_with_buf(items, &mut iter_buf)?; for para in iter { - let [font, text]: [Obj; 2] = iter_into_objs(para)?; + let [font, text]: [Obj; 2] = iter_into_array(para)?; let style: &TextStyle = theme::textstyle_number(font.try_into()?); let text: StrBuffer = text.try_into()?; paragraphs.add(Paragraph::new(style, text)); @@ -1141,7 +1143,7 @@ extern "C" fn new_confirm_more(n_args: usize, args: *const Obj, kwargs: *mut Map let mut iter_buf = IterBuf::new(); let iter = Iter::try_from_obj_with_buf(items, &mut iter_buf)?; for para in iter { - let [font, text]: [Obj; 2] = iter_into_objs(para)?; + let [font, text]: [Obj; 2] = iter_into_array(para)?; let style: &TextStyle = theme::textstyle_number(font.try_into()?); let text: StrBuffer = text.try_into()?; paragraphs.add(Paragraph::new(style, text)); @@ -1325,8 +1327,8 @@ extern "C" fn new_show_checklist(n_args: usize, args: *const Obj, kwargs: *mut M title, Dialog::new( Checklist::from_paragraphs( - Icon::new(theme::ICON_LIST_CURRENT), - Icon::new(theme::ICON_LIST_CHECK), + theme::ICON_LIST_CURRENT, + theme::ICON_LIST_CHECK, active, paragraphs .into_paragraphs() @@ -1507,7 +1509,10 @@ extern "C" fn new_show_progress_coinjoin(n_args: usize, args: *const Obj, kwargs extern "C" fn new_show_homescreen(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { - let label: StrBuffer = kwargs.get(Qstr::MP_QSTR_label)?.try_into()?; + let label: StrBuffer = kwargs + .get(Qstr::MP_QSTR_label)? + .try_into_option()? + .unwrap_or_else(|| constant::MODEL_NAME.into()); let notification: Option = kwargs.get(Qstr::MP_QSTR_notification)?.try_into_option()?; let notification_level: u8 = kwargs.get_or(Qstr::MP_QSTR_notification_level, 0)?; @@ -1526,7 +1531,10 @@ extern "C" fn new_show_homescreen(n_args: usize, args: *const Obj, kwargs: *mut extern "C" fn new_show_lockscreen(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { - let label: StrBuffer = kwargs.get(Qstr::MP_QSTR_label)?.try_into()?; + let label: StrBuffer = kwargs + .get(Qstr::MP_QSTR_label)? + .try_into_option()? + .unwrap_or_else(|| constant::MODEL_NAME.into()); let bootscreen: bool = kwargs.get(Qstr::MP_QSTR_bootscreen)?.try_into()?; let skip_first_paint: bool = kwargs.get(Qstr::MP_QSTR_skip_first_paint)?.try_into()?; @@ -1928,7 +1936,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// def show_homescreen( /// *, - /// label: str, + /// label: str | None, /// hold: bool, /// notification: str | None, /// notification_level: int = 0, @@ -1939,7 +1947,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// def show_lockscreen( /// *, - /// label: str, + /// label: str | None, /// bootscreen: bool, /// skip_first_paint: bool, /// ) -> CANCELLED: @@ -1957,7 +1965,7 @@ mod tests { use crate::{ trace::tests::trace, - ui::{geometry::Rect, model_tt::constant}, + ui::{component::text::op::OpTextLayout, geometry::Rect, model_tt::constant}, }; use super::*; @@ -1968,15 +1976,12 @@ mod tests { fn trace_example_layout() { let buttons = Button::cancel_confirm(Button::with_text("Left"), Button::with_text("Right"), false); - let mut layout = Dialog::new( - FormattedText::new( - theme::TEXT_NORMAL, - theme::FORMATTED, - "Testing text layout, with some text, and some more text. And {param}", - ) - .with("param", "parameters!"), - buttons, - ); + + let ops = OpTextLayout::new(theme::TEXT_NORMAL) + .text_normal("Testing text layout, with some text, and some more text. And ") + .text_bold("parameters!"); + let formatted = FormattedText::new(ops); + let mut layout = Dialog::new(formatted, buttons); layout.place(SCREEN); let expected = serde_json::json!({ @@ -1984,7 +1989,7 @@ mod tests { "content": { "component": "FormattedText", "text": ["Testing text layout, with", "\n", "some text, and some", "\n", - "more text. And ", "parame", "-", "\n", "ters!"], + "more text. And ", "paramet", "-", "\n", "ers!"], "fits": true, }, "controls": { diff --git a/core/embed/rust/src/ui/model_tt/theme.rs b/core/embed/rust/src/ui/model_tt/theme.rs index 1304f9441..303580606 100644 --- a/core/embed/rust/src/ui/model_tt/theme.rs +++ b/core/embed/rust/src/ui/model_tt/theme.rs @@ -2,7 +2,7 @@ use crate::{ time::Duration, ui::{ component::{ - text::{formatted::FormattedFonts, LineBreaking, PageBreaking, TextStyle}, + text::{LineBreaking, PageBreaking, TextStyle}, FixedHeightBar, }, display::{Color, Font, Icon}, @@ -53,62 +53,65 @@ pub const QR_SIDE_MAX: u32 = 140; // UI icons (greyscale). // Button icons. -pub const ICON_CANCEL: &[u8] = include_res!("model_tt/res/x24.toif"); -pub const ICON_CONFIRM: &[u8] = include_res!("model_tt/res/check24.toif"); -pub const ICON_SPACE: &[u8] = include_res!("model_tt/res/space.toif"); -pub const ICON_BACK: &[u8] = include_res!("model_tt/res/caret-left24.toif"); -pub const ICON_FORWARD: &[u8] = include_res!("model_tt/res/caret-right24.toif"); -pub const ICON_UP: &[u8] = include_res!("model_tt/res/caret-up24.toif"); -pub const ICON_DOWN: &[u8] = include_res!("model_tt/res/caret-down24.toif"); -pub const ICON_CLICK: &[u8] = include_res!("model_tt/res/finger24.toif"); - -pub const ICON_CORNER_CANCEL: &[u8] = include_res!("model_tt/res/x32.toif"); -pub const ICON_CORNER_INFO: &[u8] = include_res!("model_tt/res/info32.toif"); +include_icon!(ICON_CANCEL, "model_tt/res/x24.toif"); +include_icon!(ICON_CONFIRM, "model_tt/res/check24.toif"); +include_icon!(ICON_SPACE, "model_tt/res/space.toif"); +include_icon!(ICON_BACK, "model_tt/res/caret-left24.toif"); +include_icon!(ICON_FORWARD, "model_tt/res/caret-right24.toif"); +include_icon!(ICON_UP, "model_tt/res/caret-up24.toif"); +include_icon!(ICON_DOWN, "model_tt/res/caret-down24.toif"); +include_icon!(ICON_CLICK, "model_tt/res/finger24.toif"); + +include_icon!(ICON_CORNER_CANCEL, "model_tt/res/x32.toif"); +include_icon!(ICON_CORNER_INFO, "model_tt/res/info32.toif"); // Checklist symbols. -pub const ICON_LIST_CURRENT: &[u8] = include_res!("model_tt/res/arrow-right16.toif"); -pub const ICON_LIST_CHECK: &[u8] = include_res!("model_tt/res/check16.toif"); +include_icon!(ICON_LIST_CURRENT, "model_tt/res/arrow-right16.toif"); +include_icon!(ICON_LIST_CHECK, "model_tt/res/check16.toif"); // Homescreen notifications. -pub const ICON_WARN: &[u8] = include_res!("model_tt/res/warning16.toif"); -pub const ICON_WARNING40: &[u8] = include_res!("model_tt/res/warning40.toif"); -pub const ICON_LOCK: &[u8] = include_res!("model_tt/res/lock16.toif"); -pub const ICON_COINJOIN: &[u8] = include_res!("model_tt/res/coinjoin16.toif"); -pub const ICON_MAGIC: &[u8] = include_res!("model_tt/res/magic.toif"); +include_icon!(ICON_WARN, "model_tt/res/warning16.toif"); +include_icon!(ICON_WARNING40, "model_tt/res/warning40.toif"); +include_icon!(ICON_LOCK, "model_tt/res/lock16.toif"); +include_icon!(ICON_COINJOIN, "model_tt/res/coinjoin16.toif"); +include_icon!(ICON_MAGIC, "model_tt/res/magic.toif"); // Text arrows. -pub const ICON_PAGE_NEXT: &[u8] = include_res!("model_tt/res/page-next.toif"); -pub const ICON_PAGE_PREV: &[u8] = include_res!("model_tt/res/page-prev.toif"); +include_icon!(ICON_PAGE_NEXT, "model_tt/res/page-next.toif"); +include_icon!(ICON_PAGE_PREV, "model_tt/res/page-prev.toif"); // Large, three-color icons. pub const WARN_COLOR: Color = YELLOW; pub const INFO_COLOR: Color = BLUE; pub const SUCCESS_COLOR: Color = GREEN; pub const ERROR_COLOR: Color = RED; -pub const IMAGE_FG_WARN: &[u8] = include_res!("model_tt/res/fg-warning48.toif"); -pub const IMAGE_FG_SUCCESS: &[u8] = include_res!("model_tt/res/fg-check48.toif"); -pub const IMAGE_FG_ERROR: &[u8] = include_res!("model_tt/res/fg-error48.toif"); -pub const IMAGE_FG_INFO: &[u8] = include_res!("model_tt/res/fg-info48.toif"); -pub const IMAGE_FG_USER: &[u8] = include_res!("model_tt/res/fg-user48.toif"); -pub const IMAGE_BG_CIRCLE: &[u8] = include_res!("model_tt/res/circle48.toif"); -pub const IMAGE_BG_OCTAGON: &[u8] = include_res!("model_tt/res/octagon48.toif"); +include_icon!(IMAGE_FG_WARN, "model_tt/res/fg-warning48.toif"); +include_icon!(IMAGE_FG_SUCCESS, "model_tt/res/fg-check48.toif"); +include_icon!(IMAGE_FG_ERROR, "model_tt/res/fg-error48.toif"); +include_icon!(IMAGE_FG_INFO, "model_tt/res/fg-info48.toif"); +include_icon!(IMAGE_FG_USER, "model_tt/res/fg-user48.toif"); +include_icon!(IMAGE_BG_CIRCLE, "model_tt/res/circle48.toif"); +include_icon!(IMAGE_BG_OCTAGON, "model_tt/res/octagon48.toif"); // Non-square button backgrounds. -pub const IMAGE_BG_BACK_BTN: &[u8] = include_res!("model_tt/res/bg-back40.toif"); -pub const IMAGE_BG_BACK_BTN_TALL: &[u8] = include_res!("model_tt/res/bg-back52.toif"); +include_icon!(IMAGE_BG_BACK_BTN, "model_tt/res/bg-back40.toif"); +include_icon!(IMAGE_BG_BACK_BTN_TALL, "model_tt/res/bg-back52.toif"); // Welcome screen. -pub const ICON_LOGO: &[u8] = include_res!("model_tt/res/lock_full.toif"); +include_icon!(ICON_LOGO, "model_tt/res/lock_full.toif"); // Default homescreen pub const IMAGE_HOMESCREEN: &[u8] = include_res!("model_tt/res/bg.jpg"); // Scrollbar/PIN dots. -pub const DOT_ACTIVE: &[u8] = include_res!("model_tt/res/scroll-active.toif"); -pub const DOT_INACTIVE: &[u8] = include_res!("model_tt/res/scroll-inactive.toif"); -pub const DOT_INACTIVE_HALF: &[u8] = include_res!("model_tt/res/scroll-inactive-half.toif"); -pub const DOT_INACTIVE_QUARTER: &[u8] = include_res!("model_tt/res/scroll-inactive-quarter.toif"); -pub const DOT_SMALL: &[u8] = include_res!("model_tt/res/scroll-small.toif"); +include_icon!(DOT_ACTIVE, "model_tt/res/scroll-active.toif"); +include_icon!(DOT_INACTIVE, "model_tt/res/scroll-inactive.toif"); +include_icon!(DOT_INACTIVE_HALF, "model_tt/res/scroll-inactive-half.toif"); +include_icon!( + DOT_INACTIVE_QUARTER, + "model_tt/res/scroll-inactive-quarter.toif" +); +include_icon!(DOT_SMALL, "model_tt/res/scroll-small.toif"); pub const fn label_default() -> TextStyle { TEXT_NORMAL @@ -507,8 +510,8 @@ pub const TEXT_BOLD: TextStyle = TextStyle::new(Font::BOLD, FG, BG, GREY_LIGHT, pub const TEXT_MONO: TextStyle = TextStyle::new(Font::MONO, FG, BG, GREY_LIGHT, GREY_LIGHT) .with_line_breaking(LineBreaking::BreakWordsNoHyphen) .with_page_breaking(PageBreaking::CutAndInsertEllipsisBoth) - .with_ellipsis_icon(Icon::new(ICON_PAGE_NEXT), 0) - .with_prev_page_icon(Icon::new(ICON_PAGE_PREV), 0); + .with_ellipsis_icon(ICON_PAGE_NEXT, 0) + .with_prev_page_icon(ICON_PAGE_PREV, 0); /// Convert Python-side numeric id to a `TextStyle`. pub fn textstyle_number(num: i32) -> &'static TextStyle { @@ -530,13 +533,6 @@ pub const TEXT_CHECKLIST_SELECTED: TextStyle = pub const TEXT_CHECKLIST_DONE: TextStyle = TextStyle::new(Font::NORMAL, GREEN_DARK, BG, GREY_LIGHT, GREY_LIGHT); -pub const FORMATTED: FormattedFonts = FormattedFonts { - normal: Font::NORMAL, - demibold: Font::DEMIBOLD, - bold: Font::BOLD, - mono: Font::MONO, -}; - pub const CONTENT_BORDER: i16 = 0; pub const BUTTON_HEIGHT: i16 = 50; pub const BUTTON_WIDTH: i16 = 56; diff --git a/core/embed/rust/src/ui/util.rs b/core/embed/rust/src/ui/util.rs index 6c00cdade..1d02e20bc 100644 --- a/core/embed/rust/src/ui/util.rs +++ b/core/embed/rust/src/ui/util.rs @@ -1,8 +1,11 @@ -use crate::ui::{ - component::text::TextStyle, - display, - display::toif::Icon, - geometry::{Offset, Point, CENTER}, +use crate::{ + strutil::ShortString, + ui::{ + component::text::TextStyle, + display, + display::toif::Icon, + geometry::{Offset, Point, CENTER}, + }, }; use cstr_core::CStr; @@ -18,6 +21,8 @@ impl ResultExt for Result { fn assert_if_debugging_ui(self, #[allow(unused)] message: &str) { #[cfg(feature = "ui_debug")] if self.is_err() { + print!("Panic from assert_if_debugging_ui: "); + println!(message); panic!("{}", message); } } @@ -122,17 +127,24 @@ pub fn icon_text_center( ); } +/// Convert char to a ShortString. +pub fn char_to_string(ch: char) -> ShortString { + let mut s = String::new(); + unwrap!(s.push(ch)); + s +} + /// Returns text to be fit on one line of a given length. /// When the text is too long to fit, it is truncated with ellipsis /// on the left side. -// Hardcoding 50 as the length of the returned String - there should -// not be any lines as long as this. +/// Hardcoding 50 (via ShortString) as the length of the returned String - +/// there should not be any lines as long as this. pub fn long_line_content_with_ellipsis( text: &str, ellipsis: &str, text_font: Font, available_width: i16, -) -> String<50> { +) -> ShortString { if text_font.text_width(text) <= available_width { String::from(text) // whole text can fit } else { @@ -147,6 +159,13 @@ pub fn long_line_content_with_ellipsis( } } +#[macro_export] +macro_rules! include_icon { + ($name:ident, $path:expr) => { + pub const $name: Icon = Icon::debug_named(include_res!($path), stringify!($name)); + }; +} + #[cfg(test)] mod tests { use crate::strutil; diff --git a/core/embed/unix/background_R.h b/core/embed/unix/background_R.h deleted file mode 100644 index 6fcd52e18..000000000 --- a/core/embed/unix/background_R.h +++ /dev/null @@ -1,1593 +0,0 @@ -// clang-format off -unsigned char background_R_jpg[] = { - 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, - 0x01, 0x02, 0x00, 0x1c, 0x00, 0x1c, 0x00, 0x00, 0xff, 0xe1, 0x12, 0x90, - 0x45, 0x78, 0x69, 0x66, 0x00, 0x00, 0x49, 0x49, 0x2a, 0x00, 0x08, 0x00, - 0x00, 0x00, 0x07, 0x00, 0x12, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x1a, 0x01, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x62, 0x00, 0x00, 0x00, 0x1b, 0x01, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x6a, 0x00, 0x00, 0x00, 0x28, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x31, 0x01, 0x02, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x72, 0x00, 0x00, 0x00, 0x32, 0x01, 0x02, 0x00, 0x14, 0x00, 0x00, 0x00, - 0x80, 0x00, 0x00, 0x00, 0x69, 0x87, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x94, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x47, 0x49, 0x4d, 0x50, 0x20, 0x32, 0x2e, 0x31, 0x30, 0x2e, 0x33, 0x30, - 0x00, 0x00, 0x32, 0x30, 0x32, 0x32, 0x3a, 0x30, 0x34, 0x3a, 0x32, 0x33, - 0x20, 0x32, 0x31, 0x3a, 0x31, 0x38, 0x3a, 0x30, 0x39, 0x00, 0x01, 0x00, - 0x01, 0xa0, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0xfe, 0x00, 0x04, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x04, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x96, 0x00, 0x00, 0x00, 0x01, 0x01, 0x04, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x01, 0x03, 0x00, 0x03, 0x00, - 0x00, 0x00, 0x18, 0x01, 0x00, 0x00, 0x03, 0x01, 0x03, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x15, 0x01, 0x03, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x02, 0x04, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x1e, 0x01, 0x00, 0x00, 0x02, 0x02, 0x04, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x6a, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, - 0x08, 0x00, 0x08, 0x00, 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, - 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, - 0xff, 0xdb, 0x00, 0x43, 0x00, 0x08, 0x06, 0x06, 0x07, 0x06, 0x05, 0x08, - 0x07, 0x07, 0x07, 0x09, 0x09, 0x08, 0x0a, 0x0c, 0x14, 0x0d, 0x0c, 0x0b, - 0x0b, 0x0c, 0x19, 0x12, 0x13, 0x0f, 0x14, 0x1d, 0x1a, 0x1f, 0x1e, 0x1d, - 0x1a, 0x1c, 0x1c, 0x20, 0x24, 0x2e, 0x27, 0x20, 0x22, 0x2c, 0x23, 0x1c, - 0x1c, 0x28, 0x37, 0x29, 0x2c, 0x30, 0x31, 0x34, 0x34, 0x34, 0x1f, 0x27, - 0x39, 0x3d, 0x38, 0x32, 0x3c, 0x2e, 0x33, 0x34, 0x32, 0xff, 0xdb, 0x00, - 0x43, 0x01, 0x09, 0x09, 0x09, 0x0c, 0x0b, 0x0c, 0x18, 0x0d, 0x0d, 0x18, - 0x32, 0x21, 0x1c, 0x21, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, - 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, - 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, - 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, - 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x01, - 0x00, 0x00, 0x96, 0x03, 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, - 0x01, 0xff, 0xc4, 0x00, 0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, - 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, - 0x00, 0xb5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, - 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, - 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, - 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, - 0x52, 0xd1, 0xf0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, - 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, - 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, - 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, - 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, - 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, - 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, - 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, - 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, - 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, - 0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, - 0xfa, 0xff, 0xc4, 0x00, 0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, - 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, - 0x00, 0xb5, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, - 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, - 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, - 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, - 0x33, 0x52, 0xf0, 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, - 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, - 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, - 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, - 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, - 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, - 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, - 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, - 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, - 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, - 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, - 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, - 0x00, 0x3f, 0x00, 0xef, 0x28, 0xa2, 0x8a, 0x00, 0x28, 0xa2, 0xaa, 0xdf, - 0x6a, 0x56, 0x5a, 0x6c, 0x5e, 0x6d, 0xed, 0xdc, 0x36, 0xe9, 0xd8, 0xc8, - 0xe1, 0x73, 0xf4, 0xf5, 0xa0, 0x0b, 0x54, 0x57, 0x29, 0x73, 0xf1, 0x17, - 0xc3, 0x76, 0xe7, 0x02, 0xf5, 0xa5, 0x3f, 0xf4, 0xce, 0x32, 0x6b, 0x22, - 0xe7, 0xe2, 0xde, 0x95, 0x1e, 0x45, 0xbd, 0x8d, 0xd4, 0xa7, 0xb6, 0xe2, - 0xaa, 0x0f, 0xf3, 0xa0, 0x0f, 0x42, 0xa2, 0xbc, 0xa6, 0x6f, 0x8c, 0x12, - 0xf3, 0xe4, 0xe9, 0x11, 0xaf, 0xa6, 0xf9, 0x89, 0xfe, 0x40, 0x55, 0x47, - 0xf8, 0xb9, 0xaa, 0x9f, 0xbb, 0x65, 0x6a, 0xbf, 0xf7, 0xd1, 0xfe, 0xb4, - 0x58, 0x0f, 0x61, 0xa2, 0xbc, 0x59, 0xbe, 0x2c, 0x6b, 0xa4, 0xfc, 0xb0, - 0xda, 0x01, 0xff, 0x00, 0x5c, 0xcf, 0xf8, 0xd3, 0x7f, 0xe1, 0x6b, 0x6b, - 0xf8, 0xfb, 0x96, 0x9f, 0xf7, 0xe8, 0xff, 0x00, 0x8d, 0x3b, 0x01, 0xed, - 0x74, 0x57, 0x8c, 0x27, 0xc5, 0x8d, 0x6d, 0x47, 0xcd, 0x0d, 0xa3, 0x7f, - 0xc0, 0x08, 0xfe, 0xb5, 0x62, 0x3f, 0x8b, 0xba, 0x92, 0x91, 0xe6, 0x58, - 0x5a, 0xb7, 0xd0, 0xb0, 0xa5, 0x60, 0x3d, 0x7e, 0x8a, 0xf2, 0xf8, 0x3e, - 0x30, 0x2e, 0xe0, 0x2e, 0x34, 0x8e, 0x3b, 0x94, 0x9b, 0xfa, 0x15, 0xad, - 0x5b, 0x6f, 0x8a, 0xda, 0x14, 0xdc, 0x4b, 0x15, 0xd4, 0x3e, 0xe5, 0x43, - 0x0f, 0xd0, 0xd1, 0x60, 0x3b, 0xba, 0x2b, 0x9e, 0xb4, 0xf1, 0xc7, 0x87, - 0x2f, 0x08, 0x09, 0xa9, 0xc5, 0x1b, 0x1e, 0x82, 0x5c, 0xa7, 0xf3, 0xad, - 0xf8, 0xe4, 0x49, 0x50, 0x3c, 0x6e, 0xae, 0x87, 0x90, 0xca, 0x72, 0x0f, - 0xe3, 0x40, 0x0e, 0xa2, 0x8a, 0x28, 0x00, 0xa2, 0x8a, 0x28, 0x00, 0xa2, - 0x8a, 0x6c, 0x92, 0x08, 0xa2, 0x79, 0x18, 0xe1, 0x55, 0x4b, 0x1f, 0xc2, - 0x80, 0x38, 0xff, 0x00, 0x1c, 0xf8, 0xd5, 0x7c, 0x39, 0x00, 0xb5, 0xb4, - 0xda, 0xfa, 0x84, 0xab, 0x91, 0x9e, 0x44, 0x63, 0xd4, 0x8f, 0x5f, 0x6a, - 0xf1, 0x5b, 0xdd, 0x42, 0xef, 0x52, 0xb8, 0x33, 0xde, 0x5c, 0x49, 0x34, - 0xad, 0xd5, 0x9d, 0xb3, 0x56, 0x3c, 0x41, 0xa8, 0x49, 0xaa, 0x6b, 0xd7, - 0x77, 0x52, 0xb1, 0x25, 0xe4, 0x38, 0xcf, 0x61, 0xd8, 0x55, 0x15, 0x5e, - 0x29, 0x88, 0x4d, 0xa7, 0xa9, 0xa3, 0x6d, 0x33, 0x12, 0x49, 0x23, 0x05, - 0x3b, 0x42, 0x9c, 0x13, 0x4f, 0x55, 0x74, 0x65, 0x56, 0x39, 0xdd, 0xd0, - 0xd3, 0x00, 0xdb, 0x9a, 0x5f, 0xd2, 0x97, 0xa8, 0xc5, 0x18, 0xcf, 0x03, - 0xf3, 0xa0, 0x04, 0xc5, 0x1b, 0x45, 0x2e, 0x0d, 0x04, 0x50, 0x03, 0x71, - 0xcd, 0x1b, 0x79, 0xa7, 0xe3, 0x83, 0x49, 0x83, 0x9e, 0x28, 0x01, 0xb8, - 0xeb, 0xe9, 0x41, 0x5a, 0x78, 0x5e, 0x3a, 0x73, 0x4b, 0x8e, 0x3f, 0xa5, - 0x00, 0x47, 0xb4, 0x8e, 0x95, 0xb7, 0xe1, 0xff, 0x00, 0x15, 0xea, 0x7e, - 0x1e, 0xba, 0x57, 0xb7, 0x99, 0x9a, 0x0c, 0xfc, 0xf0, 0xb1, 0xca, 0xb0, - 0xfe, 0x9f, 0x5a, 0xc7, 0x2b, 0xdb, 0x34, 0xc6, 0x14, 0x01, 0xf4, 0x96, - 0x87, 0xac, 0xdb, 0x6b, 0xda, 0x54, 0x57, 0xf6, 0xa7, 0xe5, 0x71, 0xf3, - 0x29, 0xea, 0xad, 0xdc, 0x1a, 0xd1, 0xaf, 0x1d, 0xf8, 0x4b, 0xa9, 0xcb, - 0x16, 0xb1, 0x71, 0xa7, 0x16, 0x3e, 0x54, 0xd1, 0xef, 0x00, 0xf6, 0x61, - 0x5e, 0xc5, 0x52, 0x30, 0xa2, 0x8a, 0x28, 0x00, 0xaa, 0x7a, 0xb9, 0xc6, - 0x8d, 0x7c, 0x7f, 0xe9, 0xde, 0x4f, 0xfd, 0x04, 0xd5, 0xca, 0xa5, 0xab, - 0xff, 0x00, 0xc8, 0x1a, 0xfb, 0x3d, 0x3c, 0x87, 0xff, 0x00, 0xd0, 0x4d, - 0x00, 0x7c, 0xd7, 0x73, 0xff, 0x00, 0x1f, 0x93, 0x7f, 0xbe, 0x68, 0x19, - 0x0a, 0xcc, 0xb8, 0xc8, 0x19, 0xe6, 0x8b, 0x9f, 0xf8, 0xfd, 0x9b, 0xfd, - 0xf3, 0x46, 0x71, 0x14, 0x98, 0xfe, 0xe9, 0xaa, 0x11, 0x12, 0xb4, 0xdb, - 0x08, 0x45, 0xe0, 0xb6, 0xee, 0x05, 0x1b, 0xe7, 0x46, 0x47, 0x65, 0x25, - 0x51, 0x83, 0x63, 0x18, 0xa9, 0xf3, 0x88, 0xe3, 0xc7, 0xf7, 0x45, 0x35, - 0xcf, 0xc8, 0xe3, 0x3d, 0x54, 0xd0, 0x02, 0xed, 0x05, 0x32, 0x4e, 0x19, - 0x93, 0x23, 0x1e, 0xb5, 0x58, 0x25, 0xd1, 0x95, 0xe3, 0x0e, 0x32, 0x9c, - 0x93, 0xda, 0xac, 0xf3, 0xe5, 0xc7, 0x9e, 0xbb, 0x07, 0xf2, 0xa1, 0x17, - 0x6d, 0xc4, 0xb2, 0x13, 0xf2, 0xb2, 0xae, 0x3d, 0xfa, 0x50, 0x05, 0x3d, - 0xd3, 0xab, 0xc8, 0xa6, 0x64, 0x1b, 0x0e, 0x0e, 0x4f, 0x5f, 0xa5, 0x4b, - 0x2b, 0xb9, 0xb2, 0x8a, 0x65, 0x38, 0x2c, 0xdb, 0x49, 0xf7, 0xa9, 0x1a, - 0xd9, 0x65, 0x92, 0xe1, 0xd8, 0xe0, 0x96, 0xcc, 0x78, 0x3c, 0x77, 0xff, - 0x00, 0xeb, 0x51, 0x2c, 0x6e, 0xf6, 0x31, 0x46, 0xaa, 0xa0, 0xab, 0x72, - 0xb9, 0xa4, 0x32, 0x28, 0xd2, 0xe2, 0x40, 0xe5, 0x65, 0x53, 0xb7, 0x93, - 0xcf, 0x4a, 0x5b, 0x63, 0x23, 0x5d, 0x94, 0x76, 0xde, 0x8a, 0x09, 0x62, - 0xbd, 0x3a, 0x50, 0x12, 0xe5, 0x63, 0x75, 0x8d, 0x11, 0x03, 0x80, 0x1b, - 0x07, 0xa8, 0xa7, 0xd9, 0xa4, 0xd0, 0xc8, 0x8a, 0xea, 0xab, 0x1e, 0x49, - 0x66, 0x07, 0x93, 0xc7, 0x4a, 0x04, 0x45, 0x1f, 0xda, 0xae, 0x3e, 0x68, - 0xf8, 0x5c, 0xe0, 0x03, 0xde, 0x95, 0xae, 0x7f, 0xd1, 0xb2, 0x18, 0xac, - 0xa1, 0xf0, 0x47, 0xf9, 0xfc, 0x29, 0xc6, 0x2b, 0x88, 0xbf, 0x77, 0x0b, - 0x83, 0x1e, 0x72, 0xa7, 0xb8, 0xcd, 0x0d, 0x68, 0x3e, 0xc8, 0x47, 0x0d, - 0x31, 0x70, 0x49, 0xf6, 0xe6, 0x81, 0x84, 0x05, 0xcd, 0xca, 0xc6, 0xf2, - 0x2b, 0x07, 0xcf, 0x20, 0xf4, 0x35, 0x23, 0x0e, 0x29, 0x56, 0x04, 0x8a, - 0xe9, 0x59, 0x39, 0x8c, 0xa1, 0x1c, 0x9e, 0x54, 0xff, 0x00, 0x9f, 0xe7, - 0x43, 0x72, 0x29, 0x88, 0xea, 0x3e, 0x1b, 0x36, 0xcf, 0x1a, 0xda, 0xe3, - 0xba, 0xb0, 0x3f, 0x95, 0x7b, 0xcd, 0x78, 0x2f, 0xc3, 0x8f, 0xf9, 0x1d, - 0x2d, 0x7d, 0xc3, 0x7f, 0x2a, 0xf7, 0xaa, 0x4c, 0x61, 0x45, 0x14, 0x52, - 0x00, 0xaa, 0x7a, 0xb1, 0xc6, 0x8d, 0x7b, 0xff, 0x00, 0x5c, 0x1f, 0xff, - 0x00, 0x41, 0x35, 0x72, 0xa9, 0xea, 0xdf, 0xf2, 0x07, 0xbd, 0xff, 0x00, - 0xae, 0x0f, 0xfc, 0x8d, 0x00, 0x7c, 0xd5, 0x71, 0xff, 0x00, 0x1f, 0xb3, - 0x7f, 0xbe, 0x69, 0x57, 0x95, 0x20, 0xf4, 0x23, 0x14, 0x97, 0x03, 0xfd, - 0x36, 0x6f, 0xf7, 0xcd, 0x38, 0x0c, 0x0e, 0x4e, 0x3d, 0xea, 0x84, 0x45, - 0xe4, 0xb8, 0xfb, 0x8f, 0x9f, 0x4a, 0x6f, 0x90, 0xed, 0xcc, 0x8f, 0xf8, - 0x54, 0xfc, 0x75, 0x0c, 0x28, 0xda, 0x3d, 0x47, 0xe7, 0x48, 0x04, 0x66, - 0xf9, 0x54, 0x0e, 0xc0, 0x0a, 0x6f, 0x63, 0x4e, 0xc0, 0x20, 0xe0, 0x8f, - 0xce, 0x8d, 0xa3, 0x1c, 0x91, 0xf9, 0xd3, 0x01, 0xa3, 0x83, 0xed, 0x4e, - 0x1c, 0x50, 0x02, 0x8e, 0xe0, 0x7e, 0x34, 0xb8, 0x5f, 0xef, 0x7e, 0xb4, - 0x00, 0x03, 0xcf, 0x3d, 0x29, 0x0f, 0x3d, 0x69, 0xd8, 0x5f, 0x51, 0xf9, - 0xd2, 0x60, 0x0e, 0xe3, 0xf3, 0xa0, 0x03, 0x38, 0xe7, 0x34, 0x67, 0x9a, - 0x51, 0xb7, 0xa6, 0x47, 0xe7, 0x46, 0x3d, 0xc7, 0xe7, 0x40, 0x0d, 0xed, - 0x4d, 0x3c, 0x0e, 0x6a, 0x4c, 0x0c, 0x67, 0x70, 0xfc, 0xe9, 0x8d, 0x8c, - 0x71, 0x40, 0x1d, 0x47, 0xc3, 0x71, 0x9f, 0x1a, 0xda, 0xfd, 0x1b, 0xf9, - 0x1a, 0xf7, 0x9a, 0xf0, 0x7f, 0x86, 0xbc, 0xf8, 0xda, 0xd7, 0xd3, 0x6b, - 0xff, 0x00, 0xe8, 0x26, 0xbd, 0xe2, 0x93, 0x18, 0x51, 0x45, 0x14, 0x80, - 0x2a, 0x96, 0xaf, 0xff, 0x00, 0x20, 0x6b, 0xdf, 0xfa, 0xe0, 0xff, 0x00, - 0xc8, 0xd5, 0xda, 0xa5, 0xab, 0xff, 0x00, 0xc8, 0x1a, 0xf4, 0x7f, 0xd3, - 0x17, 0xfe, 0x54, 0x01, 0xf3, 0x64, 0xb9, 0x37, 0xb2, 0x8e, 0xe5, 0xcd, - 0x67, 0xce, 0xcc, 0x65, 0x60, 0x4f, 0x43, 0x8a, 0xd3, 0xe9, 0xa9, 0x37, - 0xfd, 0x74, 0xfe, 0xb5, 0x9b, 0x75, 0xff, 0x00, 0x1f, 0x32, 0x7f, 0xbd, - 0x4c, 0x44, 0x59, 0x3e, 0xb4, 0x99, 0x34, 0x51, 0x40, 0xc3, 0x27, 0xd6, - 0x8a, 0x28, 0xa0, 0x03, 0x34, 0x51, 0x45, 0x00, 0x14, 0x51, 0x45, 0x00, - 0x14, 0x66, 0x8a, 0x28, 0x01, 0x72, 0x7d, 0x6b, 0x42, 0xd3, 0xf7, 0xd6, - 0xd2, 0xf3, 0xf3, 0xc7, 0x83, 0xf5, 0x15, 0x9d, 0x5a, 0x1a, 0x6f, 0xdd, - 0xb9, 0xff, 0x00, 0xae, 0x7f, 0xd6, 0x80, 0x3a, 0xdf, 0x86, 0xa3, 0xfe, - 0x2b, 0x6b, 0x5f, 0xf7, 0x5f, 0xff, 0x00, 0x41, 0x35, 0xef, 0x15, 0xe1, - 0x1f, 0x0d, 0x47, 0xfc, 0x56, 0xd6, 0xc7, 0xfd, 0x87, 0xfe, 0x46, 0xbd, - 0xde, 0x90, 0x05, 0x14, 0x51, 0x40, 0x05, 0x53, 0xd5, 0xbf, 0xe4, 0x0f, - 0x79, 0xff, 0x00, 0x5c, 0x5b, 0xf9, 0x55, 0xca, 0xa7, 0xab, 0x7f, 0xc8, - 0x1e, 0xf3, 0xfe, 0xb8, 0xb7, 0xf2, 0xa0, 0x0f, 0x9b, 0xc9, 0xc6, 0xa4, - 0xff, 0x00, 0xf5, 0xd3, 0xfa, 0xd6, 0x65, 0xd7, 0xfc, 0x7c, 0xc9, 0xfe, - 0xf5, 0x69, 0x9f, 0xf9, 0x09, 0x37, 0xfd, 0x74, 0x35, 0x99, 0x75, 0xff, - 0x00, 0x1f, 0x52, 0x7f, 0xbd, 0x4c, 0x44, 0x34, 0x51, 0x45, 0x03, 0x0a, - 0x28, 0xa2, 0x80, 0x0a, 0x28, 0xa2, 0x80, 0x0a, 0x28, 0xa2, 0x80, 0x0a, - 0x28, 0xa2, 0x80, 0x0a, 0xd0, 0xd3, 0x3e, 0xed, 0xcf, 0xfb, 0x9f, 0xd6, - 0xb3, 0xeb, 0x43, 0x4c, 0xfb, 0xb7, 0x3f, 0xee, 0x7f, 0x5a, 0x00, 0xeb, - 0xfe, 0x1a, 0x0f, 0xf8, 0xad, 0x20, 0xff, 0x00, 0x71, 0xbf, 0x95, 0x7b, - 0xb5, 0x78, 0x4f, 0xc3, 0x5f, 0xf9, 0x1d, 0x20, 0xff, 0x00, 0x71, 0xbf, - 0x95, 0x7b, 0xb5, 0x20, 0x0a, 0x28, 0xa2, 0x80, 0x0a, 0xa7, 0xab, 0x7f, - 0xc8, 0x22, 0xf3, 0xfe, 0xb8, 0xb7, 0xf2, 0xab, 0x95, 0x4f, 0x56, 0xff, - 0x00, 0x90, 0x45, 0xe7, 0xfd, 0x71, 0x6f, 0xe5, 0x40, 0x1f, 0x37, 0x1f, - 0xf9, 0x08, 0xbf, 0xfd, 0x74, 0xeb, 0x59, 0xb7, 0x5f, 0xf1, 0xf3, 0x27, - 0xfb, 0xc6, 0xb4, 0x8f, 0xfc, 0x84, 0x5f, 0xfe, 0xba, 0x56, 0x65, 0xc7, - 0xfc, 0x7c, 0x49, 0xfe, 0xf1, 0xa6, 0x22, 0x2a, 0x28, 0xa2, 0x81, 0x85, - 0x14, 0x51, 0x40, 0x05, 0x14, 0x51, 0x40, 0x05, 0x14, 0x51, 0x40, 0x05, - 0x14, 0x51, 0x40, 0x05, 0x68, 0x69, 0x9f, 0x76, 0xe4, 0xff, 0x00, 0xd3, - 0x3f, 0xeb, 0x59, 0xf5, 0xa3, 0xa6, 0x72, 0x97, 0x5f, 0xf5, 0xcf, 0xfa, - 0xd0, 0x07, 0x5d, 0xf0, 0xd3, 0xfe, 0x47, 0x48, 0x3f, 0xdc, 0x7f, 0xe5, - 0x5e, 0xed, 0x5e, 0x13, 0xf0, 0xcc, 0x7f, 0xc5, 0x6b, 0x07, 0xfb, 0x8f, - 0xfc, 0xab, 0xdd, 0xa9, 0x00, 0x51, 0x45, 0x14, 0x00, 0x55, 0x2d, 0x5f, - 0xfe, 0x40, 0xf7, 0x9f, 0xf5, 0xc5, 0xbf, 0x95, 0x5d, 0xaa, 0x7a, 0xb7, - 0xfc, 0x82, 0x2f, 0x3f, 0xeb, 0x8b, 0x7f, 0x2a, 0x00, 0xf9, 0xb8, 0xff, - 0x00, 0xc8, 0x49, 0xff, 0x00, 0xeb, 0xa5, 0x66, 0x5c, 0x7f, 0xc7, 0xc4, - 0x9f, 0xef, 0x1a, 0xd3, 0x3f, 0xf2, 0x12, 0x7f, 0xfa, 0xe9, 0x59, 0x97, - 0x3f, 0xf1, 0xf3, 0x27, 0xfb, 0xc6, 0x98, 0x88, 0xa8, 0xa2, 0x8a, 0x06, - 0x14, 0x51, 0x45, 0x00, 0x14, 0x51, 0x45, 0x00, 0x14, 0x51, 0x45, 0x00, - 0x14, 0x51, 0x45, 0x00, 0x15, 0xa3, 0xa6, 0x1f, 0x96, 0xe7, 0xfe, 0xb9, - 0xff, 0x00, 0x5a, 0xce, 0xad, 0x0d, 0x33, 0xee, 0xdc, 0xff, 0x00, 0xb9, - 0xfd, 0x68, 0x03, 0xaf, 0xf8, 0x66, 0x3f, 0xe2, 0xb5, 0x83, 0xfd, 0xc7, - 0xfe, 0x55, 0xee, 0xd5, 0xe1, 0x3f, 0x0c, 0xff, 0x00, 0xe4, 0x75, 0x83, - 0xfd, 0xc7, 0xfe, 0x55, 0xee, 0xd4, 0x80, 0x28, 0xa2, 0x8a, 0x00, 0x2a, - 0x96, 0xaf, 0xff, 0x00, 0x20, 0x7b, 0xcf, 0xfa, 0xe2, 0xdf, 0xca, 0xae, - 0xd5, 0x2d, 0x5f, 0xfe, 0x40, 0xf7, 0x9f, 0xf5, 0xc5, 0xbf, 0x95, 0x00, - 0x7c, 0xde, 0x7f, 0xe4, 0x24, 0xff, 0x00, 0xf5, 0xd3, 0xfa, 0xd6, 0x6d, - 0xd7, 0xfc, 0x7c, 0xc9, 0x8f, 0xef, 0x1a, 0xd3, 0x3c, 0x6a, 0x4f, 0xff, - 0x00, 0x5d, 0x0f, 0xf3, 0xac, 0xbb, 0x9f, 0xf8, 0xf9, 0x97, 0xfd, 0xea, - 0xa1, 0x11, 0x51, 0x45, 0x14, 0x86, 0x14, 0x51, 0x45, 0x00, 0x14, 0x51, - 0x45, 0x00, 0x14, 0x51, 0x45, 0x00, 0x14, 0x51, 0x45, 0x00, 0x15, 0xa1, - 0xa6, 0xfd, 0xcb, 0x9f, 0xf7, 0x3f, 0xad, 0x67, 0xd6, 0x86, 0x99, 0xf7, - 0x2e, 0x7f, 0xeb, 0x9f, 0xf5, 0xa0, 0x0e, 0xbf, 0xe1, 0x9f, 0xfc, 0x8e, - 0xb0, 0x7f, 0xb8, 0xff, 0x00, 0xca, 0xbd, 0xda, 0xbc, 0x27, 0xe1, 0x9f, - 0xfc, 0x8e, 0xb0, 0x7f, 0xd7, 0x37, 0xfe, 0x55, 0xee, 0xd4, 0x30, 0x0a, - 0x28, 0xa2, 0x90, 0x05, 0x52, 0xd5, 0xf9, 0xd1, 0xef, 0x3f, 0xeb, 0x8b, - 0x7f, 0x2a, 0xbb, 0x54, 0xf5, 0x6f, 0xf9, 0x04, 0x5e, 0x7f, 0xd7, 0x16, - 0xfe, 0x54, 0x01, 0xf3, 0x79, 0x1f, 0xf1, 0x32, 0x7f, 0xfa, 0xe8, 0x7f, - 0x9d, 0x65, 0xdd, 0x7f, 0xc7, 0xcc, 0x9f, 0xef, 0x1a, 0xd2, 0x76, 0xdb, - 0xa8, 0xbb, 0x7a, 0x49, 0x9f, 0xd6, 0xab, 0x6a, 0x70, 0x18, 0xee, 0xdd, - 0x94, 0x65, 0x18, 0xe4, 0x1a, 0xa1, 0x14, 0x68, 0xa3, 0x07, 0xd2, 0x8c, - 0x1f, 0x4a, 0x43, 0x0a, 0x28, 0xa2, 0x80, 0x0a, 0x28, 0xc1, 0x1d, 0xa8, - 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0xb4, 0x74, - 0xbf, 0xb9, 0x73, 0xff, 0x00, 0x5c, 0xff, 0x00, 0xad, 0x67, 0x56, 0x8d, - 0x81, 0xf2, 0xad, 0xa7, 0x6f, 0xef, 0x00, 0x05, 0x00, 0x75, 0xdf, 0x0c, - 0xbf, 0xe4, 0x75, 0x80, 0xff, 0x00, 0xd3, 0x37, 0xfe, 0x55, 0xee, 0xd5, - 0xe1, 0x5f, 0x0c, 0x7f, 0xe4, 0x74, 0x87, 0xfe, 0xb9, 0xbf, 0xf2, 0xaf, - 0x75, 0xa4, 0x01, 0x45, 0x14, 0x50, 0x01, 0x54, 0xb5, 0x7f, 0xf9, 0x03, - 0xde, 0x7f, 0xd7, 0x16, 0xfe, 0x55, 0x76, 0xab, 0xdf, 0xc2, 0xd7, 0x1a, - 0x7d, 0xc4, 0x28, 0x40, 0x67, 0x8d, 0x94, 0x67, 0xdc, 0x50, 0x07, 0xcc, - 0xd7, 0x1f, 0xf1, 0xf9, 0x37, 0xfb, 0xe6, 0xa5, 0xde, 0xb2, 0xc6, 0x16, - 0x4e, 0xdd, 0xe9, 0x97, 0xc8, 0x61, 0xd4, 0x67, 0x8d, 0xbe, 0xf0, 0x73, - 0x9a, 0x8c, 0x38, 0x03, 0x24, 0xe0, 0x7b, 0xd5, 0x08, 0x9c, 0xc3, 0x6e, - 0x47, 0x24, 0xd2, 0x79, 0x56, 0xd8, 0x1c, 0x9e, 0x2a, 0xb1, 0xb8, 0x8b, - 0xfb, 0xc2, 0x90, 0xdc, 0x45, 0xfd, 0xf1, 0x40, 0x16, 0x05, 0xbd, 0xa9, - 0xa0, 0x43, 0x6c, 0x0f, 0x53, 0x91, 0x55, 0x85, 0xc4, 0x58, 0xc6, 0xf1, - 0xcd, 0x1e, 0x7c, 0x79, 0xc6, 0xf1, 0x40, 0x16, 0x4c, 0x50, 0x37, 0x5c, - 0xd3, 0x7c, 0x8b, 0x7e, 0xc2, 0xa3, 0x04, 0x74, 0x07, 0xf1, 0xa7, 0x03, - 0x40, 0x0f, 0xf2, 0x6d, 0x88, 0xe8, 0x68, 0xfb, 0x35, 0xbe, 0x29, 0x8c, - 0xe1, 0x57, 0x2d, 0xc0, 0xa8, 0xfe, 0xd3, 0x1f, 0xf7, 0xc5, 0x03, 0x26, - 0xfb, 0x3d, 0xb6, 0x3a, 0x73, 0x4b, 0xf6, 0x7b, 0x6c, 0x0a, 0x83, 0xed, - 0x11, 0xe7, 0x3b, 0xc5, 0x28, 0xb8, 0x88, 0x0f, 0xbc, 0x29, 0x01, 0x2b, - 0x5b, 0xdb, 0x0e, 0xd4, 0x8e, 0x57, 0x68, 0x55, 0x18, 0x51, 0x51, 0xf9, - 0xf1, 0x9f, 0xe3, 0x14, 0xa7, 0x18, 0xc8, 0xa6, 0x23, 0xaf, 0xf8, 0x61, - 0xff, 0x00, 0x23, 0xa4, 0x3f, 0xf5, 0xcd, 0xbf, 0x95, 0x7b, 0xb5, 0x78, - 0x7f, 0xc2, 0xab, 0x77, 0x9b, 0xc5, 0xc6, 0x55, 0xc6, 0xd8, 0x61, 0x66, - 0x6c, 0xfb, 0xf1, 0x5e, 0xe1, 0x52, 0x30, 0xa2, 0x8a, 0x28, 0x00, 0xa3, - 0x19, 0xa2, 0x8a, 0x00, 0xf9, 0xcb, 0xc6, 0x16, 0xa6, 0xd3, 0xc4, 0xf7, - 0x69, 0x8c, 0x66, 0x46, 0xfe, 0x7f, 0xfe, 0xaa, 0xc6, 0xf2, 0xc3, 0x94, - 0xdd, 0xca, 0xe0, 0x9c, 0x57, 0x79, 0xf1, 0x5f, 0x4d, 0x36, 0xfa, 0xea, - 0xdd, 0xaa, 0xfc, 0x93, 0x28, 0x6c, 0xfb, 0xe3, 0x07, 0xf9, 0x7e, 0xb5, - 0xc3, 0xf4, 0x8e, 0x36, 0x3d, 0x08, 0x23, 0xf9, 0x55, 0x08, 0x3c, 0xb8, - 0xc7, 0xf0, 0x2f, 0xe5, 0x49, 0xb6, 0x35, 0x3f, 0x71, 0x48, 0xfa, 0x53, - 0xf2, 0xa4, 0x75, 0xa6, 0xb0, 0x1e, 0xb9, 0xa0, 0x03, 0x11, 0x75, 0xd8, - 0xbf, 0xf7, 0xcd, 0x37, 0x64, 0x64, 0xff, 0x00, 0xab, 0x5f, 0xca, 0x9c, - 0x14, 0x0e, 0xe2, 0x90, 0x85, 0x03, 0xaf, 0x6a, 0x00, 0x95, 0x11, 0x1a, - 0xd2, 0x6d, 0xa3, 0x06, 0x32, 0xa4, 0x7b, 0x03, 0x90, 0x45, 0x42, 0x0d, - 0x4b, 0x6e, 0xea, 0x6d, 0x2e, 0x88, 0x3f, 0xdc, 0x03, 0xf3, 0x35, 0x5f, - 0x38, 0xa0, 0x07, 0xb4, 0x20, 0xb2, 0x16, 0xe5, 0x76, 0x93, 0x8f, 0x7c, - 0xe2, 0x94, 0xa4, 0x7f, 0xdc, 0x5c, 0xfd, 0x29, 0xcc, 0x0a, 0xc6, 0x8c, - 0x71, 0x86, 0x52, 0x01, 0xfc, 0x69, 0x01, 0x52, 0x3a, 0xd0, 0x31, 0x36, - 0xc7, 0x8c, 0xec, 0x5f, 0xca, 0x82, 0x13, 0xfe, 0x79, 0xa7, 0xe4, 0x29, - 0x78, 0x34, 0xbb, 0x7e, 0x94, 0x08, 0x66, 0xd4, 0x27, 0xfd, 0x5a, 0xfe, - 0x54, 0xa4, 0x2f, 0x92, 0xe0, 0x0c, 0x6d, 0x23, 0x1f, 0x43, 0x4e, 0x3b, - 0x42, 0xf5, 0x19, 0xa6, 0x29, 0x0d, 0x04, 0xcc, 0x0f, 0x19, 0x51, 0xfa, - 0x1a, 0x00, 0xf4, 0xdf, 0x83, 0x96, 0x67, 0x7e, 0xa7, 0x7a, 0x47, 0x00, - 0x24, 0x4a, 0x7d, 0xce, 0x49, 0xfe, 0x42, 0xbd, 0x5e, 0xb8, 0xef, 0x86, - 0x56, 0x1f, 0x62, 0xf0, 0x6c, 0x12, 0x15, 0xc3, 0x5c, 0xbb, 0x4a, 0x7d, - 0xc7, 0x41, 0xfa, 0x0a, 0xec, 0x6a, 0x46, 0x14, 0x51, 0x45, 0x00, 0x14, - 0x51, 0x45, 0x00, 0x71, 0x5f, 0x12, 0xb4, 0x73, 0xa8, 0x78, 0x7b, 0xed, - 0x11, 0xae, 0x64, 0xb7, 0x3c, 0xff, 0x00, 0xba, 0x7f, 0xfa, 0xf8, 0xaf, - 0x0f, 0x47, 0x5d, 0x86, 0x29, 0x06, 0x57, 0xb8, 0x3d, 0x8d, 0x7d, 0x41, - 0x75, 0x6f, 0x1d, 0xdd, 0xac, 0xb6, 0xf2, 0x8c, 0xc7, 0x2a, 0x95, 0x61, - 0xec, 0x6b, 0xe6, 0xef, 0x11, 0xe9, 0x72, 0xe8, 0xfa, 0xe5, 0xc5, 0xac, - 0xa3, 0x95, 0x73, 0xcf, 0xaf, 0xbf, 0xf5, 0xfc, 0x69, 0xa1, 0x19, 0xbe, - 0x44, 0x1f, 0xde, 0x90, 0x7e, 0x34, 0x9e, 0x44, 0x23, 0xf8, 0xe5, 0xfc, - 0xe9, 0x7a, 0x8a, 0x53, 0xd2, 0x80, 0x1b, 0xe4, 0x43, 0xff, 0x00, 0x3d, - 0x25, 0xfc, 0xe8, 0xfb, 0x3c, 0x19, 0xfb, 0xd2, 0x1f, 0xf8, 0x15, 0x2e, - 0x33, 0x57, 0x2c, 0x74, 0xd9, 0x6f, 0x92, 0xe2, 0x61, 0xb9, 0x2d, 0xad, - 0x93, 0x7c, 0xd3, 0x79, 0x6c, 0xca, 0x83, 0xb0, 0x38, 0x07, 0x04, 0xe0, - 0xe3, 0x3e, 0x86, 0x80, 0x2b, 0x65, 0x55, 0x02, 0x20, 0xda, 0x83, 0x9c, - 0x56, 0x81, 0xf0, 0xf6, 0xb4, 0xb9, 0xce, 0x8f, 0xa8, 0x0c, 0x0d, 0xc7, - 0x36, 0xcf, 0xc0, 0xf5, 0xe9, 0xd2, 0xb6, 0x0d, 0xc4, 0x16, 0xd1, 0xa4, - 0x96, 0x90, 0x58, 0xe9, 0x76, 0x6d, 0x23, 0x3c, 0x33, 0xdc, 0x41, 0xe7, - 0xdc, 0xca, 0x36, 0xf0, 0xca, 0xad, 0x96, 0x55, 0xc8, 0xe1, 0xb2, 0x06, - 0x7f, 0x1a, 0xd2, 0x97, 0x57, 0xb2, 0x5f, 0x06, 0xc5, 0x2c, 0x7e, 0x2a, - 0xd5, 0xce, 0xac, 0xce, 0x20, 0x95, 0xb7, 0x02, 0x4c, 0x0a, 0x72, 0xa9, - 0xe5, 0x6f, 0xed, 0xc7, 0x39, 0xc7, 0x5e, 0x4f, 0x4a, 0x2e, 0x07, 0x0f, - 0xe6, 0x03, 0x11, 0x89, 0xc6, 0xe8, 0xc9, 0xce, 0x33, 0xd0, 0xd4, 0x3f, - 0x67, 0x83, 0xfb, 0xf2, 0x63, 0xeb, 0x5d, 0x65, 0xc4, 0xd6, 0xd7, 0x16, - 0xd2, 0xc9, 0x77, 0x06, 0x9d, 0x7b, 0x6a, 0x0c, 0xb8, 0xbe, 0xb2, 0x87, - 0xc8, 0x92, 0x16, 0x28, 0x02, 0x97, 0x89, 0x40, 0x25, 0x77, 0x6d, 0x39, - 0x20, 0x8f, 0xbd, 0xea, 0x6b, 0x9e, 0xbe, 0xd3, 0xae, 0x74, 0xf7, 0x4f, - 0x3a, 0x29, 0x44, 0x52, 0x0c, 0xc3, 0x2b, 0x46, 0xca, 0xb2, 0xaf, 0xaa, - 0xe4, 0x72, 0x39, 0x14, 0x01, 0x50, 0x5b, 0xc3, 0x8f, 0xbf, 0x2f, 0xe7, - 0x4a, 0x60, 0x87, 0xfb, 0xf2, 0xfe, 0x74, 0x67, 0xf0, 0xa3, 0xf1, 0xa0, - 0x04, 0xfb, 0x3c, 0x1d, 0xda, 0x53, 0xf5, 0x35, 0x77, 0x4f, 0xb3, 0x6d, - 0x47, 0x50, 0xb5, 0xd3, 0xed, 0xd7, 0x06, 0x69, 0x02, 0x00, 0x3d, 0xcf, - 0x5a, 0xa6, 0x6b, 0xd1, 0xbe, 0x12, 0xe8, 0x5f, 0x6a, 0xd4, 0xa7, 0xd6, - 0x66, 0x4c, 0xc7, 0x6d, 0xfb, 0xb8, 0x72, 0x38, 0xde, 0x47, 0x27, 0xf0, - 0x07, 0xf5, 0xa0, 0x0f, 0x5b, 0xb2, 0xb4, 0x8e, 0xc6, 0xc6, 0x0b, 0x48, - 0x57, 0x6c, 0x70, 0xc6, 0xb1, 0xa8, 0xf6, 0x03, 0x15, 0x3d, 0x14, 0x52, - 0x18, 0x51, 0x45, 0x14, 0x00, 0x51, 0x45, 0x14, 0x00, 0x57, 0x9b, 0xfc, - 0x53, 0xf0, 0xe7, 0xda, 0xec, 0x93, 0x56, 0xb7, 0x4f, 0xde, 0x47, 0x85, - 0x97, 0x1e, 0x9d, 0x8f, 0xf4, 0xfc, 0xab, 0xd2, 0x2a, 0x2b, 0x8b, 0x78, - 0xee, 0xad, 0xa4, 0xb7, 0x99, 0x43, 0x47, 0x22, 0x95, 0x60, 0x7b, 0x83, - 0x40, 0x1f, 0x2d, 0x29, 0xec, 0x7a, 0xd3, 0xf3, 0x9e, 0x86, 0xb6, 0x7c, - 0x5b, 0xa1, 0x4b, 0xa0, 0x6b, 0x93, 0x5b, 0xb0, 0xfd, 0xde, 0xec, 0xa3, - 0x7a, 0x8e, 0xc6, 0xb1, 0x46, 0x08, 0xa6, 0x21, 0x45, 0x75, 0xd6, 0xbe, - 0x15, 0x97, 0x56, 0xd3, 0x92, 0x2b, 0x06, 0xd3, 0xe0, 0x92, 0xd2, 0x08, - 0x6f, 0x6e, 0xbe, 0xd0, 0xef, 0xbe, 0x52, 0xc4, 0x94, 0x00, 0x1e, 0x08, - 0xda, 0xc0, 0xe1, 0x47, 0x7e, 0xa4, 0xf4, 0xe4, 0x48, 0xea, 0x6b, 0xa6, - 0xd7, 0xd0, 0xb6, 0xb7, 0xab, 0x5c, 0xe8, 0x92, 0xcd, 0x1e, 0x99, 0xf6, - 0x74, 0x68, 0x83, 0xe0, 0x33, 0x40, 0xc7, 0x6e, 0x17, 0x68, 0xe1, 0x79, - 0x39, 0x07, 0x07, 0x19, 0xcf, 0x39, 0xa1, 0x81, 0x0e, 0xa2, 0xd2, 0xdc, - 0x78, 0xc1, 0x74, 0xb9, 0x2d, 0x74, 0xbb, 0xbb, 0xab, 0x8b, 0x84, 0xb7, - 0xfb, 0x40, 0x37, 0x0a, 0xa5, 0xcb, 0x04, 0xe8, 0x5c, 0x10, 0x01, 0xe3, - 0xa7, 0x6e, 0x38, 0xa9, 0x96, 0xc7, 0x4c, 0x99, 0xd6, 0x2b, 0x6b, 0xbd, - 0x0a, 0x6b, 0x89, 0x08, 0x58, 0xe2, 0x11, 0x5f, 0x02, 0xec, 0x4e, 0x00, - 0xc9, 0x38, 0x19, 0x3e, 0xb5, 0x81, 0xa7, 0x41, 0x2d, 0xd7, 0x89, 0x34, - 0x6b, 0x68, 0x26, 0x30, 0xcd, 0x2c, 0xf1, 0x24, 0x72, 0x8f, 0xe0, 0x63, - 0x2e, 0x03, 0x7e, 0x07, 0x9a, 0xe8, 0x65, 0x8f, 0x4e, 0x68, 0xd8, 0xe8, - 0xba, 0xac, 0x72, 0x5f, 0xa0, 0x2f, 0x12, 0x4b, 0xa7, 0x88, 0x8b, 0x91, - 0xce, 0x15, 0xb2, 0x46, 0xec, 0x64, 0x8c, 0xf5, 0xc6, 0x3a, 0x91, 0x48, - 0x64, 0x3a, 0x3c, 0xd0, 0x5d, 0x5f, 0x2c, 0x16, 0xd6, 0xc3, 0x4f, 0xd5, - 0x63, 0x9d, 0x2d, 0xcc, 0x51, 0x3b, 0x3c, 0x57, 0x01, 0xdc, 0x23, 0x29, - 0x0e, 0x4e, 0x3a, 0xfa, 0x90, 0x7d, 0x2b, 0x53, 0x5c, 0xf0, 0xbc, 0x5a, - 0x4d, 0xad, 0xd5, 0xa6, 0x6c, 0xa4, 0xdd, 0x09, 0xbf, 0xb0, 0xf2, 0x64, - 0x63, 0x22, 0x45, 0xb8, 0x6f, 0x57, 0xc6, 0x57, 0x85, 0xec, 0x7f, 0x03, - 0xc6, 0x0f, 0x31, 0xa5, 0x14, 0x9b, 0x42, 0xd4, 0xa3, 0x89, 0x19, 0xef, - 0xde, 0x48, 0xde, 0x42, 0xca, 0x58, 0xb4, 0x59, 0xfe, 0x1c, 0x74, 0x3b, - 0x88, 0xce, 0x7a, 0x8e, 0xe3, 0xa1, 0xe9, 0x60, 0xbb, 0xd2, 0xa6, 0xd6, - 0xd6, 0x29, 0xc5, 0xeb, 0xc1, 0x63, 0xa6, 0xcf, 0x15, 0xec, 0xf3, 0x28, - 0x0d, 0x24, 0x85, 0x89, 0x5d, 0xf9, 0xc9, 0x20, 0x33, 0x2a, 0x8e, 0xfc, - 0x0a, 0x02, 0xd6, 0x38, 0xbf, 0xca, 0x93, 0x3d, 0xa9, 0x71, 0xcd, 0x23, - 0x1f, 0xad, 0x31, 0x12, 0x5a, 0xda, 0xcf, 0x7d, 0x79, 0x15, 0xa5, 0xb2, - 0x19, 0x26, 0x95, 0x82, 0x22, 0x8e, 0xe4, 0xd7, 0xd2, 0x3e, 0x1f, 0xd1, - 0xe2, 0xd0, 0x74, 0x3b, 0x5d, 0x3a, 0x20, 0x3f, 0x74, 0x9f, 0x3b, 0x0f, - 0xe2, 0x63, 0xd4, 0xfe, 0x75, 0xe7, 0x3f, 0x09, 0xfc, 0x34, 0x1d, 0xe4, - 0xd7, 0xee, 0x50, 0xe1, 0x73, 0x1d, 0xb0, 0x23, 0xbf, 0x76, 0xfe, 0x9f, - 0x9d, 0x7a, 0xcd, 0x21, 0x85, 0x14, 0x51, 0x40, 0x05, 0x14, 0x51, 0x40, - 0x05, 0x14, 0x51, 0x40, 0x05, 0x14, 0x51, 0x40, 0x1c, 0x67, 0xc4, 0x4f, - 0x0d, 0xae, 0xb3, 0xa2, 0x35, 0xcc, 0x4b, 0x9b, 0x9b, 0x65, 0x27, 0x20, - 0x72, 0x57, 0xbf, 0xe5, 0xd6, 0xbc, 0x23, 0x05, 0x1c, 0xa3, 0x70, 0x41, - 0xc1, 0xaf, 0xaa, 0x88, 0x04, 0x10, 0x46, 0x41, 0xea, 0x2b, 0xc2, 0x3e, - 0x21, 0xf8, 0x52, 0x5d, 0x13, 0x56, 0x6b, 0xb8, 0x23, 0xcd, 0x9c, 0xec, - 0x4a, 0x90, 0x38, 0x53, 0xe9, 0x4c, 0x0e, 0x3e, 0xba, 0x45, 0xbd, 0xba, - 0xbb, 0xb2, 0xb4, 0xd4, 0xf4, 0xeb, 0xa9, 0xed, 0xf5, 0x1d, 0x3e, 0x15, - 0xb7, 0xb9, 0xfb, 0x32, 0x91, 0x24, 0x90, 0x82, 0xaa, 0xa5, 0x71, 0xc1, - 0xc2, 0xe0, 0x7e, 0x04, 0xd7, 0x30, 0xac, 0x2a, 0xde, 0x9d, 0xa9, 0x5d, - 0xe9, 0x37, 0xd1, 0x5e, 0x59, 0x4c, 0xd1, 0x4d, 0x1b, 0x02, 0x0a, 0x9e, - 0x0e, 0x0e, 0x70, 0x7d, 0x47, 0x1c, 0x8e, 0xf4, 0x08, 0xeb, 0x75, 0xaf, - 0x0e, 0x49, 0x15, 0x95, 0xbf, 0x8b, 0x64, 0x7d, 0x4a, 0xee, 0x39, 0x95, - 0x67, 0x86, 0xec, 0xdf, 0x22, 0xc9, 0xb4, 0x30, 0x50, 0xd8, 0x31, 0xe4, - 0x60, 0x94, 0x18, 0xf7, 0xf6, 0x35, 0x85, 0x79, 0xe2, 0x4b, 0xcb, 0x9b, - 0xab, 0x3b, 0x9f, 0xb4, 0xdf, 0xc9, 0x2d, 0xab, 0x97, 0x46, 0xbb, 0xba, - 0xf3, 0x48, 0xce, 0x38, 0x18, 0x55, 0xc0, 0xe3, 0x9f, 0x5a, 0xb7, 0x16, - 0xb1, 0x67, 0x34, 0x68, 0x60, 0xb8, 0x9f, 0x47, 0x9e, 0x38, 0x3c, 0xb4, - 0x48, 0x8b, 0x4b, 0x6c, 0x7e, 0x6f, 0x98, 0x6c, 0x62, 0xc4, 0x6e, 0x5c, - 0x77, 0xc1, 0x61, 0xce, 0x01, 0xc8, 0xd0, 0x2c, 0xe7, 0x49, 0x8e, 0x67, - 0xd5, 0x3c, 0x2b, 0x1c, 0x0e, 0xfb, 0x4b, 0x0d, 0x36, 0x23, 0x72, 0xa9, - 0x8e, 0x18, 0xc6, 0x17, 0xae, 0x78, 0xc0, 0xe9, 0xf4, 0xa4, 0x32, 0x5d, - 0x3f, 0xc3, 0x2d, 0x06, 0x91, 0x27, 0x8a, 0xe1, 0x4b, 0xfd, 0x36, 0xd1, - 0x54, 0x17, 0x92, 0xda, 0xf1, 0x49, 0x1b, 0x8e, 0x30, 0xaa, 0x13, 0x38, - 0x3d, 0x81, 0x3e, 0x99, 0x3d, 0xeb, 0x0e, 0x27, 0x6d, 0x13, 0x43, 0x99, - 0xe4, 0x2d, 0xfd, 0xab, 0xaa, 0x83, 0x9f, 0x35, 0x0e, 0xf4, 0xb7, 0x25, - 0xd5, 0xf7, 0x13, 0xfc, 0x4c, 0x54, 0x7e, 0x19, 0xf6, 0x35, 0x66, 0x6d, - 0x7a, 0x1b, 0x59, 0x99, 0x8d, 0xc4, 0xba, 0xd4, 0xee, 0xaa, 0x64, 0x7b, - 0x87, 0x91, 0x2d, 0xf7, 0x83, 0x90, 0xca, 0x8a, 0x54, 0x9c, 0x74, 0xe7, - 0x1c, 0xe7, 0x8f, 0x5e, 0x7e, 0xf6, 0xf6, 0xe7, 0x51, 0xbd, 0x96, 0xf2, - 0xf2, 0x66, 0x9a, 0xe2, 0x56, 0xdc, 0xee, 0xdd, 0x49, 0xff, 0x00, 0x3d, - 0xbb, 0x51, 0x60, 0xb9, 0x13, 0x1c, 0x0a, 0xbb, 0xa1, 0xe9, 0x17, 0x1a, - 0xee, 0xb3, 0x6f, 0xa7, 0xdb, 0x83, 0xba, 0x56, 0xf9, 0x9b, 0xfb, 0xab, - 0xdc, 0x9f, 0xc2, 0xa8, 0x12, 0x49, 0xc0, 0x19, 0x24, 0xf1, 0x5e, 0xdb, - 0xf0, 0xd7, 0xc2, 0x4d, 0xa2, 0x69, 0xcd, 0xa8, 0xde, 0xc6, 0x56, 0xfa, - 0xe8, 0x0c, 0x2b, 0x0e, 0x63, 0x4e, 0xc3, 0xea, 0x7b, 0xd3, 0x03, 0xb3, - 0xb0, 0xb1, 0x83, 0x4d, 0xb0, 0x82, 0xca, 0xd9, 0x02, 0xc3, 0x0a, 0x04, - 0x50, 0x2a, 0xcd, 0x14, 0x52, 0x00, 0xa2, 0x8a, 0x28, 0x00, 0xa2, 0x8a, - 0x28, 0x00, 0xa2, 0x8a, 0x28, 0x00, 0xa2, 0x8a, 0x28, 0x00, 0xa8, 0x2f, - 0x2c, 0xed, 0xef, 0xed, 0x5e, 0xda, 0xea, 0x14, 0x9a, 0x17, 0x18, 0x64, - 0x71, 0x90, 0x6a, 0x7a, 0x28, 0x03, 0xcc, 0xb5, 0x5f, 0x83, 0xf6, 0xb3, - 0xcc, 0xd2, 0x69, 0xba, 0x83, 0xdb, 0x86, 0x39, 0x11, 0xca, 0x9b, 0x80, - 0xfa, 0x11, 0x8a, 0xc4, 0x93, 0xe0, 0xfe, 0xb4, 0xa7, 0xf7, 0x77, 0xf6, - 0x4c, 0x3f, 0xda, 0xdc, 0x3f, 0xa1, 0xaf, 0x68, 0xa2, 0x80, 0x3c, 0x54, - 0x7c, 0x21, 0xd7, 0x80, 0xff, 0x00, 0x8f, 0xcb, 0x0c, 0xff, 0x00, 0xbe, - 0xdf, 0xfc, 0x4d, 0x07, 0xe1, 0x0e, 0xbd, 0xff, 0x00, 0x3f, 0x96, 0x3f, - 0xf7, 0xd3, 0x7f, 0x85, 0x7b, 0x55, 0x14, 0x01, 0xe2, 0xa3, 0xe1, 0x0e, - 0xbb, 0xff, 0x00, 0x3f, 0x96, 0x03, 0xfe, 0x04, 0xdf, 0xfc, 0x4d, 0x58, - 0x87, 0xe0, 0xee, 0xa2, 0xcd, 0xfb, 0xfd, 0x52, 0xd9, 0x17, 0xbe, 0xc4, - 0x2d, 0xfe, 0x15, 0xec, 0x54, 0x50, 0x07, 0x1d, 0xe1, 0xcf, 0x87, 0x3a, - 0x3e, 0x83, 0x2a, 0xdc, 0x49, 0xba, 0xf2, 0xe9, 0x79, 0x59, 0x26, 0x03, - 0x09, 0xf4, 0x15, 0xd8, 0xd1, 0x45, 0x00, 0x14, 0x51, 0x45, 0x00, 0x14, - 0x51, 0x45, 0x00, 0x14, 0x51, 0x45, 0x00, 0x7f, 0xff, 0xd9, 0xff, 0xe1, - 0x0c, 0x77, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, - 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x61, - 0x70, 0x2f, 0x31, 0x2e, 0x30, 0x2f, 0x00, 0x3c, 0x3f, 0x78, 0x70, 0x61, - 0x63, 0x6b, 0x65, 0x74, 0x20, 0x62, 0x65, 0x67, 0x69, 0x6e, 0x3d, 0x22, - 0xef, 0xbb, 0xbf, 0x22, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x57, 0x35, 0x4d, - 0x30, 0x4d, 0x70, 0x43, 0x65, 0x68, 0x69, 0x48, 0x7a, 0x72, 0x65, 0x53, - 0x7a, 0x4e, 0x54, 0x63, 0x7a, 0x6b, 0x63, 0x39, 0x64, 0x22, 0x3f, 0x3e, - 0x20, 0x3c, 0x78, 0x3a, 0x78, 0x6d, 0x70, 0x6d, 0x65, 0x74, 0x61, 0x20, - 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x78, 0x3d, 0x22, 0x61, 0x64, 0x6f, - 0x62, 0x65, 0x3a, 0x6e, 0x73, 0x3a, 0x6d, 0x65, 0x74, 0x61, 0x2f, 0x22, - 0x20, 0x78, 0x3a, 0x78, 0x6d, 0x70, 0x74, 0x6b, 0x3d, 0x22, 0x58, 0x4d, - 0x50, 0x20, 0x43, 0x6f, 0x72, 0x65, 0x20, 0x34, 0x2e, 0x34, 0x2e, 0x30, - 0x2d, 0x45, 0x78, 0x69, 0x76, 0x32, 0x22, 0x3e, 0x20, 0x3c, 0x72, 0x64, - 0x66, 0x3a, 0x52, 0x44, 0x46, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, - 0x72, 0x64, 0x66, 0x3d, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, - 0x77, 0x77, 0x77, 0x2e, 0x77, 0x33, 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x31, - 0x39, 0x39, 0x39, 0x2f, 0x30, 0x32, 0x2f, 0x32, 0x32, 0x2d, 0x72, 0x64, - 0x66, 0x2d, 0x73, 0x79, 0x6e, 0x74, 0x61, 0x78, 0x2d, 0x6e, 0x73, 0x23, - 0x22, 0x3e, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x72, 0x64, 0x66, 0x3a, - 0x61, 0x62, 0x6f, 0x75, 0x74, 0x3d, 0x22, 0x22, 0x20, 0x78, 0x6d, 0x6c, - 0x6e, 0x73, 0x3a, 0x78, 0x6d, 0x70, 0x4d, 0x4d, 0x3d, 0x22, 0x68, 0x74, - 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, 0x6f, 0x62, - 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x61, 0x70, 0x2f, 0x31, 0x2e, - 0x30, 0x2f, 0x6d, 0x6d, 0x2f, 0x22, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, - 0x3a, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3d, 0x22, 0x68, 0x74, 0x74, 0x70, - 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x61, 0x70, 0x2f, 0x31, 0x2e, 0x30, 0x2f, - 0x73, 0x54, 0x79, 0x70, 0x65, 0x2f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x23, 0x22, 0x20, 0x78, 0x6d, - 0x6c, 0x6e, 0x73, 0x3a, 0x64, 0x63, 0x3d, 0x22, 0x68, 0x74, 0x74, 0x70, - 0x3a, 0x2f, 0x2f, 0x70, 0x75, 0x72, 0x6c, 0x2e, 0x6f, 0x72, 0x67, 0x2f, - 0x64, 0x63, 0x2f, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, - 0x31, 0x2e, 0x31, 0x2f, 0x22, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, - 0x47, 0x49, 0x4d, 0x50, 0x3d, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, - 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x67, 0x69, 0x6d, 0x70, 0x2e, 0x6f, 0x72, - 0x67, 0x2f, 0x78, 0x6d, 0x70, 0x2f, 0x22, 0x20, 0x78, 0x6d, 0x6c, 0x6e, - 0x73, 0x3a, 0x78, 0x6d, 0x70, 0x3d, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, - 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x78, 0x61, 0x70, 0x2f, 0x31, 0x2e, 0x30, 0x2f, 0x22, - 0x20, 0x78, 0x6d, 0x70, 0x4d, 0x4d, 0x3a, 0x44, 0x6f, 0x63, 0x75, 0x6d, - 0x65, 0x6e, 0x74, 0x49, 0x44, 0x3d, 0x22, 0x67, 0x69, 0x6d, 0x70, 0x3a, - 0x64, 0x6f, 0x63, 0x69, 0x64, 0x3a, 0x67, 0x69, 0x6d, 0x70, 0x3a, 0x61, - 0x64, 0x38, 0x61, 0x34, 0x35, 0x64, 0x66, 0x2d, 0x65, 0x32, 0x31, 0x35, - 0x2d, 0x34, 0x61, 0x62, 0x38, 0x2d, 0x62, 0x39, 0x39, 0x39, 0x2d, 0x33, - 0x38, 0x63, 0x32, 0x63, 0x32, 0x66, 0x31, 0x30, 0x35, 0x35, 0x35, 0x22, - 0x20, 0x78, 0x6d, 0x70, 0x4d, 0x4d, 0x3a, 0x49, 0x6e, 0x73, 0x74, 0x61, - 0x6e, 0x63, 0x65, 0x49, 0x44, 0x3d, 0x22, 0x78, 0x6d, 0x70, 0x2e, 0x69, - 0x69, 0x64, 0x3a, 0x64, 0x35, 0x38, 0x36, 0x64, 0x66, 0x33, 0x65, 0x2d, - 0x64, 0x32, 0x61, 0x64, 0x2d, 0x34, 0x62, 0x61, 0x63, 0x2d, 0x62, 0x34, - 0x35, 0x39, 0x2d, 0x39, 0x36, 0x32, 0x36, 0x63, 0x63, 0x65, 0x35, 0x66, - 0x65, 0x33, 0x31, 0x22, 0x20, 0x78, 0x6d, 0x70, 0x4d, 0x4d, 0x3a, 0x4f, - 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x6f, 0x63, 0x75, 0x6d, - 0x65, 0x6e, 0x74, 0x49, 0x44, 0x3d, 0x22, 0x78, 0x6d, 0x70, 0x2e, 0x64, - 0x69, 0x64, 0x3a, 0x30, 0x36, 0x31, 0x31, 0x31, 0x32, 0x61, 0x36, 0x2d, - 0x61, 0x32, 0x65, 0x30, 0x2d, 0x34, 0x35, 0x36, 0x63, 0x2d, 0x39, 0x66, - 0x33, 0x38, 0x2d, 0x32, 0x61, 0x64, 0x63, 0x66, 0x39, 0x62, 0x62, 0x38, - 0x38, 0x34, 0x61, 0x22, 0x20, 0x64, 0x63, 0x3a, 0x46, 0x6f, 0x72, 0x6d, - 0x61, 0x74, 0x3d, 0x22, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x6a, 0x70, - 0x65, 0x67, 0x22, 0x20, 0x47, 0x49, 0x4d, 0x50, 0x3a, 0x41, 0x50, 0x49, - 0x3d, 0x22, 0x32, 0x2e, 0x30, 0x22, 0x20, 0x47, 0x49, 0x4d, 0x50, 0x3a, - 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x3d, 0x22, 0x4c, 0x69, - 0x6e, 0x75, 0x78, 0x22, 0x20, 0x47, 0x49, 0x4d, 0x50, 0x3a, 0x54, 0x69, - 0x6d, 0x65, 0x53, 0x74, 0x61, 0x6d, 0x70, 0x3d, 0x22, 0x31, 0x36, 0x35, - 0x30, 0x37, 0x34, 0x31, 0x34, 0x39, 0x31, 0x37, 0x38, 0x34, 0x38, 0x38, - 0x34, 0x22, 0x20, 0x47, 0x49, 0x4d, 0x50, 0x3a, 0x56, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x3d, 0x22, 0x32, 0x2e, 0x31, 0x30, 0x2e, 0x33, 0x30, - 0x22, 0x20, 0x78, 0x6d, 0x70, 0x3a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x6f, - 0x72, 0x54, 0x6f, 0x6f, 0x6c, 0x3d, 0x22, 0x47, 0x49, 0x4d, 0x50, 0x20, - 0x32, 0x2e, 0x31, 0x30, 0x22, 0x3e, 0x20, 0x3c, 0x78, 0x6d, 0x70, 0x4d, - 0x4d, 0x3a, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x3e, 0x20, 0x3c, - 0x72, 0x64, 0x66, 0x3a, 0x53, 0x65, 0x71, 0x3e, 0x20, 0x3c, 0x72, 0x64, - 0x66, 0x3a, 0x6c, 0x69, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3a, 0x61, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x3d, 0x22, 0x73, 0x61, 0x76, 0x65, 0x64, - 0x22, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3a, 0x63, 0x68, 0x61, 0x6e, - 0x67, 0x65, 0x64, 0x3d, 0x22, 0x2f, 0x22, 0x20, 0x73, 0x74, 0x45, 0x76, - 0x74, 0x3a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x44, - 0x3d, 0x22, 0x78, 0x6d, 0x70, 0x2e, 0x69, 0x69, 0x64, 0x3a, 0x61, 0x65, - 0x31, 0x36, 0x62, 0x33, 0x36, 0x37, 0x2d, 0x63, 0x64, 0x35, 0x65, 0x2d, - 0x34, 0x62, 0x39, 0x65, 0x2d, 0x61, 0x66, 0x39, 0x61, 0x2d, 0x61, 0x65, - 0x30, 0x66, 0x37, 0x36, 0x63, 0x38, 0x35, 0x65, 0x30, 0x66, 0x22, 0x20, - 0x73, 0x74, 0x45, 0x76, 0x74, 0x3a, 0x73, 0x6f, 0x66, 0x74, 0x77, 0x61, - 0x72, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x3d, 0x22, 0x47, 0x69, 0x6d, - 0x70, 0x20, 0x32, 0x2e, 0x31, 0x30, 0x20, 0x28, 0x4c, 0x69, 0x6e, 0x75, - 0x78, 0x29, 0x22, 0x20, 0x73, 0x74, 0x45, 0x76, 0x74, 0x3a, 0x77, 0x68, - 0x65, 0x6e, 0x3d, 0x22, 0x32, 0x30, 0x32, 0x32, 0x2d, 0x30, 0x34, 0x2d, - 0x32, 0x33, 0x54, 0x32, 0x31, 0x3a, 0x31, 0x38, 0x3a, 0x31, 0x31, 0x2b, - 0x30, 0x32, 0x3a, 0x30, 0x30, 0x22, 0x2f, 0x3e, 0x20, 0x3c, 0x2f, 0x72, - 0x64, 0x66, 0x3a, 0x53, 0x65, 0x71, 0x3e, 0x20, 0x3c, 0x2f, 0x78, 0x6d, - 0x70, 0x4d, 0x4d, 0x3a, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x3e, - 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x20, 0x3c, 0x2f, 0x72, 0x64, - 0x66, 0x3a, 0x52, 0x44, 0x46, 0x3e, 0x20, 0x3c, 0x2f, 0x78, 0x3a, 0x78, - 0x6d, 0x70, 0x6d, 0x65, 0x74, 0x61, 0x3e, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, - 0x20, 0x20, 0x20, 0x20, 0x3c, 0x3f, 0x78, 0x70, 0x61, 0x63, 0x6b, 0x65, - 0x74, 0x20, 0x65, 0x6e, 0x64, 0x3d, 0x22, 0x77, 0x22, 0x3f, 0x3e, 0xff, - 0xe2, 0x02, 0xb0, 0x49, 0x43, 0x43, 0x5f, 0x50, 0x52, 0x4f, 0x46, 0x49, - 0x4c, 0x45, 0x00, 0x01, 0x01, 0x00, 0x00, 0x02, 0xa0, 0x6c, 0x63, 0x6d, - 0x73, 0x04, 0x30, 0x00, 0x00, 0x6d, 0x6e, 0x74, 0x72, 0x52, 0x47, 0x42, - 0x20, 0x58, 0x59, 0x5a, 0x20, 0x07, 0xe6, 0x00, 0x04, 0x00, 0x17, 0x00, - 0x13, 0x00, 0x08, 0x00, 0x21, 0x61, 0x63, 0x73, 0x70, 0x41, 0x50, 0x50, - 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xf6, 0xd6, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xd3, - 0x2d, 0x6c, 0x63, 0x6d, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x0d, 0x64, 0x65, 0x73, 0x63, 0x00, 0x00, 0x01, - 0x20, 0x00, 0x00, 0x00, 0x40, 0x63, 0x70, 0x72, 0x74, 0x00, 0x00, 0x01, - 0x60, 0x00, 0x00, 0x00, 0x36, 0x77, 0x74, 0x70, 0x74, 0x00, 0x00, 0x01, - 0x98, 0x00, 0x00, 0x00, 0x14, 0x63, 0x68, 0x61, 0x64, 0x00, 0x00, 0x01, - 0xac, 0x00, 0x00, 0x00, 0x2c, 0x72, 0x58, 0x59, 0x5a, 0x00, 0x00, 0x01, - 0xd8, 0x00, 0x00, 0x00, 0x14, 0x62, 0x58, 0x59, 0x5a, 0x00, 0x00, 0x01, - 0xec, 0x00, 0x00, 0x00, 0x14, 0x67, 0x58, 0x59, 0x5a, 0x00, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x14, 0x72, 0x54, 0x52, 0x43, 0x00, 0x00, 0x02, - 0x14, 0x00, 0x00, 0x00, 0x20, 0x67, 0x54, 0x52, 0x43, 0x00, 0x00, 0x02, - 0x14, 0x00, 0x00, 0x00, 0x20, 0x62, 0x54, 0x52, 0x43, 0x00, 0x00, 0x02, - 0x14, 0x00, 0x00, 0x00, 0x20, 0x63, 0x68, 0x72, 0x6d, 0x00, 0x00, 0x02, - 0x34, 0x00, 0x00, 0x00, 0x24, 0x64, 0x6d, 0x6e, 0x64, 0x00, 0x00, 0x02, - 0x58, 0x00, 0x00, 0x00, 0x24, 0x64, 0x6d, 0x64, 0x64, 0x00, 0x00, 0x02, - 0x7c, 0x00, 0x00, 0x00, 0x24, 0x6d, 0x6c, 0x75, 0x63, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x65, 0x6e, 0x55, - 0x53, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x47, 0x00, - 0x49, 0x00, 0x4d, 0x00, 0x50, 0x00, 0x20, 0x00, 0x62, 0x00, 0x75, 0x00, - 0x69, 0x00, 0x6c, 0x00, 0x74, 0x00, 0x2d, 0x00, 0x69, 0x00, 0x6e, 0x00, - 0x20, 0x00, 0x73, 0x00, 0x52, 0x00, 0x47, 0x00, 0x42, 0x6d, 0x6c, 0x75, - 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x0c, 0x65, 0x6e, 0x55, 0x53, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, - 0x1c, 0x00, 0x50, 0x00, 0x75, 0x00, 0x62, 0x00, 0x6c, 0x00, 0x69, 0x00, - 0x63, 0x00, 0x20, 0x00, 0x44, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, - 0x69, 0x00, 0x6e, 0x00, 0x00, 0x58, 0x59, 0x5a, 0x20, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xf6, 0xd6, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xd3, - 0x2d, 0x73, 0x66, 0x33, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x0c, - 0x42, 0x00, 0x00, 0x05, 0xde, 0xff, 0xff, 0xf3, 0x25, 0x00, 0x00, 0x07, - 0x93, 0x00, 0x00, 0xfd, 0x90, 0xff, 0xff, 0xfb, 0xa1, 0xff, 0xff, 0xfd, - 0xa2, 0x00, 0x00, 0x03, 0xdc, 0x00, 0x00, 0xc0, 0x6e, 0x58, 0x59, 0x5a, - 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6f, 0xa0, 0x00, 0x00, 0x38, - 0xf5, 0x00, 0x00, 0x03, 0x90, 0x58, 0x59, 0x5a, 0x20, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x24, 0x9f, 0x00, 0x00, 0x0f, 0x84, 0x00, 0x00, 0xb6, - 0xc4, 0x58, 0x59, 0x5a, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x62, - 0x97, 0x00, 0x00, 0xb7, 0x87, 0x00, 0x00, 0x18, 0xd9, 0x70, 0x61, 0x72, - 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x66, - 0x66, 0x00, 0x00, 0xf2, 0xa7, 0x00, 0x00, 0x0d, 0x59, 0x00, 0x00, 0x13, - 0xd0, 0x00, 0x00, 0x0a, 0x5b, 0x63, 0x68, 0x72, 0x6d, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0xa3, 0xd7, 0x00, 0x00, 0x54, - 0x7c, 0x00, 0x00, 0x4c, 0xcd, 0x00, 0x00, 0x99, 0x9a, 0x00, 0x00, 0x26, - 0x67, 0x00, 0x00, 0x0f, 0x5c, 0x6d, 0x6c, 0x75, 0x63, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x65, 0x6e, 0x55, - 0x53, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x47, 0x00, - 0x49, 0x00, 0x4d, 0x00, 0x50, 0x6d, 0x6c, 0x75, 0x63, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x65, 0x6e, 0x55, - 0x53, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x73, 0x00, - 0x52, 0x00, 0x47, 0x00, 0x42, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x03, 0x02, - 0x02, 0x03, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04, 0x03, 0x03, 0x04, - 0x05, 0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0a, 0x07, 0x07, 0x06, 0x08, - 0x0c, 0x0a, 0x0c, 0x0c, 0x0b, 0x0a, 0x0b, 0x0b, 0x0d, 0x0e, 0x12, 0x10, - 0x0d, 0x0e, 0x11, 0x0e, 0x0b, 0x0b, 0x10, 0x16, 0x10, 0x11, 0x13, 0x14, - 0x15, 0x15, 0x15, 0x0c, 0x0f, 0x17, 0x18, 0x16, 0x14, 0x18, 0x12, 0x14, - 0x15, 0x14, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x03, 0x04, 0x04, 0x05, 0x04, - 0x05, 0x09, 0x05, 0x05, 0x09, 0x14, 0x0d, 0x0b, 0x0d, 0x14, 0x14, 0x14, - 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, - 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, - 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, - 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0xff, - 0xc2, 0x00, 0x11, 0x08, 0x01, 0x54, 0x00, 0xc8, 0x03, 0x01, 0x11, 0x00, - 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, 0x1c, 0x00, 0x01, - 0x00, 0x01, 0x05, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x07, 0x08, 0x05, 0x06, - 0xff, 0xc4, 0x00, 0x17, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, - 0x03, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x10, 0x03, 0x10, - 0x00, 0x00, 0x01, 0xda, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x83, 0xce, 0x31, 0x13, 0x10, 0xa4, 0xa8, - 0xcb, 0x32, 0xd7, 0xd2, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x41, 0xf0, 0x16, - 0x6b, 0x93, 0xe6, 0xeb, 0xc3, 0x4f, 0x38, 0xa0, 0x10, 0x48, 0x24, 0x15, - 0x1e, 0x91, 0xef, 0x2f, 0xd4, 0x46, 0xc8, 0x8f, 0xb7, 0x50, 0x00, 0x03, - 0x42, 0xd9, 0xa7, 0x6c, 0xa8, 0xa8, 0x80, 0x48, 0x04, 0x90, 0x41, 0x24, - 0x12, 0x52, 0x49, 0x49, 0xbd, 0x65, 0xdc, 0xd2, 0x80, 0x07, 0xcc, 0x59, - 0xc9, 0x56, 0x56, 0x52, 0x58, 0x32, 0x0a, 0xc1, 0x04, 0x80, 0x49, 0x49, - 0x50, 0x24, 0x82, 0x80, 0x76, 0x0e, 0x75, 0xec, 0x00, 0x0d, 0x59, 0x67, - 0x3a, 0x59, 0x90, 0x59, 0x2e, 0x10, 0x54, 0x63, 0xac, 0x03, 0x21, 0x31, - 0x96, 0xa2, 0xe2, 0x42, 0xdc, 0x4a, 0x16, 0xa4, 0xa8, 0xb2, 0x74, 0xec, - 0xbb, 0x02, 0x50, 0x06, 0xa7, 0xb3, 0x9e, 0x6c, 0xc9, 0x31, 0xca, 0xcb, - 0x66, 0x55, 0x62, 0xcb, 0x7d, 0x3c, 0xb9, 0xac, 0xeb, 0x30, 0xa5, 0xf4, - 0x2c, 0xa4, 0xb0, 0x4a, 0xd9, 0x8c, 0x9b, 0x32, 0x52, 0xc1, 0xd2, 0xb2, - 0xec, 0x89, 0x40, 0x1a, 0xa2, 0xce, 0x78, 0xb3, 0x24, 0xa4, 0xa8, 0xb6, - 0x54, 0x52, 0x64, 0x16, 0x16, 0xa3, 0x10, 0x83, 0x24, 0xb4, 0x49, 0x90, - 0x52, 0x92, 0x58, 0x3a, 0x56, 0x5d, 0x91, 0x28, 0x03, 0x54, 0x59, 0xce, - 0xf6, 0x64, 0x96, 0x8b, 0x30, 0x33, 0x6a, 0xd8, 0x20, 0xac, 0x92, 0x4a, - 0x4a, 0x89, 0x29, 0x20, 0x82, 0xd1, 0xd2, 0x92, 0xec, 0x89, 0x40, 0x1a, - 0x9e, 0xce, 0x78, 0xb3, 0x24, 0xa8, 0x92, 0x92, 0x0a, 0x49, 0x24, 0x02, - 0xa2, 0x09, 0x04, 0x02, 0x92, 0xc9, 0xd2, 0xf2, 0xec, 0x69, 0x40, 0x1a, - 0xa2, 0xce, 0x79, 0xb2, 0xe9, 0x6d, 0x44, 0x00, 0x08, 0x04, 0x92, 0x40, - 0x24, 0x02, 0xe2, 0x5b, 0x3a, 0x5a, 0x5d, 0x8d, 0x28, 0x03, 0x53, 0x59, - 0xcf, 0xb6, 0x79, 0xf2, 0xda, 0x50, 0x00, 0x00, 0x00, 0x00, 0x03, 0xd5, - 0xb2, 0x13, 0xa5, 0xa5, 0xd8, 0xd2, 0x80, 0x35, 0x2d, 0x9a, 0x0e, 0xcf, - 0x3a, 0x5c, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0xac, 0xca, 0x4e, - 0x93, 0x97, 0x62, 0xca, 0x00, 0xd5, 0x16, 0x68, 0x0b, 0x3c, 0xe9, 0x71, - 0x94, 0x00, 0x00, 0x00, 0x00, 0x00, 0xce, 0xb3, 0x2d, 0x3a, 0x4e, 0x5d, - 0x89, 0x28, 0x03, 0x54, 0x59, 0xa0, 0x2c, 0xf3, 0x97, 0x1a, 0x50, 0x00, - 0x00, 0x00, 0x00, 0x03, 0x3e, 0xcc, 0xa4, 0xe9, 0x5c, 0xdd, 0x84, 0xa0, - 0x0d, 0x4f, 0x66, 0x81, 0xb3, 0xce, 0x5c, 0x69, 0x40, 0x00, 0x00, 0x00, - 0x00, 0x0c, 0xeb, 0x32, 0x93, 0xa5, 0x73, 0x76, 0x1a, 0x80, 0x35, 0x3d, - 0x9a, 0x06, 0xcf, 0x35, 0x71, 0xe5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, - 0xac, 0xca, 0x4e, 0x96, 0xcd, 0xd8, 0x4a, 0x00, 0xd4, 0xf6, 0x68, 0x1b, - 0x3c, 0xc5, 0xb1, 0x28, 0x00, 0x00, 0x00, 0x00, 0x01, 0x9f, 0x66, 0x42, - 0x74, 0xbe, 0x6e, 0xc2, 0x50, 0x06, 0xa7, 0xb3, 0x40, 0xd9, 0xe5, 0xcd, - 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x42, 0xcc, 0x94, 0xe9, 0x5c, - 0xeb, 0x61, 0x00, 0x0d, 0x4f, 0x66, 0x80, 0xb3, 0xcc, 0x5b, 0x32, 0x80, - 0x00, 0x00, 0x00, 0x00, 0x19, 0xf6, 0x64, 0xa7, 0x4b, 0xe7, 0x5b, 0x04, - 0x00, 0x6a, 0x7b, 0x34, 0x05, 0x9e, 0x72, 0xe3, 0xca, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x67, 0xd9, 0x92, 0x9d, 0x2f, 0x9d, 0x6c, 0x10, 0x01, 0xa9, - 0xec, 0xd0, 0x16, 0x79, 0xcb, 0x8f, 0x28, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x9f, 0x66, 0x42, 0x74, 0xc4, 0xbb, 0x06, 0x50, 0x06, 0xa7, 0xb3, 0x40, - 0xd9, 0xe6, 0xae, 0x3c, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x06, 0x7d, 0x99, - 0x09, 0xd3, 0x12, 0xec, 0x19, 0x40, 0x1a, 0x9e, 0xcd, 0x03, 0x67, 0x9a, - 0xb8, 0xf2, 0x80, 0x00, 0x00, 0x00, 0x00, 0x19, 0xf6, 0x64, 0xa7, 0x4b, - 0xe7, 0x5b, 0x04, 0x00, 0x6a, 0x7b, 0x34, 0x0d, 0x9e, 0x6a, 0xe3, 0xca, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x7a, 0x16, 0x64, 0x27, 0x4b, 0xe7, 0x5b, - 0x04, 0x00, 0x6a, 0x8b, 0x34, 0x05, 0x98, 0x4b, 0x85, 0x28, 0x00, 0x00, - 0x00, 0x00, 0x01, 0xe8, 0xd9, 0x79, 0x3a, 0x67, 0x37, 0x60, 0x28, 0x03, - 0x53, 0xd9, 0xcf, 0x76, 0x66, 0xdb, 0xe2, 0x66, 0xc2, 0x49, 0x6d, 0x64, - 0xad, 0x29, 0x5a, 0x49, 0x20, 0x00, 0x01, 0xea, 0x59, 0x6e, 0x4e, 0x9f, - 0x97, 0xef, 0xd4, 0x01, 0xa9, 0xec, 0xe7, 0xab, 0x33, 0x0b, 0xd6, 0xc2, - 0x41, 0x0a, 0x21, 0x11, 0x16, 0xca, 0x80, 0x82, 0x4d, 0x51, 0x25, 0xa3, - 0x1a, 0x3a, 0x82, 0x5f, 0xbf, 0x50, 0x06, 0xac, 0xb3, 0x9c, 0xec, 0xbc, - 0x57, 0x54, 0x90, 0x44, 0x40, 0x24, 0x92, 0x5a, 0x82, 0x41, 0x05, 0x49, - 0x4a, 0x52, 0x5a, 0x8e, 0xa4, 0x97, 0xef, 0x14, 0x01, 0xf2, 0x16, 0x72, - 0x6d, 0x92, 0x53, 0x2a, 0xa4, 0x10, 0x0c, 0xbb, 0x2d, 0xc1, 0xac, 0x68, - 0xa8, 0x12, 0x82, 0x2b, 0x21, 0x2c, 0x4d, 0x76, 0x4c, 0x7b, 0x00, 0x03, - 0xcc, 0x38, 0xeb, 0x52, 0xd9, 0x7d, 0x05, 0xb0, 0x41, 0x26, 0x45, 0x61, - 0x97, 0xd6, 0x89, 0x64, 0x24, 0x20, 0x82, 0xb2, 0xf4, 0xd7, 0x68, 0x44, - 0x80, 0x08, 0x39, 0x2f, 0x59, 0xf9, 0xb5, 0xad, 0x24, 0xa0, 0x90, 0x5a, - 0x5c, 0x93, 0x09, 0x32, 0x8b, 0x2b, 0x74, 0x84, 0xa8, 0x82, 0x4a, 0x4f, - 0xbb, 0x9a, 0xe9, 0xd8, 0x00, 0x01, 0xa4, 0x75, 0x34, 0x9a, 0x5a, 0x5a, - 0x60, 0x42, 0x91, 0x59, 0x05, 0xb4, 0xac, 0x2e, 0x3c, 0x0a, 0x88, 0x05, - 0xfb, 0x3a, 0x0f, 0x3a, 0xdb, 0x00, 0x00, 0x0f, 0x02, 0xce, 0x42, 0xb2, - 0xdc, 0x4d, 0x40, 0x85, 0x65, 0x47, 0xaa, 0xbf, 0x61, 0x1f, 0x28, 0x79, - 0x95, 0xe6, 0xa5, 0x54, 0x88, 0x24, 0xc9, 0x6b, 0xb2, 0xa3, 0x30, 0x00, - 0x00, 0x39, 0xa3, 0x53, 0x59, 0xc9, 0x70, 0x50, 0x83, 0xe9, 0xb3, 0x70, - 0x0f, 0x6e, 0xbc, 0x52, 0xec, 0x79, 0xb5, 0x88, 0x82, 0x4a, 0x4d, 0xc8, - 0xd6, 0xfd, 0x80, 0x00, 0x00, 0x7c, 0xb5, 0x9c, 0x95, 0x65, 0x11, 0x50, - 0xaf, 0xa0, 0x97, 0xed, 0x32, 0xf8, 0x04, 0xf7, 0xed, 0xf2, 0x24, 0xfa, - 0xbb, 0x70, 0x4f, 0x85, 0xb2, 0x01, 0x90, 0xd7, 0x62, 0x47, 0xa8, 0x00, - 0x00, 0x00, 0x73, 0xd6, 0x9a, 0x82, 0x66, 0xe9, 0x35, 0xf4, 0x32, 0xf8, - 0xe6, 0x2e, 0x5f, 0x6e, 0xd7, 0xc2, 0xcd, 0xfd, 0x56, 0xf9, 0x7b, 0x52, - 0xeb, 0xeb, 0x99, 0x2d, 0xb5, 0xbd, 0xe3, 0x75, 0x80, 0x00, 0x00, 0x03, - 0xc9, 0x39, 0x0b, 0x59, 0xc1, 0x8a, 0xcf, 0xa0, 0x59, 0x8c, 0x19, 0x73, - 0xac, 0xc3, 0xcf, 0x5c, 0x8a, 0xa6, 0xf3, 0xf1, 0x2e, 0x69, 0x3d, 0x96, - 0xbb, 0x02, 0x2f, 0x80, 0x00, 0x00, 0x00, 0x69, 0xaa, 0xd0, 0x0c, 0xdc, - 0x32, 0x0f, 0xa3, 0x97, 0xe8, 0x4f, 0x91, 0xaf, 0x46, 0x3d, 0x25, 0xf9, - 0x5b, 0x3c, 0xa4, 0xb2, 0xd7, 0x4c, 0x46, 0xcc, 0x00, 0x00, 0x00, 0x00, - 0x18, 0xe7, 0x28, 0x5c, 0xfc, 0x93, 0x55, 0xb3, 0x98, 0x7a, 0x52, 0xe7, - 0x1e, 0x79, 0x8f, 0x5e, 0x5a, 0x41, 0xb0, 0x1a, 0xea, 0x18, 0x90, 0x00, - 0x00, 0x00, 0x00, 0x3c, 0xd3, 0x48, 0x57, 0xc5, 0x18, 0x44, 0xa1, 0x45, - 0x66, 0x71, 0xb0, 0x63, 0x74, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x07, 0xff, 0xc4, 0x00, 0x31, 0x10, 0x00, 0x00, 0x05, - 0x02, 0x03, 0x07, 0x03, 0x04, 0x02, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x11, 0x14, 0x20, 0x10, - 0x12, 0x13, 0x30, 0x33, 0x34, 0x35, 0x07, 0x21, 0x32, 0x15, 0x16, 0x31, - 0x41, 0x22, 0x24, 0x23, 0x42, 0x50, 0x43, 0xff, 0xda, 0x00, 0x08, 0x01, - 0x01, 0x00, 0x01, 0x05, 0x02, 0xff, 0x00, 0x82, 0xa7, 0xdb, 0x6c, 0x2e, - 0xb1, 0x05, 0xb0, 0xab, 0x8e, 0x98, 0x90, 0x77, 0x65, 0x24, 0x87, 0xdd, - 0xd4, 0x90, 0x57, 0x65, 0x24, 0xc2, 0x6e, 0x3a, 0x62, 0x82, 0x2b, 0x10, - 0x5c, 0x08, 0x79, 0xb7, 0x39, 0xb5, 0x5b, 0xd2, 0x0d, 0x34, 0xe4, 0xfa, - 0x87, 0x35, 0xc0, 0xf5, 0xdd, 0x54, 0x7c, 0x3b, 0x54, 0x96, 0xf8, 0x35, - 0xa8, 0xc6, 0x26, 0x3d, 0xc7, 0xb8, 0xc4, 0x62, 0x63, 0x11, 0xbc, 0x60, - 0x9c, 0x51, 0x06, 0xaa, 0xf3, 0x58, 0x0c, 0x5e, 0x35, 0x56, 0x04, 0x5f, - 0x51, 0x65, 0x24, 0xe9, 0x57, 0x74, 0x1a, 0xa2, 0xb5, 0xde, 0x17, 0x52, - 0x9d, 0x73, 0xf2, 0x30, 0x18, 0x72, 0x70, 0x18, 0x0c, 0x06, 0x03, 0x00, - 0x46, 0x69, 0x3b, 0x36, 0xe9, 0x53, 0xea, 0xd3, 0x72, 0x4f, 0xfa, 0x75, - 0x24, 0xd4, 0x6b, 0x51, 0x05, 0xab, 0x70, 0x8b, 0x88, 0xa0, 0x93, 0xc7, - 0x97, 0x80, 0x32, 0x06, 0x1a, 0x75, 0x4c, 0x3b, 0x48, 0x9b, 0xf5, 0x0a, - 0x6e, 0x8b, 0xfd, 0x58, 0x53, 0xd2, 0x08, 0x39, 0xba, 0xa5, 0x1b, 0xc9, - 0xc4, 0x9c, 0x25, 0xc8, 0xc3, 0x13, 0x37, 0x89, 0x27, 0x98, 0x48, 0xcc, - 0xa4, 0x29, 0x65, 0xc3, 0xcc, 0xa4, 0x66, 0x12, 0x1a, 0x73, 0x88, 0xa5, - 0x3e, 0x94, 0x84, 0x3a, 0x4b, 0x0a, 0x7c, 0x92, 0x6d, 0xb8, 0x4e, 0x6c, - 0x31, 0x63, 0xb9, 0xbf, 0x40, 0xd1, 0xea, 0x07, 0x66, 0x41, 0x1e, 0xe7, - 0xc3, 0xde, 0x5f, 0x04, 0x82, 0xd9, 0x2d, 0xc4, 0xf4, 0xa4, 0xa4, 0xb8, - 0x7c, 0x04, 0x13, 0x69, 0x34, 0x92, 0x50, 0x9f, 0xe8, 0x27, 0x02, 0x4c, - 0x84, 0xb4, 0xd1, 0x44, 0x67, 0x01, 0x1d, 0xa4, 0xac, 0x9c, 0xc1, 0x87, - 0xb7, 0xff, 0x00, 0xcd, 0x1c, 0xcb, 0x31, 0xfa, 0x31, 0x61, 0xf8, 0x5d, - 0x17, 0xff, 0x00, 0x66, 0x90, 0x9f, 0xcb, 0x5b, 0x17, 0xf0, 0x4f, 0xb3, - 0x4e, 0x23, 0x88, 0xdf, 0xb6, 0xe2, 0x5a, 0x4f, 0x01, 0x29, 0x51, 0xc6, - 0x26, 0x5c, 0x22, 0x5c, 0x65, 0x99, 0xb0, 0xda, 0xdb, 0x79, 0xc8, 0xdf, - 0xc9, 0x98, 0xfb, 0x8a, 0xdc, 0x22, 0x94, 0xda, 0x38, 0x4f, 0x03, 0x16, - 0x1f, 0x85, 0xd1, 0xea, 0x07, 0x64, 0x90, 0x41, 0x4d, 0x2b, 0x1c, 0x56, - 0x40, 0x9b, 0x52, 0x87, 0xe1, 0xb0, 0x60, 0xb6, 0xfe, 0xb6, 0xfe, 0xb6, - 0x28, 0x58, 0x7e, 0x17, 0x47, 0xa8, 0x1d, 0x92, 0x41, 0x6c, 0xc7, 0xdb, - 0xf2, 0x30, 0xd9, 0x80, 0xc3, 0x56, 0x1a, 0x0c, 0x58, 0x7e, 0x0f, 0x45, - 0xff, 0x00, 0xd9, 0x24, 0x82, 0x9c, 0x4a, 0x06, 0x65, 0x23, 0x32, 0x81, - 0x99, 0x40, 0xcc, 0xa4, 0x66, 0x10, 0x33, 0x29, 0x19, 0x94, 0x8c, 0xca, - 0x46, 0x69, 0x23, 0x34, 0x91, 0x99, 0x48, 0xcd, 0x24, 0x66, 0x50, 0x33, - 0x29, 0x07, 0x25, 0x20, 0x95, 0xbc, 0x14, 0x2c, 0x3f, 0x05, 0xa3, 0xd4, - 0x0e, 0xcd, 0xb2, 0xc4, 0x3b, 0xd4, 0xe7, 0xc0, 0x3d, 0xe7, 0x15, 0xec, - 0x2c, 0x3f, 0x05, 0xa3, 0xd4, 0x0e, 0xd2, 0x3f, 0x52, 0x47, 0x5b, 0x9f, - 0x07, 0xbb, 0x77, 0xe5, 0x61, 0xf8, 0x1d, 0x17, 0xff, 0x00, 0x67, 0x1f, - 0xa9, 0x27, 0xad, 0xcf, 0x85, 0xdd, 0x3b, 0xf3, 0xb1, 0x7c, 0x16, 0x8b, - 0xff, 0x00, 0xb3, 0x63, 0xa9, 0x27, 0xad, 0xcf, 0x83, 0xdd, 0x3b, 0xf2, - 0xb1, 0xbc, 0x16, 0x8b, 0xff, 0x00, 0xb3, 0x8f, 0xd4, 0x93, 0xd7, 0xe7, - 0xc2, 0xee, 0x5d, 0xf9, 0xd8, 0xbe, 0x0b, 0x45, 0xff, 0x00, 0xd9, 0x31, - 0xf3, 0x91, 0xd6, 0xe7, 0xc2, 0xee, 0x9e, 0xf9, 0xd8, 0xbe, 0x0b, 0x45, - 0xff, 0x00, 0xd9, 0xb1, 0xf3, 0x7f, 0xad, 0xcf, 0x83, 0xdd, 0x3b, 0xf3, - 0xb1, 0xbc, 0x0e, 0x8b, 0xff, 0x00, 0xb3, 0x63, 0xe6, 0xf7, 0x57, 0x9f, - 0x03, 0xbb, 0x7b, 0xe7, 0x63, 0x78, 0x1d, 0x17, 0xff, 0x00, 0x67, 0x1f, - 0xe6, 0xf7, 0x57, 0x9f, 0x07, 0xba, 0x77, 0xe7, 0x63, 0x78, 0x1d, 0x17, - 0xff, 0x00, 0x65, 0x1f, 0xa8, 0xff, 0x00, 0x5b, 0x9f, 0x07, 0xba, 0x7b, - 0xe7, 0x63, 0x78, 0x1d, 0x17, 0xff, 0x00, 0x67, 0x1f, 0xa9, 0x23, 0xad, - 0xcf, 0x83, 0xdd, 0x3d, 0xf2, 0xb1, 0xbc, 0x0e, 0x8b, 0xff, 0x00, 0xb3, - 0x8f, 0xf3, 0x91, 0xd6, 0xe7, 0xc1, 0xee, 0x9e, 0xf9, 0xd8, 0xde, 0x07, - 0x45, 0xff, 0x00, 0xd9, 0xb1, 0xd4, 0x91, 0xd6, 0xe7, 0xc1, 0xee, 0x9e, - 0xf9, 0xd8, 0xde, 0x07, 0x45, 0xff, 0x00, 0xd9, 0xb1, 0xd4, 0x91, 0xd5, - 0xe7, 0xc0, 0xee, 0x9e, 0xf9, 0xd8, 0xde, 0x03, 0x45, 0xff, 0x00, 0xd9, - 0x47, 0xea, 0x4c, 0x4e, 0xe4, 0x8e, 0x7c, 0x02, 0xfe, 0xcb, 0xa7, 0x8a, - 0xec, 0x6f, 0x01, 0xa2, 0xff, 0x00, 0xec, 0xd0, 0x78, 0x1c, 0xc6, 0xf8, - 0xc5, 0xc2, 0x31, 0xc3, 0x31, 0xc2, 0x31, 0xba, 0x63, 0x74, 0xc1, 0x34, - 0x66, 0x0d, 0xb3, 0x21, 0xba, 0x63, 0x70, 0xc6, 0xe9, 0x8d, 0xd3, 0x18, - 0x18, 0xdd, 0x31, 0xba, 0x63, 0x74, 0xc4, 0x54, 0xf0, 0xc1, 0x8b, 0x1f, - 0xc0, 0x68, 0xbf, 0xfb, 0x34, 0x84, 0x39, 0x81, 0x11, 0xa0, 0x19, 0xa0, - 0x6f, 0x20, 0x6f, 0x20, 0x1a, 0x90, 0x37, 0x90, 0x37, 0x90, 0x31, 0x49, - 0x8d, 0xe4, 0x8d, 0xe4, 0x8f, 0xe0, 0x31, 0x40, 0xc5, 0x23, 0x14, 0x83, - 0x52, 0x42, 0x95, 0x88, 0x31, 0x63, 0xf8, 0x0d, 0x17, 0xe3, 0x6a, 0x54, - 0x14, 0x9e, 0xdc, 0x46, 0x20, 0xb6, 0x7e, 0x74, 0x62, 0x31, 0xd9, 0xfb, - 0xc4, 0x62, 0x30, 0x06, 0x2c, 0xa4, 0x29, 0x14, 0x0d, 0x17, 0x53, 0x5c, - 0x4a, 0x29, 0x96, 0xea, 0xd4, 0xad, 0xd4, 0x70, 0x14, 0x63, 0x2e, 0x0a, - 0x36, 0x23, 0x2c, 0x32, 0xc0, 0x98, 0x32, 0x33, 0x46, 0x0d, 0xec, 0x26, - 0xd4, 0xe8, 0xcb, 0x8c, 0xb0, 0xcb, 0x0c, 0xb8, 0xe0, 0x04, 0xa4, 0xf0, - 0xfc, 0x9d, 0x1d, 0x9c, 0xbd, 0x2b, 0x45, 0x4d, 0x9c, 0xc5, 0x3e, 0x6a, - 0x37, 0x25, 0xff, 0x00, 0xa8, 0xfc, 0x6c, 0xde, 0x18, 0x8f, 0xd2, 0x7d, - 0xe1, 0x86, 0xc7, 0xeb, 0x41, 0x16, 0x01, 0xbe, 0xa5, 0x3a, 0x3e, 0x6e, - 0xa0, 0x45, 0x81, 0x69, 0xba, 0x21, 0x64, 0xea, 0x5f, 0xf8, 0x91, 0x90, - 0xf6, 0xd9, 0x80, 0xf6, 0x0b, 0x51, 0x11, 0x20, 0xff, 0x00, 0xa2, 0x18, - 0x23, 0x5a, 0xb7, 0xbd, 0xb1, 0x2d, 0xb8, 0x03, 0x51, 0x60, 0xc1, 0x92, - 0x97, 0x62, 0x43, 0xcc, 0xd7, 0xb5, 0x7a, 0x81, 0x4e, 0x0d, 0x2f, 0x74, - 0x95, 0x1d, 0x26, 0x79, 0x61, 0x97, 0x19, 0x61, 0x96, 0x05, 0x19, 0x21, - 0x4a, 0xc7, 0x62, 0x55, 0xba, 0x6e, 0xa1, 0x0f, 0x1e, 0x58, 0x65, 0x86, - 0x58, 0x65, 0x86, 0x58, 0x87, 0xf1, 0x42, 0x7d, 0x3d, 0xa7, 0x65, 0xe9, - 0xba, 0xab, 0x90, 0x3e, 0xa3, 0x4d, 0x90, 0xdf, 0x05, 0xf2, 0xd3, 0x16, - 0x33, 0xb3, 0x1e, 0x5c, 0x58, 0x11, 0x4a, 0x12, 0xe1, 0x4f, 0x86, 0x97, - 0x29, 0x35, 0x19, 0x12, 0xa0, 0x3d, 0x11, 0xbd, 0x98, 0xed, 0x89, 0x15, - 0x73, 0xa5, 0xc3, 0x8a, 0x88, 0x51, 0x75, 0xde, 0xf4, 0x8c, 0x94, 0xf2, - 0xd0, 0x61, 0x45, 0xfe, 0x72, 0x96, 0xd4, 0x23, 0x8b, 0x75, 0x4d, 0x8d, - 0x03, 0xea, 0x6b, 0x7c, 0x76, 0xb1, 0xe5, 0xb2, 0x86, 0x1e, 0xda, 0x63, - 0xd3, 0xba, 0x3f, 0x11, 0xfe, 0x45, 0xc7, 0x4a, 0xfa, 0xad, 0x39, 0xe6, - 0x8e, 0x3b, 0xb8, 0xed, 0xa6, 0xb8, 0x71, 0x18, 0x95, 0x6a, 0x37, 0x43, - 0x4c, 0x19, 0x73, 0xe6, 0x55, 0x73, 0xb1, 0x04, 0xba, 0x94, 0xea, 0x7b, - 0x94, 0x3a, 0x5b, 0x15, 0x59, 0x75, 0x38, 0x99, 0x38, 0xfb, 0x62, 0x45, - 0x72, 0x7c, 0xaa, 0x64, 0x06, 0xe9, 0x70, 0x79, 0x37, 0xd5, 0x0f, 0x2f, - 0x20, 0xb6, 0xbd, 0x1d, 0xa6, 0xed, 0x59, 0x0e, 0x3e, 0x65, 0x1d, 0x0e, - 0xbb, 0x59, 0x3a, 0x0c, 0x80, 0xe3, 0x87, 0x26, 0x5b, 0x8d, 0xad, 0x75, - 0xb6, 0x5a, 0x81, 0x3e, 0xb9, 0xb0, 0xc7, 0xa7, 0xb4, 0x3e, 0x1b, 0x7c, - 0xaa, 0xa4, 0x04, 0xd4, 0xa1, 0x54, 0x61, 0xaa, 0x0c, 0xa2, 0x31, 0x88, - 0xa6, 0xb6, 0x89, 0x31, 0x21, 0xc8, 0x89, 0x51, 0x6a, 0x04, 0x4a, 0x84, - 0x3a, 0xb5, 0x42, 0x33, 0x0d, 0x46, 0x9d, 0x09, 0xea, 0x91, 0xbc, 0xac, - 0xb5, 0x3a, 0x1b, 0x68, 0x8f, 0x40, 0x06, 0x62, 0x8b, 0x4b, 0x5d, 0x66, - 0xa4, 0xcb, 0x28, 0x8e, 0xcf, 0x2e, 0xfc, 0xa2, 0x71, 0x5b, 0x2f, 0x63, - 0x0c, 0x48, 0x5c, 0x57, 0x8c, 0xb7, 0x57, 0x5f, 0x6a, 0x8d, 0x52, 0x74, - 0xad, 0xea, 0xa2, 0x8a, 0x9f, 0x01, 0xa8, 0x0e, 0x56, 0xdb, 0x87, 0x56, - 0xa9, 0x54, 0xa7, 0x67, 0x24, 0x1e, 0xcb, 0x1a, 0x87, 0xf4, 0xda, 0x77, - 0x32, 0x43, 0x08, 0x94, 0xc5, 0xc9, 0x43, 0x76, 0x8f, 0x38, 0x8f, 0x64, - 0x59, 0xb2, 0x20, 0xb9, 0x22, 0x54, 0x3a, 0x88, 0x8d, 0x0e, 0x1a, 0xe0, - 0x97, 0xd2, 0xe3, 0xa6, 0x4d, 0x4e, 0x54, 0xb6, 0x46, 0x22, 0xcf, 0xb7, - 0x15, 0x58, 0x99, 0xce, 0x9f, 0x4f, 0x62, 0xa5, 0x1e, 0xa7, 0xe9, 0xcc, - 0x84, 0x2d, 0xcb, 0x46, 0xb0, 0xd0, 0x3b, 0x7a, 0xa8, 0x43, 0xed, 0xea, - 0xa9, 0x0f, 0xb7, 0xaa, 0xa3, 0xed, 0xea, 0xa0, 0x2b, 0x6e, 0xaa, 0xa3, - 0x66, 0xce, 0xac, 0x3e, 0x74, 0x9f, 0x4e, 0x77, 0x54, 0xc3, 0x0d, 0xc5, - 0x6b, 0xfe, 0xcf, 0xff, 0xc4, 0x00, 0x1b, 0x11, 0x00, 0x02, 0x02, 0x03, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x02, 0x11, 0x21, 0x40, 0x70, 0x90, 0xff, 0xda, 0x00, 0x08, - 0x01, 0x03, 0x01, 0x01, 0x3f, 0x01, 0xf6, 0x3e, 0x8c, 0x0e, 0x0a, 0xb6, - 0x63, 0x22, 0x95, 0x8f, 0x83, 0xff, 0x00, 0xff, 0xc4, 0x00, 0x1e, 0x11, - 0x00, 0x02, 0x02, 0x02, 0x03, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x11, 0x20, 0x00, 0x30, 0x10, 0x41, - 0x50, 0x60, 0x70, 0xff, 0xda, 0x00, 0x08, 0x01, 0x02, 0x01, 0x01, 0x3f, - 0x01, 0xfb, 0xf0, 0xc8, 0xd5, 0xd6, 0xf0, 0xc8, 0x64, 0x32, 0x19, 0x09, - 0x0b, 0x86, 0x43, 0x21, 0x90, 0xc8, 0x48, 0x5c, 0x24, 0x2e, 0x12, 0x17, - 0x09, 0x0b, 0x84, 0x85, 0xc2, 0x42, 0xe1, 0x21, 0x70, 0x90, 0xb8, 0x48, - 0x5c, 0x70, 0x50, 0x17, 0x0c, 0x86, 0x43, 0x23, 0xc2, 0xeb, 0xca, 0x1c, - 0x46, 0x46, 0x46, 0x46, 0x45, 0xa3, 0x23, 0x23, 0x23, 0x23, 0x48, 0xd1, - 0x3c, 0x4f, 0x9c, 0x59, 0x2c, 0x9b, 0x0f, 0xc1, 0xff, 0x00, 0xff, 0xc4, - 0x00, 0x44, 0x10, 0x00, 0x01, 0x03, 0x01, 0x04, 0x05, 0x08, 0x05, 0x0a, - 0x05, 0x04, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x03, 0x11, - 0x04, 0x12, 0x21, 0x31, 0x10, 0x20, 0x22, 0x41, 0x71, 0x05, 0x13, 0x32, - 0x40, 0x51, 0x61, 0x72, 0x81, 0x14, 0x30, 0x33, 0x52, 0x92, 0x23, 0x34, - 0x35, 0x42, 0x62, 0x82, 0xa1, 0xb1, 0xc1, 0xd1, 0x15, 0x50, 0x73, 0x83, - 0x91, 0x06, 0x24, 0x43, 0xf0, 0x63, 0x93, 0xd3, 0xff, 0xda, 0x00, 0x08, - 0x01, 0x01, 0x00, 0x06, 0x3f, 0x02, 0xfe, 0x43, 0xb5, 0x23, 0x5b, 0xc4, - 0xad, 0xab, 0x64, 0x03, 0xfb, 0x81, 0x63, 0x6d, 0x8b, 0xfc, 0xaf, 0x9e, - 0x37, 0xe1, 0x3f, 0xb2, 0xf9, 0xd8, 0xf8, 0x4a, 0xf9, 0xe3, 0x7e, 0x12, - 0xb0, 0xb6, 0xc5, 0xfe, 0x56, 0xcd, 0xb2, 0x03, 0xfd, 0xc0, 0xb6, 0x5e, - 0xd7, 0x70, 0x3e, 0xb4, 0xb1, 0x84, 0xda, 0xa5, 0x1b, 0x99, 0x97, 0xf9, - 0x47, 0x9a, 0x8a, 0x28, 0x47, 0x0b, 0xc5, 0x63, 0x6b, 0x7b, 0x7c, 0x14, - 0x6a, 0xf9, 0x4b, 0x4c, 0xaf, 0xf1, 0x3c, 0xac, 0xfd, 0x46, 0x05, 0x7c, - 0x9d, 0xaa, 0x66, 0x70, 0x79, 0x5f, 0x3a, 0x73, 0xbc, 0x60, 0x15, 0xf2, - 0xf0, 0x45, 0x28, 0xfb, 0x3b, 0x25, 0x06, 0x5e, 0x30, 0x4a, 0x7e, 0xac, - 0x9b, 0xfc, 0xfd, 0x43, 0xec, 0x56, 0x47, 0xdd, 0x88, 0x60, 0xf7, 0x8f, - 0xad, 0xd4, 0x70, 0x42, 0xc3, 0x6b, 0x75, 0x5d, 0xff, 0x00, 0x1b, 0xcf, - 0xe5, 0xad, 0x33, 0xda, 0x68, 0xf7, 0x6c, 0xb5, 0x13, 0xa7, 0x7f, 0x92, - 0xef, 0xf5, 0xcd, 0x7b, 0x4d, 0x0b, 0x4a, 0x82, 0x7d, 0xee, 0x6e, 0x3c, - 0x75, 0x62, 0x6e, 0xe3, 0x78, 0xe9, 0x66, 0x38, 0x6f, 0x59, 0xa8, 0xe9, - 0xc0, 0xac, 0x15, 0x08, 0x2b, 0x7a, 0xde, 0xaf, 0xee, 0x5b, 0xd6, 0x4a, - 0xe8, 0x18, 0xaa, 0x67, 0xa0, 0x8a, 0x1a, 0x8c, 0x16, 0x1a, 0x63, 0x1e, - 0xeb, 0x88, 0xd5, 0x83, 0xef, 0x7e, 0x9a, 0x02, 0x7e, 0xe0, 0x17, 0x49, - 0x13, 0x5c, 0x94, 0x5c, 0x3f, 0x55, 0x7a, 0x9b, 0x57, 0x91, 0xa3, 0x71, - 0xb9, 0x5a, 0xac, 0x59, 0x53, 0xdb, 0x55, 0x25, 0x70, 0xc6, 0xa1, 0x7b, - 0x3a, 0xf7, 0xa1, 0xb0, 0x0d, 0x5a, 0x0f, 0xe0, 0xaf, 0xf7, 0x1a, 0x27, - 0x39, 0xd8, 0xd0, 0xe4, 0x98, 0x63, 0xec, 0xc4, 0x27, 0x17, 0xb6, 0xa7, - 0xb1, 0x5e, 0xa5, 0x1b, 0x4c, 0x74, 0xfd, 0xed, 0x58, 0x7e, 0xf6, 0x80, - 0xa5, 0xff, 0x00, 0xbb, 0xf4, 0x39, 0x33, 0x87, 0xea, 0xae, 0xef, 0xaa, - 0x2d, 0xed, 0x65, 0x11, 0x61, 0xa5, 0xef, 0x79, 0x18, 0xc9, 0x15, 0xdc, - 0xa9, 0x7f, 0xf1, 0x55, 0x2e, 0x05, 0x35, 0xd2, 0x1b, 0xcd, 0x1d, 0xea, - 0xb1, 0x9a, 0x03, 0xb9, 0x5e, 0x76, 0x3d, 0xc9, 0xf2, 0x1a, 0x39, 0xa5, - 0x3e, 0x86, 0xb1, 0xbb, 0x4f, 0xde, 0xd5, 0x87, 0xef, 0x69, 0x25, 0xa7, - 0x3d, 0x18, 0xfe, 0x29, 0xa3, 0xb3, 0xd7, 0x7d, 0xfd, 0x58, 0xbe, 0xf7, - 0x54, 0x1e, 0x33, 0xab, 0x17, 0x9e, 0x8c, 0x73, 0xd1, 0x92, 0xde, 0xb2, - 0x2b, 0x22, 0xb2, 0x59, 0x2c, 0x8a, 0xc8, 0xac, 0x8a, 0xc8, 0xac, 0x8a, - 0xc8, 0xac, 0x8a, 0xdf, 0xa4, 0x78, 0xce, 0xac, 0x5e, 0x7a, 0x0f, 0x50, - 0xe6, 0xdd, 0x93, 0xb4, 0x0f, 0x19, 0xd5, 0x87, 0xcd, 0x37, 0x8a, 0x7f, - 0x50, 0x8f, 0x8a, 0x28, 0x78, 0xce, 0xac, 0x5e, 0x69, 0xa9, 0xdd, 0x41, - 0x89, 0xc9, 0xbe, 0x23, 0xab, 0x1f, 0x9a, 0x09, 0xdd, 0x41, 0x88, 0xa6, - 0x78, 0x8e, 0xac, 0x5e, 0x68, 0x27, 0x75, 0x06, 0x22, 0x9b, 0xe2, 0x3a, - 0xb1, 0x79, 0xa6, 0xa7, 0xf5, 0x06, 0x71, 0x47, 0x8a, 0x67, 0x88, 0xea, - 0xc5, 0xe6, 0x9a, 0x9f, 0xc7, 0xa8, 0x31, 0x39, 0x33, 0xc4, 0x75, 0x62, - 0xf3, 0x41, 0x3f, 0x8f, 0x50, 0x8f, 0x8a, 0x3c, 0x53, 0x3c, 0x47, 0x56, - 0x2f, 0x34, 0xd4, 0xee, 0x3d, 0x41, 0x88, 0xa6, 0x78, 0x8e, 0xac, 0x5e, - 0x69, 0xa9, 0xfc, 0x7a, 0x83, 0x11, 0x4c, 0xf1, 0x1d, 0x58, 0xbc, 0xd0, - 0x4e, 0xea, 0x0c, 0x45, 0x33, 0xc4, 0x75, 0x62, 0xf3, 0x41, 0x3b, 0xa8, - 0x31, 0x14, 0xcf, 0x11, 0xd5, 0x8f, 0xcd, 0x35, 0x3b, 0xa8, 0x31, 0x14, - 0xcf, 0x11, 0xd5, 0x8f, 0xcd, 0x04, 0xee, 0xa1, 0x1a, 0x2a, 0x3f, 0x11, - 0xd5, 0x8f, 0xcd, 0x35, 0x38, 0x75, 0x06, 0xa2, 0xa3, 0xf1, 0x1d, 0x58, - 0xbc, 0xd0, 0x41, 0xe3, 0xd5, 0x64, 0xb2, 0xd3, 0x96, 0x82, 0xe3, 0xa2, - 0x3f, 0x11, 0xd5, 0x8f, 0xcf, 0x45, 0x37, 0x2c, 0x96, 0x4b, 0x25, 0x92, - 0xc9, 0x64, 0xb2, 0x59, 0x2c, 0x97, 0x45, 0x64, 0xb2, 0x59, 0x2c, 0x96, - 0x5a, 0x63, 0xf1, 0x1d, 0x58, 0xc8, 0x6d, 0x47, 0x54, 0x86, 0xf0, 0xa5, - 0x4d, 0x46, 0xac, 0xbf, 0x64, 0x82, 0x88, 0xec, 0x2b, 0x05, 0x8b, 0xf1, - 0x5d, 0x32, 0xba, 0x6b, 0xa6, 0x57, 0x4c, 0xac, 0x1d, 0x52, 0x9a, 0xf1, - 0x91, 0xd3, 0x78, 0xba, 0xe8, 0xdc, 0xba, 0x65, 0x74, 0xca, 0xe9, 0x95, - 0xed, 0x0a, 0xe9, 0xa3, 0xda, 0xdd, 0x16, 0x48, 0xf2, 0x2d, 0x89, 0xbf, - 0x96, 0xad, 0xa2, 0x3e, 0xd6, 0x14, 0xfd, 0xdb, 0xd3, 0x7c, 0x5a, 0xe7, - 0xfa, 0x9a, 0x1d, 0xe1, 0x29, 0xbc, 0x35, 0x8f, 0x80, 0xa8, 0x21, 0xf7, - 0xde, 0x1a, 0xa9, 0xad, 0x23, 0x77, 0x07, 0x11, 0xfb, 0x20, 0xee, 0xc7, - 0x6b, 0xf1, 0x7d, 0x74, 0x53, 0xb9, 0x37, 0x86, 0xb3, 0xce, 0xe0, 0xc4, - 0xd7, 0xfd, 0x58, 0x41, 0x7e, 0xbb, 0x2d, 0x0d, 0x1d, 0x21, 0x43, 0xc4, - 0x2e, 0xd0, 0x77, 0x2d, 0x97, 0x16, 0xf7, 0x2f, 0x68, 0xbd, 0xaa, 0xf6, - 0xab, 0xda, 0xac, 0x5e, 0x4a, 0x03, 0x70, 0xd0, 0x0e, 0xf5, 0x7b, 0xa0, - 0xee, 0xe5, 0xed, 0x17, 0xb5, 0x5e, 0xd9, 0x7b, 0x55, 0xed, 0x15, 0xd6, - 0xa9, 0x2d, 0x4e, 0x1b, 0x53, 0xbb, 0x0e, 0x03, 0x5e, 0x58, 0xc0, 0xdb, - 0x1b, 0x4d, 0xe2, 0x9c, 0xdd, 0xdb, 0xb5, 0xb9, 0xa8, 0x80, 0x2f, 0xa1, - 0x76, 0x2e, 0x03, 0x00, 0x2a, 0x73, 0xe0, 0xa3, 0xf4, 0x9b, 0x69, 0x32, - 0x98, 0xef, 0x98, 0xec, 0xed, 0x6c, 0x82, 0xb5, 0xe8, 0xdf, 0x0e, 0xec, - 0xfc, 0xd3, 0x2d, 0xd1, 0x7f, 0xa6, 0x39, 0xcb, 0x15, 0x94, 0x1f, 0x4c, - 0x7b, 0x27, 0x93, 0x3e, 0xed, 0xa4, 0xc3, 0xce, 0xbf, 0x92, 0xf9, 0xc9, - 0x2e, 0x73, 0x41, 0x9c, 0xe3, 0x18, 0xdf, 0x7a, 0xf1, 0x72, 0x64, 0x8f, - 0xb8, 0x58, 0xf2, 0x5a, 0xd7, 0x47, 0x2b, 0x5e, 0x09, 0x14, 0xaf, 0x44, - 0xf7, 0x8d, 0x68, 0xac, 0xf1, 0xe2, 0xf9, 0x1d, 0x74, 0x28, 0xa0, 0x8f, - 0xa1, 0x1b, 0x43, 0x47, 0xa8, 0x74, 0x8c, 0x1f, 0x26, 0xfd, 0xb1, 0xfa, - 0xeb, 0x3a, 0xc5, 0x62, 0x7f, 0x34, 0xd8, 0x83, 0xc5, 0xa2, 0xd3, 0x3c, - 0x21, 0x84, 0x0c, 0x8e, 0xf7, 0x70, 0xc3, 0x3e, 0xc4, 0x3d, 0x06, 0x2e, - 0x6c, 0xd0, 0xb4, 0xcd, 0x28, 0x0e, 0x7b, 0xbb, 0xc7, 0xb9, 0xe5, 0x8f, - 0x7a, 0xb4, 0xd9, 0xaf, 0xba, 0x41, 0x3f, 0x48, 0xb8, 0xd6, 0xf7, 0x8b, - 0xde, 0xff, 0x00, 0xb9, 0x8c, 0x13, 0x19, 0x6d, 0x1e, 0x9b, 0x08, 0x75, - 0x48, 0x93, 0xa7, 0xe4, 0xfc, 0xc7, 0xe5, 0xdc, 0x98, 0xf6, 0x3c, 0xc9, - 0xc9, 0xae, 0x24, 0x3d, 0xa5, 0x8d, 0x7b, 0xe0, 0x71, 0x1d, 0xff, 0x00, - 0x81, 0xc2, 0xb4, 0x57, 0x63, 0x73, 0xdf, 0x19, 0x6b, 0x5c, 0x0c, 0x8c, - 0xba, 0x71, 0x00, 0xe5, 0x53, 0xdb, 0xab, 0x27, 0x28, 0x48, 0x36, 0x59, - 0xb1, 0x1f, 0x1d, 0xe7, 0xd4, 0xbd, 0x80, 0x56, 0x56, 0x6d, 0x33, 0xf6, - 0x45, 0xa7, 0x52, 0xd9, 0x6a, 0xbc, 0xe8, 0xe8, 0xc1, 0x0b, 0x64, 0x60, - 0x37, 0x9a, 0xe7, 0x64, 0x6b, 0xf5, 0x7a, 0x27, 0x1f, 0x2d, 0xeb, 0xd2, - 0xbf, 0x8c, 0xcc, 0xe0, 0xeb, 0x38, 0x99, 0xee, 0xb1, 0x8a, 0xbb, 0x17, - 0x01, 0x8e, 0x3f, 0x69, 0x59, 0xec, 0x7f, 0xc4, 0x6d, 0x2d, 0xe7, 0x66, - 0x6c, 0x57, 0xb9, 0xd7, 0x61, 0x53, 0x45, 0xf4, 0xc7, 0x2c, 0xfc, 0x23, - 0xff, 0x00, 0xa2, 0x88, 0xb6, 0xd9, 0x35, 0xa6, 0xcb, 0x3b, 0x79, 0xc6, - 0xb2, 0xd5, 0xb4, 0x1c, 0x2f, 0x11, 0xb4, 0xd3, 0x51, 0x9b, 0x55, 0xa9, - 0xb1, 0xb8, 0x59, 0x6c, 0xb6, 0x86, 0x47, 0x0e, 0xde, 0x21, 0x8e, 0x78, - 0xbd, 0x9d, 0x32, 0x17, 0x4d, 0x31, 0xaf, 0x47, 0xbd, 0x5b, 0xb9, 0x34, - 0x5a, 0xbd, 0x37, 0xd0, 0x25, 0x0f, 0x12, 0x50, 0xb9, 0xa1, 0x9d, 0x1a, - 0x34, 0xfd, 0x5c, 0x48, 0xa8, 0xdf, 0xe5, 0xa9, 0x15, 0x9e, 0x21, 0x57, - 0xc8, 0xeb, 0xa1, 0x43, 0x66, 0x8f, 0xa3, 0x18, 0xa7, 0x1e, 0xff, 0x00, - 0x55, 0xe9, 0x31, 0x37, 0x62, 0x4c, 0x7c, 0xf7, 0xea, 0x7a, 0x40, 0x9d, - 0xb1, 0xda, 0x24, 0xb5, 0x18, 0x8c, 0x54, 0xc6, 0x46, 0x00, 0xd3, 0x9e, - 0xea, 0x12, 0x99, 0x1c, 0xd7, 0x80, 0x60, 0x17, 0x58, 0x45, 0x00, 0x14, - 0x1b, 0xbb, 0xc5, 0x38, 0xa8, 0x99, 0x67, 0xf9, 0xc3, 0xa7, 0x02, 0x3c, - 0x69, 0xb5, 0x7b, 0x0c, 0x55, 0x23, 0x1c, 0x93, 0x3b, 0xce, 0x0d, 0x8e, - 0x2b, 0x65, 0x5c, 0xe3, 0xd8, 0x05, 0xf5, 0x1b, 0x6d, 0x4e, 0xe6, 0x58, - 0xda, 0x46, 0x6e, 0xb7, 0xd9, 0xb7, 0x82, 0x31, 0xc7, 0x2c, 0x76, 0x0b, - 0xa2, 0xe3, 0x25, 0xbe, 0xe0, 0xcb, 0x81, 0x98, 0x1a, 0xd2, 0xb8, 0xb7, - 0x7d, 0x31, 0xaa, 0xe5, 0xe6, 0xc5, 0x3c, 0x71, 0xd9, 0x39, 0xa9, 0x66, - 0x86, 0x20, 0xdf, 0x68, 0x5a, 0xd2, 0x45, 0x3b, 0x29, 0xa8, 0xee, 0x51, - 0x94, 0x6d, 0x3b, 0x66, 0x2e, 0x1b, 0xcf, 0xab, 0x92, 0x07, 0x6f, 0xc8, - 0xf6, 0x14, 0xf8, 0xde, 0x2e, 0xd0, 0xe9, 0xb6, 0xd9, 0xaf, 0x52, 0xd1, - 0x23, 0x01, 0x60, 0x73, 0x49, 0x6e, 0x18, 0x9c, 0xb2, 0x3d, 0xf9, 0x52, - 0xf2, 0x89, 0xb2, 0x58, 0x2d, 0xb6, 0x8b, 0x4c, 0x6c, 0x0c, 0x73, 0xac, - 0x8f, 0xc0, 0x81, 0x80, 0x34, 0xba, 0x77, 0x50, 0x79, 0x2b, 0x3d, 0xb7, - 0xf8, 0x75, 0xa5, 0xdc, 0xd4, 0xed, 0x9a, 0xef, 0x34, 0xec, 0x68, 0x6a, - 0xaf, 0x47, 0x61, 0xb7, 0xd9, 0x5d, 0x78, 0x0b, 0xd6, 0xa3, 0xb2, 0x73, - 0xfb, 0x21, 0x36, 0xd7, 0x66, 0x8a, 0x4b, 0x45, 0xe6, 0x81, 0x30, 0x65, - 0x5e, 0xe6, 0xbf, 0x23, 0x5f, 0x15, 0x2f, 0x79, 0xf7, 0x2f, 0x97, 0xe4, - 0xcb, 0x44, 0x76, 0x9b, 0x9c, 0xd7, 0x3f, 0x25, 0xe0, 0xce, 0x34, 0xed, - 0xa6, 0x1d, 0x9d, 0xca, 0x77, 0xca, 0xea, 0x3a, 0xd0, 0xf1, 0xcc, 0x86, - 0x34, 0xd4, 0xdd, 0x38, 0xd4, 0xe5, 0x4e, 0xec, 0xeb, 0x77, 0x4c, 0x56, - 0x66, 0xe4, 0x4d, 0x5c, 0xee, 0xc0, 0x99, 0x14, 0x62, 0xeb, 0x18, 0x28, - 0x07, 0xac, 0xf4, 0xc8, 0xc7, 0x73, 0xff, 0x00, 0x43, 0xa5, 0xb2, 0xc4, - 0xeb, 0xaf, 0x6a, 0x67, 0x28, 0xd9, 0xe5, 0x8f, 0x99, 0x91, 0xf1, 0x41, - 0x68, 0xb3, 0x32, 0x2a, 0x90, 0x48, 0xc7, 0x64, 0x8b, 0xa6, 0xa5, 0x8e, - 0x22, 0x9d, 0xd9, 0x29, 0x1b, 0xc8, 0x6e, 0x2e, 0x73, 0x2e, 0xba, 0xe3, - 0xe9, 0x1d, 0xec, 0x0d, 0xfa, 0x03, 0x42, 0x4e, 0x0d, 0xc0, 0x76, 0x95, - 0x51, 0xc9, 0xb6, 0xc2, 0x3f, 0xa0, 0xef, 0xd9, 0x19, 0x79, 0x5d, 0xae, - 0xb3, 0xc2, 0x36, 0x43, 0x1d, 0x4b, 0xe4, 0xd5, 0xb5, 0xd8, 0x38, 0xf4, - 0x4b, 0xb1, 0xed, 0x53, 0xc3, 0xc9, 0x56, 0x96, 0x59, 0xf9, 0x22, 0x18, - 0x9b, 0x39, 0x7c, 0xd1, 0x16, 0x83, 0x88, 0x6f, 0xd5, 0x6d, 0xe3, 0x8b, - 0xbf, 0x34, 0x6e, 0x0b, 0x96, 0x56, 0xb9, 0xdc, 0xcc, 0x41, 0xa1, 0xb7, - 0x5b, 0x5e, 0xed, 0xf9, 0x63, 0xdd, 0xa7, 0xd2, 0x25, 0x6d, 0x27, 0xb4, - 0x63, 0x8e, 0xe6, 0xee, 0xf5, 0xaf, 0x8a, 0x41, 0x56, 0x3c, 0x50, 0xa7, - 0x02, 0x2b, 0x19, 0xc5, 0xae, 0xed, 0x1a, 0x79, 0xcb, 0x34, 0xf2, 0x59, - 0xdf, 0x4a, 0x5e, 0x89, 0xe5, 0xa6, 0x8a, 0x67, 0x5a, 0x2c, 0xa2, 0x09, - 0x9f, 0x21, 0x94, 0x49, 0x66, 0xae, 0xd7, 0x6b, 0x4d, 0xe3, 0x80, 0xef, - 0xdd, 0xd8, 0x54, 0xee, 0x1c, 0xb8, 0xfb, 0x1c, 0x98, 0x73, 0x36, 0x67, - 0xb6, 0x43, 0xc6, 0xf1, 0x68, 0xa2, 0x7b, 0xe3, 0x8e, 0x6b, 0x54, 0xae, - 0x6d, 0xdb, 0x96, 0x91, 0x46, 0x83, 0x5e, 0x95, 0x5a, 0x6b, 0x96, 0xee, - 0xfc, 0xcd, 0x13, 0x21, 0x92, 0x67, 0x9b, 0x3b, 0x29, 0x72, 0x1b, 0xc6, - 0xe3, 0x68, 0x28, 0x28, 0x34, 0x89, 0xe5, 0x6f, 0xfb, 0x38, 0x8d, 0x49, - 0xf7, 0xcf, 0x67, 0xaf, 0x30, 0xda, 0x23, 0x0f, 0x61, 0xfc, 0x13, 0x9d, - 0x62, 0x95, 0xb2, 0x33, 0xdc, 0x7e, 0x05, 0x63, 0x61, 0x79, 0xf0, 0x90, - 0x57, 0xd1, 0xf6, 0x8f, 0xfd, 0x65, 0x7d, 0x1f, 0x68, 0xf8, 0x17, 0xcc, - 0x2d, 0x1f, 0x02, 0xfa, 0x3e, 0xd1, 0xf0, 0x2f, 0xa3, 0xe7, 0xf8, 0x15, - 0x3d, 0x0c, 0xb3, 0xbd, 0xee, 0x01, 0x07, 0xf2, 0x84, 0xc1, 0xff, 0x00, - 0xf8, 0xa2, 0xfd, 0xd3, 0x62, 0x89, 0x82, 0x38, 0xdb, 0x80, 0x6b, 0x7f, - 0x9d, 0x7f, 0xff, 0xc4, 0x00, 0x2a, 0x10, 0x01, 0x00, 0x02, 0x01, 0x02, - 0x04, 0x05, 0x05, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x11, 0x21, 0x31, 0x41, 0x51, 0x61, 0x71, 0xa1, 0x10, 0x20, 0x81, - 0x91, 0xb1, 0x30, 0x40, 0xc1, 0xd1, 0xf0, 0xe1, 0xf1, 0x50, 0xff, 0xda, - 0x00, 0x08, 0x01, 0x01, 0x00, 0x01, 0x3f, 0x21, 0xff, 0x00, 0xc1, 0xb8, - 0xc5, 0x70, 0xc8, 0x95, 0x2d, 0xc3, 0xfd, 0xe7, 0xc0, 0xbb, 0xc6, 0x6a, - 0xfe, 0x8b, 0x1f, 0xcf, 0xfe, 0x22, 0xbf, 0x37, 0xe9, 0x9f, 0x22, 0xeb, - 0x12, 0x05, 0x9d, 0xbf, 0xd6, 0x76, 0x0d, 0xdf, 0xa9, 0xa4, 0xc7, 0x78, - 0x97, 0xc5, 0xe7, 0xfa, 0xdc, 0x4b, 0x82, 0x99, 0x77, 0x31, 0xda, 0x1c, - 0x82, 0x3c, 0x1f, 0x01, 0x2f, 0x68, 0x5d, 0x45, 0x5f, 0x79, 0xaa, 0xa5, - 0xe7, 0x2e, 0x97, 0xc5, 0x2f, 0x89, 0x96, 0xad, 0x60, 0x19, 0xb9, 0x7b, - 0x81, 0x6f, 0x16, 0xd1, 0xae, 0x72, 0x92, 0x80, 0xd0, 0x34, 0xa0, 0x00, - 0x9f, 0xca, 0x90, 0x11, 0xc5, 0x36, 0xfc, 0x84, 0xd3, 0xc9, 0x8a, 0x83, - 0xa3, 0x47, 0xd0, 0x7c, 0x99, 0x3b, 0x95, 0xc0, 0xe5, 0xe0, 0x3c, 0x2a, - 0x95, 0xa4, 0xaa, 0xe5, 0x00, 0x65, 0x72, 0xf0, 0xa9, 0x58, 0x87, 0xb2, - 0x6a, 0xc6, 0x90, 0xb4, 0x33, 0x8c, 0x58, 0x2c, 0x24, 0xda, 0x1b, 0xeb, - 0x5e, 0x7f, 0xc7, 0x99, 0x69, 0x45, 0x77, 0x37, 0xfc, 0xb8, 0xcd, 0xd5, - 0x75, 0x61, 0x8f, 0xa3, 0x6c, 0xa1, 0x63, 0x4e, 0x4c, 0x4d, 0x3b, 0xa3, - 0x52, 0x6d, 0xca, 0x06, 0x3f, 0x12, 0xad, 0xd6, 0x6b, 0x3d, 0xe7, 0xcc, - 0x4f, 0x49, 0xbe, 0x3b, 0xc0, 0xbe, 0xb2, 0xb3, 0x32, 0xdb, 0xd6, 0xa2, - 0x24, 0x15, 0x15, 0xb5, 0xa0, 0x9b, 0x4f, 0xf2, 0xb5, 0x61, 0xf2, 0xa8, - 0x4e, 0x44, 0xf4, 0x0f, 0xdf, 0x83, 0x82, 0x20, 0x69, 0x97, 0xa2, 0x59, - 0x05, 0x03, 0x48, 0x9b, 0x95, 0xa2, 0xf1, 0xcd, 0x7c, 0x42, 0x87, 0xc6, - 0x33, 0x01, 0x27, 0x2a, 0x07, 0xda, 0x30, 0x02, 0xcf, 0x09, 0xc8, 0x87, - 0x8c, 0x82, 0x6a, 0xc1, 0x7d, 0x8b, 0x9b, 0xe9, 0x6f, 0x53, 0x30, 0x1e, - 0x9b, 0xc6, 0xfc, 0x85, 0x87, 0x07, 0x2d, 0x23, 0xce, 0x18, 0xcc, 0x6b, - 0xf0, 0x0f, 0x97, 0xb0, 0xf8, 0xc6, 0x10, 0x52, 0xda, 0xf3, 0x03, 0x47, - 0x23, 0x87, 0x39, 0xfc, 0x88, 0x01, 0xdf, 0x55, 0x54, 0x54, 0xa0, 0x0e, - 0xff, 0x00, 0x54, 0xd8, 0xc5, 0x93, 0x15, 0x60, 0x83, 0xd4, 0xa2, 0x2f, - 0x07, 0x36, 0x4a, 0x3e, 0xab, 0x07, 0x36, 0xd2, 0xfe, 0x23, 0x06, 0x0f, - 0x7c, 0xa5, 0x0a, 0xba, 0x0b, 0x70, 0xb6, 0xfc, 0xc6, 0xb2, 0xc2, 0xd5, - 0xf4, 0x63, 0xbd, 0x80, 0x7a, 0xae, 0x16, 0x15, 0xbe, 0x85, 0xd1, 0x23, - 0x66, 0x8d, 0xb9, 0xaa, 0x66, 0x25, 0x55, 0x57, 0x6c, 0x91, 0xac, 0xa6, - 0x32, 0xe7, 0xaf, 0xf0, 0x79, 0x5e, 0x2e, 0x5f, 0x03, 0xc0, 0xfd, 0xc8, - 0xf3, 0x22, 0x2c, 0x77, 0xb7, 0xe6, 0x1f, 0x56, 0xf9, 0x4c, 0x09, 0x0a, - 0x1c, 0xf4, 0x65, 0x83, 0xb9, 0xc9, 0xda, 0xe8, 0x95, 0xa4, 0x3b, 0x4f, - 0x65, 0x7e, 0x65, 0x2a, 0x4e, 0x2d, 0xae, 0xff, 0x00, 0x72, 0x98, 0xa0, - 0x6c, 0x42, 0xe4, 0x9b, 0xab, 0x08, 0x88, 0x50, 0x2c, 0xed, 0x89, 0x62, - 0xe3, 0xe7, 0x15, 0x04, 0x94, 0x66, 0x82, 0x16, 0x2e, 0x54, 0x1e, 0x6c, - 0xb6, 0x80, 0x92, 0xb7, 0xe2, 0x4b, 0xf6, 0xf0, 0x7f, 0x7e, 0x9e, 0x5d, - 0x17, 0xf3, 0x04, 0x31, 0xd2, 0x6e, 0x72, 0x84, 0xb0, 0x0b, 0x43, 0x58, - 0xe2, 0x5e, 0x79, 0x91, 0x7b, 0xb7, 0x5c, 0x09, 0xce, 0x13, 0x5d, 0x72, - 0xb2, 0xde, 0x13, 0x26, 0x7a, 0xe5, 0x53, 0x15, 0xda, 0x5e, 0xec, 0x32, - 0xb9, 0xdc, 0xce, 0xb9, 0x97, 0x1c, 0xe8, 0x8f, 0x56, 0x75, 0xcf, 0x33, - 0xc0, 0x13, 0xd6, 0x0f, 0x63, 0xcb, 0xfc, 0x5c, 0x8f, 0x00, 0xc3, 0x3e, - 0x25, 0xf0, 0xd6, 0x37, 0xab, 0xbf, 0x80, 0x2b, 0x49, 0x7c, 0x72, 0x97, - 0xc5, 0x99, 0x81, 0xca, 0x52, 0x9a, 0x46, 0xef, 0xf7, 0x03, 0x15, 0xae, - 0x20, 0x5f, 0x5e, 0x53, 0x0c, 0x63, 0xa4, 0xd6, 0x56, 0x22, 0x7a, 0x78, - 0x7f, 0x95, 0xc0, 0xf2, 0xff, 0x00, 0x5f, 0x22, 0x5d, 0x53, 0x55, 0x74, - 0x13, 0xad, 0x28, 0xdc, 0xc7, 0x80, 0xbc, 0x12, 0xa8, 0x73, 0x70, 0xe2, - 0x3d, 0xbc, 0x12, 0xc9, 0x1e, 0x2d, 0xc5, 0x77, 0x81, 0x8b, 0xd9, 0x41, - 0x05, 0x69, 0x35, 0x72, 0x9f, 0xdb, 0xe5, 0xe5, 0xfe, 0xfe, 0x92, 0xc8, - 0x56, 0x65, 0xc1, 0x78, 0xfd, 0x81, 0xdb, 0x6c, 0x9c, 0x98, 0x6c, 0x90, - 0x7f, 0x7f, 0x0f, 0x2f, 0x5d, 0xff, 0x00, 0x3e, 0x1d, 0xdf, 0x7d, 0x82, - 0x8b, 0xab, 0xcb, 0x2d, 0x94, 0xaf, 0xf3, 0xf3, 0x07, 0x79, 0x3b, 0xbf, - 0xb0, 0xef, 0x66, 0x34, 0x71, 0x7c, 0xe4, 0x6e, 0xf6, 0x77, 0x3f, 0x61, - 0xde, 0x4e, 0xff, 0x00, 0xcd, 0x03, 0xe1, 0x0f, 0x73, 0xf6, 0x1d, 0xfc, - 0xef, 0x7c, 0xcc, 0xef, 0x23, 0x69, 0xad, 0xcc, 0xba, 0x9f, 0x63, 0x5d, - 0xdb, 0xce, 0x84, 0xef, 0xa2, 0xf7, 0x1f, 0x61, 0xde, 0xce, 0xf1, 0xf3, - 0x13, 0xd7, 0x47, 0x7f, 0x3b, 0xa7, 0xd8, 0x87, 0x72, 0xf3, 0x40, 0xe5, - 0x8e, 0xea, 0x77, 0xef, 0xb0, 0xef, 0x61, 0xf7, 0x66, 0x3e, 0x5c, 0xef, - 0x61, 0xfb, 0x90, 0xd7, 0xd8, 0x0e, 0xe6, 0x77, 0x3e, 0x66, 0x39, 0x0f, - 0xbf, 0x05, 0x75, 0xfe, 0xc3, 0xb9, 0x9d, 0xdf, 0x9f, 0x91, 0xdd, 0xcc, - 0xba, 0xff, 0x00, 0x61, 0xde, 0xc1, 0x5d, 0x5f, 0x31, 0x28, 0xcd, 0x8e, - 0xe6, 0x67, 0x77, 0xf6, 0x1d, 0xec, 0x5a, 0xbc, 0x5f, 0x3b, 0x28, 0xfb, - 0xb3, 0xbb, 0xfb, 0x0e, 0xea, 0x77, 0x6c, 0x1e, 0x64, 0x25, 0xaf, 0xa5, - 0x92, 0xd6, 0xe3, 0xf6, 0x17, 0x3f, 0x0c, 0xcc, 0x47, 0x8f, 0x98, 0x0e, - 0x4b, 0xa7, 0x08, 0x7a, 0xca, 0xa7, 0x4e, 0x27, 0x11, 0x6a, 0x72, 0x19, - 0xca, 0x85, 0xae, 0x09, 0xce, 0xe7, 0x21, 0x9c, 0x89, 0xcf, 0x4e, 0x62, - 0x72, 0x98, 0x26, 0xcc, 0xe6, 0x27, 0x21, 0x88, 0x75, 0x55, 0x1d, 0xe6, - 0xf5, 0xf3, 0x51, 0x78, 0x26, 0x61, 0x95, 0xb4, 0xe1, 0x90, 0xf5, 0x2e, - 0x7d, 0xb0, 0xa6, 0x05, 0x38, 0xf1, 0x39, 0x2b, 0xc3, 0x86, 0x94, 0xdb, - 0x50, 0xb0, 0x08, 0x82, 0x61, 0xe2, 0xcc, 0xd3, 0xc6, 0x0e, 0x1e, 0x73, - 0x1f, 0x30, 0xa8, 0xa1, 0xbc, 0xc9, 0xac, 0x1e, 0x12, 0xe5, 0x15, 0x8d, - 0xae, 0x3a, 0x73, 0x2e, 0xa6, 0x89, 0x77, 0x5c, 0x60, 0xd4, 0x1a, 0x78, - 0xca, 0x13, 0x36, 0x2e, 0x4e, 0x12, 0xc6, 0x11, 0x8f, 0x03, 0xe0, 0x5e, - 0x8d, 0xeb, 0xb7, 0x38, 0xf9, 0x6e, 0xb5, 0x7f, 0x37, 0x5f, 0x99, 0x75, - 0xeb, 0x44, 0x40, 0xa5, 0xba, 0x13, 0x50, 0xdd, 0x12, 0xf5, 0x2b, 0x5a, - 0xef, 0xc3, 0x28, 0x91, 0x6e, 0x98, 0xef, 0x0f, 0x32, 0xc7, 0x9d, 0x47, - 0x84, 0x22, 0xba, 0xd4, 0x23, 0x20, 0xd0, 0x4b, 0xdc, 0xad, 0x0f, 0xe6, - 0x4b, 0xc2, 0xdc, 0x7e, 0xd2, 0x96, 0xe9, 0x5b, 0xcc, 0xe3, 0x34, 0x48, - 0xad, 0x94, 0x04, 0xe7, 0x4b, 0xf2, 0xe1, 0x0b, 0x6b, 0x75, 0xac, 0x4a, - 0x2d, 0x64, 0xc5, 0x5e, 0x3d, 0x99, 0x97, 0x72, 0xed, 0xa4, 0x5a, 0x96, - 0xa8, 0xa7, 0x5d, 0x65, 0xc2, 0xe2, 0xf3, 0xaf, 0x68, 0x3e, 0xf2, 0x9b, - 0x2f, 0xf4, 0x43, 0x4f, 0x96, 0x2c, 0xba, 0x96, 0xbe, 0x09, 0x0f, 0xf9, - 0xb7, 0x25, 0x27, 0xfb, 0xa6, 0x01, 0x0c, 0x06, 0x0f, 0x2e, 0xb1, 0xab, - 0x68, 0x07, 0x4d, 0x7b, 0x19, 0x55, 0xb2, 0x25, 0xe5, 0xe1, 0xad, 0x32, - 0x8d, 0xa5, 0x38, 0xe6, 0x6e, 0xc9, 0x52, 0xcc, 0xd4, 0x1e, 0x45, 0xc7, - 0x4a, 0x9c, 0xf8, 0x45, 0x45, 0xca, 0xc2, 0xfa, 0x40, 0xbd, 0x01, 0x30, - 0x6a, 0x4c, 0x7f, 0x32, 0xaf, 0x79, 0x41, 0x33, 0x09, 0xaa, 0x5a, 0xcc, - 0xb2, 0xfd, 0x49, 0x86, 0xed, 0x27, 0x5d, 0x0e, 0xef, 0x9e, 0xe7, 0x75, - 0x83, 0xd2, 0xed, 0xf1, 0x2c, 0x04, 0xa1, 0x4a, 0xde, 0x61, 0xad, 0xc5, - 0xb4, 0x41, 0xbf, 0xda, 0x7f, 0x75, 0x3a, 0xbe, 0xd1, 0x06, 0xec, 0xf2, - 0x96, 0x71, 0x3c, 0x2a, 0x51, 0x02, 0x82, 0x80, 0xda, 0x57, 0xbc, 0x63, - 0x5a, 0x0d, 0xca, 0xd9, 0x53, 0x2e, 0xc5, 0x95, 0xff, 0x00, 0x98, 0x0d, - 0x97, 0xb4, 0xa5, 0x7e, 0x93, 0x2f, 0xf3, 0x38, 0xed, 0xf4, 0x80, 0xb1, - 0x69, 0xd5, 0x75, 0x66, 0x26, 0x28, 0x5f, 0xa5, 0xf3, 0x7e, 0xde, 0x7c, - 0xce, 0x07, 0xa6, 0xfd, 0xe4, 0xf5, 0x8b, 0xa0, 0xdb, 0xd9, 0x1c, 0x2f, - 0x58, 0xe2, 0xa7, 0x19, 0x6c, 0x00, 0xa5, 0x58, 0x1b, 0x09, 0x68, 0x34, - 0x50, 0x39, 0xf0, 0x68, 0xda, 0x17, 0x00, 0xa0, 0x6e, 0x9a, 0xd4, 0x44, - 0x0b, 0x80, 0x49, 0xa5, 0xeb, 0xb9, 0x5a, 0xcc, 0x0d, 0x0d, 0xbd, 0x96, - 0x28, 0x9d, 0x2a, 0xc6, 0xf3, 0x6e, 0x0c, 0x09, 0x2e, 0x3f, 0x36, 0x25, - 0xa7, 0xfb, 0x4b, 0x5d, 0xe1, 0xb6, 0x6e, 0x65, 0xc6, 0x5f, 0xfc, 0x8b, - 0x5f, 0xf6, 0x15, 0x28, 0x90, 0x07, 0xab, 0xd1, 0x03, 0xe8, 0x2d, 0x0b, - 0xa2, 0xf3, 0xd1, 0xe8, 0xfc, 0xc7, 0x37, 0x9b, 0xf3, 0xe7, 0x09, 0xaa, - 0x0b, 0x90, 0xe1, 0x96, 0xc8, 0xd9, 0xa6, 0x86, 0x64, 0x19, 0x39, 0x18, - 0xa8, 0xb9, 0xdd, 0x7a, 0x85, 0x61, 0xa6, 0x93, 0x79, 0xe2, 0x9c, 0xfb, - 0x40, 0x98, 0xd3, 0x54, 0x74, 0x69, 0x6f, 0x26, 0x80, 0x15, 0x07, 0x00, - 0x98, 0xd5, 0x55, 0x2e, 0x96, 0x0b, 0xcb, 0x55, 0x91, 0xc5, 0x06, 0xf5, - 0x4b, 0x26, 0x8e, 0x02, 0x08, 0xe0, 0x22, 0x15, 0x56, 0x07, 0x1c, 0xce, - 0x0a, 0x3d, 0x4c, 0xac, 0xc7, 0x2c, 0xbc, 0xcc, 0x26, 0xf1, 0xad, 0xe6, - 0xaf, 0xe0, 0x31, 0xeb, 0xf4, 0x40, 0xc3, 0xd4, 0x1c, 0x7d, 0x44, 0xbc, - 0x63, 0x85, 0xc3, 0x74, 0xc6, 0xd2, 0x9b, 0x4d, 0xa1, 0xb1, 0xb6, 0x16, - 0x76, 0x82, 0x9a, 0x05, 0xca, 0xc5, 0xa6, 0xa1, 0xf6, 0xab, 0x1c, 0xa9, - 0xd5, 0x4c, 0xb6, 0xcb, 0xb3, 0x12, 0xbc, 0x82, 0xc3, 0x6a, 0xbe, 0x7e, - 0x04, 0x34, 0x93, 0xa5, 0xbe, 0xa2, 0xb5, 0x3e, 0x3b, 0x31, 0x3b, 0xb4, - 0xd3, 0x53, 0xcb, 0x50, 0x6a, 0x96, 0xc9, 0x6e, 0xd7, 0xcb, 0x63, 0x90, - 0x6b, 0x22, 0x9b, 0x11, 0x54, 0x33, 0xa1, 0xd3, 0x7c, 0x4b, 0xac, 0xcb, - 0xc4, 0xac, 0x96, 0x07, 0xe6, 0x09, 0x78, 0xb6, 0xe2, 0xdf, 0xd4, 0xfd, - 0x2e, 0x32, 0x0c, 0x0d, 0x36, 0x3d, 0x75, 0x8f, 0x12, 0xb1, 0xca, 0x39, - 0x66, 0xb5, 0xa2, 0xd9, 0x3d, 0x36, 0x32, 0x38, 0xd9, 0x7a, 0x11, 0xc6, - 0x7a, 0xbe, 0xaa, 0x93, 0x46, 0x05, 0x7d, 0x59, 0x8d, 0x05, 0x51, 0x4f, - 0x36, 0xc3, 0x26, 0x6b, 0x24, 0x51, 0x62, 0xb2, 0x5e, 0xd3, 0x50, 0x5e, - 0x11, 0x12, 0x2c, 0x1a, 0x23, 0x90, 0xe5, 0xb5, 0xe2, 0xb6, 0xb6, 0xb0, - 0x43, 0xd8, 0x48, 0x52, 0xd5, 0x81, 0x0e, 0x0d, 0x5a, 0x06, 0x68, 0x2e, - 0x34, 0x2d, 0x0f, 0x72, 0xb7, 0x99, 0x66, 0x8b, 0x2a, 0x3a, 0x70, 0x8e, - 0x6e, 0x2e, 0x93, 0xb7, 0xe4, 0x69, 0xef, 0xf4, 0xf5, 0x71, 0x37, 0xed, - 0x0c, 0x6a, 0x29, 0x04, 0x76, 0x77, 0xf1, 0x04, 0xeb, 0xd1, 0xcc, 0x6a, - 0xe0, 0xa2, 0x05, 0x2a, 0x19, 0x52, 0x08, 0x32, 0x83, 0xaa, 0x25, 0x4a, - 0x68, 0x28, 0xe0, 0xbb, 0x9d, 0x07, 0x68, 0x86, 0x9a, 0x72, 0x86, 0x31, - 0xd4, 0x68, 0x26, 0x05, 0x66, 0xc7, 0x18, 0xba, 0xb3, 0x81, 0xb2, 0xcc, - 0x6f, 0x60, 0xe9, 0x91, 0x6a, 0xa3, 0x59, 0x85, 0x6d, 0x4b, 0x6b, 0x0f, - 0x7f, 0x87, 0x16, 0x65, 0x45, 0x0b, 0x60, 0x30, 0x6b, 0xd1, 0x6e, 0x8e, - 0xa6, 0xaa, 0x6b, 0xbf, 0x81, 0x78, 0xa7, 0x24, 0xbd, 0x58, 0x48, 0x45, - 0x3e, 0xc1, 0xf5, 0x2a, 0xf3, 0x71, 0x46, 0xcf, 0xf0, 0x44, 0xd0, 0xea, - 0x4d, 0x66, 0xc5, 0x00, 0xd0, 0x9c, 0x11, 0x1c, 0x22, 0x58, 0x8e, 0x11, - 0xa6, 0x6e, 0x88, 0x41, 0xa2, 0xb8, 0xd8, 0xa5, 0x6c, 0x58, 0x28, 0x86, - 0x41, 0x30, 0x6b, 0x00, 0x26, 0xbe, 0x50, 0x37, 0x39, 0xd0, 0x82, 0xb8, - 0xae, 0xe6, 0x51, 0x5c, 0x5b, 0xc8, 0x22, 0x86, 0x83, 0x36, 0x54, 0x59, - 0x0d, 0x0d, 0x3d, 0xaa, 0x85, 0x8d, 0x5c, 0x53, 0x71, 0x58, 0x06, 0xc2, - 0xcc, 0x30, 0xa3, 0x92, 0xd3, 0x2d, 0x11, 0x62, 0x38, 0x26, 0x85, 0x11, - 0x4d, 0x5b, 0x4f, 0x5d, 0x7d, 0xbe, 0xad, 0xa3, 0x24, 0xb9, 0x45, 0xce, - 0x9e, 0x07, 0x8d, 0xe0, 0xad, 0xc6, 0x49, 0xb6, 0xb1, 0x6a, 0xab, 0x1d, - 0x30, 0x7b, 0x4d, 0x52, 0xb0, 0x82, 0x55, 0x12, 0x88, 0xa5, 0x0d, 0x9b, - 0x98, 0xec, 0xd4, 0x02, 0x5e, 0x8a, 0x4e, 0xa8, 0x68, 0x62, 0x85, 0x93, - 0x5b, 0x2f, 0x32, 0x68, 0x6d, 0xa5, 0x71, 0x04, 0x86, 0x46, 0x33, 0xd4, - 0x1a, 0xd3, 0x7d, 0x5b, 0x6d, 0x55, 0x84, 0x61, 0x3d, 0x96, 0x16, 0x9b, - 0x07, 0xf3, 0x00, 0x0a, 0x30, 0x7d, 0x6e, 0x2e, 0x04, 0xd5, 0x71, 0x1d, - 0xa2, 0x07, 0x99, 0x16, 0xae, 0x57, 0xa4, 0x6e, 0xbf, 0x8e, 0x70, 0xcd, - 0x7f, 0xd1, 0x49, 0xfe, 0xc3, 0x1b, 0xab, 0xbd, 0x85, 0xfb, 0x9d, 0x72, - 0xb8, 0x7a, 0xe8, 0x20, 0xc0, 0x27, 0x61, 0x9d, 0xe0, 0x17, 0x03, 0x79, - 0xa9, 0xeb, 0xfa, 0x43, 0x35, 0x34, 0x94, 0x07, 0xfe, 0xd7, 0xff, 0xda, - 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00, 0x10, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x13, 0x3e, 0xdb, 0xc0, 0x00, 0x00, 0x00, 0x01, 0x0e, 0x8d, 0xff, 0x00, - 0xdb, 0x6f, 0xf9, 0x98, 0x00, 0x00, 0x17, 0xef, 0xfe, 0xdb, 0xfd, 0xb7, - 0xdf, 0x20, 0x00, 0x0b, 0x7e, 0xbe, 0xfb, 0xfd, 0xff, 0x00, 0xdb, 0x7d, - 0x00, 0x07, 0x3e, 0xf7, 0xc4, 0x9e, 0x4f, 0xb8, 0x6d, 0x90, 0x00, 0xed, - 0xda, 0x6c, 0x68, 0xec, 0x08, 0x43, 0xd2, 0x00, 0x0c, 0xb6, 0xff, 0x00, - 0xfd, 0x2b, 0x70, 0x9b, 0xfe, 0x40, 0x03, 0xde, 0x7f, 0x6d, 0xf7, 0xff, - 0x00, 0xef, 0xf7, 0x68, 0x00, 0x7b, 0xdf, 0xef, 0xb6, 0xfb, 0x7d, 0xff, - 0x00, 0xd9, 0x00, 0x0f, 0x58, 0x10, 0x01, 0xb0, 0x40, 0x01, 0x3b, 0x20, - 0x01, 0xcf, 0x49, 0x24, 0x92, 0x49, 0x24, 0x93, 0xb4, 0x00, 0x3b, 0x69, - 0x24, 0x92, 0x49, 0x24, 0x92, 0xa4, 0x80, 0x03, 0x7d, 0x24, 0x92, 0x49, - 0x24, 0x92, 0x54, 0xd0, 0x00, 0x6e, 0xa4, 0x92, 0x49, 0x24, 0x92, 0x48, - 0x96, 0x00, 0x1f, 0xd4, 0x92, 0x49, 0x24, 0x92, 0x49, 0x3b, 0xc0, 0x03, - 0xba, 0x92, 0x49, 0x24, 0x92, 0x49, 0x2b, 0x58, 0x00, 0x76, 0x52, 0x49, - 0x24, 0x92, 0x49, 0x25, 0x6f, 0x00, 0x0e, 0xf2, 0x49, 0x24, 0x92, 0x49, - 0x24, 0x89, 0xc0, 0x01, 0xfd, 0x49, 0x24, 0x92, 0x49, 0x24, 0x91, 0xa8, - 0x00, 0x3f, 0x29, 0x24, 0x92, 0x49, 0x24, 0x92, 0x35, 0x00, 0x05, 0xf5, - 0x24, 0x92, 0x49, 0x24, 0x92, 0x45, 0x90, 0x00, 0x7e, 0xa4, 0x92, 0x49, - 0x24, 0x92, 0x48, 0xd2, 0x00, 0x1f, 0xd4, 0x92, 0x49, 0x24, 0x92, 0x49, - 0x12, 0x80, 0x01, 0xba, 0x92, 0x49, 0x24, 0x92, 0x49, 0x22, 0x70, 0x00, - 0x3e, 0xd2, 0x49, 0x24, 0x92, 0x49, 0x24, 0xeb, 0x00, 0x06, 0x66, 0x6f, - 0x27, 0x16, 0xd8, 0x44, 0x93, 0x60, 0x01, 0xcf, 0x4f, 0xe5, 0xda, 0x4b, - 0x2c, 0xbb, 0x6c, 0x00, 0x3f, 0xe4, 0x9f, 0xfb, 0xfb, 0x20, 0x37, 0xef, - 0x80, 0x05, 0xef, 0x40, 0x20, 0x3f, 0x65, 0xbb, 0x9d, 0x60, 0x00, 0xd0, - 0x36, 0xdf, 0x7e, 0xcd, 0x93, 0x7e, 0xe0, 0x00, 0x13, 0x86, 0xdb, 0xf9, - 0x7b, 0x13, 0x7d, 0xb8, 0x00, 0x00, 0x4f, 0x53, 0xcc, 0x6f, 0xf6, 0xcb, - 0x63, 0x80, 0x00, 0x03, 0xfb, 0xec, 0xfb, 0xfa, 0xef, 0xb6, 0x80, 0x00, - 0x00, 0x27, 0x7d, 0x9e, 0x84, 0xb3, 0xf6, 0xe0, 0x00, 0x00, 0x01, 0xad, - 0xf8, 0xd8, 0x7f, 0xbe, 0xf4, 0x00, 0x00, 0x00, 0x13, 0xf6, 0x61, 0x44, - 0x0f, 0xd6, 0x00, 0x00, 0x00, 0x03, 0x56, 0xe1, 0xcb, 0x8b, 0xde, 0x80, - 0x00, 0x00, 0x00, 0x0c, 0xfb, 0x2e, 0x20, 0x1e, 0x40, 0x00, 0x00, 0x00, - 0x00, 0x1e, 0xe7, 0xde, 0x1b, 0x90, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, - 0x3c, 0x94, 0xd0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x7f, 0xff, 0xc4, 0x00, 0x1f, 0x11, 0x00, 0x02, 0x02, 0x03, 0x01, 0x01, - 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x11, - 0x00, 0x10, 0x20, 0x31, 0x40, 0x30, 0x41, 0x50, 0x21, 0x60, 0xff, 0xda, - 0x00, 0x08, 0x01, 0x03, 0x01, 0x01, 0x3f, 0x10, 0xfc, 0x37, 0x1c, 0x71, - 0xc7, 0xee, 0xe3, 0x8f, 0xd1, 0xc7, 0x1f, 0x81, 0x3c, 0x43, 0x23, 0xd2, - 0x73, 0x51, 0x45, 0x4a, 0x2a, 0x54, 0xb0, 0x18, 0x9c, 0xc5, 0xfd, 0xad, - 0xc3, 0x09, 0xb3, 0xab, 0x18, 0x9f, 0x1f, 0xb3, 0xec, 0x71, 0xc3, 0x1c, - 0x73, 0xe6, 0x03, 0x13, 0xc8, 0x31, 0x3c, 0x83, 0x13, 0x6a, 0x28, 0xa2, - 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xac, 0x62, 0x68, 0x70, - 0x1a, 0x18, 0x9a, 0x1c, 0x07, 0x54, 0x31, 0x34, 0x38, 0x0e, 0xa8, 0x62, - 0x68, 0x70, 0x1d, 0x50, 0xc4, 0xd0, 0xe0, 0x3a, 0xa1, 0x89, 0xa1, 0xc0, - 0x75, 0x43, 0x13, 0x43, 0x80, 0xea, 0x86, 0x27, 0x88, 0xeb, 0x33, 0xc4, - 0x75, 0x99, 0xa1, 0xc0, 0x75, 0x99, 0xa1, 0xc0, 0x75, 0x43, 0x13, 0x43, - 0x80, 0xea, 0x86, 0x26, 0x87, 0x01, 0xd6, 0x66, 0x87, 0x01, 0xd6, 0x66, - 0x87, 0x01, 0xa1, 0x89, 0xa1, 0x4e, 0x3b, 0x7e, 0x83, 0x13, 0xc8, 0x31, - 0x3c, 0x83, 0x13, 0xee, 0xe3, 0xf2, 0xf9, 0x07, 0xe0, 0x1e, 0x13, 0x6e, - 0x3a, 0x71, 0xe0, 0xe3, 0x8e, 0x38, 0xe3, 0xf1, 0x3e, 0x0a, 0x28, 0xb8, - 0x35, 0x96, 0xa3, 0x8e, 0x39, 0xbf, 0x73, 0x88, 0xa6, 0x5d, 0x12, 0xa0, - 0x87, 0x8c, 0x50, 0xfa, 0x80, 0x7d, 0xc2, 0x18, 0x4f, 0xeb, 0x84, 0x2d, - 0xf1, 0x88, 0xa8, 0x88, 0x00, 0x11, 0x80, 0x8e, 0x0c, 0x26, 0x5f, 0x29, - 0xa1, 0xce, 0xe3, 0x8f, 0x9d, 0x7f, 0x80, 0xff, 0xc4, 0x00, 0x1f, 0x11, - 0x00, 0x02, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x11, 0x10, 0x20, 0x21, 0x31, 0x41, - 0x40, 0x30, 0x51, 0x50, 0xff, 0xda, 0x00, 0x08, 0x01, 0x02, 0x01, 0x01, - 0x3f, 0x10, 0xfe, 0x14, 0x32, 0x19, 0x0c, 0x86, 0x43, 0x21, 0x90, 0xfe, - 0xd0, 0x42, 0x21, 0x7d, 0x21, 0x10, 0x86, 0xbe, 0x09, 0x47, 0x89, 0xab, - 0x92, 0x97, 0x66, 0x7c, 0x0f, 0x17, 0x15, 0x19, 0x27, 0x69, 0x24, 0x92, - 0x4f, 0x49, 0x24, 0x59, 0x26, 0x92, 0x2c, 0xd5, 0xdb, 0xd5, 0x60, 0x82, - 0x0e, 0x0c, 0x8a, 0x45, 0x0e, 0x10, 0x90, 0x94, 0x8f, 0x0f, 0x07, 0x45, - 0xba, 0x31, 0xdb, 0xd0, 0xa8, 0xbb, 0x46, 0x72, 0xb1, 0x83, 0x90, 0x43, - 0x20, 0x5b, 0xc8, 0xd7, 0xe0, 0x94, 0x1d, 0x91, 0x61, 0xd5, 0xdb, 0xd0, - 0xa9, 0x04, 0xfd, 0xd8, 0xed, 0xe8, 0x5e, 0x37, 0x6f, 0x42, 0x24, 0x92, - 0x51, 0x28, 0x92, 0x51, 0x28, 0x94, 0x4a, 0x25, 0x12, 0x89, 0x44, 0xa2, - 0x51, 0x24, 0xd5, 0xdb, 0xd0, 0x87, 0xe0, 0xfc, 0x0c, 0x76, 0xf4, 0x21, - 0xf8, 0x16, 0xc6, 0x3b, 0x7a, 0x10, 0xfc, 0x0b, 0x63, 0x1d, 0xbd, 0x08, - 0x7b, 0xf0, 0x2d, 0x8c, 0x76, 0xf4, 0x21, 0xef, 0xc0, 0xb6, 0x31, 0xdb, - 0xd0, 0x87, 0xbf, 0x06, 0xc3, 0x1d, 0xbd, 0x08, 0x7b, 0xf0, 0x6c, 0x31, - 0xdb, 0xd0, 0xbc, 0x3b, 0x0c, 0x76, 0xf4, 0x22, 0x3c, 0x1b, 0x0c, 0x76, - 0xf4, 0x21, 0xef, 0xc1, 0xb0, 0xc7, 0x6f, 0x42, 0x1e, 0xfc, 0x1b, 0x0c, - 0x76, 0xf4, 0x21, 0xef, 0xc1, 0xb0, 0xc7, 0x6f, 0x42, 0x1e, 0xfc, 0x1b, - 0x0c, 0x76, 0xf4, 0x21, 0xef, 0xc1, 0xb0, 0xc7, 0x6f, 0x42, 0x12, 0x1f, - 0x83, 0x61, 0x8e, 0xde, 0x84, 0x3c, 0xdf, 0x9b, 0x72, 0x66, 0xab, 0x03, - 0x1d, 0xbd, 0x0a, 0x98, 0x30, 0x60, 0xc1, 0x83, 0x14, 0xc1, 0x8a, 0x62, - 0xb8, 0x20, 0x79, 0xa3, 0xb7, 0xab, 0x24, 0x92, 0x49, 0xb6, 0x49, 0x30, - 0x60, 0x9a, 0x6a, 0x8e, 0xdd, 0xa8, 0xc8, 0x64, 0x10, 0x41, 0x04, 0x3a, - 0xb2, 0x08, 0x20, 0x82, 0x2c, 0x76, 0xad, 0x8f, 0x63, 0xbd, 0x06, 0x2b, - 0xd7, 0xc1, 0xe5, 0x27, 0xf0, 0x5a, 0xa2, 0xdb, 0x13, 0xbb, 0xac, 0x77, - 0xe4, 0xa0, 0x43, 0x12, 0x24, 0x48, 0x90, 0x95, 0x66, 0x06, 0xa4, 0x91, - 0x22, 0x44, 0x89, 0x1c, 0x81, 0xde, 0xd0, 0xc7, 0xbb, 0x59, 0x24, 0x89, - 0x64, 0x89, 0xfb, 0xed, 0x5a, 0xc6, 0xe7, 0x84, 0x10, 0x41, 0xab, 0x9f, - 0xc5, 0x38, 0x63, 0x51, 0x71, 0x65, 0xc1, 0x03, 0xc0, 0xdc, 0x8b, 0xee, - 0xb2, 0xac, 0xda, 0x8a, 0x51, 0x2c, 0x91, 0x43, 0xd8, 0xac, 0x7f, 0x34, - 0xe1, 0x8e, 0xc4, 0xd1, 0x31, 0xac, 0x92, 0xe2, 0x04, 0xf0, 0x27, 0x2a, - 0x2d, 0x92, 0x46, 0xe8, 0xaa, 0xfe, 0xdb, 0xcd, 0x90, 0x41, 0x1f, 0xb6, - 0x3f, 0xbe, 0x89, 0x26, 0xe9, 0x27, 0xfb, 0x7f, 0xff, 0xc4, 0x00, 0x2a, - 0x10, 0x01, 0x00, 0x02, 0x02, 0x01, 0x02, 0x06, 0x02, 0x03, 0x00, 0x03, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x11, 0x21, 0x31, 0x41, 0x51, - 0x61, 0x20, 0x71, 0x81, 0x91, 0xa1, 0xb1, 0xc1, 0xf0, 0x30, 0x40, 0xd1, - 0x10, 0x50, 0xf1, 0xe1, 0xff, 0xda, 0x00, 0x08, 0x01, 0x01, 0x00, 0x01, - 0x3f, 0x10, 0xff, 0x00, 0xa1, 0x29, 0x03, 0xb3, 0x43, 0xee, 0xc7, 0xe6, - 0x7b, 0x56, 0x1e, 0x91, 0x71, 0x66, 0xbf, 0x7d, 0x5c, 0xb7, 0x11, 0xda, - 0xbd, 0xc8, 0x18, 0xf8, 0xa3, 0x28, 0x03, 0x7c, 0x84, 0x9a, 0x2d, 0x8d, - 0x7f, 0xbc, 0x26, 0x9a, 0x01, 0x49, 0x7d, 0xe0, 0x51, 0x29, 0xd3, 0x63, - 0xec, 0xff, 0x00, 0x22, 0x81, 0x56, 0x83, 0x6b, 0x1e, 0xe0, 0xa0, 0x8e, - 0x89, 0x31, 0xec, 0x82, 0x26, 0x38, 0x68, 0x1e, 0xa8, 0x51, 0xb2, 0x50, - 0x0a, 0x1e, 0xa4, 0x92, 0xbb, 0x9a, 0x80, 0x7d, 0x0d, 0x23, 0x8a, 0x83, - 0x36, 0x97, 0xee, 0x16, 0x9b, 0x71, 0xb7, 0xac, 0x6d, 0xe9, 0xe6, 0xa6, - 0x44, 0xd1, 0x01, 0xd9, 0xde, 0xe5, 0x05, 0x88, 0x7d, 0xc2, 0x92, 0xa5, - 0x74, 0x98, 0x32, 0xcf, 0x79, 0x95, 0x1d, 0xa4, 0x57, 0xcc, 0x37, 0x51, - 0x82, 0x4f, 0x22, 0xe7, 0x2f, 0xaa, 0x32, 0xf9, 0xa8, 0xbe, 0xf7, 0x01, - 0x08, 0xda, 0x3b, 0xbd, 0xdf, 0xc1, 0x0e, 0xec, 0xac, 0xd0, 0xc6, 0x09, - 0xb6, 0x5a, 0x06, 0x97, 0x83, 0xf8, 0x10, 0x80, 0x34, 0xed, 0x25, 0x8f, - 0x4e, 0x37, 0xe5, 0x15, 0x0d, 0xda, 0xfc, 0x4b, 0xea, 0x81, 0xed, 0x5a, - 0x9b, 0xd1, 0x4b, 0xc9, 0x5b, 0x8b, 0x12, 0xee, 0xb9, 0xa8, 0x0b, 0xa5, - 0xeb, 0x84, 0x97, 0x74, 0x57, 0xaa, 0x20, 0x1a, 0xe8, 0x6e, 0xfd, 0x20, - 0x1a, 0x44, 0xf9, 0xc0, 0x13, 0xbf, 0x21, 0x1a, 0x2b, 0x81, 0xf8, 0xa8, - 0x5e, 0xc3, 0x6b, 0x8b, 0x26, 0x4a, 0x4b, 0xf5, 0x5c, 0x45, 0x84, 0xb4, - 0xe9, 0x31, 0x68, 0xbb, 0x6b, 0x0f, 0xdc, 0xf2, 0x07, 0x17, 0xc6, 0x62, - 0xd1, 0x55, 0xac, 0x75, 0x60, 0xc6, 0x40, 0x44, 0xb3, 0xca, 0x36, 0x9d, - 0x95, 0x9b, 0x85, 0xe7, 0xb9, 0xf2, 0xe9, 0x5e, 0x1c, 0x37, 0x37, 0x34, - 0x84, 0xda, 0x74, 0x40, 0xaf, 0xa8, 0x44, 0x16, 0xd4, 0xb9, 0x20, 0x56, - 0x5c, 0xd6, 0x02, 0x00, 0x6c, 0xed, 0x17, 0xab, 0x99, 0x48, 0x4e, 0xbf, - 0xc5, 0x1b, 0x67, 0xc8, 0x10, 0x39, 0x96, 0x1a, 0xdf, 0x51, 0xcc, 0xa1, - 0x28, 0x55, 0xf0, 0xfc, 0xcb, 0x02, 0x24, 0x3d, 0x2a, 0x53, 0x95, 0x5f, - 0x7e, 0xf1, 0x04, 0x3a, 0x20, 0x55, 0x37, 0x63, 0x86, 0x28, 0x97, 0xb1, - 0xfb, 0xa8, 0x2d, 0x8b, 0x1e, 0x4a, 0x94, 0x05, 0xdf, 0xc9, 0x80, 0xb8, - 0x50, 0x99, 0x6d, 0xe2, 0x5e, 0xe2, 0x2f, 0x7a, 0x19, 0x68, 0x73, 0x64, - 0x54, 0x85, 0x63, 0xb6, 0x48, 0x43, 0x49, 0xbb, 0x43, 0x76, 0x3c, 0x32, - 0xd8, 0xaa, 0x98, 0x7e, 0x85, 0x8b, 0xeb, 0xe1, 0xa6, 0x60, 0x45, 0xe2, - 0xe9, 0x7d, 0xa0, 0xbd, 0xd3, 0x6e, 0xe5, 0x84, 0xa0, 0x35, 0x61, 0x4e, - 0x3c, 0xa2, 0xf0, 0x1d, 0x68, 0xda, 0x54, 0x33, 0xe8, 0x30, 0xe0, 0xb4, - 0x81, 0x80, 0x3a, 0x62, 0x24, 0xd6, 0x5c, 0xc1, 0x64, 0xbe, 0x44, 0x50, - 0x01, 0x56, 0xab, 0x2a, 0x7a, 0x44, 0xc7, 0xd4, 0x80, 0x6c, 0xf5, 0x85, - 0x7a, 0x29, 0xba, 0xad, 0xca, 0x0b, 0x16, 0xea, 0xab, 0x1f, 0x70, 0x56, - 0x46, 0x1b, 0x16, 0x82, 0xd7, 0xdc, 0x1b, 0xda, 0xa9, 0x99, 0x76, 0xef, - 0x01, 0x51, 0xd1, 0xd6, 0x99, 0x54, 0x8b, 0x9e, 0x01, 0x62, 0x76, 0x00, - 0xe0, 0x6b, 0xfc, 0x95, 0xea, 0x51, 0xce, 0xb4, 0x80, 0xca, 0x96, 0xc1, - 0xc3, 0x9e, 0x61, 0x55, 0xaa, 0x6d, 0x1c, 0xbd, 0x04, 0x16, 0x55, 0xb8, - 0x2e, 0xff, 0x00, 0xfb, 0x11, 0xb7, 0x0a, 0xf3, 0x96, 0xe3, 0x89, 0x83, - 0x1d, 0x06, 0x8f, 0xb7, 0xc3, 0x98, 0x52, 0x82, 0xe2, 0x15, 0xe8, 0x0b, - 0xeb, 0x80, 0x84, 0x00, 0x34, 0x53, 0x89, 0x89, 0x89, 0xad, 0x79, 0x04, - 0xc5, 0x76, 0xfd, 0xf9, 0xcb, 0x55, 0x89, 0x65, 0x2f, 0x21, 0xf9, 0xf8, - 0x8a, 0x4c, 0x16, 0x06, 0xe8, 0xcb, 0xaf, 0x2a, 0x89, 0xa7, 0x50, 0x57, - 0x22, 0x2b, 0x8f, 0x39, 0x71, 0x94, 0xa5, 0xb1, 0xb2, 0xef, 0xaa, 0xc7, - 0x28, 0xdc, 0x22, 0x84, 0x02, 0x83, 0xa3, 0x6f, 0xac, 0xc3, 0xa0, 0x08, - 0xe8, 0x07, 0xe8, 0xf6, 0x60, 0x15, 0x1a, 0xd8, 0xe5, 0xec, 0xcc, 0x72, - 0x1c, 0x15, 0x15, 0x1a, 0x78, 0x69, 0xe9, 0x0b, 0xbe, 0x66, 0xca, 0x51, - 0x25, 0xf5, 0x14, 0xf5, 0x98, 0x8d, 0x06, 0xdb, 0x42, 0xb7, 0x37, 0xc4, - 0x28, 0xa5, 0x80, 0xdd, 0x8a, 0x0e, 0xc9, 0x9f, 0x58, 0x6f, 0x64, 0xda, - 0xbb, 0x97, 0xe7, 0x10, 0x48, 0x63, 0xc2, 0xd0, 0x28, 0x17, 0xdd, 0x4a, - 0xef, 0x0d, 0x96, 0x0c, 0xd4, 0xa8, 0xd6, 0xfb, 0x40, 0x11, 0xba, 0xb8, - 0xbf, 0x08, 0x16, 0x2b, 0x87, 0xac, 0x0e, 0x07, 0x49, 0x99, 0xaa, 0x5e, - 0x5c, 0x67, 0x32, 0xcc, 0x76, 0x86, 0x4f, 0x46, 0x25, 0xcb, 0x02, 0xbc, - 0xb7, 0x09, 0x76, 0x41, 0xb0, 0x31, 0xa4, 0x04, 0xb6, 0xab, 0x98, 0xc6, - 0x98, 0xd4, 0x33, 0x98, 0xc2, 0x51, 0xbf, 0x69, 0xa9, 0x0e, 0x5b, 0x68, - 0x03, 0xde, 0xac, 0x61, 0x99, 0x9d, 0x16, 0x9b, 0xa1, 0x7a, 0x20, 0x3d, - 0x60, 0x9f, 0x54, 0x2d, 0x1a, 0x40, 0xbc, 0x53, 0xf2, 0x94, 0xb7, 0x9c, - 0x34, 0x22, 0x24, 0xe0, 0xa7, 0x7f, 0x3b, 0x88, 0xb0, 0x61, 0x42, 0x98, - 0x43, 0xa0, 0xd6, 0x35, 0x0b, 0x64, 0xb5, 0x55, 0xd4, 0x5f, 0x27, 0xcc, - 0xcc, 0x64, 0x93, 0x74, 0xf5, 0x58, 0x06, 0xa0, 0xdd, 0xe4, 0x12, 0xe1, - 0xe4, 0x1c, 0x0b, 0x07, 0x47, 0x19, 0x10, 0x3d, 0xe3, 0x6b, 0x6f, 0xd5, - 0xcf, 0x68, 0x40, 0x42, 0xbf, 0x7a, 0x5c, 0xa5, 0x78, 0x44, 0x3e, 0xcf, - 0x0e, 0x74, 0x37, 0xbc, 0x72, 0x07, 0xa9, 0x71, 0x96, 0x94, 0x23, 0xdd, - 0x98, 0x87, 0x96, 0x54, 0xad, 0xae, 0x93, 0x99, 0x73, 0x88, 0x39, 0xdf, - 0x03, 0x3a, 0xbc, 0x5d, 0x06, 0x8b, 0x54, 0x52, 0x60, 0x4e, 0x03, 0xea, - 0x1e, 0x91, 0x4a, 0x0b, 0x66, 0xb7, 0x10, 0x45, 0xe0, 0x38, 0x01, 0x99, - 0x5e, 0x68, 0x72, 0x56, 0x20, 0x82, 0xd3, 0xba, 0x7d, 0x41, 0x5e, 0xdd, - 0x69, 0xe3, 0xa4, 0x48, 0x91, 0x93, 0x4e, 0xf3, 0x14, 0xb6, 0x8b, 0x73, - 0x85, 0x6e, 0x16, 0x68, 0xe4, 0xe4, 0xad, 0x77, 0xef, 0x16, 0x9b, 0x2a, - 0xaf, 0x37, 0x7e, 0xf2, 0xf4, 0x01, 0xe1, 0xbf, 0x96, 0x2c, 0x63, 0x45, - 0xdb, 0x79, 0x96, 0xd6, 0x90, 0x21, 0xbc, 0x1e, 0xa6, 0xe3, 0x95, 0x28, - 0xce, 0x6e, 0x87, 0xfc, 0xf8, 0x41, 0x6e, 0x6c, 0x06, 0xf1, 0xfa, 0x11, - 0xa1, 0xba, 0x34, 0x06, 0xe7, 0x78, 0x3a, 0x24, 0x54, 0x14, 0xb4, 0x3b, - 0x6a, 0x82, 0xe2, 0xb6, 0x56, 0x78, 0xbc, 0xd7, 0x73, 0xa7, 0xa4, 0x6f, - 0x92, 0xb4, 0x8d, 0x98, 0x2b, 0x72, 0x2b, 0x81, 0xaa, 0x99, 0xda, 0x23, - 0xc4, 0x68, 0xae, 0xc7, 0x24, 0xc9, 0x6b, 0xa3, 0x07, 0xac, 0xa7, 0x67, - 0x78, 0x34, 0x6f, 0xb5, 0x7b, 0xcb, 0x85, 0xa5, 0xee, 0xc1, 0xa8, 0x09, - 0x78, 0xd0, 0x7e, 0xbf, 0x6e, 0x25, 0x17, 0x26, 0x2b, 0xe6, 0x28, 0xc1, - 0x91, 0x8b, 0x67, 0xcf, 0xe2, 0x59, 0x17, 0x4d, 0x68, 0xd3, 0xff, 0x00, - 0x92, 0x80, 0xa6, 0x0d, 0x59, 0xb3, 0x18, 0x82, 0xb5, 0x18, 0x38, 0x31, - 0x79, 0xce, 0x23, 0x25, 0xdd, 0x3a, 0x4a, 0xd3, 0x7a, 0x3b, 0x47, 0x6d, - 0xb1, 0xa1, 0xaf, 0xd7, 0x37, 0xe1, 0x55, 0x43, 0x90, 0x75, 0x02, 0x1a, - 0x5d, 0xf3, 0x98, 0x51, 0x72, 0x89, 0x0d, 0x84, 0x66, 0x38, 0xe9, 0x31, - 0x0a, 0x5d, 0x8a, 0x88, 0x3e, 0x70, 0x42, 0xd5, 0x51, 0x71, 0x60, 0x4b, - 0xd7, 0x71, 0xd0, 0x0c, 0xcb, 0xda, 0x70, 0xf3, 0x44, 0xb6, 0x5b, 0x73, - 0x91, 0xb8, 0xea, 0xd5, 0xce, 0x3f, 0xd8, 0xee, 0x37, 0x4c, 0x90, 0x37, - 0xa7, 0x93, 0x19, 0x8e, 0x6a, 0x5e, 0xba, 0xcc, 0xcb, 0x76, 0x7a, 0x11, - 0xe0, 0x4f, 0x70, 0x49, 0x4d, 0x06, 0x2e, 0xd5, 0xf9, 0x94, 0x1e, 0x71, - 0x31, 0x13, 0xd1, 0x6b, 0xcf, 0x0c, 0x0e, 0x0e, 0x76, 0x88, 0x44, 0xbc, - 0x6e, 0xf2, 0xf0, 0x9b, 0x59, 0x5d, 0x7e, 0xd0, 0xc2, 0x85, 0x34, 0x5b, - 0x02, 0x65, 0xa5, 0x3f, 0xa1, 0xa3, 0x4b, 0x2f, 0xa2, 0xfe, 0xf7, 0x8e, - 0xa5, 0xd8, 0xd4, 0x62, 0xe6, 0xef, 0xd0, 0xc7, 0x84, 0x04, 0x82, 0xd5, - 0x8f, 0x1a, 0x8e, 0xb2, 0xb0, 0xd9, 0xdf, 0x73, 0xf7, 0x7b, 0x7f, 0x41, - 0x0a, 0x39, 0x17, 0xed, 0x30, 0x2b, 0xc2, 0xa7, 0x5c, 0xc2, 0x2b, 0x12, - 0xdf, 0x1e, 0x1a, 0x02, 0xe6, 0xf2, 0xf4, 0x20, 0x32, 0x63, 0x5f, 0x49, - 0xfb, 0xdd, 0xbf, 0xa1, 0xfb, 0x7d, 0xa5, 0x45, 0xab, 0xf7, 0x40, 0x8e, - 0xbb, 0xfc, 0x5e, 0x1a, 0x8f, 0xcd, 0xfe, 0x12, 0xcc, 0xdf, 0xad, 0xdb, - 0xfa, 0x02, 0xdf, 0x8b, 0xfd, 0x4a, 0xad, 0xdf, 0xf7, 0x32, 0xfd, 0xed, - 0x78, 0x7d, 0xcb, 0xf0, 0x82, 0xf3, 0xf2, 0x7d, 0xcc, 0x7a, 0x1f, 0xe3, - 0xfa, 0x01, 0xa8, 0xeb, 0xfa, 0x97, 0x16, 0xb9, 0xfd, 0xcc, 0xbf, 0x5b, - 0x5e, 0x1b, 0xb4, 0xbc, 0x9f, 0x04, 0x6d, 0x5c, 0x1c, 0xc6, 0xda, 0xeb, - 0xfe, 0x81, 0x71, 0xc5, 0x9c, 0xbb, 0xfb, 0x22, 0xbc, 0x7a, 0x2f, 0xaf, - 0x09, 0xcf, 0xaa, 0x7e, 0x84, 0x74, 0x93, 0x62, 0x3f, 0xa3, 0xfd, 0x03, - 0x8c, 0xdc, 0x37, 0xd2, 0x8f, 0x79, 0x8e, 0x0d, 0xfe, 0x2f, 0x08, 0xbe, - 0xb1, 0xfa, 0x11, 0xa8, 0xcf, 0x23, 0xf6, 0x5f, 0xe8, 0x1b, 0x38, 0x60, - 0x6c, 0x32, 0x3f, 0xdb, 0x1e, 0x1c, 0x03, 0x76, 0x7e, 0x09, 0x6b, 0xba, - 0xc6, 0xaf, 0xed, 0x7f, 0xd0, 0xb6, 0x2d, 0x9f, 0x84, 0x26, 0xf7, 0x23, - 0x80, 0xf5, 0x37, 0xc7, 0x86, 0xe3, 0x1f, 0xa8, 0x4c, 0x0b, 0xc0, 0xfb, - 0x8f, 0x7b, 0x39, 0x38, 0xfe, 0x82, 0xc8, 0x6e, 0xdf, 0x51, 0x97, 0x75, - 0x7f, 0x72, 0xbe, 0xe5, 0xf1, 0xe1, 0x4a, 0x78, 0x1f, 0xa1, 0x11, 0xe8, - 0xc5, 0x35, 0xeb, 0xfe, 0x86, 0x2c, 0x75, 0x7d, 0x33, 0x2a, 0xfa, 0xfe, - 0xe2, 0x28, 0xaa, 0xdd, 0xf1, 0xe1, 0x4f, 0xec, 0xe9, 0x05, 0x1d, 0xe6, - 0x5e, 0x4e, 0xef, 0xfd, 0x0c, 0x83, 0xbb, 0xe9, 0x9e, 0xa1, 0xfd, 0xc2, - 0xde, 0xf3, 0xf8, 0xbc, 0x39, 0x9f, 0x1f, 0xe2, 0x31, 0x09, 0x87, 0xba, - 0x3c, 0x9d, 0xdf, 0xd0, 0xb5, 0xfc, 0xeb, 0x2a, 0xda, 0xff, 0x00, 0x68, - 0xcb, 0xcf, 0xda, 0xbc, 0x36, 0xbf, 0x8a, 0xb7, 0xc4, 0x2c, 0xcd, 0x1a, - 0x76, 0x86, 0xbf, 0xa1, 0xab, 0x9b, 0xcb, 0x0d, 0x8f, 0xed, 0x70, 0x8a, - 0xbb, 0xdf, 0x1e, 0x15, 0x41, 0xfa, 0xd4, 0x0f, 0x3c, 0x98, 0xa5, 0x37, - 0x63, 0xfa, 0x17, 0x26, 0xb3, 0x30, 0x94, 0x30, 0xaf, 0xb8, 0x2b, 0xf5, - 0xf5, 0xe1, 0xa9, 0xaa, 0xed, 0x1f, 0x53, 0x60, 0xe8, 0xc2, 0xca, 0x95, - 0xec, 0xeb, 0xde, 0x73, 0xe9, 0xe7, 0x19, 0xd1, 0x16, 0xe1, 0x88, 0x9b, - 0x5f, 0x14, 0x1c, 0xd8, 0x84, 0x8a, 0x0e, 0xac, 0xe0, 0x0f, 0x24, 0xc6, - 0xbe, 0xf1, 0x05, 0x16, 0xf4, 0x44, 0x37, 0xec, 0x44, 0xff, 0x00, 0xc2, - 0x7f, 0xe5, 0x4b, 0x6e, 0x2e, 0xd3, 0xdf, 0x3a, 0x4c, 0xa9, 0xf1, 0x40, - 0x79, 0x1a, 0x01, 0xeb, 0x19, 0x3b, 0x15, 0xc1, 0x5d, 0xe4, 0xfa, 0xf0, - 0x95, 0x55, 0xc7, 0xe1, 0x02, 0x07, 0x38, 0x96, 0x2f, 0x91, 0x7d, 0x30, - 0x19, 0x68, 0xe9, 0x51, 0x5c, 0x0d, 0x47, 0x08, 0xaf, 0x74, 0x55, 0xab, - 0x7e, 0x53, 0x8a, 0x27, 0x62, 0x36, 0x1e, 0x7c, 0x13, 0xa0, 0x5d, 0xc0, - 0x94, 0x93, 0x3d, 0x21, 0x75, 0xe4, 0xbd, 0x47, 0xa8, 0x08, 0x20, 0x2f, - 0xac, 0xc5, 0x42, 0x73, 0xda, 0x15, 0x6c, 0x16, 0x5e, 0xa5, 0x35, 0x13, - 0x9c, 0x90, 0x4a, 0x87, 0xb4, 0xba, 0xa0, 0x2d, 0xc3, 0xd9, 0xd2, 0x7e, - 0xd3, 0xb7, 0x86, 0xe0, 0x92, 0xd0, 0x11, 0x61, 0xe6, 0xd3, 0xed, 0x00, - 0x1a, 0xc1, 0xa1, 0x56, 0xad, 0xa4, 0x40, 0xdb, 0x8f, 0x59, 0xd8, 0x1e, - 0x70, 0x50, 0x51, 0xe0, 0x96, 0x8d, 0x13, 0x38, 0x88, 0xe6, 0x84, 0x4c, - 0x87, 0x6b, 0x97, 0x48, 0x6b, 0x21, 0xf2, 0x85, 0x64, 0xbf, 0x39, 0x49, - 0xa5, 0x59, 0x98, 0x8a, 0xe5, 0xe2, 0xe3, 0x61, 0x5a, 0xbd, 0x91, 0x9d, - 0x47, 0x54, 0xde, 0x3a, 0x28, 0x2e, 0x20, 0xa3, 0x9e, 0x91, 0xb5, 0xe4, - 0xcc, 0x25, 0xbb, 0xa8, 0x45, 0x5b, 0xab, 0x9a, 0x7d, 0x89, 0xab, 0xf0, - 0x0e, 0xd8, 0x7c, 0x20, 0x8d, 0xa2, 0xf6, 0xc5, 0xbe, 0xce, 0x2e, 0xf0, - 0x9d, 0xf6, 0x62, 0x25, 0x85, 0x07, 0x77, 0x44, 0xd7, 0x05, 0xb0, 0xe0, - 0xed, 0x11, 0x3c, 0x98, 0x98, 0x04, 0xf8, 0x17, 0xb8, 0xe6, 0x06, 0x7e, - 0xa5, 0x4b, 0x43, 0xca, 0xb2, 0x24, 0x20, 0x64, 0xd7, 0xd9, 0x32, 0xb8, - 0xd1, 0xc1, 0xed, 0x77, 0x8a, 0xc3, 0x37, 0xdb, 0x98, 0x6b, 0x1a, 0x17, - 0xde, 0x88, 0x6d, 0x4a, 0x76, 0x96, 0x55, 0x90, 0x5f, 0x68, 0x30, 0xd9, - 0x86, 0xb1, 0x2a, 0xef, 0x64, 0x55, 0x8b, 0x13, 0x58, 0xe3, 0xde, 0x06, - 0xd9, 0xf2, 0xa6, 0x0f, 0x24, 0x03, 0x22, 0xf0, 0xf5, 0x8d, 0x9d, 0xca, - 0x4c, 0x04, 0xc7, 0xaa, 0xac, 0x7c, 0x97, 0xc2, 0x18, 0xf1, 0x45, 0x6c, - 0x27, 0xe4, 0x12, 0xc0, 0x42, 0x91, 0xe7, 0x97, 0xe6, 0xe5, 0x7a, 0x97, - 0xc6, 0x71, 0x0a, 0x2f, 0x15, 0x79, 0x83, 0x64, 0xa2, 0xce, 0xbe, 0xd7, - 0x2a, 0x41, 0x15, 0xe7, 0xa3, 0x50, 0x82, 0xd9, 0x65, 0xb6, 0xe6, 0x8a, - 0x73, 0x78, 0xb7, 0x53, 0x2b, 0x4a, 0x56, 0xdd, 0x40, 0x3b, 0x6c, 0x76, - 0x99, 0x02, 0x80, 0x94, 0x79, 0xa3, 0x0a, 0x98, 0x3b, 0x15, 0x82, 0x2a, - 0x98, 0xc6, 0x86, 0xe1, 0x5f, 0x73, 0x82, 0x08, 0xe7, 0x2e, 0x65, 0xae, - 0x1c, 0x37, 0x86, 0xe7, 0xb0, 0x65, 0xdc, 0x7a, 0x8c, 0x0d, 0xc7, 0x59, - 0x3d, 0xf5, 0x70, 0x9b, 0x2b, 0x2f, 0xe6, 0x00, 0xa0, 0xc0, 0x38, 0x0f, - 0x0a, 0x01, 0x12, 0xc7, 0x08, 0xce, 0x4f, 0x49, 0x73, 0x6c, 0xf5, 0x85, - 0x72, 0x16, 0x79, 0xc2, 0x93, 0xed, 0x26, 0x9a, 0x95, 0x68, 0xa3, 0x88, - 0xb1, 0x68, 0x59, 0x9c, 0xc7, 0x20, 0x80, 0xf3, 0x80, 0x16, 0x03, 0x91, - 0x78, 0x26, 0x56, 0x58, 0x1c, 0x75, 0x82, 0xd3, 0x2e, 0x59, 0x9f, 0x00, - 0x1e, 0xef, 0x45, 0xf9, 0x4b, 0xea, 0x3c, 0xdc, 0xc1, 0x55, 0x5b, 0x8d, - 0xaa, 0x8a, 0x37, 0x91, 0xa3, 0xb1, 0x0a, 0x47, 0xda, 0x1b, 0x44, 0xf2, - 0xdc, 0x1b, 0x52, 0x9b, 0xbb, 0x8c, 0x0a, 0x50, 0xdf, 0x91, 0x1b, 0x00, - 0xf4, 0x84, 0x40, 0xb4, 0x66, 0x11, 0x85, 0x81, 0x1c, 0xb5, 0x11, 0xdc, - 0xb3, 0xba, 0xc6, 0x1e, 0xfd, 0x2f, 0xa7, 0x8d, 0xf8, 0xea, 0x0d, 0x8d, - 0xd9, 0xea, 0xe2, 0x43, 0x88, 0x71, 0xc8, 0x63, 0xe5, 0xc7, 0x77, 0xc8, - 0x66, 0x08, 0x56, 0xee, 0xbf, 0xf6, 0x04, 0x44, 0x65, 0x75, 0x7f, 0xec, - 0xaa, 0xe1, 0x3a, 0xb7, 0xff, 0x00, 0x66, 0x41, 0x67, 0x85, 0xee, 0x55, - 0xa4, 0xb8, 0x52, 0xfd, 0xe7, 0x62, 0x0e, 0x41, 0xd6, 0x51, 0x5d, 0x20, - 0xa8, 0x92, 0xab, 0x89, 0xdb, 0xa4, 0x7d, 0xa9, 0x6a, 0xf9, 0x8d, 0x75, - 0x8f, 0x53, 0xef, 0xaa, 0x84, 0xd0, 0xf5, 0xd7, 0xdc, 0xa3, 0x05, 0x5e, - 0x7c, 0xfc, 0xca, 0x0b, 0xb7, 0xab, 0x3f, 0x32, 0xe6, 0xc5, 0xd2, 0x0e, - 0xfe, 0xe1, 0xb7, 0x35, 0xcc, 0xb6, 0x44, 0x5b, 0xbb, 0x09, 0xe6, 0xfd, - 0xaf, 0x1d, 0x4a, 0x6b, 0x5b, 0x77, 0x81, 0xe4, 0x83, 0x3b, 0x02, 0xf6, - 0xea, 0xc8, 0x7a, 0x6b, 0xd2, 0x5a, 0x1b, 0x79, 0x69, 0x94, 0x39, 0xb4, - 0x9a, 0xac, 0x4c, 0x99, 0x2b, 0xc9, 0x77, 0x72, 0xdb, 0x43, 0xce, 0x56, - 0x25, 0xa5, 0x07, 0x3b, 0x94, 0xa4, 0x10, 0x9a, 0xf0, 0x80, 0x37, 0x2e, - 0x6a, 0x8b, 0x50, 0x80, 0x86, 0x86, 0xe3, 0xb2, 0x9a, 0xc5, 0x48, 0x94, - 0x8c, 0x08, 0x8b, 0x54, 0x87, 0x10, 0x08, 0xc6, 0xe8, 0x72, 0xd6, 0x61, - 0x30, 0x37, 0x91, 0xd4, 0x4a, 0xa9, 0xd8, 0x04, 0xa1, 0x11, 0x65, 0x4c, - 0xc8, 0x55, 0x09, 0x46, 0x47, 0x7a, 0x5d, 0x35, 0x50, 0x22, 0x80, 0x69, - 0x6e, 0xf0, 0x14, 0xd8, 0x5d, 0x75, 0xff, 0x00, 0x20, 0xad, 0x12, 0x9e, - 0x87, 0x13, 0x02, 0x9b, 0x35, 0x67, 0x74, 0x23, 0x8b, 0xc6, 0x85, 0x6a, - 0xde, 0xc1, 0x6a, 0xf4, 0x27, 0x78, 0x20, 0xb4, 0x02, 0xde, 0xee, 0xde, - 0xef, 0xf0, 0x32, 0x74, 0x28, 0x30, 0x0b, 0xf2, 0xb5, 0xc0, 0x23, 0xd3, - 0x97, 0xb4, 0x33, 0x6b, 0x6a, 0xf7, 0x2b, 0x01, 0x03, 0x2c, 0x08, 0x4a, - 0xa6, 0x8c, 0xe0, 0x4a, 0x9a, 0x2b, 0x54, 0x54, 0x3e, 0x29, 0x72, 0x1b, - 0x64, 0x52, 0xc9, 0x66, 0x95, 0x33, 0x37, 0x8f, 0xfa, 0x15, 0x00, 0x95, - 0x0a, 0xd1, 0x58, 0x51, 0xd1, 0xb2, 0xd9, 0xb6, 0x05, 0x9e, 0xa8, 0x0a, - 0x18, 0x09, 0x80, 0xd5, 0xc3, 0xae, 0xc6, 0x13, 0x83, 0x00, 0x82, 0x02, - 0xc2, 0xc1, 0x58, 0xda, 0xa8, 0xd0, 0xc1, 0x45, 0xaa, 0xc7, 0x79, 0xed, - 0x05, 0x26, 0xc5, 0xad, 0x23, 0x80, 0xab, 0x8a, 0x71, 0x74, 0x0f, 0x2a, - 0xce, 0x75, 0xa9, 0x70, 0xd2, 0x9d, 0x20, 0x8d, 0x0d, 0x62, 0x2b, 0x2e, - 0x15, 0xed, 0xb8, 0x97, 0x68, 0x70, 0x21, 0x83, 0xcc, 0x3d, 0x5e, 0x9f, - 0xc2, 0x30, 0xc1, 0xb0, 0xca, 0xaf, 0x63, 0x07, 0x9d, 0x43, 0xee, 0x8f, - 0x63, 0xbf, 0xd4, 0x2a, 0xcb, 0x8c, 0xdb, 0x05, 0x39, 0x35, 0x7c, 0x10, - 0xb0, 0x6d, 0x9b, 0x25, 0xb6, 0x36, 0x42, 0x82, 0xd8, 0xc8, 0x48, 0x53, - 0xb4, 0x52, 0x94, 0x64, 0xce, 0xb5, 0x10, 0x05, 0x6b, 0xad, 0x4a, 0x85, - 0x67, 0x74, 0xb7, 0x9b, 0x6d, 0x65, 0x5f, 0xf1, 0x98, 0x6c, 0xc1, 0x8a, - 0x7f, 0x5b, 0xa2, 0x42, 0xf4, 0x04, 0x75, 0x67, 0x3a, 0x55, 0x79, 0x15, - 0x23, 0xf0, 0xa0, 0x02, 0x11, 0xb9, 0xd1, 0x30, 0x50, 0x4c, 0x2c, 0xda, - 0x30, 0xa6, 0x29, 0x65, 0xa3, 0x68, 0xf2, 0x31, 0xbb, 0x79, 0x9c, 0x0e, - 0x8e, 0x99, 0x8f, 0x9f, 0x8e, 0x21, 0x5c, 0xaa, 0xd0, 0x16, 0xaf, 0x01, - 0x00, 0xf5, 0xb6, 0xb4, 0xef, 0x6e, 0xe8, 0xbe, 0xbf, 0xc4, 0x80, 0xc1, - 0xd4, 0xc4, 0x79, 0xf7, 0x54, 0xf5, 0x26, 0xb2, 0x39, 0xf8, 0x86, 0x17, - 0x97, 0x35, 0x2d, 0x78, 0x15, 0xdc, 0x8b, 0x92, 0x6b, 0x39, 0xa6, 0x05, - 0x4a, 0x0a, 0x1e, 0x53, 0x5a, 0xfc, 0x79, 0x98, 0x08, 0xe0, 0xb1, 0x12, - 0xf0, 0x4a, 0x6d, 0x78, 0x48, 0xd4, 0x02, 0x19, 0xa6, 0x5e, 0x40, 0x9b, - 0x11, 0x80, 0x80, 0x40, 0x4c, 0x28, 0x2b, 0x39, 0x00, 0x65, 0x50, 0x26, - 0x33, 0x0b, 0x45, 0x9a, 0x2a, 0x55, 0x2a, 0xd5, 0x08, 0x42, 0x91, 0x3a, - 0xe8, 0x36, 0xde, 0x1a, 0x80, 0xe0, 0x68, 0x64, 0x65, 0xe9, 0xd7, 0xff, - 0x00, 0xb8, 0x44, 0x70, 0x48, 0x61, 0x75, 0xe6, 0xb3, 0x6b, 0x07, 0x04, - 0x1d, 0x3a, 0x4c, 0x0e, 0x6e, 0xb4, 0xcd, 0x4e, 0xa1, 0x75, 0xb4, 0x3d, - 0xd1, 0x4e, 0xc7, 0x57, 0xf1, 0x87, 0xc1, 0x65, 0x9a, 0xf5, 0x7a, 0xfc, - 0x2c, 0x3b, 0x25, 0xf6, 0xb6, 0x87, 0xbe, 0x7c, 0xa7, 0x2b, 0xc6, 0xae, - 0x58, 0x31, 0xae, 0x48, 0xb2, 0x68, 0xaa, 0x39, 0xcb, 0x21, 0x78, 0xf2, - 0x85, 0x8f, 0xf3, 0xf3, 0xcb, 0xae, 0x02, 0xca, 0x40, 0x39, 0x12, 0x98, - 0xb2, 0x04, 0xb9, 0x6f, 0x5b, 0xc2, 0xe0, 0xdc, 0x6d, 0x56, 0x92, 0x69, - 0xcc, 0x37, 0x6c, 0x0e, 0x1d, 0x96, 0x36, 0x15, 0xe7, 0xcc, 0xe4, 0x12, - 0x29, 0x3c, 0x1e, 0x2e, 0x88, 0x90, 0xb5, 0x53, 0xf0, 0x15, 0x35, 0x6e, - 0x35, 0xb2, 0x31, 0x4c, 0xbf, 0xaf, 0x37, 0xaa, 0xac, 0xdb, 0x71, 0x2d, - 0x90, 0x35, 0x65, 0xf3, 0x14, 0x69, 0x2b, 0x8b, 0x85, 0x5d, 0x30, 0x16, - 0x65, 0x37, 0xa6, 0x0e, 0xaa, 0x43, 0x16, 0x8e, 0xe0, 0xd4, 0x1e, 0xc7, - 0xf2, 0x1d, 0x97, 0x83, 0x3d, 0x9f, 0x21, 0xdb, 0xd2, 0x20, 0x0a, 0x4a, - 0xa4, 0xe7, 0xbc, 0x10, 0x07, 0x3b, 0x8f, 0x35, 0xca, 0x6c, 0x08, 0x85, - 0x2b, 0x01, 0x28, 0x02, 0x29, 0x04, 0x87, 0x72, 0x87, 0x14, 0x90, 0x20, - 0x57, 0x36, 0x80, 0x5a, 0x38, 0xb3, 0x82, 0x16, 0x01, 0x71, 0xbb, 0x32, - 0x40, 0xaf, 0x25, 0x8e, 0x7b, 0xa2, 0x99, 0xf8, 0xd5, 0x0a, 0xa5, 0x0b, - 0x4a, 0x14, 0xb3, 0x00, 0xa9, 0x1f, 0xba, 0xc2, 0x20, 0xd6, 0x97, 0x14, - 0x21, 0x6c, 0xae, 0x67, 0x97, 0xfc, 0xb6, 0x46, 0x50, 0x69, 0x08, 0x51, - 0x99, 0x8a, 0xac, 0x5e, 0xea, 0x2c, 0x1b, 0x7b, 0x6a, 0x3a, 0x31, 0x54, - 0x2b, 0x6f, 0xb3, 0xc9, 0xe7, 0xd1, 0xfc, 0xa5, 0x0d, 0xc3, 0xe5, 0x15, - 0x8e, 0xfc, 0x8f, 0x58, 0x85, 0x28, 0x33, 0xb4, 0xec, 0x4d, 0x27, 0x52, - 0x18, 0x75, 0x7e, 0x26, 0x06, 0x4c, 0x15, 0x1a, 0xd7, 0xd1, 0x11, 0x13, - 0x12, 0xa5, 0x2b, 0x56, 0x3a, 0x41, 0xac, 0x5a, 0xb2, 0x69, 0x40, 0x2b, - 0x4d, 0xf1, 0x09, 0x30, 0xd3, 0x03, 0x94, 0x06, 0x6c, 0x0e, 0x40, 0x45, - 0x64, 0x1c, 0x99, 0x49, 0x6c, 0x48, 0x55, 0x14, 0x0b, 0xa1, 0x12, 0x13, - 0xdd, 0xb8, 0x94, 0x22, 0x09, 0x73, 0x7b, 0x04, 0x07, 0x27, 0xca, 0x6f, - 0x75, 0xc5, 0xdc, 0x37, 0x17, 0x16, 0x83, 0x9e, 0xa0, 0xba, 0x69, 0xa3, - 0x1b, 0x48, 0x08, 0x00, 0x28, 0x02, 0x80, 0xfe, 0x6c, 0xdb, 0x98, 0xd3, - 0xd6, 0x25, 0xfa, 0xc7, 0x27, 0x8f, 0x93, 0x13, 0x25, 0xe6, 0xa7, 0x94, - 0x57, 0x44, 0xe4, 0x77, 0xdf, 0x8b, 0x22, 0xab, 0x1f, 0xe0, 0x11, 0x08, - 0xf2, 0xf0, 0xbf, 0x89, 0x4b, 0x95, 0x75, 0xb7, 0xf1, 0x1c, 0x02, 0xd7, - 0x5a, 0xfd, 0xea, 0x57, 0xa5, 0xeb, 0xef, 0x26, 0x88, 0xe4, 0x42, 0x9e, - 0xfc, 0x1b, 0xbf, 0xb1, 0x35, 0xd9, 0x70, 0x76, 0x9c, 0x1a, 0xea, 0x0e, - 0x77, 0x06, 0x89, 0x7c, 0x8a, 0xc7, 0xe7, 0x9f, 0xfb, 0xaf, 0xff, 0xd9 -}; -unsigned int background_R_jpg_len = 19068; diff --git a/core/embed/unix/background_R.jpg b/core/embed/unix/background_R.jpg deleted file mode 100644 index e3c97cc1e..000000000 Binary files a/core/embed/unix/background_R.jpg and /dev/null differ diff --git a/core/embed/unix/common.c b/core/embed/unix/common.c index 204d6dc79..0ce4fbd2d 100644 --- a/core/embed/unix/common.c +++ b/core/embed/unix/common.c @@ -165,7 +165,9 @@ uint32_t hal_ticks_ms() { } static int SDLCALL emulator_event_filter(void *userdata, SDL_Event *event) { +#if defined TREZOR_MODEL_T float gamma = display_gamma(0); +#endif switch (event->type) { case SDL_QUIT: trezor_shutdown(); diff --git a/core/embed/unix/display-unix.c b/core/embed/unix/display-unix.c index b42245f68..40317ecc3 100644 --- a/core/embed/unix/display-unix.c +++ b/core/embed/unix/display-unix.c @@ -121,6 +121,7 @@ static void prepare_gamma_lut(float gamma) { } } +#if defined TREZOR_MODEL_T static void gamma_correct_buffer_to_display(void) { // Gamma correct all the pixels in BUFFER_TO_DISPLAY. pixel_color *pixels = (pixel_color *)BUFFER_TO_DISPLAY->pixels; @@ -131,6 +132,7 @@ static void gamma_correct_buffer_to_display(void) { } } } +#endif float display_gamma(float gamma) { float prev_gamma = DISPLAY_GAMMA; @@ -261,14 +263,10 @@ void display_init(void) { #include "background_T.h" BACKGROUND = IMG_LoadTexture_RW( RENDERER, SDL_RWFromMem(background_T_jpg, background_T_jpg_len), 0); -#elif defined TREZOR_MODEL_1 +#elif defined TREZOR_MODEL_1 || defined TREZOR_MODEL_R #include "background_1.h" BACKGROUND = IMG_LoadTexture_RW( RENDERER, SDL_RWFromMem(background_1_jpg, background_1_jpg_len), 0); -#elif defined TREZOR_MODEL_R -#include "background_R.h" - BACKGROUND = IMG_LoadTexture_RW( - RENDERER, SDL_RWFromMem(background_R_jpg, background_R_jpg_len), 0); #endif #endif if (BACKGROUND) { @@ -281,7 +279,14 @@ void display_init(void) { sdl_touch_offset_x = EMULATOR_BORDER; sdl_touch_offset_y = EMULATOR_BORDER; } +#if defined TREZOR_MODEL_1 || defined TREZOR_MODEL_R + // T1 and TR do not have backlight capabilities in hardware, so + // setting its value here for emulator to avoid + // calling any `set_backlight` functions + DISPLAY_BACKLIGHT = 255; +#else DISPLAY_BACKLIGHT = 0; +#endif #ifdef TREZOR_EMULATOR_RASPI DISPLAY_ORIENTATION = 270; SDL_ShowCursor(SDL_DISABLE); @@ -359,7 +364,7 @@ int display_orientation(int degrees) { int display_get_orientation(void) { return DISPLAY_ORIENTATION; } int display_backlight(int val) { -#if defined TREZOR_MODEL_1 +#if defined TREZOR_MODEL_1 || defined TREZOR_MODEL_R val = 255; #endif if (DISPLAY_BACKLIGHT != val && val >= 0 && val <= 255) { diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index 5af51d868..5c90456ab 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -1,6 +1,7 @@ from typing import * CONFIRMED: object CANCELLED: object +INFO: object # rust/src/ui/model_tr/layout.rs @@ -8,13 +9,18 @@ def disable_animation(disable: bool) -> None: """Disable animations, debug builds only.""" +# rust/src/ui/model_tr/layout.rs +def toif_info(data: bytes) -> tuple[int, int, bool]: + """Get TOIF image dimensions and format (width: int, height: int, is_grayscale: bool).""" + + # rust/src/ui/model_tr/layout.rs def confirm_action( *, title: str, - action: str | None = None, - description: str | None = None, - verb: str | None = None, + action: str | None, + description: str | None, + verb: str = "CONFIRM", verb_cancel: str | None = None, hold: bool = False, hold_danger: bool = False, # unused on TR @@ -24,13 +30,340 @@ def confirm_action( # rust/src/ui/model_tr/layout.rs -def confirm_text( +def confirm_blob( *, title: str, - data: str, + data: str | bytes, description: str | None, + extra: str | None, + verb: str = "CONFIRM", + verb_cancel: str | None = None, + hold: bool = False, +) -> object: + """Confirm byte sequence data.""" + + +# rust/src/ui/model_tr/layout.rs +def confirm_address( + *, + title: str, + data: str, + description: str | None, # unused on TR + extra: str | None, # unused on TR +) -> object: + """Confirm address.""" + + +# rust/src/ui/model_tr/layout.rs +def confirm_properties( + *, + title: str, + items: list[tuple[str | None, str | bytes | None, bool]], + hold: bool = False, +) -> object: + """Confirm list of key-value pairs. The third component in the tuple should be True if + the value is to be rendered as binary with monospace font, False otherwise. + This only concerns the text style, you need to decode the value to UTF-8 in python.""" + + +# rust/src/ui/model_tr/layout.rs +def confirm_reset_device( + *, + title: str, + button: str, +) -> object: + """Confirm TOS before device setup.""" + + +# rust/src/ui/model_tr/layout.rs +def show_address_details( + *, + address: str, + case_sensitive: bool, + account: str | None, + path: str | None, + xpubs: list[tuple[str, str]], +) -> object: + """Show address details - QR code, account, path, cosigner xpubs.""" + + +# rust/src/ui/model_tr/layout.rs +def confirm_value( + *, + title: str, + description: str, + value: str, + verb: str | None = None, + hold: bool = False, +) -> object: + """Confirm value.""" + + +# rust/src/ui/model_tr/layout.rs +def confirm_joint_total( + *, + spending_amount: str, + total_amount: str, +) -> object: + """Confirm total if there are external inputs.""" + + +# rust/src/ui/model_tr/layout.rs +def confirm_modify_output( + *, + address: str, + sign: int, + amount_change: str, + amount_new: str, +) -> object: + """Decrease or increase amount for given address.""" + + +# rust/src/ui/model_tr/layout.rs +def confirm_output( + *, + address: str, + amount: str, + address_title: str, + amount_title: str, +) -> object: + """Confirm output.""" + + +# rust/src/ui/model_tr/layout.rs +def confirm_total( + *, + total_amount: str, + fee_amount: str, + fee_rate_amount: str | None, + total_label: str, + fee_label: str, +) -> object: + """Confirm summary of a transaction.""" + + +# rust/src/ui/model_tr/layout.rs +def tutorial() -> object: + """Show user how to interact with the device.""" + + +# rust/src/ui/model_tr/layout.rs +def confirm_modify_fee( + *, + sign: int, + user_fee_change: str, + total_fee_new: str, + fee_rate_amount: str | None, ) -> object: - """Confirm text.""" + """Decrease or increase transaction fee.""" + + +# rust/src/ui/model_tr/layout.rs +def confirm_fido( + *, + title: str, # unused on TR + app_name: str, + icon_name: str | None, # unused on TR + accounts: list[str | None], +) -> int | object: + """FIDO confirmation. + Returns page index in case of confirmation and CANCELLED otherwise. + """ + + +# rust/src/ui/model_tr/layout.rs +def show_info( + *, + title: str, + description: str = "", + time_ms: int = 0, +) -> object: + """Info modal.""" + + +# rust/src/ui/model_tr/layout.rs +def show_mismatch() -> object: + """Warning modal, receiving address mismatch.""" + + +# rust/src/ui/model_tr/layout.rs +def confirm_with_info( + *, + title: str, + button: str, # unused on TR + info_button: str, # unused on TR + items: Iterable[Tuple[int, str]], +) -> object: + """Confirm given items but with third button. Always single page + without scrolling.""" + + +# rust/src/ui/model_tr/layout.rs +def confirm_coinjoin( + *, + max_rounds: str, + max_feerate: str, +) -> object: + """Confirm coinjoin authorization.""" + + +# rust/src/ui/model_tr/layout.rs +def request_pin( + *, + prompt: str, + subprompt: str, + allow_cancel: bool = True, # unused on TR + wrong_pin: bool = False, # unused on TR +) -> str | object: + """Request pin on device.""" + + +# rust/src/ui/model_tr/layout.rs +def request_passphrase( + *, + prompt: str, + max_len: int, # unused on TR +) -> str | object: + """Get passphrase.""" + + +# rust/src/ui/model_tr/layout.rs +def request_bip39( + *, + prompt: str, +) -> str: + """Get recovery word for BIP39.""" + + +# rust/src/ui/model_tr/layout.rs +def request_slip39( + *, + prompt: str, +) -> str: + """SLIP39 word input keyboard.""" + + +# rust/src/ui/model_tr/layout.rs +def select_word( + *, + title: str, # unused on TR + description: str, + words: Iterable[str], +) -> int: + """Select mnemonic word from three possibilities - seed check after backup. The + iterable must be of exact size. Returns index in range `0..3`.""" + + +# rust/src/ui/model_tr/layout.rs +def show_share_words( + *, + title: str, + share_words: Iterable[str], +) -> object: + """Shows a backup seed.""" + + +# rust/src/ui/model_tr/layout.rs +def request_number( + *, + title: str, + count: int, + min_count: int, + max_count: int, + description: Callable[[int], str] | None = None, # unused on TR +) -> object: + """Number input with + and - buttons, description, and info button.""" + + +# rust/src/ui/model_tr/layout.rs +def show_checklist( + *, + title: str, # unused on TR + items: Iterable[str], + active: int, + button: str, +) -> object: + """Checklist of backup steps. Active index is highlighted, previous items have check + mark next to them.""" + + +# rust/src/ui/model_tr/layout.rs +def confirm_recovery( + *, + title: str, # unused on TR + description: str, + button: str, + dry_run: bool, + info_button: bool, # unused on TR +) -> object: + """Device recovery homescreen.""" + + +# rust/src/ui/model_tr/layout.rs +def select_word_count( + *, + dry_run: bool, # unused on TR +) -> int | str: # TR returns str + """Select mnemonic word count from (12, 18, 20, 24, 33).""" + + +# rust/src/ui/model_tr/layout.rs +def show_group_share_success( + *, + lines: Iterable[str], +) -> int: + """Shown after successfully finishing a group.""" + + +# rust/src/ui/model_tr/layout.rs +def show_progress( + *, + title: str, + indeterminate: bool = False, + description: str = "", +) -> object: + """Show progress loader. Please note that the number of lines reserved on screen for + description is determined at construction time. If you want multiline descriptions + make sure the initial description has at least that amount of lines.""" + + +# rust/src/ui/model_tr/layout.rs +def show_progress_coinjoin( + *, + title: str, + indeterminate: bool = False, + time_ms: int = 0, + skip_first_paint: bool = False, +) -> object: + """Show progress loader for coinjoin. Returns CANCELLED after a specified time when + time_ms timeout is passed.""" + + +# rust/src/ui/model_tr/layout.rs +def show_homescreen( + *, + label: str | None, + hold: bool, # unused on TR + notification: str | None, + notification_level: int = 0, + skip_first_paint: bool, +) -> CANCELLED: + """Idle homescreen.""" + + +# rust/src/ui/model_tr/layout.rs +def show_lockscreen( + *, + label: str | None, + bootscreen: bool, + skip_first_paint: bool, +) -> CANCELLED: + """Homescreen for locked device.""" + + +# rust/src/ui/model_tr/layout.rs +def draw_welcome_screen() -> None: + """Show logo icon with the model name at the bottom and return.""" CONFIRMED: object CANCELLED: object INFO: object @@ -440,7 +773,7 @@ def show_progress_coinjoin( # rust/src/ui/model_tt/layout.rs def show_homescreen( *, - label: str, + label: str | None, hold: bool, notification: str | None, notification_level: int = 0, @@ -452,7 +785,7 @@ def show_homescreen( # rust/src/ui/model_tt/layout.rs def show_lockscreen( *, - label: str, + label: str | None, bootscreen: bool, skip_first_paint: bool, ) -> CANCELLED: diff --git a/core/src/_vulture_ignore.txt b/core/src/_vulture_ignore.txt index 916f1bfa3..1141a7a12 100644 --- a/core/src/_vulture_ignore.txt +++ b/core/src/_vulture_ignore.txt @@ -1,17 +1,19 @@ -_.sd_card_present # unused attribute (src/apps/base.py:111) -_.initialized # unused attribute (src/apps/base.py:112) -_.recovery_mode # unused attribute (src/apps/base.py:122) +_.sd_card_present # unused attribute (src/apps/base.py:101) +_.sd_card_present # unused attribute (src/apps/base.py:103) +_.initialized # unused attribute (src/apps/base.py:105) +_.recovery_mode # unused attribute (src/apps/base.py:115) +_.sd_protection # unused attribute (src/apps/base.py:122) _.sd_protection # unused attribute (src/apps/base.py:124) -_.wipe_code_protection # unused attribute (src/apps/base.py:125) +_.wipe_code_protection # unused attribute (src/apps/base.py:126) SIGHASH_ALL_FORKID # unused variable (src/apps/bitcoin/common.py:43) -_.extra_data_offset # unused attribute (src/apps/bitcoin/sign_tx/helpers.py:292) -_.request_index # unused attribute (src/apps/bitcoin/sign_tx/helpers.py:309) -_.request_index # unused attribute (src/apps/bitcoin/sign_tx/helpers.py:320) -_.request_index # unused attribute (src/apps/bitcoin/sign_tx/helpers.py:336) -_.request_index # unused attribute (src/apps/bitcoin/sign_tx/helpers.py:347) -_.request_index # unused attribute (src/apps/bitcoin/sign_tx/helpers.py:360) -_.request_index # unused attribute (src/apps/bitcoin/sign_tx/helpers.py:380) -_.extra_data_offset # unused attribute (src/apps/bitcoin/sign_tx/helpers.py:383) +_.extra_data_offset # unused attribute (src/apps/bitcoin/sign_tx/helpers.py:314) +_.request_index # unused attribute (src/apps/bitcoin/sign_tx/helpers.py:331) +_.request_index # unused attribute (src/apps/bitcoin/sign_tx/helpers.py:342) +_.request_index # unused attribute (src/apps/bitcoin/sign_tx/helpers.py:358) +_.request_index # unused attribute (src/apps/bitcoin/sign_tx/helpers.py:369) +_.request_index # unused attribute (src/apps/bitcoin/sign_tx/helpers.py:382) +_.request_index # unused attribute (src/apps/bitcoin/sign_tx/helpers.py:402) +_.extra_data_offset # unused attribute (src/apps/bitcoin/sign_tx/helpers.py:405) is_other_warning # unused variable (src/apps/cardano/helpers/credential.py:34) _.is_other_warning # unused attribute (src/apps/cardano/helpers/credential.py:84) _.is_other_warning # unused attribute (src/apps/cardano/helpers/credential.py:118) @@ -21,18 +23,16 @@ exc_tb # unused variable (src/apps/cardano/helpers/hash_builder_collection.py:7 exc_val # unused variable (src/apps/cardano/helpers/hash_builder_collection.py:73) exc_val # unused variable (src/apps/common/keychain.py:174) tb # unused variable (src/apps/common/keychain.py:174) -_.reset_entropy # unused attribute (src/apps/debug/__init__.py:167) -_.layout_lines # unused attribute (src/apps/debug/__init__.py:176) -_.reset_word_pos # unused attribute (src/apps/debug/__init__.py:179) -_.reset_word # unused attribute (src/apps/debug/__init__.py:181) -_.signature_v # unused attribute (src/apps/ethereum/sign_tx.py:176) -_.signature_v # unused attribute (src/apps/ethereum/sign_tx.py:178) +PATTERN_BIP44_PUBKEY # unused variable (src/apps/common/paths.py:333) +_.reset_entropy # unused attribute (src/apps/debug/__init__.py:206) _.signature_v # unused attribute (src/apps/ethereum/sign_tx.py:180) -_.signature_r # unused attribute (src/apps/ethereum/sign_tx.py:182) -_.signature_s # unused attribute (src/apps/ethereum/sign_tx.py:183) -_.signature_v # unused attribute (src/apps/ethereum/sign_tx_eip1559.py:168) -_.signature_r # unused attribute (src/apps/ethereum/sign_tx_eip1559.py:169) -_.signature_s # unused attribute (src/apps/ethereum/sign_tx_eip1559.py:170) +_.signature_v # unused attribute (src/apps/ethereum/sign_tx.py:182) +_.signature_v # unused attribute (src/apps/ethereum/sign_tx.py:184) +_.signature_r # unused attribute (src/apps/ethereum/sign_tx.py:186) +_.signature_s # unused attribute (src/apps/ethereum/sign_tx.py:187) +_.signature_v # unused attribute (src/apps/ethereum/sign_tx_eip1559.py:171) +_.signature_r # unused attribute (src/apps/ethereum/sign_tx_eip1559.py:172) +_.signature_s # unused attribute (src/apps/ethereum/sign_tx_eip1559.py:173) _.tx_derivations # unused attribute (src/apps/monero/get_tx_keys.py:84) _.tx_keys # unused attribute (src/apps/monero/get_tx_keys.py:87) _.key_offsets # unused attribute (src/apps/monero/signing/step_02_set_input.py:82) @@ -47,22 +47,24 @@ XmrStructuredType # unused class (src/apps/monero/xmr/serialize/base_types.py:3 _.ndata # unused attribute (src/apps/monero/xmr/serialize/readwriter.py:16) _.ndata # unused attribute (src/apps/monero/xmr/serialize/readwriter.py:48) _.ndata # unused attribute (src/apps/monero/xmr/serialize/readwriter.py:94) -_.versionInterface # unused attribute (src/apps/webauthn/fido2.py:1190) -_.versionMajor # unused attribute (src/apps/webauthn/fido2.py:1191) -_.versionMinor # unused attribute (src/apps/webauthn/fido2.py:1192) -_.versionBuild # unused attribute (src/apps/webauthn/fido2.py:1193) -_.capFlags # unused attribute (src/apps/webauthn/fido2.py:1194) -_.registerId # unused attribute (src/apps/webauthn/fido2.py:1287) -_.keyHandleLen # unused attribute (src/apps/webauthn/fido2.py:1289) +_.versionInterface # unused attribute (src/apps/webauthn/fido2.py:1193) +_.versionMajor # unused attribute (src/apps/webauthn/fido2.py:1194) +_.versionMinor # unused attribute (src/apps/webauthn/fido2.py:1195) +_.versionBuild # unused attribute (src/apps/webauthn/fido2.py:1196) +_.capFlags # unused attribute (src/apps/webauthn/fido2.py:1197) +_.registerId # unused attribute (src/apps/webauthn/fido2.py:1290) +_.keyHandleLen # unused attribute (src/apps/webauthn/fido2.py:1292) _.txid_digest # unused method (src/apps/zcash/hasher.py:65) +combine_signatures # unused variable (src/trezor/crypto/cosi.py:8) exc_val # unused variable (src/trezor/sdcard.py:63) tb # unused variable (src/trezor/sdcard.py:63) -format_ordinal # unused function (src/trezor/strings.py:26) -_.repaint # unused attribute (src/trezor/ui/__init__.py:126) -_.repaint # unused attribute (src/trezor/ui/__init__.py:140) -_.repaint # unused attribute (src/trezor/ui/__init__.py:161) -button_number # unused variable (src/trezor/ui/__init__.py:163) -button_number # unused variable (src/trezor/ui/__init__.py:166) +BOLD # unused variable (src/trezor/ui/__init__.py:14) +_.repaint # unused attribute (src/trezor/ui/__init__.py:119) +_.repaint # unused attribute (src/trezor/ui/__init__.py:133) +_.repaint # unused attribute (src/trezor/ui/__init__.py:154) +button_number # unused variable (src/trezor/ui/__init__.py:156) +button_number # unused variable (src/trezor/ui/__init__.py:159) +red # unused variable (src/trezor/ui/layouts/tr/__init__.py:605) mem_dump # unused function (src/trezor/utils.py:106) __buf # unused variable (src/trezor/utils.py:142) __data # unused variable (src/trezor/utils.py:150) @@ -72,5 +74,5 @@ DebugHashContextWrapper # unused class (src/trezor/utils.py:164) argv # unused variable (src/trezor/wire/__init__.py:121) argv # unused variable (src/trezor/wire/__init__.py:124) argv # unused variable (src/trezor/wire/__init__.py:127) -kill_default # unused function (src/trezor/workflow.py:102) +kill_default # unused function (src/trezor/workflow.py:120) __getattr__ # unused function (src/typing.py:20) diff --git a/core/src/all_modules.py b/core/src/all_modules.py index 47ab4c333..8e68d7675 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -99,6 +99,8 @@ trezor.enums.Capability import trezor.enums.Capability trezor.enums.DebugButton import trezor.enums.DebugButton +trezor.enums.DebugPhysicalButton +import trezor.enums.DebugPhysicalButton trezor.enums.DebugSwipeDirection import trezor.enums.DebugSwipeDirection trezor.enums.DecredStakingSpendType @@ -161,6 +163,16 @@ trezor.ui.layouts.reset import trezor.ui.layouts.reset trezor.ui.layouts.tr import trezor.ui.layouts.tr +trezor.ui.layouts.tr.fido +import trezor.ui.layouts.tr.fido +trezor.ui.layouts.tr.homescreen +import trezor.ui.layouts.tr.homescreen +trezor.ui.layouts.tr.progress +import trezor.ui.layouts.tr.progress +trezor.ui.layouts.tr.recovery +import trezor.ui.layouts.tr.recovery +trezor.ui.layouts.tr.reset +import trezor.ui.layouts.tr.reset trezor.ui.layouts.tt_v2 import trezor.ui.layouts.tt_v2 trezor.ui.layouts.tt_v2.fido @@ -331,6 +343,8 @@ apps.management.sd_protect import apps.management.sd_protect apps.management.set_u2f_counter import apps.management.set_u2f_counter +apps.management.show_tutorial +import apps.management.show_tutorial apps.management.wipe_device import apps.management.wipe_device apps.misc diff --git a/core/src/apps/base.py b/core/src/apps/base.py index f26c199fd..d62ef5be2 100644 --- a/core/src/apps/base.py +++ b/core/src/apps/base.py @@ -41,9 +41,7 @@ def busy_expiry_ms() -> int: def get_features() -> Features: import storage.recovery as storage_recovery - import storage.sd_salt as storage_sd_salt - from trezor import sdcard from trezor.enums import Capability from trezor.messages import Features @@ -92,11 +90,18 @@ def get_features() -> Features: Capability.ShamirGroups, ] - # Other models are not capable of PassphraseEntry - if utils.MODEL in ("T",): + # Some models are not capable of PassphraseEntry + if utils.MODEL in ("T", "R"): f.capabilities.append(Capability.PassphraseEntry) - f.sd_card_present = sdcard.is_present() + # Only some models are capable of SD card + if utils.USE_SD_CARD: + from trezor import sdcard + + f.sd_card_present = sdcard.is_present() + else: + f.sd_card_present = False + f.initialized = storage_device.is_initialized() # private fields: @@ -109,7 +114,15 @@ def get_features() -> Features: f.flags = storage_device.get_flags() f.recovery_mode = storage_recovery.is_in_progress() f.backup_type = mnemonic.get_type() - f.sd_protection = storage_sd_salt.is_enabled() + + # Only some models are capable of SD card + if utils.USE_SD_CARD: + import storage.sd_salt as storage_sd_salt + + f.sd_protection = storage_sd_salt.is_enabled() + else: + f.sd_protection = False + f.wipe_code_protection = config.has_wipe_code() f.passphrase_always_on_device = storage_device.get_passphrase_always_on_device() f.safety_checks = safety_checks.read_setting() @@ -252,7 +265,8 @@ async def handle_UnlockPath(ctx: wire.Context, msg: UnlockPath) -> protobuf.Mess ctx, "confirm_coinjoin_access", title="Coinjoin", - description="Do you want to access your coinjoin account?", + description="Access your coinjoin account?", + verb="ALLOW", ) wire_types = (MessageType.GetAddress, MessageType.GetPublicKey, MessageType.SignTx) diff --git a/core/src/apps/common/request_pin.py b/core/src/apps/common/request_pin.py index 959ecad45..230fec744 100644 --- a/core/src/apps/common/request_pin.py +++ b/core/src/apps/common/request_pin.py @@ -54,10 +54,11 @@ async def request_pin( async def request_pin_confirm(ctx: Context, *args: Any, **kwargs: Any) -> str: - from trezor.ui.layouts import pin_mismatch_popup + from trezor.ui.layouts import pin_mismatch_popup, confirm_reenter_pin while True: pin1 = await request_pin(ctx, "Enter new PIN", *args, **kwargs) + await confirm_reenter_pin(ctx) pin2 = await request_pin(ctx, "Re-enter new PIN", *args, **kwargs) if pin1 == pin2: return pin1 diff --git a/core/src/apps/debug/__init__.py b/core/src/apps/debug/__init__.py index 293c0bafa..450030a94 100644 --- a/core/src/apps/debug/__init__.py +++ b/core/src/apps/debug/__init__.py @@ -9,7 +9,7 @@ if __debug__: import trezorui2 - from trezor import log, loop, wire + from trezor import log, loop, utils, wire from trezor.ui import display from trezor.enums import MessageType from trezor.messages import ( @@ -176,8 +176,13 @@ if __debug__: debug_events.last_event += 1 # TT click on specific coordinates, with possible hold - if x is not None and y is not None: + if x is not None and y is not None and utils.MODEL in ("T",): click_chan.publish((debug_events.last_event, x, y, msg.hold_ms)) + # TR press specific button + elif msg.physical_button is not None and utils.MODEL in ("R",): + button_chan.publish( + (debug_events.last_event, msg.physical_button, msg.hold_ms) + ) else: # Will get picked up by _dispatch_debuglink_decision eventually debuglink_decision_chan.publish((debug_events.last_event, msg)) diff --git a/core/src/apps/homescreen/__init__.py b/core/src/apps/homescreen/__init__.py index 6196bfcd4..78b0c8066 100644 --- a/core/src/apps/homescreen/__init__.py +++ b/core/src/apps/homescreen/__init__.py @@ -14,14 +14,17 @@ async def busyscreen() -> None: async def homescreen() -> None: + from trezor import utils + if storage.device.is_initialized(): label = storage.device.get_label() else: - label = "Go to trezor.io/start" + label = f"Trezor Model {utils.MODEL}" notification = None notification_is_error = False if is_set_any_session(MessageType.AuthorizeCoinJoin): + # TODO: is too long for TR notification = "COINJOIN AUTHORIZED" elif storage.device.is_initialized() and storage.device.no_backup(): notification = "SEEDLESS" @@ -30,10 +33,11 @@ async def homescreen() -> None: notification = "BACKUP FAILED" notification_is_error = True elif storage.device.is_initialized() and storage.device.needs_backup(): - notification = "NEEDS BACKUP" + notification = "BACKUP NEEDED" elif storage.device.is_initialized() and not config.has_pin(): notification = "PIN NOT SET" elif storage.device.get_experimental_features(): + # TODO: is too long for TR notification = "EXPERIMENTAL MODE" await Homescreen( diff --git a/core/src/apps/management/apply_settings.py b/core/src/apps/management/apply_settings.py index 2f6716e92..090b6d0b1 100644 --- a/core/src/apps/management/apply_settings.py +++ b/core/src/apps/management/apply_settings.py @@ -1,9 +1,12 @@ from typing import TYPE_CHECKING +from trezor import utils from trezor.enums import ButtonRequestType from trezor.ui.layouts import confirm_action, confirm_homescreen from trezor.wire import DataError +import trezorui2 + if TYPE_CHECKING: from trezor.messages import ApplySettings, Success from trezor.wire import Context, GenericContext @@ -12,9 +15,36 @@ if TYPE_CHECKING: BRT_PROTECT_CALL = ButtonRequestType.ProtectCall # CACHE +if utils.MODEL == "R": + + def _validate_homescreen_model_specific(homescreen: bytes) -> None: + try: + w, h, is_grayscale = trezorui2.toif_info(homescreen) + except ValueError: + raise DataError("Invalid homescreen") + if w != 128 or h != 64: + raise DataError("Homescreen must be 128x64 pixel large") + if not is_grayscale: + raise DataError("Homescreen must be grayscale") + +else: + + def _validate_homescreen_model_specific(homescreen: bytes) -> None: + try: + w, h, mcu_height = trezorui2.jpeg_info(homescreen) + except ValueError: + raise DataError("Invalid homescreen") + if w != 240 or h != 240: + raise DataError("Homescreen must be 240x240 pixel large") + if mcu_height > 16: + raise DataError("Unsupported jpeg type") + try: + trezorui2.jpeg_test(homescreen) + except ValueError: + raise DataError("Invalid homescreen") + def _validate_homescreen(homescreen: bytes) -> None: - import trezorui2 import storage.device as storage_device if homescreen == b"": @@ -25,18 +55,7 @@ def _validate_homescreen(homescreen: bytes) -> None: f"Homescreen is too large, maximum size is {storage_device.HOMESCREEN_MAXSIZE} bytes" ) - try: - w, h, mcu_height = trezorui2.jpeg_info(homescreen) - except ValueError: - raise DataError("Invalid homescreen") - if w != 240 or h != 240: - raise DataError("Homescreen must be 240x240 pixel large") - if mcu_height > 16: - raise DataError("Unsupported jpeg type") - try: - trezorui2.jpeg_test(homescreen) - except ValueError: - raise DataError("Invalid homescreen") + _validate_homescreen_model_specific(homescreen) async def apply_settings(ctx: Context, msg: ApplySettings) -> Success: diff --git a/core/src/apps/management/change_wipe_code.py b/core/src/apps/management/change_wipe_code.py index ecc09fbca..91664c9c6 100644 --- a/core/src/apps/management/change_wipe_code.py +++ b/core/src/apps/management/change_wipe_code.py @@ -101,6 +101,7 @@ def _require_confirm_action( async def _request_wipe_code_confirm(ctx: Context, pin: str) -> str: from apps.common.request_pin import request_pin from trezor.ui.layouts import ( + confirm_reenter_pin, pin_mismatch_popup, wipe_code_same_as_pin_popup, ) @@ -110,6 +111,7 @@ async def _request_wipe_code_confirm(ctx: Context, pin: str) -> str: if code1 == pin: await wipe_code_same_as_pin_popup(ctx) continue + await confirm_reenter_pin(ctx, is_wipe_code=True) code2 = await request_pin(ctx, "Re-enter wipe code") if code1 == code2: return code1 diff --git a/core/src/apps/management/recovery_device/layout.py b/core/src/apps/management/recovery_device/layout.py index c3a260aa6..d2ff42c28 100644 --- a/core/src/apps/management/recovery_device/layout.py +++ b/core/src/apps/management/recovery_device/layout.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING from trezor.enums import ButtonRequestType +from trezor.ui.layouts import confirm_action from trezor.ui.layouts.recovery import ( # noqa: F401 request_word_count, show_group_share_success, @@ -17,8 +18,6 @@ if TYPE_CHECKING: async def _confirm_abort(ctx: GenericContext, dry_run: bool = False) -> None: - from trezor.ui.layouts import confirm_action - if dry_run: await confirm_action( ctx, @@ -45,8 +44,11 @@ async def request_mnemonic( from . import word_validity from trezor.ui.layouts.common import button_request from trezor.ui.layouts.recovery import request_word + from trezor.ui.layouts import mnemonic_word_entering + + await mnemonic_word_entering(ctx) - await button_request(ctx, "mnemonic", ButtonRequestType.MnemonicInput) + await button_request(ctx, "mnemonic", code=ButtonRequestType.MnemonicInput) words: list[str] = [] for i in range(word_count): diff --git a/core/src/apps/management/show_tutorial.py b/core/src/apps/management/show_tutorial.py new file mode 100644 index 000000000..ae46c47f2 --- /dev/null +++ b/core/src/apps/management/show_tutorial.py @@ -0,0 +1,17 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from trezor.messages import ShowDeviceTutorial, Success + from trezor.wire import Context + + +async def show_tutorial(ctx: Context, msg: ShowDeviceTutorial) -> Success: + from trezor.messages import Success + + # NOTE: tutorial is defined only for TR, and this function should + # also be called only in case of TR + from trezor.ui.layouts import tutorial + + await tutorial(ctx) + + return Success(message="Tutorial shown") diff --git a/core/src/apps/workflow_handlers.py b/core/src/apps/workflow_handlers.py index 057450b29..82966d2f2 100644 --- a/core/src/apps/workflow_handlers.py +++ b/core/src/apps/workflow_handlers.py @@ -47,11 +47,14 @@ def _find_message_handler_module(msg_type: int) -> str: return "apps.management.change_pin" if msg_type == MessageType.ChangeWipeCode: return "apps.management.change_wipe_code" - elif msg_type == MessageType.GetNonce: + if msg_type == MessageType.GetNonce: return "apps.management.get_nonce" - elif msg_type == MessageType.RebootToBootloader: + if msg_type == MessageType.RebootToBootloader: return "apps.management.reboot_to_bootloader" + if utils.MODEL in ("R",) and msg_type == MessageType.ShowDeviceTutorial: + return "apps.management.show_tutorial" + if utils.USE_SD_CARD and msg_type == MessageType.SdProtect: return "apps.management.sd_protect" diff --git a/core/src/storage/device.py b/core/src/storage/device.py index 8213a38da..fd935aac7 100644 --- a/core/src/storage/device.py +++ b/core/src/storage/device.py @@ -196,7 +196,7 @@ def get_passphrase_always_on_device() -> bool: from trezor import utils # Some models do not support passphrase input on device - if utils.MODEL in ("1", "R"): + if utils.MODEL in ("1",): return False return common.get_bool(_NAMESPACE, _PASSPHRASE_ALWAYS_ON_DEVICE) diff --git a/core/src/trezor/enums/DebugPhysicalButton.py b/core/src/trezor/enums/DebugPhysicalButton.py new file mode 100644 index 000000000..e10b03def --- /dev/null +++ b/core/src/trezor/enums/DebugPhysicalButton.py @@ -0,0 +1,7 @@ +# Automatically generated by pb2py +# fmt: off +# isort:skip_file + +LEFT_BTN = 0 +MIDDLE_BTN = 1 +RIGHT_BTN = 2 diff --git a/core/src/trezor/enums/MessageType.py b/core/src/trezor/enums/MessageType.py index ebd1e6847..75b9228f0 100644 --- a/core/src/trezor/enums/MessageType.py +++ b/core/src/trezor/enums/MessageType.py @@ -46,6 +46,7 @@ GetFirmwareHash = 88 FirmwareHash = 89 UnlockPath = 93 UnlockedPathRequest = 94 +ShowDeviceTutorial = 95 FirmwareErase = 6 FirmwareUpload = 7 FirmwareRequest = 8 diff --git a/core/src/trezor/enums/__init__.py b/core/src/trezor/enums/__init__.py index c83b8b88c..7d4e516aa 100644 --- a/core/src/trezor/enums/__init__.py +++ b/core/src/trezor/enums/__init__.py @@ -63,6 +63,7 @@ if TYPE_CHECKING: FirmwareHash = 89 UnlockPath = 93 UnlockedPathRequest = 94 + ShowDeviceTutorial = 95 SetU2FCounter = 63 GetNextU2FCounter = 80 NextU2FCounter = 81 @@ -456,6 +457,11 @@ if TYPE_CHECKING: YES = 1 INFO = 2 + class DebugPhysicalButton(IntEnum): + LEFT_BTN = 0 + MIDDLE_BTN = 1 + RIGHT_BTN = 2 + class EthereumDefinitionType(IntEnum): NETWORK = 0 TOKEN = 1 diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index 1d70c614c..e90bb501b 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -35,6 +35,7 @@ if TYPE_CHECKING: from trezor.enums import CardanoTxSigningMode # noqa: F401 from trezor.enums import CardanoTxWitnessType # noqa: F401 from trezor.enums import DebugButton # noqa: F401 + from trezor.enums import DebugPhysicalButton # noqa: F401 from trezor.enums import DebugSwipeDirection # noqa: F401 from trezor.enums import DecredStakingSpendType # noqa: F401 from trezor.enums import EthereumDataType # noqa: F401 @@ -2611,6 +2612,12 @@ if TYPE_CHECKING: def is_type_of(cls, msg: Any) -> TypeGuard["UnlockedPathRequest"]: return isinstance(msg, cls) + class ShowDeviceTutorial(protobuf.MessageType): + + @classmethod + def is_type_of(cls, msg: Any) -> TypeGuard["ShowDeviceTutorial"]: + return isinstance(msg, cls) + class DebugLinkDecision(protobuf.MessageType): button: "DebugButton | None" swipe: "DebugSwipeDirection | None" @@ -2619,6 +2626,7 @@ if TYPE_CHECKING: y: "int | None" wait: "bool | None" hold_ms: "int | None" + physical_button: "DebugPhysicalButton | None" def __init__( self, @@ -2630,6 +2638,7 @@ if TYPE_CHECKING: y: "int | None" = None, wait: "bool | None" = None, hold_ms: "int | None" = None, + physical_button: "DebugPhysicalButton | None" = None, ) -> None: pass diff --git a/core/src/trezor/strings.py b/core/src/trezor/strings.py index 58918f547..c26414c5b 100644 --- a/core/src/trezor/strings.py +++ b/core/src/trezor/strings.py @@ -21,12 +21,10 @@ def format_amount(amount: int, decimals: int) -> str: return s -if False: # noqa - - def format_ordinal(number: int) -> str: - return str(number) + {1: "st", 2: "nd", 3: "rd"}.get( - 4 if 10 <= number % 100 < 20 else number % 10, "th" - ) +def format_ordinal(number: int) -> str: + return str(number) + {1: "st", 2: "nd", 3: "rd"}.get( + 4 if 10 <= number % 100 < 20 else number % 10, "th" + ) def format_plural(string: str, count: int, plural: str) -> str: diff --git a/core/src/trezor/ui/layouts/fido.py b/core/src/trezor/ui/layouts/fido.py index 49d21b2de..00810aa85 100644 --- a/core/src/trezor/ui/layouts/fido.py +++ b/core/src/trezor/ui/layouts/fido.py @@ -1 +1,6 @@ -from .tt_v2.fido import * # noqa: F401,F403 +from trezor import utils + +if utils.MODEL in ("T",): + from .tt_v2.fido import * # noqa: F401,F403 +elif utils.MODEL in ("R",): + from .tr.fido import * # noqa: F401,F403 diff --git a/core/src/trezor/ui/layouts/homescreen.py b/core/src/trezor/ui/layouts/homescreen.py index 0ca2c462d..229c9c7cc 100644 --- a/core/src/trezor/ui/layouts/homescreen.py +++ b/core/src/trezor/ui/layouts/homescreen.py @@ -1 +1,6 @@ -from .tt_v2.homescreen import * # noqa: F401,F403 +from trezor import utils + +if utils.MODEL in ("T",): + from .tt_v2.homescreen import * # noqa: F401,F403 +elif utils.MODEL in ("R",): + from .tr.homescreen import * # noqa: F401,F403 diff --git a/core/src/trezor/ui/layouts/progress.py b/core/src/trezor/ui/layouts/progress.py index f61db8b56..6f5f6756c 100644 --- a/core/src/trezor/ui/layouts/progress.py +++ b/core/src/trezor/ui/layouts/progress.py @@ -1 +1,6 @@ -from .tt_v2.progress import * # noqa: F401,F403 +from trezor import utils + +if utils.MODEL in ("T",): + from .tt_v2.progress import * # noqa: F401,F403 +elif utils.MODEL in ("R",): + from .tr.progress import * # noqa: F401,F403 diff --git a/core/src/trezor/ui/layouts/recovery.py b/core/src/trezor/ui/layouts/recovery.py index 1866e3002..adcd40786 100644 --- a/core/src/trezor/ui/layouts/recovery.py +++ b/core/src/trezor/ui/layouts/recovery.py @@ -1 +1,6 @@ -from .tt_v2.recovery import * # noqa: F401,F403 +from trezor import utils + +if utils.MODEL in ("T",): + from .tt_v2.recovery import * # noqa: F401,F403 +elif utils.MODEL in ("R",): + from .tr.recovery import * # noqa: F401,F403 diff --git a/core/src/trezor/ui/layouts/reset.py b/core/src/trezor/ui/layouts/reset.py index 4e611559b..538af3ed5 100644 --- a/core/src/trezor/ui/layouts/reset.py +++ b/core/src/trezor/ui/layouts/reset.py @@ -1 +1,6 @@ -from .tt_v2.reset import * # noqa: F401,F403 +from trezor import utils + +if utils.MODEL in ("T",): + from .tt_v2.reset import * # noqa: F401,F403 +elif utils.MODEL in ("R",): + from .tr.reset import * # noqa: F401,F403 diff --git a/core/src/trezor/ui/layouts/tr/__init__.py b/core/src/trezor/ui/layouts/tr/__init__.py index 7812ac429..ffe6210cd 100644 --- a/core/src/trezor/ui/layouts/tr/__init__.py +++ b/core/src/trezor/ui/layouts/tr/__init__.py @@ -1,25 +1,33 @@ -from typing import TYPE_CHECKING, Sequence +from typing import TYPE_CHECKING -from trezor import io, log, loop, ui, wire, workflow +from trezor import io, loop, ui from trezor.enums import ButtonRequestType -from trezor.utils import DISABLE_ANIMATION +from trezor.wire import ActionCancelled import trezorui2 from ..common import button_request, interact if TYPE_CHECKING: - from typing import Any, NoReturn, Type + from typing import Any, NoReturn, Awaitable, Iterable, Sequence, TypeVar - ExceptionType = BaseException | Type[BaseException] + from trezor.wire import GenericContext, Context + from ..common import PropertyType, ExceptionType + T = TypeVar("T") -if __debug__: - trezorui2.disable_animation(bool(DISABLE_ANIMATION)) + +CONFIRMED = trezorui2.CONFIRMED +CANCELLED = trezorui2.CANCELLED +INFO = trezorui2.INFO + +BR_TYPE_OTHER = ButtonRequestType.Other # global_import_cache -def is_confirmed(x: Any) -> bool: - return x is trezorui2.CONFIRMED +if __debug__: + from trezor.utils import DISABLE_ANIMATION + + trezorui2.disable_animation(bool(DISABLE_ANIMATION)) class RustLayout(ui.Layout): @@ -27,19 +35,191 @@ class RustLayout(ui.Layout): def __init__(self, layout: Any): self.layout = layout self.timer = loop.Timer() - self.layout.set_timer_fn(self.set_timer) + self.layout.attach_timer_fn(self.set_timer) def set_timer(self, token: int, deadline: int) -> None: self.timer.schedule(deadline, token) - def create_tasks(self) -> tuple[loop.Task, ...]: - return self.handle_input_and_rendering(), self.handle_timers() + def request_complete_repaint(self) -> None: + msg = self.layout.request_complete_repaint() + assert msg is None + + def _paint(self) -> None: + import storage.cache as storage_cache + + painted = self.layout.paint() + + ui.refresh() + if storage_cache.homescreen_shown is not None and painted: + storage_cache.homescreen_shown = None + + if __debug__: + from trezor.enums import DebugPhysicalButton + + def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: + return ( + self.handle_input_and_rendering(), + self.handle_timers(), + self.handle_swipe_signal(), + self.handle_button_signal(), + self.handle_result_signal(), + ) + + async def handle_result_signal(self) -> None: + """Enables sending arbitrary input - ui.Result. + + Waits for `result_signal` and carries it out. + """ + from apps.debug import result_signal + from storage import debug as debug_storage + + while True: + event_id, result = await result_signal() + # Layout change will be notified in _first_paint of the next layout + debug_storage.new_layout_event_id = event_id + raise ui.Result(result) + + def read_content_into(self, content_store: list[str]) -> None: + """Reads all the strings/tokens received from Rust into given list.""" + + def callback(*args: Any) -> None: + for arg in args: + content_store.append(str(arg)) + + content_store.clear() + self.layout.trace(callback) + + async def _press_left(self, hold_ms: int | None) -> Any: + """Triggers left button press.""" + self.layout.button_event(io.BUTTON_PRESSED, io.BUTTON_LEFT) + self._paint() + if hold_ms is not None: + await loop.sleep(hold_ms) + return self.layout.button_event(io.BUTTON_RELEASED, io.BUTTON_LEFT) + + async def _press_right(self, hold_ms: int | None) -> Any: + """Triggers right button press.""" + self.layout.button_event(io.BUTTON_PRESSED, io.BUTTON_RIGHT) + self._paint() + if hold_ms is not None: + await loop.sleep(hold_ms) + return self.layout.button_event(io.BUTTON_RELEASED, io.BUTTON_RIGHT) + + async def _press_middle(self, hold_ms: int | None) -> Any: + """Triggers middle button press.""" + self.layout.button_event(io.BUTTON_PRESSED, io.BUTTON_LEFT) + self._paint() + self.layout.button_event(io.BUTTON_PRESSED, io.BUTTON_RIGHT) + self._paint() + if hold_ms is not None: + await loop.sleep(hold_ms) + self.layout.button_event(io.BUTTON_RELEASED, io.BUTTON_LEFT) + self._paint() + return self.layout.button_event(io.BUTTON_RELEASED, io.BUTTON_RIGHT) + + async def _press_button( + self, + event_id: int | None, + btn_to_press: DebugPhysicalButton, + hold_ms: int | None, + ) -> Any: + from trezor.enums import DebugPhysicalButton + from trezor import workflow + from apps.debug import notify_layout_change + from storage import debug as debug_storage + + if btn_to_press == DebugPhysicalButton.LEFT_BTN: + msg = await self._press_left(hold_ms) + elif btn_to_press == DebugPhysicalButton.MIDDLE_BTN: + msg = await self._press_middle(hold_ms) + elif btn_to_press == DebugPhysicalButton.RIGHT_BTN: + msg = await self._press_right(hold_ms) + else: + raise Exception(f"Unknown button: {btn_to_press}") + + if msg is not None: + # Layout change will be notified in _first_paint of the next layout + debug_storage.new_layout_event_id = event_id + raise ui.Result(msg) + + # So that these presses will keep trezor awake + # (it will not be locked after auto_lock_delay_ms) + workflow.idle_timer.touch() + + self._paint() + notify_layout_change(self, event_id) + + async def _swipe(self, event_id: int | None, direction: int) -> None: + """Triggers swipe in the given direction. + + Only `UP` and `DOWN` directions are supported. + """ + from trezor.enums import DebugPhysicalButton, DebugSwipeDirection + + if direction == DebugSwipeDirection.UP: + btn_to_press = DebugPhysicalButton.RIGHT_BTN + elif direction == DebugSwipeDirection.DOWN: + btn_to_press = DebugPhysicalButton.LEFT_BTN + else: + raise Exception(f"Unsupported direction: {direction}") + + await self._press_button(event_id, btn_to_press, None) + + async def handle_swipe_signal(self) -> None: + """Enables pagination through the current page/flow page. + + Waits for `swipe_signal` and carries it out. + """ + from apps.debug import swipe_signal + + while True: + event_id, direction = await swipe_signal() + await self._swipe(event_id, direction) + + async def handle_button_signal(self) -> None: + """Enables clicking arbitrary of the three buttons. + + Waits for `button_signal` and carries it out. + """ + from apps.debug import button_signal + + while True: + event_id, btn, hold_ms = await button_signal() + await self._press_button(event_id, btn, hold_ms) + + else: + + def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: + return self.handle_timers(), self.handle_input_and_rendering() + + def _first_paint(self) -> None: + # Clear the screen of any leftovers. + ui.display.clear() + self._paint() + + if __debug__ and self.should_notify_layout_change: + from apps.debug import notify_layout_change + from storage import debug as debug_storage + + # notify about change and do not notify again until next await. + # (handle_rendering might be called multiple times in a single await, + # because of the endless loop in __iter__) + self.should_notify_layout_change = False + + # Possibly there is an event ID that caused the layout change, + # so notifying with this ID. + event_id = None + if debug_storage.new_layout_event_id is not None: + event_id = debug_storage.new_layout_event_id + debug_storage.new_layout_event_id = None + + notify_layout_change(self, event_id) def handle_input_and_rendering(self) -> loop.Task: # type: ignore [awaitable-is-generator] + from trezor import workflow + button = loop.wait(io.BUTTON) - ui.display.clear() - self.layout.paint() - ui.refresh() + self._first_paint() while True: # Using `yield` instead of `await` to avoid allocations. event, button_num = yield button @@ -49,8 +229,7 @@ class RustLayout(ui.Layout): msg = self.layout.button_event(event, button_num) if msg is not None: raise ui.Result(msg) - self.layout.paint() - ui.refresh() + self._paint() def handle_timers(self) -> loop.Task: # type: ignore [awaitable-is-generator] while True: @@ -59,232 +238,1070 @@ class RustLayout(ui.Layout): msg = self.layout.timer(token) if msg is not None: raise ui.Result(msg) - self.layout.paint() - ui.refresh() + self._paint() + def page_count(self) -> int: + """How many paginated pages current screen has.""" + return self.layout.page_count() -async def confirm_action( - ctx: wire.GenericContext, + +def draw_simple(layout: Any) -> None: + # Simple drawing not supported for layouts that set timers. + def dummy_set_timer(token: int, deadline: int) -> None: + raise RuntimeError + + layout.attach_timer_fn(dummy_set_timer) + ui.display.clear() + layout.paint() + ui.refresh() + + +# Temporary function, so we know where it is used +# Should be gradually replaced by custom designs/layouts +async def _placeholder_confirm( + ctx: GenericContext, br_type: str, title: str, - action: str | None = None, + data: str | None = None, description: str | None = None, - description_param: str | None = None, + *, verb: str = "CONFIRM", - verb_cancel: str | None = None, + verb_cancel: str | None = "", hold: bool = False, - reverse: bool = False, - exc: ExceptionType = wire.ActionCancelled, - br_code: ButtonRequestType = ButtonRequestType.Other, -) -> None: - if verb_cancel is not None: - verb_cancel = verb_cancel.upper() - - if description is not None and description_param is not None: - description = description.format(description_param) + br_code: ButtonRequestType = BR_TYPE_OTHER, +) -> Any: + return await confirm_action( + ctx, + br_type, + title.upper(), + data, + description, + verb=verb, + verb_cancel=verb_cancel, + hold=hold, + reverse=True, + br_code=br_code, + ) - if hold: - log.error(__name__, "confirm_action hold not implemented") +async def get_bool( + ctx: GenericContext, + br_type: str, + title: str, + data: str | None = None, + description: str | None = None, + verb: str = "CONFIRM", + verb_cancel: str | None = "", + hold: bool = False, + br_code: ButtonRequestType = BR_TYPE_OTHER, +) -> bool: result = await interact( ctx, RustLayout( trezorui2.confirm_action( title=title.upper(), - action=action, + action=data, description=description, verb=verb, verb_cancel=verb_cancel, hold=hold, - reverse=reverse, ) ), br_type, br_code, ) - if not is_confirmed(result): + + return result is CONFIRMED + + +async def raise_if_not_confirmed(a: Awaitable[T], exc: Any = ActionCancelled) -> T: + result = await a + if result is not CONFIRMED: raise exc + return result -async def confirm_text( - ctx: wire.GenericContext, +async def confirm_action( + ctx: GenericContext, br_type: str, title: str, - data: str, + action: str | None = None, description: str | None = None, - br_code: ButtonRequestType = ButtonRequestType.Other, + description_param: str | None = None, + verb: str = "CONFIRM", + verb_cancel: str | None = "", + hold: bool = False, + hold_danger: bool = False, + reverse: bool = False, + exc: ExceptionType = ActionCancelled, + br_code: ButtonRequestType = BR_TYPE_OTHER, ) -> None: - result = await interact( - ctx, - RustLayout( - trezorui2.confirm_text( - title=title.upper(), - data=data, - description=description, - ) + if verb_cancel is not None: + verb_cancel = verb_cancel.upper() + + if description is not None and description_param is not None: + description = description.format(description_param) + + await raise_if_not_confirmed( + interact( + ctx, + RustLayout( + trezorui2.confirm_action( + title=title.upper(), + action=action, + description=description, + verb=verb.upper(), + verb_cancel=verb_cancel, + hold=hold, + reverse=reverse, + ) + ), + br_type, + br_code, ), - br_type, - br_code, + exc, ) - if not is_confirmed(result): - raise wire.ActionCancelled -async def show_success( - ctx: wire.GenericContext, - br_type: str, - content: str, +async def confirm_reset_device( + ctx: GenericContext, + title: str, + recovery: bool = False, ) -> None: - result = await interact( + if recovery: + button = "RECOVER WALLET" + else: + button = "CREATE WALLET" + + # Title sent from general code would be too long + # TODO: support two lines title for TR + if recovery: + title = "WALLET RECOVERY" + else: + title = "WALLET CREATION" + + await raise_if_not_confirmed( + interact( + ctx, + RustLayout( + trezorui2.confirm_reset_device( + title=title, + button=button, + ) + ), + "recover_device" if recovery else "setup_device", + ButtonRequestType.ProtectCall + if recovery + else ButtonRequestType.ResetDevice, + ) + ) + + +# TODO cleanup @ redesign +async def confirm_backup(ctx: GenericContext) -> bool: + if await get_bool( ctx, - RustLayout( - trezorui2.confirm_text( - title="Success", - data=content, - description="", - ) - ), - br_type, - br_code=ButtonRequestType.Other, + "backup_device", + "SUCCESS", + description="New wallet has been created.\nIt should be backed up now!", + verb="BACK UP", + verb_cancel="SKIP", + br_code=ButtonRequestType.ResetDevice, + ): + return True + + return await get_bool( + ctx, + "backup_device", + "WARNING", + "Are you sure you want to skip the backup?\n", + "You can back up your Trezor once, at any time.", + verb="BACK UP", + verb_cancel="SKIP", + br_code=ButtonRequestType.ResetDevice, + ) + + +async def confirm_path_warning( + ctx: GenericContext, + path: str, + path_type: str | None = None, +) -> None: + if path_type: + title = f"Unknown {path_type}" + else: + title = "Unknown path" + return await _placeholder_confirm( + ctx, + "path_warning", + title.upper(), + description=path, + br_code=ButtonRequestType.UnknownDerivationPath, + ) + + +async def confirm_homescreen( + ctx: GenericContext, + image: bytes, +) -> None: + # TODO: show homescreen preview? + await confirm_action( + ctx, + "set_homescreen", + "Set homescreen", + description="Do you really want to set new homescreen image?", + br_code=ButtonRequestType.ProtectCall, + ) + + +def _show_xpub(xpub: str, title: str, cancel: str | None) -> ui.Layout: + return RustLayout( + trezorui2.confirm_blob( + title=title.upper(), + data=xpub, + verb_cancel=cancel, + description=None, + extra=None, + ) + ) + + +async def show_xpub(ctx: GenericContext, xpub: str, title: str) -> None: + await raise_if_not_confirmed( + interact( + ctx, + _show_xpub(xpub, title, None), + "show_xpub", + ButtonRequestType.PublicKey, + ) ) - if not is_confirmed(result): - raise wire.ActionCancelled async def show_address( - ctx: wire.GenericContext, + ctx: GenericContext, address: str, *, - case_sensitive: bool = True, address_qr: str | None = None, - title: str = "Confirm address", + case_sensitive: bool = True, + path: str | None = None, + account: str | None = None, network: str | None = None, multisig_index: int | None = None, xpubs: Sequence[str] = (), ) -> None: - result = await interact( - ctx, - RustLayout( - trezorui2.confirm_text( - title="ADDRESS", + send_button_request = True + # Will be a marquee in case of multisig + title = ( + "RECEIVE ADDRESS (MULTISIG)" + if multisig_index is not None + else "RECEIVE ADDRESS" + ) + while True: + layout = RustLayout( + trezorui2.confirm_address( + title=title, data=address, - description="Confirm address", + description="", # unused on TR + extra=None, # unused on TR ) - ), - "show_address", - ButtonRequestType.Address, + ) + if send_button_request: + send_button_request = False + await button_request( + ctx, + "show_address", + ButtonRequestType.Address, + pages=layout.page_count(), + ) + result = await ctx.wait(layout) + + # User confirmed with middle button. + if result is CONFIRMED: + break + + # User pressed right button, go to address details. + elif result is INFO: + + def xpub_title(i: int) -> str: + # Will be marquee (cannot fit one line) + result = f"MULTISIG XPUB #{i + 1}" + result += " (YOURS)" if i == multisig_index else " (COSIGNER)" + return result + + result = await ctx.wait( + RustLayout( + trezorui2.show_address_details( + address=address if address_qr is None else address_qr, + case_sensitive=case_sensitive, + account=account, + path=path, + xpubs=[(xpub_title(i), xpub) for i, xpub in enumerate(xpubs)], + ) + ), + ) + # Can only go back from the address details. + assert result is CANCELLED + + # User pressed left cancel button, show mismatch dialogue. + else: + result = await ctx.wait(RustLayout(trezorui2.show_mismatch())) + assert result in (CONFIRMED, CANCELLED) + # Right button aborts action, left goes back to showing address. + if result is CONFIRMED: + raise ActionCancelled + + +def show_pubkey( + ctx: Context, pubkey: str, title: str = "Confirm public key" +) -> Awaitable[None]: + return confirm_blob( + ctx, + "show_pubkey", + title.upper(), + pubkey, + br_code=ButtonRequestType.PublicKey, + ) + + +async def _show_modal( + ctx: GenericContext, + br_type: str, + header: str, + subheader: str | None, + content: str, + button_confirm: str | None, + button_cancel: str | None, + br_code: ButtonRequestType, + exc: ExceptionType = ActionCancelled, +) -> None: + await confirm_action( + ctx, + br_type, + header.upper(), + subheader, + content, + verb=button_confirm or "", + verb_cancel=button_cancel, + exc=exc, + br_code=br_code, + ) + + +async def show_error_and_raise( + ctx: GenericContext, + br_type: str, + content: str, + header: str = "Error", + subheader: str | None = None, + button: str = "Close", + red: bool = False, # unused on TR + exc: ExceptionType = ActionCancelled, +) -> NoReturn: + await _show_modal( + ctx, + br_type, + header, + subheader, + content, + button_confirm=None, + button_cancel=button, + br_code=BR_TYPE_OTHER, + exc=exc, + ) + raise exc + + +def show_warning( + ctx: GenericContext, + br_type: str, + content: str, + subheader: str | None = None, + button: str = "Try again", + br_code: ButtonRequestType = ButtonRequestType.Warning, +) -> Awaitable[None]: + return _show_modal( + ctx, + br_type, + "", + subheader or "WARNING", + content, + button_confirm=button, + button_cancel=None, + br_code=br_code, + ) + + +def show_success( + ctx: GenericContext, + br_type: str, + content: str, + subheader: str | None = None, + button: str = "Continue", +) -> Awaitable[None]: + title = "Success" + + # In case only subheader is supplied, showing it + # in regular font, not bold. + if not content and subheader: + content = subheader + subheader = None + + # Special case for Shamir backup - to show everything just on one page + # in regular font. + if "Continue with" in content: + content = f"{subheader}\n{content}" + subheader = None + title = "" + + return _show_modal( + ctx, + br_type, + title, + subheader, + content, + button_confirm=button, + button_cancel=None, + br_code=ButtonRequestType.Success, ) - if not is_confirmed(result): - raise wire.ActionCancelled async def confirm_output( - ctx: wire.GenericContext, + ctx: GenericContext, address: str, amount: str, title: str = "Confirm sending", + hold: bool = False, br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput, address_label: str | None = None, + output_index: int | None = None, +) -> None: + address_title = ( + "RECIPIENT" if output_index is None else f"RECIPIENT #{output_index + 1}" + ) + amount_title = "AMOUNT" if output_index is None else f"AMOUNT #{output_index + 1}" + + # TODO: implement `hold` to be consistent with `TT`? + # TODO: incorporate label? - label = f" ({address_label})" if address_label else "" + + await raise_if_not_confirmed( + interact( + ctx, + RustLayout( + trezorui2.confirm_output( + address=address, + address_title=address_title, + amount_title=amount_title, + amount=amount, + ) + ), + "confirm_output", + br_code, + ) + ) + + +async def tutorial( + ctx: GenericContext, + br_code: ButtonRequestType = BR_TYPE_OTHER, ) -> None: - label = f" ({address_label})" if address_label else "" + """Showing users how to interact with the device.""" + await interact( + ctx, + RustLayout(trezorui2.tutorial()), + "tutorial", + br_code, + ) + + +async def confirm_payment_request( + ctx: GenericContext, + recipient_name: str, + amount: str, + memos: list[str], +) -> Any: + memos_str = "\n".join(memos) + return await _placeholder_confirm( + ctx, + "confirm_payment_request", + "CONFIRM SENDING", + description=f"{amount} to\n{recipient_name}\n{memos_str}", + br_code=ButtonRequestType.ConfirmOutput, + ) + + +async def should_show_more( + ctx: GenericContext, + title: str, + para: Iterable[tuple[int, str]], + button_text: str = "Show all", + br_type: str = "should_show_more", + br_code: ButtonRequestType = BR_TYPE_OTHER, + confirm: str | bytes | None = None, +) -> bool: + """Return True if the user wants to show more (they click a special button) + and False when the user wants to continue without showing details. + + Raises ActionCancelled if the user cancels. + """ + if confirm is None or not isinstance(confirm, str): + confirm = "CONFIRM" + result = await interact( ctx, RustLayout( - trezorui2.confirm_text( - title=title, - data=f"Send {amount} to {address}{label}?", - description="Confirm Output", + trezorui2.confirm_with_info( + title=title.upper(), + items=para, + button=confirm.upper(), + info_button=button_text.upper(), ) ), - "confirm_output", + br_type, br_code, ) - if not is_confirmed(result): - raise wire.ActionCancelled + + if result is CONFIRMED: + return False + elif result is INFO: + return True + else: + assert result is CANCELLED + raise ActionCancelled + + +async def confirm_blob( + ctx: GenericContext, + br_type: str, + title: str, + data: bytes | str, + description: str | None = None, + hold: bool = False, + br_code: ButtonRequestType = BR_TYPE_OTHER, + ask_pagination: bool = False, +) -> None: + title = title.upper() + description = description or "" + layout = RustLayout( + trezorui2.confirm_blob( + title=title, + description=description, + data=data, + extra=None, + hold=hold, + ) + ) + + await raise_if_not_confirmed( + interact( + ctx, + layout, + br_type, + br_code, + ) + ) + + +async def confirm_address( + ctx: GenericContext, + title: str, + address: str, + description: str | None = "Address:", + br_type: str = "confirm_address", + br_code: ButtonRequestType = BR_TYPE_OTHER, +) -> Awaitable[None]: + return confirm_blob( + ctx, + br_type, + title.upper(), + address, + description, + br_code=br_code, + ) + + +async def confirm_text( + ctx: GenericContext, + br_type: str, + title: str, + data: str, + description: str | None = None, + br_code: ButtonRequestType = BR_TYPE_OTHER, +) -> Any: + return await _placeholder_confirm( + ctx, + br_type, + title, + data, + description, + br_code=br_code, + ) + + +def confirm_amount( + ctx: GenericContext, + title: str, + amount: str, + description: str = "Amount:", + br_type: str = "confirm_amount", + br_code: ButtonRequestType = BR_TYPE_OTHER, +) -> Awaitable[None]: + return confirm_blob( + ctx, + br_type, + title.upper(), + amount, + description, + br_code=br_code, + ) + + +async def confirm_properties( + ctx: GenericContext, + br_type: str, + title: str, + props: Iterable[PropertyType], + hold: bool = False, + br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput, +) -> None: + from ubinascii import hexlify + + def handle_bytes(prop: PropertyType): + if isinstance(prop[1], bytes): + return (prop[0], hexlify(prop[1]).decode(), True) + else: + # When there is not space in the text, taking it as data + # to not include hyphens + is_data = prop[1] and " " not in prop[1] + return (prop[0], prop[1], is_data) + + await raise_if_not_confirmed( + interact( + ctx, + RustLayout( + trezorui2.confirm_properties( + title=title.upper(), + items=map(handle_bytes, props), # type: ignore [cannot be assigned to parameter "items"] + hold=hold, + ) + ), + br_type, + br_code, + ) + ) + + +def confirm_value( + ctx: GenericContext, + title: str, + value: str, + description: str, + br_type: str, + br_code: ButtonRequestType = BR_TYPE_OTHER, + *, + verb: str | None = None, + hold: bool = False, +) -> Awaitable[None]: + """General confirmation dialog, used by many other confirm_* functions.""" + + if not verb and not hold: + raise ValueError("Either verb or hold=True must be set") + + return raise_if_not_confirmed( + interact( + ctx, + RustLayout( + trezorui2.confirm_value( # type: ignore [Argument missing for parameter "subtitle"] + title=title.upper(), + description=description, + value=value, + verb=verb or "HOLD TO CONFIRM", + hold=hold, + ) + ), + br_type, + br_code, + ) + ) async def confirm_total( - ctx: wire.GenericContext, + ctx: GenericContext, total_amount: str, fee_amount: str, fee_rate_amount: str | None = None, - title: str = "Confirm transaction", - total_label: str = "Total amount:\n", - fee_label: str = "\nincluding fee:\n", + title: str = "SENDING", + total_label: str = "TOTAL AMOUNT", + fee_label: str = "INCLUDING FEE", account_label: str | None = None, br_type: str = "confirm_total", br_code: ButtonRequestType = ButtonRequestType.SignTx, ) -> None: - result = await interact( - ctx, - RustLayout( - trezorui2.confirm_text( - title=title, - data=f"{total_label}{total_amount}\n{fee_label}{fee_amount}", - description="Confirm Output", - ) - ), - br_type, - br_code, + # TODO: incorporate account_label + # f"From {account_label}\r\n{total_label}" if account_label else total_label, + await raise_if_not_confirmed( + interact( + ctx, + RustLayout( + # TODO: resolve these differences in TT's and TR's confirm_total + trezorui2.confirm_total( # type: ignore [Arguments missing] + total_amount=total_amount, # type: ignore [No parameter named] + fee_amount=fee_amount, # type: ignore [No parameter named] + fee_rate_amount=fee_rate_amount, # type: ignore [No parameter named] + total_label=total_label.upper(), # type: ignore [No parameter named] + fee_label=fee_label.upper(), # type: ignore [No parameter named] + ) + ), + br_type, + br_code, + ) ) - if not is_confirmed(result): - raise wire.ActionCancelled -async def confirm_blob( - ctx: wire.GenericContext, +async def confirm_joint_total( + ctx: GenericContext, spending_amount: str, total_amount: str +) -> None: + + await raise_if_not_confirmed( + interact( + ctx, + RustLayout( + trezorui2.confirm_joint_total( + spending_amount=spending_amount, + total_amount=total_amount, + ) + ), + "confirm_joint_total", + ButtonRequestType.SignTx, + ) + ) + + +async def confirm_metadata( + ctx: GenericContext, br_type: str, title: str, - data: bytes | str, - description: str | None = None, + content: str, + param: str | None = None, + br_code: ButtonRequestType = ButtonRequestType.SignTx, hold: bool = False, - br_code: ButtonRequestType = ButtonRequestType.Other, - ask_pagination: bool = False, ) -> None: - result = await interact( + await _placeholder_confirm( ctx, + br_type, + title.upper(), + description=content.format(param), + hold=hold, + br_code=br_code, + ) + + +async def confirm_replacement(ctx: GenericContext, description: str, txid: str) -> None: + await confirm_value( + ctx, + description.upper(), + txid, + "Confirm transaction ID:", + "confirm_replacement", + ButtonRequestType.SignTx, + verb="CONFIRM", + ) + + +async def confirm_modify_output( + ctx: GenericContext, + address: str, + sign: int, + amount_change: str, + amount_new: str, +) -> None: + await raise_if_not_confirmed( + interact( + ctx, + RustLayout( + trezorui2.confirm_modify_output( + address=address, + sign=sign, + amount_change=amount_change, + amount_new=amount_new, + ) + ), + "modify_output", + ButtonRequestType.ConfirmOutput, + ) + ) + + +async def confirm_modify_fee( + ctx: GenericContext, + sign: int, + user_fee_change: str, + total_fee_new: str, + fee_rate_amount: str | None = None, +) -> None: + await raise_if_not_confirmed( + interact( + ctx, + RustLayout( + trezorui2.confirm_modify_fee( + sign=sign, + user_fee_change=user_fee_change, + total_fee_new=total_fee_new, + fee_rate_amount=fee_rate_amount, + ) + ), + "modify_fee", + ButtonRequestType.SignTx, + ) + ) + + +async def confirm_coinjoin( + ctx: GenericContext, max_rounds: int, max_fee_per_vbyte: str +) -> None: + await raise_if_not_confirmed( + interact( + ctx, + RustLayout( + trezorui2.confirm_coinjoin( + max_rounds=str(max_rounds), + max_feerate=max_fee_per_vbyte, + ) + ), + "coinjoin_final", + BR_TYPE_OTHER, + ) + ) + + +# TODO cleanup @ redesign +async def confirm_sign_identity( + ctx: GenericContext, proto: str, identity: str, challenge_visual: str | None +) -> None: + text = "" + if challenge_visual: + text += f"{challenge_visual}\n\n" + text += identity + + await _placeholder_confirm( + ctx, + "confirm_sign_identity", + f"Sign {proto}".upper(), + text, + br_code=BR_TYPE_OTHER, + ) + + +async def confirm_signverify( + ctx: GenericContext, coin: str, message: str, address: str, verify: bool +) -> None: + if verify: + header = f"Verify {coin} message" + br_type = "verify_message" + else: + header = f"Sign {coin} message" + br_type = "sign_message" + + await confirm_blob( + ctx, + br_type, + header.upper(), + address, + "Confirm address:", + br_code=BR_TYPE_OTHER, + ) + + await confirm_value( + ctx, + header.upper(), + message, + "Confirm message:", + br_type, + BR_TYPE_OTHER, + verb="CONFIRM", + ) + + +async def show_error_popup( + title: str, + description: str, + subtitle: str | None = None, + description_param: str = "", + *, + button: str = "", + timeout_ms: int = 0, +) -> None: + if button: + raise NotImplementedError("Button not implemented") + description = description.format(description_param) + if subtitle: + description = f"{subtitle}\n{description}" + await RustLayout( + trezorui2.show_info( + title=title, + description=description, + time_ms=timeout_ms, + ) + ) + + +def request_passphrase_on_host() -> None: + draw_simple( + trezorui2.show_info( + title="HIDDEN WALLET", + description="Please type your passphrase on the connected host.", + ) + ) + + +async def request_passphrase_on_device(ctx: GenericContext, max_len: int) -> str: + await button_request( + ctx, "passphrase_device", code=ButtonRequestType.PassphraseEntry + ) + + result = await ctx.wait( RustLayout( - trezorui2.confirm_text( - title=title, - data=str(data), - description=description, + trezorui2.request_passphrase( + prompt="ENTER PASSPHRASE", + max_len=max_len, ) - ), - br_type, - br_code, + ) ) - if not is_confirmed(result): - raise wire.ActionCancelled + if result is CANCELLED: + raise ActionCancelled("Passphrase entry cancelled") + + assert isinstance(result, str) + return result async def request_pin_on_device( - ctx: wire.GenericContext, + ctx: GenericContext, prompt: str, attempts_remaining: int | None, allow_cancel: bool, + wrong_pin: bool = False, ) -> str: + from trezor import wire + + # Not showing the prompt in case user did not enter it badly yet + # (has full 16 attempts left) + if attempts_remaining is None or attempts_remaining == 16: + subprompt = "" + elif attempts_remaining == 1: + subprompt = "Last attempt" + else: + subprompt = f"{attempts_remaining} tries left" + await button_request(ctx, "pin_device", code=ButtonRequestType.PinEntry) - # TODO: this should not be callable on TR - return "1234" + dialog = RustLayout( + trezorui2.request_pin( + prompt=prompt, + subprompt=subprompt, + allow_cancel=allow_cancel, + wrong_pin=wrong_pin, + ) + ) + result = await ctx.wait(dialog) + if result is CANCELLED: + raise wire.PinCancelled + assert isinstance(result, str) + return result -async def show_error_and_raise( - ctx: wire.GenericContext, - br_type: str, - content: str, - subheader: str | None = None, - button: str = "Close", - exc: ExceptionType = wire.ActionCancelled, -) -> NoReturn: - raise NotImplementedError + +async def confirm_reenter_pin( + ctx: GenericContext, + is_wipe_code: bool = False, +) -> None: + br_type = "reenter_wipe_code" if is_wipe_code else "reenter_pin" + title = "CHECK WIPE CODE" if is_wipe_code else "CHECK PIN" + return await confirm_action( + ctx, + br_type, + title, + action="Please re-enter to confirm.", + verb="BEGIN", + br_code=BR_TYPE_OTHER, + ) -async def show_error_popup( +async def pin_mismatch_popup( + ctx: GenericContext, + is_wipe_code: bool = False, +) -> None: + title = "WIPE CODE MISMATCH" if is_wipe_code else "PIN MISMATCH" + description = "wipe codes" if is_wipe_code else "PINs" + return await confirm_action( + ctx, + "pin_mismatch", + title, + description=f"The {description} you entered do not match.\nPlease try again.", + verb="TRY AGAIN", + verb_cancel=None, + br_code=BR_TYPE_OTHER, + ) + + +async def wipe_code_same_as_pin_popup( + ctx: GenericContext, + is_wipe_code: bool = False, +) -> None: + return await confirm_action( + ctx, + "wipe_code_same_as_pin", + "INVALID WIPE CODE", + description="The wipe code must be different from your PIN.\nPlease try again.", + verb="TRY AGAIN", + verb_cancel=None, + br_code=BR_TYPE_OTHER, + ) + + +async def confirm_set_new_pin( + ctx: GenericContext, + br_type: str, title: str, description: str, - subtitle: str | None = None, - description_param: str = "", - timeout_ms: int = 3000, + information: list[str], + br_code: ButtonRequestType = BR_TYPE_OTHER, ) -> None: - raise NotImplementedError + await confirm_action( + ctx, + br_type, + title, + description=description, + verb="ENABLE", + br_code=br_code, + ) + + # Additional information for the user to know about PIN/WIPE CODE + + if "wipe_code" in br_type: + verb = "HODL TO BEGIN" # Easter egg from @Hannsek + else: + information.append( + "Position of individual numbers will change between entries for enhanced security." + ) + verb = "HOLD TO BEGIN" + + return await confirm_action( + ctx, + br_type, + "", + description="\n\r".join(information), + verb=verb, + hold=True, + br_code=br_code, + ) + + +async def mnemonic_word_entering(ctx: GenericContext) -> None: + await confirm_action( + ctx, + "request_word", + "WORD ENTERING", + description="You'll only have to select the first 2-3 letters.", + verb="CONTINUE", + verb_cancel=None, + br_code=ButtonRequestType.MnemonicInput, + ) diff --git a/core/src/trezor/ui/layouts/tr/fido.py b/core/src/trezor/ui/layouts/tr/fido.py new file mode 100644 index 000000000..27ee589a9 --- /dev/null +++ b/core/src/trezor/ui/layouts/tr/fido.py @@ -0,0 +1,60 @@ +from typing import TYPE_CHECKING + +from trezor.enums import ButtonRequestType + +import trezorui2 + +from ..common import interact +from . import RustLayout + +if TYPE_CHECKING: + from trezor.wire import GenericContext + + +async def confirm_fido( + ctx: GenericContext | None, + header: str, + app_name: str, + icon_name: str | None, + accounts: list[str | None], +) -> int: + """Webauthn confirmation for one or more credentials.""" + confirm = RustLayout( + trezorui2.confirm_fido( # type: ignore [Arguments missing] + app_name=app_name, + accounts=accounts, + ) + ) + + if ctx is None: + result = await confirm + else: + result = await interact(ctx, confirm, "confirm_fido", ButtonRequestType.Other) + + # The Rust side returns either an int or `CANCELLED`. We detect the int situation + # and assume cancellation otherwise. + if isinstance(result, int): + return result + + # For the usage in device tests, assuming CONFIRMED (sent by debuglink) + # is choosing the first credential. + if __debug__ and result is trezorui2.CONFIRMED: + return 0 + + # Late import won't get executed on the happy path. + from trezor.wire import ActionCancelled + + raise ActionCancelled + + +async def confirm_fido_reset() -> bool: + confirm = RustLayout( + trezorui2.confirm_action( + title="FIDO2 RESET", + description="Do you really want to erase all credentials?", + action=None, + verb_cancel="", + verb="CONFIRM", + ) + ) + return (await confirm) is trezorui2.CONFIRMED diff --git a/core/src/trezor/ui/layouts/tr/homescreen.py b/core/src/trezor/ui/layouts/tr/homescreen.py new file mode 100644 index 000000000..455a8a21e --- /dev/null +++ b/core/src/trezor/ui/layouts/tr/homescreen.py @@ -0,0 +1,125 @@ +from typing import TYPE_CHECKING + +import storage.cache as storage_cache +from trezor import ui + +import trezorui2 + +from . import RustLayout + +if TYPE_CHECKING: + from trezor import loop + from typing import Any, Tuple + + +class HomescreenBase(RustLayout): + RENDER_INDICATOR: object | None = None + + def __init__(self, layout: Any) -> None: + super().__init__(layout=layout) + + def _paint(self) -> None: + self.layout.paint() + ui.refresh() + + def _first_paint(self) -> None: + if storage_cache.homescreen_shown is not self.RENDER_INDICATOR: + super()._first_paint() + storage_cache.homescreen_shown = self.RENDER_INDICATOR + else: + self._paint() + + +class Homescreen(HomescreenBase): + RENDER_INDICATOR = storage_cache.HOMESCREEN_ON + + def __init__( + self, + label: str | None, + notification: str | None, + notification_is_error: bool, + hold_to_lock: bool, + ) -> None: + level = 1 + if notification is not None: + notification = notification.rstrip("!") + if "EXPERIMENTAL" in notification: + level = 2 + elif notification_is_error: + level = 0 + + skip = storage_cache.homescreen_shown is self.RENDER_INDICATOR + super().__init__( + layout=trezorui2.show_homescreen( + label=label, + notification=notification, + notification_level=level, + hold=hold_to_lock, + skip_first_paint=skip, + ), + ) + + async def usb_checker_task(self) -> None: + from trezor import io, loop + + usbcheck = loop.wait(io.USB_CHECK) + while True: + is_connected = await usbcheck + self.layout.usb_event(is_connected) + self.layout.paint() + ui.refresh() + + def create_tasks(self) -> Tuple[loop.AwaitableTask, ...]: + return super().create_tasks() + (self.usb_checker_task(),) + + +class Lockscreen(HomescreenBase): + RENDER_INDICATOR = storage_cache.LOCKSCREEN_ON + + def __init__( + self, + label: str | None, + bootscreen: bool = False, + ) -> None: + self.bootscreen = bootscreen + skip = ( + not bootscreen and storage_cache.homescreen_shown is self.RENDER_INDICATOR + ) + super().__init__( + layout=trezorui2.show_lockscreen( + label=label, + bootscreen=bootscreen, + skip_first_paint=skip, + ), + ) + + async def __iter__(self) -> Any: + result = await super().__iter__() + if self.bootscreen: + self.request_complete_repaint() + return result + + +class Busyscreen(HomescreenBase): + RENDER_INDICATOR = storage_cache.BUSYSCREEN_ON + + def __init__(self, delay_ms: int) -> None: + skip = storage_cache.homescreen_shown is self.RENDER_INDICATOR + super().__init__( + layout=trezorui2.show_progress_coinjoin( + title="Waiting for others", + indeterminate=True, + time_ms=delay_ms, + skip_first_paint=skip, + ) + ) + + async def __iter__(self) -> Any: + from apps.base import set_homescreen + + # Handle timeout. + result = await super().__iter__() + assert result == trezorui2.CANCELLED + storage_cache.delete(storage_cache.APP_COMMON_BUSY_DEADLINE_MS) + set_homescreen() + return result diff --git a/core/src/trezor/ui/layouts/tr/progress.py b/core/src/trezor/ui/layouts/tr/progress.py new file mode 100644 index 000000000..7a6fbccfb --- /dev/null +++ b/core/src/trezor/ui/layouts/tr/progress.py @@ -0,0 +1,72 @@ +from typing import TYPE_CHECKING + +from trezor import ui + +import trezorui2 + +if TYPE_CHECKING: + from typing import Any + + from ..common import ProgressLayout + + +class RustProgress: + def __init__( + self, + layout: Any, + ): + self.layout = layout + ui.display.clear() + self.layout.attach_timer_fn(self.set_timer) + self.layout.paint() + + def set_timer(self, token: int, deadline: int) -> None: + raise RuntimeError # progress layouts should not set timers + + def report(self, value: int, description: str | None = None): + msg = self.layout.progress_event(value, description or "") + assert msg is None + self.layout.paint() + ui.refresh() + + +def progress( + message: str = "PLEASE WAIT", + description: str | None = None, + indeterminate: bool = False, +) -> ProgressLayout: + return RustProgress( + layout=trezorui2.show_progress( + title=message.upper(), + indeterminate=indeterminate, + description=description or "", + ) + ) + + +def bitcoin_progress(message: str) -> ProgressLayout: + return progress(message) + + +def coinjoin_progress(message: str) -> ProgressLayout: + # TODO: create show_progress_coinjoin for TR + return progress(message, description="Coinjoin") + # return RustProgress( + # layout=trezorui2.show_progress_coinjoin(title=message, indeterminate=False) + # ) + + +def pin_progress(message: str, description: str) -> ProgressLayout: + return progress(message, description=description) + + +def monero_keyimage_sync_progress() -> ProgressLayout: + return progress("SYNCING") + + +def monero_live_refresh_progress() -> ProgressLayout: + return progress("REFRESHING", indeterminate=True) + + +def monero_transaction_progress_inner() -> ProgressLayout: + return progress("SIGNING TRANSACTION") diff --git a/core/src/trezor/ui/layouts/tr/recovery.py b/core/src/trezor/ui/layouts/tr/recovery.py new file mode 100644 index 000000000..e654c2d91 --- /dev/null +++ b/core/src/trezor/ui/layouts/tr/recovery.py @@ -0,0 +1,114 @@ +from typing import TYPE_CHECKING + +from trezor.enums import ButtonRequestType + +import trezorui2 + +from ..common import button_request, interact +from . import RustLayout, raise_if_not_confirmed, show_warning + +if TYPE_CHECKING: + from trezor.wire import GenericContext + from typing import Iterable, Callable + + +async def request_word_count(ctx: GenericContext, dry_run: bool) -> int: + await button_request(ctx, "word_count", code=ButtonRequestType.MnemonicWordCount) + count = await interact( + ctx, + RustLayout(trezorui2.select_word_count(dry_run=dry_run)), + "word_count", + ButtonRequestType.MnemonicWordCount, + ) + # It can be returning a string (for example for __debug__ in tests) + return int(count) + + +async def request_word( + ctx: GenericContext, word_index: int, word_count: int, is_slip39: bool +) -> str: + prompt = f"WORD {word_index + 1} OF {word_count}" + + if is_slip39: + word_choice = RustLayout(trezorui2.request_slip39(prompt=prompt)) + else: + word_choice = RustLayout(trezorui2.request_bip39(prompt=prompt)) + + word: str = await ctx.wait(word_choice) + return word + + +async def show_remaining_shares( + ctx: GenericContext, + groups: Iterable[tuple[int, tuple[str, ...]]], # remaining + list 3 words + shares_remaining: list[int], + group_threshold: int, +) -> None: + raise NotImplementedError + + +async def show_group_share_success( + ctx: GenericContext, share_index: int, group_index: int +) -> None: + await raise_if_not_confirmed( + interact( + ctx, + RustLayout( + trezorui2.show_group_share_success( + lines=[ + "You have entered", + f"Share {share_index + 1}", + "from", + f"Group {group_index + 1}", + ], + ) + ), + "share_success", + ButtonRequestType.Other, + ) + ) + + +async def continue_recovery( + ctx: GenericContext, + button_label: str, + text: str, + subtext: str | None, + info_func: Callable | None, + dry_run: bool, +) -> bool: + # TODO: implement info_func? + # There is very limited space on the screen + # (and having middle button would mean shortening the right button text) + + description = text + if subtext: + description += f"\n\n{subtext}" + + homepage = RustLayout( + trezorui2.confirm_recovery( + title="", + description=description, + button=button_label.upper(), + info_button=False, + dry_run=dry_run, + ) + ) + result = await interact( + ctx, + homepage, + "recovery", + ButtonRequestType.RecoveryHomepage, + ) + return result is trezorui2.CONFIRMED + + +async def show_recovery_warning( + ctx: GenericContext, + br_type: str, + content: str, + subheader: str | None = None, + button: str = "TRY AGAIN", + br_code: ButtonRequestType = ButtonRequestType.Warning, +) -> None: + await show_warning(ctx, br_type, content, subheader, button, br_code) diff --git a/core/src/trezor/ui/layouts/tr/reset.py b/core/src/trezor/ui/layouts/tr/reset.py new file mode 100644 index 000000000..f34cb5271 --- /dev/null +++ b/core/src/trezor/ui/layouts/tr/reset.py @@ -0,0 +1,286 @@ +from typing import TYPE_CHECKING + +from trezor.enums import ButtonRequestType +from trezor.wire import ActionCancelled + +import trezorui2 + +from ..common import interact +from . import RustLayout, confirm_action + +CONFIRMED = trezorui2.CONFIRMED # global_import_cache + +if TYPE_CHECKING: + from trezor.wire import GenericContext + from trezor.enums import BackupType + from typing import Sequence + + +async def show_share_words( + ctx: GenericContext, + share_words: Sequence[str], + share_index: int | None = None, + group_index: int | None = None, +) -> None: + from . import get_bool + + if share_index is None: + title = "RECOVERY SEED" + elif group_index is None: + title = f"SHARE #{share_index + 1}" + else: + title = f"G{group_index + 1} - SHARE {share_index + 1}" + + # Showing words, asking for write down confirmation and preparing for check + # until user accepts everything. + while True: + result = await interact( + ctx, + RustLayout( + trezorui2.show_share_words( # type: ignore [Argument missing for parameter "pages"] + title=title, + share_words=share_words, # type: ignore [No parameter named "share_words"] + ) + ), + "backup_words", + ButtonRequestType.ResetDevice, + ) + if result is not CONFIRMED: + raise ActionCancelled + + if share_index is None: + check_title = "CHECK BACKUP" + elif group_index is None: + check_title = f"CHECK SHARE #{share_index + 1}" + else: + check_title = f"GROUP {group_index + 1} - SHARE {share_index + 1}" + + if await get_bool( + ctx, + "backup_words", + check_title, + None, + "Select correct word for each position.", + verb_cancel="SEE AGAIN", + verb="BEGIN", + br_code=ButtonRequestType.ResetDevice, + ): + # All went well, we can break the loop. + break + + +async def select_word( + ctx: GenericContext, + words: Sequence[str], + share_index: int | None, + checked_index: int, + count: int, + group_index: int | None = None, +) -> str: + from trezor.strings import format_ordinal + + # It may happen (with a very low probability) + # that there will be less than three unique words to choose from. + # In that case, duplicating the last word to make it three. + words = list(words) + while len(words) < 3: + words.append(words[-1]) + + result = await ctx.wait( + RustLayout( + trezorui2.select_word( + title="", + description=f"SELECT {format_ordinal(checked_index + 1).upper()} WORD", + words=(words[0].lower(), words[1].lower(), words[2].lower()), + ) + ) + ) + if __debug__ and isinstance(result, str): + return result + assert isinstance(result, int) and 0 <= result <= 2 + return words[result] + + +async def slip39_show_checklist( + ctx: GenericContext, step: int, backup_type: BackupType +) -> None: + from trezor.enums import BackupType + + assert backup_type in (BackupType.Slip39_Basic, BackupType.Slip39_Advanced) + + items = ( + ( + "Number of shares", + "Set threshold", + "Write down and check all shares", + ) + if backup_type == BackupType.Slip39_Basic + else ( + "Number of groups", + "Number of shares", + "Set sizes and thresholds", + ) + ) + + result = await interact( + ctx, + RustLayout( + trezorui2.show_checklist( + title="BACKUP CHECKLIST", + button="CONTINUE", + active=step, + items=items, + ) + ), + "slip39_checklist", + ButtonRequestType.ResetDevice, + ) + if result is not CONFIRMED: + raise ActionCancelled + + +async def _prompt_number( + ctx: GenericContext, + title: str, + count: int, + min_count: int, + max_count: int, + br_name: str, +) -> int: + num_input = RustLayout( + trezorui2.request_number( + title=title.upper(), + count=count, + min_count=min_count, + max_count=max_count, + ) + ) + + result = await interact( + ctx, + num_input, + br_name, + ButtonRequestType.ResetDevice, + ) + + return int(result) + + +async def slip39_prompt_threshold( + ctx: GenericContext, num_of_shares: int, group_id: int | None = None +) -> int: + await confirm_action( + ctx, + "slip39_prompt_threshold", + "Threshold", + description="= number of shares needed for recovery", + verb="BEGIN", + verb_cancel=None, + ) + + count = num_of_shares // 2 + 1 + # min value of share threshold is 2 unless the number of shares is 1 + # number of shares 1 is possible in advanced slip39 + min_count = min(2, num_of_shares) + max_count = num_of_shares + + if group_id is not None: + title = f"THRESHOLD - GROUP {group_id + 1}" + else: + title = "SET THRESHOLD" + + return await _prompt_number( + ctx, + title, + count, + min_count, + max_count, + "slip39_threshold", + ) + + +async def slip39_prompt_number_of_shares( + ctx: GenericContext, group_id: int | None = None +) -> int: + await confirm_action( + ctx, + "slip39_shares", + "Number of shares", + description="= total number of unique word lists used for wallet backup", + verb="BEGIN", + verb_cancel=None, + ) + + count = 5 + min_count = 1 + max_count = 16 + + if group_id is not None: + title = f"# SHARES - GROUP {group_id + 1}" + else: + title = "NUMBER OF SHARES" + + return await _prompt_number( + ctx, + title, + count, + min_count, + max_count, + "slip39_shares", + ) + + +async def slip39_advanced_prompt_number_of_groups(ctx: GenericContext) -> int: + count = 5 + min_count = 2 + max_count = 16 + + return await _prompt_number( + ctx, + "NUMBER OF GROUPS", + count, + min_count, + max_count, + "slip39_groups", + ) + + +async def slip39_advanced_prompt_group_threshold( + ctx: GenericContext, num_of_groups: int +) -> int: + count = num_of_groups // 2 + 1 + min_count = 1 + max_count = num_of_groups + + return await _prompt_number( + ctx, + "GROUP THRESHOLD", + count, + min_count, + max_count, + "slip39_group_threshold", + ) + + +async def show_warning_backup(ctx: GenericContext, slip39: bool) -> None: + await confirm_action( + ctx, + "backup_warning", + "SHAMIR BACKUP" if slip39 else "WALLET BACKUP", + description="You can use your backup to recover your wallet at any time.", + verb="HOLD TO BEGIN", + hold=True, + br_code=ButtonRequestType.ResetDevice, + ) + + +async def show_success_backup(ctx: GenericContext) -> None: + await confirm_action( + ctx, + "success_backup", + "BACKUP IS DONE", + description="Keep it safe!", + verb="CONTINUE", + verb_cancel=None, + br_code=ButtonRequestType.Success, + ) diff --git a/core/src/trezor/ui/layouts/tt_v2/__init__.py b/core/src/trezor/ui/layouts/tt_v2/__init__.py index b61eab57a..1806538f6 100644 --- a/core/src/trezor/ui/layouts/tt_v2/__init__.py +++ b/core/src/trezor/ui/layouts/tt_v2/__init__.py @@ -1181,6 +1181,14 @@ async def request_pin_on_device( return result +async def confirm_reenter_pin( + ctx: GenericContext, + is_wipe_code: bool = False, +) -> None: + """Not supported for TT.""" + pass + + async def pin_mismatch_popup( ctx: GenericContext, is_wipe_code: bool = False, @@ -1220,3 +1228,8 @@ async def confirm_set_new_pin( verb="ENABLE", br_code=br_code, ) + + +async def mnemonic_word_entering(ctx: GenericContext) -> None: + """Not supported for TT.""" + pass diff --git a/core/tools/codegen/fonts/Unifont-Bold.otf b/core/tools/codegen/fonts/Unifont-Bold.otf new file mode 100644 index 000000000..384a58b99 Binary files /dev/null and b/core/tools/codegen/fonts/Unifont-Bold.otf differ diff --git a/core/tools/codegen/fonts/Unifont-Regular.otf b/core/tools/codegen/fonts/Unifont-Regular.otf new file mode 100644 index 000000000..b58f18291 Binary files /dev/null and b/core/tools/codegen/fonts/Unifont-Regular.otf differ diff --git a/core/tools/codegen/gen_font.py b/core/tools/codegen/gen_font.py index ba95462da..230d86ed5 100755 --- a/core/tools/codegen/gen_font.py +++ b/core/tools/codegen/gen_font.py @@ -226,3 +226,7 @@ process_face("RobotoMono", "Medium", 20) process_face("PixelOperator", "Regular", 8, bpp=1, shaveX=1) process_face("PixelOperator", "Bold", 8, bpp=1, shaveX=1) process_face("PixelOperatorMono", "Regular", 8, bpp=1, shaveX=1) + +# For model R +process_face("Unifont", "Regular", 16, bpp=1, shaveX=1, ext="otf") +process_face("Unifont", "Bold", 16, bpp=1, shaveX=1, ext="otf") diff --git a/core/tools/rust_api_check.py b/core/tools/rust_api_check.py index a662d9ee2..072a9baa8 100755 --- a/core/tools/rust_api_check.py +++ b/core/tools/rust_api_check.py @@ -95,7 +95,7 @@ def _get_all_qstr_code_types(file: Path) -> dict[str, dict[str, str]]: # There could be a default value default = None if "unwrap_or_else" in one_line: - default_match = re.search(r"unwrap_or_else\(\|_\|\s+(.*?)\)", one_line) + default_match = re.search(r"unwrap_or_else\(\|_?\|\s+(.*?)\)", one_line) if default_match: default = default_match.group(1) else: diff --git a/docs/ci/jobs.md b/docs/ci/jobs.md index 9e6ee1690..f26c76a02 100644 --- a/docs/ci/jobs.md +++ b/docs/ci/jobs.md @@ -51,7 +51,7 @@ or contain `[no changelog]` in the commit message. ## BUILD stage - [build.yml](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml) All builds are published as artifacts so they can be downloaded and used. -Consists of **25 jobs** below: +Consists of **29 jobs** below: ### [core fw regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L20) Build of Core into firmware. Regular version. @@ -71,68 +71,76 @@ Build of Core into firmware. Bitcoin-only version. ### [core fw btconly production build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L117) -### [core unix regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L138) +### [core fw R debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L136) + +### [core fw R build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L152) + +### [core unix regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L169) Non-frozen emulator build. This means you still need Python files present which get interpreted. -### [core unix regular asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L150) +### [core unix regular asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L181) -### [core unix frozen regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L169) +### [core unix frozen regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L200) Build of Core into UNIX emulator. Something you can run on your laptop. Frozen version. That means you do not need any other files to run it, it is just a single binary file that you can execute directly. -### [core unix frozen btconly debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L186) +### [core unix frozen btconly debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L217) Build of Core into UNIX emulator. Something you can run on your laptop. Frozen version. That means you do not need any other files to run it, it is just a single binary file that you can execute directly. See [Emulator](../core/emulator/index.md) for more info. Debug mode enabled, Bitcoin-only version. -### [core unix frozen btconly debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L202) +### [core unix frozen btconly debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L233) -### [core unix frozen debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L225) +### [core unix frozen debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L256) Build of Core into UNIX emulator. Something you can run on your laptop. Frozen version. That means you do not need any other files to run it, it is just a single binary file that you can execute directly. **Are you looking for a Trezor T emulator? This is most likely it.** -### [core unix frozen debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L238) +### [core unix frozen R debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L269) + +### [core unix frozen R debug build arm](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L283) + +### [core unix frozen debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L301) -### [core unix frozen debug build arm](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L254) +### [core unix frozen debug build arm](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L317) -### [core macos frozen regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L276) +### [core macos frozen regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L339) -### [crypto build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L301) +### [crypto build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L364) Build of our cryptographic library, which is then incorporated into the other builds. -### [legacy fw regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L331) +### [legacy fw regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L394) -### [legacy fw regular debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L347) +### [legacy fw regular debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L410) -### [legacy fw btconly build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L364) +### [legacy fw btconly build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L427) -### [legacy fw btconly debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L383) +### [legacy fw btconly debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L446) -### [legacy emu regular debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L404) +### [legacy emu regular debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L467) Regular version (not only Bitcoin) of above. **Are you looking for a Trezor One emulator? This is most likely it.** -### [legacy emu regular debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L419) +### [legacy emu regular debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L482) -### [legacy emu regular debug build arm](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L437) +### [legacy emu regular debug build arm](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L500) -### [legacy emu btconly debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L463) +### [legacy emu btconly debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L526) Build of Legacy into UNIX emulator. Use keyboard arrows to emulate button presses. Bitcoin-only version. -### [legacy emu btconly debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L480) +### [legacy emu btconly debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L543) --- ## TEST stage - [test.yml](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml) All the tests run test cases on the freshly built emulators from the previous `BUILD` stage. -Consists of **33 jobs** below: +Consists of **35 jobs** below: ### [core unit python test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L15) Python unit tests, checking core functionality. @@ -148,73 +156,79 @@ with the expected UI result. See artifacts for a comprehensive report of UI. See [docs/tests/ui-tests](../tests/ui-tests.md) for more info. -### [core device asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L84) +### [core device R test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L84) -### [core btconly device test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L103) +### [core device asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L115) + +### [core btconly device test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L134) Device tests excluding altcoins, only for BTC. -### [core btconly device asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L123) +### [core btconly device asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L154) -### [core monero test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L144) +### [core monero test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L175) Monero tests. -### [core monero asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L164) +### [core monero asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L195) -### [core u2f test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L187) +### [core u2f test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L218) Tests for U2F and HID. -### [core u2f asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L206) +### [core u2f asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L237) -### [core fido2 test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L224) +### [core fido2 test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L255) FIDO2 device tests. -### [core fido2 asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L247) +### [core fido2 asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L278) -### [core click test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L267) +### [core click test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L298) Click tests - UI. See [docs/tests/click-tests](../tests/click-tests.md) for more info. -### [core click asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L296) +### [core click R test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L329) +Click tests. +See [docs/tests/click-tests](../tests/click-tests.md) for more info. + +### [core click asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L357) -### [core upgrade test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L317) +### [core upgrade test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L378) Upgrade tests. See [docs/tests/upgrade-tests](../tests/upgrade-tests.md) for more info. -### [core upgrade asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L336) +### [core upgrade asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L397) -### [core persistence test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L358) +### [core persistence test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L419) Persistence tests - UI. -### [core persistence asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L387) +### [core persistence asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L448) -### [core hwi test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L405) +### [core hwi test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L466) -### [crypto test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L424) +### [crypto test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L485) -### [legacy device test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L456) +### [legacy device test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L517) Legacy device test - UI. -### [legacy asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L483) +### [legacy asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L544) -### [legacy btconly test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L495) +### [legacy btconly test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L556) -### [legacy btconly asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L515) +### [legacy btconly asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L576) -### [legacy upgrade test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L530) +### [legacy upgrade test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L591) -### [legacy upgrade asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L549) +### [legacy upgrade asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L610) -### [legacy hwi test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L570) +### [legacy hwi test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L631) -### [python test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L590) +### [python test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L651) -### [python support test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L609) +### [python support test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L670) -### [storage test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L619) +### [storage test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L680) -### [core unix memory profiler](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L643) +### [core unix memory profiler](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L704) -### [connect test core](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L667) +### [connect test core](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L728) --- ## TEST-HW stage - [test-hw.yml](https://github.com/trezor/trezor-firmware/blob/master/ci/test-hw.yml) @@ -285,8 +299,8 @@ Consists of **13 jobs** below: ### [ui tests fixtures deploy](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L229) -### [sync emulators to aws](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L253) +### [sync emulators to aws](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L254) -### [common sync](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L278) +### [common sync](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L279) --- diff --git a/docs/tests/device-tests.md b/docs/tests/device-tests.md index 195dffb37..0947d6286 100644 --- a/docs/tests/device-tests.md +++ b/docs/tests/device-tests.md @@ -105,9 +105,11 @@ the following marker: This marker must be registered in `REGISTERED_MARKERS` file in `tests` folder. -If you wish to run a test only on TT, mark it with `@pytest.mark.skip_t1`. -If the test should only run on T1, mark it with `@pytest.mark.skip_t2`. -You must not use both on the same test. +Tests can be run only for specific models - it is done by disallowing the tests for the other models. +`@pytest.mark.skip_t1` +`@pytest.mark.skip_t2` +`@pytest.mark.skip_tr` +are valid markers to skip current test for T1, TT and TR respectively. [pytest-random-order]: https://pypi.org/project/pytest-random-order/ diff --git a/legacy/firmware/protob/Makefile b/legacy/firmware/protob/Makefile index 3f6ec0d6c..35cad8d0f 100644 --- a/legacy/firmware/protob/Makefile +++ b/legacy/firmware/protob/Makefile @@ -7,7 +7,7 @@ SKIPPED_MESSAGES := Binance Cardano DebugMonero Eos Monero Ontology Ripple SdPro DebugLinkLayout DebugLinkResetDebugEvents GetNonce \ TxAckInput TxAckOutput TxAckPrev TxAckPaymentRequest \ EthereumSignTypedData EthereumTypedDataStructRequest EthereumTypedDataStructAck \ - EthereumTypedDataValueRequest EthereumTypedDataValueAck + EthereumTypedDataValueRequest EthereumTypedDataValueAck ShowDeviceTutorial ifeq ($(BITCOIN_ONLY), 1) SKIPPED_MESSAGES += Ethereum NEM Stellar diff --git a/python/.changelog.d/2795.added b/python/.changelog.d/2795.added new file mode 100644 index 000000000..a107b78e7 --- /dev/null +++ b/python/.changelog.d/2795.added @@ -0,0 +1 @@ +Add possibility to call tutorial flow diff --git a/python/.changelog.d/2967.added b/python/.changelog.d/2967.added new file mode 100644 index 000000000..c9ad38cc7 --- /dev/null +++ b/python/.changelog.d/2967.added @@ -0,0 +1 @@ +Add ability to change homescreen for Model R diff --git a/python/src/trezorlib/cli/device.py b/python/src/trezorlib/cli/device.py index 57f769213..be38a7d87 100644 --- a/python/src/trezorlib/cli/device.py +++ b/python/src/trezorlib/cli/device.py @@ -292,6 +292,13 @@ def reboot_to_bootloader(obj: "TrezorConnection") -> str: return device.reboot_to_bootloader(client) +@cli.command() +@with_client +def tutorial(client: "TrezorClient") -> str: + """Show on-device tutorial.""" + return device.show_device_tutorial(client) + + @cli.command() @click.argument("enable", type=ChoiceType({"on": True, "off": False}), required=False) @click.option( diff --git a/python/src/trezorlib/cli/settings.py b/python/src/trezorlib/cli/settings.py index e3021d97c..d7f98061a 100644 --- a/python/src/trezorlib/cli/settings.py +++ b/python/src/trezorlib/cli/settings.py @@ -39,6 +39,8 @@ SAFETY_LEVELS = { "prompt": messages.SafetyCheckLevel.PromptTemporarily, } +T1_TR_IMAGE_SIZE = (128, 64) + def image_to_t1(filename: Path) -> bytes: if not PIL_AVAILABLE: @@ -54,13 +56,56 @@ def image_to_t1(filename: Path) -> bytes: except Exception as e: raise click.ClickException("Failed to load image") from e - if image.size != (128, 64): - raise click.ClickException("Wrong size of the image - should be 128x64") + if image.size != T1_TR_IMAGE_SIZE: + if click.confirm( + f"Image is not 128x64, but {image.size}. Do you want to resize it automatically?", + default=True, + ): + image = image.resize(T1_TR_IMAGE_SIZE, Image.ANTIALIAS) + else: + raise click.ClickException("Wrong size of the image - should be 128x64") image = image.convert("1") return image.tobytes("raw", "1") +def image_to_tr(filename: Path) -> bytes: + if not PIL_AVAILABLE: + raise click.ClickException( + "Image library is missing. Please install via 'pip install Pillow'." + ) + + try: + image = Image.open(filename) + except Exception as e: + raise click.ClickException("Failed to load image") from e + + if image.size != T1_TR_IMAGE_SIZE: + if click.confirm( + f"Image is not 128x64, but {image.size}. Do you want to resize it automatically?", + default=True, + ): + image = image.resize(T1_TR_IMAGE_SIZE, Image.ANTIALIAS) + else: + raise click.ClickException("Wrong size of the image - should be 128x64") + + image = image.convert("1") # black-and-white + toif_image = toif.from_image(image) + return toif_image.to_bytes() + + +def image_to_tt(client: "TrezorClient", path: Path) -> bytes: + if client.features.homescreen_format == messages.HomescreenFormat.Jpeg240x240: + return image_to_jpeg_240x240(path) + elif client.features.homescreen_format in ( + messages.HomescreenFormat.Toif144x144, + None, + ): + return image_to_toif_144x144(path) + else: + raise click.ClickException("Unknown image format requested by the device.") + + def image_to_toif_144x144(filename: Path) -> bytes: if filename.suffix == ".toif": try: @@ -236,22 +281,12 @@ def homescreen(client: "TrezorClient", filename: str) -> str: if client.features.model == "1": img = image_to_t1(path) + elif client.features.model == "R": + img = image_to_tr(path) + elif client.features.model == "T": + img = image_to_tt(client, path) else: - if ( - client.features.homescreen_format - == messages.HomescreenFormat.Jpeg240x240 - ): - img = image_to_jpeg_240x240(path) - elif ( - client.features.homescreen_format - == messages.HomescreenFormat.Toif144x144 - or client.features.homescreen_format is None - ): - img = image_to_toif_144x144(path) - else: - raise click.ClickException( - "Unknown image format requested by the device." - ) + raise click.ClickException("Unknown device model") return device.apply_settings(client, homescreen=img) diff --git a/python/src/trezorlib/debuglink.py b/python/src/trezorlib/debuglink.py index 86efb87f5..60579ee5f 100644 --- a/python/src/trezorlib/debuglink.py +++ b/python/src/trezorlib/debuglink.py @@ -72,7 +72,7 @@ class UnstructuredJSONReader: self.json_str = json_str # We may not receive valid JSON, e.g. from an old model in upgrade tests try: - self.dict: AnyDict = json.loads(json_str) + self.dict: "AnyDict" = json.loads(json_str) except json.JSONDecodeError: self.dict = {} @@ -92,8 +92,17 @@ class UnstructuredJSONReader: return list(recursively_find(self.dict)) + def find_unique_object_with_key_and_value( + self, key: str, value: Any + ) -> Optional["AnyDict"]: + objects = self.find_objects_with_key_and_value(key, value) + if not objects: + return None + assert len(objects) == 1 + return objects[0] + def find_values_by_key( - self, key: str, only_type: Union[type, None] = None + self, key: str, only_type: Optional[type] = None ) -> List[Any]: def recursively_find(data: Any) -> Iterator[Any]: if isinstance(data, dict): @@ -113,7 +122,7 @@ class UnstructuredJSONReader: return values def find_unique_value_by_key( - self, key: str, default: Any, only_type: Union[type, None] = None + self, key: str, default: Any, only_type: Optional[type] = None ) -> Any: values = self.find_values_by_key(key, only_type=only_type) if not values: @@ -123,10 +132,7 @@ class UnstructuredJSONReader: class LayoutContent(UnstructuredJSONReader): - """Stores content of a layout as returned from Trezor. - - Contains helper functions to extract specific parts of the layout. - """ + """Contains helper functions to extract specific parts of the layout.""" def __init__(self, json_tokens: Sequence[str]) -> None: json_str = "".join(json_tokens) @@ -148,7 +154,7 @@ class LayoutContent(UnstructuredJSONReader): You are about to sign 3 actions. ******************** - Icon:cancel [Cancel], --- [None], CONFIRM [Confirm] + ICON_CANCEL, -, CONFIRM """ title_separator = f"\n{20*'-'}\n" btn_separator = f"\n{20*'*'}\n" @@ -188,31 +194,72 @@ class LayoutContent(UnstructuredJSONReader): def text_content(self) -> str: """What is on the screen, in one long string, so content can be - asserted regardless of newlines. + asserted regardless of newlines. Also getting rid of possible ellipsis. """ - return self.screen_content().replace("\n", " ") + content = self.screen_content().replace("\n", " ") + if content.endswith("..."): + content = content[:-3] + if content.startswith("..."): + content = content[3:] + return content def screen_content(self) -> str: """Getting text that is displayed in the main part of the screen. Preserving the line breaks. """ - main_text_blocks: List[str] = [] + # Look for paragraphs first (will match most of the time for TT) paragraphs = self.raw_content_paragraphs() - if not paragraphs: - return self.main_component() - for par in paragraphs: - par_content = "" - for line_or_newline in par: - par_content += line_or_newline - par_content.replace("\n", " ") - main_text_blocks.append(par_content) - return "\n".join(main_text_blocks) - - def raw_content_paragraphs(self) -> Union[List[List[str]], None]: + if paragraphs: + main_text_blocks: List[str] = [] + for par in paragraphs: + par_content = "" + for line_or_newline in par: + par_content += line_or_newline + par_content.replace("\n", " ") + main_text_blocks.append(par_content) + return "\n".join(main_text_blocks) + + # Formatted text + formatted_text = self.find_unique_object_with_key_and_value( + "component", "FormattedText" + ) + if formatted_text: + text_lines = formatted_text["text"] + return "".join(text_lines) + + # Check the choice_page - mainly for TR + choice_page = self.find_unique_object_with_key_and_value( + "component", "ChoicePage" + ) + if choice_page: + left = choice_page.get("prev_choice", {}).get("content", "") + middle = choice_page.get("current_choice", {}).get("content", "") + right = choice_page.get("next_choice", {}).get("content", "") + return " ".join(choice for choice in (left, middle, right) if choice) + + # Screen content - in TR share words + screen_content = self.find_unique_value_by_key( + "screen_content", default="", only_type=str + ) + if screen_content: + return screen_content + + # Flow page - for TR + flow_page = self.find_unique_value_by_key( + "flow_page", default={}, only_type=dict + ) + if flow_page: + text_lines = flow_page["text"] + return "".join(text_lines) + + # Default when not finding anything + return self.main_component() + + def raw_content_paragraphs(self) -> Optional[List[List[str]]]: """Getting raw paragraphs as sent from Rust.""" return self.find_unique_value_by_key("paragraphs", default=None, only_type=list) - def button_contents(self) -> List[str]: + def tt_check_seed_button_contents(self) -> List[str]: """Getting list of button contents.""" buttons: List[str] = [] button_objects = self.find_objects_with_key_and_value("component", "Button") @@ -223,6 +270,28 @@ class LayoutContent(UnstructuredJSONReader): buttons.append(button["text"]) return buttons + def button_contents(self) -> List[str]: + """Getting list of button contents.""" + buttons = self.find_unique_value_by_key("buttons", default={}, only_type=dict) + + def get_button_content(btn_key: str) -> str: + button_obj = buttons.get(btn_key, {}) + if button_obj.get("component") == "Button": + if "icon" in button_obj: + return button_obj["icon"] + elif "text" in button_obj: + return button_obj["text"] + elif button_obj.get("component") == "HoldToConfirm": + text = button_obj.get("loader", {}).get("text", "") + duration = button_obj.get("loader", {}).get("duration", "") + return f"{text} ({duration}ms)" + + # default value + return "-" + + button_keys = ("left_btn", "middle_btn", "right_btn") + return [get_button_content(btn_key) for btn_key in button_keys] + def seed_words(self) -> List[str]: """Get all the seed words on the screen in order. @@ -244,7 +313,7 @@ class LayoutContent(UnstructuredJSONReader): def passphrase(self) -> str: """Get passphrase from the layout.""" - assert self.main_component() == "PassphraseKeyboard" + assert "PassphraseKeyboard" in self.all_components() return self.find_unique_value_by_key("passphrase", default="", only_type=str) def page_count(self) -> int: @@ -265,6 +334,22 @@ class LayoutContent(UnstructuredJSONReader): """In what order the PIN buttons are shown on the screen. Only for TT.""" return self.top_level_value("digits_order") + def get_middle_choice(self) -> str: + """What is the choice being selected right now.""" + return self.choice_items()[1] + + def choice_items(self) -> Tuple[str, str, str]: + """Getting actions for all three possible buttons.""" + choice_obj = self.find_unique_value_by_key( + "choice_page", default={}, only_type=dict + ) + if not choice_obj: + raise RuntimeError("No choice_page object in trace") + choice_keys = ("prev_choice", "current_choice", "next_choice") + return tuple( + choice_obj.get(choice, {}).get("content", "") for choice in choice_keys + ) + def multipage_content(layouts: List[LayoutContent]) -> str: """Get overall content from multiple-page layout.""" @@ -362,7 +447,7 @@ class DebugLink: def reset_debug_events(self) -> None: # Only supported on TT and above certain version - if self.model == "T" and not self.legacy_debug: + if self.model in ("T", "R") and not self.legacy_debug: return self._call(messages.DebugLinkResetDebugEvents()) return None @@ -407,6 +492,7 @@ class DebugLink: self, word: Optional[str] = None, button: Optional[messages.DebugButton] = None, + physical_button: Optional[messages.DebugPhysicalButton] = None, swipe: Optional[messages.DebugSwipeDirection] = None, x: Optional[int] = None, y: Optional[int] = None, @@ -416,14 +502,21 @@ class DebugLink: if not self.allow_interactions: return None - args = sum(a is not None for a in (word, button, swipe, x)) + args = sum(a is not None for a in (word, button, physical_button, swipe, x)) if args != 1: raise ValueError( - "Invalid input - must use one of word, button, swipe, click(x,y)" + "Invalid input - must use one of word, button, physical_button, swipe, click(x,y)" ) decision = messages.DebugLinkDecision( - button=button, swipe=swipe, input=word, x=x, y=y, wait=wait, hold_ms=hold_ms + button=button, + physical_button=physical_button, + swipe=swipe, + input=word, + x=x, + y=y, + wait=wait, + hold_ms=hold_ms, ) ret = self._call(decision, nowait=not wait) @@ -463,8 +556,8 @@ class DebugLink: f.write(screen_content) f.write("\n" + 80 * "/" + "\n") - # Type overloads make sure that when we supply `wait=True` into `click()`, - # it will always return `LayoutContent` and we do not need to assert `is not None`. + # Type overloads below make sure that when we supply `wait=True` into functions, + # they will always return `LayoutContent` and we do not need to assert `is not None`. @overload def click(self, click: Tuple[int, int]) -> None: @@ -525,6 +618,57 @@ class DebugLink: def swipe_left(self, wait: bool = False) -> Union[LayoutContent, None]: return self.input(swipe=messages.DebugSwipeDirection.LEFT, wait=wait) + @overload + def press_left(self) -> None: + ... + + @overload + def press_left(self, wait: Literal[True]) -> LayoutContent: + ... + + def press_left(self, wait: bool = False) -> Optional[LayoutContent]: + return self.input( + physical_button=messages.DebugPhysicalButton.LEFT_BTN, wait=wait + ) + + @overload + def press_middle(self) -> None: + ... + + @overload + def press_middle(self, wait: Literal[True]) -> LayoutContent: + ... + + def press_middle(self, wait: bool = False) -> Optional[LayoutContent]: + return self.input( + physical_button=messages.DebugPhysicalButton.MIDDLE_BTN, wait=wait + ) + + @overload + def press_right(self) -> None: + ... + + @overload + def press_right(self, wait: Literal[True]) -> LayoutContent: + ... + + def press_right(self, wait: bool = False) -> Optional[LayoutContent]: + return self.input( + physical_button=messages.DebugPhysicalButton.RIGHT_BTN, wait=wait + ) + + def press_right_htc( + self, hold_ms: int, extra_ms: int = 200 + ) -> Optional[LayoutContent]: + hold_ms = hold_ms + extra_ms # safety margin + result = self.input( + physical_button=messages.DebugPhysicalButton.RIGHT_BTN, + hold_ms=hold_ms, + ) + # sleeping little longer for UI to update + time.sleep(hold_ms / 1000 + 0.1) + return result + def stop(self) -> None: self._call(messages.DebugLinkStop(), nowait=True) @@ -535,8 +679,8 @@ class DebugLink: self, directory: str, refresh_index: Optional[int] = None ) -> None: self.screenshot_recording_dir = directory - # Different recording logic between TT and T1 - if self.model == "T": + # Different recording logic between core and legacy + if self.model in ("T", "R"): self._call( messages.DebugLinkRecordScreen( target_directory=directory, refresh_index=refresh_index @@ -550,7 +694,7 @@ class DebugLink: def stop_recording(self) -> None: self.screenshot_recording_dir = None # Different recording logic between TT and T1 - if self.model == "T": + if self.model in ("T", "R"): self._call(messages.DebugLinkRecordScreen(target_directory=None)) else: self.t1_take_screenshots = False diff --git a/python/src/trezorlib/device.py b/python/src/trezorlib/device.py index bad8d5b75..87fde1755 100644 --- a/python/src/trezorlib/device.py +++ b/python/src/trezorlib/device.py @@ -242,6 +242,12 @@ def reboot_to_bootloader(client: "TrezorClient") -> "MessageType": return client.call(messages.RebootToBootloader()) +@session +@expect(messages.Success, field="message", ret_type=str) +def show_device_tutorial(client: "TrezorClient") -> "MessageType": + return client.call(messages.ShowDeviceTutorial()) + + @expect(messages.Success, field="message", ret_type=str) @session def set_busy(client: "TrezorClient", expiry_ms: Optional[int]) -> "MessageType": diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index e3194d38f..6dc9ae949 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -71,6 +71,7 @@ class MessageType(IntEnum): FirmwareHash = 89 UnlockPath = 93 UnlockedPathRequest = 94 + ShowDeviceTutorial = 95 SetU2FCounter = 63 GetNextU2FCounter = 80 NextU2FCounter = 81 @@ -493,6 +494,12 @@ class DebugButton(IntEnum): INFO = 2 +class DebugPhysicalButton(IntEnum): + LEFT_BTN = 0 + MIDDLE_BTN = 1 + RIGHT_BTN = 2 + + class EthereumDefinitionType(IntEnum): NETWORK = 0 TOKEN = 1 @@ -3710,6 +3717,10 @@ class UnlockedPathRequest(protobuf.MessageType): self.mac = mac +class ShowDeviceTutorial(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 95 + + class DebugLinkDecision(protobuf.MessageType): MESSAGE_WIRE_TYPE = 100 FIELDS = { @@ -3720,6 +3731,7 @@ class DebugLinkDecision(protobuf.MessageType): 5: protobuf.Field("y", "uint32", repeated=False, required=False, default=None), 6: protobuf.Field("wait", "bool", repeated=False, required=False, default=None), 7: protobuf.Field("hold_ms", "uint32", repeated=False, required=False, default=None), + 8: protobuf.Field("physical_button", "DebugPhysicalButton", repeated=False, required=False, default=None), } def __init__( @@ -3732,6 +3744,7 @@ class DebugLinkDecision(protobuf.MessageType): y: Optional["int"] = None, wait: Optional["bool"] = None, hold_ms: Optional["int"] = None, + physical_button: Optional["DebugPhysicalButton"] = None, ) -> None: self.button = button self.swipe = swipe @@ -3740,6 +3753,7 @@ class DebugLinkDecision(protobuf.MessageType): self.y = y self.wait = wait self.hold_ms = hold_ms + self.physical_button = physical_button class DebugLinkLayout(protobuf.MessageType): diff --git a/tests/click_tests/common.py b/tests/click_tests/common.py index 026dd0c65..e34103c04 100644 --- a/tests/click_tests/common.py +++ b/tests/click_tests/common.py @@ -43,8 +43,98 @@ def get_char_category(char: str) -> PassphraseCategory: def go_next(debug: "DebugLink", wait: bool = False) -> "LayoutContent" | None: - return debug.click(buttons.OK, wait=wait) # type: ignore + if debug.model == "T": + return debug.click(buttons.OK, wait=wait) # type: ignore + elif debug.model == "R": + return debug.press_right(wait=wait) # type: ignore + else: + raise RuntimeError("Unknown model") def go_back(debug: "DebugLink", wait: bool = False) -> "LayoutContent" | None: - return debug.click(buttons.CANCEL, wait=wait) # type: ignore + if debug.model == "T": + return debug.click(buttons.CANCEL, wait=wait) # type: ignore + elif debug.model == "R": + return debug.press_left(wait=wait) # type: ignore + else: + raise RuntimeError("Unknown model") + + +def navigate_to_action_and_press( + debug: "DebugLink", + wanted_action: str, + all_actions: list[str], + is_carousel: bool = True, +) -> None: + """Navigate to the button with certain action and press it""" + # Orient + try: + _get_action_index(wanted_action, all_actions) + except ValueError: + raise ValueError(f"Action {wanted_action} is not supported in {all_actions}") + + def current_action() -> str: + return layout.get_middle_choice() + + def current_is_wanted(wanted_action: str) -> bool: + # Allowing for possible multiple actions on one button + return ( + current_action() == wanted_action + or current_action() in wanted_action.split("|") + ) + + # Navigate + layout = debug.read_layout() + while not current_is_wanted(wanted_action): + layout = _move_one_closer( + debug=debug, + wanted_action=wanted_action, + current_action=current_action(), + all_actions=all_actions, + is_carousel=is_carousel, + ) + + # Press + debug.press_middle(wait=True) + + +def _get_action_index(wanted_action: str, all_actions: list[str]) -> int: + """Get index of the action in the list of all actions""" + if wanted_action in all_actions: + return all_actions.index(wanted_action) + else: + # It may happen that one action item can mean multiple actions + # (e.g. "CANCEL|DELETE" in the passphrase layout - both actions are on the same button) + for index, action in enumerate(all_actions): + subactions = action.split("|") + if wanted_action in subactions: + return index + + raise ValueError(f"Action {wanted_action} is not supported in {all_actions}") + + +def _move_one_closer( + debug: "DebugLink", + wanted_action: str, + current_action: str, + all_actions: list[str], + is_carousel: bool, +) -> "LayoutContent": + """Pressing either left or right regarding to the current situation""" + index_diff = _get_action_index(wanted_action, all_actions) - _get_action_index( + current_action, all_actions + ) + if not is_carousel: + # Simply move according to the index in a closed list + if index_diff > 0: + return debug.press_right(wait=True) + else: + return debug.press_left(wait=True) + else: + # Carousel can move in a circle - over the edges + # Always move the shortest way + action_half = len(all_actions) // 2 + if index_diff > action_half or -action_half < index_diff < 0: + return debug.press_left(wait=True) + else: + return debug.press_right(wait=True) diff --git a/tests/click_tests/recovery.py b/tests/click_tests/recovery.py index 798eb8ffc..b2c948bd3 100644 --- a/tests/click_tests/recovery.py +++ b/tests/click_tests/recovery.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING from .. import buttons +from .common import go_next if TYPE_CHECKING: from trezorlib.debuglink import DebugLink, LayoutContent @@ -9,41 +10,87 @@ if TYPE_CHECKING: def enter_word( debug: "DebugLink", word: str, is_slip39: bool = False ) -> "LayoutContent": - typed_word = word[:4] - for coords in buttons.type_word(typed_word, is_slip39=is_slip39): - debug.click(coords) - - return debug.click(buttons.CONFIRM_WORD, wait=True) + if debug.model == "T": + typed_word = word[:4] + for coords in buttons.type_word(typed_word, is_slip39=is_slip39): + debug.click(coords) + + return debug.click(buttons.CONFIRM_WORD, wait=True) + elif debug.model == "R": + letter_index = 0 + layout = debug.read_layout() + + # Letter choices + while layout.find_values_by_key("letter_choices"): + letter = word[letter_index] + while not layout.get_middle_choice() == letter: + layout = debug.press_right(wait=True) + + layout = debug.press_middle(wait=True) + letter_index += 1 + + # Word choices + while not layout.get_middle_choice() == word: + layout = debug.press_right(wait=True) + + return debug.press_middle(wait=True) + else: + raise ValueError("Unknown model") def confirm_recovery(debug: "DebugLink") -> None: - if not debug.legacy_ui and not debug.legacy_debug: + if debug.model == "T": + if not debug.legacy_ui and not debug.legacy_debug: + layout = debug.wait_layout() + assert layout.title().startswith("WALLET RECOVERY") + debug.click(buttons.OK, wait=True) + elif debug.model == "R": layout = debug.wait_layout() - assert layout.title().startswith("WALLET RECOVERY") - debug.click(buttons.OK, wait=True) + assert layout.title() == "WALLET RECOVERY" + debug.press_right(wait=True) + debug.press_right() + + +def select_number_of_words( + debug: "DebugLink", num_of_words: int = 20, wait: bool = True +) -> None: + if wait: + debug.wait_layout() + if debug.model == "T": + # select number of words + if not debug.legacy_ui and not debug.legacy_debug: + assert "number of words" in debug.read_layout().text_content() + layout = debug.click(buttons.OK, wait=True) + if debug.legacy_ui: + assert layout.json_str == "WordSelector" + elif debug.legacy_debug: + assert "SelectWordCount" in layout.json_str + else: + # Two title options + assert layout.title() in ("SEED CHECK", "WALLET RECOVERY") + + # click the number + word_option_offset = 6 + word_options = (12, 18, 20, 24, 33) + index = word_option_offset + word_options.index( + num_of_words + ) # raises if num of words is invalid + coords = buttons.grid34(index % 3, index // 3) + layout = debug.click(coords, wait=True) + elif debug.model == "R": + assert "number of words" in debug.read_layout().text_content() + layout = debug.press_right(wait=True) + assert layout.title() == "NUMBER OF WORDS" -def select_number_of_words(debug: "DebugLink", num_of_words: int = 20) -> None: - # select number of words - if not debug.legacy_ui and not debug.legacy_debug: - assert "number of words" in debug.read_layout().text_content() - layout = debug.click(buttons.OK, wait=True) - if debug.legacy_ui: - assert layout.json_str == "WordSelector" - elif debug.version < (2, 6, 1): - assert "SelectWordCount" in layout.json_str + # navigate to the number and confirm it + word_options = (12, 18, 20, 24, 33) + index = word_options.index(num_of_words) + for _ in range(index): + debug.press_right(wait=True) + layout = debug.press_middle(wait=True) else: - # Two title options - assert layout.title() in ("SEED CHECK", "WALLET RECOVERY") - - # click the number - word_option_offset = 6 - word_options = (12, 18, 20, 24, 33) - index = word_option_offset + word_options.index( - num_of_words - ) # raises if num of words is invalid - coords = buttons.grid34(index % 3, index // 3) - layout = debug.click(coords, wait=True) + raise ValueError("Unknown model") if not debug.legacy_ui and not debug.legacy_debug: if num_of_words in (20, 33): @@ -53,19 +100,33 @@ def select_number_of_words(debug: "DebugLink", num_of_words: int = 20) -> None: def enter_share(debug: "DebugLink", share: str) -> "LayoutContent": - layout = debug.click(buttons.OK, wait=True) + if debug.model == "T": + layout = debug.click(buttons.OK, wait=True) - if debug.legacy_ui: - assert layout.json_str == "Slip39Keyboard" - elif debug.legacy_debug: - assert "MnemonicKeyboard" in layout.json_str - else: - assert layout.main_component() == "MnemonicKeyboard" + if debug.legacy_ui: + assert layout.json_str == "Slip39Keyboard" + elif debug.legacy_debug: + assert "MnemonicKeyboard" in layout.json_str + else: + assert layout.main_component() == "MnemonicKeyboard" - for word in share.split(" "): - layout = enter_word(debug, word, is_slip39=True) + for word in share.split(" "): + layout = enter_word(debug, word, is_slip39=True) - return layout + return layout + elif debug.model == "R": + layout = debug.press_right(wait=True) + assert layout.title() == "WORD ENTERING" + + layout = debug.press_right(wait=True) + assert "Slip39Entry" in layout.all_components() + + for word in share.split(" "): + layout = enter_word(debug, word, is_slip39=True) + + return layout + else: + raise ValueError("Unknown model") def enter_shares(debug: "DebugLink", shares: list[str]) -> None: @@ -82,17 +143,32 @@ def enter_shares(debug: "DebugLink", shares: list[str]) -> None: def enter_seed(debug: "DebugLink", seed_words: list[str]) -> None: - assert "Enter" in debug.read_layout().text_content() + if debug.model == "T": + assert "Enter" in debug.read_layout().text_content() + + layout = debug.click(buttons.OK, wait=True) + assert layout.main_component() == "MnemonicKeyboard" - layout = debug.click(buttons.OK, wait=True) - assert layout.main_component() == "MnemonicKeyboard" + for word in seed_words: + layout = enter_word(debug, word, is_slip39=False) - for word in seed_words: - layout = enter_word(debug, word, is_slip39=False) + assert "You have finished recovering your wallet" in layout.text_content() + elif debug.model == "R": + assert "Enter" in debug.read_layout().text_content() - assert "You have finished recovering your wallet" in layout.text_content() + layout = debug.press_right(wait=True) + assert layout.title() == "WORD ENTERING" + + layout = debug.press_right(wait=True) + assert "Bip39Entry" in layout.all_components() + + for word in seed_words: + layout = enter_word(debug, word, is_slip39=False) + + assert "You have finished recovering your wallet" in layout.text_content() def finalize(debug: "DebugLink") -> None: - layout = debug.click(buttons.OK, wait=True) + layout = go_next(debug, wait=True) + assert layout is not None assert layout.main_component() == "Homescreen" diff --git a/tests/click_tests/reset.py b/tests/click_tests/reset.py index eefa1521e..53d88d711 100644 --- a/tests/click_tests/reset.py +++ b/tests/click_tests/reset.py @@ -11,15 +11,24 @@ if TYPE_CHECKING: def confirm_new_wallet(debug: "DebugLink") -> None: - assert debug.wait_layout().title().startswith("WALLET CREATION") - debug.click(buttons.OK, wait=True) + layout = debug.wait_layout() + if debug.model == "T": + assert layout.title().startswith("WALLET CREATION") + debug.click(buttons.OK, wait=True) + elif debug.model == "R": + assert layout.title() == "WALLET CREATION" + debug.press_right(wait=True) + debug.press_right(wait=True) def confirm_read(debug: "DebugLink", title: str, hold: bool = False) -> None: layout = debug.read_layout() if title == "Caution": - # TODO: could look into button texts - assert "OK, I UNDERSTAND" in layout.json_str + if debug.model == "T": + # TODO: could look into button texts + assert "OK, I UNDERSTAND" in layout.json_str + elif debug.model == "R": + assert "use your backup to recover" in layout.text_content() elif title == "Success": # TODO: improve this assert any( @@ -37,14 +46,38 @@ def confirm_read(debug: "DebugLink", title: str, hold: bool = False) -> None: else: assert title.upper() in layout.title() - debug.click(buttons.OK, wait=True) + if debug.model == "T": + debug.click(buttons.OK, wait=True) + elif debug.model == "R": + if layout.page_count() > 1: + debug.press_right(wait=True) + if hold: + # TODO: create debug.hold_right()? + debug.press_yes() + else: + debug.press_right() + debug.wait_layout() def set_selection(debug: "DebugLink", button: tuple[int, int], diff: int) -> None: - assert "NumberInputDialog" in debug.read_layout().all_components() - for _ in range(diff): - debug.click(button) - debug.click(buttons.OK, wait=True) + if debug.model == "T": + assert "NumberInputDialog" in debug.read_layout().all_components() + for _ in range(diff): + debug.click(button) + debug.click(buttons.OK, wait=True) + elif debug.model == "R": + layout = debug.read_layout() + if layout.title() in ("NUMBER OF SHARES", "THRESHOLD"): + # Special info screens + layout = debug.press_right(wait=True) + assert "NumberInput" in layout.all_components() + if button == buttons.RESET_MINUS: + for _ in range(diff): + debug.press_left(wait=True) + else: + for _ in range(diff): + debug.press_right(wait=True) + debug.press_middle(wait=True) def read_words( @@ -53,23 +86,37 @@ def read_words( words: list[str] = [] layout = debug.read_layout() - if backup_type == messages.BackupType.Slip39_Advanced: - assert layout.title().startswith("GROUP") - elif backup_type == messages.BackupType.Slip39_Basic: - assert layout.title().startswith("RECOVERY SHARE #") - else: - assert layout.title() == "RECOVERY SEED" - - # Swiping through all the page and loading the words - for _ in range(layout.page_count() - 1): - words.extend(layout.seed_words()) + if debug.model == "T": + if backup_type == messages.BackupType.Slip39_Advanced: + assert layout.title().startswith("GROUP") + elif backup_type == messages.BackupType.Slip39_Basic: + assert layout.title().startswith("RECOVERY SHARE #") + else: + assert layout.title() == "RECOVERY SEED" + elif debug.model == "R": + if backup_type == messages.BackupType.Slip39_Advanced: + assert "SHARE" in layout.text_content() + elif backup_type == messages.BackupType.Slip39_Basic: + assert layout.text_content().startswith("SHARE #") + else: + assert layout.text_content().startswith("RECOVERY SEED") + + # Swiping through all the pages and loading the words + for i in range(layout.page_count() - 1): + # In model R, first two pages are just informational + if not (debug.model == "R" and i < 2): + words.extend(layout.seed_words()) layout = debug.input(swipe=messages.DebugSwipeDirection.UP, wait=True) assert layout is not None - words.extend(layout.seed_words()) + if debug.model == "T": + words.extend(layout.seed_words()) # There is hold-to-confirm button if do_htc: - debug.click_hold(buttons.OK, hold_ms=1500) + if debug.model == "T": + debug.click_hold(buttons.OK, hold_ms=1500) + elif debug.model == "R": + debug.press_right_htc(1200) else: # It would take a very long time to test 16-of-16 with doing 1500 ms HTC after # each word set @@ -80,16 +127,32 @@ def read_words( def confirm_words(debug: "DebugLink", words: list[str]) -> None: layout = debug.wait_layout() - assert "Select word" in layout.text_content() - for _ in range(3): - # "Select word 3 of 20" - # ^ - word_pos = int(layout.text_content().split()[2]) - # Unifying both the buttons and words to lowercase - btn_texts = [text.lower() for text in layout.button_contents()] - wanted_word = words[word_pos - 1].lower() - button_pos = btn_texts.index(wanted_word) - layout = debug.click(buttons.RESET_WORD_CHECK[button_pos], wait=True) + if debug.model == "T": + assert "Select word" in layout.text_content() + for _ in range(3): + # "Select word 3 of 20" + # ^ + word_pos = int(layout.text_content().split()[2]) + # Unifying both the buttons and words to lowercase + btn_texts = [ + text.lower() for text in layout.tt_check_seed_button_contents() + ] + wanted_word = words[word_pos - 1].lower() + button_pos = btn_texts.index(wanted_word) + layout = debug.click(buttons.RESET_WORD_CHECK[button_pos], wait=True) + elif debug.model == "R": + assert "Select correct word" in layout.text_content() + layout = debug.press_right(wait=True) + for _ in range(3): + # "SELECT 2ND WORD" + # ^ + word_pos = int(layout.title().split()[1][:-2]) + wanted_word = words[word_pos - 1].lower() + + while not layout.get_middle_choice() == wanted_word: + layout = debug.press_right(wait=True) + + layout = debug.press_middle(wait=True) def validate_mnemonics(mnemonics: list[str], expected_ems: bytes) -> None: diff --git a/tests/click_tests/test_autolock.py b/tests/click_tests/test_autolock.py index 8dc278abb..354186625 100644 --- a/tests/click_tests/test_autolock.py +++ b/tests/click_tests/test_autolock.py @@ -28,6 +28,7 @@ from .. import buttons, common from ..device_tests.bitcoin.payment_req import make_coinjoin_request from ..tx_cache import TxCache from . import recovery +from .common import go_next if TYPE_CHECKING: from ..device_handler import BackgroundDeviceHandler @@ -65,7 +66,7 @@ def set_autolock_delay(device_handler: "BackgroundDeviceHandler", delay_ms: int) f"auto-lock your device after {delay_ms // 1000} seconds" in debug.wait_layout().text_content() ) - layout = debug.click(buttons.OK, wait=True) + layout = go_next(debug, wait=True) assert layout.main_component() == "Homescreen" assert device_handler.result() == "Settings applied" @@ -98,9 +99,15 @@ def test_autolock_interrupts_signing(device_handler: "BackgroundDeviceHandler"): in debug.wait_layout().text_content().replace(" ", "") ) - debug.click(buttons.OK, wait=True) - layout = debug.click(buttons.OK, wait=True) - assert "Total amount: 0.0039 BTC" in layout.text_content() + if debug.model == "T": + debug.click(buttons.OK, wait=True) + layout = debug.click(buttons.OK, wait=True) + assert "Total amount: 0.0039 BTC" in layout.text_content() + elif debug.model == "R": + debug.press_right(wait=True) + debug.press_right(wait=True) + layout = debug.press_right(wait=True) + assert "TOTAL AMOUNT 0.0039 BTC" in layout.text_content() # wait for autolock to kick in time.sleep(10.1) @@ -138,10 +145,14 @@ def test_autolock_does_not_interrupt_signing(device_handler: "BackgroundDeviceHa in debug.wait_layout().text_content().replace(" ", "") ) - debug.click(buttons.OK, wait=True) - - layout = debug.click(buttons.OK, wait=True) - assert "Total amount: 0.0039 BTC" in layout.text_content() + if debug.model == "T": + debug.click(buttons.OK, wait=True) + layout = debug.click(buttons.OK, wait=True) + assert "Total amount: 0.0039 BTC" in layout.text_content() + elif debug.model == "R": + debug.press_right(wait=True) + layout = debug.press_right(wait=True) + assert "TOTAL AMOUNT 0.0039 BTC" in layout.text_content() def sleepy_filter(msg: MessageType) -> MessageType: time.sleep(10.1) @@ -151,7 +162,10 @@ def test_autolock_does_not_interrupt_signing(device_handler: "BackgroundDeviceHa with device_handler.client: device_handler.client.set_filter(messages.TxAck, sleepy_filter) # confirm transaction - debug.click(buttons.OK) + if debug.model == "T": + debug.click(buttons.OK) + elif debug.model == "R": + debug.press_right_htc(1200) signatures, tx = device_handler.result() assert len(signatures) == 1 @@ -168,18 +182,34 @@ def test_autolock_passphrase_keyboard(device_handler: "BackgroundDeviceHandler") # get address device_handler.run(common.get_test_address) # type: ignore - assert debug.wait_layout().main_component() == "PassphraseKeyboard" + assert "PassphraseKeyboard" in debug.wait_layout().all_components() + + if debug.model == "R": + # Going into the first character category (abc) + debug.press_middle() # enter passphrase - slowly # keep clicking for long enough to trigger the autolock if it incorrectly ignored key presses for _ in range(math.ceil(11 / 1.5)): - # click at "j" - debug.click(CENTER_BUTTON) + if debug.model == "T": + # click at "j" + debug.click(CENTER_BUTTON) + elif debug.model == "R": + # press "a" + debug.press_middle() time.sleep(1.5) # Confirm the passphrase - debug.click(buttons.OK, wait=True) - assert device_handler.result() == "mnF4yRWJXmzRB6EuBzuVigqeqTqirQupxJ" + if debug.model == "T": + debug.click(buttons.OK, wait=True) + assert device_handler.result() == "mnF4yRWJXmzRB6EuBzuVigqeqTqirQupxJ" + elif debug.model == "R": + debug.press_left() # go to BACK + debug.press_middle() # PRESS back + debug.press_left() # go to ENTER + debug.press_middle() # press ENTER + debug.wait_layout() + assert device_handler.result() == "mfar3NVufmeGNamk1sCpmCiSLPoFJ9JQDa" @pytest.mark.setup_client(pin=PIN4, passphrase=True) @@ -190,12 +220,19 @@ def test_autolock_interrupts_passphrase(device_handler: "BackgroundDeviceHandler # get address device_handler.run(common.get_test_address) # type: ignore - assert debug.wait_layout().main_component() == "PassphraseKeyboard" + assert "PassphraseKeyboard" in debug.wait_layout().all_components() + + if debug.model == "R": + # Going into the first character category (abc) + debug.press_middle() # enter passphrase - slowly # autolock must activate even if we pressed some buttons for _ in range(math.ceil(6 / 1.5)): - debug.click(CENTER_BUTTON) + if debug.model == "T": + debug.click(CENTER_BUTTON) + elif debug.model == "R": + debug.press_middle() time.sleep(1.5) # wait for autolock to kick in @@ -210,7 +247,7 @@ def unlock_dry_run(debug: "DebugLink") -> "LayoutContent": "Do you really want to check the recovery seed?" in debug.wait_layout().text_content() ) - layout = debug.click(buttons.OK, wait=True) + layout = go_next(debug, wait=True) assert layout.main_component() == "PinKeyboard" layout = debug.input(PIN4, wait=True) @@ -226,7 +263,10 @@ def test_dryrun_locks_at_number_of_words(device_handler: "BackgroundDeviceHandle device_handler.run(device.recover, dry_run=True) # type: ignore layout = unlock_dry_run(debug) - assert "number of words " in layout.text_content() + assert "number of words" in layout.text_content() + + if debug.model == "R": + debug.press_right(wait=True) # wait for autolock to trigger time.sleep(10.1) @@ -235,10 +275,9 @@ def test_dryrun_locks_at_number_of_words(device_handler: "BackgroundDeviceHandle device_handler.result() # unlock - debug.wait_layout( - wait_for_external_change=True - ) # lockscreen triggered automatically - layout = debug.click(buttons.OK, wait=True) + # lockscreen triggered automatically + debug.wait_layout(wait_for_external_change=True) + layout = go_next(debug, wait=True) assert layout.main_component() == "PinKeyboard" layout = debug.input(PIN4, wait=True) assert layout is not None @@ -259,8 +298,15 @@ def test_dryrun_locks_at_word_entry(device_handler: "BackgroundDeviceHandler"): # select 20 words recovery.select_number_of_words(debug, 20) - layout = debug.click(buttons.OK, wait=True) - assert layout.main_component() == "MnemonicKeyboard" + if debug.model == "T": + layout = debug.click(buttons.OK, wait=True) + assert layout.main_component() == "MnemonicKeyboard" + elif debug.model == "R": + layout = debug.press_right(wait=True) + assert "WORD ENTERING" in layout.title() + layout = debug.press_right(wait=True) + assert "Slip39Entry" in layout.all_components() + # make sure keyboard locks time.sleep(10.1) assert debug.wait_layout().main_component() == "Lockscreen" @@ -280,16 +326,30 @@ def test_dryrun_enter_word_slowly(device_handler: "BackgroundDeviceHandler"): # select 20 words recovery.select_number_of_words(debug, 20) - layout = debug.click(buttons.OK, wait=True) - assert layout.main_component() == "MnemonicKeyboard" - - # type the word OCEAN slowly - for coords in buttons.type_word("ocea", is_slip39=True): - time.sleep(9) - debug.click(coords) - layout = debug.click(buttons.CONFIRM_WORD, wait=True) - # should not have locked, even though we took 9 seconds to type each letter - assert layout.main_component() == "MnemonicKeyboard" + if debug.model == "T": + layout = debug.click(buttons.OK, wait=True) + assert layout.main_component() == "MnemonicKeyboard" + + # type the word OCEAN slowly + for coords in buttons.type_word("ocea", is_slip39=True): + time.sleep(9) + debug.click(coords) + layout = debug.click(buttons.CONFIRM_WORD, wait=True) + # should not have locked, even though we took 9 seconds to type each letter + assert layout.main_component() == "MnemonicKeyboard" + elif debug.model == "R": + layout = debug.press_right(wait=True) + assert "WORD ENTERING" in layout.title() + layout = debug.press_right(wait=True) + assert "Slip39Entry" in layout.all_components() + + # type the word `ACADEMIC` slowly (A, C, and the whole word confirmation) + for _ in range(3): + time.sleep(9) + debug.press_middle() + layout = debug.wait_layout() + # should not have locked, even though we took 9 seconds to type each letter + assert "Slip39Entry" in layout.all_components() device_handler.kill_task() diff --git a/tests/click_tests/test_lock.py b/tests/click_tests/test_lock.py index 545b173ac..0b6cd2f28 100644 --- a/tests/click_tests/test_lock.py +++ b/tests/click_tests/test_lock.py @@ -32,11 +32,14 @@ PIN4 = "1234" def test_hold_to_lock(device_handler: "BackgroundDeviceHandler"): debug = device_handler.debuglink() - short_duration = 1000 - lock_duration = 3500 + short_duration = 1000 if debug.model == "T" else 500 + lock_duration = 3500 if debug.model == "T" else 1200 def hold(duration: int, wait: bool = True) -> None: - debug.input(x=13, y=37, hold_ms=duration, wait=wait) + if debug.model == "R": + debug.press_right_htc(hold_ms=duration) + else: + debug.input(x=13, y=37, hold_ms=duration, wait=wait) assert device_handler.features().unlocked is False @@ -60,7 +63,12 @@ def test_hold_to_lock(device_handler: "BackgroundDeviceHandler"): assert device_handler.features().unlocked is False # unlock by touching - layout = debug.click(buttons.INFO, wait=True) + if debug.model == "R": + # Doing a short HTC to simulate a click + debug.press_right_htc(hold_ms=100) + layout = debug.wait_layout() + else: + layout = debug.click(buttons.INFO, wait=True) assert layout.main_component() == "PinKeyboard" debug.input("1234", wait=True) diff --git a/tests/click_tests/test_passphrase_tr.py b/tests/click_tests/test_passphrase_tr.py new file mode 100644 index 000000000..c5c37ba65 --- /dev/null +++ b/tests/click_tests/test_passphrase_tr.py @@ -0,0 +1,261 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2023 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + +from contextlib import contextmanager +from typing import TYPE_CHECKING, Generator, Optional + +import pytest + +from trezorlib import exceptions + +from ..common import get_test_address +from .common import ( + CommonPass, + PassphraseCategory, + get_char_category, + navigate_to_action_and_press, +) + +if TYPE_CHECKING: + from ..device_handler import BackgroundDeviceHandler + from trezorlib.debuglink import DebugLink + + +pytestmark = [pytest.mark.skip_t1, pytest.mark.skip_t2] + +# Testing the maximum length is really 50 +# TODO: show some UI message when length reaches 50? + +AAA_50 = 50 * "a" +AAA_50_ADDRESS = "miPeCUxf1Ufh5DtV3AuBopNM8YEDvnQZMh" +assert len(AAA_50) == 50 + +AAA_49 = AAA_50[:-1] +AAA_49_ADDRESS = "n2MPUjAB86MuVmyYe8HCgdznJS1FXk3qvg" +assert len(AAA_49) == 49 +assert AAA_49_ADDRESS != AAA_50_ADDRESS + +AAA_51 = AAA_50 + "a" +AAA_51_ADDRESS = "miPeCUxf1Ufh5DtV3AuBopNM8YEDvnQZMh" +assert len(AAA_51) == 51 +assert AAA_51_ADDRESS == AAA_50_ADDRESS + + +# fmt: off +BACK = "BACK" +MENU_ACTIONS = ["SHOW", "CANCEL|DELETE", "ENTER", "abc", "ABC", "123", "#$!", "SPACE"] +DIGITS_ACTIONS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", BACK] +LOWERCASE_ACTIONS = [ + "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", + "t", "u", "v", "w", "x", "y", "z", BACK +] +UPPERCASE_ACTIONS = [ + "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", + "T", "U", "V", "W", "X", "Y", "Z", BACK +] +SPECIAL_ACTIONS = [ + "_", "<", ">", ".", ":", "@", "/", "|", "\\", "!", "(", ")", "+", "%", "&", "-", "[", "]", "?", + "{", "}", ",", "'", "`", ";", "\"", "~", "$", "^", "=", "*", "#", BACK +] +# fmt: on + +CATEGORY_ACTIONS = { + PassphraseCategory.MENU: MENU_ACTIONS, + PassphraseCategory.DIGITS: DIGITS_ACTIONS, + PassphraseCategory.LOWERCASE: LOWERCASE_ACTIONS, + PassphraseCategory.UPPERCASE: UPPERCASE_ACTIONS, + PassphraseCategory.SPECIAL: SPECIAL_ACTIONS, +} + + +@contextmanager +def prepare_passphrase_dialogue( + device_handler: "BackgroundDeviceHandler", address: Optional[str] = None +) -> Generator["DebugLink", None, None]: + debug = device_handler.debuglink() + device_handler.run(get_test_address) # type: ignore + layout = debug.wait_layout() + assert "PassphraseKeyboard" in layout.all_components() + assert layout.passphrase() == "" + assert _current_category(debug) == PassphraseCategory.MENU + + yield debug + + result = device_handler.result() + if address is not None: + assert result == address + + +def _current_category(debug: "DebugLink") -> PassphraseCategory: + """What is the current category we are in""" + layout = debug.read_layout() + category = layout.find_unique_value_by_key("current_category", "") + return PassphraseCategory(category) + + +def _current_actions(debug: "DebugLink") -> list[str]: + """What are the actions in the current category""" + current = _current_category(debug) + return CATEGORY_ACTIONS[current] + + +def go_to_category( + debug: "DebugLink", category: PassphraseCategory, use_carousel: bool = True +) -> None: + """Go to a specific category""" + # Already there + if _current_category(debug) == category: + return + + # Need to be in MENU anytime to change category + if _current_category(debug) != PassphraseCategory.MENU: + navigate_to_action_and_press( + debug, BACK, _current_actions(debug), is_carousel=use_carousel + ) + + assert _current_category(debug) == PassphraseCategory.MENU + + # Go to the right one, unless we want MENU + if category != PassphraseCategory.MENU: + navigate_to_action_and_press( + debug, category.value, _current_actions(debug), is_carousel=use_carousel + ) + + assert _current_category(debug) == category + + +def press_char(debug: "DebugLink", char: str) -> None: + """Press a character""" + # Space is a special case + if char == " ": + go_to_category(debug, PassphraseCategory.MENU) + navigate_to_action_and_press(debug, "SPACE", _current_actions(debug)) + else: + char_category = get_char_category(char) + go_to_category(debug, char_category) + navigate_to_action_and_press(debug, char, _current_actions(debug)) + + +def input_passphrase(debug: "DebugLink", passphrase: str) -> None: + """Input a passphrase with validation it got added""" + before = debug.read_layout().passphrase() + for char in passphrase: + press_char(debug, char) + after = debug.read_layout().passphrase() + assert after == before + passphrase + + +def show_passphrase(debug: "DebugLink") -> None: + """Show a passphrase""" + go_to_category(debug, PassphraseCategory.MENU) + navigate_to_action_and_press(debug, "SHOW", _current_actions(debug)) + + +def enter_passphrase(debug: "DebugLink") -> None: + """Enter a passphrase""" + go_to_category(debug, PassphraseCategory.MENU) + navigate_to_action_and_press(debug, "ENTER", _current_actions(debug)) + + +def delete_char(debug: "DebugLink") -> None: + """Deletes the last char""" + go_to_category(debug, PassphraseCategory.MENU) + navigate_to_action_and_press(debug, "CANCEL|DELETE", _current_actions(debug)) + + +def cancel(debug: "DebugLink") -> None: + """Cancels the whole dialogue - clicking the same button as in DELETE""" + delete_char(debug) + + +VECTORS = ( # passphrase, address + (CommonPass.SHORT, CommonPass.SHORT_ADDRESS), + (CommonPass.WITH_SPACE, CommonPass.WITH_SPACE_ADDRESS), + (CommonPass.RANDOM_25, CommonPass.RANDOM_25_ADDRESS), + (AAA_49, AAA_49_ADDRESS), + (AAA_50, AAA_50_ADDRESS), +) + + +@pytest.mark.parametrize("passphrase, address", VECTORS) +@pytest.mark.setup_client(passphrase=True) +def test_passphrase_input( + device_handler: "BackgroundDeviceHandler", passphrase: str, address: str +): + with prepare_passphrase_dialogue(device_handler, address) as debug: + input_passphrase(debug, passphrase) + show_passphrase(debug) + enter_passphrase(debug) + + +@pytest.mark.setup_client(passphrase=True) +def test_passphrase_input_over_50_chars(device_handler: "BackgroundDeviceHandler"): + with prepare_passphrase_dialogue(device_handler, AAA_51_ADDRESS) as debug: # type: ignore + # First 50 chars + input_passphrase(debug, AAA_51[:-1]) + layout = debug.read_layout() + assert AAA_51[:-1] in layout.passphrase() + + show_passphrase(debug) + + # Over-limit character + press_char(debug, AAA_51[-1]) + + # No change + layout = debug.read_layout() + assert AAA_51[:-1] in layout.passphrase() + assert AAA_51 not in layout.passphrase() + + show_passphrase(debug) + enter_passphrase(debug) + + +@pytest.mark.setup_client(passphrase=True) +def test_passphrase_delete(device_handler: "BackgroundDeviceHandler"): + with prepare_passphrase_dialogue(device_handler, CommonPass.SHORT_ADDRESS) as debug: + input_passphrase(debug, CommonPass.SHORT[:8]) + show_passphrase(debug) + + for _ in range(4): + delete_char(debug) + show_passphrase(debug) + + input_passphrase(debug, CommonPass.SHORT[8 - 4 :]) + show_passphrase(debug) + enter_passphrase(debug) + + +@pytest.mark.setup_client(passphrase=True) +def test_cancel(device_handler: "BackgroundDeviceHandler"): + with pytest.raises(exceptions.Cancelled): + with prepare_passphrase_dialogue(device_handler) as debug: + input_passphrase(debug, "abc") + show_passphrase(debug) + for _ in range(3): + delete_char(debug) + show_passphrase(debug) + cancel(debug) + + +@pytest.mark.setup_client(passphrase=True) +def test_passphrase_loop_all_characters(device_handler: "BackgroundDeviceHandler"): + with prepare_passphrase_dialogue(device_handler, CommonPass.EMPTY_ADDRESS) as debug: + for category in PassphraseCategory: + go_to_category(debug, category) + # use_carousel=False because we want to reach BACK at the end of the list + go_to_category(debug, PassphraseCategory.MENU, use_carousel=False) + + enter_passphrase(debug) diff --git a/tests/click_tests/test_passphrase_tt.py b/tests/click_tests/test_passphrase_tt.py index a2f54f8c0..2f5c924c5 100644 --- a/tests/click_tests/test_passphrase_tt.py +++ b/tests/click_tests/test_passphrase_tt.py @@ -29,7 +29,7 @@ if TYPE_CHECKING: from trezorlib.debuglink import DebugLink -pytestmark = pytest.mark.skip_t1 +pytestmark = [pytest.mark.skip_t1, pytest.mark.skip_tr] # TODO: it is not possible to cancel the passphrase entry on TT # NOTE: the prompt (underscoring) is not there when a space is entered diff --git a/tests/click_tests/test_pin.py b/tests/click_tests/test_pin.py index 24192996c..b74dfe96f 100644 --- a/tests/click_tests/test_pin.py +++ b/tests/click_tests/test_pin.py @@ -23,7 +23,7 @@ import pytest from trezorlib import device, exceptions from .. import buttons -from .common import go_back, go_next +from .common import go_back, go_next, navigate_to_action_and_press if TYPE_CHECKING: from ..device_handler import BackgroundDeviceHandler @@ -84,7 +84,13 @@ def prepare( # Set new PIN device_handler.run(device.change_pin) # type: ignore assert "enable PIN protection" in debug.wait_layout().text_content() - go_next(debug) + if debug.model == "T": + go_next(debug) + elif debug.model == "R": + go_next(debug, wait=True) + go_next(debug, wait=True) + go_next(debug, wait=True) + debug.press_right_htc(1000) elif situation == Situation.PIN_CHANGE: # Change PIN device_handler.run(device.change_pin) # type: ignore @@ -98,7 +104,9 @@ def prepare( if old_pin: _input_see_confirm(debug, old_pin) assert "enable wipe code" in debug.wait_layout().text_content() - go_next(debug) + go_next(debug, wait=True) + if debug.model == "R": + debug.press_right_htc(1000) if old_pin: debug.wait_layout() _input_see_confirm(debug, old_pin) @@ -116,13 +124,18 @@ def _assert_pin_entry(debug: "DebugLink") -> None: def _input_pin(debug: "DebugLink", pin: str, check: bool = False) -> None: """Input the PIN""" - before = debug.read_layout().pin() + if check: + before = debug.read_layout().pin() - digits_order = debug.read_layout().tt_pin_digits_order() - for digit in pin: - digit_index = digits_order.index(digit) - coords = buttons.pin_passphrase_index(digit_index) - debug.click(coords, wait=True) + if debug.model == "T": + digits_order = debug.read_layout().tt_pin_digits_order() + for digit in pin: + digit_index = digits_order.index(digit) + coords = buttons.pin_passphrase_index(digit_index) + debug.click(coords, wait=True) + elif debug.model == "R": + for digit in pin: + navigate_to_action_and_press(debug, digit, TR_PIN_ACTIONS) if check: after = debug.read_layout().pin() @@ -131,7 +144,10 @@ def _input_pin(debug: "DebugLink", pin: str, check: bool = False) -> None: def _see_pin(debug: "DebugLink") -> None: """Navigate to "SHOW" and press it""" - debug.click(buttons.TOP_ROW, wait=True) + if debug.model == "T": + debug.click(buttons.TOP_ROW, wait=True) + elif debug.model == "R": + navigate_to_action_and_press(debug, "SHOW", TR_PIN_ACTIONS) def _delete_pin(debug: "DebugLink", digits_to_delete: int, check: bool = True) -> None: @@ -140,7 +156,10 @@ def _delete_pin(debug: "DebugLink", digits_to_delete: int, check: bool = True) - before = debug.read_layout().pin() for _ in range(digits_to_delete): - debug.click(buttons.pin_passphrase_grid(9), wait=True) + if debug.model == "T": + debug.click(buttons.pin_passphrase_grid(9), wait=True) + elif debug.model == "R": + navigate_to_action_and_press(debug, "DELETE", TR_PIN_ACTIONS) if check: after = debug.read_layout().pin() @@ -150,12 +169,16 @@ def _delete_pin(debug: "DebugLink", digits_to_delete: int, check: bool = True) - def _cancel_pin(debug: "DebugLink") -> None: """Navigate to "CANCEL" and press it""" # It is the same button as DELETE + # TODO: implement cancel PIN for TR? _delete_pin(debug, 1, check=False) def _confirm_pin(debug: "DebugLink") -> None: """Navigate to "ENTER" and press it""" - debug.click(buttons.pin_passphrase_grid(11), wait=True) + if debug.model == "T": + debug.click(buttons.pin_passphrase_grid(11), wait=True) + elif debug.model == "R": + navigate_to_action_and_press(debug, "ENTER", TR_PIN_ACTIONS) def _input_see_confirm(debug: "DebugLink", pin: str) -> None: @@ -166,6 +189,11 @@ def _input_see_confirm(debug: "DebugLink", pin: str) -> None: def _enter_two_times(debug: "DebugLink", pin1: str, pin2: str) -> None: _input_see_confirm(debug, pin1) + + if debug.model == "R": + # Please re-enter + go_next(debug, wait=True) + _input_see_confirm(debug, pin2) @@ -210,10 +238,10 @@ def test_pin_longer_than_max(device_handler: "BackgroundDeviceHandler"): def test_pin_incorrect(device_handler: "BackgroundDeviceHandler"): with prepare(device_handler) as debug: _input_see_confirm(debug, "1235") - # debug.wait_layout() _input_see_confirm(debug, PIN4) +@pytest.mark.skip_tr("TODO: will we support cancelling on TR?") @pytest.mark.setup_client(pin=PIN4) def test_pin_cancel(device_handler: "BackgroundDeviceHandler"): with PIN_CANCELLED, prepare(device_handler) as debug: @@ -235,7 +263,10 @@ def test_pin_setup_mismatch(device_handler: "BackgroundDeviceHandler"): with PIN_CANCELLED, prepare(device_handler, Situation.PIN_SETUP) as debug: _enter_two_times(debug, "1", "2") go_next(debug) - _cancel_pin(debug) + if debug.model == "T": + _cancel_pin(debug) + elif debug.model == "R": + debug.press_no() @pytest.mark.setup_client(pin="1") @@ -250,6 +281,17 @@ def test_wipe_code_setup(device_handler: "BackgroundDeviceHandler"): _enter_two_times(debug, "2", "2") +# @pytest.mark.setup_client(pin="1") +# @pytest.mark.timeout(15) +# @pytest.mark.xfail(reason="It will disconnect from the emulator") +# def test_wipe_code_setup_and_trigger(device_handler: "BackgroundDeviceHandler"): +# with prepare(device_handler, Situation.WIPE_CODE_SETUP, old_pin="1") as debug: +# _enter_two_times(debug, "2", "2") +# device_handler.client.lock() +# with prepare(device_handler) as debug: +# _input_see_confirm(debug, "2") + + @pytest.mark.setup_client(pin="1") def test_wipe_code_same_as_pin(device_handler: "BackgroundDeviceHandler"): with prepare(device_handler, Situation.WIPE_CODE_SETUP, old_pin="1") as debug: diff --git a/tests/click_tests/test_reset_bip39.py b/tests/click_tests/test_reset_bip39.py index 93fd92045..1b1302956 100644 --- a/tests/click_tests/test_reset_bip39.py +++ b/tests/click_tests/test_reset_bip39.py @@ -22,6 +22,7 @@ from trezorlib import device, messages from ..common import WITH_MOCK_URANDOM from . import reset +from .common import go_next if TYPE_CHECKING: from ..device_handler import BackgroundDeviceHandler @@ -64,7 +65,7 @@ def test_reset_bip39(device_handler: "BackgroundDeviceHandler"): reset.confirm_read(debug, "Success") # Your backup is done - debug.press_yes() + go_next(debug) # TODO: some validation of the generated secret? diff --git a/tests/click_tests/test_tutorial.py b/tests/click_tests/test_tutorial.py new file mode 100644 index 000000000..369ba1d8a --- /dev/null +++ b/tests/click_tests/test_tutorial.py @@ -0,0 +1,86 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2023 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + +from contextlib import contextmanager +from typing import TYPE_CHECKING, Generator + +import pytest + +from trezorlib import device + +if TYPE_CHECKING: + from ..device_handler import BackgroundDeviceHandler + from trezorlib.debuglink import DebugLink + + +# TR-only +pytestmark = [pytest.mark.skip_t1, pytest.mark.skip_t2] + + +@contextmanager +def prepare_tutorial_and_cancel_after_it( + device_handler: "BackgroundDeviceHandler", +) -> Generator["DebugLink", None, None]: + debug = device_handler.debuglink() + device_handler.run(device.show_device_tutorial) + + yield debug + + device_handler.result() + + +def go_through_tutorial(debug: "DebugLink") -> None: + debug.press_right(wait=True) + debug.press_right(wait=True) + debug.press_right_htc(hold_ms=1000) + debug.press_right(wait=True) + debug.press_right(wait=True) + layout = debug.press_middle(wait=True) + assert "Tutorial complete" in layout.text_content() + + +@pytest.mark.setup_client(uninitialized=True) +def test_tutorial_finish(device_handler: "BackgroundDeviceHandler"): + with prepare_tutorial_and_cancel_after_it(device_handler) as debug: + # CLICK THROUGH + go_through_tutorial(debug) + + # FINISH + debug.press_right(wait=True) + + +@pytest.mark.setup_client(uninitialized=True) +def test_tutorial_skip(device_handler: "BackgroundDeviceHandler"): + with prepare_tutorial_and_cancel_after_it(device_handler) as debug: + # SKIP + # debug.press_left() + # debug.press_right() + debug.press_left(wait=True) + debug.press_right(wait=True) + + +@pytest.mark.setup_client(uninitialized=True) +def test_tutorial_again_and_skip(device_handler: "BackgroundDeviceHandler"): + with prepare_tutorial_and_cancel_after_it(device_handler) as debug: + # CLICK THROUGH + go_through_tutorial(debug) + + # AGAIN + debug.press_left(wait=True) + + # SKIP + debug.press_left(wait=True) + debug.press_right(wait=True) diff --git a/tests/common.py b/tests/common.py index 960b979df..0f4d9215e 100644 --- a/tests/common.py +++ b/tests/common.py @@ -15,6 +15,7 @@ # If not, see . import json +import re import time from pathlib import Path from typing import TYPE_CHECKING, Generator, Optional @@ -133,6 +134,22 @@ def recovery_enter_shares( shares: list[str], groups: bool = False, click_info: bool = False, +) -> Generator[None, "ButtonRequest", None]: + if debug.model == "T": + yield from recovery_enter_shares_tt( + debug, shares, groups=groups, click_info=click_info + ) + elif debug.model == "R": + yield from recovery_enter_shares_tr(debug, shares, groups=groups) + else: + raise ValueError(f"Unknown model: {debug.model}") + + +def recovery_enter_shares_tt( + debug: "DebugLink", + shares: list[str], + groups: bool = False, + click_info: bool = False, ) -> Generator[None, "ButtonRequest", None]: """Perform the recovery flow for a set of Shamir shares. @@ -184,6 +201,62 @@ def recovery_enter_shares( debug.press_yes() +def recovery_enter_shares_tr( + debug: "DebugLink", + shares: list[str], + groups: bool = False, +) -> Generator[None, "ButtonRequest", None]: + """Perform the recovery flow for a set of Shamir shares. + + For use in an input flow function. + Example: + + def input_flow(): + yield # start recovery + client.debug.press_yes() + yield from recovery_enter_shares(client.debug, SOME_SHARES) + """ + word_count = len(shares[0].split(" ")) + + # Homescreen - proceed to word number selection + yield + debug.press_yes() + # Input word number + br = yield + assert br.code == ButtonRequestType.MnemonicWordCount + debug.input(str(word_count)) + # Homescreen - proceed to share entry + yield + debug.press_yes() + + # Enter shares + for share in shares: + br = yield + assert br.code == ButtonRequestType.RecoveryHomepage + + # Word entering + yield + debug.press_yes() + + # Enter mnemonic words + for word in share.split(" "): + debug.input(word) + + if groups: + # Confirm share entered + yield + debug.press_yes() + + # Homescreen - continue + # or Homescreen - confirm success + yield + + # Finishing with current share + debug.press_yes() + + yield + + def click_through( debug: "DebugLink", screens: int, code: Optional[ButtonRequestType] = None ) -> Generator[None, "ButtonRequest", None]: @@ -208,6 +281,20 @@ def click_through( def read_and_confirm_mnemonic( debug: "DebugLink", choose_wrong: bool = False +) -> Generator[None, "ButtonRequest", Optional[str]]: + # TODO: these are very similar, reuse some code + if debug.model == "T": + mnemonic = yield from read_and_confirm_mnemonic_tt(debug, choose_wrong) + elif debug.model == "R": + mnemonic = yield from read_and_confirm_mnemonic_tr(debug, choose_wrong) + else: + raise ValueError(f"Unknown model: {debug.model}") + + return mnemonic + + +def read_and_confirm_mnemonic_tt( + debug: "DebugLink", choose_wrong: bool = False ) -> Generator[None, "ButtonRequest", Optional[str]]: """Read a given number of mnemonic words from Trezor T screen and correctly answer confirmation questions. Return the full mnemonic. @@ -236,7 +323,7 @@ def read_and_confirm_mnemonic( debug.press_yes() # check share - for i in range(3): + for _ in range(3): word_pos = int(debug.wait_layout().text_content().split()[2]) index = word_pos - 1 if choose_wrong: @@ -248,6 +335,39 @@ def read_and_confirm_mnemonic( return " ".join(mnemonic) +def read_and_confirm_mnemonic_tr( + debug: "DebugLink", choose_wrong: bool = False +) -> Generator[None, "ButtonRequest", Optional[str]]: + mnemonic: list[str] = [] + br = yield + assert br.pages is not None + for i in range(br.pages - 1): + layout = debug.wait_layout() + # First two pages have just instructions + if i > 1: + words = layout.seed_words() + mnemonic.extend(words) + debug.press_right() + debug.press_yes() + + yield # Select correct words... + debug.press_right() + + # check share + for _ in range(3): + word_pos_match = re.search(r"\d+", debug.wait_layout().title()) + assert word_pos_match is not None + word_pos = int(word_pos_match.group(0)) + index = word_pos - 1 + if choose_wrong: + debug.input(mnemonic[(index + 1) % len(mnemonic)]) + return None + else: + debug.input(mnemonic[index]) + + return " ".join(mnemonic) + + def click_info_button(debug: "DebugLink"): """Click Shamir backup info button and return back.""" debug.press_info() diff --git a/tests/conftest.py b/tests/conftest.py index 3a4116613..639333ef1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -168,7 +168,7 @@ def client( Every test function that requires a client instance will get it from here. If we can't connect to a debuggable device, the test will fail. If 'skip_t2' is used and TT is connected, the test is skipped. Vice versa with T1 - and 'skip_t1'. + and 'skip_t1'. Same with TR. The client instance is wiped and preconfigured with "all all all..." mnemonic, no password and no pin. It is possible to customize this with the `setup_client` @@ -190,6 +190,8 @@ def client( pytest.skip("Test excluded on Trezor T") if request.node.get_closest_marker("skip_t1") and _raw_client.features.model == "1": pytest.skip("Test excluded on Trezor 1") + if request.node.get_closest_marker("skip_tr") and _raw_client.features.model == "R": + pytest.skip("Test excluded on Trezor R") sd_marker = request.node.get_closest_marker("sd_card") if sd_marker and not _raw_client.features.sd_card_present: @@ -352,6 +354,7 @@ def pytest_configure(config: "Config") -> None: # register known markers config.addinivalue_line("markers", "skip_t1: skip the test on Trezor One") config.addinivalue_line("markers", "skip_t2: skip the test on Trezor T") + config.addinivalue_line("markers", "skip_tr: skip the test on Trezor R") config.addinivalue_line( "markers", "experimental: enable experimental features on Trezor" ) @@ -374,8 +377,10 @@ def pytest_runtest_setup(item: pytest.Item) -> None: Ensures that altcoin tests are skipped, and that no test is skipped on both T1 and TT. """ - if item.get_closest_marker("skip_t1") and item.get_closest_marker("skip_t2"): - raise RuntimeError("Don't skip tests for both trezors!") + if all( + item.get_closest_marker(marker) for marker in ("skip_t1", "skip_t2", "skip_tr") + ): + raise RuntimeError("Don't skip tests for all trezor models!") skip_altcoins = int(os.environ.get("TREZOR_PYTEST_SKIP_ALTCOINS", 0)) if item.get_closest_marker("altcoin") and skip_altcoins: diff --git a/tests/device_tests/bitcoin/test_getaddress_show.py b/tests/device_tests/bitcoin/test_getaddress_show.py index 8c4f279e2..c017298f5 100644 --- a/tests/device_tests/bitcoin/test_getaddress_show.py +++ b/tests/device_tests/bitcoin/test_getaddress_show.py @@ -51,6 +51,7 @@ VECTORS = ( # path, script_type, address @pytest.mark.skip_t2 +@pytest.mark.skip_tr @pytest.mark.parametrize("path, script_type, address", VECTORS) def test_show_t1( client: Client, path: str, script_type: messages.InputScriptType, address: str diff --git a/tests/device_tests/bitcoin/test_signmessage.py b/tests/device_tests/bitcoin/test_signmessage.py index ae1f790f2..08b74752a 100644 --- a/tests/device_tests/bitcoin/test_signmessage.py +++ b/tests/device_tests/bitcoin/test_signmessage.py @@ -317,14 +317,17 @@ def test_signmessage_pagination(client: Client, message: str): ) # We cannot differentiate between a newline and space in the message read from Trezor. - expected_message = ( - ("Confirm message: " + message).replace("\n", "").replace(" ", "") - ) - message_read = IF.message_read.replace(" ", "").replace("...", "") - assert expected_message == message_read + # TODO: do the check also for model R + if client.features.model == "T": + expected_message = ( + ("Confirm message: " + message).replace("\n", "").replace(" ", "") + ) + message_read = IF.message_read.replace(" ", "").replace("...", "") + assert expected_message == message_read @pytest.mark.skip_t1 +@pytest.mark.skip_tr(reason="Different screen size") def test_signmessage_pagination_trailing_newline(client: Client): message = "THIS\nMUST NOT\nBE\nPAGINATED\n" # The trailing newline must not cause a new paginated screen to appear. diff --git a/tests/device_tests/bitcoin/test_signtx.py b/tests/device_tests/bitcoin/test_signtx.py index 676a2775e..4f2cd5250 100644 --- a/tests/device_tests/bitcoin/test_signtx.py +++ b/tests/device_tests/bitcoin/test_signtx.py @@ -1516,6 +1516,7 @@ def test_lock_time_datetime(client: Client, lock_time_str: str): @pytest.mark.skip_t1(reason="Cannot test layouts on T1") +@pytest.mark.skip_tr(reason="Not implemented yet") def test_information(client: Client): # input tx: 0dac366fd8a67b2a89fbb0d31086e7acded7a5bbf9ef9daa935bc873229ef5b5 @@ -1568,6 +1569,7 @@ def test_information(client: Client): @pytest.mark.skip_t1(reason="Cannot test layouts on T1") +@pytest.mark.skip_tr(reason="Not implemented yet") def test_information_mixed(client: Client): inp1 = messages.TxInputType( address_n=parse_path("m/44h/1h/0h/0/0"), # mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q @@ -1624,6 +1626,7 @@ def test_information_mixed(client: Client): @pytest.mark.skip_t1(reason="Cannot test layouts on T1") +@pytest.mark.skip_tr(reason="Not implemented yet") def test_information_cancel(client: Client): # input tx: 0dac366fd8a67b2a89fbb0d31086e7acded7a5bbf9ef9daa935bc873229ef5b5 diff --git a/tests/device_tests/bitcoin/test_signtx_payreq.py b/tests/device_tests/bitcoin/test_signtx_payreq.py index 31fb52014..d199c7ade 100644 --- a/tests/device_tests/bitcoin/test_signtx_payreq.py +++ b/tests/device_tests/bitcoin/test_signtx_payreq.py @@ -177,6 +177,9 @@ def test_payment_request(client: Client, payment_request_params): def test_payment_request_details(client: Client): + if client.features.model == "R": + pytest.skip("Details not implemented on TR") + # Test that payment request details are shown when requested. outputs[0].payment_req_index = 0 outputs[1].payment_req_index = 0 diff --git a/tests/device_tests/bitcoin/test_signtx_replacement.py b/tests/device_tests/bitcoin/test_signtx_replacement.py index f19e6d936..474bb2601 100644 --- a/tests/device_tests/bitcoin/test_signtx_replacement.py +++ b/tests/device_tests/bitcoin/test_signtx_replacement.py @@ -115,7 +115,7 @@ def test_p2pkh_fee_bump(client: Client): orig_index=1, ) - new_model = client.features.model in ("T",) + new_model = client.features.model in ("T", "R") with client: client.set_expected_responses( diff --git a/tests/device_tests/cardano/test_sign_tx.py b/tests/device_tests/cardano/test_sign_tx.py index 2eb969fc4..2a58bd0b2 100644 --- a/tests/device_tests/cardano/test_sign_tx.py +++ b/tests/device_tests/cardano/test_sign_tx.py @@ -30,11 +30,14 @@ pytestmark = [ def show_details_input_flow(client: Client): - SHOW_ALL_BUTTON_POSITION = (143, 167) - yield client.debug.wait_layout() - client.debug.click(SHOW_ALL_BUTTON_POSITION) + # Clicking for model T, pressing right for model R + if client.features.model == "T": + SHOW_ALL_BUTTON_POSITION = (143, 167) + client.debug.click(SHOW_ALL_BUTTON_POSITION) + elif client.features.model == "R": + client.debug.press_yes() # reset ui flow to continue "automatically" client.ui.input_flow = None yield diff --git a/tests/device_tests/ethereum/test_definitions.py b/tests/device_tests/ethereum/test_definitions.py index 95d364c34..b3ff6ad8d 100644 --- a/tests/device_tests/ethereum/test_definitions.py +++ b/tests/device_tests/ethereum/test_definitions.py @@ -198,7 +198,9 @@ METHODS = ( _call_getaddress, _call_signmessage, pytest.param(_call_sign_typed_data, marks=pytest.mark.skip_t1), - pytest.param(_call_sign_typed_data_hash, marks=pytest.mark.skip_t2), + pytest.param( + _call_sign_typed_data_hash, marks=[pytest.mark.skip_t2, pytest.mark.skip_tr] + ), ) diff --git a/tests/device_tests/ethereum/test_getpublickey.py b/tests/device_tests/ethereum/test_getpublickey.py index 711ca2946..c244f4c88 100644 --- a/tests/device_tests/ethereum/test_getpublickey.py +++ b/tests/device_tests/ethereum/test_getpublickey.py @@ -45,6 +45,7 @@ def test_slip25_disallowed(client: Client): @pytest.mark.skip_t2 +@pytest.mark.skip_tr def test_legacy_restrictions(client: Client): path = parse_path("m/46'") with pytest.raises(TrezorFailure, match="Invalid path for EthereumGetPublicKey"): diff --git a/tests/device_tests/ethereum/test_sign_typed_data.py b/tests/device_tests/ethereum/test_sign_typed_data.py index 8cc809d98..65d8a2991 100644 --- a/tests/device_tests/ethereum/test_sign_typed_data.py +++ b/tests/device_tests/ethereum/test_sign_typed_data.py @@ -42,6 +42,7 @@ def test_ethereum_sign_typed_data(client: Client, parameters, result): @pytest.mark.skip_t2 +@pytest.mark.skip_tr @parametrize_using_common_fixtures("ethereum/sign_typed_data.json") def test_ethereum_sign_typed_data_blind(client: Client, parameters, result): with client: diff --git a/tests/device_tests/ethereum/test_signtx.py b/tests/device_tests/ethereum/test_signtx.py index 0f2d6000e..313ef2fbc 100644 --- a/tests/device_tests/ethereum/test_signtx.py +++ b/tests/device_tests/ethereum/test_signtx.py @@ -356,6 +356,8 @@ def input_flow_scroll_down(client: Client, cancel: bool = False): def input_flow_go_back(client: Client, cancel: bool = False): + if client.features.model == "R": + pytest.skip("Go back not supported for model R") return InputFlowEthereumSignTxGoBack(client, cancel).get() diff --git a/tests/device_tests/reset_recovery/test_recovery_bip39_dryrun.py b/tests/device_tests/reset_recovery/test_recovery_bip39_dryrun.py index 941ce3817..a647769ad 100644 --- a/tests/device_tests/reset_recovery/test_recovery_bip39_dryrun.py +++ b/tests/device_tests/reset_recovery/test_recovery_bip39_dryrun.py @@ -81,6 +81,7 @@ def test_seed_mismatch(client: Client): @pytest.mark.skip_t2 +@pytest.mark.skip_tr def test_invalid_seed_t1(client: Client): with pytest.raises(exceptions.TrezorFailure, match="Invalid seed"): do_recover(client, ["stick"] * 12) diff --git a/tests/device_tests/reset_recovery/test_recovery_bip39_t1.py b/tests/device_tests/reset_recovery/test_recovery_bip39_t1.py index 17ccadf60..05b7972d2 100644 --- a/tests/device_tests/reset_recovery/test_recovery_bip39_t1.py +++ b/tests/device_tests/reset_recovery/test_recovery_bip39_t1.py @@ -25,7 +25,7 @@ from ...common import MNEMONIC12 PIN4 = "1234" PIN6 = "789456" -pytestmark = pytest.mark.skip_t2 +pytestmark = [pytest.mark.skip_t2, pytest.mark.skip_tr] @pytest.mark.setup_client(uninitialized=True) diff --git a/tests/device_tests/reset_recovery/test_recovery_slip39_basic.py b/tests/device_tests/reset_recovery/test_recovery_slip39_basic.py index 0bbf6fb2b..e48b3653e 100644 --- a/tests/device_tests/reset_recovery/test_recovery_slip39_basic.py +++ b/tests/device_tests/reset_recovery/test_recovery_slip39_basic.py @@ -117,6 +117,8 @@ def test_noabort(client: Client): @pytest.mark.setup_client(uninitialized=True) def test_ask_word_number(client: Client): + if client.features.model == "R": + pytest.skip("Flow is not working correctly for TR") with client: IF = InputFlowSlip39BasicRecoveryRetryFirst(client) client.set_input_flow(IF.get()) diff --git a/tests/device_tests/reset_recovery/test_reset_bip39_skipbackup.py b/tests/device_tests/reset_recovery/test_reset_bip39_skipbackup.py index 0c84b2982..ee7db378d 100644 --- a/tests/device_tests/reset_recovery/test_reset_bip39_skipbackup.py +++ b/tests/device_tests/reset_recovery/test_reset_bip39_skipbackup.py @@ -22,7 +22,7 @@ from trezorlib.debuglink import TrezorClientDebugLink as Client from ...common import EXTERNAL_ENTROPY, generate_entropy -pytestmark = pytest.mark.skip_t2 +pytestmark = [pytest.mark.skip_t2, pytest.mark.skip_tr] STRENGTH = 128 diff --git a/tests/device_tests/reset_recovery/test_reset_bip39_t1.py b/tests/device_tests/reset_recovery/test_reset_bip39_t1.py index b847bf87d..9ad7b1a38 100644 --- a/tests/device_tests/reset_recovery/test_reset_bip39_t1.py +++ b/tests/device_tests/reset_recovery/test_reset_bip39_t1.py @@ -23,7 +23,7 @@ from trezorlib.tools import parse_path from ...common import EXTERNAL_ENTROPY, generate_entropy -pytestmark = pytest.mark.skip_t2 +pytestmark = [pytest.mark.skip_t2, pytest.mark.skip_tr] def reset_device(client: Client, strength: int): diff --git a/tests/device_tests/reset_recovery/test_reset_bip39_t2.py b/tests/device_tests/reset_recovery/test_reset_bip39_t2.py index 87e9b1b45..aacbb75ba 100644 --- a/tests/device_tests/reset_recovery/test_reset_bip39_t2.py +++ b/tests/device_tests/reset_recovery/test_reset_bip39_t2.py @@ -166,6 +166,12 @@ def test_failed_pin(client: Client): client.debug.input("654") ret = client.call_raw(messages.ButtonAck()) + # Re-enter PIN for TR + if client.debug.model == "R": + assert isinstance(ret, messages.ButtonRequest) + client.debug.press_yes() + ret = client.call_raw(messages.ButtonAck()) + # Enter PIN for second time assert isinstance(ret, messages.ButtonRequest) client.debug.input("456") diff --git a/tests/device_tests/test_bip32_speed.py b/tests/device_tests/test_bip32_speed.py index 6043e0be6..aee9cf8ce 100644 --- a/tests/device_tests/test_bip32_speed.py +++ b/tests/device_tests/test_bip32_speed.py @@ -25,6 +25,7 @@ from trezorlib.tools import H_ pytestmark = [ pytest.mark.skip_t2, + pytest.mark.skip_tr, pytest.mark.flaky(max_runs=5), ] diff --git a/tests/device_tests/test_debuglink.py b/tests/device_tests/test_debuglink.py index d8ee68d7a..492ec0590 100644 --- a/tests/device_tests/test_debuglink.py +++ b/tests/device_tests/test_debuglink.py @@ -25,12 +25,14 @@ from ..common import MNEMONIC12 @pytest.mark.skip_t2 +@pytest.mark.skip_tr def test_layout(client: Client): layout = client.debug.state().layout assert len(layout) == 1024 @pytest.mark.skip_t2 +@pytest.mark.skip_tr @pytest.mark.setup_client(mnemonic=MNEMONIC12) def test_mnemonic(client: Client): client.ensure_unlocked() @@ -39,6 +41,7 @@ def test_mnemonic(client: Client): @pytest.mark.skip_t2 +@pytest.mark.skip_tr @pytest.mark.setup_client(mnemonic=MNEMONIC12, pin="1234", passphrase="") def test_pin(client: Client): resp = client.call_raw(messages.GetAddress(address_n=parse_path("m/44'/0'/0'/0/0"))) diff --git a/tests/device_tests/test_firmware_hash.py b/tests/device_tests/test_firmware_hash.py index 7b6072e3d..ef5986899 100644 --- a/tests/device_tests/test_firmware_hash.py +++ b/tests/device_tests/test_firmware_hash.py @@ -8,6 +8,7 @@ from trezorlib.debuglink import TrezorClientDebugLink as Client FIRMWARE_LENGTHS = { "1": 7 * 128 * 1024 + 64 * 1024, "T": 13 * 128 * 1024, + "R": 13 * 128 * 1024, } diff --git a/tests/device_tests/test_msg_applysettings.py b/tests/device_tests/test_msg_applysettings.py index 9ab10f739..a2a14861f 100644 --- a/tests/device_tests/test_msg_applysettings.py +++ b/tests/device_tests/test_msg_applysettings.py @@ -33,10 +33,20 @@ EXPECTED_RESPONSES_NOPIN = [ EXPECTED_RESPONSES_PIN_T1 = [messages.PinMatrixRequest()] + EXPECTED_RESPONSES_NOPIN EXPECTED_RESPONSES_PIN_TT = [messages.ButtonRequest()] + EXPECTED_RESPONSES_NOPIN +EXPECTED_RESPONSES_EXPERIMENTAL_FEATURES = [ + messages.ButtonRequest, + messages.Success, + messages.Features, +] + PIN4 = "1234" +# All the tests are starting with a setup client! pytestmark = pytest.mark.setup_client(pin=PIN4) +T1_HOMESCREEN = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"\x00\x00\x00\x00\x04\x80\x00\x00\x00\x00\x00\x00\x00\x00\x04\x88\x02\x00\x00\x00\x02\x91\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x00\x90@\x00\x11@\x00\x00\x00\x00\x00\x00\x08\x00\x10\x92\x12\x04\x00\x00\x05\x12D\x00\x00\x00\x00\x00 \x00\x00\x08\x00Q\x00\x00\x02\xc0\x00\x00\x00\x00\x00\x00\x00\x10\x02 \x01\x04J\x00)$\x00\x00\x00\x00\x80\x00\x00\x00\x00\x08\x10\xa1\x00\x00\x02\x81 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\tP\x00\x00\x00\x00\x00\x00 \x00\x00\xa0\x00\xa0R \x12\x84\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\t\x08\x00\tP\x00\x00\x00\x00 \x00\x04 \x00\x80\x02\x00@\x02T\xc2 \x00\x00\x00\x00\x00\x00\x00\x10@\x00)\t@\n\xa0\x80\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x80@\x14\xa9H\x04\x00\x00\x88@\x00\x00\x00\x00\x00\x02\x02$\x00\x15B@\x00\nP\x00\x00\x00\x00\x00\x80\x00\x00\x91\x01UP\x00\x00 \x02\x00\x00\x00\x00\x00\x00\x02\x08@ Z\xa5 \x00\x00\x80\x00\x00\x00\x00\x00\x00\x08\xa1%\x14*\xa0\x00\x00\x02\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00@\xaa\x91 \x00\x05E\x80\x00\x00\x00\x00\x00\x02*T\x05-D\x00\x00\x05 @\x00\x00\x00\x00\x00%@\x80\x11V\xa0\x88\x00\x05@\xb0\x00\x00\x00\x00\x00\x818$\x04\xabD \x00\x06\xa1T\x00\x00\x00\x00\x02\x03\xb8\x01R\xd5\x01\x00\x00\x05AP\x00\x00\x00\x00\x08\xadT\x00\x05j\xa4@\x00\x87ah\x00\x00\x00\x00\x02\x8d\xb8\x08\x00.\x01\x00\x00\x02\xa5\xa8\x10\x00\x00\x00*\xc1\xec \n\xaa\x88 \x02@\xf6\xd0\x02\x00\x00\x00\x0bB\xb6\x14@U"\x80\x00\x01{`\x00\x00\x00\x00M\xa3\xf8 \x15*\x00\x00\x00\x10n\xc0\x04\x00\x00\x02\x06\xc2\xa8)\x00\x96\x84\x80\x00\x00\x1b\x00\x00\x80@\x10\x87\xa7\xf0\x84\x10\xaa\x10\x00\x00D\x00\x00\x02 \x00\x8a\x06\xfa\xe0P\n-\x02@\x00\x12\x00\x00\x00\x00\x10@\x83\xdf\xa0\x00\x08\xaa@\x00\x00\x01H\x00\x05H\x04\x12\x01\xf7\x81P\x02T\t\x00\x00\x00 \x00\x00\x84\x10\x00\x00z\x00@)* \x00\x00\x01\n\xa0\x02 \x05\n\x00\x00\x05\x10\x84\xa8\x84\x80\x00\x00@\x14\x00\x92\x10\x80\x00\x04\x11@\tT\x00\x00\x00\x00\n@\x00\x08\x84@$\x00H\x00\x12Q\x02\x00\x00\x00\x00\x90\x02A\x12\xa8\n\xaa\x92\x10\x04\xa8\x10@\x00\x00\x04\x04\x00\x04I\x00\x04\x14H\x80"R\x01\x00\x00\x00!@\x00\x00$\xa0EB\x80\x08\x95hH\x00\x00\x00\x84\x10 \x05Z\x00\x00(\x00\x02\x00\xa1\x01\x00\x00\x04\x00@\x82\x00\xadH*\x92P\x00\xaaP\x00\x00\x00\x00\x11\x02\x01*\xad\x01\x00\x01\x01"\x11D\x08\x00\x00\x10\x80 \x00\x81W\x80J\x94\x04\x08\xa5 !\x00\x00\x00\x02\x00B*\xae\xa1\x00\x80\x10\x01\x08\xa4\x00\x00\x00\x00\x00\x84\x00\t[@"HA\x04E\x00\x84\x00\x00\x00\x10\x00\x01J\xd5\x82\x90\x02\x00!\x02\xa2\x00\x00\x00\x00\x00\x00\x00\x05~\xa0\x00 \x10\n)\x00\x11\x00\x00\x00\x00\x00\x00!U\x80\xa8\x88\x82\x80\x01\x00\x00\x00\x00\x00\x00H@\x11\xaa\xc0\x82\x00 *\n\x00\x00\x00\x00\x00\x00\x00\x00\n\xabb@ \x04\x00! \x84\x00\x00\x00\x00\x02@\xa5\x15A$\x04\x81(\n\x00\x00\x00\x00\x00\x00 \x01\x10\x02\xe0\x91\x02\x00\x00\x04\x00\x00\x00\x00\x00\x00\x01 \xa9\tQH@\x91 P\x00\x00\x00\x00\x00\x00\x08\x00\x00\xa0T\xa5\x00@\x80\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"\x00\x00\x00\x00\xa2\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00 T\xa0\t\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00@\x02\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00*\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x10\x00\x00\x10\x02\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\t\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00@\x04\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x08@\x10\x00\x00\x00\x00' +TR_HOMESCREEN = b"TOIG\x80\x00@\x00\x0c\x04\x00\x00\xa5RY\x96\xdc0\x08\xe4\x06\xdc\xff\x96\xdc\x80\xa8\x16\x90z\xd2y\xf9\x18{\xc0\xf1\xe5\xc9y\x0f\x95\x7f;C\xfe\xd0\xe1K\xefS\x96o\xf9\xb739\x1a\n\xc7\xde\x89\xff\x11\xd8=\xd5\xcf\xb1\x9f\xf7U\xf2\xa3spx\xb0&t\xe4\xaf3x\xcaT\xec\xe50k\xb4\xe8\nl\x16\xbf`'\xf3\xa7Z\x8d-\x98h\x1c\x03\x07\xf0\xcf\xf0\x8aD\x13\xec\x1f@y\x9e\xd8\xa3\xc6\x84F*\x1dx\x02U\x00\x10\xd3\x8cF\xbb\x97y\x18J\xa5T\x18x\x1c\x02\xc6\x90\xfd\xdc\x89\x1a\x94\xb3\xeb\x01\xdc\x9f2\x8c/\xe9/\x8c$\xc6\x9c\x1e\xf8C\x8f@\x17Q\x1d\x11F\x02g\xe4A \xebO\xad\xc6\xe3F\xa7\x8b\xf830R\x82\x0b\x8e\x16\x1dL,\x14\xce\x057tht^\xfe\x00\x9e\x86\xc2\x86\xa3b~^Bl\x18\x1f\xb9+w\x11\x14\xceO\xe9\xb6W\xd8\x85\xbeX\x17\xc2\x13,M`y\xd1~\xa3/\xcd0\xed6\xda\xf5b\x15\xb5\x18\x0f_\xf6\xe2\xdc\x8d\x8ez\xdd\xd5\r^O\x9e\xb6|\xc4e\x0f\x1f\xff0k\xd4\xb8\n\x12{\x8d\x8a>\x0b5\xa2o\xf2jZ\xe5\xee\xdc\x14\xd1\xbd\xd5\xad\x95\xbe\x8c\t\x8f\xb9\xde\xc4\xa551,#`\x94'\x1b\xe7\xd53u\x8fq\xbd4v>3\x8f\xcc\x1d\xbcV>\x90^\xb3L\xc3\xde0]\x05\xec\x83\xd0\x07\xd2(\xbb\xcf+\xd0\xc7ru\xecn\x14k-\xc0|\xd2\x0e\xe8\xe08\xa8<\xdaQ+{\xad\x01\x02#\x16\x12+\xc8\xe0P\x06\xedD7\xae\xd0\xa4\x97\x84\xe32\xca;]\xd04x:\x94`\xbe\xca\x89\xe2\xcb\xc5L\x03\xac|\xe7\xd5\x1f\xe3\x08_\xee!\x04\xd2\xef\x00\xd8\xea\x91p)\xed^#\xb1\xa78eJ\x00F*\xc7\xf1\x0c\x1a\x04\xf5l\xcc\xfc\xa4\x83,c\x1e\xb1>\xc5q\x8b\xe6Y9\xc7\x07\xfa\xcf\xf9\x15\x8a\xdd\x11\x1f\x98\x82\xbe>\xbe+u#g]aC\\\x1bC\xb1\xe8P\xce2\xd6\xb6r\x12\x1c*\xd3\x92\x9d9\xf9cB\x82\xf9S.\xc2B\xe7\x9d\xcf\xdb\xf3\xfd#\xfd\x94x9p\x8d%\x14\xa5\xb3\xe9p5\xa1;~4:\xcd\xe0&\x11\x1d\xe9\xf6\xa1\x1fw\xf54\x95eWx\xda\xd0u\x91\x86\xb8\xbc\xdf\xdc\x008f\x15\xc6\xf6\x7f\xf0T\xb8\xc1\xa3\xc5_A\xc0G\x930\xe7\xdc=\xd5\xa7\xc1\xbcI\x16\xb8s\x9c&\xaa\x06\xc1}\x8b\x19\x9d'c\xc3\xe3^\xc3m\xb6n\xb0(\x16\xf6\xdeg\xb3\x96:i\xe5\x9c\x02\x93\x9fF\x9f-\xa7\"w\xf3X\x9f\x87\x08\x84\"v,\xab!9: GeneratorType: + def input_flow_common(self) -> GeneratorType: yield # do you want to set/change the wipe code? self.debug.press_yes() + if self.debug.model == "R": + yield from swipe_if_necessary(self.debug) # wipe code info + self.debug.press_yes() + yield # enter current pin self.debug.input(self.pin) yield # enter new wipe code @@ -107,13 +111,20 @@ class InputFlowNewCodeMismatch(InputFlowBase): self.first_code = first_code self.second_code = second_code - def input_flow_tt(self) -> GeneratorType: + def input_flow_common(self) -> GeneratorType: yield # do you want to set/change the pin/wipe code? self.debug.press_yes() + if self.debug.model == "R": + yield from swipe_if_necessary(self.debug) # code info + self.debug.press_yes() + def input_two_different_pins(): yield # enter new PIN/wipe_code self.debug.input(self.first_code) + if self.debug.model == "R": + yield # Please re-enter PIN to confirm + self.debug.press_yes() yield # enter new PIN/wipe_code again (but different) self.debug.input(self.second_code) @@ -141,7 +152,7 @@ class InputFlowCodeChangeFail(InputFlowBase): self.new_pin_1 = new_pin_1 self.new_pin_2 = new_pin_2 - def input_flow_tt(self) -> GeneratorType: + def input_flow_common(self) -> GeneratorType: yield # do you want to change pin? self.debug.press_yes() yield # enter current pin @@ -149,6 +160,11 @@ class InputFlowCodeChangeFail(InputFlowBase): yield # enter new pin self.debug.input(self.new_pin_1) + + if self.debug.model == "R": + yield # Please re-enter PIN to confirm + self.debug.press_yes() + yield # enter new pin again (but different) self.debug.input(self.new_pin_2) @@ -165,7 +181,7 @@ class InputFlowWrongPIN(InputFlowBase): super().__init__(client) self.wrong_pin = wrong_pin - def input_flow_tt(self) -> GeneratorType: + def input_flow_common(self) -> GeneratorType: yield # do you want to change pin? self.debug.press_yes() yield # enter wrong current pin @@ -180,7 +196,7 @@ class InputFlowPINBackoff(InputFlowBase): self.wrong_pin = wrong_pin self.good_pin = good_pin - def input_flow_tt(self) -> GeneratorType: + def input_flow_common(self) -> GeneratorType: """Inputting some bad PINs and finally the correct one""" yield # PIN entry for attempt in range(3): @@ -218,6 +234,19 @@ class InputFlowSignMessagePagination(InputFlowBase): self.debug.press_yes() + def input_flow_tr(self) -> GeneratorType: + # confirm address + yield + self.debug.press_yes() + + br = yield + # TODO: try load the message_read the same way as in model T + if br.pages is not None: + for i in range(br.pages): + if i < br.pages - 1: + self.debug.swipe_up() + self.debug.press_yes() + class InputFlowShowAddressQRCode(InputFlowBase): def __init__(self, client: Client): @@ -237,6 +266,17 @@ class InputFlowShowAddressQRCode(InputFlowBase): self.debug.press_no(wait=True) self.debug.press_yes() + def input_flow_tr(self) -> GeneratorType: + yield + # Go into details + self.debug.press_right() + # Go through details and back + self.debug.press_right() + self.debug.press_left() + self.debug.press_left() + # Confirm + self.debug.press_middle() + class InputFlowShowAddressQRCodeCancel(InputFlowBase): def __init__(self, client: Client): @@ -253,6 +293,19 @@ class InputFlowShowAddressQRCodeCancel(InputFlowBase): self.debug.press_no(wait=True) self.debug.press_yes() + def input_flow_tr(self) -> GeneratorType: + yield + # Go into details + self.debug.press_right() + # Go through details and back + self.debug.press_right() + self.debug.press_left() + self.debug.press_left() + # Cancel + self.debug.press_left() + # Confirm address mismatch + self.debug.press_right() + class InputFlowShowMultisigXPUBs(InputFlowBase): def __init__(self, client: Client, address: str, xpubs: list[str], index: int): @@ -293,6 +346,44 @@ class InputFlowShowMultisigXPUBs(InputFlowBase): # show address self.debug.press_yes() + def input_flow_tr(self) -> GeneratorType: + yield # show address + layout = self.debug.wait_layout() + assert "RECEIVE ADDRESS (MULTISIG)" in layout.title() + assert layout.text_content().replace(" ", "") == self.address + + self.debug.press_right() + assert "Qr" in self.debug.wait_layout().all_components() + + layout = self.debug.press_right(wait=True) + # address details + # TODO: locate it more precisely + assert "Multisig 2 of 3" in layout.json_str + + # Three xpub pages with the same testing logic + for xpub_num in range(3): + expected_title = f"MULTISIG XPUB #{xpub_num + 1} " + ( + "(YOURS)" if self.index == xpub_num else "(COSIGNER)" + ) + layout = self.debug.press_right(wait=True) + assert expected_title in layout.title() + xpub_part_1 = layout.text_content().replace(" ", "") + # Press "SHOW MORE" + layout = self.debug.press_middle(wait=True) + xpub_part_2 = layout.text_content().replace(" ", "") + # Go back + self.debug.press_left(wait=True) + assert self.xpubs[xpub_num] == xpub_part_1 + xpub_part_2 + + for _ in range(5): + self.debug.press_left() + # show address + self.debug.press_left() + # address mismatch + self.debug.press_left() + # show address + self.debug.press_middle() + class InputFlowPaymentRequestDetails(InputFlowBase): def __init__(self, client: Client, outputs: list[messages.TxOutputType]): @@ -346,10 +437,18 @@ class InputFlowSignTxHighFee(InputFlowBase): ] yield from self.go_through_all_screens(screens) + def input_flow_tr(self) -> GeneratorType: + screens = [ + B.ConfirmOutput, + B.FeeOverThreshold, + B.SignTx, + ] + yield from self.go_through_all_screens(screens) + def lock_time_input_flow_tt( debug: DebugLink, - layout_assert_func: Callable[[str], None], + layout_assert_func: Callable[[DebugLink], None], double_confirm: bool = False, ) -> GeneratorType: yield # confirm output @@ -360,8 +459,7 @@ def lock_time_input_flow_tt( debug.press_yes() yield # confirm locktime - layout_text = debug.wait_layout().text_content() - layout_assert_func(layout_text) + layout_assert_func(debug) debug.press_yes() yield # confirm transaction @@ -371,19 +469,43 @@ def lock_time_input_flow_tt( debug.press_yes() +def lock_time_input_flow_tr( + debug: DebugLink, layout_assert_func: Callable[[DebugLink], None] +) -> GeneratorType: + yield # confirm output + debug.wait_layout() + debug.swipe_up() + debug.wait_layout() + debug.press_yes() + + yield # confirm locktime + layout_assert_func(debug) + debug.press_yes() + + yield # confirm transaction + debug.press_yes() + + class InputFlowLockTimeBlockHeight(InputFlowBase): def __init__(self, client: Client, block_height: str): super().__init__(client) self.block_height = block_height - def layout_assert_func(self, layout_text: str) -> None: - assert "blockheight" in layout_text - assert self.block_height in layout_text - def input_flow_tt(self) -> GeneratorType: - yield from lock_time_input_flow_tt( - self.debug, self.layout_assert_func, double_confirm=True - ) + def assert_func(debug: DebugLink) -> None: + layout_text = debug.wait_layout().text_content() + assert "blockheight" in layout_text + assert self.block_height in layout_text + + yield from lock_time_input_flow_tt(self.debug, assert_func, double_confirm=True) + + def input_flow_tr(self) -> GeneratorType: + def assert_func(debug: DebugLink) -> None: + assert "blockheight" in debug.wait_layout().text_content() + debug.press_right() + assert self.block_height in debug.wait_layout().text_content() + + yield from lock_time_input_flow_tr(self.debug, assert_func) class InputFlowLockTimeDatetime(InputFlowBase): @@ -391,11 +513,21 @@ class InputFlowLockTimeDatetime(InputFlowBase): super().__init__(client) self.lock_time_str = lock_time_str - def layout_assert_func(self, layout_text: str) -> None: - assert self.lock_time_str in layout_text - def input_flow_tt(self) -> GeneratorType: - yield from lock_time_input_flow_tt(self.debug, self.layout_assert_func) + def assert_func(debug: DebugLink): + layout_text = debug.wait_layout().text_content() + assert "Locktime" in layout_text + assert self.lock_time_str in layout_text + + yield from lock_time_input_flow_tt(self.debug, assert_func) + + def input_flow_tr(self) -> GeneratorType: + def assert_func(debug: DebugLink): + assert "Locktime" in debug.wait_layout().text_content() + debug.press_right() + assert self.lock_time_str in debug.wait_layout().text_content() + + yield from lock_time_input_flow_tr(self.debug, assert_func) class InputFlowEIP712ShowMore(InputFlowBase): @@ -407,9 +539,12 @@ class InputFlowEIP712ShowMore(InputFlowBase): def _confirm_show_more(self) -> None: """Model-specific, either clicks a screen or presses a button.""" - self.debug.click(self.SHOW_MORE) + if self.model() == "T": + self.debug.click(self.SHOW_MORE) + elif self.model() == "R": + self.debug.press_right() - def input_flow_tt(self) -> GeneratorType: + def input_flow_common(self) -> GeneratorType: """Triggers show more wherever possible""" yield # confirm address self.debug.press_yes() @@ -456,7 +591,7 @@ class InputFlowEIP712Cancel(InputFlowBase): def __init__(self, client: Client): super().__init__(client) - def input_flow_tt(self) -> GeneratorType: + def input_flow_common(self) -> GeneratorType: """Clicks cancelling button""" yield # confirm address self.debug.press_yes() @@ -470,7 +605,7 @@ class InputFlowEthereumSignTxSkip(InputFlowBase): super().__init__(client) self.cancel = cancel - def input_flow_tt(self) -> GeneratorType: + def input_flow_common(self) -> GeneratorType: yield # confirm address self.debug.press_yes() yield # confirm amount @@ -527,6 +662,27 @@ class InputFlowEthereumSignTxScrollDown(InputFlowBase): yield # hold to confirm self.debug.press_yes() + def input_flow_tr(self) -> GeneratorType: + yield # confirm address + self.debug.wait_layout() + self.debug.press_yes() + + br = yield # paginated data + assert br.pages is not None + for _ in range(br.pages): + self.debug.wait_layout() + self.debug.swipe_up() + + yield # confirm amount + self.debug.wait_layout() + self.debug.press_yes() + + yield # confirm before send + if self.cancel: + self.debug.press_no() + else: + self.debug.press_yes() + class InputFlowEthereumSignTxGoBack(InputFlowBase): SHOW_ALL = (143, 167) @@ -597,7 +753,7 @@ class InputFlowBip39Backup(InputFlowBase): super().__init__(client) self.mnemonic = None - def input_flow_tt(self) -> GeneratorType: + def input_flow_common(self) -> GeneratorType: # 1. Confirm Reset yield from click_through(self.debug, screens=1, code=B.ResetDevice) @@ -611,7 +767,7 @@ class InputFlowBip39ResetBackup(InputFlowBase): self.mnemonic = None # NOTE: same as above, just two more YES - def input_flow_tt(self) -> GeneratorType: + def input_flow_common(self) -> GeneratorType: # 1. Confirm Reset # 2. Backup your seed # 3. Confirm warning @@ -626,7 +782,7 @@ class InputFlowBip39ResetPIN(InputFlowBase): super().__init__(client) self.mnemonic = None - def input_flow_tt(self) -> GeneratorType: + def input_flow_common(self) -> GeneratorType: br = yield # Confirm Reset assert br.code == B.ResetDevice self.debug.press_yes() @@ -634,6 +790,10 @@ class InputFlowBip39ResetPIN(InputFlowBase): yield # Enter new PIN self.debug.input("654") + if self.debug.model == "R": + yield # Re-enter PIN + self.debug.press_yes() + yield # Confirm PIN self.debug.input("654") @@ -666,7 +826,7 @@ class InputFlowBip39ResetFailedCheck(InputFlowBase): super().__init__(client) self.mnemonic = None - def input_flow_tt(self) -> GeneratorType: + def input_flow_common(self) -> GeneratorType: # 1. Confirm Reset # 2. Backup your seed # 3. Confirm warning @@ -742,6 +902,31 @@ class InputFlowSlip39BasicBackup(InputFlowBase): assert br.code == B.Success self.debug.press_yes() + def input_flow_tr(self) -> GeneratorType: + yield # Checklist + self.debug.press_yes() + yield # Number of shares info + self.debug.press_yes() + yield # Number of shares (5) + self.debug.input("5") + yield # Checklist + self.debug.press_yes() + yield # Threshold info + self.debug.press_yes() + yield # Threshold (3) + self.debug.input("3") + yield # Checklist + self.debug.press_yes() + yield # Confirm show seeds + self.debug.press_yes() + + # Mnemonic phrases + self.mnemonics = yield from load_5_shares(self.debug) + + br = yield # Confirm backup + assert br.code == B.Success + self.debug.press_yes() + class InputFlowSlip39BasicResetRecovery(InputFlowBase): def __init__(self, client: Client): @@ -766,6 +951,35 @@ class InputFlowSlip39BasicResetRecovery(InputFlowBase): assert br.code == B.Success self.debug.press_yes() + def input_flow_tr(self) -> GeneratorType: + yield # Confirm Reset + self.debug.press_yes() + yield # Backup your seed + self.debug.press_yes() + yield # Checklist + self.debug.press_yes() + yield # Number of shares info + self.debug.press_yes() + yield # Number of shares (5) + self.debug.input("5") + yield # Checklist + self.debug.press_yes() + yield # Threshold info + self.debug.press_yes() + yield # Threshold (3) + self.debug.input("3") + yield # Checklist + self.debug.press_yes() + yield # Confirm show seeds + self.debug.press_yes() + + # Mnemonic phrases + self.mnemonics = yield from load_5_shares(self.debug) + + br = yield # Confirm backup + assert br.code == B.Success + self.debug.press_yes() + def load_5_groups_5_shares( debug: DebugLink, @@ -825,6 +1039,36 @@ class InputFlowSlip39AdvancedBackup(InputFlowBase): assert br.code == B.Success self.debug.press_yes() + def input_flow_tr(self) -> GeneratorType: + yield # 1. Checklist + self.debug.press_yes() + yield # 2. Set and confirm group count + self.debug.input("5") + yield # 3. Checklist + self.debug.press_yes() + yield # 4. Set and confirm group threshold + self.debug.input("3") + yield # 5. Checklist + self.debug.press_yes() + for _ in range(5): # for each of 5 groups + yield # Number of shares info + self.debug.press_yes() + yield # Number of shares (5) + self.debug.input("5") + yield # Threshold info + self.debug.press_yes() + yield # Threshold (3) + self.debug.input("3") + yield # Confirm show seeds + self.debug.press_yes() + + # Mnemonic phrases - show & confirm shares for all groups + self.mnemonics = yield from load_5_groups_5_shares(self.debug) + + br = yield # Confirm backup + assert br.code == B.Success + self.debug.press_yes() + class InputFlowSlip39AdvancedResetRecovery(InputFlowBase): def __init__(self, client: Client, click_info: bool): @@ -853,6 +1097,40 @@ class InputFlowSlip39AdvancedResetRecovery(InputFlowBase): assert br.code == B.Success self.debug.press_yes() + def input_flow_tr(self) -> GeneratorType: + yield # Wallet backup + self.debug.press_yes() + yield # Wallet creation + self.debug.press_yes() + yield # Checklist + self.debug.press_yes() + yield # Set and confirm group count + self.debug.input("5") + yield # Checklist + self.debug.press_yes() + yield # Set and confirm group threshold + self.debug.input("3") + yield # Checklist + self.debug.press_yes() + for _ in range(5): # for each of 5 groups + yield # Number of shares info + self.debug.press_yes() + yield # Number of shares (5) + self.debug.input("5") + yield # Threshold info + self.debug.press_yes() + yield # Threshold (3) + self.debug.input("3") + yield # Confirm show seeds + self.debug.press_yes() + + # Mnemonic phrases - show & confirm shares for all groups + self.mnemonics = yield from load_5_groups_5_shares(self.debug) + + br = yield # safety warning + assert br.code == B.Success + self.debug.press_yes() + def enter_recovery_seed_dry_run(debug: DebugLink, mnemonic: list[str]) -> GeneratorType: yield @@ -893,6 +1171,40 @@ class InputFlowBip39RecoveryDryRun(InputFlowBase): self.debug.wait_layout() self.debug.click(buttons.OK) + def input_flow_tr(self) -> GeneratorType: + yield + assert "check the recovery seed" in self.layout().text_content() + self.debug.press_yes() + + yield + assert "number of words" in self.layout().text_content() + self.debug.press_yes() + + yield + yield + assert "NUMBER OF WORDS" in self.layout().title() + word_options = (12, 18, 20, 24, 33) + index = word_options.index(len(self.mnemonic)) + for _ in range(index): + self.debug.press_right() + self.debug.input(str(len(self.mnemonic))) + + yield + assert "Enter recovery seed" in self.layout().text_content() + self.debug.press_yes() + + yield + self.debug.press_yes() + yield + for index, word in enumerate(self.mnemonic): + assert "WORD" in self.layout().title() + assert str(index + 1) in self.layout().title() + self.debug.input(word) + + yield + self.debug.press_right() + self.debug.press_yes() + class InputFlowBip39RecoveryDryRunInvalid(InputFlowBase): def __init__(self, client: Client): @@ -915,6 +1227,47 @@ class InputFlowBip39RecoveryDryRunInvalid(InputFlowBase): assert "ABORT SEED CHECK" == self.layout().title() self.debug.click(buttons.OK) + def input_flow_tr(self) -> GeneratorType: + yield + assert "check the recovery seed" in self.layout().text_content() + self.debug.press_right() + + yield + assert "number of words" in self.layout().text_content() + self.debug.press_yes() + + yield + yield + assert "NUMBER OF WORDS" in self.layout().title() + # select 12 words + self.debug.press_middle() + + yield + assert "Enter recovery seed" in self.layout().text_content() + self.debug.press_yes() + + yield + assert "WORD ENTERING" in self.layout().title() + self.debug.press_yes() + + yield + for _ in range(12): + assert "WORD" in self.layout().title() + self.debug.input("stick") + + br = yield + assert br.code == messages.ButtonRequestType.Warning + assert "invalid recovery seed" in self.layout().text_content() + self.debug.press_right() + + yield # retry screen + assert "number of words" in self.layout().text_content() + self.debug.press_left() + + yield + assert "abort" in self.layout().text_content() + self.debug.press_right() + def bip39_recovery_possible_pin( debug: DebugLink, mnemonic: list[str], pin: Optional[str] @@ -966,6 +1319,53 @@ class InputFlowBip39RecoveryPIN(InputFlowBase): def input_flow_tt(self) -> GeneratorType: yield from bip39_recovery_possible_pin(self.debug, self.mnemonic, pin="654") + def input_flow_tr(self) -> GeneratorType: + yield + assert "By continuing you agree" in self.layout().text_content() + self.debug.press_right() + assert "trezor.io/tos" in self.layout().text_content() + self.debug.press_yes() + + yield + assert "ENTER" in self.layout().text_content() + self.debug.input("654") + + yield + assert "re-enter to confirm" in self.layout().text_content() + self.debug.press_right() + + yield + assert "ENTER" in self.layout().text_content() + self.debug.input("654") + + yield + assert "number of words" in self.layout().text_content() + self.debug.press_yes() + + yield + yield + assert "NUMBER OF WORDS" in self.layout().title() + self.debug.input(str(len(self.mnemonic))) + + yield + assert "Enter recovery seed" in self.layout().text_content() + self.debug.press_yes() + + yield + assert "WORD ENTERING" in self.layout().title() + self.debug.press_right() + + yield + for word in self.mnemonic: + assert "WORD" in self.layout().title() + self.debug.input(word) + + yield + assert ( + "You have finished recovering your wallet." in self.layout().text_content() + ) + self.debug.press_yes() + class InputFlowBip39RecoveryNoPIN(InputFlowBase): def __init__(self, client: Client, mnemonic: list[str]): @@ -975,13 +1375,34 @@ class InputFlowBip39RecoveryNoPIN(InputFlowBase): def input_flow_tt(self) -> GeneratorType: yield from bip39_recovery_possible_pin(self.debug, self.mnemonic, pin=None) + def input_flow_tr(self) -> GeneratorType: + yield # Confirm recovery + self.debug.press_yes() + yield # Homescreen + self.debug.press_yes() + + yield # Enter word count + self.debug.input(str(len(self.mnemonic))) + + yield # Homescreen + self.debug.press_yes() + yield # Homescreen + self.debug.press_yes() + yield # Enter words + for word in self.mnemonic: + self.debug.input(word) + + yield # confirm success + self.debug.press_yes() + yield + class InputFlowSlip39AdvancedRecoveryDryRun(InputFlowBase): def __init__(self, client: Client, shares: list[str]): super().__init__(client) self.shares = shares - def input_flow_tt(self) -> GeneratorType: + def input_flow_common(self) -> GeneratorType: yield # Confirm Dryrun self.debug.press_yes() # run recovery flow @@ -994,7 +1415,7 @@ class InputFlowSlip39AdvancedRecovery(InputFlowBase): self.shares = shares self.click_info = click_info - def input_flow_tt(self) -> GeneratorType: + def input_flow_common(self) -> GeneratorType: yield # Confirm Recovery self.debug.press_yes() # Proceed with recovery @@ -1007,7 +1428,7 @@ class InputFlowSlip39AdvancedRecoveryAbort(InputFlowBase): def __init__(self, client: Client): super().__init__(client) - def input_flow_tt(self) -> GeneratorType: + def input_flow_common(self) -> GeneratorType: yield # Confirm Recovery self.debug.press_yes() yield # Homescreen - abort process @@ -1021,7 +1442,7 @@ class InputFlowSlip39AdvancedRecoveryNoAbort(InputFlowBase): super().__init__(client) self.shares = shares - def input_flow_tt(self) -> GeneratorType: + def input_flow_common(self) -> GeneratorType: yield # Confirm Recovery self.debug.press_yes() yield # Homescreen - abort process @@ -1063,6 +1484,40 @@ class InputFlowSlip39AdvancedRecoveryTwoSharesWarning(InputFlowBase): self.client.cancel() + def input_flow_tr(self) -> GeneratorType: + yield # Confirm Recovery + self.debug.press_yes() + yield # Homescreen - start process + self.debug.press_yes() + yield # Enter number of words + self.debug.input(str(len(self.first_share))) + yield # Homescreen - proceed to share entry + self.debug.press_yes() + yield # Enter first share + self.debug.press_yes() + yield # Enter first share + for word in self.first_share: + self.debug.input(word) + + yield # Continue to next share + self.debug.press_yes() + yield # Homescreen - next share + self.debug.press_yes() + yield # Homescreen - next share + self.debug.press_yes() + yield # Enter next share + for word in self.second_share: + self.debug.input(word) + + yield + br = yield + assert br.code == messages.ButtonRequestType.Warning + self.debug.press_right() + self.debug.press_yes() + yield + + self.client.cancel() + def slip39_recovery_possible_pin( debug: DebugLink, shares: list[str], pin: Optional[str] @@ -1073,6 +1528,9 @@ def slip39_recovery_possible_pin( if pin is not None: yield # Enter PIN debug.input(pin) + if debug.model == "R": + yield # Reenter PIN + debug.press_yes() yield # Enter PIN again debug.input(pin) @@ -1085,7 +1543,7 @@ class InputFlowSlip39BasicRecovery(InputFlowBase): super().__init__(client) self.shares = shares - def input_flow_tt(self) -> GeneratorType: + def input_flow_common(self) -> GeneratorType: yield from slip39_recovery_possible_pin(self.debug, self.shares, pin=None) @@ -1095,7 +1553,7 @@ class InputFlowSlip39BasicRecoveryPIN(InputFlowBase): self.shares = shares self.pin = pin - def input_flow_tt(self) -> GeneratorType: + def input_flow_common(self) -> GeneratorType: yield from slip39_recovery_possible_pin(self.debug, self.shares, pin=self.pin) @@ -1103,7 +1561,7 @@ class InputFlowSlip39BasicRecoveryAbort(InputFlowBase): def __init__(self, client: Client): super().__init__(client) - def input_flow_tt(self) -> GeneratorType: + def input_flow_common(self) -> GeneratorType: yield # Confirm Recovery self.debug.press_yes() yield # Homescreen - abort process @@ -1117,7 +1575,7 @@ class InputFlowSlip39BasicRecoveryNoAbort(InputFlowBase): super().__init__(client) self.shares = shares - def input_flow_tt(self) -> GeneratorType: + def input_flow_common(self) -> GeneratorType: yield # Confirm Recovery self.debug.press_yes() yield # Homescreen - abort process @@ -1169,6 +1627,55 @@ class InputFlowSlip39BasicRecoveryRetryFirst(InputFlowBase): yield # Confirm abort self.debug.press_yes() + def input_flow_tr(self) -> GeneratorType: + yield # Confirm Recovery + self.debug.press_right() + self.debug.press_yes() + yield # Homescreen - start process + self.debug.press_yes() + yield # Enter number of words + self.debug.input("20") + yield # Homescreen - proceed to share entry + self.debug.press_yes() + yield # Enter first share + self.debug.press_yes() + for _ in range(20): + self.debug.input("slush") + + yield + # assert br.code == messages.ButtonRequestType.Warning + self.debug.press_yes() + + yield # Homescreen - start process + self.debug.press_yes() + yield # Enter number of words + self.debug.input("33") + yield # Homescreen - proceed to share entry + self.debug.press_yes() + yield # Homescreen - proceed to share entry + self.debug.press_yes() + yield + for _ in range(33): + self.debug.input("slush") + + yield + self.debug.press_yes() + + yield + self.debug.press_no() + + yield + self.debug.press_right() + + yield + self.debug.press_right() + + yield + self.debug.press_right() + + yield + self.debug.press_yes() + class InputFlowSlip39BasicRecoveryRetrySecond(InputFlowBase): def __init__(self, client: Client, shares: list[str]): @@ -1205,6 +1712,45 @@ class InputFlowSlip39BasicRecoveryRetrySecond(InputFlowBase): yield # Confirm abort self.debug.press_yes() + def input_flow_tr(self) -> GeneratorType: + yield # Confirm Recovery + self.debug.press_right() + self.debug.press_yes() + yield # Homescreen - start process + self.debug.press_yes() + yield # Enter number of words + self.debug.input("20") + yield # Homescreen - proceed to share entry + self.debug.press_yes() + yield # Enter first share + self.debug.press_yes() + yield # Enter first share + share = self.shares[0].split(" ") + for word in share: + self.debug.input(word) + + yield # More shares needed + self.debug.press_yes() + + yield # Enter another share + share = share[:3] + ["slush"] * 17 + for word in share: + self.debug.input(word) + + yield # Invalid share + # assert br.code == messages.ButtonRequestType.Warning + self.debug.press_yes() + + yield # Proceed to next share + share = self.shares[1].split(" ") + for word in share: + self.debug.input(word) + + yield # More shares needed + self.debug.press_no() + yield # Confirm abort + self.debug.press_yes() + class InputFlowSlip39BasicRecoveryWrongNthWord(InputFlowBase): def __init__(self, client: Client, share: list[str], nth_word: int): @@ -1234,6 +1780,39 @@ class InputFlowSlip39BasicRecoveryWrongNthWord(InputFlowBase): self.client.cancel() + def input_flow_tr(self) -> GeneratorType: + yield # Confirm Recovery + self.debug.press_right() + self.debug.press_yes() + yield # Homescreen - start process + self.debug.press_yes() + yield # Enter number of words + self.debug.input(str(len(self.share))) + yield # Homescreen - proceed to share entry + self.debug.press_yes() + yield # Enter first share + self.debug.press_yes() + yield # Enter first share + for word in self.share: + self.debug.input(word) + + yield # Continue to next share + self.debug.press_yes() + yield # Enter next share + self.debug.press_yes() + yield # Enter next share + for i, word in enumerate(self.share): + if i < self.nth_word: + self.debug.input(word) + else: + self.debug.input(self.share[-1]) + break + + yield + # assert br.code == messages.ButtonRequestType.Warning + + self.client.cancel() + class InputFlowSlip39BasicRecoverySameShare(InputFlowBase): def __init__(self, client: Client, first_share: list[str], second_share: list[str]): @@ -1263,6 +1842,39 @@ class InputFlowSlip39BasicRecoverySameShare(InputFlowBase): self.client.cancel() + def input_flow_tr(self) -> GeneratorType: + yield # Confirm Recovery + self.debug.press_right() + self.debug.press_yes() + yield # Homescreen - start process + self.debug.press_yes() + yield # Enter number of words + self.debug.input(str(len(self.first_share))) + yield # Homescreen - proceed to share entry + self.debug.press_yes() + yield # Homescreen - proceed to share entry + self.debug.press_yes() + yield # Enter first share + for word in self.first_share: + self.debug.input(word) + + yield # Continue to next share + self.debug.press_yes() + yield # Continue to next share + self.debug.press_yes() + yield # Enter next share + for word in self.second_share: + self.debug.input(word) + + br = yield + br = yield + assert br.code == messages.ButtonRequestType.Warning + self.debug.press_right() + self.debug.press_yes() + yield + + self.client.cancel() + class InputFlowResetSkipBackup(InputFlowBase): def __init__(self, client: Client): @@ -1275,3 +1887,13 @@ class InputFlowResetSkipBackup(InputFlowBase): self.debug.press_no() yield # Confirm skip backup self.debug.press_no() + + def input_flow_tr(self) -> GeneratorType: + yield # Confirm Recovery + self.debug.press_right() + self.debug.press_yes() + yield # Skip Backup + self.debug.press_no() + yield # Confirm skip backup + self.debug.press_right() + self.debug.press_no() diff --git a/tests/persistence_tests/test_shamir_persistence.py b/tests/persistence_tests/test_shamir_persistence.py index 0bcc9378a..d053f4490 100644 --- a/tests/persistence_tests/test_shamir_persistence.py +++ b/tests/persistence_tests/test_shamir_persistence.py @@ -84,7 +84,7 @@ def test_recovery_single_reset(core_emulator: Emulator): assert features.recovery_mode is True # we need to enter the number of words again, that's a feature - recovery.select_number_of_words(debug) + recovery.select_number_of_words(debug, wait=False) recovery.enter_shares(debug, MNEMONIC_SLIP39_BASIC_20_3of6) recovery.finalize(debug) @@ -121,7 +121,7 @@ def test_recovery_on_old_wallet(core_emulator: Emulator): assert features.recovery_mode is True # enter number of words - recovery.select_number_of_words(debug) + recovery.select_number_of_words(debug, wait=False) first_share = MNEMONIC_SLIP39_BASIC_20_3of6[0] words = first_share.split(" ") @@ -197,7 +197,7 @@ def test_recovery_multiple_resets(core_emulator: Emulator): assert features.recovery_mode is True # enter the number of words again, that's a feature! - recovery.select_number_of_words(debug) + recovery.select_number_of_words(debug, wait=False) # enter shares and restart after each one enter_shares_with_restarts(debug) diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index 0ab4d02e3..81d4275dc 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -695,6 +695,1193 @@ "T1_zcash-test_sign_tx.py::test_version_group_id_missing": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" } }, +"TR": { +"click_tests": { +"TR_test_autolock.py::test_autolock_does_not_interrupt_preauthorized": "852bedaf4727ea1b5cb56ab55ac314e6b09dccefc71bc6e32df201bdc98817e2", +"TR_test_autolock.py::test_autolock_does_not_interrupt_signing": "f99ab9a7fe7808c787269647288086ec6911691319d0b324d471258e031db74b", +"TR_test_autolock.py::test_autolock_interrupts_passphrase": "d12fccb3e8578043bed55ea5a7c2fa1fa6c834a2629c91f37cf7c46d33e802c9", +"TR_test_autolock.py::test_autolock_interrupts_signing": "062408fd528052b9ae39c5ca6be25d81259dd3b86112d917da40a588a11fce6e", +"TR_test_autolock.py::test_autolock_passphrase_keyboard": "83ce820d94307fca8b964d1087f54b57f3a0524b77f050d959db719c6cefa0e5", +"TR_test_autolock.py::test_dryrun_enter_word_slowly": "96dd19df7cd85394ed9b11c5c6ee2575b4e6dddb75d9e709ee905f265e3ff965", +"TR_test_autolock.py::test_dryrun_locks_at_number_of_words": "cb114503460241b59aa6dff465e5d07266c939e18da5665fc83dcd6fd4edea53", +"TR_test_autolock.py::test_dryrun_locks_at_word_entry": "2930cb8c4e03f5b485cfcc234f6c061266b66174b60483ac305e6128bcc79ad5", +"TR_test_lock.py::test_hold_to_lock": "9faab870f373edf47416d80e8297a14089ba1bed36f0aa0c67b3819b2cea03e3", +"TR_test_passphrase_tr.py::test_cancel": "490472798c6107483c0ffffc043d69af96d5701b311abed9c8534ee9e4f08f37", +"TR_test_passphrase_tr.py::test_passphrase_delete": "2edff7335a27d3f9fea5ed9291a835ff3025014df8c67a160c23b20d20ad06da", +"TR_test_passphrase_tr.py::test_passphrase_input[Y@14lw%p)JN@f54MYvys@zj'g-mnkoxeaMzLgfCxUdDSZW-381132c0": "883a69432774d30762fd442a89fa1364483935ecc660d52cceff2f006a4e75cf", +"TR_test_passphrase_tr.py::test_passphrase_input[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-166c898a": "b035194b44bbcfc715ed8514ed81b46daf0e65c7cd84ddcaaee64fd369df70c7", +"TR_test_passphrase_tr.py::test_passphrase_input[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-334262e9": "5297b79f4657be0e187f5d46b05b60998bddd7f978cd8c712919a5c78789bf44", +"TR_test_passphrase_tr.py::test_passphrase_input[abc 123-mvqzZUb9NaUc62Buk9WCP4L7hunsXFyamT]": "27fbe8ef2b0f880f16ae59a9664237e7be06497395fc3cfffbfc135ded354612", +"TR_test_passphrase_tr.py::test_passphrase_input[abc123ABC_<>-mtHHfh6uHtJiACwp7kzJZ97yueT6sEdQiG]": "18a225184fae76d633d6d822dfb194612122e7fb9da85b0d0827f91bb0bd810e", +"TR_test_passphrase_tr.py::test_passphrase_input_over_50_chars": "4348ca13e8f87b3d6b90821c9d2fa0a7575da9d113b855d09d3a2c072d2f79a7", +"TR_test_passphrase_tr.py::test_passphrase_loop_all_characters": "f0b53c5b71f5521aa87d03a570957afe63c9313d8c3903d1ceeb4db5228a5301", +"TR_test_pin.py::test_pin_change": "b7c0e303098d91546a266ba0f59e3eed8aa0c40845d6811698b58b09ee9b47b9", +"TR_test_pin.py::test_pin_incorrect": "3b57d4a025689b145fd10593b78963294633ed7ad129c076865377f87fbe51be", +"TR_test_pin.py::test_pin_long": "53bcc304d0e9181a5b467ada641998837f5081da55406bc5ef80fd70ac29001a", +"TR_test_pin.py::test_pin_long_delete": "e87c64f69295a134ff2c04c00a7065cc93d8666c2b66c02e849848b242e323a2", +"TR_test_pin.py::test_pin_longer_than_max": "c15b2a40cba62a14db70f405e21b9e319f655cb86037b0510b0f141ed5da14be", +"TR_test_pin.py::test_pin_same_as_wipe_code": "495213f0ee5e57c4c07998a347d3e9c9f82e2c68bcf400031acb2f3a74cc82ff", +"TR_test_pin.py::test_pin_setup": "47ce6cfa489b484ba980102d896c87d4e67a9d177266549004f0a168f51656aa", +"TR_test_pin.py::test_pin_setup_mismatch": "edad7b7156edff69ef54cb024e952c69fffe0ede13997102d6c7b8a15ae570fe", +"TR_test_pin.py::test_pin_short": "6fbef41cd1b516565683d6c4274d7c09f9254b545c140f14a6f5f09e4ec43dfa", +"TR_test_pin.py::test_wipe_code_same_as_pin": "af78871940ecd25399d12fda760a6019ace98660d1abedf5b2c3a191c79ce6f2", +"TR_test_pin.py::test_wipe_code_setup": "f529d924eba2056a0c3a6b48ca54edbdca210bb31302148662346c65a035a3bc", +"TR_test_recovery.py::test_recovery_bip39": "2147a7b5723915352ee2d474974b395a6936acdecf07c3c99a1ead1fed761300", +"TR_test_recovery.py::test_recovery_slip39_basic": "37eb68c729b99776816515c341fc5b2c23673cb367d41b231e456690b4caa224", +"TR_test_reset_bip39.py::test_reset_bip39": "45a8f1ec97b152438fcd4161c709efd61f7b98283102432a256c95051721abe2", +"TR_test_reset_slip39_advanced.py::test_reset_slip39_advanced[16of16]": "78da94ed0a465fb093fca05404c474bc05b5fdfab9c2881805c1c6258122c5c6", +"TR_test_reset_slip39_advanced.py::test_reset_slip39_advanced[2of2]": "c37ec2580398f2d7129072cd0b18f8813debfd2fdefe3b17b1cc9b6a1c1bac6b", +"TR_test_reset_slip39_basic.py::test_reset_slip39_basic[16of16]": "72ea356096ec36af86c2f2e7561e6b6f11a1ca02319bf6e76398434f78ece69f", +"TR_test_reset_slip39_basic.py::test_reset_slip39_basic[1of1]": "29647900796fe07bc7c15e1b9f4540180e230135a684321095d15d25b0e534f0", +"TR_test_tutorial.py::test_tutorial_again_and_skip": "361dfaa421a210e8b9adf47b6f966efc4c5bd2d7b2b0642a4d5242525dad7078", +"TR_test_tutorial.py::test_tutorial_finish": "e6894ec903c03124a3ca92c8c0d79b86c0c01791b555b492f5e50b629fbe9960", +"TR_test_tutorial.py::test_tutorial_skip": "c5343d06a573f7603f4b04b773d203ee33b6ca866d24be79e9dd75d4a3da3896" +}, +"device_tests": { +"TR_binance-test_get_address.py::test_binance_get_address[m-44h-714h-0h-0-0-bnb1hgm0p7khfk85zpz-68e2cb5a": "06e3aba5199f2730f3e87b37d44d77d5386cb9b798a46589f9f85e4bf589f26d", +"TR_binance-test_get_address.py::test_binance_get_address[m-44h-714h-0h-0-1-bnb1egswqkszzfc2uq7-1adfb691": "101ad4adcbbaf130340eef6a794c72591af650cc00f7dc73957b2cee566fd694", +"TR_binance-test_get_public_key.py::test_binance_get_public_key": "ef58c0377dab4b1d95c3b9e1437ec187f281bdbaf85cc7012465a34da7b55cbc", +"TR_binance-test_sign_tx.py::test_binance_sign_message[message0-expected_response0]": "5e4b029ce0e1b096e2605e26ca10272ef2609f5569587ee4fb232e73f20e0c7d", +"TR_binance-test_sign_tx.py::test_binance_sign_message[message1-expected_response1]": "e88654f0db8bd99bda4523fd18daf1f1623cd7c7b11ec89f2937f5fb8ab34437", +"TR_binance-test_sign_tx.py::test_binance_sign_message[message2-expected_response2]": "1d7b37735999a13c129eef16db99ceb82c88e4dd27461b2f139e0a9cd3b093e2", +"TR_bitcoin-test_authorize_coinjoin.py::test_cancel_authorization": "c42e3704e40f3d54bcb0031d225a058d1db1bdc752083cadc7db3eb91f86e849", +"TR_bitcoin-test_authorize_coinjoin.py::test_get_address": "5c0bfdcbc80cbc6d00479af0cd7df927fcabe20453b31ec73d91af996532fc72", +"TR_bitcoin-test_authorize_coinjoin.py::test_get_public_key": "5b745c36869d876aa5263ba6411251cbbf21b817ee328beae1dd3b68f9e7e8ae", +"TR_bitcoin-test_authorize_coinjoin.py::test_multisession_authorization": "cd45ca58163dc0c52fc2db2dc024dd3727b525cfa4cf7892962cb63b7853f3f0", +"TR_bitcoin-test_authorize_coinjoin.py::test_sign_tx": "255e7939fe83a8617c369e32d9db0acfbe63f156da639f1191cfc2d6d90fd9d7", +"TR_bitcoin-test_authorize_coinjoin.py::test_sign_tx_large": "81e66356c099c0790ee2926d08bbace2ad520e85e303610234e3320c32149c1e", +"TR_bitcoin-test_authorize_coinjoin.py::test_sign_tx_migration": "0efda8ca649793d0349f2883131adebb9895e8e1e6180bcc0fc9716eb15178eb", +"TR_bitcoin-test_authorize_coinjoin.py::test_sign_tx_spend": "97108fb204281a90f6cbae51711661fcce892164e12993fbf9f4b475db5d93e8", +"TR_bitcoin-test_authorize_coinjoin.py::test_wrong_account_type": "c42e3704e40f3d54bcb0031d225a058d1db1bdc752083cadc7db3eb91f86e849", +"TR_bitcoin-test_authorize_coinjoin.py::test_wrong_coordinator": "c42e3704e40f3d54bcb0031d225a058d1db1bdc752083cadc7db3eb91f86e849", +"TR_bitcoin-test_bcash.py::test_attack_change_input": "89d4be01a9d05131a97eecde5edfa5ecddd3fb66f74de4115a56b9869196346f", +"TR_bitcoin-test_bcash.py::test_send_bch_change": "89d4be01a9d05131a97eecde5edfa5ecddd3fb66f74de4115a56b9869196346f", +"TR_bitcoin-test_bcash.py::test_send_bch_external_presigned": "85591c3a002ad56e409f6495a92a02fbef39f1aff21bedfb9bf35e091a937522", +"TR_bitcoin-test_bcash.py::test_send_bch_multisig_change": "85319d2df349014a7d6c41d8902597788a153fe8c90ebd5c66663c7502ff8776", +"TR_bitcoin-test_bcash.py::test_send_bch_multisig_wrongchange": "5141f2665bac56163410ed1ff05b6b0d3d67aa56dde2f6256f3453eeb3affe33", +"TR_bitcoin-test_bcash.py::test_send_bch_nochange": "888b55ccdc12a35d7d26448e64d3373f49370fa7d00296b3347f7894c0e5ffdd", +"TR_bitcoin-test_bcash.py::test_send_bch_oldaddr": "7228b6073cb70772fc5702e9669d0413870e8658890296bd4b5799d8eb04bb1c", +"TR_bitcoin-test_bgold.py::test_attack_change_input": "f2169cef2010f31656153279b45e480f20aac75b64ebf8f34c85a24a5b59b04c", +"TR_bitcoin-test_bgold.py::test_send_bitcoin_gold_change": "f2169cef2010f31656153279b45e480f20aac75b64ebf8f34c85a24a5b59b04c", +"TR_bitcoin-test_bgold.py::test_send_bitcoin_gold_nochange": "794136d3e81aa6c7e51a235bb71bd4c513045452e4f4a2edcd9a41bfaf6e9879", +"TR_bitcoin-test_bgold.py::test_send_btg_external_presigned": "34961e42b12f96c63a7908864d1925ad669f5ffcb7598a2d75843af89a606665", +"TR_bitcoin-test_bgold.py::test_send_btg_multisig_change": "95b9ffeb2da0d80b405c61962b47f07e87307ea292932500e2aebda0a8444f0d", +"TR_bitcoin-test_bgold.py::test_send_mixed_inputs": "1b06fec96b820db361399f949ce72c483dd0e0daf9e2cb0eab43a2f0d8d2666b", +"TR_bitcoin-test_bgold.py::test_send_multisig_1": "dc61d1ce90bd2d60cabb0fddbc044c19640370c55c05d83018dfd566c248cf07", +"TR_bitcoin-test_bgold.py::test_send_p2sh": "ccd1b9535d29bbd7468856fc16701f1955b6f269340eba9df4eadf0c4e22737f", +"TR_bitcoin-test_bgold.py::test_send_p2sh_witness_change": "9ae38c62867aed70de48f0f56f17082b3b42dfc93a560fe86e3604f47625b28d", +"TR_bitcoin-test_dash.py::test_send_dash": "8fce92d91ac560a42adaf04778c59a15f705554c1ef84052089c4f6344f16443", +"TR_bitcoin-test_dash.py::test_send_dash_dip2_input": "1b2cd30f51a44e0aa5106976d0638dacdf3f8621b7205b36ff0c11e01dcf5d30", +"TR_bitcoin-test_decred.py::test_decred_multisig_change": "16bc4e7e80d0a4e6991917f43de1930849b489856d082b7d08f0cdf8533dc41f", +"TR_bitcoin-test_decred.py::test_purchase_ticket_decred": "11b288144b0e64974e84495da4f92fcd05e5c3c3808024dbbdb50efbe3257231", +"TR_bitcoin-test_decred.py::test_send_decred": "6c37702c334131c6f0e6adbaaf7d17fd5ab51a6af5ca96a718943c106c3f8588", +"TR_bitcoin-test_decred.py::test_send_decred_change": "902b502732db620501408cb1d44b419e16dfe7f1f6c97f5b517326fdc0622fec", +"TR_bitcoin-test_decred.py::test_spend_from_stake_generation_and_revocation_decred": "fed21afafbfc85764dcbcabffac9c0d92072946cce8a960412b4500a14b62135", +"TR_bitcoin-test_descriptors.py::test_descriptors[Bitcoin-0-10025-InputScriptType.SPENDTAPROOT--ad9e3c78": "473eff8e1035e5d50b7b1e6c26f2c4694176f65ec01f59ef696f2138a8719c02", +"TR_bitcoin-test_descriptors.py::test_descriptors[Bitcoin-0-44-InputScriptType.SPENDADDRESS-pkh-efa37663": "e27453535fb9d9b94e83854a47b36f8f763e62420082a7f1beafa615639794ac", +"TR_bitcoin-test_descriptors.py::test_descriptors[Bitcoin-0-49-InputScriptType.SPENDP2SHWITNESS-77f1e2d2": "113e65acb773062fc8b6de6866c9dfb9da54bfd9c24579ed2ef8c822525cc714", +"TR_bitcoin-test_descriptors.py::test_descriptors[Bitcoin-0-84-InputScriptType.SPENDWITNESS-wpk-16507754": "9c4c59f4640936dd60df7fbd1d5cec2792abc78c72e5333ee29f78142fd2926d", +"TR_bitcoin-test_descriptors.py::test_descriptors[Bitcoin-0-86-InputScriptType.SPENDTAPROOT-tr(-2c28c019": "2fffa87bd229acd3fe2cec25059ed1aa1727682b27f0c59fbdfd8c2a2ec620f8", +"TR_bitcoin-test_descriptors.py::test_descriptors[Bitcoin-1-44-InputScriptType.SPENDADDRESS-pkh-b15b8a1d": "9c9ac829825b85d6ab06dd2d1d7180918fdd6b8ce68fae1eeceb019713d54d31", +"TR_bitcoin-test_descriptors.py::test_descriptors[Bitcoin-1-49-InputScriptType.SPENDP2SHWITNESS-965f4fa3": "d86b279342c3525372a3811df931e6b62e7964457e91d299da339d4392c932ad", +"TR_bitcoin-test_descriptors.py::test_descriptors[Bitcoin-1-84-InputScriptType.SPENDWITNESS-wpk-c761bcc8": "84476b356193a8e2b979615542206962bf7022090e3ff1630cadf2a64c2db131", +"TR_bitcoin-test_descriptors.py::test_descriptors[Bitcoin-1-86-InputScriptType.SPENDTAPROOT-tr(-686799ea": "e1981b3fa0b2f87aa19cf25abfb198a072f52c603ee1f6c682f638546757a294", +"TR_bitcoin-test_descriptors.py::test_descriptors[Testnet-0-10025-InputScriptType.SPENDTAPROOT--77d3a71b": "81597488bf6345870cdc609a1de69d980de19a9306f82671dc7ba23f0d5c56bf", +"TR_bitcoin-test_descriptors.py::test_descriptors[Testnet-0-44-InputScriptType.SPENDADDRESS-pkh-a26143a7": "0cfa631e6572dd119b5a6ce5ee559505517d5e3cf80dcf55c4fc55e4eefde50d", +"TR_bitcoin-test_descriptors.py::test_descriptors[Testnet-0-49-InputScriptType.SPENDP2SHWITNESS-195ebda5": "00ae6e0b856a42d36fd277a65d641f2556078437280916724fd54a855e62d3dc", +"TR_bitcoin-test_descriptors.py::test_descriptors[Testnet-0-84-InputScriptType.SPENDWITNESS-wpk-68f8b526": "1910b260fee1be6670e27ead46a11e0391df41b28b5ffbb9a08ce1e1ccb72418", +"TR_bitcoin-test_descriptors.py::test_descriptors[Testnet-0-86-InputScriptType.SPENDTAPROOT-tr(-07da691f": "54767e1ac3a0d992bd0110d033893c1a4839cac21bade4f09b7d2ffba72ad391", +"TR_bitcoin-test_descriptors.py::test_descriptors[Testnet-1-44-InputScriptType.SPENDADDRESS-pkh-ca853567": "b4667bbc8a1ac23bf3fa63c69c975418769aadf2d4a49cd032fa18e8e1527889", +"TR_bitcoin-test_descriptors.py::test_descriptors[Testnet-1-49-InputScriptType.SPENDP2SHWITNESS-5da04067": "3c8f674f8106c4de20d13647cd196ae9ef045cbfd02355785d6d4622a33df584", +"TR_bitcoin-test_descriptors.py::test_descriptors[Testnet-1-84-InputScriptType.SPENDWITNESS-wpk-adba8950": "8291fa43dd14bad5b08cd301e93d84a77050d218b0b3dd944c8b468b52584b4d", +"TR_bitcoin-test_descriptors.py::test_descriptors[Testnet-1-86-InputScriptType.SPENDTAPROOT-tr(-e31edeff": "f17603727ea2fbd97c07ec9aeef1332cb495a6d5b39604563ff4092157f01267", +"TR_bitcoin-test_firo.py::test_spend_lelantus": "bcb2e8b202eb64b367ba5ec189f8af547077ad2a7bde0f993e40e60ff25aaae3", +"TR_bitcoin-test_fujicoin.py::test_send_p2tr": "eb320a23e297011739945b2a5743fcd199bb37f01af6d107be574c4e1dde1796", +"TR_bitcoin-test_getaddress.py::test_address_mac": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress.py::test_altcoin_address_mac": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress.py::test_bch": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress.py::test_bch_multisig": "0e34779861a5c7100886141680ca0c8aacf77e35a724266c11c130704a88ae1e", +"TR_bitcoin-test_getaddress.py::test_btc": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress.py::test_crw": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress.py::test_elements": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress.py::test_grs": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress.py::test_invalid_path": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress.py::test_ltc": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress.py::test_multisig": "c48d3765e27c9bc6ad282d7fef6dfe1e7a4e48e8394837b0dec89022385f6e08", +"TR_bitcoin-test_getaddress.py::test_multisig_missing[False]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress.py::test_multisig_missing[True]": "2a53a8bc2308df521576631f61ebdc32093e493dbab1f0e2f51a0382f4a17424", +"TR_bitcoin-test_getaddress.py::test_public_ckd": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress.py::test_tbtc": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress.py::test_tgrs": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress.py::test_unknown_path": "7d7a69a760476c5442a4515cb2c745a81f222db4bcf209abe415bef908bb4af6", +"TR_bitcoin-test_getaddress_segwit.py::test_multisig_missing[False]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit.py::test_multisig_missing[True]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit.py::test_show_multisig_3": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit.py::test_show_segwit": "377a96b86ca626e401c404af76512961269434795643f32b687ad4355e510872", +"TR_bitcoin-test_getaddress_segwit.py::test_show_segwit_altcoin": "69f90312a22e7071711d0122a8630c47a0d15dbdb725e9c105b3d58f15785040", +"TR_bitcoin-test_getaddress_segwit_native.py::test_bip86[m-86h-0h-0h-0-0-bc1p5cyxnuxmeuwuvkwfem-dc12f29f": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit_native.py::test_bip86[m-86h-0h-0h-0-1-bc1p4qhjn9zdvkux4e44uh-1f521bf2": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit_native.py::test_bip86[m-86h-0h-0h-1-0-bc1p3qkhfews2uk44qtvau-d8b57624": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit_native.py::test_multisig_missing[False]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit_native.py::test_multisig_missing[True]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_multisig_3": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Bitcoin-m-84h-0h-0h-0-0-InputScr-6bc4ffc3": "9a955872984ad2b990eff6dc17fccd332d557e9554981a123913facfdb5e6329", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Bitcoin-m-84h-0h-0h-0-0-InputScr-8943c1dc": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Bitcoin-m-84h-0h-0h-1-0-InputScr-016718c1": "ab802e286cda283f4133ce3ffd98c7241010c5c0ad1bd51cca867e89bb7abf5b", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Bitcoin-m-84h-0h-0h-1-0-InputScr-7656a4db": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Bitcoin-m-86h-0h-0h-0-0-InputScr-3d3cc8eb": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Bitcoin-m-86h-0h-0h-0-0-InputScr-8571d5e0": "90e0a339a6a1c7eadb95cf39bb659734bd86cbb76fc5971596ca8d4d11e8edb5", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Bitcoin-m-86h-0h-0h-1-0-InputScr-ab700de2": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Bitcoin-m-86h-0h-0h-1-0-InputScr-da3803e0": "b2ba0e24e25971acc9806dc901f475e7d7f0e2fbed7b1b12a8f23c005c05542e", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Elements-m-84h-1h-0h-0-0-InputSc-490228be": "23b51b2f1254618a20cfab9adac8836c0d48f7551cb13685736ed7166ccdca5a", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Elements-m-84h-1h-0h-0-0-InputSc-ed587e90": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Groestlcoin Testnet-m-84h-1h-0h--40b95144": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Groestlcoin Testnet-m-84h-1h-0h--45b4ff5d": "f59d2bd6b422aabba4de0127252812f3f04e169b9401fdf82e67a88391de92fd", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Groestlcoin Testnet-m-84h-1h-0h--8f7c658b": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Groestlcoin Testnet-m-84h-1h-0h--93c9c3ff": "6f52263b414b7297ae3c5e40b909c10fea16ba0ee5c3c566f12b30490e70a7d9", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Groestlcoin Testnet-m-86h-1h-0h--5feb8c64": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Groestlcoin Testnet-m-86h-1h-0h--8d4476a1": "11c27496dacc27f6cac9804b83026a7b2bc8b4da46bc4a3df692c419e0016257", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Groestlcoin-m-84h-17h-0h-0-0-Inp-0200a67b": "da6152be98b88cbbe875b3ee5c2352796ee1074fbb72de319e38b0fc452fb950", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Groestlcoin-m-84h-17h-0h-0-0-Inp-e6c1098a": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Groestlcoin-m-84h-17h-0h-1-0-Inp-9688a507": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Groestlcoin-m-84h-17h-0h-1-0-Inp-f825f217": "ff0df071a32bb33fbfa2a9550c0219f00b2eb1bd022d5af9c366ee7e2c23b2bf", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Groestlcoin-m-86h-17h-0h-0-0-Inp-38cd93cf": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Groestlcoin-m-86h-17h-0h-0-0-Inp-ebecce6e": "6f48e64ae31cd76372dc707f4a7de839ced137a5f18f758dd8de0344304c6bcb", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Testnet-m-84h-1h-0h-0-0-InputScr-313b9443": "9f70e1755f5b89f9ee2ded6f8c9f2661389b6ac84b9434f95a3e220ebfa186a5", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Testnet-m-84h-1h-0h-0-0-InputScr-ce15ec92": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Testnet-m-84h-1h-0h-1-0-InputScr-040186c0": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Testnet-m-84h-1h-0h-1-0-InputScr-b10918be": "bd9587c245d4336151622633472c260eef26f38dfc3323210427bee2b03be5dc", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Testnet-m-86h-1h-0h-0-0-InputScr-55ae0ae6": "fe539784a4c30a86e5da04c9bad2ca3e7fece85cfe215b1889237c5bf4765e97", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Testnet-m-86h-1h-0h-0-0-InputScr-821a199d": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Testnet-m-86h-1h-0h-1-0-InputScr-9d2fa8bc": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getaddress_segwit_native.py::test_show_segwit[Testnet-m-86h-1h-0h-1-0-InputScr-d5b7f8fc": "94d0e2a7d463fbfea32e006425f316f2ba1abfc36ee7f26efa4a51be57a2e159", +"TR_bitcoin-test_getaddress_show.py::test_show_cancel[m-44h-0h-12h-0-0-InputScriptType.SPENDADD-4eca71e0": "3dca800a87874a857b3c40a4c524ceef11925b9537a7a11e6412548ec0f50a78", +"TR_bitcoin-test_getaddress_show.py::test_show_cancel[m-49h-0h-12h-0-0-InputScriptType.SPENDP2S-4ec777e0": "2c979dfb3a67302e4e716629e397fe033dc1309259768464dfb91fc84c9635e2", +"TR_bitcoin-test_getaddress_show.py::test_show_cancel[m-84h-0h-12h-0-0-InputScriptType.SPENDWIT-d6991e22": "1265f3be7c25a04687c49b091c5b3a4e6793e6daf1b58287a7d6305a26e7e811", +"TR_bitcoin-test_getaddress_show.py::test_show_cancel[m-86h-0h-12h-0-0-InputScriptType.SPENDTAP-4c5b2b38": "b571aff99f69f50660f1dc78cade1326c21b3b90056b1105f5391457634805a3", +"TR_bitcoin-test_getaddress_show.py::test_show_multisig_15": "36b83d6fab39cefd75c3852b3e897f96a2250998678a93593233bfe08356280e", +"TR_bitcoin-test_getaddress_show.py::test_show_multisig_3": "875414bf1fa9b8c898cf48f426140d55af3b7071daf34a14a4e879ef7221f870", +"TR_bitcoin-test_getaddress_show.py::test_show_multisig_xpubs[InputScriptType.SPENDMULTISIG-0-3-4efd9cf3": "c22c48bcaa595e5aa2842d3c2d1054097ed49b39585408334ecb154ef413ecc3", +"TR_bitcoin-test_getaddress_show.py::test_show_multisig_xpubs[InputScriptType.SPENDMULTISIG-0-3-98a7e339": "c22c48bcaa595e5aa2842d3c2d1054097ed49b39585408334ecb154ef413ecc3", +"TR_bitcoin-test_getaddress_show.py::test_show_multisig_xpubs[InputScriptType.SPENDP2SHWITNESS--2cf5f03c": "78dd78c0717156b653c830f136e60b7199ce211013bf8363bc8428fcd60e9325", +"TR_bitcoin-test_getaddress_show.py::test_show_multisig_xpubs[InputScriptType.SPENDP2SHWITNESS--5ea18367": "54560d76bac2e60658948123590f83a50d6e3e63601fb03a554bc0e91bed7a53", +"TR_bitcoin-test_getaddress_show.py::test_show_multisig_xpubs[InputScriptType.SPENDWITNESS-2-bc-e70b56ea": "3205596cd86232fd1f9f907493a91e8c7277e37b0f03e33c33c82cefa7618856", +"TR_bitcoin-test_getaddress_show.py::test_show_multisig_xpubs[InputScriptType.SPENDWITNESS-2-bc-f3c4650f": "a812f84cdff2b7148466ea311537619a5affbfaf2cba0f3e14f00a0108ebe70e", +"TR_bitcoin-test_getaddress_show.py::test_show_tt[m-44h-0h-12h-0-0-InputScriptType.SPENDADDRESS-7e3bc134": "0ff292a149050fc8e73569a593eeecf3301d9211c51311e39bb5bd3756d3c6f0", +"TR_bitcoin-test_getaddress_show.py::test_show_tt[m-49h-0h-12h-0-0-InputScriptType.SPENDP2SHWIT-fffcf75c": "8b9db22289569987bbd0871d877df15252784440c12ed02df08a1a5fd9e4765f", +"TR_bitcoin-test_getaddress_show.py::test_show_tt[m-84h-0h-12h-0-0-InputScriptType.SPENDWITNESS-2ad0a0fd": "4a03437ac3ca130aa4f7a7166ae20b720486872e83b2bb072c4956473ce4f983", +"TR_bitcoin-test_getaddress_show.py::test_show_tt[m-86h-0h-12h-0-0-InputScriptType.SPENDTAPROOT-299be1ac": "f9a7c113692ce73e1db0f72237f565979e8aaa45ee1a8d36283e35b0f664d036", +"TR_bitcoin-test_getaddress_show.py::test_show_unrecognized_path": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getownershipproof.py::test_attack_ownership_id": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getownershipproof.py::test_confirm_ownership_proof": "58e0a6e67dcb3d23eb89bb40969e8a65814cde0256e63992a74df319ad4bf35e", +"TR_bitcoin-test_getownershipproof.py::test_confirm_ownership_proof_with_data": "f18723580b890cb3d46fcc74f915ed96eb5f2a6e50f7a7197c8a82379e93f74b", +"TR_bitcoin-test_getownershipproof.py::test_fake_ownership_id": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getownershipproof.py::test_p2tr_ownership_id": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getownershipproof.py::test_p2tr_ownership_proof": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getownershipproof.py::test_p2wpkh_ownership_id": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getownershipproof.py::test_p2wpkh_ownership_proof": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey.py::test_get_public_node[Bitcoin-76067358-path0-xpub6BiVtCpG9fQPx-40a56ca3": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey.py::test_get_public_node[Bitcoin-76067358-path1-xpub6BiVtCpG9fQQR-1abafc98": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey.py::test_get_public_node[Bitcoin-76067358-path2-xpub6FVDRC1jiWNTu-47a67414": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey.py::test_get_public_node[Bitcoin-76067358-path3-xpub6GhTNegKCjTqj-990e0830": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey.py::test_get_public_node[Bitcoin-76067358-path6-xpub68Zyu13qjcQvJ-8285bd20": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey.py::test_get_public_node[Litecoin-27108450-path10-Ltub2dcb6Nghj3k-53e5db37": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey.py::test_get_public_node[Litecoin-27108450-path7-Ltub2Y8PyEMWQVgi-d0bb059c": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey.py::test_get_public_node[Litecoin-27108450-path8-Ltub2Y8PyEMWQVgi-98ae2c41": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey.py::test_get_public_node[Litecoin-27108450-path9-Ltub2dTvwC4v7GNe-8d6d95fb": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey.py::test_get_public_node[Testnet-70617039-path4-tpubDDKn3FtHc74Ca-f3b70aff": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey.py::test_get_public_node[Testnet-70617039-path5-tpubDGwNSs8z8jZU2-8b5efa13": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey.py::test_invalid_path[Bcash-path5]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey.py::test_invalid_path[Bitcoin-path0]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey.py::test_invalid_path[Bitcoin-path2]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey.py::test_invalid_path[Bitcoin-path3]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey.py::test_invalid_path[Litecoin-path4]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey.py::test_invalid_path[Testnet-path1]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey.py::test_script_type[InputScriptType.SPENDADDRESS-xpub6BiVtCp7ozs-9813cc48": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey.py::test_script_type[InputScriptType.SPENDP2SHWITNESS-ypub6WYmBsV-0710fbb3": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey.py::test_script_type[InputScriptType.SPENDWITNESS-zpub6qP2VY9x7Mx-84eaa56c": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey.py::test_script_type[None-xpub6BiVtCp7ozsRo7kaoYNrCNAVJwPYTQHjoXF-c37a47fd": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey.py::test_slip25_path": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey_curve.py::test_coin_and_curve": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey_curve.py::test_ed25519_public": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey_curve.py::test_publickey_curve[ed25519-path4-002e28dc0346d6d30d4e-e6c7a440": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey_curve.py::test_publickey_curve[nist256p1-path2-0324c6860c25cdf7a8-d75f4900": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey_curve.py::test_publickey_curve[nist256p1-path3-03b93f7e6c777143ad-2d6b178b": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey_curve.py::test_publickey_curve[secp256k1-path0-02f65ce170451f66f4-9c982c22": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_getpublickey_curve.py::test_publickey_curve[secp256k1-path1-0212f4629f4f224db0-0209bb73": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_grs.py::test_legacy": "a5147071fa6222a3c7fcea6dece58548e28c3792f2ef761d6b2ff29a4b507279", +"TR_bitcoin-test_grs.py::test_legacy_change": "1e54d772574a364a4ba4f74791e4cebe28417f213071bebbd3c8ece1004e47ff", +"TR_bitcoin-test_grs.py::test_send_p2tr": "62479b2c0aca173eec98c1a523b8ffd6a8fbb01f260f969e831ef48412ee769f", +"TR_bitcoin-test_grs.py::test_send_segwit_native": "ce9461df6056a3cf71052e05e2e9ac58984b3e321d43550b48ddaa3e5abd9884", +"TR_bitcoin-test_grs.py::test_send_segwit_native_change": "802a780858b7cb012dde458fdcd3a5dfa9514d5606b9663e0eaa6b6a3d0683e1", +"TR_bitcoin-test_grs.py::test_send_segwit_p2sh": "44e484ae8368dec3e326f264fc0000d328e45564b6e0ac6c8609755a4da1873d", +"TR_bitcoin-test_grs.py::test_send_segwit_p2sh_change": "ce2f3954dbce8f282d7767db1225a120da3a038438a5a9e492b938673b302820", +"TR_bitcoin-test_komodo.py::test_one_one_fee_sapling": "a65c241ddd12ebc2c70ee226ea22e7ed22ffb1a4fd48bffc1f16355124af9e62", +"TR_bitcoin-test_komodo.py::test_one_one_rewards_claim": "fae1ddeb48c90ba6e69ed96ede7605ce8b4199998c1da33f5903cdebbe356719", +"TR_bitcoin-test_multisig.py::test_15_of_15": "38f7f8e3cc3d42422f2fbedc32abfdeac4a90a5353b07342ab83e1dfb243aea0", +"TR_bitcoin-test_multisig.py::test_2_of_3": "09c2acb9a6074cae67c97aaea4a4eeec1d3dc0429b07af7db03422380e454c25", +"TR_bitcoin-test_multisig.py::test_attack_change_input": "9b101eec09c22fedc2d68eb27262259e3e6756360477aeccb2eeeb0ceb64d098", +"TR_bitcoin-test_multisig.py::test_missing_pubkey": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_multisig_change.py::test_external_external": "94d492643c846cc4f21f8e643d3107af41e4a1fbe332c2954a5472a21b53cfa0", +"TR_bitcoin-test_multisig_change.py::test_external_internal": "5857e42c7a1214a98e33674749c832659707d85afb76d5a1ec157df1e622a121", +"TR_bitcoin-test_multisig_change.py::test_internal_external": "02f702c9a82adcbc0f09420cd2d452ede160b6e1a2a2d27e01c470bc55e2229a", +"TR_bitcoin-test_multisig_change.py::test_multisig_change_match_first": "0e985283058a681e1ed3fa55ddb6ef0a000e3f1ae12ed4ea09707d5b08fe1823", +"TR_bitcoin-test_multisig_change.py::test_multisig_change_match_second": "01a753acfb9ec405aad6610263ee0e6ef9a3ffee62d930c98a3a9284f81b8ef5", +"TR_bitcoin-test_multisig_change.py::test_multisig_external_external": "9818e5d60d786c5e666c6bbaeeee1bfbe801625f2437a4a7ea7505a8c9efb359", +"TR_bitcoin-test_multisig_change.py::test_multisig_mismatch_change": "6c7d84aa4a329ebc018caf647bd2f3a64b55c2522a7373a1cb5ea977485b64ed", +"TR_bitcoin-test_multisig_change.py::test_multisig_mismatch_inputs": "cec4e425202a4d75b20448dacaa7ed3c7ce0fcb2394c9c3e381a6049ac3e87e7", +"TR_bitcoin-test_nonstandard_paths.py::test_getaddress[m-1195487518-6-255-script_types3]": "2fe9af1b024444d7c528dd5e3f300f8ee22821f4a4196de7ba4cc9c0915b8562", +"TR_bitcoin-test_nonstandard_paths.py::test_getaddress[m-1195487518-script_types2]": "e88886b95cb792eef4621fff952c01d20776cb7cde2d443e96c872d45d7a40df", +"TR_bitcoin-test_nonstandard_paths.py::test_getaddress[m-3h-100h-4-255-script_types1]": "4d0f2519d407987aa45a71fcba8cb859fad00c07916a3414d271ecbabd70b474", +"TR_bitcoin-test_nonstandard_paths.py::test_getaddress[m-4-255-script_types0]": "589193b3e25561d963ef38d6cf6e9d0ffce517dbdb4dddc8504fdac966d7d4b0", +"TR_bitcoin-test_nonstandard_paths.py::test_getaddress[m-49-0-63-0-255-script_types4]": "1283544c7350537634c668292a85be027ad70e4c823c11c5b54d5cde117dfd63", +"TR_bitcoin-test_nonstandard_paths.py::test_getaddress_multisig[paths0-address_index0]": "a271a5f52de327ef6942d6c130795b952a75042a199ae9c6b5cd1f224db84f00", +"TR_bitcoin-test_nonstandard_paths.py::test_getaddress_multisig[paths1-address_index1]": "a292bf814f60c81a06e7089378ea997e8338dd965271af193c32c076d34399b6", +"TR_bitcoin-test_nonstandard_paths.py::test_getaddress_multisig[paths2-address_index2]": "aac4398952a08c155b518c5c9a954a4bedc487b4276a5052b670be29c7e1843a", +"TR_bitcoin-test_nonstandard_paths.py::test_getaddress_multisig[paths3-address_index3]": "d4e2b7e276eefe34c06ed2e77775d7a650c4d0ebcfe8a5c1bbf37b0b818a8e7e", +"TR_bitcoin-test_nonstandard_paths.py::test_getaddress_multisig[paths4-address_index4]": "3fd44ffc2e37aac9b548cf3a0aece804935886eeb1078b28b7a591a0941b852b", +"TR_bitcoin-test_nonstandard_paths.py::test_getaddress_multisig[paths5-address_index5]": "79567cc69e6cedf2b99797b777f3f2bc55ff60cb4abdae919d28110c431f831b", +"TR_bitcoin-test_nonstandard_paths.py::test_getaddress_multisig[paths6-address_index6]": "a32e0e738263f6d74f6da5904ef23f96cfc1d805bdc0576f5f2957742777315f", +"TR_bitcoin-test_nonstandard_paths.py::test_getaddress_multisig[paths7-address_index7]": "eca4cb3ffd9b6e93f67a68ab34ad712575d7afc3f55ac06eedb92ead463b32d3", +"TR_bitcoin-test_nonstandard_paths.py::test_getpublicnode[m-1195487518-6-255-script_types3]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_nonstandard_paths.py::test_getpublicnode[m-1195487518-script_types2]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_nonstandard_paths.py::test_getpublicnode[m-3h-100h-4-255-script_types1]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_nonstandard_paths.py::test_getpublicnode[m-4-255-script_types0]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_nonstandard_paths.py::test_getpublicnode[m-49-0-63-0-255-script_types4]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_nonstandard_paths.py::test_signmessage[m-1195487518-6-255-script_types3]": "cdca9baa79780e4708508a89efa2a428674592693eba59095d44194e7b20223e", +"TR_bitcoin-test_nonstandard_paths.py::test_signmessage[m-1195487518-script_types2]": "5cdcb580039d06f9bd1a76deced46cf9b97b887c48cd4a18493f4b6f4278ce00", +"TR_bitcoin-test_nonstandard_paths.py::test_signmessage[m-3h-100h-4-255-script_types1]": "f5a8545ecc94f5e962f882c36aa549f5c0be4e9172b7700050eb52701a404b19", +"TR_bitcoin-test_nonstandard_paths.py::test_signmessage[m-4-255-script_types0]": "7c877d9518ccb400567fd00914356de5acb24e2fb87427c209c24946532cb71f", +"TR_bitcoin-test_nonstandard_paths.py::test_signmessage[m-49-0-63-0-255-script_types4]": "35eb1d016f26ea8e7d67e4cf397406c360619207fdac840b1e543116a9f4c9f1", +"TR_bitcoin-test_nonstandard_paths.py::test_signtx[m-1195487518-6-255-script_types3]": "2335ba47fb0082da8b247cf225605db9964ec3ac959461757fc49675c228bd4e", +"TR_bitcoin-test_nonstandard_paths.py::test_signtx[m-1195487518-script_types2]": "16647b541a1c7483db297bbd5b784e339364293ef80e8a27ae27ee0084b544e6", +"TR_bitcoin-test_nonstandard_paths.py::test_signtx[m-3h-100h-4-255-script_types1]": "b04a9dc5e6bc9bfc1877039028e454d8a07a07f00cd86f095bca0162f8d528e7", +"TR_bitcoin-test_nonstandard_paths.py::test_signtx[m-4-255-script_types0]": "b04a9dc5e6bc9bfc1877039028e454d8a07a07f00cd86f095bca0162f8d528e7", +"TR_bitcoin-test_nonstandard_paths.py::test_signtx[m-49-0-63-0-255-script_types4]": "ef84e13e3c662df6ca6600ebb433729a44c80a3884b4c9f0bbc5c4a3faae9cc7", +"TR_bitcoin-test_nonstandard_paths.py::test_signtx_multisig[paths0-address_index0]": "dcb0df9138484d29950c22ff3f4286f80f85829b4179ddb6866fda6f6968e373", +"TR_bitcoin-test_nonstandard_paths.py::test_signtx_multisig[paths1-address_index1]": "dcb0df9138484d29950c22ff3f4286f80f85829b4179ddb6866fda6f6968e373", +"TR_bitcoin-test_nonstandard_paths.py::test_signtx_multisig[paths2-address_index2]": "692f90e71427151fef89fdaa506d549985d4839272f0e671f0edc365e856b6cb", +"TR_bitcoin-test_nonstandard_paths.py::test_signtx_multisig[paths3-address_index3]": "40cdc50a581b5d2c02ccbf4c2df752f608a93bcb265a78057b2ad3e662968714", +"TR_bitcoin-test_nonstandard_paths.py::test_signtx_multisig[paths4-address_index4]": "dcb0df9138484d29950c22ff3f4286f80f85829b4179ddb6866fda6f6968e373", +"TR_bitcoin-test_nonstandard_paths.py::test_signtx_multisig[paths5-address_index5]": "dcb0df9138484d29950c22ff3f4286f80f85829b4179ddb6866fda6f6968e373", +"TR_bitcoin-test_nonstandard_paths.py::test_signtx_multisig[paths6-address_index6]": "dcb0df9138484d29950c22ff3f4286f80f85829b4179ddb6866fda6f6968e373", +"TR_bitcoin-test_nonstandard_paths.py::test_signtx_multisig[paths7-address_index7]": "abd4bfa80d97cfa9226995fcd6c736cb540b9c1f0c3cad4242aede7d5ac20ae7", +"TR_bitcoin-test_op_return.py::test_nonzero_opreturn": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_op_return.py::test_opreturn": "450ea7ca6beb8107b1bfccc93f52fda4591a8236a0d9579b63c184885ad29621", +"TR_bitcoin-test_op_return.py::test_opreturn_address": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_peercoin.py::test_timestamp_included": "9939242daff6ae4d9bb8578da3b880e54bf008766ef57a0211dcd9b325ca157f", +"TR_bitcoin-test_peercoin.py::test_timestamp_missing": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_peercoin.py::test_timestamp_missing_prevtx": "b92fde37769c85e0ae35913c97aef385f271a0753f20e011012e625816d9e127", +"TR_bitcoin-test_signmessage.py::test_signmessage[NFC message]": "71f423c2b32a3633e4467ec31f1209962541b6de419370bbc251a83e3162338a", +"TR_bitcoin-test_signmessage.py::test_signmessage[NFKD message]": "71f423c2b32a3633e4467ec31f1209962541b6de419370bbc251a83e3162338a", +"TR_bitcoin-test_signmessage.py::test_signmessage[bcash]": "bd2bd6a8a44d81cb521ce9ac9ea1e48db7bf3e915e01312747132db175334ffd", +"TR_bitcoin-test_signmessage.py::test_signmessage[decred-empty]": "c7f3967461f3eaca0b0d20f8fb58a8ceb8ad6f4102cc771dc6e974925a4da2c2", +"TR_bitcoin-test_signmessage.py::test_signmessage[decred]": "6bba3ba240fe29a189ce1141f9df0bba81ce8e13da11373ef32ccf95057b2188", +"TR_bitcoin-test_signmessage.py::test_signmessage[grs-p2pkh]": "14547b0dccf8b8a74f9e05506bc1845668c996d2e75110d19aa898572f13895e", +"TR_bitcoin-test_signmessage.py::test_signmessage[grs-segwit-native]": "7020490c3a4ac02b5cac26d1a680fd16968d242a8d1f2e9fe8c1f63cbe9918c4", +"TR_bitcoin-test_signmessage.py::test_signmessage[grs-segwit-p2sh]": "38de77ff1cff1e3d75d888186f1663b64481a80d69f89fd3afa5ecaf7f73db5c", +"TR_bitcoin-test_signmessage.py::test_signmessage[p2pkh long message]": "60d9a0c765c187e8f10e8fe9eb304047f3c1098c8216ae53501dc8bd70a76668", +"TR_bitcoin-test_signmessage.py::test_signmessage[p2pkh0]": "bf0ea70b034ea47403b9fd8a9605e8c4d78c2dfedd7f583ccfa78eacff0b340b", +"TR_bitcoin-test_signmessage.py::test_signmessage[p2pkh1]": "bf0ea70b034ea47403b9fd8a9605e8c4d78c2dfedd7f583ccfa78eacff0b340b", +"TR_bitcoin-test_signmessage.py::test_signmessage[p2pkh2]": "867e8eeda495bc740905ea13178991e599a3fe823e3946247a7f97e28ed9bc42", +"TR_bitcoin-test_signmessage.py::test_signmessage[segwit-native long message]": "c45d69e2fe4d249cb77a004e3328eb90e279878447fe1dc52cdb7cb7d4528619", +"TR_bitcoin-test_signmessage.py::test_signmessage[segwit-native0]": "374902fa5998da92a2fb8b36104b42163a456d55103ceccb0855e7de65477b89", +"TR_bitcoin-test_signmessage.py::test_signmessage[segwit-native1]": "374902fa5998da92a2fb8b36104b42163a456d55103ceccb0855e7de65477b89", +"TR_bitcoin-test_signmessage.py::test_signmessage[segwit-native2]": "2ccf184b029570368a45a9a3c0a775a13ef2123b602415204b1b0588efa6734e", +"TR_bitcoin-test_signmessage.py::test_signmessage[segwit-p2sh long message]": "798a30ab763f95fd3d12b937880d876fe8b86c76fe4216c7232f125b994640f2", +"TR_bitcoin-test_signmessage.py::test_signmessage[segwit-p2sh0]": "30067e1b131e8989f1a8fa29d82d93996a38d06e266f1004dd63eb99fe7f30a1", +"TR_bitcoin-test_signmessage.py::test_signmessage[segwit-p2sh1]": "30067e1b131e8989f1a8fa29d82d93996a38d06e266f1004dd63eb99fe7f30a1", +"TR_bitcoin-test_signmessage.py::test_signmessage[segwit-p2sh2]": "758ef7d9d2b4531dd8061cd3bbea2488a020ad1641cdbc23f128c64f33b2466a", +"TR_bitcoin-test_signmessage.py::test_signmessage[t1 firmware path]": "d1bbde45cee22bacfc18ac086c844c4a02fc62b5c173a1323a0495614136a5ec", +"TR_bitcoin-test_signmessage.py::test_signmessage_pagination[long_words]": "b92575287142703d069268966a344269cfeec0166a390d8dfe1fe4409e3b85ea", +"TR_bitcoin-test_signmessage.py::test_signmessage_pagination[newlines]": "c54f8f050db3e2f3cf2db5512dc54f3d2305151543dfba525b2c5f1eed4b168a", +"TR_bitcoin-test_signmessage.py::test_signmessage_pagination[no_spaces]": "0a1367f5c4c6e3d96caeb5b634d1a80999d83d02a07354d79c2c23cbc443873f", +"TR_bitcoin-test_signmessage.py::test_signmessage_pagination[normal_text]": "eb1876a6d4578359ab99270549289a24084d100dc5c310a4d128c74c9a09fb2f", +"TR_bitcoin-test_signmessage.py::test_signmessage_pagination[single_line_over]": "28a2373bff4178fe6a7c2e6faafbb149eaacb3e7f072c59a78fc6cf9b14949ce", +"TR_bitcoin-test_signmessage.py::test_signmessage_pagination[utf_nospace]": "69aacf4c2c7f025410ff4774a4c41248ea3585fbea4246b7156e7c8e85229d45", +"TR_bitcoin-test_signmessage.py::test_signmessage_pagination[utf_text]": "0c285ee50980c7ce84d31c04c8f298e3658aa9ee7a48e229801b74ae4f094aff", +"TR_bitcoin-test_signmessage.py::test_signmessage_path_warning": "fd86199a157ed8d3b85051b52573f1d4ef85bd9cc25379a8aba69d5f3408dc56", +"TR_bitcoin-test_signtx.py::test_attack_change_input_address": "93d94f33f4f8518821e8c796c0c6b2cb1a163ae6d88293f0fbe28ac32a552337", +"TR_bitcoin-test_signtx.py::test_attack_change_outputs": "0c9dd63e7b0b0a317c175236c2a536bb9cc45071610188652816c82d302126df", +"TR_bitcoin-test_signtx.py::test_attack_modify_change_address": "094f79bd475f92b6b9e15c7a9030e1aaec070bebc9e439666297c12304748fa8", +"TR_bitcoin-test_signtx.py::test_change_on_main_chain_allowed": "094f79bd475f92b6b9e15c7a9030e1aaec070bebc9e439666297c12304748fa8", +"TR_bitcoin-test_signtx.py::test_fee_high_hardfail": "c68d8c809f6eccd55eb22453963111db727780d9d3bdd3ba16dc356df8363d79", +"TR_bitcoin-test_signtx.py::test_fee_high_warning": "b75dd6c976b8be96f905ce72a0643ae84e8c103b1475993a2341a4e4903f254c", +"TR_bitcoin-test_signtx.py::test_incorrect_input_script_type[InputScriptType.EXTERNAL]": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_signtx.py::test_incorrect_input_script_type[InputScriptType.SPENDADDRESS]": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_signtx.py::test_incorrect_output_script_type[OutputScriptType.PAYTOADDRESS]": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_signtx.py::test_incorrect_output_script_type[OutputScriptType.PAYTOSCRIPTHASH]": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_signtx.py::test_lock_time[1-4294967295]": "aa243f666c311abc219ba5b43f48e64a6b615372c5bdd7891e5dbb18b899b451", +"TR_bitcoin-test_signtx.py::test_lock_time[499999999-4294967294]": "b36ffce2a4f0efc9c42a07967710c867a0948b07cbd1e5568b55d1b75adc7c94", +"TR_bitcoin-test_signtx.py::test_lock_time[500000000-4294967294]": "4c15e0d8dd25f4b6c9cf3180ebf4c1a33c13d50a2e31d5a2f634c3adffcb8fca", +"TR_bitcoin-test_signtx.py::test_lock_time_blockheight": "b36ffce2a4f0efc9c42a07967710c867a0948b07cbd1e5568b55d1b75adc7c94", +"TR_bitcoin-test_signtx.py::test_lock_time_datetime[1985-11-05 00:53:20]": "4c15e0d8dd25f4b6c9cf3180ebf4c1a33c13d50a2e31d5a2f634c3adffcb8fca", +"TR_bitcoin-test_signtx.py::test_lock_time_datetime[2048-08-16 22:14:00]": "1a43370ff45323e02bf101f71b66ae871faf01ba2d1c2c09a322d4650c1e68d4", +"TR_bitcoin-test_signtx.py::test_lots_of_change": "7bd28a1000b7586670e2668a66d04d4e4237166379354e629a9c1c95e6daa9f4", +"TR_bitcoin-test_signtx.py::test_lots_of_inputs": "5841c94d70828e498dab3a286c78e298a0af1045f57fc49e94a3658bf8d6778a", +"TR_bitcoin-test_signtx.py::test_lots_of_outputs": "b4b9082d1dc5fcc3c871504b765eca3cfbdd8ba92bad8c0b7ffe51d2a267804d", +"TR_bitcoin-test_signtx.py::test_not_enough_funds": "becd8eec38524aba13591443592b541500c18fc0b3cba980e568c4ed9c02fe87", +"TR_bitcoin-test_signtx.py::test_not_enough_vouts": "dcb4d8aa19e5263b30b07ab7f197d1bc2f57cb631e4a10c31e075e4e15fefa6c", +"TR_bitcoin-test_signtx.py::test_one_one_fee": "0027dbf7dfa8758efb7371d0f470658cded72b9acb2f2bdd23d45f291e645a7b", +"TR_bitcoin-test_signtx.py::test_one_three_fee": "0eff6b810c49a36bd437d915552f5f7f4277450437f3a8c39d8beece7a870271", +"TR_bitcoin-test_signtx.py::test_one_two_fee": "a818a0e1ecc9c645f67d9ccc4c7581746cdd967e09034d1fd527daca8f6414f8", +"TR_bitcoin-test_signtx.py::test_p2sh": "af2f731666cc82adb82400db2479109a75b365d35224b8670167308b751ff2f6", +"TR_bitcoin-test_signtx.py::test_prevtx_forbidden_fields[branch_id-13]": "8453f1d7f78a6a3ee1062d5cc2ecef4c4750327ac8044a0f0c8d5b46d6b5fb54", +"TR_bitcoin-test_signtx.py::test_prevtx_forbidden_fields[expiry-9]": "8453f1d7f78a6a3ee1062d5cc2ecef4c4750327ac8044a0f0c8d5b46d6b5fb54", +"TR_bitcoin-test_signtx.py::test_prevtx_forbidden_fields[extra_data-hello world]": "8453f1d7f78a6a3ee1062d5cc2ecef4c4750327ac8044a0f0c8d5b46d6b5fb54", +"TR_bitcoin-test_signtx.py::test_prevtx_forbidden_fields[timestamp-42]": "8453f1d7f78a6a3ee1062d5cc2ecef4c4750327ac8044a0f0c8d5b46d6b5fb54", +"TR_bitcoin-test_signtx.py::test_prevtx_forbidden_fields[version_group_id-69]": "8453f1d7f78a6a3ee1062d5cc2ecef4c4750327ac8044a0f0c8d5b46d6b5fb54", +"TR_bitcoin-test_signtx.py::test_signtx_forbidden_fields[branch_id-13]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_signtx.py::test_signtx_forbidden_fields[expiry-9]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_signtx.py::test_signtx_forbidden_fields[timestamp-42]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_signtx.py::test_signtx_forbidden_fields[version_group_id-69]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_signtx.py::test_spend_coinbase": "32d8f55bfb72bcc0b39b361379005b3d59d2dc6fa0f02ee6ab849c8e16d83a1d", +"TR_bitcoin-test_signtx.py::test_testnet_big_amount": "788359b41d2dc6e6a6e365e71c6195dbd8b04605de26fcec3aaaf6f04c0c8ed5", +"TR_bitcoin-test_signtx.py::test_testnet_fee_high_warning": "de7047112ff73f92a5940b942f4d6948e9a6fe8d719cd26f87df13e77644cca7", +"TR_bitcoin-test_signtx.py::test_testnet_one_two_fee": "094f79bd475f92b6b9e15c7a9030e1aaec070bebc9e439666297c12304748fa8", +"TR_bitcoin-test_signtx.py::test_two_changes": "6ce50782a0c77935c25355597d7a302115eade1e1d24e1e0f5f99015a2879da0", +"TR_bitcoin-test_signtx.py::test_two_two": "217cf9b6d294c1d5b81395898b86a61b018445f4d9877d1ecd64755fd1f15478", +"TR_bitcoin-test_signtx_amount_unit.py::test_signtx_btc[AmountUnit.BITCOIN]": "0027dbf7dfa8758efb7371d0f470658cded72b9acb2f2bdd23d45f291e645a7b", +"TR_bitcoin-test_signtx_amount_unit.py::test_signtx_btc[AmountUnit.MICROBITCOIN]": "98bd8eb214831b43ac9645a659a0c9b6259e5cf1fad5d736b73bbb665806e609", +"TR_bitcoin-test_signtx_amount_unit.py::test_signtx_btc[AmountUnit.MILLIBITCOIN]": "624f3bc8975a0737bb4d1bb71b3ae46fb42e14cebb96d1a2862421612a8a6788", +"TR_bitcoin-test_signtx_amount_unit.py::test_signtx_btc[AmountUnit.SATOSHI]": "f96285da2c49ee9f17bab6ddcc68457044c11dc4d600a77bbb9b89de87194857", +"TR_bitcoin-test_signtx_amount_unit.py::test_signtx_btc[None]": "0027dbf7dfa8758efb7371d0f470658cded72b9acb2f2bdd23d45f291e645a7b", +"TR_bitcoin-test_signtx_amount_unit.py::test_signtx_testnet[AmountUnit.BITCOIN]": "fda2cfc5e937611a4dff181e98abee3773e74e0c78bf7e362c2bc2a53d6c614f", +"TR_bitcoin-test_signtx_amount_unit.py::test_signtx_testnet[AmountUnit.MICROBITCOIN]": "38f7d96a22ddcee844a2dee03fc005e673a2429cac5f8edb225f855e250fa35f", +"TR_bitcoin-test_signtx_amount_unit.py::test_signtx_testnet[AmountUnit.MILLIBITCOIN]": "05080ddd194c5898065ca667be966c961d39991714e6eb1a20828e535ea8700a", +"TR_bitcoin-test_signtx_amount_unit.py::test_signtx_testnet[AmountUnit.SATOSHI]": "c5198a0732e85ee2c10899890f506795cb7ed716ba704c1c06fd287f23e15c12", +"TR_bitcoin-test_signtx_amount_unit.py::test_signtx_testnet[None]": "fda2cfc5e937611a4dff181e98abee3773e74e0c78bf7e362c2bc2a53d6c614f", +"TR_bitcoin-test_signtx_external.py::test_p2pkh_presigned": "b241d7381a8fefc6117a46e5d79fdfb12cab0a7c482f413cfaed11c1c2d3cd71", +"TR_bitcoin-test_signtx_external.py::test_p2pkh_with_proof": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_signtx_external.py::test_p2tr_external_presigned": "c8cfd18018d624f4119370628828b58cf3f4f4a54eaec6a2d4c636b3ebcf4451", +"TR_bitcoin-test_signtx_external.py::test_p2tr_external_unverified": "9bdb87d4ff85ba84b1958064d4ea86b8130041f3c97c97cabe9289b16fe38420", +"TR_bitcoin-test_signtx_external.py::test_p2tr_with_proof": "e0b89909544e42ab52b02023ea3c48f4c526591b3e1ef0c0478eea60d7f1ccf6", +"TR_bitcoin-test_signtx_external.py::test_p2wpkh_external_unverified": "f8a0df778ac21688478f9c048c2b413d40667280dc5e1884862b96bf8d0f02f7", +"TR_bitcoin-test_signtx_external.py::test_p2wpkh_in_p2sh_presigned": "de270052f860379e3eb16b8f04b5f28a18d32a20885766bcc691734d75d8ac01", +"TR_bitcoin-test_signtx_external.py::test_p2wpkh_in_p2sh_with_proof": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_signtx_external.py::test_p2wpkh_presigned": "15b05306dec8a0374d7a8d716878ccc637cb18943e59a7bc62a8e0d705af130a", +"TR_bitcoin-test_signtx_external.py::test_p2wpkh_with_false_proof": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_signtx_external.py::test_p2wpkh_with_proof": "b9121e13b138f66948b831df6c7fbd5469ff997aab4051106a9365c145bd9e7f", +"TR_bitcoin-test_signtx_external.py::test_p2wsh_external_presigned": "bd93cc3e28de9c7e09743e40df9d0a30232b9299196b6b807e896efcc0080db3", +"TR_bitcoin-test_signtx_invalid_path.py::test_attack_path_segwit": "e700102c4b9155c15f2365fd2bb8af96a44bf4c059027870e3f852504fb56c00", +"TR_bitcoin-test_signtx_invalid_path.py::test_invalid_path_fail": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_signtx_invalid_path.py::test_invalid_path_fail_asap": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_signtx_invalid_path.py::test_invalid_path_pass_forkid": "6cf109f1fe46094d51c0aceb4436796504693bcfb570bf2789adf7a74e8e2bf3", +"TR_bitcoin-test_signtx_invalid_path.py::test_invalid_path_prompt": "602acab1aa3756887ca198da4a8c3844eca150ee86d208b24ffcca9bfb4388ff", +"TR_bitcoin-test_signtx_mixed_inputs.py::test_non_segwit_segwit_inputs": "028bc514c1ae38d964afb1bb1943c2ec27aee623538b504ab78d01de3e0b2bf0", +"TR_bitcoin-test_signtx_mixed_inputs.py::test_non_segwit_segwit_non_segwit_inputs": "a55193664cf6b5acb1f1d300f4f591634eb6832cfdae3f212062f0ea2d4c5b04", +"TR_bitcoin-test_signtx_mixed_inputs.py::test_segwit_non_segwit_inputs": "028bc514c1ae38d964afb1bb1943c2ec27aee623538b504ab78d01de3e0b2bf0", +"TR_bitcoin-test_signtx_mixed_inputs.py::test_segwit_non_segwit_segwit_inputs": "2ca0c1c56e6b2f08542eec651811f3d505a760de3eae2837dd7db79233bf8522", +"TR_bitcoin-test_signtx_payreq.py::test_payment_req_wrong_amount": "0feb6c74789a7c20812edc3d05ec47a3b9397f033cd8a4104455667c9d360f6f", +"TR_bitcoin-test_signtx_payreq.py::test_payment_req_wrong_mac_purchase": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_signtx_payreq.py::test_payment_req_wrong_mac_refund": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_signtx_payreq.py::test_payment_req_wrong_output": "c4f8a7f2ca33d7f696218159e6fb85b98cb674176c7508fdd4d8587f523fa8de", +"TR_bitcoin-test_signtx_payreq.py::test_payment_request[out0+out1]": "871adeced80d88ba0fa4e28fd9fd4abcf1047d461760928ab36355422cf0022d", +"TR_bitcoin-test_signtx_payreq.py::test_payment_request[out012]": "a317300fe72e4021fe6ec83f5ca0acbfc3d249603281fc4c40e6e306bd43e943", +"TR_bitcoin-test_signtx_payreq.py::test_payment_request[out01]": "b4b17de615246beba516c421937aa33c996dd071e80528bd707472e543d864e4", +"TR_bitcoin-test_signtx_payreq.py::test_payment_request[out0]": "511e2fae2f5d3586cc4cfabaf9e5ee4e97ed557384da931a9c94a12903ce251a", +"TR_bitcoin-test_signtx_payreq.py::test_payment_request[out12]": "7a1ffdb36c7a8cc136b9c54c558066e46c46d2c61ed3fccbf7c0b07f8c797528", +"TR_bitcoin-test_signtx_payreq.py::test_payment_request[out1]": "f8a3a112a31a8c672e30f0747e31923ed2ce23d4f56c41e2af1674828c10118c", +"TR_bitcoin-test_signtx_payreq.py::test_payment_request[out2]": "622fe0f5bd74a4950c0df46c286ad4bc4e4e949ac2c4293dd40252df0d755626", +"TR_bitcoin-test_signtx_payreq.py::test_payment_request_details": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_signtx_prevhash.py::test_invalid_prev_hash[]": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_signtx_prevhash.py::test_invalid_prev_hash[hello world]": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_signtx_prevhash.py::test_invalid_prev_hash[x]": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_signtx_prevhash.py::test_invalid_prev_hash[xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx]": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_signtx_prevhash.py::test_invalid_prev_hash_attack[]": "bf5fd1fbe139b33d65886af5dfaf60e896bba0c96755e96ff5e8b17f5bf9d867", +"TR_bitcoin-test_signtx_prevhash.py::test_invalid_prev_hash_attack[hello world]": "bf5fd1fbe139b33d65886af5dfaf60e896bba0c96755e96ff5e8b17f5bf9d867", +"TR_bitcoin-test_signtx_prevhash.py::test_invalid_prev_hash_attack[x]": "bf5fd1fbe139b33d65886af5dfaf60e896bba0c96755e96ff5e8b17f5bf9d867", +"TR_bitcoin-test_signtx_prevhash.py::test_invalid_prev_hash_attack[xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx]": "bf5fd1fbe139b33d65886af5dfaf60e896bba0c96755e96ff5e8b17f5bf9d867", +"TR_bitcoin-test_signtx_prevhash.py::test_invalid_prev_hash_in_prevtx[]": "f0271524cd8333424dbf3887faa8f150fdd25b29ed02aba868ac8985f8fc9d73", +"TR_bitcoin-test_signtx_prevhash.py::test_invalid_prev_hash_in_prevtx[hello world]": "f0271524cd8333424dbf3887faa8f150fdd25b29ed02aba868ac8985f8fc9d73", +"TR_bitcoin-test_signtx_prevhash.py::test_invalid_prev_hash_in_prevtx[x]": "f0271524cd8333424dbf3887faa8f150fdd25b29ed02aba868ac8985f8fc9d73", +"TR_bitcoin-test_signtx_prevhash.py::test_invalid_prev_hash_in_prevtx[xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx]": "f0271524cd8333424dbf3887faa8f150fdd25b29ed02aba868ac8985f8fc9d73", +"TR_bitcoin-test_signtx_replacement.py::test_attack_fake_ext_input_amount": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_signtx_replacement.py::test_attack_fake_int_input_amount": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_signtx_replacement.py::test_attack_false_internal": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_signtx_replacement.py::test_attack_steal_change": "24843d78dac0c4a13f1d66fc6a242be5a98ac9a398c02ea74a96b63127659677", +"TR_bitcoin-test_signtx_replacement.py::test_p2pkh_fee_bump": "7fd20ce8772837d945e6a4747efcdfd3f1d5e970b4df049853df01409c75b876", +"TR_bitcoin-test_signtx_replacement.py::test_p2tr_fee_bump": "e16ecf98c80e69396f5fa3a87dbcb04d55c874c1abaf6e52fd4db76defce168a", +"TR_bitcoin-test_signtx_replacement.py::test_p2tr_invalid_signature": "e16ecf98c80e69396f5fa3a87dbcb04d55c874c1abaf6e52fd4db76defce168a", +"TR_bitcoin-test_signtx_replacement.py::test_p2wpkh_finalize": "4d09268711a1778f7a20e0b2c0f195a7882f03150463b32e3d741180b6a9860e", +"TR_bitcoin-test_signtx_replacement.py::test_p2wpkh_in_p2sh_fee_bump_from_external": "64cd45b3ca26b7262d1b7f8a24143c592a01ad4b8a2247df77c8e8ba13cbd3d2", +"TR_bitcoin-test_signtx_replacement.py::test_p2wpkh_in_p2sh_remove_change": "3c965e6147eefaacbc371ba5cd36571b4ef88c6ed175d265473147d63b1c0c12", +"TR_bitcoin-test_signtx_replacement.py::test_p2wpkh_invalid_signature": "4d09268711a1778f7a20e0b2c0f195a7882f03150463b32e3d741180b6a9860e", +"TR_bitcoin-test_signtx_replacement.py::test_p2wpkh_op_return_fee_bump": "84d8247f554f7f63326f9b4aad5163769a732f3b580569448efbbf71f676d114", +"TR_bitcoin-test_signtx_replacement.py::test_p2wpkh_payjoin[19909659-90000-02483045022100aa1b91-c9b963ae": "d6cab7c9c15e4d22aec5bbded25336477b2310e900ac71c15408c78a63b49f9d", +"TR_bitcoin-test_signtx_replacement.py::test_p2wpkh_payjoin[19909718-90000-024730440220753f5304-ecb983d1": "d6cab7c9c15e4d22aec5bbded25336477b2310e900ac71c15408c78a63b49f9d", +"TR_bitcoin-test_signtx_replacement.py::test_p2wpkh_payjoin[19909800-89859-0248304502210097a42b-7a89e474": "d6cab7c9c15e4d22aec5bbded25336477b2310e900ac71c15408c78a63b49f9d", +"TR_bitcoin-test_signtx_replacement.py::test_p2wpkh_payjoin[19909859-89800-02483045022100af3a87-80428fad": "cfdc8a75720f592bfb469e577f942b593ea0cecfa36b4f72b5a9e5a11f3dcb36", +"TR_bitcoin-test_signtx_replacement.py::test_p2wpkh_payjoin[19909859-89859-02483045022100eb74ab-881c7bef": "d6cab7c9c15e4d22aec5bbded25336477b2310e900ac71c15408c78a63b49f9d", +"TR_bitcoin-test_signtx_replacement.py::test_tx_meld": "e6e6fbfc4e9e7c63c45ca4b19af66099b4943bb0427b607428e213225e7e5eaa", +"TR_bitcoin-test_signtx_segwit.py::test_attack_change_input_address": "e55c48475216dbb21f6f6d27ab87c6c30126b6b5ee127162b0d279efee90d9bc", +"TR_bitcoin-test_signtx_segwit.py::test_attack_mixed_inputs": "ca3ca7ae24a3696368d258c622e61bc8e130e97db71918f12fba6ce0bb4975ff", +"TR_bitcoin-test_signtx_segwit.py::test_send_multisig_1": "e100aa361abfe2bac68d4aa535b28685a0553841e474d88a7010e36e17147388", +"TR_bitcoin-test_signtx_segwit.py::test_send_p2sh": "dec13e7b6aaa9bc1ed7e61fac79c19d61c8222d563765cb9669ff53f63afdf68", +"TR_bitcoin-test_signtx_segwit.py::test_send_p2sh_change": "ec43323f7491c5df8d6cfa68045b6bf1f7b6eeacba31ae95dca971f1a4fd4d1b", +"TR_bitcoin-test_signtx_segwit.py::test_testnet_segwit_big_amount": "f3b0baf3b1a9adb05be3648fd4f73f742bee48cfd0573c9933987518dc6d4ca2", +"TR_bitcoin-test_signtx_segwit_native.py::test_multisig_mismatch_inputs_single": "264385a63be2838c607927a39a7ebf6c3b577b833724a4cc38f7ff5890e0376d", +"TR_bitcoin-test_signtx_segwit_native.py::test_send_both": "a6f608699cac6c51764bd2cc6769bfefd11601ca22705bdd5ea6dd7b9a0603be", +"TR_bitcoin-test_signtx_segwit_native.py::test_send_multisig_1": "f9263723daf25484d9173a9615eb248ea1fc1a31d2ce59209df2a9e7ebc7d43d", +"TR_bitcoin-test_signtx_segwit_native.py::test_send_multisig_2": "6748ee042e28c18bdf3f5745786ffaf7fcd0be6cb2ac8bb6a7930ca00e4a0da0", +"TR_bitcoin-test_signtx_segwit_native.py::test_send_multisig_3_change": "3729db5bc7d2637da1ec9c90037334f6bf43af48d630af82907bff940c2337de", +"TR_bitcoin-test_signtx_segwit_native.py::test_send_multisig_4_change": "aa9dd2e969e33bb2dc0272dfe81f7ee06844b4def479a70a72fb51c2e272a493", +"TR_bitcoin-test_signtx_segwit_native.py::test_send_native": "fda2cfc5e937611a4dff181e98abee3773e74e0c78bf7e362c2bc2a53d6c614f", +"TR_bitcoin-test_signtx_segwit_native.py::test_send_native_change": "4ad8f245c57d59f9171ebb14c13c2e002610e994dd938244e7e96016b000c917", +"TR_bitcoin-test_signtx_segwit_native.py::test_send_p2sh": "4d9afb8140599b4998ba5b959cfa15893478650597c074784a2cd7ba7a91fbfb", +"TR_bitcoin-test_signtx_segwit_native.py::test_send_p2sh_change": "011be3f1a33b6a59d40bfc896465a3d147ec0f347846291a7ff3a12fdf4bcdcb", +"TR_bitcoin-test_signtx_segwit_native.py::test_send_to_taproot": "5525737ffc5b31088ca2fe4a75ab317b21e4c4d52bfda783c09d851d1e408efa", +"TR_bitcoin-test_signtx_taproot.py::test_attack_script_type": "c33efccf209159e9793cb4f9af210a5e387138d1146c279c18ebd2b69a192a3b", +"TR_bitcoin-test_signtx_taproot.py::test_send_invalid_address[tb1pam775nxmvam4pfpqlm5q06k0y84e3-a257be51": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_signtx_taproot.py::test_send_invalid_address[tb1plllllllllllllllllllllllllllll-aaa668e3": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_signtx_taproot.py::test_send_invalid_address[tb1plycg5qvjtrp3qjf5f7zl382j9x6nr-5447628e": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_signtx_taproot.py::test_send_invalid_address[tb1zlycg5qvjtrp3qjf5f7zl382j9x6nr-880d4a6b": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_bitcoin-test_signtx_taproot.py::test_send_mixed": "7086414beefcfc522d0231712aa959e1ed3f6512c1cd072dbbb98e14865bf262", +"TR_bitcoin-test_signtx_taproot.py::test_send_p2tr": "693ecc50df8caecd521f4cdb7eca6dff0757832deb7a80e2bb88ccd0ac4c0474", +"TR_bitcoin-test_signtx_taproot.py::test_send_two_with_change": "3858a5f1fb88b3120466dcb1ad0cc744c02077a667374d595f48d30f189737a9", +"TR_bitcoin-test_verifymessage.py::test_message_grs": "e2d9597cd3941fff681101b2c3db21b0e830ea4f7e23002da0c7c6b2d72c1e67", +"TR_bitcoin-test_verifymessage.py::test_message_long": "c10e66739bb99b7b85a7f22a31129e1889abc7df1a6c9ac1adb007e6d5492a22", +"TR_bitcoin-test_verifymessage.py::test_message_testnet": "6f74b8891c31aaac53b8dd99902869d8fdc863511d86844a6c9e58d1e34bc7de", +"TR_bitcoin-test_verifymessage.py::test_message_verify": "7a3ec8f57d9b417ec9731dcdb74df2012af889bfdc88ea1eeddaba5bf402e0e5", +"TR_bitcoin-test_verifymessage.py::test_message_verify_bcash": "d6269529f1143f86af92b4eb76b26baa750c5d2eb281a27f61ef5a4008177b6b", +"TR_bitcoin-test_verifymessage.py::test_verify_bitcoind": "66982388ca3969c7506fef34636fd3031239b1c006cb89ce2ea597a2ca81ccf7", +"TR_bitcoin-test_verifymessage.py::test_verify_utf": "8929e120df4ff6108caa13b139d7ed53dfaa86318634e3b0fa0f07346f385a78", +"TR_bitcoin-test_verifymessage_segwit.py::test_message_long": "d2d3d89984f08fed458fd14a92ec6df21038154fde7eb1ddd8a7fc814d5ca530", +"TR_bitcoin-test_verifymessage_segwit.py::test_message_testnet": "79aabc00863c912b0287a9fb965256eccbd36e15e641ff9003fb58cf9e382f86", +"TR_bitcoin-test_verifymessage_segwit.py::test_message_verify": "67495abb2fa2924b2039cde6adb53ea81713197daaf84e630a9c66b759974690", +"TR_bitcoin-test_verifymessage_segwit.py::test_verify_utf": "fa88d63e456c3d5d177744718ae19cde50a73e930bb97e20ad5ca58485078563", +"TR_bitcoin-test_verifymessage_segwit_native.py::test_message_long": "7166cfcff11c41d4c06159b882d4c3f8a340708bd6f646442aeb96ce2a224ecb", +"TR_bitcoin-test_verifymessage_segwit_native.py::test_message_testnet": "a449f4f9df8f1d0c7af509bc1921593127f5567fc98e1585ee58c18e9d365e9e", +"TR_bitcoin-test_verifymessage_segwit_native.py::test_message_verify": "3ba5082a880384a525bde7b2a43d89e85423a2b57abc4ea2c049ad53d56c3921", +"TR_bitcoin-test_verifymessage_segwit_native.py::test_verify_utf": "c6b76de79a48abf53f266ed9ab5f616831e31ff2abc586e7da67bccbdbbeef1e", +"TR_bitcoin-test_zcash.py::test_external_presigned": "1b8667fe36eaaf077c0e59f451091dffea61b40a19487a32bdfe7a8285d62e41", +"TR_bitcoin-test_zcash.py::test_one_one_fee_sapling": "12d2c2fc30a88b6fffec2eaed3580af3773f2ba53aa32e62760d6093299f1dcb", +"TR_bitcoin-test_zcash.py::test_spend_old_versions": "5be238d746f2b7fb038d50807b47d2225128147b5b97f28564e1978d563bb7b6", +"TR_bitcoin-test_zcash.py::test_v3_not_supported": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_bitcoin-test_zcash.py::test_version_group_id_missing": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[icarus-derivation]": "0ac2f822d0a26f57ca9a6e5b997ba5cb3a99df7578c487507fa3f6de12fd060e", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[icarus-trezor-derivation]": "f24e4ef7f192deee960aaa3af4de3e7572fc1c631f70389d7fb0013ce7cc3eb9", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[ledger-derivation]": "439fd55d0631f1f941ea405cf1aa8b0e80a5e0bfceeaadc82988a9f67a95c91d", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters0-result0]": "0507fc1c9ad8ff278b7d516d5c0d829c05d10ca20fbfe33fafcc33ca11d56fbc", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters1-result1]": "7cb433c8a8402ea6e7652ddf4692fe022ca2fa553c75730ecf39c243aa13174b", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters10-result10]": "ce10f830013729b683d95f0d85d80997b8c6704e11580003db4c7f600a25850f", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters11-result11]": "e1a10b4ae9d1fe6505851f2477b53ca47468ddce08cd3773b04e2a1d73550e61", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters12-result12]": "f43e0bd658721a523b1b219a48f25728e6b1d66d51e1bae6c9d18d5cebc979dc", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters13-result13]": "fd0a229e7a262ca8a1b2ecdbe34240d501706676a54c6face97f1b7bc3749116", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters14-result14]": "c7d1957a664aaa612c98b6acb820e2775d23be789f61d8faa4794c7a57c5269b", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters15-result15]": "5403f1fa4e4e81afcb82ad45aed9efb35eef2bc18757ad515017cb350f6e322f", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters16-result16]": "0e99dae4fe44d8a9313dbdd5831557d79da55f72f510dc2fe6779d93050179b9", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters17-result17]": "1638da4eb1d5a28d17db0434458371b7c62d780c7ef26eedf6786299890c8add", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters18-result18]": "4604c8a9a1618ebf73cfa0f6873f7245236dc9ecbdfb5d4a202c50f61be5c3fc", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters19-result19]": "a1af7e67b43a15a3c8b2927107224faf3f78cf2db16822261a567f4fa9a65d64", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters2-result2]": "9d36ae2fdf191bef0154d74d0428f9d4b6c1e0c622cf3bd449e90e7893df0968", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters20-result20]": "cf268d9b898d2a2d6089f2f59c6b4bddb4ff4a950bd1ddd1a231ff9b671a6d79", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters21-result21]": "8ed9875c03d7b553f4c9a59d4bb373cac4fd1ba71d0ffeab92c65d5b8feb3f70", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters22-result22]": "c6a239db37b31c4edbe4146a7d50442772e8e1d81f12174651a0b588a22f1660", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters23-result23]": "77fceddd4ee71a387513896fa7f389912ffe8afd7cae088853c8a74a9f8b6bbc", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters24-result24]": "7e4b356266e35ac2569e044b0143629fb89fd26137b70f885976b150bc098606", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters25-result25]": "d6438779a969f95e14156888e9199d4fb17a0aa910f031c19c1ac8db192b9464", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters26-result26]": "5991ef8e69fd09e03fed59e76652c8f26e3e18525e9db4bad96b5c0d9d177e36", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters27-result27]": "f8b1aff74b20061a9f4ad6f39426c5607a7e5869863a260a7489cafae0237b63", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters28-result28]": "227078060934a13e919e01493155d8a38693ec161855474cd56e31605c6e61f8", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters29-result29]": "dc7b1fe4abffe1ed399d891e3f402e56b224a438366dc12025ac50cae7fadfdf", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters3-result3]": "d485d443b7beb733ef911394e851506c0d076667530179794b31556d1329bb3f", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters30-result30]": "e3ccf181a3ace32266c642ed022c3e0c3575526bf47a8c928ddc020530e393d5", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters31-result31]": "84b45905318e5707123e041861b44456645e9f12ab250d570ac2ba8e61775635", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters32-result32]": "1967679d1092009d843289f8066db7085236122b5eeb3785a99e4c2076b62ef8", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters33-result33]": "87b1fdcd48b61e35dad256538cc26fc1eade7d4f977d5333230dc86c8a1995af", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters34-result34]": "45107d04e82da59b0f2df70f9ff2d4bb2222ef55869ee636e4c93b31bc0fcbcb", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters35-result35]": "6ad2c6146cf6dee69f40025e7df7698a7820bfa120d882ed54e6688b25219b22", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters36-result36]": "a699e3a7a9b4f3c61176d76cc5548c729939012a5d3b110d7b04692235e6dce0", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters4-result4]": "f51362c14860a8df3e15e6a3315d9c9033a05b91f0cfbd33e12eee5da4cf7b28", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters5-result5]": "5c313bb37bb4ed67a06dcb58db7276f80dc0b42a97846d8496133d6510a477d4", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters6-result6]": "1b7504fbfa74c0c9e574a236a68fcec4db0d4f37028e63a1d7923e04c981deac", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters7-result7]": "5852f94c427a3e2c9a2cef766f14e920ad05abba7994dda3537fa27350c8af04", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters8-result8]": "6c4c4c7dbff0bf55327f47d12520bf11decb393ad4bd970be8625f05841dcf1a", +"TR_cardano-test_address_public_key.py::test_cardano_get_address[parameters9-result9]": "c233a451a3b258d89f9d67f228f3bead3134b88112ede607af1434b1d86dab55", +"TR_cardano-test_address_public_key.py::test_cardano_get_public_key[icarus-derivation]": "97df1d41e48484b8372bf540ccc580b9532ef50e95101876777809029207deca", +"TR_cardano-test_address_public_key.py::test_cardano_get_public_key[icarus-trezor-derivation]": "97df1d41e48484b8372bf540ccc580b9532ef50e95101876777809029207deca", +"TR_cardano-test_address_public_key.py::test_cardano_get_public_key[ledger-derivation]": "97df1d41e48484b8372bf540ccc580b9532ef50e95101876777809029207deca", +"TR_cardano-test_address_public_key.py::test_cardano_get_public_key[parameters0-result0]": "97df1d41e48484b8372bf540ccc580b9532ef50e95101876777809029207deca", +"TR_cardano-test_address_public_key.py::test_cardano_get_public_key[parameters1-result1]": "97df1d41e48484b8372bf540ccc580b9532ef50e95101876777809029207deca", +"TR_cardano-test_address_public_key.py::test_cardano_get_public_key[parameters10-result10]": "97df1d41e48484b8372bf540ccc580b9532ef50e95101876777809029207deca", +"TR_cardano-test_address_public_key.py::test_cardano_get_public_key[parameters11-result11]": "97df1d41e48484b8372bf540ccc580b9532ef50e95101876777809029207deca", +"TR_cardano-test_address_public_key.py::test_cardano_get_public_key[parameters12-result12]": "97df1d41e48484b8372bf540ccc580b9532ef50e95101876777809029207deca", +"TR_cardano-test_address_public_key.py::test_cardano_get_public_key[parameters13-result13]": "97df1d41e48484b8372bf540ccc580b9532ef50e95101876777809029207deca", +"TR_cardano-test_address_public_key.py::test_cardano_get_public_key[parameters14-result14]": "b9407d461ef8c185537f9db85ef0334dcc1278ba9ac342593824ec228272ca1c", +"TR_cardano-test_address_public_key.py::test_cardano_get_public_key[parameters15-result15]": "b9407d461ef8c185537f9db85ef0334dcc1278ba9ac342593824ec228272ca1c", +"TR_cardano-test_address_public_key.py::test_cardano_get_public_key[parameters16-result16]": "b9407d461ef8c185537f9db85ef0334dcc1278ba9ac342593824ec228272ca1c", +"TR_cardano-test_address_public_key.py::test_cardano_get_public_key[parameters2-result2]": "97df1d41e48484b8372bf540ccc580b9532ef50e95101876777809029207deca", +"TR_cardano-test_address_public_key.py::test_cardano_get_public_key[parameters3-result3]": "97df1d41e48484b8372bf540ccc580b9532ef50e95101876777809029207deca", +"TR_cardano-test_address_public_key.py::test_cardano_get_public_key[parameters4-result4]": "97df1d41e48484b8372bf540ccc580b9532ef50e95101876777809029207deca", +"TR_cardano-test_address_public_key.py::test_cardano_get_public_key[parameters5-result5]": "97df1d41e48484b8372bf540ccc580b9532ef50e95101876777809029207deca", +"TR_cardano-test_address_public_key.py::test_cardano_get_public_key[parameters6-result6]": "97df1d41e48484b8372bf540ccc580b9532ef50e95101876777809029207deca", +"TR_cardano-test_address_public_key.py::test_cardano_get_public_key[parameters7-result7]": "97df1d41e48484b8372bf540ccc580b9532ef50e95101876777809029207deca", +"TR_cardano-test_address_public_key.py::test_cardano_get_public_key[parameters8-result8]": "97df1d41e48484b8372bf540ccc580b9532ef50e95101876777809029207deca", +"TR_cardano-test_address_public_key.py::test_cardano_get_public_key[parameters9-result9]": "97df1d41e48484b8372bf540ccc580b9532ef50e95101876777809029207deca", +"TR_cardano-test_derivations.py::test_bad_session": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_cardano-test_derivations.py::test_derivation_irrelevant_on_slip39[CardanoDerivationType.ICA-3b0af713": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_cardano-test_derivations.py::test_derivation_irrelevant_on_slip39[CardanoDerivationType.ICARUS]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_cardano-test_derivations.py::test_derivation_irrelevant_on_slip39[CardanoDerivationType.LEDGER]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_cardano-test_derivations.py::test_ledger_available_always": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_cardano-test_get_native_script_hash.py::test_cardano_get_native_script_hash[all_script]": "395f67eaf298b3ee3c57049f135d7d9ba522833a144e7ced9daa0c1f42ac277f", +"TR_cardano-test_get_native_script_hash.py::test_cardano_get_native_script_hash[all_script_cont-aae1e6c3": "d055c60add5cdf4ffea7d5f737b0eb772c1c74bb22e6ef452365e0226ba64883", +"TR_cardano-test_get_native_script_hash.py::test_cardano_get_native_script_hash[all_script_cont-e4ca0ea5": "82d15b09ee3869f8e6d8cc1e226b7ec88e3d64fc4851bf253c7db172c3f3267c", +"TR_cardano-test_get_native_script_hash.py::test_cardano_get_native_script_hash[any_script]": "d2f49fa3c59070160dea27f5817f916061615ed5a714ea9948f04790b112fcc9", +"TR_cardano-test_get_native_script_hash.py::test_cardano_get_native_script_hash[any_script_with-f2466a2e": "4dac9fbee2cc2b7defa1f632e7df747824da2e031943281554c594e8c35080a9", +"TR_cardano-test_get_native_script_hash.py::test_cardano_get_native_script_hash[invalid_before_script]": "b81e2d6b097ffde5bb6cfb8b72cd4293371498cf8796084048faa52edd5d520f", +"TR_cardano-test_get_native_script_hash.py::test_cardano_get_native_script_hash[invalid_hereaft-d01d7292": "626531cae63e49c4e935507def961b1627357e3542f499c7c841488a82acdea0", +"TR_cardano-test_get_native_script_hash.py::test_cardano_get_native_script_hash[n_of_k_script]": "dd9b2f3a8e124f31596d2db4d708c833e454bde641892052334839c108dc03aa", +"TR_cardano-test_get_native_script_hash.py::test_cardano_get_native_script_hash[n_of_k_script_c-d93f5fb0": "b356623fcf0516fe5bb17fd0a16e23ce5f8fa134de23e233a6faf1a7bd17dd5c", +"TR_cardano-test_get_native_script_hash.py::test_cardano_get_native_script_hash[nested_script]": "eb1ac3c581cb6e69b9bbd92496d02c1a52dc2b26ddaae4174e86f41c45941c2d", +"TR_cardano-test_get_native_script_hash.py::test_cardano_get_native_script_hash[nested_script_w-789238e6": "3093106fb1ccefd5c0ca65e8de775df67cf85627d578b26c9533f448efb44257", +"TR_cardano-test_get_native_script_hash.py::test_cardano_get_native_script_hash[pub_key_script]": "2e6d5b5c7fd85523ef1c156ef0c9453314a488ba3c43d07640335e29b6e2f61c", +"TR_cardano-test_get_native_script_hash.py::test_cardano_get_native_script_hash[pub_key_script_-1579fe2a": "5805da58abbc49bf45cb9dcaac6566cd5a5db819b40a2fb858a9a944983af655", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[byron_to_shelley_transfer]": "fd3948ecbe300eb82a65d07654f8e2320e565d3ca622ecc4df59a320acc09ab3", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[mainnet_transaction_with_change0]": "1f22f4d8ab1b09feaaaa46884956a432c9712033db2b8a7e03d70c4352d2e73a", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[mainnet_transaction_with_change1]": "e1306f25bcd3dc8b177c01efe0e6979fa4e0c413ef2b4df86c3d2ca64e70f955", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[mainnet_transaction_with_multiple_inputs]": "b3274b6dc28f012db8ad5d522541bfa2d0ea82190def9260765d91be1ec62b30", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[mainnet_transaction_without_change0]": "b3274b6dc28f012db8ad5d522541bfa2d0ea82190def9260765d91be1ec62b30", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[mainnet_transaction_without_change1]": "3a0c44af977a9b4c8abd8b9b32ac660f3c4c29bd08abd56d82c5beed19089b53", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[mary_era_transaction_with_different_policies_-1dbb1bfb": "24b5506c53e008e925b7f1c38595f06e1f8e7c644659fecc84b8d6eee4c05545", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[mary_era_transaction_with_multiasset_output]": "176a41971c425052282a7e6081013872516079eb4eed6291a2bad3029cc51932", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[mary_era_transaction_with_no_ttl-validity_start]": "a3293dba368b119493f16d4aaf66bc55442c2c6d97906cee2ba9aac7bd2447aa", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[multisig_transaction_with_a_required_signer]": "8a1e9ed65e1dbdd7cd7818ac248d5c947e807e7381000007f81e05e29503fe96", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[multisig_transaction_with_most_elements_fille-29691455": "d79790c8cb0e576b981491e06dc00a805fcab6d33b6dadb7551cfa8a13131e5d", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[multisig_transaction_with_output_datum_hash]": "d6f28a78c39ffbfa67b074466982123ab889353ba7b8b04050892d9315736823", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[multisig_transaction_with_script_data_hash]": "4c96266b1e38e640e3fb7e7788491021322713253c54da73c0d47d0bbda86794", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[multisig_transaction_with_stake_deregistratio-1ab28f77": "70a781bc09555ff1dc0865511373c4b2909a99bdffd28e78725a91e81c139e53", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[multisig_transaction_with_stake_deregistration]": "70a781bc09555ff1dc0865511373c4b2909a99bdffd28e78725a91e81c139e53", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[multisig_transaction_with_stake_registration_-a144c34c": "74d4c362de6c297db24561d5c5670234612b9a4818d5ba4680130c9d4e030856", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[multisig_transaction_with_stake_registration_-bf5f9707": "b5eeeca3e36df405d40759417998ce09697f7cbb9407f1476d10f65a13eb5e18", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[multisig_transaction_with_token_minting]": "400bc3ff4e11c74251e15b95592d09cf7a40dbe7fc9775b590cfe12aafb9b4b1", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[ordinary_transaction_with_a_required_signer]": "b218e5fa46b31875f15ab948128c2ba2811729a7801f43557541112a2a71e560", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[ordinary_transaction_with_both_output_formats]": "8c890f3e7669b3681fd2330e5e3f7cf3abc3e7f0a6b7db6e34e88b6e3d664e26", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[ordinary_transaction_with_inline_datum,_refer-d1082570": "2cffdc4708cc8c84e3922679b11f364c256c31083dd02370e490dbc4fc1081b6", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[ordinary_transaction_with_long_inline_datum,_-1f681aaa": "478afa7cea77cf323be0732ca684c96edbdf20d2c455e3a20c736ea4c5d6f08e", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[ordinary_transaction_with_multiple_correctly_-6545455a": "b8159e15b98af768c4923244ac9aecc62dff880594800a9fbf398502ea18b8d9", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[ordinary_transaction_with_network_id_included-d9df16f9": "b3274b6dc28f012db8ad5d522541bfa2d0ea82190def9260765d91be1ec62b30", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[ordinary_transaction_with_output_datum_hash]": "2cffdc4708cc8c84e3922679b11f364c256c31083dd02370e490dbc4fc1081b6", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[ordinary_transaction_with_output_datum_hash_a-84ad587a": "478afa7cea77cf323be0732ca684c96edbdf20d2c455e3a20c736ea4c5d6f08e", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[ordinary_transaction_with_script_address_but_-b0da7209": "0b38ae4c0a2f95d91c2f390d9f84651604bdc43c60f2ec99c194bea2eccfc931", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[ordinary_transaction_with_script_data_hash]": "b218e5fa46b31875f15ab948128c2ba2811729a7801f43557541112a2a71e560", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[ordinary_transaction_with_token_minting]": "c89c5064ae6fad901c4992feedb117c9f91ddbda34c619c22d133646313de6c7", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[plutus_transaction_with_an_ordinary_input]": "1817d2ee899e2170c7146df7fd9637ad8ec0bb4ab1a73ea04c9d8a8d460b9e6c", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[plutus_transaction_with_base_address_device-o-a33e6741": "be066a0a4cba4ad8c3cbf7495cbbec1c0d167be1dc3759caddba7dfe0d72be29", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[plutus_transaction_with_base_address_device-o-a6481374": "7046337eb890b0ace16449cc076914a8ac004a04d2212c810cd55c44489ba982", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[plutus_transaction_with_base_key_script_addre-d3366a63": "769c8f7a63ce2f7613d0fae59fef6a7896e096e8318938673e4c4c0b25d77ebc", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[plutus_transaction_with_device-owned_collater-8f38b4d7": "b53472cb48d26ab4661c3115f8caacf671a3b6d3d6d5d19a32419b7509908075", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[plutus_transaction_with_external_collateral_return]": "89dcc9611d5f4567b867cae82084b4027b49ecc685db84a43c446eef42ac430c", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[plutus_transaction_with_output_datum_hash]": "6db0173e7d0de96d2f6d8ebbd3608125639c76e335290ddea9a4e9ee269fc028", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[plutus_transaction_with_reference_input]": "0ca8084af623c3029143a07b9eb15fa72f7a74b66716b0e6c67de3254da280cc", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[plutus_transaction_with_stake_credentials_giv-6a67c1eb": "39b1ac98f3235676d5d534d28af7b23ec02dc6f7ac12337d575217e42bec47f8", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[plutus_transaction_with_stake_credentials_giv-72ef969e": "3a47ba2ba80fce3d33c2a28537069504fda2d107be141f07d41bc6d1065f3937", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[plutus_transaction_with_stake_deregistration]": "d137e2091d4d16bb74351a097d7bf653a3a6d4b619a5ffd77e326054ec42fbc6", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[plutus_transaction_with_stake_deregistration_-78f5c748": "280c2fc2c71454a96359cf030a4c86a160aa174d2d6084885f714320fa4084e4", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[plutus_transaction_with_stake_registration_an-4da9385a": "e3550624f67dde0e58280816199c1fe566a1400a89c93d9319e949cf5d3083d6", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[plutus_transaction_with_stake_registration_ce-46b0a250": "a2efa9752f7efd168165b8e577d1037a9f58896da4cf52ea5b87b88cbc9d352f", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[plutus_transaction_with_token_minting]": "090a03e9c834407795d9d5d3d63882112e384407bd1d698f375eb44a100d39db", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[plutus_transaction_with_total_collateral]": "c5bacf71c99d65ab73cc3e3fe32e6db1256bbd1212dbe04bffc0e0bbfc9fc65c", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[plutus_transaction_with_total_collateral_and_-3f0b305a": "c3929d621ae800a5fdc2c93ff3e5d6100be3588d9a4b5785fe02cb0114ebda0f", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[plutus_transaction_with_total_collateral_and_-c92d773b": "12b33cb0d2b9799fed2185cf738dd9af825d65cef54b8a2e2d93b19013de8d60", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[plutus_transaction_without_script_data_hash_a-9590827f": "faec052eb9d03b0acadbb44dd25402a4d464c32c7145df0e79a494f66be9a90c", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[sample_stake_pool_registration_certificate]": "eabd5b4bbc318d666cd690dc095136328ce207349a71b1f6e273aa6b5c658b7c", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[sample_stake_pool_registration_certificate_wi-336f4a44": "eabd5b4bbc318d666cd690dc095136328ce207349a71b1f6e273aa6b5c658b7c", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[sample_stake_pool_registration_certificate_wi-d3427614": "91cb8952154bd6367ac057b2e5d8c0a3c3534b0790a0e90d5a25a08828a63121", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[sample_stake_pool_registration_with_zero_margin]": "4d4667658f5a093f11937c1422198298cc73255fa52a59a64871e263892bc899", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[simple_plutus_transaction]": "df7e8b64dd214d506690090dabf2b5e3f12759f8076ae4a99d71ca32507f56c6", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[simple_plutus_transaction_with_additional_wit-36ba8ce8": "ba8437b39fa9b3552c15019a3ef2ac2c6bc50e13eb08c045a860538843cf5d0a", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[simple_plutus_transaction_with_required_signers]": "3e2985891282173c0043e544dadf2935eff5477a06d5aa34926bbaa13947c70c", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[simple_transaction_with_base_address_change_o-0c37e6dc": "695e91da2c50dfc25a1bff9c4a01db02e1409ef06a6ccb6220ffdd152fb9dab3", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[simple_transaction_with_base_address_change_o-7f1d12f6": "ce7eaa445d97283fa0a43a87a8b4f8848da9b4075e1cfd9b9ccc682bf3bf698d", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[simple_transaction_with_base_address_change_output]": "b218e5fa46b31875f15ab948128c2ba2811729a7801f43557541112a2a71e560", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[simple_transaction_with_base_script_address_a-56fc16f7": "afcc1e47bc323cccf5e8cd6feca4e20ac5c677ef3df5f1ad7814aa619245e635", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[simple_transaction_with_enterprise_address_ch-15518a4c": "479d07aaee306171d313faf390b378ce39af5d906e3fe8805464c9be9c4626a3", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[simple_transaction_with_pointer_address_change_output]": "bc7133ef5176c228c860fba4703e69652c7eaf5c929736a031cbdcb2e9fd0a08", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[stake_pool_registration_certificate_with_no_p-0bbad967": "96b1d7aea6a461ef66044e084c32856729c7ac47219ceac909a0590ac2b63e63", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[stake_pool_registration_on_testnet]": "a1eb915e5d88915ec9d2f2fbddd61b72d5487ce590af0b6cf3b285c72c8c1d73", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[testnet_transaction0]": "7c17d42d118b34f1395ac41c6fe30c7926305baf1be3cfe5aa51c0b0ecdf2980", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[testnet_transaction1]": "988cf0cb9a10f6d2ba565d66afe02063318ae120f4a1abecd3470cc1ce0abacd", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_auxiliary_data_hash]": "b218e5fa46b31875f15ab948128c2ba2811729a7801f43557541112a2a71e560", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_base_address_change_output_p-3c7243e1": "fdbbacc7af5f3c0c0fc7b39a1fd53ffe6e2bad5c76cd4a899264abd85bf3b55e", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_base_address_change_output_s-20438873": "1805e1553f0d39e46e4c9f94e1aa9099e5b9cc722abeeccab4ecddaaa13ab5c4", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_cip15_registration]": "779ed5119da556481828277a934aa9bcb9b930ea2681af1efc2cd7ac640c9fe5", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_cip36_registration_and_exter-a66e1a50": "570630f7b6d7b163cd31cca7eda08ad1f776e43c7ed58c04a97556266510bc06", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_cip36_registration_and_exter-b18e613a": "ef992cf2048d083c4e1045c491af7453a792cee175f76ce41c1a75e692bb1877", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_cip36_registration_and_non-p-26df89e6": "b99898edcc13abd63aaff8c4fac7466b78259b0a6ce2f809fda07580cd503d74", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_cip36_registration_and_other-6a6c5c8d": "91a6971fdfb2f8365efc4e433eaf9add064f88771b06adefdcb52a55248d0c16", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_cip36_registration_and_votin-1b01d6f0": "d1b389cf4d9488c4d0ec21a0456d6fc55dcbcf5b69ab7ee91571a4a0eecc1e4c", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_everything_set_except_pool_r-1e1ef130": "d00239398e36c83dc9ca261ae8b54de09e3c1311cce951ab6a28f9ade25e198f", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_stake_deregistration]": "bf24bed0b0715e10bc5437ecdf5e53028d903594dde8fbcab55f978ab4bdd818", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_stake_deregistration_and_withdrawal]": "bf24bed0b0715e10bc5437ecdf5e53028d903594dde8fbcab55f978ab4bdd818", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_stake_deregistration_with_ac-9ca046f0": "fe03f08dd1aa74e033ec947468663b26dd9a82b7400613b2fe2ac7a5ba05a710", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_stake_registration_and_stake-3fdfc583": "105e3810c0542c7e29a99e73d4b7f46a5948b619808560b4df911c5a1656879a", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_stake_registration_certifica-e7bd462a": "00ef32b05ea16584ae884ffa0900ef4deb5c2cba6e53b67341ced098b373ccb0", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_stake_registration_certificate]": "d29df53b23d3dd47b406517e5c14b4368e7da6e2b57b31953cd2639cb6d206f7", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_ttl_equal_to_0]": "12163b570b2e7295b8d15a9578b1f51e69ba14739b14f0167e3b390165ec2f4a", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx[transaction_with_validity_interval_start_equal_to_0]": "ff7125eb214a5e6694e4a0d98315d7d7ef557ab3429522178826e2d1e53569f5", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[1854_change_output_path_in_ordinary_tr-805f9bd0": "a6453ead0c25a7dca0da99733da94697150c00d3e98490e20dbe626d50d5db43", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[1854_input_path_in_ordinary_transaction]": "b3274b6dc28f012db8ad5d522541bfa2d0ea82190def9260765d91be1ec62b30", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[additional_witness_requests_in_ordinar-9c4f94c0": "b3274b6dc28f012db8ad5d522541bfa2d0ea82190def9260765d91be1ec62b30", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[all_tx_inputs_must_be_external_(without_path)]": "272d58a503b5dcafc3d83016bec2a3e355b45011c4495a66922e12f14e29416b", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[asset_names_in_mint_token_group_in_wrong_order]": "861a794a765750eb718778a3531fcd4d07231fb55c5ca3a291a7443d24476ac5", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[asset_names_in_multiasset_token_group_-7c1351bc": "ef6bcd503b729e9ece54d97886f898f4023f336c36fb4f75134ff370d5f3145f", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[auxiliary_data_hash_has_incorrect_length]": "8364b275eae318f260ccf548a2ea2baa08b0477e2489b02b6a44540c5370f137", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[both_datum_hash_and_inline_datum_present]": "a6453ead0c25a7dca0da99733da94697150c00d3e98490e20dbe626d50d5db43", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[byron_to_shelley_transfer_input_accoun-863fee7d": "fd3948ecbe300eb82a65d07654f8e2320e565d3ca622ecc4df59a320acc09ab3", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[byron_to_shelley_transfer_output_accou-5a99fb35": "a3293dba368b119493f16d4aaf66bc55442c2c6d97906cee2ba9aac7bd2447aa", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[certificate_has_both_path_and_key_hash]": "8364b275eae318f260ccf548a2ea2baa08b0477e2489b02b6a44540c5370f137", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[certificate_has_both_path_and_script_hash]": "8364b275eae318f260ccf548a2ea2baa08b0477e2489b02b6a44540c5370f137", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[certificate_has_invalid_pool_size]": "8364b275eae318f260ccf548a2ea2baa08b0477e2489b02b6a44540c5370f137", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[certificate_has_key_hash]": "8364b275eae318f260ccf548a2ea2baa08b0477e2489b02b6a44540c5370f137", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[certificate_has_multisig_path]": "8364b275eae318f260ccf548a2ea2baa08b0477e2489b02b6a44540c5370f137", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[certificate_has_non_staking_path]": "8364b275eae318f260ccf548a2ea2baa08b0477e2489b02b6a44540c5370f137", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[certificate_has_script_hash]": "8364b275eae318f260ccf548a2ea2baa08b0477e2489b02b6a44540c5370f137", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[change_output_and_stake_deregistration-e17db500": "560cecf89593881f0c62190ed04a4463212ae901ab187f17aaf92adc6078eab9", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[change_output_and_withdrawal_account_mismatch]": "560cecf89593881f0c62190ed04a4463212ae901ab187f17aaf92adc6078eab9", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[change_output_path_larger_than_100]": "560cecf89593881f0c62190ed04a4463212ae901ab187f17aaf92adc6078eab9", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[change_output_staking_path_larger_than_100]": "560cecf89593881f0c62190ed04a4463212ae901ab187f17aaf92adc6078eab9", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[change_output_with_script_in_payment_part]": "560cecf89593881f0c62190ed04a4463212ae901ab187f17aaf92adc6078eab9", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[collateral_input_is_present]": "89abae5675b51ab98ef65245c05ba47bf6232a5388eaed8b5dd6b3482236bc92", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[collateral_input_prev_hash_has_incorre-99d2dc0e": "88d0142e319db6713c2b632f4c956a3df2dcbeb715d103b277cb8c02bfead9b7", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[collateral_return_is_present]": "89abae5675b51ab98ef65245c05ba47bf6232a5388eaed8b5dd6b3482236bc92", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[collateral_return_with_datum_hash]": "3a456bff5fbc5dae2bcadc0710c6e16e5fd1e903272e6e3edeecc926e4649716", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[collateral_return_with_script_address]": "3a456bff5fbc5dae2bcadc0710c6e16e5fd1e903272e6e3edeecc926e4649716", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[contains_a_different_certificate]": "d0f9be928e2ff1d810703104ded073e3f3dca02d00532179ca5652875199f0b9", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[contains_multiple_pool_registration_ce-3000d4f0": "89abae5675b51ab98ef65245c05ba47bf6232a5388eaed8b5dd6b3482236bc92", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[contains_withdrawal]": "89abae5675b51ab98ef65245c05ba47bf6232a5388eaed8b5dd6b3482236bc92", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[fee_is_too_high]": "89abae5675b51ab98ef65245c05ba47bf6232a5388eaed8b5dd6b3482236bc92", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[inline_datum_present_in_output_with_le-43c025ef": "a6453ead0c25a7dca0da99733da94697150c00d3e98490e20dbe626d50d5db43", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[input_and_change_output_account_mismatch]": "26792cf725d5aefee89846d81be6cced42c35f4d3222e992cd5a15a700faf093", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[input_and_stake_deregistration_certifi-b3383de2": "8f8291ce8f212b4e15c5ad369eaf71fc83493e864aad6dba23eb395a24cb0ed6", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[input_and_withdrawal_account_mismatch]": "b218e5fa46b31875f15ab948128c2ba2811729a7801f43557541112a2a71e560", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[input_prev_hash_has_incorrect_length]": "a6453ead0c25a7dca0da99733da94697150c00d3e98490e20dbe626d50d5db43", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[invalid_pool_id]": "d0f9be928e2ff1d810703104ded073e3f3dca02d00532179ca5652875199f0b9", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[mainnet_protocol_magic_with_testnet_network_id]": "89abae5675b51ab98ef65245c05ba47bf6232a5388eaed8b5dd6b3482236bc92", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[mainnet_transaction_with_testnet_output]": "a6453ead0c25a7dca0da99733da94697150c00d3e98490e20dbe626d50d5db43", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[margin_higher_than_1]": "d0f9be928e2ff1d810703104ded073e3f3dca02d00532179ca5652875199f0b9", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[missing_owner_with_path]": "38052fff21166fe8b7ff9245a4bb0b6f55899d1a55a1cfe4d96447b2cb69a182", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_1852_multisi-b7679330": "74fac0659a7ab6e0b7bf7cb2bbaf93d4534b04db969472699d0c01c0d65157da", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_a_collateral_input]": "89abae5675b51ab98ef65245c05ba47bf6232a5388eaed8b5dd6b3482236bc92", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_collateral_return]": "89abae5675b51ab98ef65245c05ba47bf6232a5388eaed8b5dd6b3482236bc92", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_long_token_m-9fb3cfe5": "89ec530303602d24ea31646eb406dee42a1d19dbde588fd0f1498ffccad6b0f3", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_output_conta-e3b36436": "af3670a8eaa77dc2b71d323af3cb105b849dd462f9ae907614447dcb00078158", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_reference_input]": "89abae5675b51ab98ef65245c05ba47bf6232a5388eaed8b5dd6b3482236bc92", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_repeated_withdrawal]": "6ffbd4dd781f3d9a00f40e736df69a707428407c44f454c5566fd6bcc9529f4e", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_stake_delega-19d1722c": "bf577b1f949a17a1e24b2444f984c77aec7f818bf72ee540876eb8ce478cebe0", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_stake_delega-394991f1": "bf577b1f949a17a1e24b2444f984c77aec7f818bf72ee540876eb8ce478cebe0", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_stake_deregi-351ce869": "bf577b1f949a17a1e24b2444f984c77aec7f818bf72ee540876eb8ce478cebe0", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_stake_deregi-43da91d4": "bf577b1f949a17a1e24b2444f984c77aec7f818bf72ee540876eb8ce478cebe0", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_stake_regist-456f1292": "bf577b1f949a17a1e24b2444f984c77aec7f818bf72ee540876eb8ce478cebe0", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_stake_regist-84b1254e": "bf577b1f949a17a1e24b2444f984c77aec7f818bf72ee540876eb8ce478cebe0", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_total_collateral]": "89abae5675b51ab98ef65245c05ba47bf6232a5388eaed8b5dd6b3482236bc92", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_withdrawal_c-9f7e1700": "6ffbd4dd781f3d9a00f40e736df69a707428407c44f454c5566fd6bcc9529f4e", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_withdrawal_c-e98b1f5c": "6ffbd4dd781f3d9a00f40e736df69a707428407c44f454c5566fd6bcc9529f4e", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_with_wthdrawal_ad-3291ee9e": "6ffbd4dd781f3d9a00f40e736df69a707428407c44f454c5566fd6bcc9529f4e", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[multisig_transaction_without_minting_b-da5ba399": "74fac0659a7ab6e0b7bf7cb2bbaf93d4534b04db969472699d0c01c0d65157da", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[ordinary_transaction_with_long_token_m-350c65f4": "d37b7845c6a80ff4f375552966444d31df68cc5f0cceeee7ae9f5b0b7e859225", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[ordinary_transaction_with_token_mintin-bc56f145": "4fe5781e4b6dcff1f257c20448bafdb63bb6e562cba2d62073ff6f51b6a849e6", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[ordinary_transaction_without_token_min-a128d577": "8e7693c80831280dea8eee1897791a4154108c4d5cd963912ad63a033a2197f7", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[output_address_has_invalid_crc]": "a6453ead0c25a7dca0da99733da94697150c00d3e98490e20dbe626d50d5db43", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[output_address_is_a_valid_cbor_but_inv-ea3da215": "a6453ead0c25a7dca0da99733da94697150c00d3e98490e20dbe626d50d5db43", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[output_address_is_invalid_cbor]": "a6453ead0c25a7dca0da99733da94697150c00d3e98490e20dbe626d50d5db43", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[output_address_is_too_long]": "a6453ead0c25a7dca0da99733da94697150c00d3e98490e20dbe626d50d5db43", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[output_address_is_too_short]": "a6453ead0c25a7dca0da99733da94697150c00d3e98490e20dbe626d50d5db43", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[output_datum_hash_has_incorrect_length]": "a6453ead0c25a7dca0da99733da94697150c00d3e98490e20dbe626d50d5db43", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[output_has_both_address_and_address_pa-2efc280f": "a6453ead0c25a7dca0da99733da94697150c00d3e98490e20dbe626d50d5db43", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[output_total_is_too_high]": "3970696e7b06b86986d743f3137ba75e741a627eb76d9531a81d2574c8ece2df", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[output_with_reward_address]": "a6453ead0c25a7dca0da99733da94697150c00d3e98490e20dbe626d50d5db43", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[plutus_transaction_with_output_contain-74465253": "1a0e417ebbe38e86dc91257f614dc0963b2ee3f1521617b1ca8a8273dcb7b57c", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[policyids_in_mint_in_wrong_order]": "a675130eb38f7690201dbaf6450175bfba0630b5ff2e1aec1bbc479a14cc0d61", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[policyids_in_multiasset_output_in_wrong_order]": "e0cd1f4fededc5e663471b14d101fb162f0a3e0f80610e04f4f2aec0a96dd4ef", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[pool_reward_address_belongs_to_differe-e79b6855": "d0f9be928e2ff1d810703104ded073e3f3dca02d00532179ca5652875199f0b9", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[pool_reward_address_is_a_base_address]": "d0f9be928e2ff1d810703104ded073e3f3dca02d00532179ca5652875199f0b9", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[reference_input_is_present]": "89abae5675b51ab98ef65245c05ba47bf6232a5388eaed8b5dd6b3482236bc92", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[repeated_asset_name_in_mint_token_group]": "fa3dd9adbb16d453ae663bede8cbec46cbc67a5db67a144cd0f8f681a4914aaf", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[repeated_asset_name_in_multiasset_token_group]": "5d237e9a6a172b9e8629cec1db3d846db8d41742b63411a1d6047d857c4ac893", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[repeated_policyid_in_mint]": "53905dc233d9cfc37f68a3e5910687dc378adac2c80eda7d73157c0f9c741d52", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[repeated_policyid_in_multiasset_output]": "3d03c920c2f2d907f24d2c87d17fb9534e9cc72a38d772c57f630672e30bcf23", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[required_signer_with_both_key_path_and-7d9a3c59": "3a456bff5fbc5dae2bcadc0710c6e16e5fd1e903272e6e3edeecc926e4649716", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[sample_stake_pool_registration_certifi-02b129f8": "89abae5675b51ab98ef65245c05ba47bf6232a5388eaed8b5dd6b3482236bc92", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[sample_stake_pool_registration_certifi-11c8b442": "eabd5b4bbc318d666cd690dc095136328ce207349a71b1f6e273aa6b5c658b7c", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[sample_stake_pool_registration_certifi-2d1899d5": "89abae5675b51ab98ef65245c05ba47bf6232a5388eaed8b5dd6b3482236bc92", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[sample_stake_pool_registration_certifi-3f8170f6": "eabd5b4bbc318d666cd690dc095136328ce207349a71b1f6e273aa6b5c658b7c", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[sample_stake_pool_registration_certifi-60961d51": "89abae5675b51ab98ef65245c05ba47bf6232a5388eaed8b5dd6b3482236bc92", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[sample_stake_pool_registration_certifi-790fc948": "89abae5675b51ab98ef65245c05ba47bf6232a5388eaed8b5dd6b3482236bc92", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[sample_stake_pool_registration_certifi-883e81d5": "472159c42dfd3953614a83c150d8fc5d9af18257c72fb2fc4b105d8839db36d9", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[sample_stake_pool_registration_certifi-9ae6620c": "89abae5675b51ab98ef65245c05ba47bf6232a5388eaed8b5dd6b3482236bc92", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[sample_stake_pool_registration_certifi-d0eba163": "89abae5675b51ab98ef65245c05ba47bf6232a5388eaed8b5dd6b3482236bc92", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[sample_stake_pool_registration_certifi-e7a533e7": "d0f9be928e2ff1d810703104ded073e3f3dca02d00532179ca5652875199f0b9", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[sample_stake_pool_registration_certifi-e908b1a8": "89abae5675b51ab98ef65245c05ba47bf6232a5388eaed8b5dd6b3482236bc92", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[sample_stake_pool_registration_certifi-f9976ae8": "d0f9be928e2ff1d810703104ded073e3f3dca02d00532179ca5652875199f0b9", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[script_data_hash_has_incorrect_length]": "a07aba7f92bafcf93b81386ba61a065b7dc8d1c8d2e98570f50a328e88c38506", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[shelley_mainnet_transaction_with_testn-af110e3e": "a6453ead0c25a7dca0da99733da94697150c00d3e98490e20dbe626d50d5db43", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[shelley_testnet_transaction_with_mainn-ba78ab8f": "a6453ead0c25a7dca0da99733da94697150c00d3e98490e20dbe626d50d5db43", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[stake_deregistration_account_larger_than_100]": "560cecf89593881f0c62190ed04a4463212ae901ab187f17aaf92adc6078eab9", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[stake_deregistration_certificate_and_w-003a1023": "04dd0fdadafecea3d5d9ae6bef947207ebb4bc30cefc0d38f7b9f1f0b00c4004", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[testnet_protocol_magic_with_mainnet_network_id]": "89abae5675b51ab98ef65245c05ba47bf6232a5388eaed8b5dd6b3482236bc92", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[testnet_transaction_with_mainnet_output]": "a6453ead0c25a7dca0da99733da94697150c00d3e98490e20dbe626d50d5db43", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[total_collateral_is_present]": "89abae5675b51ab98ef65245c05ba47bf6232a5388eaed8b5dd6b3482236bc92", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[transaction_with_both_auxiliary_data_b-6f1ead27": "560cecf89593881f0c62190ed04a4463212ae901ab187f17aaf92adc6078eab9", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[transaction_with_both_vote_public_key_-3e8cccb4": "560cecf89593881f0c62190ed04a4463212ae901ab187f17aaf92adc6078eab9", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[transaction_with_cvote_registration_co-2dcb1cea": "560cecf89593881f0c62190ed04a4463212ae901ab187f17aaf92adc6078eab9", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[two_owners_with_path]": "818a3e1b9d56b370e28e2479a452482f5a2e1092146e9f2e61c20fd276962cbb", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[unsupported_address_type]": "a6453ead0c25a7dca0da99733da94697150c00d3e98490e20dbe626d50d5db43", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[with_multisig_transaction_signing_mode]": "bf577b1f949a17a1e24b2444f984c77aec7f818bf72ee540876eb8ce478cebe0", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[with_ordinary_transaction_signing_mode]": "560cecf89593881f0c62190ed04a4463212ae901ab187f17aaf92adc6078eab9", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[with_plutus_transaction_signing_mode]": "4b10353ea18b58bc621341628bee70e26e7f826ddf1e91ab18c9f487ca561346", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[withdrawal_amount_is_too_large]": "8364b275eae318f260ccf548a2ea2baa08b0477e2489b02b6a44540c5370f137", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[withdrawal_contains_both_path_and_key_hash]": "8364b275eae318f260ccf548a2ea2baa08b0477e2489b02b6a44540c5370f137", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[withdrawal_contains_both_path_and_script_hash]": "8364b275eae318f260ccf548a2ea2baa08b0477e2489b02b6a44540c5370f137", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[withdrawal_has_key_hash]": "8364b275eae318f260ccf548a2ea2baa08b0477e2489b02b6a44540c5370f137", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[withdrawal_has_multisig_path]": "8364b275eae318f260ccf548a2ea2baa08b0477e2489b02b6a44540c5370f137", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[withdrawal_has_non_staking_path]": "8364b275eae318f260ccf548a2ea2baa08b0477e2489b02b6a44540c5370f137", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_failed[withdrawal_has_script_hash]": "8364b275eae318f260ccf548a2ea2baa08b0477e2489b02b6a44540c5370f137", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_show_details[mainnet_transaction_without_change]": "b3274b6dc28f012db8ad5d522541bfa2d0ea82190def9260765d91be1ec62b30", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_show_details[multisig_transaction_with_a_requ-c2fba589": "8a1e9ed65e1dbdd7cd7818ac248d5c947e807e7381000007f81e05e29503fe96", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_show_details[ordinary_transaction_with_a_requ-9728607e": "b218e5fa46b31875f15ab948128c2ba2811729a7801f43557541112a2a71e560", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_show_details[ordinary_transaction_with_long_i-708443f3": "478afa7cea77cf323be0732ca684c96edbdf20d2c455e3a20c736ea4c5d6f08e", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_show_details[ordinary_transaction_with_output-9ba7352d": "2cffdc4708cc8c84e3922679b11f364c256c31083dd02370e490dbc4fc1081b6", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_show_details[plutus_transaction_with_reference_input]": "0ca8084af623c3029143a07b9eb15fa72f7a74b66716b0e6c67de3254da280cc", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_show_details[plutus_transaction_with_total_co-e846c221": "c3929d621ae800a5fdc2c93ff3e5d6100be3588d9a4b5785fe02cb0114ebda0f", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_show_details[transaction_with_cip36_registrat-b9111c27": "d1b389cf4d9488c4d0ec21a0456d6fc55dcbcf5b69ab7ee91571a4a0eecc1e4c", +"TR_cardano-test_sign_tx.py::test_cardano_sign_tx_show_details[transaction_with_stake_deregistr-6e84da2f": "bf24bed0b0715e10bc5437ecdf5e53028d903594dde8fbcab55f978ab4bdd818", +"TR_eos-test_get_public_key.py::test_eos_get_public_key": "b9195f81538a80961931d4e7f7ed0fb11356dc48799f6402d86713d54e34b933", +"TR_eos-test_signtx.py::test_eos_signtx_buyram": "dc0b71f70211624d19c38e03a03f354543af08576595d24d701db4dcda04e9c1", +"TR_eos-test_signtx.py::test_eos_signtx_buyrambytes": "37048969adcb9212f8137f88ac85a993a2e87967618ec82a9fe3e64a55941175", +"TR_eos-test_signtx.py::test_eos_signtx_delegate": "d821f80e7321c5908fb20a21f8e439712537a5d89a05ea4140fb8d60c7fd8ada", +"TR_eos-test_signtx.py::test_eos_signtx_deleteauth": "f30b585908724bc77b9041a265f558b528e27d1f835d8a780890610fdd4570bf", +"TR_eos-test_signtx.py::test_eos_signtx_linkauth": "22a2c032fbe56b0f9ac87bc84faa1064a824845b8af98776b856f1a5b2cde726", +"TR_eos-test_signtx.py::test_eos_signtx_newaccount": "a384c310822533885a4ad45c08b3669a74e4fc2c5b93ef0928f9e963d5565bc5", +"TR_eos-test_signtx.py::test_eos_signtx_refund": "87ec21d9a37c60d55661fdef746c29e14041c66b34aaa03ea1437d02e903f1f0", +"TR_eos-test_signtx.py::test_eos_signtx_sellram": "158379c91db679333660c18b5c3dafff69e958dfc8ff99666251ab30fad121de", +"TR_eos-test_signtx.py::test_eos_signtx_setcontract": "df5a4b6b4739e87a9fc38b0c86c1d6ecd81da69b3b3eb15f1a5cb906a9785947", +"TR_eos-test_signtx.py::test_eos_signtx_transfer_token": "66497db0bb377b79c11e78ca11b67d0f5af667ba15b86c4de862363ba221b3bd", +"TR_eos-test_signtx.py::test_eos_signtx_undelegate": "ca56b203afe3fcd0f64c77f2ea056e277554f9075ca6cb6ed25b805ef81c45d4", +"TR_eos-test_signtx.py::test_eos_signtx_unknown": "25fd07a5ba5a83337ddc8ce49302fd1cfb377a1152bc994395d517b04bae2363", +"TR_eos-test_signtx.py::test_eos_signtx_unlinkauth": "9d70349ae31e93ba7c0f5e30c4a2419868e97a1dc9d0b9a524da551e99c1606f", +"TR_eos-test_signtx.py::test_eos_signtx_updateauth": "11b8986e75775671aadeec1f7b8eaa40a10a70de425c3a2c47ee20bc3ef63b39", +"TR_eos-test_signtx.py::test_eos_signtx_vote": "3ad55005011e28da4d5ca9e7222c9500f1dd02631bfb7bc2baf34e4bcfc536a8", +"TR_eos-test_signtx.py::test_eos_signtx_vote_proxy": "a2962e71947ce4ba1b6d81d6390f585207d3dead669aaeab1894d7a4018f8af2", +"TR_ethereum-test_definitions.py::test_builtin": "7029667fdf835dba6ee471fc753db0b413cc579883f234798c0059d6b009c68c", +"TR_ethereum-test_definitions.py::test_builtin_token": "a521f5f994a888e0048da322f9763a7423ed6680c7473f54fe9f54772e9cb53a", +"TR_ethereum-test_definitions.py::test_chain_id_allowed": "c4d7ad68466775076a2c833c9162ca9705e18537ab32f09927479acfbd7a9ade", +"TR_ethereum-test_definitions.py::test_chain_id_mismatch": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions.py::test_definition_does_not_override_builtin": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions.py::test_external_chain_token_mismatch": "9b16d07e8c16b86965f167de68ee6698a6f857d3694267d41e1c4b57aadc9dc0", +"TR_ethereum-test_definitions.py::test_external_chain_token_ok": "f7f404eb2eb280c89b6e3f96be634ff84ffbd1a2b5e09f74a5f04fff672742bc", +"TR_ethereum-test_definitions.py::test_external_chain_without_token": "9b16d07e8c16b86965f167de68ee6698a6f857d3694267d41e1c4b57aadc9dc0", +"TR_ethereum-test_definitions.py::test_external_token": "e6f0e6bb9263687bdeaf9e15b7c55c67dd12fab7e83e111af2361650c49492f0", +"TR_ethereum-test_definitions.py::test_method_builtin[_call_getaddress]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions.py::test_method_builtin[_call_sign_typed_data]": "e7578096045dd4f44c5d467fa665292d85fbaccc06ac619abea262ee934aea81", +"TR_ethereum-test_definitions.py::test_method_builtin[_call_signmessage]": "8ca064cdf340ad88607f3141d151ba91616f65056a47ff5289d585de9fd61b80", +"TR_ethereum-test_definitions.py::test_method_def_missing[_call_getaddress]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions.py::test_method_def_missing[_call_sign_typed_data]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions.py::test_method_def_missing[_call_signmessage]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions.py::test_method_external[_call_getaddress]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions.py::test_method_external[_call_sign_typed_data]": "e7578096045dd4f44c5d467fa665292d85fbaccc06ac619abea262ee934aea81", +"TR_ethereum-test_definitions.py::test_method_external[_call_signmessage]": "eb937b7852a1237fe2f4676a2772c132398cf95228f82c7e21a147387814ca6e", +"TR_ethereum-test_definitions.py::test_method_external_mismatch[_call_getaddress]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions.py::test_method_external_mismatch[_call_sign_typed_data]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions.py::test_method_external_mismatch[_call_signmessage]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions.py::test_slip44_disallowed": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions.py::test_slip44_external": "1f098c546bb0fea1a9d6fc9f38f38da7128d2886e3565cd5b4f3f94d73be50ff", +"TR_ethereum-test_definitions.py::test_slip44_external_disallowed": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions_bad.py::test_bad_prefix": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions_bad.py::test_bad_proof": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions_bad.py::test_bad_type": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions_bad.py::test_malformed_protobuf": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions_bad.py::test_mangled_payload": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions_bad.py::test_mangled_signature": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions_bad.py::test_missing_signature": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions_bad.py::test_not_enough_signatures": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions_bad.py::test_outdated": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions_bad.py::test_proof_length_mismatch": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions_bad.py::test_protobuf_mismatch": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions_bad.py::test_short_message": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions_bad.py::test_trailing_garbage": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_definitions_bad.py::test_trimmed_proof": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_getaddress.py::test_getaddress[ETC]": "0a13c32ef2583f05123d229434464d70ea125707864069c91b001b90a62e53a0", +"TR_ethereum-test_getaddress.py::test_getaddress[Ledger Live legacy path]": "4d5259a96dba8d249ad690ae779f8fa7b8db63802da27db570bab2726bcc8a88", +"TR_ethereum-test_getaddress.py::test_getaddress[parameters0-result0]": "aeed62a6326e174ee4aa6c10be61f0cfb21b54770f1c294c3d6755858fab2753", +"TR_ethereum-test_getaddress.py::test_getaddress[parameters1-result1]": "67944db390b9d6ba945cb48faeaeddc2c141650a50f6ddc76604bf16866d0001", +"TR_ethereum-test_getaddress.py::test_getaddress[parameters2-result2]": "544ba683329e416950c3ca5fbfb99d09549750f06c09c4e186a0a56ae4c9c63b", +"TR_ethereum-test_getaddress.py::test_getaddress[parameters3-result3]": "ea69c1e4c658c04ce04a0a4f39f80bfbbc6c20db4f03f5788008d804fd76264b", +"TR_ethereum-test_getpublickey.py::test_ethereum_getpublickey[Ledger Live legacy path]": "97df1d41e48484b8372bf540ccc580b9532ef50e95101876777809029207deca", +"TR_ethereum-test_getpublickey.py::test_ethereum_getpublickey[parameters0-result0]": "97df1d41e48484b8372bf540ccc580b9532ef50e95101876777809029207deca", +"TR_ethereum-test_getpublickey.py::test_ethereum_getpublickey[parameters1-result1]": "97df1d41e48484b8372bf540ccc580b9532ef50e95101876777809029207deca", +"TR_ethereum-test_getpublickey.py::test_ethereum_getpublickey[parameters2-result2]": "97df1d41e48484b8372bf540ccc580b9532ef50e95101876777809029207deca", +"TR_ethereum-test_getpublickey.py::test_slip25_disallowed": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_sign_typed_data.py::test_ethereum_sign_typed_data[array_of_structs]": "ecb1329f495352cd225453a5531e026b812e5cbfe0680e84bf8653cd4ac57e85", +"TR_ethereum-test_sign_typed_data.py::test_ethereum_sign_typed_data[bare_minimum]": "a7061dadb4da7cf332d9b1aa1fbb30b8a12d594b35d037bad2d054466ae3480b", +"TR_ethereum-test_sign_typed_data.py::test_ethereum_sign_typed_data[basic_data]": "2847b5957b7f9dd590441bc409b65f842d331d7d4d8518944d4588075f85476b", +"TR_ethereum-test_sign_typed_data.py::test_ethereum_sign_typed_data[complex_data]": "13f90fda9c0e2c07ba3452b14827976108d9486a0e2cb8612feecb817865d60a", +"TR_ethereum-test_sign_typed_data.py::test_ethereum_sign_typed_data[full_domain_empty_message]": "fff6397ca5301a3082fd7e2dc46adaef0cf28639100ec07536afb0447be85d20", +"TR_ethereum-test_sign_typed_data.py::test_ethereum_sign_typed_data[injective_testcase]": "4b21f99762f021fadc1c3b1fb517661bf3cfa3d31ccb64fc59cd99ff426746b7", +"TR_ethereum-test_sign_typed_data.py::test_ethereum_sign_typed_data[struct_list_non_v4]": "2847b5957b7f9dd590441bc409b65f842d331d7d4d8518944d4588075f85476b", +"TR_ethereum-test_sign_typed_data.py::test_ethereum_sign_typed_data[struct_list_v4]": "2847b5957b7f9dd590441bc409b65f842d331d7d4d8518944d4588075f85476b", +"TR_ethereum-test_sign_typed_data.py::test_ethereum_sign_typed_data[structs_arrays_v4]": "2847b5957b7f9dd590441bc409b65f842d331d7d4d8518944d4588075f85476b", +"TR_ethereum-test_sign_typed_data.py::test_ethereum_sign_typed_data_cancel": "c8fe7f95850a4f8843150c2d711fc623dd47256815a7bfcd008f0bb448e69730", +"TR_ethereum-test_sign_typed_data.py::test_ethereum_sign_typed_data_show_more_button": "4ee36fcfa474acfb5c944f9a24e638d65f357ae005ced8410610c2447d218d72", +"TR_ethereum-test_sign_verify_message.py::test_signmessage[parameters0-result0]": "933765760539a59c7759414fafa3f574803457b78915f66ff179704c3621e739", +"TR_ethereum-test_sign_verify_message.py::test_signmessage[parameters1-result1]": "4ae1060dcf83b91047f18b6ef81373736383d2a65909b1ff3f086daeb5ebba91", +"TR_ethereum-test_sign_verify_message.py::test_signmessage[parameters2-result2]": "f33ef5d021f2e7a30f1be0d54160e651efc538f5f82ad613c4bc29e1847af3c2", +"TR_ethereum-test_sign_verify_message.py::test_signmessage[parameters3-result3]": "f0d5bc95ec2106b6eb24a6aefbab717c4d6a2c2896f6d0afcbafb08fbef02b8b", +"TR_ethereum-test_sign_verify_message.py::test_signmessage[parameters4-result4]": "6c422a60d66f5eb5fc6c8cb774c6d8386cebb7b0ff4bea71178803a4939a4133", +"TR_ethereum-test_sign_verify_message.py::test_signmessage[parameters5-result5]": "064f766a1bd6a8af9d146ad56e2779cd651356f9025d544128458469ee2be1b4", +"TR_ethereum-test_sign_verify_message.py::test_signmessage[parameters6-result6]": "bbb26b24d8a318522ca38b4709bed018a3edac7d80006ebef3ed481f30840dc3", +"TR_ethereum-test_sign_verify_message.py::test_signmessage[parameters7-result7]": "e17a70ea0db081d6b0ce43f41a6deabb799d50952359bb633aaac9be34f5a4f4", +"TR_ethereum-test_sign_verify_message.py::test_signmessage[parameters8-result8]": "7cf772b78712dc30dc9f05c62e3de49080098d4736def834422f766fa67a64cd", +"TR_ethereum-test_sign_verify_message.py::test_verify[parameters0-result0]": "f44afbe9285003a7f0273fbb565b595519c6f1604f48416e82550dcf1ec335b2", +"TR_ethereum-test_sign_verify_message.py::test_verify[parameters1-result1]": "82b7f9504a2f82a632a3293b8e439f741b1c68e0a92677223f49aa311a69f870", +"TR_ethereum-test_sign_verify_message.py::test_verify[parameters2-result2]": "b613866e0e0e9fe48cf3c47e039be952a1c3b75d24781d7d81c13586074621a8", +"TR_ethereum-test_sign_verify_message.py::test_verify[parameters3-result3]": "e976596bd060363a8d4a5b07830d5e5c179a26eb985168473db77a58e499dd8b", +"TR_ethereum-test_sign_verify_message.py::test_verify[parameters4-result4]": "8a1c5ad66b2f4454cb141ec7cbf2c19b19c46272529dbabdd3901faaa5ed8b34", +"TR_ethereum-test_sign_verify_message.py::test_verify[parameters5-result5]": "2ca8c8561977c7198030037ae8aa29068db00f36a320f8720732029ecbee015c", +"TR_ethereum-test_sign_verify_message.py::test_verify[parameters6-result6]": "8882e9d7973d8e76504453656b04e66f0d956674f98814591686aaef14fdf6ff", +"TR_ethereum-test_sign_verify_message.py::test_verify[parameters7-result7]": "eb914f34112d28a985d1d8a607ad0071d23fa685633156909f54526ecf1bcd2d", +"TR_ethereum-test_sign_verify_message.py::test_verify_invalid": "f44afbe9285003a7f0273fbb565b595519c6f1604f48416e82550dcf1ec335b2", +"TR_ethereum-test_signtx.py::test_data_streaming": "5b6518b26da713e56ca1cfd37987d976f7b02c0eb73dc1e9de3a637cedbb9322", +"TR_ethereum-test_signtx.py::test_sanity_checks": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_signtx.py::test_sanity_checks_eip1559": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_signtx.py::test_signtx[Auxilium]": "95433e521e34da77b56d6b529c0e5dec07ecc1cdfac8512c307e62d7fa0db245", +"TR_ethereum-test_signtx.py::test_signtx[ETC]": "5cf9f365550f74ddd2d57a2e7c4e6822e026b68ef8b91647553c8d62a67be8a4", +"TR_ethereum-test_signtx.py::test_signtx[Ethereum]": "2748d1d0820d31360106fcedeb19dd5b3fcf9ec8d65d4d00c2eeece6c348104f", +"TR_ethereum-test_signtx.py::test_signtx[Ledger Live legacy path0]": "75652710610192530278a5d3cf487a5d6b67b80f7e05ee6ccb644342b5e9a722", +"TR_ethereum-test_signtx.py::test_signtx[Ledger Live legacy path1]": "2748d1d0820d31360106fcedeb19dd5b3fcf9ec8d65d4d00c2eeece6c348104f", +"TR_ethereum-test_signtx.py::test_signtx[Palm]": "95433e521e34da77b56d6b529c0e5dec07ecc1cdfac8512c307e62d7fa0db245", +"TR_ethereum-test_signtx.py::test_signtx[Pirl]": "95433e521e34da77b56d6b529c0e5dec07ecc1cdfac8512c307e62d7fa0db245", +"TR_ethereum-test_signtx.py::test_signtx[Rinkeby]": "776502957927f135b528b2bbbe12111b498c25d73e2e9d42fa243448bcc44639", +"TR_ethereum-test_signtx.py::test_signtx[Ropsten]": "776502957927f135b528b2bbbe12111b498c25d73e2e9d42fa243448bcc44639", +"TR_ethereum-test_signtx.py::test_signtx[Unknown_chain_id_eth_path]": "95433e521e34da77b56d6b529c0e5dec07ecc1cdfac8512c307e62d7fa0db245", +"TR_ethereum-test_signtx.py::test_signtx[Unknown_chain_id_testnet_path]": "95433e521e34da77b56d6b529c0e5dec07ecc1cdfac8512c307e62d7fa0db245", +"TR_ethereum-test_signtx.py::test_signtx[data_1]": "6cb64e658d78b38b576ba90c425bc28edc1f4654fc28e0104f95136397b5ad58", +"TR_ethereum-test_signtx.py::test_signtx[data_2_bigdata]": "21077e84562e3f196c123e3d18068856260e5f5f05a8c91cf9a5025f903b6472", +"TR_ethereum-test_signtx.py::test_signtx[erc20_token]": "f3671933a6645825eddf3e2fd77e1e3a6e935e4871077351a8f6cc9c725137ab", +"TR_ethereum-test_signtx.py::test_signtx[max_chain_id]": "95433e521e34da77b56d6b529c0e5dec07ecc1cdfac8512c307e62d7fa0db245", +"TR_ethereum-test_signtx.py::test_signtx[max_chain_plus_one]": "95433e521e34da77b56d6b529c0e5dec07ecc1cdfac8512c307e62d7fa0db245", +"TR_ethereum-test_signtx.py::test_signtx[max_uint64]": "95433e521e34da77b56d6b529c0e5dec07ecc1cdfac8512c307e62d7fa0db245", +"TR_ethereum-test_signtx.py::test_signtx[newcontract]": "3e87936991e06928728b613e47afc170ec023ac5b1cb592a9626e825c83ee790", +"TR_ethereum-test_signtx.py::test_signtx[nodata_1]": "32cc598d3d7cf0e16efae14b0a550a351353ef92575d2f3d0d10c13d41ed9477", +"TR_ethereum-test_signtx.py::test_signtx[nodata_2_bigvalue]": "2ac34a9bafdacfe349806fea91ab8e8b792c885e6248a631c8459183795a75ce", +"TR_ethereum-test_signtx.py::test_signtx[wanchain]": "db1b3674837f293bd6b441e8b587d679ce3f0e072ccda976fd90e96697814614", +"TR_ethereum-test_signtx.py::test_signtx_data_pagination[input_flow_go_back]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ethereum-test_signtx.py::test_signtx_data_pagination[input_flow_scroll_down]": "eccd1fd0b662323aee8f1a5315020e9d76611de9f18050ac655fecc838e14e75", +"TR_ethereum-test_signtx.py::test_signtx_data_pagination[input_flow_skip]": "e00eeeb85e95b6bc5327726dc71c31453819685f87fc626b2190b36d1c25a294", +"TR_ethereum-test_signtx.py::test_signtx_eip1559[Ledger Live legacy path]": "c76243602bbf5e6fefb1bc3e46b5abcf80e2524366f23951daf9ad918077efaa", +"TR_ethereum-test_signtx.py::test_signtx_eip1559[data_1]": "890c42f7a1836d1060b3eefee8be613a8a555c559fd6eb71ea66b4b116c123a9", +"TR_ethereum-test_signtx.py::test_signtx_eip1559[data_2_bigdata]": "2ba29f5800595b529af8988cb53d5ea937e8c6bb4e793e5ae4d4115da78b4a01", +"TR_ethereum-test_signtx.py::test_signtx_eip1559[erc20]": "4c31deb8e8eb0942cd0166f5ed40e3df3b0e143a3bb272f6fbd3a56b1ceca197", +"TR_ethereum-test_signtx.py::test_signtx_eip1559[large_chainid]": "af83dea47fe4eb4080a2afbb46a3a66380e8a46c70faad305794ff946407d752", +"TR_ethereum-test_signtx.py::test_signtx_eip1559[long_fees]": "49185ebecb6c56ab273803d3537d6ea3a0ac9cd3bc763b55c0bde707863c17b1", +"TR_ethereum-test_signtx.py::test_signtx_eip1559[nodata]": "c76243602bbf5e6fefb1bc3e46b5abcf80e2524366f23951daf9ad918077efaa", +"TR_ethereum-test_signtx.py::test_signtx_eip1559_access_list": "b14dc8b98000d0707afab734261ead0bd68dd8d38723a8eb934fa2fc56e5842c", +"TR_ethereum-test_signtx.py::test_signtx_eip1559_access_list_larger": "b14dc8b98000d0707afab734261ead0bd68dd8d38723a8eb934fa2fc56e5842c", +"TR_misc-test_cosi.py::test_cosi_different_key": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_misc-test_cosi.py::test_cosi_nonce": "b09e96cc0dc30dba7d78ddbea134ed441722a55c73a608133ec449221a00eead", +"TR_misc-test_cosi.py::test_cosi_pubkey": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_misc-test_cosi.py::test_cosi_sign1": "b09e96cc0dc30dba7d78ddbea134ed441722a55c73a608133ec449221a00eead", +"TR_misc-test_cosi.py::test_cosi_sign2": "89ebde600157727c9ee7d7c827c8089a9457b9ba4ef86b9898c9c174b5bdba11", +"TR_misc-test_cosi.py::test_cosi_sign3": "824bac8dd52ee53204cede57e06bd721d06bbe53d8089d4fd3697298dfa59ca5", +"TR_misc-test_cosi.py::test_invalid_path[m-10018-0]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_misc-test_cosi.py::test_invalid_path[m-1]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_misc-test_cosi.py::test_invalid_path[m-44h-0h-0h-0-0]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_misc-test_cosi.py::test_invalid_path[m-44h-60h-0h-0-0]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_misc-test_cosi.py::test_invalid_path[m-44h-60h-1h]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_misc-test_cosi.py::test_invalid_path[m-84h-60h-1h-0]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_misc-test_cosi.py::test_slip26_paths[42-0]": "ba774c4e7f994ca93841b8d16cc17d77f2c02f1caeb0aefa496e8fd7cc6d97f6", +"TR_misc-test_cosi.py::test_slip26_paths[T1B1-0]": "c30fc937dace6ead424e81ca833a17ad68dbae6d0fc0a07ca328ed8ab09330df", +"TR_misc-test_cosi.py::test_slip26_paths[T2B1-0]": "b9629c54bc7ae2ead58852311e36c4efd10c8c7854a4e8398c69c45a181aef80", +"TR_misc-test_cosi.py::test_slip26_paths[T2B1-1]": "41942fb856bad23ca2247006314e70e4329ae7453d852980d953851c77252c6c", +"TR_misc-test_cosi.py::test_slip26_paths[T2B1-2]": "748dc15e4f0b1ac2acae0228d91b5d3b5b46990b0f953edcf63fa10e97f1425f", +"TR_misc-test_cosi.py::test_slip26_paths[T2B1-3]": "41ce160d7f7969278710c4a3fba7933adbec4b8c3310435d29fa8897ee424264", +"TR_misc-test_cosi.py::test_slip26_paths[T2T1-0]": "74fc96c8a94e085a3db2281059c556e62f7f53bb85fedb24dee28dd57be02404", +"TR_misc-test_cosi.py::test_slip26_paths[T3W1-0]": "2f55b367ebd647ac59aafb046e453fbd4e7df3030010b899f72e858b1eab5597", +"TR_misc-test_cosi.py::test_slip26_paths[\\x00-0]": "a73933f96948b7ba3c85d3e17ad6f22e2d0ab6c2d68edf7f273d07ce044bc471", +"TR_misc-test_cosi.py::test_slip26_paths[\\x00-3]": "cb2b9ae7002c4449a3b4ee847ba810bfc92b4d00bf9a13076ea1d2ba676f19e4", +"TR_misc-test_cosi.py::test_slip26_paths[\\xfe\\xfe\\xfe\\xfe-0]": "636ddb2ba10bfc7872a16ea16758c5be9ec80ad12a370d4c9ef6e6b3782599bb", +"TR_misc-test_cosi.py::test_slip26_paths[dog-0]": "aeecf997192e894bbd84c7cfa5d6ea9af13b2e34650cd14fc084acd05141e6ea", +"TR_misc-test_msg_cipherkeyvalue.py::test_decrypt": "2d48c68bf066835cab92f6f6ca8eeff64425504a9d08e49e1015c38f94703f55", +"TR_misc-test_msg_cipherkeyvalue.py::test_decrypt_badlen": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_misc-test_msg_cipherkeyvalue.py::test_encrypt": "e1fdf02022aa036c122b84f6ba10447e6f18e4d5d60b60f5db496e23ca4847ce", +"TR_misc-test_msg_cipherkeyvalue.py::test_encrypt_badlen": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_misc-test_msg_getecdhsessionkey.py::test_ecdh": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_misc-test_msg_getentropy.py::test_entropy[128]": "6cec5f35e501f3a75cd8300c311d3cd594d8c511cf610a9155a342afbe002feb", +"TR_misc-test_msg_getentropy.py::test_entropy[129]": "6cec5f35e501f3a75cd8300c311d3cd594d8c511cf610a9155a342afbe002feb", +"TR_misc-test_msg_getentropy.py::test_entropy[16]": "6cec5f35e501f3a75cd8300c311d3cd594d8c511cf610a9155a342afbe002feb", +"TR_misc-test_msg_getentropy.py::test_entropy[17]": "6cec5f35e501f3a75cd8300c311d3cd594d8c511cf610a9155a342afbe002feb", +"TR_misc-test_msg_getentropy.py::test_entropy[1]": "6cec5f35e501f3a75cd8300c311d3cd594d8c511cf610a9155a342afbe002feb", +"TR_misc-test_msg_getentropy.py::test_entropy[20]": "6cec5f35e501f3a75cd8300c311d3cd594d8c511cf610a9155a342afbe002feb", +"TR_misc-test_msg_getentropy.py::test_entropy[21]": "6cec5f35e501f3a75cd8300c311d3cd594d8c511cf610a9155a342afbe002feb", +"TR_misc-test_msg_getentropy.py::test_entropy[256]": "6cec5f35e501f3a75cd8300c311d3cd594d8c511cf610a9155a342afbe002feb", +"TR_misc-test_msg_getentropy.py::test_entropy[257]": "6cec5f35e501f3a75cd8300c311d3cd594d8c511cf610a9155a342afbe002feb", +"TR_misc-test_msg_getentropy.py::test_entropy[32]": "6cec5f35e501f3a75cd8300c311d3cd594d8c511cf610a9155a342afbe002feb", +"TR_misc-test_msg_getentropy.py::test_entropy[33]": "6cec5f35e501f3a75cd8300c311d3cd594d8c511cf610a9155a342afbe002feb", +"TR_misc-test_msg_getentropy.py::test_entropy[3]": "6cec5f35e501f3a75cd8300c311d3cd594d8c511cf610a9155a342afbe002feb", +"TR_misc-test_msg_getentropy.py::test_entropy[4]": "6cec5f35e501f3a75cd8300c311d3cd594d8c511cf610a9155a342afbe002feb", +"TR_misc-test_msg_getentropy.py::test_entropy[512]": "6cec5f35e501f3a75cd8300c311d3cd594d8c511cf610a9155a342afbe002feb", +"TR_misc-test_msg_getentropy.py::test_entropy[513]": "6cec5f35e501f3a75cd8300c311d3cd594d8c511cf610a9155a342afbe002feb", +"TR_misc-test_msg_getentropy.py::test_entropy[5]": "6cec5f35e501f3a75cd8300c311d3cd594d8c511cf610a9155a342afbe002feb", +"TR_misc-test_msg_getentropy.py::test_entropy[64]": "6cec5f35e501f3a75cd8300c311d3cd594d8c511cf610a9155a342afbe002feb", +"TR_misc-test_msg_getentropy.py::test_entropy[65]": "6cec5f35e501f3a75cd8300c311d3cd594d8c511cf610a9155a342afbe002feb", +"TR_misc-test_msg_getentropy.py::test_entropy[8]": "6cec5f35e501f3a75cd8300c311d3cd594d8c511cf610a9155a342afbe002feb", +"TR_misc-test_msg_getentropy.py::test_entropy[9]": "6cec5f35e501f3a75cd8300c311d3cd594d8c511cf610a9155a342afbe002feb", +"TR_misc-test_msg_signidentity.py::test_sign": "a2d45ca6465ce7058bcc3722ec3505829e3a4e7e4db56be2b8279fec48240ca0", +"TR_monero-test_getaddress.py::test_monero_getaddress": "b927751b452f3442301dbba4cd2a59ea4c333c538ba7eb3378da8ef09e8f6308", +"TR_monero-test_getwatchkey.py::test_monero_getwatchkey": "0a12a9155dc10d5ad5f677e0039634166d08b0d5dec16af61690d2a6633d6cf1", +"TR_nem-test_getaddress.py::test_nem_getaddress": "af5b21a932d5a9bb32ca5fe8a96ce3cc66eff75b12cd7c601c3eb2a9fdd6ebe9", +"TR_nem-test_signtx_mosaics.py::test_nem_signtx_mosaic_creation": "f16ea53cce63358d1f83d4d078422282978ec0ab58e640c5d060db7c31297319", +"TR_nem-test_signtx_mosaics.py::test_nem_signtx_mosaic_creation_levy": "576cd07f40720e5d3bf78948c8d8309bb7393e53433188fecd82881856d98add", +"TR_nem-test_signtx_mosaics.py::test_nem_signtx_mosaic_creation_properties": "bb0ba278cc1fd21b80b22f9f7a91aa01b2f6cb4b95fa166b83c46f683e046837", +"TR_nem-test_signtx_mosaics.py::test_nem_signtx_mosaic_supply_change": "eaa66ad395178b7ab51338ed3dadc9278fdfb138ed6a5dfbb507836fec6cc4ef", +"TR_nem-test_signtx_multisig.py::test_nem_signtx_aggregate_modification": "0d73d8c65eafd1a4a3d5d5acbece26221f1bf8645dd11a9f4a729bead522ccbb", +"TR_nem-test_signtx_multisig.py::test_nem_signtx_multisig": "ee24b8addc4c4db1e71e2bb0746528c9b866c817c5cd30569cbf76661ae52a0f", +"TR_nem-test_signtx_multisig.py::test_nem_signtx_multisig_signer": "26658a8f68a47e6817524ccc7b3c35058c6b509fdf4032beb297f57135f12c81", +"TR_nem-test_signtx_others.py::test_nem_signtx_importance_transfer": "4246b19d5f36f5c4ad48e5ddf88d70d8f66bd2d96d6e599445a2987beaca1a68", +"TR_nem-test_signtx_others.py::test_nem_signtx_provision_namespace": "9117d015fe24cd6fda0c3d2c262af7a33f12185de3aa409edd8bde44c7825c2b", +"TR_nem-test_signtx_transfers.py::test_nem_signtx_encrypted_payload": "65d18adc7f8afa4e4a9959c6c5f23c717c23e8eceaeae60a656ccf89863fd01f", +"TR_nem-test_signtx_transfers.py::test_nem_signtx_known_mosaic": "ab836313d1961143f550cd46794526127c8988bde28ba5cbe4799f7b3c08a2be", +"TR_nem-test_signtx_transfers.py::test_nem_signtx_known_mosaic_with_levy": "19ab0bacd57d9dd3d1226cf55d2400e4ebdb36cdcd8d41ec4ed64c2a03ce7f48", +"TR_nem-test_signtx_transfers.py::test_nem_signtx_multiple_mosaics": "47c07ec11e0f62bfcd276e342b1ffa527324ba593e9d6958437d9535cdf5f143", +"TR_nem-test_signtx_transfers.py::test_nem_signtx_simple": "45dd9638cc06e697e1588e87e41ee6b87e3abb752ff7afebf687cd5cd55395c4", +"TR_nem-test_signtx_transfers.py::test_nem_signtx_unknown_mosaic": "9cf58bc81c610ffd8fa7a188e84082a9e4659ea063a4e382e78e5560953fdae1", +"TR_nem-test_signtx_transfers.py::test_nem_signtx_xem_as_mosaic": "52914dc2cfbd1718ebe467c0d37e9f7b9be9d8baeab787f2ab0a21438b1b13db", +"TR_reset_recovery-test_recovery_bip39_dryrun.py::test_bad_parameters[label-test]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_reset_recovery-test_recovery_bip39_dryrun.py::test_bad_parameters[language-test]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_reset_recovery-test_recovery_bip39_dryrun.py::test_bad_parameters[passphrase_protection-True]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_reset_recovery-test_recovery_bip39_dryrun.py::test_bad_parameters[pin_protection-True]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_reset_recovery-test_recovery_bip39_dryrun.py::test_bad_parameters[u2f_counter-1]": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_reset_recovery-test_recovery_bip39_dryrun.py::test_dry_run": "590daa1cb3c66ffc542e399f4e1800a92e6b29f66b0559291b78a7b5ef5a91aa", +"TR_reset_recovery-test_recovery_bip39_dryrun.py::test_invalid_seed_core": "9c8036e097d1edcc95ba18a3fafc293e4d9062b68a176fd57aff65937547432b", +"TR_reset_recovery-test_recovery_bip39_dryrun.py::test_seed_mismatch": "79442e2fc2418d74cfdddcb68fc3b18979ec2eefecea94bc0eccb1947424c6e2", +"TR_reset_recovery-test_recovery_bip39_dryrun.py::test_uninitialized": "f49c8d846c2d56a575f0ad49463845ba641b02656783e4fcfc67d74e8fa671dd", +"TR_reset_recovery-test_recovery_bip39_t2.py::test_already_initialized": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_reset_recovery-test_recovery_bip39_t2.py::test_tt_nopin_nopassphrase": "848913147d226b18864f3eb6c11d4e68e8d5389e4031c16fa2a64b651a70aaa8", +"TR_reset_recovery-test_recovery_bip39_t2.py::test_tt_pin_passphrase": "869126f686622c3a876cc557bd72246d5b252d342fef07bfb36860dc64bba6db", +"TR_reset_recovery-test_recovery_slip39_advanced.py::test_abort": "830e0d32e08eeb5dc579e34d4f447ec9bc14f3ba6f44f0f9acad24b4f14f5eab", +"TR_reset_recovery-test_recovery_slip39_advanced.py::test_extra_share_entered": "fefbd96f578c6187a4d7dbe09af716f81b8e0fc296353864d9b29cb8e3c1abe2", +"TR_reset_recovery-test_recovery_slip39_advanced.py::test_group_threshold_reached": "1636048b0252304440a574206db44187ad8e4bf17dc3f77e0e30fcf58be9ce18", +"TR_reset_recovery-test_recovery_slip39_advanced.py::test_noabort": "674e5265f3895e849dc4ccef2753f446658533cde4299b4a30780c7b7d6d7242", +"TR_reset_recovery-test_recovery_slip39_advanced.py::test_same_share": "15a538229ccd1cfd3c6b2d9195ae9e89d8119f9f805764c842f224dc6899bc78", +"TR_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares0-c2d2e26ad06023c60145f1-afc2dad5": "bf0f04d94b0d74bf8e96d2c9bce23f5d84abe4de77170b4ed5f2b1ca234d6c55", +"TR_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares1-c41d5cf80fed71a008a3a0-eb47093e": "77b7443de063ec2da13903c87c1ac908a9d0612ec75206ecf7061a0e4cf2746f", +"TR_reset_recovery-test_recovery_slip39_advanced.py::test_secret_click_info_button[shares0-c2d2-850ffa77": "bf0f04d94b0d74bf8e96d2c9bce23f5d84abe4de77170b4ed5f2b1ca234d6c55", +"TR_reset_recovery-test_recovery_slip39_advanced.py::test_secret_click_info_button[shares1-c41d-ca9ddec8": "77b7443de063ec2da13903c87c1ac908a9d0612ec75206ecf7061a0e4cf2746f", +"TR_reset_recovery-test_recovery_slip39_advanced_dryrun.py::test_2of3_dryrun": "3eaf66efc78bb2fb312eeb84ad8080d26042d0628e77e46ba5b184ba206c8bbb", +"TR_reset_recovery-test_recovery_slip39_advanced_dryrun.py::test_2of3_invalid_seed_dryrun": "25993f8b19e78bc810ad00ff084230da8a6290a2d580bd9abc44345cf6981be7", +"TR_reset_recovery-test_recovery_slip39_basic.py::test_1of1": "ec0c9db450aa19ffbea90f5a18a9b137b6423b574d2c6cee593e980c1d516c6a", +"TR_reset_recovery-test_recovery_slip39_basic.py::test_abort": "830e0d32e08eeb5dc579e34d4f447ec9bc14f3ba6f44f0f9acad24b4f14f5eab", +"TR_reset_recovery-test_recovery_slip39_basic.py::test_ask_word_number": "f49c8d846c2d56a575f0ad49463845ba641b02656783e4fcfc67d74e8fa671dd", +"TR_reset_recovery-test_recovery_slip39_basic.py::test_noabort": "ca75618222c9ad1fb6ab0d193bbe22d8431f039d87ddc40d282fe7eec6e2090a", +"TR_reset_recovery-test_recovery_slip39_basic.py::test_recover_with_pin_passphrase": "159b1732ba861c5193729eb44dbbe9bfc5bc020148c3aca7be9706b83137a2a1", +"TR_reset_recovery-test_recovery_slip39_basic.py::test_same_share": "3853c4973fddbd658a7c9898f477f6c4d66dfab951406c7fba992f810d8d649e", +"TR_reset_recovery-test_recovery_slip39_basic.py::test_secret[shares0-491b795b80fc21ccdf466c0fbc98c8fc]": "f229bbbcd4f31b5624ff6cd8d3d5c3bbe2ba601fa54257f5a632c74b7d6ba98f", +"TR_reset_recovery-test_recovery_slip39_basic.py::test_secret[shares1-b770e0da1363247652de97a39-a50896b7": "e44240fb76c271e6a2c3f6b8faa62814cfabaf3cb6e89d398e477ea293891f44", +"TR_reset_recovery-test_recovery_slip39_basic.py::test_wrong_nth_word[0]": "de0ea2fddcf1fa028f980cdfff6bd0978a4b945197da968f3deac3360e6e7f4d", +"TR_reset_recovery-test_recovery_slip39_basic.py::test_wrong_nth_word[1]": "de0ea2fddcf1fa028f980cdfff6bd0978a4b945197da968f3deac3360e6e7f4d", +"TR_reset_recovery-test_recovery_slip39_basic.py::test_wrong_nth_word[2]": "de0ea2fddcf1fa028f980cdfff6bd0978a4b945197da968f3deac3360e6e7f4d", +"TR_reset_recovery-test_recovery_slip39_basic_dryrun.py::test_2of3_dryrun": "01705237581339df45968a6a7999839db24761d0850ec3ebb97e1d8746a57750", +"TR_reset_recovery-test_recovery_slip39_basic_dryrun.py::test_2of3_invalid_seed_dryrun": "05012ae88bb35ab7d97300d545a1823dcc71fc8cc8f1afbafd6c48153aead500", +"TR_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Bip39-backup_flow_bip39]": "ed672fdb355c20a391a75ee54e935caaaac4b5dda3d4a7fb53bd53085ce46888", +"TR_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Slip39_Advanced-bac-f67baa1c": "e39063181e305044f91cd868d2faf0816496932418440d251b702b7f0e557449", +"TR_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Slip39_Basic-backup-6348e7fe": "7ae9c45c88b68d4e0e966334e3b1068df7c1cd26e5d0a4cf7c0c63eeafcedaa5", +"TR_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Bip39-backup_flow_bip39]": "5c4c560efc9770920d25c697607ade4c8fa941aac2389ea948ff73063029bed0", +"TR_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced-backup-dcbda5cf": "64fe6e8eb15b74dcce69f86657477a8a814682b31f3782ad1b2f70d4282d8cd9", +"TR_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic-backup_fl-1577de4d": "99a731244947b84dc7f762eddd082634b8d7ce2e131fba170f74adb2a39fcee8", +"TR_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "8d7fc7fa6ece46b3dc97f01975fb03ff5bc3155a0f2df90e9c80b71066fe905a", +"TR_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "63e6f1be075f26e98fcc2536e36b3b313f9cbd505b7980ba41ec611ec98dc4cd", +"TR_reset_recovery-test_reset_bip39_t2.py::test_reset_device_192": "7b93d1f497865195236c16410c4d466058933c73311d0134d1699f88ec4dde8c", +"TR_reset_recovery-test_reset_bip39_t2.py::test_reset_device_pin": "0c07985b04b0a3e82f7701aa06e784c3cb7af9e17e42c7d750a54f5587b4ea7b", +"TR_reset_recovery-test_reset_bip39_t2.py::test_reset_failed_check": "0020535eeb01036bfbf1458ea2d0557d782ddc308e85cf78b6ff3bb21585dcb9", +"TR_reset_recovery-test_reset_recovery_bip39.py::test_reset_recovery": "4d7f35c6c32fd4eca0c4e1ef8295e1ccded93d0f80965d5310a8b9aa3e23a6b3", +"TR_reset_recovery-test_reset_recovery_slip39_advanced.py::test_reset_recovery": "9e525255414b42e2e4c10cc8985d72f1a08ad549c9e582b286d42625eeb3184a", +"TR_reset_recovery-test_reset_recovery_slip39_basic.py::test_reset_recovery": "a033f780b5dfbfcdc089b4156232a7284d566c6cb2fa7a6c52f6f2c42fb67455", +"TR_reset_recovery-test_reset_slip39_advanced.py::test_reset_device_slip39_advanced": "09092fda3283c5039dd61a58559babf7533710106e93e6198af18f6454025121", +"TR_reset_recovery-test_reset_slip39_basic.py::test_reset_device_slip39_basic": "4df0fbb7abcf66b86f2dd2f2fafb1921c971d5aba9c62e735cb381e5588193a8", +"TR_reset_recovery-test_reset_slip39_basic.py::test_reset_device_slip39_basic_256": "804db6bed0d53ece939c6fd2d9216782700c1d1418f89b9d175771adb4e64ec8", +"TR_ripple-test_get_address.py::test_ripple_get_address": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ripple-test_get_address.py::test_ripple_get_address_other": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ripple-test_sign_tx.py::test_ripple_sign_invalid_fee": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_ripple-test_sign_tx.py::test_ripple_sign_simple_tx": "90ec0dbfb2390edd1a35b39f9316c21145cdee462c6c5beb4e62905db7be3519", +"TR_stellar-test_stellar.py::test_get_address[parameters0-result0]": "f3c37c0cd8b898ffbac2e7b797f1394999a98b8d9b1ee335f18b46e8cf25956c", +"TR_stellar-test_stellar.py::test_get_address[parameters1-result1]": "f36dd230c3f0536767e7107716be7d81d0966ea312933fa077a43a6ca5c80ba4", +"TR_stellar-test_stellar.py::test_get_address[parameters2-result2]": "d815e728ba9237f85a46b93be8f151bc8098ee7bedd3027b36f5018ecf2a6331", +"TR_stellar-test_stellar.py::test_get_address[parameters3-result3]": "3b8de60f3bd77106ee7e27b83f145478f7bcfded68f0382c71874cb362612112", +"TR_stellar-test_stellar.py::test_get_address[parameters4-result4]": "d4dc713dfc6bf1e695a5022bb21de89515fcd504c5639ed846267e99d81b2d75", +"TR_stellar-test_stellar.py::test_get_address[parameters5-result5]": "9a65d3fdd5c46a3696b1f2dab0642cf4521d13e01850f8d9571dd525692c2eef", +"TR_stellar-test_stellar.py::test_get_address[parameters6-result6]": "0dcf83bdc71df68a01ba85233d2aad67dcd38e6f67ef34f7894a95a976566d4a", +"TR_stellar-test_stellar.py::test_get_address[parameters7-result7]": "ee3446f682ca5e87714350bf91bea2dc3f072f26a97a558e533b3a30f2fe4f9c", +"TR_stellar-test_stellar.py::test_get_address[parameters8-result8]": "113666e3be3abaaed1336ce25298c1f0ba7e1253661de41d8a6ad9f5472a117f", +"TR_stellar-test_stellar.py::test_get_address[parameters9-result9]": "5cc38fe64684543a797d6fe7639d36349f21b543ba89a04e2561a12e75d0be8f", +"TR_stellar-test_stellar.py::test_sign_tx[StellarAccountMergeOp]": "8a2c230686b60e53967d102ff10aa31d49c8915ef7dca8bdd53ad511ec69d4b0", +"TR_stellar-test_stellar.py::test_sign_tx[StellarAllowTrustOp-allow]": "de5380ba495945a1f5a37b93695e37decc6d3a88b4f9fde2ab5e7dc47e7013ae", +"TR_stellar-test_stellar.py::test_sign_tx[StellarAllowTrustOp-revoke]": "76fccfb32e7af4c0d453a06a24ea4cb0fefc5aa90f0376e9752dc53278852b18", +"TR_stellar-test_stellar.py::test_sign_tx[StellarBumpSequenceOp]": "6da7de3f6daa704999169ef5559870b69c4cce4d55fb3e448f60db5c02fcfa27", +"TR_stellar-test_stellar.py::test_sign_tx[StellarChangeTrustOp-add]": "635a0b6665f37f8bcab18604a602d09d86646c0085f5c07390609bb7929ca73e", +"TR_stellar-test_stellar.py::test_sign_tx[StellarChangeTrustOp-delete]": "a3bc4dd4cda7527a4d5ab451be5991dfd85330b10ea431877a0fc5a2303f450b", +"TR_stellar-test_stellar.py::test_sign_tx[StellarCreateAccountOp]": "a8ce09e7ca7c3a207dd0c39c80dd4d5b8ed058975bc90e6d73a560bebc0cea05", +"TR_stellar-test_stellar.py::test_sign_tx[StellarCreatePassiveSellOfferOp]": "57fe22584044474d8a8f6261b7047f5a99afcf6a52b315783df0e9b5443a9ab7", +"TR_stellar-test_stellar.py::test_sign_tx[StellarManageBuyOfferOp]": "d5080bd3b8df0b3622b3d86fff26a849d07e19c53466aae373ab483eaf22474e", +"TR_stellar-test_stellar.py::test_sign_tx[StellarManageDataOp]": "34afc1b227b401504300d27bf0b1a6017c079ce91baee43ec4cb6d2b2cd1af12", +"TR_stellar-test_stellar.py::test_sign_tx[StellarManageSellOfferOp]": "9be53703514dc641a8564e0ce1e70a14bd395e1d2b0f7982d8c86ead931a2ef4", +"TR_stellar-test_stellar.py::test_sign_tx[StellarPathPaymentStrictReceiveOp]": "5b247ae5f6aec85f2e9a4f09ba8fcccbeb2ebb1f45180124f6f6c6c134b7347e", +"TR_stellar-test_stellar.py::test_sign_tx[StellarPathPaymentStrictSendOp]": "e73951d048831ea16eb3d565b2d368ad36cc81a3a7f1100d084c9a9e6fc23bac", +"TR_stellar-test_stellar.py::test_sign_tx[StellarPaymentOp-asset12]": "3de4f9d79f038473b926cea21e0d0187419b1969aa6daed35903bba6987942fe", +"TR_stellar-test_stellar.py::test_sign_tx[StellarPaymentOp-asset4]": "2a4026223f656b58ecf1014f43444c2acf4aebf61f115aabdd570c3d8d687703", +"TR_stellar-test_stellar.py::test_sign_tx[StellarPaymentOp-native_asset]": "784f63246546229932b6e892a694c7159e334bea862451931830ead7e5f29230", +"TR_stellar-test_stellar.py::test_sign_tx[StellarSetOptionsOp-all]": "2b9e1bb851eb6dc049c0d842cc929712c76907facbab6b4f1cfcc2603d98a8d0", +"TR_stellar-test_stellar.py::test_sign_tx[StellarSetOptionsOp-one]": "2d17fb9ab3c049e48b1eaba6c409f3dd364cf8d5fb4e6e3a6b16b1bd8f6b71d0", +"TR_stellar-test_stellar.py::test_sign_tx[StellarSetOptionsOp-some]": "6b19e6399a33f52c8cb6c1025218335189d93ae12f6824a283a97291d0768259", +"TR_stellar-test_stellar.py::test_sign_tx[memo_hash]": "98af70eb02b2de2bbf4a2b7f4a3e8586d2c82ce270d318b22469361f316c1dc5", +"TR_stellar-test_stellar.py::test_sign_tx[memo_id]": "d36a751b8b392b572dd7f9ad821021dfc2af1c6775f177ea392630dbc52db9e4", +"TR_stellar-test_stellar.py::test_sign_tx[memo_return]": "0987da15422b655418a57ae363207b17b9b7a23868cadf6eeffb96881f4790c2", +"TR_stellar-test_stellar.py::test_sign_tx[memo_text]": "b23b860c234848c6373557b99d23e397021c9c88e15f12e295162685a6720b6c", +"TR_stellar-test_stellar.py::test_sign_tx[multiple_operations]": "2daa240dcf4fc7b0def87994289968995a0d8b8a5ee6aabc650d455053ec81ee", +"TR_stellar-test_stellar.py::test_sign_tx[source_account]": "784f63246546229932b6e892a694c7159e334bea862451931830ead7e5f29230", +"TR_stellar-test_stellar.py::test_sign_tx[timebounds-0-0]": "d543724975b9c7aab78e91b20d0aba9a11a03a58a6e0ece6befce9bef2d0b43d", +"TR_stellar-test_stellar.py::test_sign_tx[timebounds-0-1575234180]": "1fa84001237c7b1dd450edec4b7fffb4427516a8a98c9076396f87f88480c243", +"TR_stellar-test_stellar.py::test_sign_tx[timebounds-461535181-0]": "bf0b33352a8ab2c1994bd10584b0df79300744031310cdb267abd44f81383dc3", +"TR_stellar-test_stellar.py::test_sign_tx[timebounds-461535181-1575234180]": "784f63246546229932b6e892a694c7159e334bea862451931830ead7e5f29230", +"TR_test_autolock.py::test_apply_auto_lock_delay": "cfa4477748667896c16fce2dfccf689ba901094ca3ef0851a80ebd899126e956", +"TR_test_autolock.py::test_apply_auto_lock_delay_out_of_range[0]": "9f4f8e3c1ed1b25f3ac75a2bbcae6b3b792a4c554a325061c7ab0d3afc086b0e", +"TR_test_autolock.py::test_apply_auto_lock_delay_out_of_range[1]": "9f4f8e3c1ed1b25f3ac75a2bbcae6b3b792a4c554a325061c7ab0d3afc086b0e", +"TR_test_autolock.py::test_apply_auto_lock_delay_out_of_range[4194304]": "9f4f8e3c1ed1b25f3ac75a2bbcae6b3b792a4c554a325061c7ab0d3afc086b0e", +"TR_test_autolock.py::test_apply_auto_lock_delay_out_of_range[536871]": "9f4f8e3c1ed1b25f3ac75a2bbcae6b3b792a4c554a325061c7ab0d3afc086b0e", +"TR_test_autolock.py::test_apply_auto_lock_delay_out_of_range[9]": "9f4f8e3c1ed1b25f3ac75a2bbcae6b3b792a4c554a325061c7ab0d3afc086b0e", +"TR_test_autolock.py::test_apply_auto_lock_delay_valid[10]": "99ab6967467647364e5ff358dea525a80a390dcc305cd442e18a933b34c0e1c3", +"TR_test_autolock.py::test_apply_auto_lock_delay_valid[123]": "73ff46ad8c43257c06c2910327fff57971243082c5a456fa2a2e2ab968de402b", +"TR_test_autolock.py::test_apply_auto_lock_delay_valid[3601]": "6bba188585116eee2dab8aeef77db7a8117c2b175d4f40fa22deb82e2748565a", +"TR_test_autolock.py::test_apply_auto_lock_delay_valid[536870]": "432ce8faa7b9d32454ca174b28571478892cdf8a028388b2098dab63ad5dfa56", +"TR_test_autolock.py::test_apply_auto_lock_delay_valid[60]": "44603c01407f88e95e13c0ead9103d6327d7ff602d451ae204c9ccaf578359be", +"TR_test_autolock.py::test_apply_auto_lock_delay_valid[7227]": "bca9598c4c2f9934c92dc43e5563fc563a8bfd105f2cad89ecfb6500415a2322", +"TR_test_autolock.py::test_autolock_cancels_ui": "4ade35d66bddb3979f8403bfd8d11c24986ec46579242da66a1b24e802e23c0d", +"TR_test_autolock.py::test_autolock_default_value": "843e547eff7c81fbef0447cb848dff6f4d7b25b3ef408a74655703ef0a6783a0", +"TR_test_autolock.py::test_autolock_ignores_getaddress": "e8bf2d8340aec0c111f0896fc4553c532ff05b10dedd7cb80a6e26a51912c6c0", +"TR_test_autolock.py::test_autolock_ignores_initialize": "e8bf2d8340aec0c111f0896fc4553c532ff05b10dedd7cb80a6e26a51912c6c0", +"TR_test_basic.py::test_device_id_different": "30a09bff445999b891a8f11b239226ab34b7e8924554f0a6d16d5c75e7b0a7f9", +"TR_test_basic.py::test_device_id_same": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_test_basic.py::test_features": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_test_basic.py::test_ping": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_test_busy_state.py::test_busy_expiry": "ded9b055182c012f0aac1350a2133d0beabdda57458c6c20dfc154ad9f1ffbbc", +"TR_test_busy_state.py::test_busy_state": "56bc384f1583ff3d3c446106429064c88e9becaf0063cce23cb5ebf84817dbca", +"TR_test_cancel.py::test_cancel_message_via_cancel[message0]": "1fecfecbb5c0193a5740502e8b14854d7fb5dfaca7f767362abba09f711f0136", +"TR_test_cancel.py::test_cancel_message_via_cancel[message1]": "1fecfecbb5c0193a5740502e8b14854d7fb5dfaca7f767362abba09f711f0136", +"TR_test_cancel.py::test_cancel_message_via_initialize[message0]": "1fecfecbb5c0193a5740502e8b14854d7fb5dfaca7f767362abba09f711f0136", +"TR_test_cancel.py::test_cancel_message_via_initialize[message1]": "1fecfecbb5c0193a5740502e8b14854d7fb5dfaca7f767362abba09f711f0136", +"TR_test_cancel.py::test_cancel_on_paginated": "abc94f166091d2e4ad077be851211ecd00af621c6156b14badcbdee1479c6cb3", +"TR_test_debuglink.py::test_softlock_instability": "c53ac74b524ce81e65ec01e3955639af8cefc450158d1f485471eb226e28e527", +"TR_test_firmware_hash.py::test_firmware_hash_emu": "5c20e27dcba8ba5e74512a5b6c4517e07c29c3c2f622fc4b9532eddbf82c6ac9", +"TR_test_firmware_hash.py::test_firmware_hash_hw": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_test_msg_applysettings.py::test_apply_homescreen_tr_toif_good": "68c7050f48e71edbae71eb48d8d8f6a13c2a67273f867ef4383cc446f4f4b4cb", +"TR_test_msg_applysettings.py::test_apply_homescreen_tr_toif_with_long_label": "e313ed3709066940ab13798914aa39fe7d9adacb5494ae5bac95879b5d269431", +"TR_test_msg_applysettings.py::test_apply_homescreen_tr_toif_with_notification": "0173dff4286f921dfd547597a952d25a2afc0159271a0cfce2a6abf3aa8a5539", +"TR_test_msg_applysettings.py::test_apply_homescreen_tr_toif_wrong_size": "9f4f8e3c1ed1b25f3ac75a2bbcae6b3b792a4c554a325061c7ab0d3afc086b0e", +"TR_test_msg_applysettings.py::test_apply_homescreen_tr_upload_jpeg_fail": "9f4f8e3c1ed1b25f3ac75a2bbcae6b3b792a4c554a325061c7ab0d3afc086b0e", +"TR_test_msg_applysettings.py::test_apply_homescreen_tr_upload_t1_fail": "9f4f8e3c1ed1b25f3ac75a2bbcae6b3b792a4c554a325061c7ab0d3afc086b0e", +"TR_test_msg_applysettings.py::test_apply_settings": "5e04998816de1e1b8ec305f3016181f4d457bdeb7b8b52421e696b3e6eaadda5", +"TR_test_msg_applysettings.py::test_apply_settings_passphrase": "24552ce3474d186135be6bdb430ff92a61b37dc7faf4cf693866b53a92c6d345", +"TR_test_msg_applysettings.py::test_apply_settings_passphrase_on_device": "b798ba82060749ee58b3674d109b80e4c74e7b49a69e87b416bafba7307d3454", +"TR_test_msg_applysettings.py::test_apply_settings_rotation": "b57335603d51b120f804d897d27e041ad5965f480f1e4641950fbed760df430f", +"TR_test_msg_applysettings.py::test_experimental_features": "704835bd36c57634d2067dcc8a7709ff13e771f9396cb615f428b2a5f1eaa6f6", +"TR_test_msg_applysettings.py::test_label_too_long": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_test_msg_applysettings.py::test_safety_checks": "b57e1cd411e548649dd58583a430a8fcd8ff2b5c72587fd14ec463014d251d01", +"TR_test_msg_backup_device.py::test_backup_bip39": "b0358fa67450f4e04b58129a80297978651a91ec2ef93700cb58e6f3167b46b6", +"TR_test_msg_backup_device.py::test_backup_slip39_advanced[click_info]": "f6285c644035097bc5d828c52068b42edced80dd6372fa12cfb8e126d1b82455", +"TR_test_msg_backup_device.py::test_backup_slip39_advanced[no_click_info]": "b178435b12cf173256e3a786ee8175a9ace546431be1be78a21a03ec244e890a", +"TR_test_msg_backup_device.py::test_backup_slip39_basic[click_info]": "f6285c644035097bc5d828c52068b42edced80dd6372fa12cfb8e126d1b82455", +"TR_test_msg_backup_device.py::test_backup_slip39_basic[no_click_info]": "2b13a618d722fb62ba28e0f7adc92845993153b0017d5a4dd5b48a7dddef5b34", +"TR_test_msg_backup_device.py::test_interrupt_backup_fails": "37d014442e3151b01deabad623c6305bb1ef6d3162df7af34927cceeb4861257", +"TR_test_msg_backup_device.py::test_no_backup_fails": "804e49b88c36bb401f14d0f9e120f870580ef2a68b5e917b8108524fa1aa4fca", +"TR_test_msg_backup_device.py::test_no_backup_show_entropy_fails": "f49c8d846c2d56a575f0ad49463845ba641b02656783e4fcfc67d74e8fa671dd", +"TR_test_msg_change_wipe_code_t2.py::test_set_pin_to_wipe_code": "ca26008b1bd8e37b3dc779a11129fb9ee238f9d4008f734b5ccf6cbd67918ad8", +"TR_test_msg_change_wipe_code_t2.py::test_set_remove_wipe_code": "e84f96d216c92817b2269333ef507b02e808f4b97baae0ff0edcf237a75fd90a", +"TR_test_msg_change_wipe_code_t2.py::test_set_wipe_code_mismatch": "19ed628e347a0c9f2f429679d328bfa7a96c57e30985d5e2c5d1b6a6e09cdbdd", +"TR_test_msg_change_wipe_code_t2.py::test_set_wipe_code_to_pin": "2a98eee66bd08f0bad6b15dc3ea2313932f34e99f0c949ad1d81516567e7b24c", +"TR_test_msg_changepin_t2.py::test_change_failed": "48ac6a118d402154f32f65f600ef5fe61e43c711edf08852f14cb781b8ec04f5", +"TR_test_msg_changepin_t2.py::test_change_invalid_current": "90ec9c649c1240f04e076757301caac06b174c169671b7e824b1a02a5bf76374", +"TR_test_msg_changepin_t2.py::test_change_pin": "ba2294ce07c4b926e5c98074cb9266435addc737bce3ad8463a3b3b1c721a7bf", +"TR_test_msg_changepin_t2.py::test_remove_pin": "5fea07a9f484ce9fe2261f7bd0a999b06cc71f770d13ac36e77c888916d59723", +"TR_test_msg_changepin_t2.py::test_set_failed": "a4cd48c0fb4b70cfa29b0b12fd4512fdd9ce0771fc4652b065c9e3ac214bb2b5", +"TR_test_msg_changepin_t2.py::test_set_pin": "c5be153db623246c68eaa9babe9440e553a63f05a9d9199f90016ae89eb71a91", +"TR_test_msg_loaddevice.py::test_load_device_1": "5b94292bda41b70e07033ef9c82b188878b58397362b40d7976e7f8facc25a4a", +"TR_test_msg_loaddevice.py::test_load_device_2": "922867984a4859f4e091ef7f481feb203e83f15005d6d4b728c0eef8c5a54244", +"TR_test_msg_loaddevice.py::test_load_device_slip39_advanced": "5b94292bda41b70e07033ef9c82b188878b58397362b40d7976e7f8facc25a4a", +"TR_test_msg_loaddevice.py::test_load_device_slip39_basic": "5b94292bda41b70e07033ef9c82b188878b58397362b40d7976e7f8facc25a4a", +"TR_test_msg_loaddevice.py::test_load_device_utf": "87daa136eb012cffdce488493f6ae3e029a8380065d735e4782bc18e32d1ac95", +"TR_test_msg_ping.py::test_ping": "e5f42ea39c37305bd7c5096ff40cb641ee8574dc536bdd8d919898a8d58d21e3", +"TR_test_msg_wipedevice.py::test_autolock_not_retained": "7a8ae74bf2ebca70eddf6c0727cffb3ecf894172ce9221228e5b895d9e07f285", +"TR_test_msg_wipedevice.py::test_wipe_device": "30a09bff445999b891a8f11b239226ab34b7e8924554f0a6d16d5c75e7b0a7f9", +"TR_test_passphrase_slip39_advanced.py::test_128bit_passphrase": "815efae2f2acaf1406b5c372987b29bb1354eae36de5b471fae67b3a55fd9d6c", +"TR_test_passphrase_slip39_advanced.py::test_256bit_passphrase": "815efae2f2acaf1406b5c372987b29bb1354eae36de5b471fae67b3a55fd9d6c", +"TR_test_passphrase_slip39_basic.py::test_2of5_passphrase": "b9407d461ef8c185537f9db85ef0334dcc1278ba9ac342593824ec228272ca1c", +"TR_test_passphrase_slip39_basic.py::test_3of6_passphrase": "b9407d461ef8c185537f9db85ef0334dcc1278ba9ac342593824ec228272ca1c", +"TR_test_pin.py::test_correct_pin": "9f4f8e3c1ed1b25f3ac75a2bbcae6b3b792a4c554a325061c7ab0d3afc086b0e", +"TR_test_pin.py::test_exponential_backoff_t2": "18ab36a14463518c24750bed3773e060f1a9a3214fecbf65e297313243dd2b1e", +"TR_test_pin.py::test_incorrect_pin_t2": "bf23794ce742e9b4d5471bfecaf360e335cd99da77916eb4fe4c9ac9de483570", +"TR_test_pin.py::test_no_protection": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_test_protection_levels.py::test_apply_settings": "31f1b0720f748a2d888a6114aeff59b9e65b3376e0c9dfaff66550b385cde9c9", +"TR_test_protection_levels.py::test_change_pin_t2": "50d942d7baef0cf9c7ed82f6aae347433b25790ca5c0ae13adeec5f26c1f8d25", +"TR_test_protection_levels.py::test_get_address": "1a2bc5fd59ca303a8369dac6392f13f0d41fee2308b31de8b1d152336beff847", +"TR_test_protection_levels.py::test_get_entropy": "bb4bf662180831d0697ec8ebbb36cc0854fe1867e0537a26466a15d0be4e4b90", +"TR_test_protection_levels.py::test_get_public_key": "1a2bc5fd59ca303a8369dac6392f13f0d41fee2308b31de8b1d152336beff847", +"TR_test_protection_levels.py::test_initialize": "a3c5ab21d8f475b62313161f38ec8716cc85edd33ba8b080534afc543e5f1104", +"TR_test_protection_levels.py::test_passphrase_cached": "c5d0b5c38f3bddbedf6dd096587707c541e64cec208e653019f9c7c36e81adf3", +"TR_test_protection_levels.py::test_passphrase_reporting[False]": "f6cf3b47963b962bd25105f502c1a51dabf3631e82edd8cce5afe0838be7a7f9", +"TR_test_protection_levels.py::test_passphrase_reporting[True]": "78bc3aa02fee3a2794b809d6af4659ef0ff92591b9a2a82b8523c240a66fa21e", +"TR_test_protection_levels.py::test_ping": "e5f42ea39c37305bd7c5096ff40cb641ee8574dc536bdd8d919898a8d58d21e3", +"TR_test_protection_levels.py::test_sign_message": "9e5453eee1f6065efa5a49e4356e7dd7a2aa9e002ce6d427558203935ed21f20", +"TR_test_protection_levels.py::test_signtx": "fc347eaf1e04788f09f64158ac4528de63988a4a395f56858ec6f9b4774316a9", +"TR_test_protection_levels.py::test_unlocked": "3c5ba985aae1301a716d182252c7e4eb2a27d50d459df3069c055e238e0783ff", +"TR_test_protection_levels.py::test_verify_message_t2": "524bd4ba051b2069eac5a95f4a2ccac8c54e6f68e764741c401f2989e5515387", +"TR_test_protection_levels.py::test_wipe_device": "64997f43be7b528bb87b2868046d3ced16d389db4b9b27c21eeba1b004ac1270", +"TR_test_session.py::test_cannot_resume_ended_session": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_test_session.py::test_clear_session": "1a2bc5fd59ca303a8369dac6392f13f0d41fee2308b31de8b1d152336beff847", +"TR_test_session.py::test_derive_cardano_empty_session": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_test_session.py::test_derive_cardano_running_session": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_test_session.py::test_end_session": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_test_session.py::test_end_session_only_current": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_test_session.py::test_session_recycling": "b9407d461ef8c185537f9db85ef0334dcc1278ba9ac342593824ec228272ca1c", +"TR_test_session_id_and_passphrase.py::test_cardano_passphrase": "48f760de0007331ad4fd41a6c1b41b4c7c78c28a2156ad3664f1dc4a18754b50", +"TR_test_session_id_and_passphrase.py::test_hide_passphrase_from_host": "5c909bd44d51fdbf2c6777d73e7a4c14cf123a0c82fc061c294fefbdb5327c35", +"TR_test_session_id_and_passphrase.py::test_max_sessions_with_passphrases": "0cd358bef0f54a5281bc945e1dcb56de3b3cc592b83644275554e8f8488cb1ae", +"TR_test_session_id_and_passphrase.py::test_multiple_passphrases": "6ef3032ecd01d4f62294f878bb2e682b57c91521cca0ee158d2098bd6ad8375c", +"TR_test_session_id_and_passphrase.py::test_multiple_sessions": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_test_session_id_and_passphrase.py::test_passphrase_ack_mismatch": "97df1d41e48484b8372bf540ccc580b9532ef50e95101876777809029207deca", +"TR_test_session_id_and_passphrase.py::test_passphrase_always_on_device": "420c224571fe05c13286fac19363eaf954adcaa3fdc6735363e6f3d717210128", +"TR_test_session_id_and_passphrase.py::test_passphrase_length": "1b586b3f59194b29ffecc26198339e70dd8b140647a8bb668fb48284295b746c", +"TR_test_session_id_and_passphrase.py::test_passphrase_missing": "c5d0b5c38f3bddbedf6dd096587707c541e64cec208e653019f9c7c36e81adf3", +"TR_test_session_id_and_passphrase.py::test_passphrase_on_device": "5f799f05463021be742fee81eeddbdf71eb9369b800a8171587d3aebc066bb56", +"TR_test_session_id_and_passphrase.py::test_session_enable_passphrase": "05ac00a6703559a8354c8d5ca158e43dc0c13caec53434a999fd82d733d48dd0", +"TR_test_session_id_and_passphrase.py::test_session_with_passphrase": "43325012941787da894d614d0760a52d3e509e31f88602fbc22a8d2f7fb986af", +"TR_tezos-test_getaddress.py::test_tezos_get_address": "537e61b567a83d0f04debff71a0f4bb15fa7057b3156cefd00f1599cb7b52352", +"TR_tezos-test_getpublickey.py::test_tezos_get_public_key": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b", +"TR_tezos-test_sign_tx.py::test_tezos_kt_remove_delegation": "dd7fbb468f5204bf5274b7bbb6f045b77fe70889ad166dc4b3087df8112c71de", +"TR_tezos-test_sign_tx.py::test_tezos_sign_tx_delegation": "e8281f7826637ead2da3a965ebdb7c0c3143cd4c96baedf6ff883d82bf0e32e8", +"TR_tezos-test_sign_tx.py::test_tezos_sign_tx_multiple_proposals": "30b450f228881ad81134728dfd5c91b28605a774f9e22734ca4996aad63145a0", +"TR_tezos-test_sign_tx.py::test_tezos_sign_tx_origination": "6eda2e3caf3d6a30a874d479a1b873ee934de0b36265b807806afb455f294ad9", +"TR_tezos-test_sign_tx.py::test_tezos_sign_tx_proposal": "e7f502f3ca59f77ca6d9da0e7e2c91cd8fb217914be87ac4fd1ee294b146a729", +"TR_tezos-test_sign_tx.py::test_tezos_sign_tx_reveal": "4fb7882fa2f0708910b358943efb4e8ccd750a17b366031367a4fe752f4edcb3", +"TR_tezos-test_sign_tx.py::test_tezos_sign_tx_tranasaction": "9f20f6e43c7157969b8ed66c3f467e0bbb2398c221f2a3d666692ff167a893f4", +"TR_tezos-test_sign_tx.py::test_tezos_sing_tx_ballot_nay": "605a07269339e6f21346d9d6a72943a1dfaec71a3313ca869a2341a20ed5dd40", +"TR_tezos-test_sign_tx.py::test_tezos_sing_tx_ballot_pass": "97496b89b37842b234e7f44d2ffb2a6818fe654117179543837f1e038a1eb8a7", +"TR_tezos-test_sign_tx.py::test_tezos_sing_tx_ballot_yay": "53ceb3f7d5f02bfd44289efd3ab0980f0acdbabeba94493a58803cabdb4e2aa5", +"TR_tezos-test_sign_tx.py::test_tezos_smart_contract_delegation": "0bfa467f184a2199144fc4a9af7d294e932e416534968276fbc0a110992e8d45", +"TR_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer": "f617f8ba0365183578a4bc7cb48adbbb586cdc318d30269508d9a547bf4217f9", +"TR_tezos-test_sign_tx.py::test_tezos_smart_contract_transfer_to_contract": "edb0067a1761ed27b54f4d405a5f8debabac16dd9c3a1b7934c0c43dced8fe63", +"TR_webauthn-test_msg_webauthn.py::test_add_remove": "88fba47850635c4f6c13262196bfe39e747b9b320b864cbd0d7fdae307944999", +"TR_webauthn-test_u2f_counter.py::test_u2f_counter": "7be38df32071aa68585cd535a089c1920732d8067ff7783b8f03430ab7f23dc8", +"TR_zcash-test_sign_tx.py::test_external_presigned": "1b8667fe36eaaf077c0e59f451091dffea61b40a19487a32bdfe7a8285d62e41", +"TR_zcash-test_sign_tx.py::test_one_two": "e06dac38a6ba8f0384eb63571df00af430d8133b50d7a8b5d1cfa53989afd614", +"TR_zcash-test_sign_tx.py::test_refuse_replacement_tx": "28c2d334e8bb88a319e7df22e12d5036346bbe90aa3298e9cbd1218267c36743", +"TR_zcash-test_sign_tx.py::test_send_to_multisig": "872ee186b937d4829a4010598d20e950d9a531777f0400c801fadbf25224a9a3", +"TR_zcash-test_sign_tx.py::test_spend_multisig": "b7f7e177e1d230276888c5ef5171bf955e866f1bc0e024d7b52b2f1d388b6632", +"TR_zcash-test_sign_tx.py::test_spend_v4_input": "c5d17e68ce1e57309829c2bf4c9b903d2be41044932aee3b25c4bf38bb15debc", +"TR_zcash-test_sign_tx.py::test_spend_v5_input": "7ca33a0b18f2c72f830f4f3c6c612c26ccfb7cc137e5bc3fb516862fcdbcabc1", +"TR_zcash-test_sign_tx.py::test_unified_address": "fdbe3c2922d817cae91761de1e5d9f92ef5ebfab410f13565076c21e5b7bdede", +"TR_zcash-test_sign_tx.py::test_version_group_id_missing": "57e3aa5a6a55926dcc95ca290bf1b2826bbc86b535e0baa162f7c79b1784c96b" +} +}, "TT": { "click_tests": { "TT_test_autolock.py::test_autolock_does_not_interrupt_preauthorized": "03e156da82e7acd3509f831d1fc2e635fcebc673f3c14d61d141273e608fc7a4", @@ -732,13 +1919,13 @@ "TT_test_pin.py::test_pin_short": "b5377990e4a1f324133601e3ca4726cde7af50b3e2c3f53738022ed9108a6a79", "TT_test_pin.py::test_wipe_code_same_as_pin": "cb8aa7d781b689452e863a3e704c1df06d217cbf8761b698cd912e0417da1513", "TT_test_pin.py::test_wipe_code_setup": "353855511c20bcf4f8ec01d141c11a108472dc64e3dac06c5f567b42bddbaba9", -"TT_test_recovery.py::test_recovery_bip39": "85f92b26902f1ce64913d6ca9874a508fcb21fff8411145c438de69077a4989a", -"TT_test_recovery.py::test_recovery_slip39_basic": "53fa3abfe1ce7b11ec189fa359f463763b53802ae5925e84a6593271fe4e8f9a", -"TT_test_reset_bip39.py::test_reset_bip39": "01547ca97308b6b3b54fab9c8f18d4d43a8ca0637a95f4de6e0690808342fe4c", -"TT_test_reset_slip39_advanced.py::test_reset_slip39_advanced[16of16]": "4ddcb8eeb9062e739926998abb0bbed918b63cf1065c61199194f99034d54968", -"TT_test_reset_slip39_advanced.py::test_reset_slip39_advanced[2of2]": "39ef6d9d132f5489a790521cdbcefcd98abb1c3e85f9e847b307550c0f26d509", -"TT_test_reset_slip39_basic.py::test_reset_slip39_basic[16of16]": "1a1d30da89b002e082c03fca80c99d515fee1ff144ec01a24f8bcfd4cf831ec3", -"TT_test_reset_slip39_basic.py::test_reset_slip39_basic[1of1]": "ffc727ca4ec65738618ed20fcf0bd91c6d693d22575ab9a56c8c1bab1c66b3ec" +"TT_test_recovery.py::test_recovery_bip39": "6ff84bfab19bec7db2d5672a2059a69dbae56cc1e06464700024e431c9a1349f", +"TT_test_recovery.py::test_recovery_slip39_basic": "e038348233dc15169cd2678482f4512ed863fedca5cd70d94e571d88fa40c0a7", +"TT_test_reset_bip39.py::test_reset_bip39": "a37589806445d80b11edc57f3c80241fa14d538dbcd08f382bad7d010c3b708a", +"TT_test_reset_slip39_advanced.py::test_reset_slip39_advanced[16of16]": "67207985c09645772e42512a6cc3389e9389247d2164bb8e5731f86e707bfef7", +"TT_test_reset_slip39_advanced.py::test_reset_slip39_advanced[2of2]": "62f22bb7ec26ee3c12560bdf29496b7d4ca34b83083a6e4889c7d412c5a3e9ab", +"TT_test_reset_slip39_basic.py::test_reset_slip39_basic[16of16]": "ff60196c3efff9f8f9fc25d29d7e5241521d3c80a7ee43ead5f48e4914fac099", +"TT_test_reset_slip39_basic.py::test_reset_slip39_basic[1of1]": "35de95c810ac552b82d3af5b581dd3ade06f12b72a11cb45894d8a83c6e26be2" }, "device_tests": { "TT_binance-test_get_address.py::test_binance_get_address[m-44h-714h-0h-0-0-bnb1hgm0p7khfk85zpz-68e2cb5a": "483ff25f0ff24de80631dfb202ac681c38dfbf44c5905505281c2d0719a94fc6", @@ -748,13 +1935,13 @@ "TT_binance-test_sign_tx.py::test_binance_sign_message[message1-expected_response1]": "78b3b0efe134085ed595dcc859f53e39b2f044c3ae17e52ef7ff74d33303f5a9", "TT_binance-test_sign_tx.py::test_binance_sign_message[message2-expected_response2]": "86d841cce4f0a9ef06cdd35c264bc8d6384ef75dc7c72ffafdb8bfa2ca9ef5ee", "TT_bitcoin-test_authorize_coinjoin.py::test_cancel_authorization": "0a9ed8c97bfbf9f5615ffd9286c6ffb18f92f81481a2c97fb643b83cd96d2545", -"TT_bitcoin-test_authorize_coinjoin.py::test_get_address": "d6540eb29807b3954ffda0c49bbe34c85805b43d94a35de43e6cc6036db82230", -"TT_bitcoin-test_authorize_coinjoin.py::test_get_public_key": "064eb02d220e974352247d3b7065c38dd3d4fa3d0c5571574bf0d80f369b69d1", +"TT_bitcoin-test_authorize_coinjoin.py::test_get_address": "a350f2131e942e52029dd8646c33a5392335eef2f4e8a64499a3e185c8890f57", +"TT_bitcoin-test_authorize_coinjoin.py::test_get_public_key": "fe6f38a712c28289520dae54bbdf5aa67e7a34916a1b8c7064130419d5f22e04", "TT_bitcoin-test_authorize_coinjoin.py::test_multisession_authorization": "fe8cd9934667d12430865eeccd7fdfd2f957ac933aa438fd15d5eaa6a7c1ab7b", "TT_bitcoin-test_authorize_coinjoin.py::test_sign_tx": "23ab128ca97aaac5506d8e77f0fe9951db62538a60ea3664cab4f3d7b84501f6", "TT_bitcoin-test_authorize_coinjoin.py::test_sign_tx_large": "fd523b4cd46d79aba746ab20e75a519ffa9858d94471ab53a7f22c2edf7306ab", -"TT_bitcoin-test_authorize_coinjoin.py::test_sign_tx_migration": "7d51f031ccdb5ced6f0cad7f1451c5d8d626e8205b77c560dda93edaad5bed36", -"TT_bitcoin-test_authorize_coinjoin.py::test_sign_tx_spend": "90183688f4c10d3175737f9b87a7c63d8affb4885fa4b65b13cbff3098f3e958", +"TT_bitcoin-test_authorize_coinjoin.py::test_sign_tx_migration": "750b3cebac7e8467763642fce5c5b3a7984877e199bc0b1acf36eb9b266985e8", +"TT_bitcoin-test_authorize_coinjoin.py::test_sign_tx_spend": "4df5b3f3038732bef4f0613ffa82ba840f571cd44740ea5c4e0dd8fd3e37a915", "TT_bitcoin-test_authorize_coinjoin.py::test_wrong_account_type": "0a9ed8c97bfbf9f5615ffd9286c6ffb18f92f81481a2c97fb643b83cd96d2545", "TT_bitcoin-test_authorize_coinjoin.py::test_wrong_coordinator": "0a9ed8c97bfbf9f5615ffd9286c6ffb18f92f81481a2c97fb643b83cd96d2545", "TT_bitcoin-test_bcash.py::test_attack_change_input": "1dc0c4d7c20171bc335c042e14d755f5a6b70e95e1ea1b037687c96ae683a6a4", @@ -780,7 +1967,7 @@ "TT_bitcoin-test_decred.py::test_send_decred": "547d6ab1c2d786c4f2cfa5295840be29bdd2869e3ab2d1af350621c8885a97b1", "TT_bitcoin-test_decred.py::test_send_decred_change": "094be5a480eb0a61e308e616a6036d3c1b40596ed3d65a70ba4ec81b5428b267", "TT_bitcoin-test_decred.py::test_spend_from_stake_generation_and_revocation_decred": "4b80acda816aba126b998713fda8781ef7c0e85cde52e2725ea95770dcd2840c", -"TT_bitcoin-test_descriptors.py::test_descriptors[Bitcoin-0-10025-InputScriptType.SPENDTAPROOT--ad9e3c78": "4a12b2c06657aba79b4e3a4e781853f72b3d8b72ff3f064f29b6d27b55d3a367", +"TT_bitcoin-test_descriptors.py::test_descriptors[Bitcoin-0-10025-InputScriptType.SPENDTAPROOT--ad9e3c78": "83e01dc7332e7dd4a7ddb6ed3498c65e4f05f71e3096f122416e4e58787cba2e", "TT_bitcoin-test_descriptors.py::test_descriptors[Bitcoin-0-44-InputScriptType.SPENDADDRESS-pkh-efa37663": "1e5f4063c58532f2d7f08fe7691e5ca53755be72b45b23878ee66a54d41b99a3", "TT_bitcoin-test_descriptors.py::test_descriptors[Bitcoin-0-49-InputScriptType.SPENDP2SHWITNESS-77f1e2d2": "a296683c9c10db1666ade04c78718778a4f508c4eb157cee15fe635cfce2849c", "TT_bitcoin-test_descriptors.py::test_descriptors[Bitcoin-0-84-InputScriptType.SPENDWITNESS-wpk-16507754": "1a76e0ddc47471bb3d218d537068e3ebb69e2375b22a906bc9fc6f88b9c7b87b", @@ -789,7 +1976,7 @@ "TT_bitcoin-test_descriptors.py::test_descriptors[Bitcoin-1-49-InputScriptType.SPENDP2SHWITNESS-965f4fa3": "a729825dd2b4040e8f1b8e04e62c717f57f1dd4566c6a13c30b857eddca1961f", "TT_bitcoin-test_descriptors.py::test_descriptors[Bitcoin-1-84-InputScriptType.SPENDWITNESS-wpk-c761bcc8": "ab9309a30fb08e37bfdc73dca1cbdb2d1f121fc7a5409fd254ecc1e962759c78", "TT_bitcoin-test_descriptors.py::test_descriptors[Bitcoin-1-86-InputScriptType.SPENDTAPROOT-tr(-686799ea": "9e001b2c57b836741a897bf302c6247bf11230795347fb7ed0f020bd92077065", -"TT_bitcoin-test_descriptors.py::test_descriptors[Testnet-0-10025-InputScriptType.SPENDTAPROOT--77d3a71b": "c0cbf053143c87dd8517fa7704973462c8de00c5f4f3ba1736260c0d94529870", +"TT_bitcoin-test_descriptors.py::test_descriptors[Testnet-0-10025-InputScriptType.SPENDTAPROOT--77d3a71b": "79e02463db9267b097ec4919336b65a59f536c7ad196c72cb37b46f2e454cf1e", "TT_bitcoin-test_descriptors.py::test_descriptors[Testnet-0-44-InputScriptType.SPENDADDRESS-pkh-a26143a7": "2a196fb10afe61c28404b2195ba5bb01939d2e7c2e6053e1c99acbaebff240b4", "TT_bitcoin-test_descriptors.py::test_descriptors[Testnet-0-49-InputScriptType.SPENDP2SHWITNESS-195ebda5": "0df76c7071e5f5aff6ff4ccbb1c88307fae8ca3c334e5853c0afd944498f6728", "TT_bitcoin-test_descriptors.py::test_descriptors[Testnet-0-84-InputScriptType.SPENDWITNESS-wpk-68f8b526": "151a5602d4f9f3ce6bed346d57453101de56a1a6b4d9c6cb51d1fb9017af5fd3", @@ -1661,52 +2848,52 @@ "TT_reset_recovery-test_recovery_bip39_dryrun.py::test_dry_run": "a6bb86ea88a3a70da10cfe9ead2c693a67c50bb6c8eba131d9b68a73bb0f3386", "TT_reset_recovery-test_recovery_bip39_dryrun.py::test_invalid_seed_core": "0cf1479f503749cac5f601b0c3daa279b20788cf8af425e05e3ee1767c988287", "TT_reset_recovery-test_recovery_bip39_dryrun.py::test_seed_mismatch": "ace9103b7e4912b788772a803b1fc31e5287945fbd42b0f61f5c10853bd596cd", -"TT_reset_recovery-test_recovery_bip39_dryrun.py::test_uninitialized": "e00c46a70bc5bbfdd3bb6da5a712698e7c79bc8ebd78262ec0f13ef9ee6aec95", +"TT_reset_recovery-test_recovery_bip39_dryrun.py::test_uninitialized": "001377ce61dcd189e6a9d17e20dcd71130e951dc3314b40ff26f816bd9355bdd", "TT_reset_recovery-test_recovery_bip39_t2.py::test_already_initialized": "80a6e289138a604cf351a29511cf6f85e2243591317894703152787e1351a1a3", -"TT_reset_recovery-test_recovery_bip39_t2.py::test_tt_nopin_nopassphrase": "5a9de868c46bcd142b787055dec14d4021e1e9b739a7caddc6b0e6310e75c779", -"TT_reset_recovery-test_recovery_bip39_t2.py::test_tt_pin_passphrase": "48713e0cce260b57158bd0ccc7e1afbd78a462e40dfb5d3565dd73b8e9f03db1", -"TT_reset_recovery-test_recovery_slip39_advanced.py::test_abort": "298566438821aba89609e87bab5db4b1b0f970daeff582cefda03ebd13249ad7", -"TT_reset_recovery-test_recovery_slip39_advanced.py::test_extra_share_entered": "5d12077928e7d64b19ec269d4acb966ea5e0b25fd63018dafff8fc59ea697541", -"TT_reset_recovery-test_recovery_slip39_advanced.py::test_group_threshold_reached": "e22a581c54ac6d1483d2b439d1c70507d2219d537ae40b73d83e173902fdc36e", -"TT_reset_recovery-test_recovery_slip39_advanced.py::test_noabort": "8c30677e2140fb4dd6356cc5accca86c3b1165b556c6429f4ec1da3046b95c09", -"TT_reset_recovery-test_recovery_slip39_advanced.py::test_same_share": "1e3ed9ed33d53badc71daa3c64cde25e4065bee1ab1397f6afeb8c8fbd579252", -"TT_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares0-c2d2e26ad06023c60145f1-afc2dad5": "7f65ee03efccb79784dc67db341c89d3a3d9320282198895ddaadeb9787c1530", -"TT_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares1-c41d5cf80fed71a008a3a0-eb47093e": "1aee3a12001dfdeeb074ead1902bf687540021ebd30d68caca499e2f7a72a08f", -"TT_reset_recovery-test_recovery_slip39_advanced.py::test_secret_click_info_button[shares0-c2d2-850ffa77": "8be5e4e0f50b470183b8f7fa81b89c1cc34e5ccc9870d4b74f4338f1f16eff6d", -"TT_reset_recovery-test_recovery_slip39_advanced.py::test_secret_click_info_button[shares1-c41d-ca9ddec8": "6858b02b98439d6d39cf1a6140886f4f3bf60c83c3f7da719c9a7f37c8ffda7e", +"TT_reset_recovery-test_recovery_bip39_t2.py::test_tt_nopin_nopassphrase": "4438d1c972b7e2e4648c028f3ef1d81496c5e745d7fc2edf4934bede5472ee5a", +"TT_reset_recovery-test_recovery_bip39_t2.py::test_tt_pin_passphrase": "0c6dec48b7f80a1395d905927a2f7c6a32ba071254fa742a7b2e770cd6b8073c", +"TT_reset_recovery-test_recovery_slip39_advanced.py::test_abort": "6bed776bc2908a522d3b7bde5342fe2289b3fdbbd789b9ce4af077255e5d65ca", +"TT_reset_recovery-test_recovery_slip39_advanced.py::test_extra_share_entered": "8942507f2c9f6bdf88944664775d3339851c6a51f6d1427c5153a87a91521eae", +"TT_reset_recovery-test_recovery_slip39_advanced.py::test_group_threshold_reached": "1a0588f23f11e0c3534b3edab0eaa0b3548888ecf6a81eca5943c645c1029fa5", +"TT_reset_recovery-test_recovery_slip39_advanced.py::test_noabort": "22365e97774a181742d498066a50f3ff4827731b4585839993a4bd821fdf69be", +"TT_reset_recovery-test_recovery_slip39_advanced.py::test_same_share": "473a4dd65d79da831e35fc04180db882c3fab6ffeb1496a956664f9b310255b4", +"TT_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares0-c2d2e26ad06023c60145f1-afc2dad5": "e43b38e994bfe300236965c37cf46fa87ce28a0db3896d917e1d93d2f926575b", +"TT_reset_recovery-test_recovery_slip39_advanced.py::test_secret[shares1-c41d5cf80fed71a008a3a0-eb47093e": "70c237ab2ffd286c8d2fffb3c0b115220c55e3b14b42e11785a0111123341e3e", +"TT_reset_recovery-test_recovery_slip39_advanced.py::test_secret_click_info_button[shares0-c2d2-850ffa77": "ac06a431b3d41e791f371dcf059aa239a54770283c0bcf0730dfe0e4b5255941", +"TT_reset_recovery-test_recovery_slip39_advanced.py::test_secret_click_info_button[shares1-c41d-ca9ddec8": "3d01ce6c2ececb26be5b859e9b32dde545e3f44c039528b6c28dc9e7ff089757", "TT_reset_recovery-test_recovery_slip39_advanced_dryrun.py::test_2of3_dryrun": "2424ec3ff74afe05bdd8eecfe319edbf6e02d9b9ec99c74285cfd531cfc329e2", "TT_reset_recovery-test_recovery_slip39_advanced_dryrun.py::test_2of3_invalid_seed_dryrun": "0d4d106399303bd2b91021ad200bb8127071ac0da20fe743bae012c3c954064a", -"TT_reset_recovery-test_recovery_slip39_basic.py::test_1of1": "0aec95e491b414b5a9e35655d5bdd96c957f04c327b9ee5d7aae9cec1438af30", -"TT_reset_recovery-test_recovery_slip39_basic.py::test_abort": "298566438821aba89609e87bab5db4b1b0f970daeff582cefda03ebd13249ad7", -"TT_reset_recovery-test_recovery_slip39_basic.py::test_ask_word_number": "f741ca408166a991ecfed4be1603d63dcd9e8c074c081716ec9cc5fe1d7acad1", -"TT_reset_recovery-test_recovery_slip39_basic.py::test_noabort": "08f76f65cd88dbb795860a439f1687a5cab259fa1081591bd06834e6ea1a470e", -"TT_reset_recovery-test_recovery_slip39_basic.py::test_recover_with_pin_passphrase": "06c8f0b25b010fb2a13b720b1fdca30c3cfed20dc7fb00a5631fdcb4dcd97e19", -"TT_reset_recovery-test_recovery_slip39_basic.py::test_same_share": "66a7a9ba1a9a040285fd2a217411205ecd8921ae68044b85495fe84a704292ca", -"TT_reset_recovery-test_recovery_slip39_basic.py::test_secret[shares0-491b795b80fc21ccdf466c0fbc98c8fc]": "14a54cbc1fd02e1cd8f9339ebab8da85fa0e427b211facd48bc87ab06a1ccbf6", -"TT_reset_recovery-test_recovery_slip39_basic.py::test_secret[shares1-b770e0da1363247652de97a39-a50896b7": "e56168efe1ffb2dcef749748ee3f0ec843005afbee86579f6c73e6a337cf6ce1", -"TT_reset_recovery-test_recovery_slip39_basic.py::test_wrong_nth_word[0]": "518f09692194650323eaf16bb49b39fcf63b20bd3ae8c633b91552cb6ce88c69", -"TT_reset_recovery-test_recovery_slip39_basic.py::test_wrong_nth_word[1]": "69ef06c06a149aff209d6dc8491de8362fac7b1537c5d94f11a1e276003646df", -"TT_reset_recovery-test_recovery_slip39_basic.py::test_wrong_nth_word[2]": "b2cd7038d3e0a3098f3d91e08dcdf8b52909f9931c4d5ebef8ea4b64919b9bcc", +"TT_reset_recovery-test_recovery_slip39_basic.py::test_1of1": "998aa239cd9e3fd07323678d0f5359caee71917bd7a9ce0d4135b63d89f114f4", +"TT_reset_recovery-test_recovery_slip39_basic.py::test_abort": "6bed776bc2908a522d3b7bde5342fe2289b3fdbbd789b9ce4af077255e5d65ca", +"TT_reset_recovery-test_recovery_slip39_basic.py::test_ask_word_number": "9828bd72da5224891bc1128d63c0a95b26312922dbe452f0ba7792b28f7af58c", +"TT_reset_recovery-test_recovery_slip39_basic.py::test_noabort": "d931d3bcb239a1d87a826b1f1e56afcfd135f849e3dd0b78746804a6c6353fff", +"TT_reset_recovery-test_recovery_slip39_basic.py::test_recover_with_pin_passphrase": "dd780a71332d7a45c022b991701d836aba3b7b40ce5c4658dde6c60874847ee0", +"TT_reset_recovery-test_recovery_slip39_basic.py::test_same_share": "55f9148cb51b29428358d082db7b45a3ba4cc753f2cd266e218a7e298636009c", +"TT_reset_recovery-test_recovery_slip39_basic.py::test_secret[shares0-491b795b80fc21ccdf466c0fbc98c8fc]": "484663328603f121d853d4df3529f9279299e70cc51810b04723f164b0cb177b", +"TT_reset_recovery-test_recovery_slip39_basic.py::test_secret[shares1-b770e0da1363247652de97a39-a50896b7": "d50c6f7d2346cff381279acdc413dc8734c1df3c2e56b70ea84fed58de254276", +"TT_reset_recovery-test_recovery_slip39_basic.py::test_wrong_nth_word[0]": "41f503116c4a65d673335e4813fba43daab3a73b4fb94b8e469c980904908934", +"TT_reset_recovery-test_recovery_slip39_basic.py::test_wrong_nth_word[1]": "cbf8f6392e8ad562c7b15d66dd1730c388c997327c9d3da487d3ce9345133632", +"TT_reset_recovery-test_recovery_slip39_basic.py::test_wrong_nth_word[2]": "be1dd4ecbfc29a767dd3f1faf6ee12282422f0504175b996afa352b39a7f540b", "TT_reset_recovery-test_recovery_slip39_basic_dryrun.py::test_2of3_dryrun": "87718399c69d11e8656d4af3af448f542970fbfca524a53955adcd911903efd0", "TT_reset_recovery-test_recovery_slip39_basic_dryrun.py::test_2of3_invalid_seed_dryrun": "0fbabbe652adc6a4e0d7285b165687dce0ad617762ed228f6a21d56de409d799", -"TT_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Bip39-backup_flow_bip39]": "31342942033bfb5fa25003467cff9c21767ec2534d439225e5b8e15852bbf4c2", -"TT_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Slip39_Advanced-bac-f67baa1c": "4efa25214bb6972a833e3ab3dc1754f80844fbc0116321daa5f08acc42f49bf8", -"TT_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Slip39_Basic-backup-6348e7fe": "8bf5619172299ec2b031f408e24850c8e8c564414d62761b573282acf9e150c9", -"TT_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Bip39-backup_flow_bip39]": "009db3044ceb8aad2ae01539c92a17d658a711416e4721dff45620de75c7ae5f", -"TT_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced-backup-dcbda5cf": "dbadedde1fd638f72f33590ae32425a0a645c9a0818a5030f0bd9adbe961c2bf", -"TT_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic-backup_fl-1577de4d": "245fafadd5b9181f1a512414ce9c486fff2b9c76044c4855bc143e4f5fd28592", +"TT_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Bip39-backup_flow_bip39]": "774d71f7654fa0fd89198359c96cfac655a21f4410c056f40e12b9309874b196", +"TT_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Slip39_Advanced-bac-f67baa1c": "8410e57b746339b2a35078412fc2b83d26ddffbf73a8545f52dcda07b052b7b3", +"TT_reset_recovery-test_reset_backup.py::test_skip_backup_manual[BackupType.Slip39_Basic-backup-6348e7fe": "6dedfe69fd4f29b5c0fc65e85ef51cb0294b95c30021b401655c0516409a09ad", +"TT_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Bip39-backup_flow_bip39]": "1aa3ce14a715dfaf9bba07c613bd96473bcfa57e48888f959d40a37fd8cda4c6", +"TT_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Advanced-backup-dcbda5cf": "a5fc539ca06049a3753b767a2dc409e0dacefd5878cfeda0b2c20c8ed87036b5", +"TT_reset_recovery-test_reset_backup.py::test_skip_backup_msg[BackupType.Slip39_Basic-backup_fl-1577de4d": "12b002d816c964295d1f49318871555ab7eb71f42af04fab94e7978f05b25d9e", "TT_reset_recovery-test_reset_bip39_t2.py::test_already_initialized": "80a6e289138a604cf351a29511cf6f85e2243591317894703152787e1351a1a3", -"TT_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "3ec95417936f90e01b92ab89b50d1a5acb98befb51c5767914f501298e1a794c", -"TT_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "fa804eaa19c56485ae7f156ba47a3c9642aa70e65295a19e018cf7e0c7156d25", -"TT_reset_recovery-test_reset_bip39_t2.py::test_reset_device_192": "cf6b6b9b6c760f229725028cb9413ffc8fe31563045d8b13361a17c040e8a41f", -"TT_reset_recovery-test_reset_bip39_t2.py::test_reset_device_pin": "c4a8bf133da655de8b9f56d79e5c390a7eda43fbf49cc43e91688e3f09b4a846", -"TT_reset_recovery-test_reset_bip39_t2.py::test_reset_failed_check": "3fb31bd98acab477867d855a434e10db27daefeab6574c9eb6d6fabd55a011d1", -"TT_reset_recovery-test_reset_recovery_bip39.py::test_reset_recovery": "704d1d6eb72c545e56007882356fbf64a70e071efb8eedded6d4f771cad93e21", -"TT_reset_recovery-test_reset_recovery_slip39_advanced.py::test_reset_recovery": "13ec15333838611fcd996e8c6b78973b7e2a965d2cd9e18c45dbcdf1f9290aa8", -"TT_reset_recovery-test_reset_recovery_slip39_basic.py::test_reset_recovery": "707ce5ce7f1bf07b0316db19bd82ffe45f26862bbfb3d8b96020ec7597cd5264", -"TT_reset_recovery-test_reset_slip39_advanced.py::test_reset_device_slip39_advanced": "68555d1b1da91289c2841d015166ec4643e14268c0a597bd6f6eb968e7fa2a4d", -"TT_reset_recovery-test_reset_slip39_basic.py::test_reset_device_slip39_basic": "438e9f0555ec91b6bdd416404f084a71d287591c4b7efe06c996c25c742feca0", -"TT_reset_recovery-test_reset_slip39_basic.py::test_reset_device_slip39_basic_256": "3b30eaaffa00672b6ebfd60a34f1c0356aaef063ac8c40357b24cf86bd772314", +"TT_reset_recovery-test_reset_bip39_t2.py::test_failed_pin": "cb7f55d17ebf06888ff02ab3db0a5bd2b5355dcc465eab5fc009089845623c72", +"TT_reset_recovery-test_reset_bip39_t2.py::test_reset_device": "8f08225079c56dc3514277913a3fd409fd9ca68fb899b1b04fe94c31eba20da0", +"TT_reset_recovery-test_reset_bip39_t2.py::test_reset_device_192": "dcdb1b242a1747766fcb3f91d95226c47ad68e31c1a68c15b3dad219d4bf733a", +"TT_reset_recovery-test_reset_bip39_t2.py::test_reset_device_pin": "0fb3df94993fe08bb219ff293329a07e367e6dc2e659a8817a3a9b039835e5b9", +"TT_reset_recovery-test_reset_bip39_t2.py::test_reset_failed_check": "e447e861244e3ee749842f503ac344192fa6bb7fee499317f53985579c5c840e", +"TT_reset_recovery-test_reset_recovery_bip39.py::test_reset_recovery": "28187d0bd07df432025468ed1ec5dc96ad5a8df7eceb5013e66cead01db76524", +"TT_reset_recovery-test_reset_recovery_slip39_advanced.py::test_reset_recovery": "0b6c7b04b3c71326d6849d2d93b52f49fae67393fcd967d16fd64062bab23142", +"TT_reset_recovery-test_reset_recovery_slip39_basic.py::test_reset_recovery": "a43eb7f0c6fe3f42f7086efd984c78c02994798dcc134cd8e683c1b53f394d1f", +"TT_reset_recovery-test_reset_slip39_advanced.py::test_reset_device_slip39_advanced": "bdf31edf5ed0384dfd429427d6c8362f6e6862ac8424a2e04bdc0603c9167e64", +"TT_reset_recovery-test_reset_slip39_basic.py::test_reset_device_slip39_basic": "ec94b714938f999d301198681e22eecaca59cfbc5ca3499fc58268f7d52bab89", +"TT_reset_recovery-test_reset_slip39_basic.py::test_reset_device_slip39_basic_256": "f5e65eeadaa2a3adc45c6425135434d84131d81056f7426bded9503b98e2517c", "TT_ripple-test_get_address.py::test_ripple_get_address": "80a6e289138a604cf351a29511cf6f85e2243591317894703152787e1351a1a3", "TT_ripple-test_get_address.py::test_ripple_get_address_other": "80a6e289138a604cf351a29511cf6f85e2243591317894703152787e1351a1a3", "TT_ripple-test_sign_tx.py::test_ripple_sign_invalid_fee": "80a6e289138a604cf351a29511cf6f85e2243591317894703152787e1351a1a3", @@ -1766,7 +2953,7 @@ "TT_test_autolock.py::test_autolock_default_value": "0126aab250a451219f64248cb9ff5a34e36e9c8caf85b175f16fbaa90a38e654", "TT_test_autolock.py::test_autolock_ignores_getaddress": "21605135cfbb4914cd525d6b76f260a17fe6552fdadcf9a89b2ec6e3572606f5", "TT_test_autolock.py::test_autolock_ignores_initialize": "21605135cfbb4914cd525d6b76f260a17fe6552fdadcf9a89b2ec6e3572606f5", -"TT_test_basic.py::test_device_id_different": "4c7862bedbc9de710152f7b7ab15256ac9885f3c9f8082e6d04125acca6a71d7", +"TT_test_basic.py::test_device_id_different": "6f8d7ba4ef109b2c20d09689d1fa329e23d5a11d21ce47a91339aeb21ffcf1ad", "TT_test_basic.py::test_device_id_same": "80a6e289138a604cf351a29511cf6f85e2243591317894703152787e1351a1a3", "TT_test_basic.py::test_features": "80a6e289138a604cf351a29511cf6f85e2243591317894703152787e1351a1a3", "TT_test_basic.py::test_ping": "80a6e289138a604cf351a29511cf6f85e2243591317894703152787e1351a1a3", @@ -1777,7 +2964,7 @@ "TT_test_cancel.py::test_cancel_message_via_initialize[message0]": "1bd3157d54327e33542f89dcac6c7cd23808f7c9aa1b0adb390e5fcc1fd858a5", "TT_test_cancel.py::test_cancel_message_via_initialize[message1]": "1bd3157d54327e33542f89dcac6c7cd23808f7c9aa1b0adb390e5fcc1fd858a5", "TT_test_cancel.py::test_cancel_on_paginated": "18c6d381e39960d3f276935dd0d3864292a442a19c65f0608926eb77b410ea9e", -"TT_test_debuglink.py::test_softlock_instability": "61fd0675369673bd7da1314b1c30435d7fc2ba14ab63252c443172b6d27f3457", +"TT_test_debuglink.py::test_softlock_instability": "a80a8c9a99a3abf8c697d5aaaced7a028ceaa217856bbaf82b9db7c083cb21eb", "TT_test_firmware_hash.py::test_firmware_hash_emu": "2a63f0bd10ba99e223f571482d4af635653bb8a3bddc1d8400777ee5519bc605", "TT_test_firmware_hash.py::test_firmware_hash_hw": "80a6e289138a604cf351a29511cf6f85e2243591317894703152787e1351a1a3", "TT_test_msg_applysettings.py::test_apply_homescreen_jpeg": "118a204cef5364279671bda429c116c91626606952d40bf606b1609793b34505", @@ -1791,14 +2978,14 @@ "TT_test_msg_applysettings.py::test_experimental_features": "e7bdcfa8708fc0fbe88f5f4cc7f93497c94fa5ae9e08282496c04e993123674c", "TT_test_msg_applysettings.py::test_label_too_long": "80a6e289138a604cf351a29511cf6f85e2243591317894703152787e1351a1a3", "TT_test_msg_applysettings.py::test_safety_checks": "4ec93f34a54d13317c2d5434ed5853d0edf0f6d91772aab2a15acf7e3de75458", -"TT_test_msg_backup_device.py::test_backup_bip39": "fc72e8b9b93359d9376f2302df88f0a81cd081849c69d97886ac55fd2989d1ad", -"TT_test_msg_backup_device.py::test_backup_slip39_advanced[click_info]": "b1d9f2f70ae7a0e1a4cf2b7c9f83527b294ce788e258f5fe79c3be343c934457", -"TT_test_msg_backup_device.py::test_backup_slip39_advanced[no_click_info]": "3a51e40b5553f97c96e300d28c1006610f3b8185b93b25b5c24ff5f7d6af9021", -"TT_test_msg_backup_device.py::test_backup_slip39_basic[click_info]": "dedf59842374b64e023dec94965b9b9cf9263c212c9fb3dfed2ba3e80518e998", -"TT_test_msg_backup_device.py::test_backup_slip39_basic[no_click_info]": "be7d16c53965837cb6ae0d89985e50cc7aeee9d3ede0f578def43c9832e3cd2b", -"TT_test_msg_backup_device.py::test_interrupt_backup_fails": "cdf801e16b046079569e4055f43a86a45d7eace58793427ef5433f71e75961f9", +"TT_test_msg_backup_device.py::test_backup_bip39": "f893431c45c12b0cab6695ad1ed3740bef8a546ea77c000e02287acc51f49396", +"TT_test_msg_backup_device.py::test_backup_slip39_advanced[click_info]": "6e0456552d918a2bfeff518f664bf217ff69fb78585b9bedb786e453a602e0ed", +"TT_test_msg_backup_device.py::test_backup_slip39_advanced[no_click_info]": "58261eb9d6e99256ceb5ebbea9fb91529bc9fd3bdb178787aa4e1e7ad85fc4f3", +"TT_test_msg_backup_device.py::test_backup_slip39_basic[click_info]": "7977be97fb539b1f0706fb792a3ba603148e656e6d1f1dcf599fc033aa8e02e2", +"TT_test_msg_backup_device.py::test_backup_slip39_basic[no_click_info]": "5e1e10a652aac8c95a771e873a1823ca35811df7fdb2356c15d1c2ab06ae7d3c", +"TT_test_msg_backup_device.py::test_interrupt_backup_fails": "ae147498028d68aa71c7337544e4a5049c4c943897f905c6fe29e88e5c3ab056", "TT_test_msg_backup_device.py::test_no_backup_fails": "fada9d38ec099b3c6a4fd8bf994bb1f3431e40085128b4e0cd9deb8344dec53e", -"TT_test_msg_backup_device.py::test_no_backup_show_entropy_fails": "e00c46a70bc5bbfdd3bb6da5a712698e7c79bc8ebd78262ec0f13ef9ee6aec95", +"TT_test_msg_backup_device.py::test_no_backup_show_entropy_fails": "001377ce61dcd189e6a9d17e20dcd71130e951dc3314b40ff26f816bd9355bdd", "TT_test_msg_change_wipe_code_t2.py::test_set_pin_to_wipe_code": "b3abe56cbc85b9012e1fa27dce0e869f0a01de7a72db24e44177690a57392b52", "TT_test_msg_change_wipe_code_t2.py::test_set_remove_wipe_code": "2e2cdd155ac500a8ef6136eb1c972c049c8dfb83ce7e8e8e51bc4e0cb51f2b13", "TT_test_msg_change_wipe_code_t2.py::test_set_wipe_code_mismatch": "b40c8acdfa2042070a2f725afde819971a55116848b9316abf12683f7098ab11", @@ -1809,17 +2996,17 @@ "TT_test_msg_changepin_t2.py::test_remove_pin": "1a415b1d0d626bf52c08f948f8761ad84f18550931ba34e422c0ee58862f060e", "TT_test_msg_changepin_t2.py::test_set_failed": "ca78b60d44956e7e9626315bb5b282305914f05db17414f39b02aef5175bc4ce", "TT_test_msg_changepin_t2.py::test_set_pin": "0319bceb94bf789e5cfd34c733f088bf4c5e538f1a578a380e760073bf2da37f", -"TT_test_msg_loaddevice.py::test_load_device_1": "3643774c15de3f99b82fff125bc05b9af066f5c08b7861a117f0c05da79ba1c5", -"TT_test_msg_loaddevice.py::test_load_device_2": "e37151e13b8a880033a82c065a41271d19f8c4a2b3c34985a7f5f04c1f4f0010", -"TT_test_msg_loaddevice.py::test_load_device_slip39_advanced": "3643774c15de3f99b82fff125bc05b9af066f5c08b7861a117f0c05da79ba1c5", -"TT_test_msg_loaddevice.py::test_load_device_slip39_basic": "3643774c15de3f99b82fff125bc05b9af066f5c08b7861a117f0c05da79ba1c5", -"TT_test_msg_loaddevice.py::test_load_device_utf": "01ea492a17d567fb331c385d0c332a0b268d4dbb2b2e6fdbfb3ae03db8dc6fc8", +"TT_test_msg_loaddevice.py::test_load_device_1": "b08653f2009bdb9ed6f1e2a78a05e278c451a7c8eb41f6c8cb528efdaef4e3e0", +"TT_test_msg_loaddevice.py::test_load_device_2": "56375c448c4671170375981d66b9049dfaa1be31e6aa5d06a13efc6ec81f2b47", +"TT_test_msg_loaddevice.py::test_load_device_slip39_advanced": "b08653f2009bdb9ed6f1e2a78a05e278c451a7c8eb41f6c8cb528efdaef4e3e0", +"TT_test_msg_loaddevice.py::test_load_device_slip39_basic": "b08653f2009bdb9ed6f1e2a78a05e278c451a7c8eb41f6c8cb528efdaef4e3e0", +"TT_test_msg_loaddevice.py::test_load_device_utf": "5a6aefeb786bd065c9f6902da021568f3cf039f419a238ed9278c32c0bd17fe9", "TT_test_msg_ping.py::test_ping": "2b52cecd024fb6dc1a53b1a68a745e540161961f4e0f9460bb601c2ae2a80fe3", "TT_test_msg_sd_protect.py::test_enable_disable": "0f1bde609b1b5b7521d7560673ee8d287c3c8fe61982bcc6931282b20ad1cd7b", "TT_test_msg_sd_protect.py::test_refresh": "48ae62a7db380712cebdbb52672031eaf65de1eae404c2f4d146831c069269d9", -"TT_test_msg_sd_protect.py::test_wipe": "47c6292187ad5a81613c74ae5d7b9d5483be59269a13a5b94078286057b9c58d", -"TT_test_msg_wipedevice.py::test_autolock_not_retained": "208f3d181dc0753496e5241931dafd1bca62942a37856cfbef48ddbbd31ddebf", -"TT_test_msg_wipedevice.py::test_wipe_device": "4c7862bedbc9de710152f7b7ab15256ac9885f3c9f8082e6d04125acca6a71d7", +"TT_test_msg_sd_protect.py::test_wipe": "9e2cbe6146e1d06c4ca6f52a815a34dadbe2798874dc52dc7fa500db6b0cf66a", +"TT_test_msg_wipedevice.py::test_autolock_not_retained": "51afe39ffb390df772c7032fb71f2ab6e06e6098c19e90a00f4d6b7ced3c3dca", +"TT_test_msg_wipedevice.py::test_wipe_device": "6f8d7ba4ef109b2c20d09689d1fa329e23d5a11d21ce47a91339aeb21ffcf1ad", "TT_test_passphrase_slip39_advanced.py::test_128bit_passphrase": "1bd915d8dd1b00fcf945b46053ad73f6471f29c5af7fc437090c755e176810c3", "TT_test_passphrase_slip39_advanced.py::test_256bit_passphrase": "1bd915d8dd1b00fcf945b46053ad73f6471f29c5af7fc437090c755e176810c3", "TT_test_passphrase_slip39_basic.py::test_2of5_passphrase": "a4c20f8079bb5ae1c7a043317e7db798e630135d679b27f2c8e45d3928c7f8c1", @@ -1842,7 +3029,7 @@ "TT_test_protection_levels.py::test_signtx": "ae6bf89d19d71296b4e834cefb9eeef3d03afbee274222a5bd42898c0533a219", "TT_test_protection_levels.py::test_unlocked": "853d15981e03a33f65efe884810715a27d3418f11bb7771ad0f2f6138d8ad74f", "TT_test_protection_levels.py::test_verify_message_t2": "aae290fcc35d5b6f039bcb562866d8a9d39ab217553735563b603799acc6cb54", -"TT_test_protection_levels.py::test_wipe_device": "af075bdd0d287154aeee0aa6f8a1066be24f2aab8a6f5f4ab7e1bdd26af380e9", +"TT_test_protection_levels.py::test_wipe_device": "08b412a9d9c29287c7aa76cf80501b22bde79f270d1e9df40385845d670f8839", "TT_test_sdcard.py::test_sd_format": "45731c43d45fd5d86cdc462a1dbbde1ad83a02fdb5abffa2e8bf54af2f0cd1ee", "TT_test_sdcard.py::test_sd_no_format": "3590feffa9193c304105fba0afae84bc93e54f1a4706490bd63b9d56a202a1a7", "TT_test_sdcard.py::test_sd_protect_unlock": "4e744c259c67aada41c7b0762fd76bf9bcf6b15ca8d5ba1f7fbc5bdc48cec1ea", @@ -1893,14 +3080,14 @@ "TT_zcash-test_sign_tx.py::test_version_group_id_missing": "80a6e289138a604cf351a29511cf6f85e2243591317894703152787e1351a1a3" }, "persistence_tests": { -"TT_test_safety_checks.py::test_safety_checks_level_after_reboot[SafetyCheckLevel.PromptAlways--081810a6": "d4a6d7692b341f8c2cfc9aea6b842f5410f0a53174944085c42e894daa270fef", -"TT_test_safety_checks.py::test_safety_checks_level_after_reboot[SafetyCheckLevel.PromptTempora-b3d21f4a": "6bb0fd358d92a42b1945c2488a954f1cadc4d84f20e93f587cb8eb8ff69ee9b3", -"TT_test_safety_checks.py::test_safety_checks_level_after_reboot[SafetyCheckLevel.Strict-Safety-f1ff9c26": "8ed84fa3ffad4f6c3432780ad5553e3672f9097d327a32dee714579eece05d20", -"TT_test_shamir_persistence.py::test_abort": "01ffa622b60df60bac7a7c3d7b89c0c9991cd92c98c7f6fecc4968547074e42a", -"TT_test_shamir_persistence.py::test_recovery_multiple_resets": "18cb949c09521d33e5c75bed30989ce458060b76b857043dce20a0362168596e", -"TT_test_shamir_persistence.py::test_recovery_on_old_wallet": "9b1f56ff82f9e6fbe9a1ec92e812dcf3cf06382a0123e4bb4d2831f72f965913", -"TT_test_shamir_persistence.py::test_recovery_single_reset": "b486ae0ebbab7a29b334b46f1fbe45f2f1364ad0b84bce9799d9b7037baa8ad0", -"TT_test_wipe_code.py::test_wipe_code_activate_core": "820d443b371a32dd7e108bc52df0b3c3e4a3a6c1a896ba2f75e6c46774281b3d" +"TT_test_safety_checks.py::test_safety_checks_level_after_reboot[SafetyCheckLevel.PromptAlways--081810a6": "4d7a97dc30aaae84f79e9f65eba244a31025c70882c0bdcdcbdbfceba112b935", +"TT_test_safety_checks.py::test_safety_checks_level_after_reboot[SafetyCheckLevel.PromptTempora-b3d21f4a": "eba2090e67f8f61b16ca780efa9bcd6de7ac872164894a74003e8d16ae64bd9b", +"TT_test_safety_checks.py::test_safety_checks_level_after_reboot[SafetyCheckLevel.Strict-Safety-f1ff9c26": "56e98311369965bb64f9674db6b2ea1fb54abb40c46d7fbf9e5292c7f74f16da", +"TT_test_shamir_persistence.py::test_abort": "42e18cc0d537cc7c1ea458de440eeff146fb30056a59ed4948a012f6a3a086d9", +"TT_test_shamir_persistence.py::test_recovery_multiple_resets": "8a420ecc0a6d126d70e092f0e9afb4a7439ac80702518ac92016c15440a4e1a0", +"TT_test_shamir_persistence.py::test_recovery_on_old_wallet": "6b149437bf8c5925fe50cebc6001b9478b37d8a717b32455c8ebfe0b6ae0f40e", +"TT_test_shamir_persistence.py::test_recovery_single_reset": "6862ef25c4d83a888019dd6d88991c590b465885ff7970e3f6fd6b50cf8d9e1e", +"TT_test_wipe_code.py::test_wipe_code_activate_core": "531da0a0e4b79b0fca6c7bc1e87276aad4d8b7abc6d5b361cf8be15493619894" } } } diff --git a/tests/upgrade_tests/test_firmware_upgrades.py b/tests/upgrade_tests/test_firmware_upgrades.py index 89413eeb7..ec7d46681 100644 --- a/tests/upgrade_tests/test_firmware_upgrades.py +++ b/tests/upgrade_tests/test_firmware_upgrades.py @@ -310,7 +310,7 @@ def test_upgrade_shamir_recovery(gen: str, tag: Optional[str]): device_handler.run(device.recover, pin_protection=False) recovery.confirm_recovery(debug) - recovery.select_number_of_words(debug) + recovery.select_number_of_words(debug, wait=not debug.legacy_debug) layout = recovery.enter_share(debug, MNEMONIC_SLIP39_BASIC_20_3of6[0]) if not debug.legacy_ui and not debug.legacy_debug: assert "2 more shares" in layout.text_content() diff --git a/tools/snippets/font_multiplier.py b/tools/snippets/font_multiplier.py new file mode 100644 index 000000000..4aba4ccc7 --- /dev/null +++ b/tools/snippets/font_multiplier.py @@ -0,0 +1,153 @@ +# Helpers to increase the font size from existing font data. +# Thanks to it we can save space by not having to include +# the larger font definitions. + +from __future__ import annotations + +from typing_extensions import Literal +from typing import Tuple + + +Bit = Literal[0, 1] +Point = Tuple[int, int] + + +def magnify_glyph_by_two(width: int, height: int, bytes_data: list[int]) -> list[int]: + """Magnifying the font size by two. + + Input and output are bytes (that is the standard in font files), + but internally works with bits. + """ + bits_data = _bytes_to_bits(bytes_data, height * width) + double_size_data = _double_the_bits(width, bits_data) + return _bits_to_bytes(double_size_data) + + +def _bytes_to_bits(bytes_data: list[int], bits_to_take: int) -> list[Bit]: + """Transform bytes into bits.""" + bits_data = [f"{i:08b}" for i in bytes_data[:-1]] + + # Last element needs to be handled carefully, + # to respect the number of bits to take. + missing_bits = bits_to_take - len(bits_data) * 8 + last_byte = bytes_data[-1] + last_binary = f"{last_byte:08b}" + # Taking either the right or left part of the last byte + if last_byte < 2**missing_bits: + bits_data.append(last_binary[-missing_bits:]) + else: + bits_data.append(last_binary[:missing_bits]) + + return [1 if int(x) else 0 for x in "".join(bits_data)] + + +def _bits_to_bytes(bits_data: list[Bit]) -> list[int]: + """Transform bits into bytes.""" + bits_str = "".join([str(bit) for bit in bits_data]) + bytes_str_list = [bits_str[i : i + 8] for i in range(0, len(bits_str), 8)] + # Last element needs to be right-padded to 8 bits + while len(bytes_str_list[-1]) != 8: + bytes_str_list[-1] += "0" + return [int(byte, 2) for byte in bytes_str_list] + + +def _double_the_bits(width: int, bits_data: list[Bit]) -> list[Bit]: + """Double the dimension of a given glyph.""" + # Allocate space for the new data - 2*2 bigger than the original + # Then fill all the new indexes with appropriate bits + double_size_data: list[Bit] = [0 for _ in range(4 * len(bits_data))] + for original_index, bit in enumerate(bits_data): + for new_index in _corresponding_indexes(original_index, width): + double_size_data[new_index] = bit + return double_size_data + + +def _corresponding_indexes(index: int, width: int) -> list[int]: + """Find the indexes of the four pixels that correspond to the given one.""" + point = _index_to_point(index, width) + points = _scale_by_two(point) + new_width = 2 * width + return [_point_to_index(new_point, new_width) for new_point in points] + + +def _scale_by_two(point: Point) -> tuple[Point, Point, Point, Point]: + """Translate one pixel into four adjacent pixels to visually scale it by two.""" + x, y = point + return ( + (x * 2, y * 2), + (x * 2 + 1, y * 2), + (x * 2, y * 2 + 1), + (x * 2 + 1, y * 2 + 1), + ) + + +def _point_to_index(point: Point, width: int) -> int: + """Translate point to index according to the glyph width.""" + x, y = point + assert 0 <= x < width + return y * width + x + + +def _index_to_point(index: int, width: int) -> Point: + """Translate index to point according to the glyph width.""" + x = index % width + y = index // width + return x, y + + +def print_from_bits(width: int, height: int, bit_data: list[Bit]): + """Print the glyph into terminal from bit data.""" + for line_id in range(height): + line = bit_data[line_id * width : (line_id + 1) * width] + str_line = "".join([str(x) for x in line]) + print(str_line.replace("0", " ")) + print() + + +def print_from_bytes(width: int, height: int, bytes_data: list[int]): + """Print the glyph into terminal from byte data.""" + bits_data = _bytes_to_bits(bytes_data, height * width) + print_from_bits(width, height, bits_data) + + +################################ +# TEST SECTION for pytest +################################ + +# fmt: off +HEIGHT = 7 +K_WIDTH = 5 +K_GLYPH = [140, 169, 138, 74, 32] +K_BITS = [1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1] +K_MAGNIFIED = [192, 240, 60, 51, 12, 204, 51, 15, 3, 192, 204, 51, 12, 51, 12, 192, 240, 48] +M_GLYPH = [131, 7, 29, 89, 48, 96, 128] +M_WIDTH = 7 +# fmt: on + + +def test_bits_to_bytes_and_back(): + vectors = ( # height, width, bytes_data + (HEIGHT, K_WIDTH, K_GLYPH), + (HEIGHT, M_WIDTH, M_GLYPH), + ) + + for height, width, bytes_data in vectors: + bits_data = _bytes_to_bits(bytes_data, height * width) + assert _bits_to_bytes(bits_data) == bytes_data + + +def test_bit_to_bytes(): + assert _bytes_to_bits(K_GLYPH, HEIGHT * K_WIDTH) == K_BITS + + +def test_overall_magnify(): + assert magnify_glyph_by_two(K_WIDTH, HEIGHT, K_GLYPH) == K_MAGNIFIED + + +if __name__ == "__main__": + # Print letter K, magnify it and print it also + print("K_GLYPH", K_GLYPH) + print_from_bytes(K_WIDTH, HEIGHT, K_GLYPH) + magnified_data = magnify_glyph_by_two(K_WIDTH, HEIGHT, K_GLYPH) + print("magnified_data", magnified_data) + print_from_bytes(2 * K_WIDTH, 2 * HEIGHT, magnified_data)