mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-09-14 04:45:08 +00:00
Merge branch 'dev' into version_screen_with_git_info
This commit is contained in:
@@ -1,8 +1,33 @@
|
||||
## Description
|
||||
|
||||
_Describe the change simply. Provide a reason for the change._
|
||||
### Problem or Issue being addressed
|
||||
|
||||
_Include screenshots of any new or modified screens (or at least explain why they were omitted)_
|
||||
<!--
|
||||
Describe the problem this PR solves.
|
||||
Include background context, links to relevant issues, BIPs, discussions, etc.
|
||||
-->
|
||||
|
||||
### Solution
|
||||
|
||||
<!--
|
||||
Describe your approach and key technical implementation details.
|
||||
Explain why this approach was chosen.
|
||||
-->
|
||||
|
||||
### Additional Information
|
||||
|
||||
<!--
|
||||
Tradeoffs, follow-ups, limitations, or anything reviewers should be aware of.
|
||||
-->
|
||||
|
||||
### Screenshots
|
||||
|
||||
<!--
|
||||
Include screenshots for any new or modified screens.
|
||||
If omitted, explain why.
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
This pull request is categorized as a:
|
||||
|
||||
@@ -12,21 +37,51 @@ This pull request is categorized as a:
|
||||
- [ ] Documentation
|
||||
- [ ] Other
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] I’ve run `pytest` and made sure all unit tests pass before submitting the PR
|
||||
|
||||
If you modified or added functionality/workflow, did you add new unit tests?
|
||||
|
||||
- [ ] No, I’m a fool
|
||||
- [ ] Yes
|
||||
#### I ran `pytest` locally
|
||||
- [ ] All tests passed before submitting the PR
|
||||
- [ ] I couldn't run the tests
|
||||
- [ ] N/A
|
||||
|
||||
I have tested this PR on the following platforms/os:
|
||||
---
|
||||
|
||||
- [ ] Raspberry Pi OS [Manual Build](https://github.com/SeedSigner/seedsigner/blob/dev/docs/manual_installation.md)
|
||||
#### I included screenshots of any new or modified screens
|
||||
|
||||
Should be part of the PR description above.
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
- [ ] N/A
|
||||
|
||||
---
|
||||
|
||||
#### I added or updated tests
|
||||
|
||||
Any new or altered functionality should be covered in a unit test. Any new or updated sequences require FlowTests.
|
||||
- [ ] Yes
|
||||
- [ ] No, I’m a fool
|
||||
- [ ] N/A
|
||||
|
||||
---
|
||||
|
||||
#### I tested this PR hands-on on the following platform(s):
|
||||
- [ ] Raspberry Pi OS [Manual Build](https://github.com/SeedSigner/seedsigner/blob/dev/docs/raspberry_pi_os_build_instructions.md)
|
||||
- [ ] [SeedSigner OS](https://github.com/SeedSigner/seedsigner-os) on a Pi0/Pi0W board
|
||||
- [ ] Other
|
||||
- [ ] Emulator
|
||||
|
||||
---
|
||||
|
||||
Note: Keep your changes limited in scope; if you uncover other issues or improvements along the way, ideally submit those as a separate PR. The more complicated the PR the harder to review, test, and merge.
|
||||
#### I have reviewed these notes:
|
||||
* Keep your changes limited in scope.
|
||||
* If you uncover other issues or improvements along the way, ideally submit those as a separate PR.
|
||||
* The more complicated the PR, the harder it is to review, test, and merge.
|
||||
* We appreciate your efforts, but we're a small team of volunteers so PR review can be a very slow process.
|
||||
* Please only "@" mention a contributor if their input is truly needed to enable further progress.
|
||||
|
||||
- [ ] I understand
|
||||
|
||||
---
|
||||
|
||||
Thank you! Please join our [Devs' Telegram group](https://t.me/seedsigner_new_devs) to get more involved.
|
||||
@@ -16,6 +16,18 @@ on:
|
||||
default: main
|
||||
required: true
|
||||
|
||||
# Explicitly restrict the auto-provisioned GITHUB_TOKEN to the least privilege required
|
||||
# for this workflow.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# Cancel superseded runs on the same branch/PR. Intermediate builds on dev/main
|
||||
# have no downstream consumers (official releases are built offline), so only
|
||||
# the latest commit per ref needs to be built.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# Increment this number as part of a PR to trigger an image build for the PR
|
||||
# trigger = 0
|
||||
|
||||
@@ -23,6 +35,7 @@ jobs:
|
||||
build:
|
||||
name: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
# Prevent resource consuming cron triggered runs in forks
|
||||
if: (!github.event.repository.fork || github.event_name == 'workflow_dispatch')
|
||||
strategy:
|
||||
@@ -71,6 +84,9 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: delete unnecessary files
|
||||
# The seedsigner-os buildroot overlay only needs src/; drop everything
|
||||
# else from the seedsigner checkout (tests, docs, tooling, .git, etc.)
|
||||
# to keep the overlay minimal.
|
||||
run: |
|
||||
cd seedsigner-os/opt/rootfs-overlay/opt
|
||||
find . -mindepth 1 -maxdepth 1 ! -name src -exec rm -rf {} +
|
||||
@@ -151,10 +167,7 @@ jobs:
|
||||
ls -lRa images
|
||||
|
||||
- name: get seedsigner latest commit hash
|
||||
id: get-seedsigner-hash
|
||||
run: |
|
||||
git init
|
||||
echo "source_hash=$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_ENV
|
||||
run: echo "source_hash=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
|
||||
- name: write sha256sum
|
||||
run: |
|
||||
|
||||
@@ -4,6 +4,11 @@ on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
# Explicitly restrict the auto-provisioned GITHUB_TOKEN to the least privilege required
|
||||
# for this workflow.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
# Concurrency group that uses the workflow name and PR number if available
|
||||
# or commit SHA as a fallback. If a new build is triggered under that
|
||||
@@ -24,6 +29,7 @@ env:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
matrix:
|
||||
# 3.10: currently used by Seedsigner
|
||||
@@ -39,19 +45,24 @@ jobs:
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: "pip"
|
||||
cache-dependency-path: |
|
||||
requirements.txt
|
||||
tests/requirements.txt
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get install libzbar0
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt -r tests/requirements.txt
|
||||
pip install .
|
||||
pip install -r requirements.txt -r tests/requirements.txt -r l10n/requirements-l10n.txt
|
||||
pip install -e .
|
||||
- name: Compile translations
|
||||
run: python setup.py compile_catalog
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
mkdir artifacts
|
||||
mkdir -p artifacts
|
||||
python -m pytest \
|
||||
--color=yes \
|
||||
--cov=seedsigner \
|
||||
--cov-append \
|
||||
--cov-branch \
|
||||
--durations 5 \
|
||||
-vv
|
||||
@@ -74,5 +85,5 @@ jobs:
|
||||
path: artifacts/**
|
||||
retention-days: 10
|
||||
# Upload also when tests fail. The workflow result (red/green) will
|
||||
# be not effected by this.
|
||||
# not be affected by this.
|
||||
if: always()
|
||||
|
||||
@@ -112,23 +112,23 @@ Instructions to build a SeedSigner OS image (using precisely the same process th
|
||||
## Downloading the Software
|
||||
|
||||
|
||||
Download the current Version (0.8.6) software image that is compatible with your Raspberry Pi Hardware. The Pi Zero 1.3 is the most common and recommended board.
|
||||
Download the current Version (0.8.7) software image that is compatible with your Raspberry Pi Hardware. The Pi Zero 1.3 is the most common and recommended board.
|
||||
| Board | Download Image Link/Name |
|
||||
| --------------------- | --------------------------------- |
|
||||
|**[Raspberry Pi Zero 1.3](https://www.raspberrypi.com/products/raspberry-pi-zero/)** |[`seedsigner_os.0.8.6.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.6/seedsigner_os.0.8.6.pi0.img) |
|
||||
|[Raspberry Pi Zero W](https://www.raspberrypi.com/products/raspberry-pi-zero-w/) |[`seedsigner_os.0.8.6.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.6/seedsigner_os.0.8.6.pi0.img) |
|
||||
|[Raspberry Pi Zero 2 W](https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/) |[`seedsigner_os.0.8.6.pi02w.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.6/seedsigner_os.0.8.6.pi02w.img) |
|
||||
|[Raspberry Pi 1 Model B/B+](https://www.raspberrypi.com/products/raspberry-pi-1-model-b-plus/) |[`seedsigner_os.0.8.6.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.6/seedsigner_os.0.8.6.pi0.img) |
|
||||
|[Raspberry Pi 2 Model B](https://www.raspberrypi.com/products/raspberry-pi-2-model-b/) |[`seedsigner_os.0.8.6.pi2.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.6/seedsigner_os.0.8.6.pi2.img) |
|
||||
|[Raspberry Pi 3 Model B](https://www.raspberrypi.com/products/raspberry-pi-3-model-b/) |[`seedsigner_os.0.8.6.pi02w.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.6/seedsigner_os.0.8.6.pi02w.img) |
|
||||
|[Raspberry Pi 4 Model B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/) |[`seedsigner_os.0.8.6.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.6/seedsigner_os.0.8.6.pi4.img) |
|
||||
|[Raspberry Pi 400](https://www.raspberrypi.com/products/raspberry-pi-400-unit/) |[`seedsigner_os.0.8.6.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.6/seedsigner_os.0.8.6.pi4.img) |
|
||||
|**[Raspberry Pi Zero 1.3](https://www.raspberrypi.com/products/raspberry-pi-zero/)** |[`seedsigner_os.0.8.7.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.7/seedsigner_os.0.8.7.pi0.img) |
|
||||
|[Raspberry Pi Zero W](https://www.raspberrypi.com/products/raspberry-pi-zero-w/) |[`seedsigner_os.0.8.7.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.7/seedsigner_os.0.8.7.pi0.img) |
|
||||
|[Raspberry Pi Zero 2 W](https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/) |[`seedsigner_os.0.8.7.pi02w.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.7/seedsigner_os.0.8.7.pi02w.img) |
|
||||
|[Raspberry Pi 1 Model B/B+](https://www.raspberrypi.com/products/raspberry-pi-1-model-b-plus/) |[`seedsigner_os.0.8.7.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.7/seedsigner_os.0.8.7.pi0.img) |
|
||||
|[Raspberry Pi 2 Model B](https://www.raspberrypi.com/products/raspberry-pi-2-model-b/) |[`seedsigner_os.0.8.7.pi2.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.7/seedsigner_os.0.8.7.pi2.img) |
|
||||
|[Raspberry Pi 3 Model B](https://www.raspberrypi.com/products/raspberry-pi-3-model-b/) |[`seedsigner_os.0.8.7.pi02w.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.7/seedsigner_os.0.8.7.pi02w.img) |
|
||||
|[Raspberry Pi 4 Model B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/) |[`seedsigner_os.0.8.7.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.7/seedsigner_os.0.8.7.pi4.img) |
|
||||
|[Raspberry Pi 400](https://www.raspberrypi.com/products/raspberry-pi-400-unit/) |[`seedsigner_os.0.8.7.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.7/seedsigner_os.0.8.7.pi4.img) |
|
||||
|
||||
Note: If you have physically removed the WiFi component from your board, you will still use the image file of the original (un-modified) hardware. (Our files are compiled/based on the *processor* architecture). Although it is better to spend a few minutes upfront to determine which specific Pi hardware/model you have, if you are still unsure which hardware you have, you can try using the pi0.img file. Making an incorrect choice here will not ruin your board, because this is software, not firmware.
|
||||
|
||||
**Also download** these 2 signature verification files to the same folder
|
||||
[The Plaintext manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.8.6/seedsigner.0.8.6.sha256.txt)
|
||||
[The Signature of the manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.8.6/seedsigner.0.8.6.sha256.txt.sig)
|
||||
[The Plaintext manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.8.7/seedsigner.0.8.7.sha256.txt)
|
||||
[The Signature of the manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.8.7/seedsigner.0.8.7.sha256.txt.sig)
|
||||
|
||||
|
||||
Users familiar with older versions of the SeedSigner software might be surprised with how fast their software downloads now are, because since version 0.6.0 the software image files are now 100x smaller! Each image file is now under 42 Megabytes so your downloads and verifications will be very quick now (and might even seem *too* quick)!
|
||||
@@ -160,7 +160,7 @@ The result should confirm that 1 key was *either* imported or updated. *Ignore*
|
||||
|
||||
Next, you will run the *verify* command on the signature (.sig) file. (*Verify* must be run from inside the same folder that you downloaded the files into earlier.)
|
||||
```
|
||||
gpg --verify seedsigner.0.8.6.sha256.txt.sig
|
||||
gpg --verify seedsigner.0.8.7.sha256.txt.sig
|
||||
```
|
||||
|
||||
When the verify command completes successfully, it should display output like this:
|
||||
@@ -230,22 +230,22 @@ Now that you have confirmed that you do have the real SeedSigner Project's Publi
|
||||
|
||||
**On Linux or OSX:** Run this command
|
||||
```
|
||||
shasum -a 256 --ignore-missing --check seedsigner.0.8.6.sha256.txt
|
||||
shasum -a 256 --ignore-missing --check seedsigner.0.8.7.sha256.txt
|
||||
```
|
||||
Note: macOS versions earlier than v11 (Big Sur) do not support the `--ignore-missing` flag. You can omit it and disregard any missing file warnings.
|
||||
|
||||
**On Windows (inside Powershell):** Run this command
|
||||
```
|
||||
CertUtil -hashfile seedsigner_os.0.8.6.Insert_Your_Pi_Models_binary_here_For_Example_pi02w.img SHA256
|
||||
CertUtil -hashfile seedsigner_os.0.8.7.Insert_Your_Pi_Models_binary_here_For_Example_pi02w.img SHA256
|
||||
```
|
||||
On Windows, you must then manually compare the resulting file hash value to the corresponding hash value shown inside the .SHA256 cleartext file.
|
||||
<BR>
|
||||
|
||||
Wait up to 30 seconds for the command to complete, and it should display:
|
||||
```
|
||||
seedsigner_os.0.8.6.[Your_Pi_Model_For_Example:pi02w].img: OK
|
||||
seedsigner_os.0.8.7.[Your_Pi_Model_For_Example:pi02w].img: OK
|
||||
```
|
||||
**If you receive the "OK" message** for your **seedsigner_os.0.8.6.[Your_Pi_Model_For_Example:pi02w].img file**, as shown above, then your verification is fully complete!
|
||||
**If you receive the "OK" message** for your **seedsigner_os.0.8.7.[Your_Pi_Model_For_Example:pi02w].img file**, as shown above, then your verification is fully complete!
|
||||
**All of your downloaded files have now been confirmed as both authentic and unaltered!** You can proceed to create/write your MicroSD card😄😄 !!
|
||||
|
||||
If your file result shows "FAILED", then you must stop here immediately. Do not continue. Contact us for assistance at the Telegram group address above.
|
||||
|
||||
+35
-7
@@ -139,19 +139,47 @@ see: https://www.circuitbasics.com/raspberry-pi-zero-ethernet-gadget/
|
||||
|
||||
|
||||
### Linux
|
||||
To enable IP forwarding on your Host:
|
||||
```bash
|
||||
nano /etc/sysctl.conf
|
||||
```
|
||||
|
||||
and uncomment the following line:
|
||||
To enable internet sharing on your Linux host:
|
||||
|
||||
#### Step 1: Enable IP Forwarding
|
||||
```bash
|
||||
# Check current IP forwarding status
|
||||
cat /proc/sys/net/ipv4/ip_forward
|
||||
|
||||
# Enable IP forwarding temporarily
|
||||
sudo sh -c "echo 1 > /proc/sys/net/ipv4/ip_forward"
|
||||
|
||||
# Enable permanently
|
||||
sudo nano /etc/sysctl.conf
|
||||
```
|
||||
Uncomment or add:
|
||||
```
|
||||
net.ipv4.ip_forward = 1
|
||||
```
|
||||
Save and apply:
|
||||
```bash
|
||||
sudo sysctl -p
|
||||
```
|
||||
|
||||
Then run `sudo sysctl -p` to have immediate effect.
|
||||
#### Step 2: Configure Network Sharing
|
||||
```bash
|
||||
# Assuming eth0 is your internet-connected interface and usb0 is the USB interface
|
||||
sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
sudo iptables -A FORWARD -i usb0 -o eth0 -j ACCEPT
|
||||
sudo iptables -A FORWARD -i eth0 -o usb0 -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
|
||||
You should now be able to SSH into your internet connected Pi Zero 1.3, as described in the ["Get started"](#get-started) section.
|
||||
# Save the rules
|
||||
sudo apt install iptables-persistent
|
||||
sudo netfilter-persistent save
|
||||
sudo netfilter-persistent reload
|
||||
```
|
||||
|
||||
#### Step 3: Test the Connection
|
||||
```bash
|
||||
ping 8.8.8.8
|
||||
```
|
||||
If you see responses, the Pi has internet access.
|
||||
|
||||
|
||||
## Set a static IP on Linux
|
||||
|
||||
+125
-68
@@ -1,14 +1,14 @@
|
||||
# Translations template for seedsigner.
|
||||
# Copyright (C) 2025 ORGANIZATION
|
||||
# Copyright (C) 2026 ORGANIZATION
|
||||
# This file is distributed under the same license as the seedsigner project.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2026.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: seedsigner 0.8.6\n"
|
||||
"Project-Id-Version: seedsigner 0.8.7\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-11-21 14:39-0600\n"
|
||||
"POT-Creation-Date: 2026-03-05 22:02+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -141,19 +141,21 @@ msgstr[1] ""
|
||||
msgid "{} change"
|
||||
msgstr ""
|
||||
|
||||
#. Describes the address type (change or receive)
|
||||
#: src/seedsigner/gui/screens/psbt_screens.py
|
||||
#: src/seedsigner/models/settings_definition.py
|
||||
#: src/seedsigner/views/seed_views.py
|
||||
msgid "Multisig"
|
||||
#: src/seedsigner/gui/screens/seed_screens.py
|
||||
msgid "change address"
|
||||
msgstr ""
|
||||
|
||||
#. Describes the address type (change or receive)
|
||||
#: src/seedsigner/gui/screens/psbt_screens.py
|
||||
msgid "Change"
|
||||
#: src/seedsigner/gui/screens/seed_screens.py
|
||||
msgid "receive address"
|
||||
msgstr ""
|
||||
|
||||
#. Abbreviation for receive address
|
||||
#. Symbol for index number, e.g. "address #3"
|
||||
#: src/seedsigner/gui/screens/psbt_screens.py
|
||||
msgid "Addr"
|
||||
msgid "#"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/gui/screens/psbt_screens.py
|
||||
@@ -237,14 +239,6 @@ msgid ""
|
||||
"All in-memory data will be wiped."
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/gui/screens/screen.py
|
||||
msgid "Powering Off"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/gui/screens/screen.py
|
||||
msgid "Please wait about 30 seconds before disconnecting power."
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/gui/screens/screen.py
|
||||
msgid "Just Unplug It"
|
||||
msgstr ""
|
||||
@@ -264,17 +258,6 @@ msgstr ""
|
||||
msgid "fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/gui/screens/seed_screens.py
|
||||
#: src/seedsigner/views/seed_views.py
|
||||
msgid "Backup Seed"
|
||||
msgstr ""
|
||||
|
||||
#. Additional explainer for the two seed backup options (mnemonic phrase and
|
||||
#. SeedQR).
|
||||
#: src/seedsigner/gui/screens/seed_screens.py
|
||||
msgid "Backups do not include your passphrase."
|
||||
msgstr ""
|
||||
|
||||
#. Displays the page number and total: (e.g. page 1 of 6)
|
||||
#: src/seedsigner/gui/screens/seed_screens.py
|
||||
msgid "Seed Words: {}/{}"
|
||||
@@ -392,16 +375,6 @@ msgstr ""
|
||||
msgid "Address Verified"
|
||||
msgstr ""
|
||||
|
||||
#. Describes the address type (change or receive)
|
||||
#: src/seedsigner/gui/screens/seed_screens.py
|
||||
msgid "change address"
|
||||
msgstr ""
|
||||
|
||||
#. Describes the address type (change or receive)
|
||||
#: src/seedsigner/gui/screens/seed_screens.py
|
||||
msgid "receive address"
|
||||
msgstr ""
|
||||
|
||||
#. Describes the address index (e.g. "index 7")
|
||||
#: src/seedsigner/gui/screens/seed_screens.py
|
||||
msgid "index {}"
|
||||
@@ -613,6 +586,21 @@ msgstr ""
|
||||
msgid "Required"
|
||||
msgstr ""
|
||||
|
||||
#. QR code format option; "default" = this is the format most wallets use
|
||||
#: src/seedsigner/models/settings_definition.py
|
||||
msgid "Animated (default)"
|
||||
msgstr ""
|
||||
|
||||
#. QR code format option (static = single frame, not animated)
|
||||
#: src/seedsigner/models/settings_definition.py
|
||||
msgid "Static"
|
||||
msgstr ""
|
||||
|
||||
#. QR code format option: old format that Specter Desktop used to use
|
||||
#: src/seedsigner/models/settings_definition.py
|
||||
msgid "Specter legacy"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/models/settings_definition.py
|
||||
msgid "BTC"
|
||||
msgstr ""
|
||||
@@ -681,6 +669,11 @@ msgstr ""
|
||||
msgid "Single Sig"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/models/settings_definition.py
|
||||
#: src/seedsigner/views/seed_views.py
|
||||
msgid "Multisig"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/models/settings_definition.py
|
||||
msgid "Native Segwit"
|
||||
msgstr ""
|
||||
@@ -701,6 +694,17 @@ msgstr ""
|
||||
msgid "Custom Derivation"
|
||||
msgstr ""
|
||||
|
||||
#. MicroSD notification duration setting - Display notification for 5 seconds
|
||||
#: src/seedsigner/models/settings_definition.py
|
||||
msgid "5 seconds"
|
||||
msgstr ""
|
||||
|
||||
#. MicroSD notification duration setting - Display notification until the SD
|
||||
#. card is removed
|
||||
#: src/seedsigner/models/settings_definition.py
|
||||
msgid "Until SD removed"
|
||||
msgstr ""
|
||||
|
||||
#. Terminology used by Electrum seeds; equivalent to BIP-39 passphrase
|
||||
#: src/seedsigner/models/settings_definition.py
|
||||
msgid "Custom Extension"
|
||||
@@ -718,10 +722,6 @@ msgstr ""
|
||||
msgid "Persistent settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/models/settings_definition.py
|
||||
msgid "Coordinator software"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/models/settings_definition.py
|
||||
msgid "Denomination display"
|
||||
msgstr ""
|
||||
@@ -734,10 +734,6 @@ msgstr ""
|
||||
msgid "QR code density"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/models/settings_definition.py
|
||||
msgid "Xpub export"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/models/settings_definition.py
|
||||
msgid "Sig types"
|
||||
msgstr ""
|
||||
@@ -746,6 +742,10 @@ msgstr ""
|
||||
msgid "Script types"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/models/settings_definition.py
|
||||
msgid "Xpub QR format"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/models/settings_definition.py
|
||||
msgid "Show xpub details"
|
||||
msgstr ""
|
||||
@@ -774,6 +774,10 @@ msgstr ""
|
||||
msgid "Native Segwit only"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/models/settings_definition.py
|
||||
msgid "MicroSD notification duration"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/models/settings_definition.py
|
||||
msgid "Message signing"
|
||||
msgstr ""
|
||||
@@ -853,7 +857,7 @@ msgid ""
|
||||
"addresses."
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/psbt_views.py
|
||||
#: src/seedsigner/views/psbt_views.py src/seedsigner/views/view.py
|
||||
msgid "Continue"
|
||||
msgstr ""
|
||||
|
||||
@@ -898,7 +902,7 @@ msgid "Self-Transfer"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/psbt_views.py
|
||||
msgid "Verify Multisig Addr"
|
||||
msgid "Verify multisig addr"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/psbt_views.py
|
||||
@@ -936,7 +940,7 @@ msgid "Approve transaction"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/psbt_views.py
|
||||
msgid "Select diff seed"
|
||||
msgid "Select different seed"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/psbt_views.py
|
||||
@@ -1003,9 +1007,8 @@ msgstr ""
|
||||
msgid "QRCode is invalid or is a data format not yet supported."
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/scan_views.py src/seedsigner/views/seed_views.py
|
||||
#: src/seedsigner/views/view.py
|
||||
msgid "Done"
|
||||
#: src/seedsigner/views/scan_views.py src/seedsigner/views/view.py
|
||||
msgid "Back to Main Menu"
|
||||
msgstr ""
|
||||
|
||||
#. This is on the opening splash screen, displayed above the HRF logo
|
||||
@@ -1050,7 +1053,7 @@ msgid "Create a seed"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/seed_views.py
|
||||
msgid "Load A Seed"
|
||||
msgid "Load a Seed"
|
||||
msgstr ""
|
||||
|
||||
#. Inserts the word number (e.g. "Seed Word #6")
|
||||
@@ -1074,6 +1077,10 @@ msgstr ""
|
||||
msgid "Checksum failure; not a valid seed phrase."
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/seed_views.py
|
||||
msgid "Done"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/seed_views.py
|
||||
msgid "Edit passphrase"
|
||||
msgstr ""
|
||||
@@ -1116,7 +1123,7 @@ msgid "Discard Seed?"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/seed_views.py
|
||||
msgid "Electrum warning"
|
||||
msgid "Electrum Warning"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/seed_views.py
|
||||
@@ -1127,10 +1134,6 @@ msgstr ""
|
||||
msgid "Scan transaction"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/seed_views.py
|
||||
msgid "Verify addr"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py
|
||||
msgid "Address explorer"
|
||||
msgstr ""
|
||||
@@ -1155,10 +1158,18 @@ msgstr ""
|
||||
msgid "Export as SeedQR"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/seed_views.py
|
||||
msgid "Backup Seed"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/seed_views.py
|
||||
msgid "Export Xpub"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/seed_views.py
|
||||
msgid "Xpub QR Format"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/seed_views.py
|
||||
msgid "Xpub can be used to view all future transactions."
|
||||
msgstr ""
|
||||
@@ -1322,11 +1333,16 @@ msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/seed_views.py
|
||||
msgid "Can't validate a single sig addr without specifying a seed"
|
||||
msgid "Return to transaction"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/seed_views.py
|
||||
msgid "Return to transaction"
|
||||
msgid "Verify addr"
|
||||
msgstr ""
|
||||
|
||||
#. Multisig policy. For a "2 of 3" policy, "threshold" = 2; "n" = 3
|
||||
#: src/seedsigner/views/seed_views.py
|
||||
msgid "{threshold} of {n}"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/seed_views.py
|
||||
@@ -1349,6 +1365,24 @@ msgstr ""
|
||||
msgid "Dev Options"
|
||||
msgstr ""
|
||||
|
||||
#. Title of a warning dialog when configuring a setting that requires at least
|
||||
#. one option to be selected.
|
||||
#: src/seedsigner/views/settings_views.py
|
||||
msgid "Selection Required"
|
||||
msgstr ""
|
||||
|
||||
#. The name of the setting being configured (e.g. "Script types") will be
|
||||
#. inserted.
|
||||
#: src/seedsigner/views/settings_views.py
|
||||
msgid "At least one option must be selected for \"{}\"."
|
||||
msgstr ""
|
||||
|
||||
#. Text for the button that returns the user to the setting configuration
|
||||
#. screen.
|
||||
#: src/seedsigner/views/settings_views.py
|
||||
msgid "Return to setting"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/settings_views.py
|
||||
msgid "Persistent Settings enabled. Settings saved to SD card."
|
||||
msgstr ""
|
||||
@@ -1382,7 +1416,7 @@ msgid "24 words"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/tools_views.py
|
||||
msgid "Mnemonic Length?"
|
||||
msgid "Mnemonic Length"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/tools_views.py
|
||||
@@ -1399,10 +1433,6 @@ msgstr ""
|
||||
msgid "24 words ({} rolls)"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/tools_views.py
|
||||
msgid "Mnemonic Length"
|
||||
msgstr ""
|
||||
|
||||
#. Label to gather entropy through coin tosses
|
||||
#: src/seedsigner/views/tools_views.py
|
||||
msgid "Coin flip entropy"
|
||||
@@ -1441,6 +1471,11 @@ msgstr ""
|
||||
msgid "Change addresses"
|
||||
msgstr ""
|
||||
|
||||
#. Multisig policy. For a "2 / 3 multisig" policy, "threshold" = 2; "n" = 3
|
||||
#: src/seedsigner/views/tools_views.py
|
||||
msgid "{threshold} / {n} multisig"
|
||||
msgstr ""
|
||||
|
||||
#. a status message that our payment addresses are being calculated
|
||||
#: src/seedsigner/views/tools_views.py
|
||||
msgid "Calculating addrs..."
|
||||
@@ -1509,15 +1544,27 @@ msgstr ""
|
||||
msgid "Change Setting"
|
||||
msgstr ""
|
||||
|
||||
#. Inserts mainnet/testnet/regtest and derivation path
|
||||
#. "network" will be mainnet/testnet/regtest.
|
||||
#: src/seedsigner/views/view.py
|
||||
msgid "Current network setting ({}) doesn't match {}."
|
||||
msgid "Current network setting ({network}) doesn't match {derivation_path}."
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/view.py
|
||||
msgid "System Error"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/view.py
|
||||
msgid "Hardware Error"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/view.py
|
||||
msgid "Cannot access camera"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/view.py
|
||||
msgid "Disconnect power and check for a loose camera connection."
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/view.py
|
||||
msgid "Update setting"
|
||||
msgstr ""
|
||||
@@ -1532,3 +1579,13 @@ msgstr ""
|
||||
msgid "Option Disabled"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/view.py
|
||||
msgid "Action Required"
|
||||
msgstr ""
|
||||
|
||||
#: src/seedsigner/views/view.py
|
||||
msgid ""
|
||||
"You must remove the\n"
|
||||
"MicroSD card to continue."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
Babel==2.16.0
|
||||
setuptools>=82.0.0
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ description = "Build an offline, airgapped Bitcoin signing device for less than
|
||||
name = "seedsigner"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "0.8.6"
|
||||
version = "0.8.7"
|
||||
|
||||
[project.urls]
|
||||
"Bug Tracker" = "https://github.com/SeedSigner/seedsigner/issues"
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
embit==0.8.0
|
||||
Pillow==10.0.1
|
||||
Pillow==10.3.0
|
||||
pyzbar @ git+https://github.com/seedsigner/pyzbar.git@c3c237821c6a20b17953efe59b90df0b514a1c03
|
||||
qrcode==7.3.1
|
||||
urtypes==1.0.1
|
||||
|
||||
@@ -17,7 +17,7 @@ from seedsigner.models.settings import SettingsConstants
|
||||
from seedsigner.models.singleton import Singleton
|
||||
from seedsigner.models.threads import BaseThread
|
||||
from seedsigner.views.screensaver import ScreensaverScreen
|
||||
from seedsigner.views.view import Destination
|
||||
from seedsigner.views.view import Destination, View
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -475,3 +475,18 @@ class Controller(Singleton):
|
||||
exception_msg,
|
||||
]
|
||||
return Destination(UnhandledExceptionView, view_args={"error": error}, clear_history=True)
|
||||
|
||||
|
||||
@property
|
||||
def is_screensaver_start_allowed(self) -> bool:
|
||||
"""
|
||||
Determines whether the screensaver is allowed to start.
|
||||
|
||||
The screensaver can start only if:
|
||||
- It is not currently running.
|
||||
- The current active view allows screensaver activity.
|
||||
"""
|
||||
from seedsigner.views import MainMenuView
|
||||
# Confusingly, the top item in the `BackStack` is actually the *current* View
|
||||
active_view = self.back_stack[-1].view if self.back_stack else MainMenuView()
|
||||
return not self.is_screensaver_running and active_view.is_screensaver_allowed
|
||||
|
||||
@@ -52,7 +52,7 @@ class GUIConstants:
|
||||
"default": "OpenSans-Regular",
|
||||
SettingsConstants.LOCALE__ARABIC: "NotoSansAR-Regular",
|
||||
SettingsConstants.LOCALE__CHINESE_SIMPLIFIED: "NotoSansSC-Regular",
|
||||
# SettingsConstants.LOCALE__CHINESE_TRADITIONAL: "NotoSansTC-Regular",
|
||||
SettingsConstants.LOCALE__HINDI: "NotoSansDevanagari-Regular",
|
||||
SettingsConstants.LOCALE__JAPANESE: "NotoSansJP-Regular",
|
||||
SettingsConstants.LOCALE__KOREAN: "NotoSansKR-Regular",
|
||||
SettingsConstants.LOCALE__PERSIAN: "NotoSansAR-Regular", # Uses the Arabic font
|
||||
@@ -63,9 +63,10 @@ class GUIConstants:
|
||||
TOP_NAV_TITLE_FONT_NAME["default"] = "OpenSans-SemiBold"
|
||||
TOP_NAV_TITLE_FONT_SIZE = {
|
||||
"default": 20,
|
||||
SettingsConstants.LOCALE__CHINESE_SIMPLIFIED: 23, # Some chars won't render below 23px
|
||||
SettingsConstants.LOCALE__HINDI: 22,
|
||||
SettingsConstants.LOCALE__JAPANESE: 22, # Titles won't render below 22px
|
||||
SettingsConstants.LOCALE__KOREAN: 23, # Titles won't render below 23px
|
||||
SettingsConstants.LOCALE__CHINESE_SIMPLIFIED: 23, # Some chars won't render below 23px
|
||||
}
|
||||
TOP_NAV_HEIGHT = 48
|
||||
TOP_NAV_BUTTON_SIZE = 32
|
||||
@@ -73,9 +74,10 @@ class GUIConstants:
|
||||
BODY_FONT_NAME = BASE_LOCALE_FONTS.copy()
|
||||
BODY_FONT_SIZE = {
|
||||
"default": 17,
|
||||
SettingsConstants.LOCALE__CHINESE_SIMPLIFIED: 18,
|
||||
SettingsConstants.LOCALE__HINDI: 18,
|
||||
SettingsConstants.LOCALE__JAPANESE: 18,
|
||||
SettingsConstants.LOCALE__KOREAN: 18,
|
||||
SettingsConstants.LOCALE__CHINESE_SIMPLIFIED: 18,
|
||||
}
|
||||
BODY_FONT_MAX_SIZE = TOP_NAV_TITLE_FONT_SIZE["default"]
|
||||
BODY_FONT_MIN_SIZE = 15
|
||||
@@ -95,6 +97,7 @@ class GUIConstants:
|
||||
"default": 18,
|
||||
"ar": 18,
|
||||
"fa": 18,
|
||||
SettingsConstants.LOCALE__HINDI: 20,
|
||||
SettingsConstants.LOCALE__JAPANESE: 20,
|
||||
SettingsConstants.LOCALE__KOREAN: 20,
|
||||
SettingsConstants.LOCALE__CHINESE_SIMPLIFIED: 20,
|
||||
@@ -247,11 +250,19 @@ def load_image(image_name: str) -> Image.Image:
|
||||
|
||||
|
||||
class Fonts(Singleton):
|
||||
font_path = os.path.join(
|
||||
pathlib.Path(__file__).parent.resolve().parent.resolve(),
|
||||
"resources",
|
||||
"fonts"
|
||||
)
|
||||
font_paths = [
|
||||
os.path.join(
|
||||
pathlib.Path(__file__).parent.resolve().parent.resolve(),
|
||||
"resources",
|
||||
"fonts"
|
||||
),
|
||||
os.path.join(
|
||||
pathlib.Path(__file__).parent.resolve().parent.resolve(),
|
||||
"resources",
|
||||
"seedsigner-translations",
|
||||
"fonts"
|
||||
)
|
||||
]
|
||||
fonts = {}
|
||||
|
||||
@classmethod
|
||||
@@ -264,13 +275,24 @@ class Fonts(Singleton):
|
||||
file_extension = "otf"
|
||||
|
||||
if size not in cls.fonts[font_name]:
|
||||
try:
|
||||
cls.fonts[font_name][size] = ImageFont.truetype(os.path.join(cls.font_path, f"{font_name}.{file_extension}"), size)
|
||||
except OSError as e:
|
||||
if "cannot open resource" in str(e):
|
||||
raise Exception(f"Font {font_name}.{file_extension} not found: {repr(e)}")
|
||||
|
||||
# loop over possible font locations and attempt to load font object
|
||||
# TODO: single location for all font files
|
||||
captured_exception = None
|
||||
for font_path in cls.font_paths:
|
||||
try:
|
||||
cls.fonts[font_name][size] = ImageFont.truetype(os.path.join(font_path, f"{font_name}.{file_extension}"), size)
|
||||
break
|
||||
except OSError as e:
|
||||
captured_exception = e
|
||||
continue
|
||||
|
||||
# throw error at this point if font object was unable to be loaded
|
||||
if size not in cls.fonts[font_name]:
|
||||
if "cannot open resource" in str(captured_exception):
|
||||
raise Exception(f"Font {font_name}.{file_extension} not found: {repr(captured_exception)}")
|
||||
else:
|
||||
raise e
|
||||
raise captured_exception
|
||||
|
||||
return cls.fonts[font_name][size]
|
||||
|
||||
@@ -389,7 +411,13 @@ class TextArea(BaseComponent):
|
||||
# Guaranteed to be a single line of text, possibly wider than self.width
|
||||
self.text_lines = [{"text": self.text, "text_width": full_text_width}]
|
||||
self.text_width = full_text_width
|
||||
if self.text_width > self.visible_width:
|
||||
|
||||
# Technically, the math says that we should enable scrolling for as little as
|
||||
# 1px beyond the available width, but it's not worth having the text twitch
|
||||
# back and forth by such a small amount.
|
||||
min_scrollable_diff = 1
|
||||
|
||||
if self.text_width > self.visible_width + min_scrollable_diff:
|
||||
# We'll have to left justify the text and scroll it (if scrolling is enabled,
|
||||
# otherwise it'll just run off the right edge).
|
||||
self.is_text_centered = False
|
||||
@@ -566,7 +594,7 @@ class TextArea(BaseComponent):
|
||||
self.horizontal_scroll_position = 0
|
||||
self.scroll_increment_sign = 1 # flip to negative to scroll text to the right
|
||||
|
||||
self.renderer = Renderer.get_instance()
|
||||
self.renderer = Renderer.get_instance()
|
||||
|
||||
|
||||
def stop_scrolling(self):
|
||||
@@ -587,45 +615,57 @@ class TextArea(BaseComponent):
|
||||
readability. 45px/sec is better but still perceptually a bit stuttery.
|
||||
"""
|
||||
max_scroll = self.rendered_text_img.width - self.visible_width
|
||||
last_render_time = None
|
||||
|
||||
# The scrolling pauses at the start and end of the text line. These vars track
|
||||
# when we started holding and how long we should hold for.
|
||||
hold_started_at = None
|
||||
cur_hold_duration = None
|
||||
|
||||
while self.keep_running:
|
||||
if not self.scrolling_active:
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
with self.renderer.lock:
|
||||
if not self.scrolling_active:
|
||||
# We were stopped while waiting for the lock
|
||||
if cur_hold_duration is not None:
|
||||
# We're currently holding; see if we've held long enough
|
||||
hold_time_elapsed = time.time() - hold_started_at
|
||||
if hold_time_elapsed < cur_hold_duration:
|
||||
# Still have to hold longer; skip scrolling logic
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
else:
|
||||
# We've held long enough; reset the vars and resume scrolling
|
||||
hold_started_at = None
|
||||
cur_hold_duration = None
|
||||
|
||||
else:
|
||||
# We're not holding, but if we've reached either end, we need to start
|
||||
# holding.
|
||||
if self.horizontal_scroll_position == 0:
|
||||
# Pause on initial (left-justified) position...
|
||||
hold_started_at = time.time()
|
||||
cur_hold_duration = self.begin_hold_secs
|
||||
|
||||
# Next scroll direction will be left
|
||||
self.scroll_increment_sign = 1
|
||||
|
||||
# Don't count those pause seconds
|
||||
last_render_time = None
|
||||
continue
|
||||
|
||||
img = self.rendered_text_img.crop((self.horizontal_scroll_position, 0, self.horizontal_scroll_position + self.visible_width, self.rendered_text_img.height))
|
||||
self.renderer.canvas.paste(img, (self.screen_x, self.screen_y - self.scroll_y))
|
||||
self.renderer.show_image()
|
||||
elif self.horizontal_scroll_position == max_scroll:
|
||||
# ...and slight pause at end of scroll
|
||||
hold_started_at = time.time()
|
||||
cur_hold_duration = self.end_hold_secs
|
||||
|
||||
if self.horizontal_scroll_position == 0:
|
||||
# Pause on initial (left-justified) position...
|
||||
time.sleep(self.begin_hold_secs)
|
||||
# Don't count those pause seconds
|
||||
last_render_time = None
|
||||
|
||||
# Don't count those pause seconds
|
||||
last_render_time = None
|
||||
# Scroll will be to the right
|
||||
self.scroll_increment_sign = -1
|
||||
continue
|
||||
|
||||
# Scroll the text left
|
||||
self.scroll_increment_sign = 1
|
||||
|
||||
elif self.horizontal_scroll_position == max_scroll:
|
||||
# ...and slight pause at end of scroll
|
||||
time.sleep(self.end_hold_secs)
|
||||
|
||||
# Don't count those pause seconds
|
||||
last_render_time = None
|
||||
|
||||
# Scroll the text right
|
||||
self.scroll_increment_sign = -1
|
||||
|
||||
else:
|
||||
# No need to CPU limit when running in its own thread?
|
||||
time.sleep(0.02)
|
||||
|
||||
next_render_time = time.time()
|
||||
|
||||
if not last_render_time:
|
||||
@@ -633,18 +673,39 @@ class TextArea(BaseComponent):
|
||||
# "get off zero" for the real increment calc logic to kick in.
|
||||
scroll_position_increment = 1 * self.scroll_increment_sign
|
||||
else:
|
||||
# Calculate how far to scroll based on time elapsed since last render
|
||||
scroll_position_increment = int(self.horizontal_scroll_speed * (next_render_time - last_render_time) * self.scroll_increment_sign)
|
||||
|
||||
# Only render an update if we're going to move at least 1px
|
||||
if abs(scroll_position_increment) > 0:
|
||||
self.horizontal_scroll_position += scroll_position_increment
|
||||
self.horizontal_scroll_position = max(0, min(self.horizontal_scroll_position, max_scroll))
|
||||
# max: Don't over-scroll when returning to the left edge (0)
|
||||
# min: Don't over-scroll when revealing the right edge (max_scroll)
|
||||
self.horizontal_scroll_position = max(
|
||||
0,
|
||||
min(self.horizontal_scroll_position + scroll_position_increment, max_scroll)
|
||||
)
|
||||
|
||||
# Render the scroll update
|
||||
with self.renderer.lock:
|
||||
if not self.scrolling_active:
|
||||
# We were stopped while waiting for the lock
|
||||
continue
|
||||
|
||||
# The pre-rendered text img slides within a cropping window
|
||||
img = self.rendered_text_img.crop((self.horizontal_scroll_position, 0, self.horizontal_scroll_position + self.visible_width, self.rendered_text_img.height))
|
||||
self.renderer.canvas.paste(img, (self.screen_x, self.screen_y - self.scroll_y))
|
||||
self.renderer.show_image()
|
||||
|
||||
last_render_time = next_render_time
|
||||
|
||||
else:
|
||||
# Wait to accumulate more time before scrolling
|
||||
# Wait to accumulate more time so we can scroll at least 1px
|
||||
pass
|
||||
|
||||
# Free up the processor for a bit each loop
|
||||
time.sleep(0.02)
|
||||
|
||||
|
||||
def render(self):
|
||||
"""
|
||||
Even if we need to animate for scrolling, all instances should explicitly render
|
||||
|
||||
@@ -34,10 +34,34 @@ class Keyboard:
|
||||
|
||||
KEY_BACKSPACE = {
|
||||
"code": "DEL",
|
||||
"letter": SeedSignerIconConstants.DELETE,
|
||||
"font": ICON_KEY_FONT,
|
||||
"size": 2,
|
||||
"letter": SeedSignerIconConstants.DELETE,
|
||||
"font": ICON_KEY_FONT,
|
||||
"size": 3,
|
||||
}
|
||||
KEY_BACKSPACE_2 = {
|
||||
"code": "DEL",
|
||||
"letter": SeedSignerIconConstants.DELETE,
|
||||
"font": ICON_KEY_FONT,
|
||||
"size": 2,
|
||||
}
|
||||
KEY_BACKSPACE_4 = {
|
||||
"code": "DEL",
|
||||
"letter": SeedSignerIconConstants.DELETE,
|
||||
"font": ICON_KEY_FONT,
|
||||
"size": 4,
|
||||
}
|
||||
KEY_BACKSPACE_5 = {
|
||||
"code": "DEL",
|
||||
"letter": SeedSignerIconConstants.DELETE,
|
||||
"font": ICON_KEY_FONT,
|
||||
"size": 5,
|
||||
}
|
||||
KEY_BACKSPACE_6 = {
|
||||
"code": "DEL",
|
||||
"letter": SeedSignerIconConstants.DELETE,
|
||||
"font": ICON_KEY_FONT,
|
||||
"size": 6,
|
||||
}
|
||||
|
||||
KEY_SPACE = {
|
||||
"code": "SPACE",
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from PIL import Image, ImageDraw
|
||||
from threading import Lock
|
||||
|
||||
# from seedsigner.hardware.st7789_mpy import ST7789
|
||||
from seedsigner.hardware.displays.display_driver import ALL_DISPLAY_TYPES, DISPLAY_TYPE__ILI9341, DISPLAY_TYPE__ILI9486, DISPLAY_TYPE__ST7789, DisplayDriver
|
||||
from seedsigner.hardware.displays.ili9341 import ILI9341, ILI9341_TFTWIDTH, ILI9341_TFTHEIGHT
|
||||
from seedsigner.hardware.displays.display_driver import ALL_DISPLAY_TYPES, DISPLAY_TYPE__ILI9341, DISPLAY_TYPE__ILI9486, DISPLAY_TYPE__ST7789, DisplayDriverFactory
|
||||
from seedsigner.models.settings import Settings
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
from seedsigner.models.singleton import ConfigurableSingleton
|
||||
@@ -45,7 +43,12 @@ class Renderer(ConfigurableSingleton):
|
||||
raise Exception(f"Invalid display type: {self.display_type}")
|
||||
|
||||
width, height = display_config.split("_")[1].split("x")
|
||||
self.disp = DisplayDriver(self.display_type, width=int(width), height=int(height))
|
||||
|
||||
if self.disp:
|
||||
# Existing instances might need to close resources like pwm
|
||||
self.disp.cleanup()
|
||||
|
||||
self.disp = DisplayDriverFactory.instantiate_display_driver(self.display_type, width=int(width), height=int(height))
|
||||
|
||||
if Settings.get_instance().get_value(SettingsConstants.SETTING__DISPLAY_COLOR_INVERTED, default_if_none=True) == SettingsConstants.OPTION__ENABLED:
|
||||
self.disp.invert()
|
||||
|
||||
@@ -1129,6 +1129,8 @@ class KeyboardScreen(BaseTopNavScreen):
|
||||
return_after_n_chars: int = None
|
||||
show_save_button: bool = False
|
||||
initial_value: str = ""
|
||||
from dataclasses import dataclass, field
|
||||
custom_additional_keys: dict = field(default_factory=lambda: Keyboard.ADDITIONAL_KEYS)
|
||||
|
||||
def __post_init__(self):
|
||||
if self.keyboard_font_size is None:
|
||||
@@ -1187,6 +1189,7 @@ class KeyboardScreen(BaseTopNavScreen):
|
||||
GUIConstants.EDGE_PADDING + self.keyboard_width,
|
||||
keyboard_start_y + self.rows * self.key_height + (self.rows - 1) * 2
|
||||
),
|
||||
additional_keys=self.custom_additional_keys,
|
||||
auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT],
|
||||
render_now=False
|
||||
)
|
||||
|
||||
@@ -60,6 +60,7 @@ class SeedMnemonicEntryScreen(BaseTopNavScreen):
|
||||
GUIConstants.EDGE_PADDING + self.keyboard_width,
|
||||
self.canvas_height
|
||||
),
|
||||
additional_keys=[Keyboard.KEY_BACKSPACE_4],
|
||||
auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT]
|
||||
)
|
||||
|
||||
@@ -547,6 +548,7 @@ class SeedBIP85SelectChildIndexScreen(KeyboardScreen):
|
||||
self.cols = 5
|
||||
self.keys_charset = "0123456789"
|
||||
self.show_save_button = True
|
||||
self.custom_additional_keys = [Keyboard.KEY_BACKSPACE_5]
|
||||
|
||||
super().__post_init__()
|
||||
|
||||
@@ -579,6 +581,7 @@ class SeedExportXpubCustomDerivationScreen(KeyboardScreen):
|
||||
self.cols = 6
|
||||
self.keys_charset = "/'0123456789"
|
||||
self.show_save_button = True
|
||||
self.custom_additional_keys = [Keyboard.KEY_BACKSPACE_6]
|
||||
|
||||
super().__post_init__()
|
||||
|
||||
@@ -757,7 +760,7 @@ class SeedAddPassphraseScreen(BaseTopNavScreen):
|
||||
Keyboard.KEY_SPACE_2,
|
||||
Keyboard.KEY_CURSOR_LEFT,
|
||||
Keyboard.KEY_CURSOR_RIGHT,
|
||||
Keyboard.KEY_BACKSPACE
|
||||
Keyboard.KEY_BACKSPACE_2
|
||||
],
|
||||
auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT],
|
||||
render_now=False
|
||||
@@ -778,7 +781,7 @@ class SeedAddPassphraseScreen(BaseTopNavScreen):
|
||||
Keyboard.KEY_SPACE_2,
|
||||
Keyboard.KEY_CURSOR_LEFT,
|
||||
Keyboard.KEY_CURSOR_RIGHT,
|
||||
Keyboard.KEY_BACKSPACE
|
||||
Keyboard.KEY_BACKSPACE_2
|
||||
],
|
||||
auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT],
|
||||
render_now=False
|
||||
|
||||
@@ -145,7 +145,11 @@ class IOTestScreen(BaseTopNavScreen):
|
||||
outline_color=GUIConstants.ACCENT_COLOR,
|
||||
is_scrollable_text=False, # Text has to dynamically update, can't use scrollable Button
|
||||
)
|
||||
self.key2_button.text = " " # but default state is empty
|
||||
if not self.renderer.is_screenshot_generator:
|
||||
# The button text should be empty for its initial state ("Clear" doesn't make
|
||||
# any sense until a test image is captured). But the screenshot generator
|
||||
# should show the text so its translation can be reviewed.
|
||||
self.key2_button.text = " "
|
||||
self.components.append(self.key2_button)
|
||||
|
||||
self.key1_button = IconButton(
|
||||
|
||||
@@ -11,6 +11,7 @@ from seedsigner.gui.components import FontAwesomeIconConstants, Fonts, GUIConsta
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, BaseScreen, ButtonListScreen, ButtonOption, KeyboardScreen
|
||||
from seedsigner.hardware.buttons import HardwareButtonsConstants
|
||||
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
|
||||
from seedsigner.gui.keyboard import Keyboard
|
||||
|
||||
|
||||
|
||||
@@ -163,6 +164,7 @@ class ToolsDiceEntropyEntryScreen(KeyboardScreen):
|
||||
def __post_init__(self):
|
||||
# TRANSLATOR_NOTE: current roll number vs total rolls (e.g. roll 7 of 50)
|
||||
self.title = _("Dice Roll {}/{}").format(1, self.return_after_n_chars)
|
||||
self.custom_additional_keys = [Keyboard.KEY_BACKSPACE]
|
||||
|
||||
# Specify the keys in the keyboard
|
||||
self.rows = 3
|
||||
@@ -226,6 +228,7 @@ class ToolsCoinFlipEntryScreen(KeyboardScreen):
|
||||
# Override values set by the parent class
|
||||
# TRANSLATOR_NOTE: current coin-flip number vs total flips (e.g. flip 3 of 4)
|
||||
self.title = _("Coin Flip {}/{}").format(1, self.return_after_n_chars)
|
||||
self.custom_additional_keys = [Keyboard.KEY_BACKSPACE_2]
|
||||
|
||||
# Specify the keys in the keyboard
|
||||
self.rows = 1
|
||||
@@ -489,7 +492,7 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen):
|
||||
self.components.append(IconTextLine(
|
||||
# TRANSLATOR_NOTE: a label for a BIP-380-ish Output Descriptor
|
||||
label_text=_("Wallet descriptor"),
|
||||
value_text=self.wallet_descriptor_display_name, # TODO: English text from embit (e.g. "1 / 2 multisig"); make l10 friendly
|
||||
value_text=self.wallet_descriptor_display_name,
|
||||
is_text_centered=True,
|
||||
screen_x=GUIConstants.EDGE_PADDING,
|
||||
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
|
||||
|
||||
@@ -10,7 +10,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class HardwareButtons(Singleton):
|
||||
if GPIO.RPI_INFO['P1_REVISION'] == 3: #This indicates that we have revision 3 GPIO
|
||||
logger.info("Detected 40pin GPIO (Rasbperry Pi 2 and above)")
|
||||
logger.info("Detected 40pin GPIO (Raspberry Pi 2 and above)")
|
||||
KEY_UP_PIN = 31
|
||||
KEY_DOWN_PIN = 35
|
||||
KEY_LEFT_PIN = 29
|
||||
@@ -22,7 +22,7 @@ class HardwareButtons(Singleton):
|
||||
KEY3_PIN = 36
|
||||
|
||||
else:
|
||||
logger.info("Assuming 26 Pin GPIO (Raspberry P1 1)")
|
||||
logger.info("Assuming 26 Pin GPIO (Raspberry Pi 1)")
|
||||
KEY_UP_PIN = 5
|
||||
KEY_DOWN_PIN = 11
|
||||
KEY_LEFT_PIN = 3
|
||||
@@ -82,7 +82,7 @@ class HardwareButtons(Singleton):
|
||||
return HardwareButtonsConstants.OVERRIDE
|
||||
|
||||
cur_time = int(time.time() * 1000)
|
||||
if cur_time - self.last_input_time > controller.screensaver_activation_ms and not controller.is_screensaver_running:
|
||||
if cur_time - self.last_input_time > controller.screensaver_activation_ms and controller.is_screensaver_start_allowed:
|
||||
# Start the screensaver. Will block execution until input detected.
|
||||
controller.start_screensaver()
|
||||
|
||||
|
||||
@@ -9,9 +9,7 @@ from seedsigner.models.singleton import Singleton
|
||||
|
||||
|
||||
class CameraConnectionError(Exception):
|
||||
def __init__(self, *args, **kwargs):
|
||||
message = _("Camera error. Check camera connections.")
|
||||
super().__init__(message)
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,16 +2,23 @@ import spidev
|
||||
import RPi.GPIO as GPIO
|
||||
import time
|
||||
import array
|
||||
from dataclasses import dataclass
|
||||
|
||||
from seedsigner.hardware.displays.display_driver import BaseDisplayDriver
|
||||
|
||||
|
||||
|
||||
class ST7789(object):
|
||||
"""class for ST7789 240*240 1.3inch OLED displays."""
|
||||
@dataclass
|
||||
class ST7789(BaseDisplayDriver):
|
||||
"""
|
||||
The original SeedSigner display driver.
|
||||
|
||||
def __init__(self):
|
||||
self.width = 240
|
||||
self.height = 240
|
||||
Note that self._width and self._height are provided by the parent DisplayDriver class
|
||||
and are set during instantiation via the DisplayDriverFactory.
|
||||
|
||||
class for ST7789 240*240 1.3inch OLED displays.
|
||||
"""
|
||||
def __post_init__(self):
|
||||
#Initialize DC RST pin
|
||||
self._dc = 22
|
||||
self._rst = 13
|
||||
@@ -41,7 +48,7 @@ class ST7789(object):
|
||||
self._spi.writebytes([val])
|
||||
|
||||
def init(self):
|
||||
"""Initialize dispaly"""
|
||||
"""Initialize display"""
|
||||
self.reset()
|
||||
|
||||
self.command(0x36)
|
||||
|
||||
@@ -1,17 +1,71 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
DISPLAY_TYPE__ST7789 = "st7789"
|
||||
DISPLAY_TYPE__ILI9341 = "ili9341"
|
||||
DISPLAY_TYPE__ILI9486 = "ili9486"
|
||||
|
||||
ALL_DISPLAY_TYPES = [DISPLAY_TYPE__ST7789, DISPLAY_TYPE__ILI9341, DISPLAY_TYPE__ILI9486]
|
||||
|
||||
|
||||
|
||||
class DisplayDriver:
|
||||
def __init__(self, display_type: str = DISPLAY_TYPE__ST7789, width: int = None, height: int = None):
|
||||
@dataclass
|
||||
class BaseDisplayDriver:
|
||||
_width: int
|
||||
_height: int
|
||||
|
||||
def __str__(self):
|
||||
return f"DisplayDriver(display_type={getattr(self, 'display_type', None)}, width={self.width}, height={self.height})"
|
||||
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self._width
|
||||
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self._height
|
||||
|
||||
|
||||
def invert(self, enabled: bool = True):
|
||||
"""
|
||||
Invert how the display interprets colors.
|
||||
Implementation in child classes is optional.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def show_image(self, image, x_start: int = 0, y_start: int = 0):
|
||||
"""
|
||||
The main rendering call to the display driver.
|
||||
Must be implemented in child classes.
|
||||
"""
|
||||
raise Exception("show_image() must be implemented in child classes")
|
||||
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Cleanup resources used by the display driver.
|
||||
Implementation in child classes is optional.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class DisplayDriverFactory:
|
||||
"""
|
||||
Manages all logic related to instantiating display drivers based on type and resolution.
|
||||
|
||||
Imports for specific display drivers are done within this class to avoid circular imports.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def instantiate_display_driver(cls, display_type: str = DISPLAY_TYPE__ST7789, width: int = None, height: int = None) -> BaseDisplayDriver:
|
||||
if display_type not in ALL_DISPLAY_TYPES:
|
||||
raise ValueError(f"Invalid display type: {display_type}")
|
||||
self.display_type = display_type
|
||||
|
||||
if self.display_type == DISPLAY_TYPE__ST7789:
|
||||
if display_type == DISPLAY_TYPE__ST7789:
|
||||
if width not in [240, 320] or height != 240:
|
||||
raise ValueError("ST7789 display only supports 240x240 or 320x240 resolutions")
|
||||
|
||||
@@ -19,42 +73,20 @@ class DisplayDriver:
|
||||
# TODO: For now the original ST7789 driver has to be used for 240x240.
|
||||
# The mpy version below renders incorrectly (almost like each row of pixels
|
||||
# is one pixel short, so the entire screen exhibits a diagonal skew).
|
||||
from seedsigner.hardware.displays.ST7789 import ST7789
|
||||
self.display = ST7789()
|
||||
from seedsigner.hardware.displays.ST7789 import ST7789 as original_ST7789
|
||||
return original_ST7789(_width=width, _height=height)
|
||||
|
||||
elif width == 320:
|
||||
from seedsigner.hardware.displays.st7789_mpy import ST7789
|
||||
from seedsigner.hardware.displays.st7789_mpy import ST7789 as mpy_ST7789
|
||||
# Have to swap width and height; screen is natively 240x320
|
||||
self.display = ST7789(width=height, height=width)
|
||||
|
||||
elif self.display_type == DISPLAY_TYPE__ILI9341:
|
||||
return mpy_ST7789(_width=height, _height=width)
|
||||
|
||||
elif display_type == DISPLAY_TYPE__ILI9341:
|
||||
from seedsigner.hardware.displays.ili9341 import ILI9341
|
||||
self.display = ILI9341()
|
||||
self.display.begin()
|
||||
display = ILI9341(_width=width, _height=height)
|
||||
display.begin()
|
||||
return display
|
||||
|
||||
elif self.display_type == DISPLAY_TYPE__ILI9486:
|
||||
elif display_type == DISPLAY_TYPE__ILI9486:
|
||||
# TODO: improve performance of ili9486 driver
|
||||
raise Exception("ILI9486 display not implemented yet")
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"DisplayDriver(display_type={self.display_type}, width={self.width}, height={self.height})"
|
||||
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self.display.width
|
||||
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self.display.height
|
||||
|
||||
|
||||
def invert(self, enabled: bool = True):
|
||||
"""Invert how the display interprets colors"""
|
||||
self.display.invert(enabled)
|
||||
|
||||
|
||||
def show_image(self, image, x_start: int = 0, y_start: int = 0):
|
||||
self.display.show_image(image, x_start, y_start)
|
||||
@@ -29,12 +29,16 @@ import time
|
||||
# import numpy as np
|
||||
import array
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from PIL import Image
|
||||
from PIL import ImageDraw
|
||||
|
||||
import RPi.GPIO as GPIO
|
||||
from spidev import SpiDev
|
||||
|
||||
from seedsigner.hardware.displays.display_driver import BaseDisplayDriver
|
||||
|
||||
|
||||
# Constants for interacting with display registers.
|
||||
ILI9341_TFTWIDTH = 240
|
||||
@@ -129,16 +133,15 @@ def image_to_data(image):
|
||||
return arr.tobytes()
|
||||
|
||||
|
||||
class ILI9341(object):
|
||||
@dataclass
|
||||
class ILI9341(BaseDisplayDriver):
|
||||
"""Representation of an ILI9341 TFT LCD."""
|
||||
|
||||
def __init__(self, dc=22, rst=13, led=12, width=ILI9341_TFTWIDTH,
|
||||
height=ILI9341_TFTHEIGHT, rotation=90):
|
||||
"""Create an instance of the display using SPI communication. Must
|
||||
provide the GPIO pin number for the D/C pin and the SPI driver. Can
|
||||
optionally provide the GPIO pin number for the reset pin as the rst
|
||||
parameter.
|
||||
"""
|
||||
def __post_init__(self):
|
||||
dc=22
|
||||
rst=13
|
||||
led=12
|
||||
rotation=90
|
||||
spi = SpiDev(0, 0)
|
||||
# spi.mode = 0b10 # [CPOL|CPHA] -> polarity 1, phase 0
|
||||
spi.max_speed_hz = 64_000_000
|
||||
@@ -146,8 +149,6 @@ class ILI9341(object):
|
||||
self._dc = dc
|
||||
self._rst = rst
|
||||
self._spi = spi
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.rotation = rotation
|
||||
self.inverted = False
|
||||
# if self._gpio is None:
|
||||
@@ -165,15 +166,7 @@ class ILI9341(object):
|
||||
GPIO.output(self._rst, GPIO.HIGH)
|
||||
|
||||
# Create an image buffer.
|
||||
self.buffer = Image.new('RGB', (width, height))
|
||||
|
||||
# @property
|
||||
# def width(self):
|
||||
# return self.width
|
||||
|
||||
# @property
|
||||
# def height(self):
|
||||
# return self.height
|
||||
self.buffer = Image.new('RGB', (self.width, self.height))
|
||||
|
||||
|
||||
def send(self, data, is_data=True, chunk_size=4096):
|
||||
@@ -310,15 +303,15 @@ class ILI9341(object):
|
||||
self.reset()
|
||||
self._init()
|
||||
|
||||
def invert(self, state: bool = True):
|
||||
def invert(self, enabled: bool = True):
|
||||
"""Sets display inversion to the specified state. If not provided, state
|
||||
is True, which inverts the display. If state is False, the display turns
|
||||
back into normal mode."""
|
||||
if state:
|
||||
if enabled:
|
||||
self.command(ILI9341_INVON)
|
||||
else:
|
||||
self.command(ILI9341_INVOFF)
|
||||
self.inverted = state
|
||||
self.inverted = enabled
|
||||
return self
|
||||
|
||||
def set_window(self, x0=0, y0=0, x1=None, y1=None):
|
||||
|
||||
@@ -54,8 +54,11 @@ import array
|
||||
import spidev
|
||||
import RPi.GPIO as GPIO
|
||||
|
||||
from dataclasses import dataclass
|
||||
from math import sin, cos
|
||||
|
||||
from seedsigner.hardware.displays.display_driver import BaseDisplayDriver
|
||||
|
||||
#
|
||||
# This allows sphinx to build the docs
|
||||
#
|
||||
@@ -228,7 +231,9 @@ def color565(red, green=0, blue=0):
|
||||
return (red & 0xF8) << 8 | (green & 0xFC) << 3 | blue >> 3
|
||||
|
||||
|
||||
class ST7789:
|
||||
|
||||
@dataclass
|
||||
class ST7789(BaseDisplayDriver):
|
||||
"""
|
||||
ST7789 driver class
|
||||
|
||||
@@ -262,20 +267,15 @@ class ST7789:
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
# spi,
|
||||
width,
|
||||
height,
|
||||
reset=13,
|
||||
dc=22,
|
||||
cs=None,
|
||||
backlight=18,
|
||||
rotation=1,
|
||||
color_order=BGR,
|
||||
custom_init=None,
|
||||
custom_rotations=None,
|
||||
):
|
||||
def __post_init__(self):
|
||||
reset=13
|
||||
dc=22
|
||||
cs=None
|
||||
backlight=18
|
||||
rotation=1
|
||||
color_order=BGR
|
||||
custom_init=None
|
||||
custom_rotations=None
|
||||
|
||||
GPIO.setmode(GPIO.BOARD)
|
||||
GPIO.setwarnings(False)
|
||||
@@ -290,20 +290,20 @@ class ST7789:
|
||||
"""
|
||||
Initialize display.
|
||||
"""
|
||||
self.rotations = custom_rotations or self._find_rotations(width, height)
|
||||
self.rotations = custom_rotations or self._find_rotations(self.width, self.height)
|
||||
if not self.rotations:
|
||||
supported_displays = ", ".join(
|
||||
[f"{display[0]}x{display[1]}" for display in _SUPPORTED_DISPLAYS]
|
||||
)
|
||||
raise ValueError(
|
||||
f"Unsupported {width}x{height} display. Supported displays: {supported_displays}"
|
||||
f"Unsupported {self.width}x{self.height} display. Supported displays: {supported_displays}"
|
||||
)
|
||||
|
||||
if dc is None:
|
||||
raise ValueError("dc pin is required.")
|
||||
|
||||
self.physical_width = self.width = width
|
||||
self.physical_height = self.height = height
|
||||
self.physical_width = self.width
|
||||
self.physical_height = self.height
|
||||
self.xstart = 0
|
||||
self.ystart = 0
|
||||
self.spi = spi
|
||||
@@ -445,8 +445,8 @@ class ST7789:
|
||||
self._rotation = rotation
|
||||
(
|
||||
madctl,
|
||||
self.width,
|
||||
self.height,
|
||||
self._width, # have to use the settable internal vars
|
||||
self._height, # have to use the settable internal vars
|
||||
self.xstart,
|
||||
self.ystart,
|
||||
self.needs_swap,
|
||||
|
||||
@@ -42,7 +42,7 @@ class PiVideoStream:
|
||||
self.rawCapture.truncate(0)
|
||||
|
||||
# if the thread indicator variable is set, stop the thread
|
||||
# and resource camera resources
|
||||
# and release camera resources
|
||||
if self.should_stop:
|
||||
logger.info("PiVideoStream: closing everything")
|
||||
self.stream.close()
|
||||
|
||||
@@ -104,6 +104,14 @@ def get_multisig_address(descriptor: Descriptor, index: int = 0, is_change: bool
|
||||
|
||||
|
||||
|
||||
def get_multisig_policy(descriptor: Descriptor) -> tuple:
|
||||
"""Extract (threshold, n) from a basic multisig descriptor."""
|
||||
if not descriptor.is_basic_multisig:
|
||||
raise ValueError(f"Expected a basic multisig descriptor, got: {descriptor.brief_policy}")
|
||||
return (str(descriptor.miniscript.args[0]), str(len(descriptor.keys)))
|
||||
|
||||
|
||||
|
||||
def get_embit_network_name(settings_name):
|
||||
""" Convert SeedSigner SettingsConstants for `network` to embit's NETWORK key """
|
||||
lookup = {
|
||||
|
||||
@@ -238,7 +238,11 @@ class BaseSimpleAnimatedQREncoder(BaseQrEncoder):
|
||||
|
||||
|
||||
@dataclass
|
||||
class SpecterXPubQrEncoder(BaseSimpleAnimatedQREncoder, BaseXpubQrEncoder):
|
||||
class SpecterLegacyXPubQrEncoder(BaseSimpleAnimatedQREncoder, BaseXpubQrEncoder):
|
||||
"""
|
||||
Legacy "pXofY" format. Included here for compatibility with much older versions of
|
||||
Specter Desktop. Can probably eventually be removed.
|
||||
"""
|
||||
@property
|
||||
def qr_max_fragment_size(self):
|
||||
density_mapping = {
|
||||
|
||||
@@ -81,6 +81,10 @@ class Settings(Singleton):
|
||||
for entry in data.split()[split_index:]:
|
||||
abbreviated_name, value = entry.split("=")
|
||||
|
||||
# Empty values ("some_setting= other_setting=E") are invalid
|
||||
if value == "":
|
||||
raise InvalidSettingsQRData(f"{abbreviated_name} cannot be empty")
|
||||
|
||||
# Parse multi-value settings; integer-ize where needed
|
||||
if "," in value:
|
||||
values_updated = []
|
||||
@@ -103,6 +107,7 @@ class Settings(Singleton):
|
||||
values = [value]
|
||||
else:
|
||||
values = value
|
||||
|
||||
for v in values:
|
||||
if v not in [opt[0] for opt in settings_entry.selection_options]:
|
||||
if settings_entry.attr_name == SettingsConstants.SETTING__PERSISTENT_SETTINGS and v == SettingsConstants.OPTION__ENABLED:
|
||||
@@ -154,8 +159,13 @@ class Settings(Singleton):
|
||||
# Clean the incoming data, if necessary
|
||||
if entry.type == SettingsConstants.TYPE__MULTISELECT:
|
||||
if type(new_settings[entry.attr_name]) == str:
|
||||
# Break comma-separated SettingsQR input into List
|
||||
new_settings[entry.attr_name] = new_settings[entry.attr_name].split(",")
|
||||
# Break comma-separated multiselect options into List; avoid empty
|
||||
# values.
|
||||
new_settings[entry.attr_name] = [value for value in new_settings[entry.attr_name].split(",") if value.strip()]
|
||||
|
||||
if not new_settings[entry.attr_name]:
|
||||
# Multiselect cannot be empty; load defaults to avoid issues
|
||||
new_settings[entry.attr_name] = entry.default_value
|
||||
|
||||
for key, value in new_settings.items():
|
||||
self.set_value(key, value)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import pathlib
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List
|
||||
|
||||
@@ -38,17 +39,18 @@ class SettingsConstants:
|
||||
]
|
||||
|
||||
# User-facing selection options
|
||||
COORDINATOR__BLUE_WALLET = "bw"
|
||||
COORDINATOR__NUNCHUK = "nun"
|
||||
COORDINATOR__SPARROW = "spa"
|
||||
COORDINATOR__SPECTER_DESKTOP = "spd"
|
||||
COORDINATOR__KEEPER = "kpr"
|
||||
ALL_COORDINATORS = [
|
||||
(COORDINATOR__BLUE_WALLET, "BlueWallet"),
|
||||
(COORDINATOR__NUNCHUK, "Nunchuk"),
|
||||
(COORDINATOR__SPARROW, "Sparrow"),
|
||||
(COORDINATOR__SPECTER_DESKTOP, "Specter Desktop"),
|
||||
(COORDINATOR__KEEPER, "Keeper"),
|
||||
XPUB_QR_FORMAT__UR_CRYPTO_ACCOUNT = "urca"
|
||||
XPUB_QR_FORMAT__STATIC = "sta"
|
||||
XPUB_QR_FORMAT__SPECTER_LEGACY = "spl"
|
||||
ALL_XPUB_QR_FORMATS = [
|
||||
# TRANSLATOR_NOTE: QR code format option; "default" = this is the format most wallets use
|
||||
(XPUB_QR_FORMAT__UR_CRYPTO_ACCOUNT, _mft("Animated (default)")),
|
||||
|
||||
# TRANSLATOR_NOTE: QR code format option (static = single frame, not animated)
|
||||
(XPUB_QR_FORMAT__STATIC, _mft("Static")),
|
||||
|
||||
# TRANSLATOR_NOTE: QR code format option: old format that Specter Desktop used to use
|
||||
(XPUB_QR_FORMAT__SPECTER_LEGACY, _mft("Specter legacy")),
|
||||
]
|
||||
|
||||
# Over-specifying current and possible future locales to reduce/eliminate main repo
|
||||
@@ -128,10 +130,23 @@ class SettingsConstants:
|
||||
LOCALE__DUTCH: "Nederlands",
|
||||
|
||||
# --------- Beta languages ------------------------------------------------------
|
||||
LOCALE__CZECH: "(beta) Čeština",
|
||||
LOCALE__CHINESE_SIMPLIFIED: "(beta) 简体中文 (Chinese Simplified)",
|
||||
LOCALE__HINDI: "(beta) हिन्दी (Hindi)",
|
||||
LOCALE__INDONESIAN: "(beta) Indonesia (Indonesian)",
|
||||
LOCALE__JAPANESE: "(beta) 日本語 (Japanese)",
|
||||
LOCALE__KOREAN: "(beta) 한국어 (Korean)",
|
||||
LOCALE__PERSIAN: "(beta) فارسی (Persian)",
|
||||
LOCALE__POLISH: "(beta) Polski",
|
||||
LOCALE__PORTUGUESE_BR: "(beta) Português (Brasil)",
|
||||
LOCALE__THAI: "(beta) ไทย (Thai)",
|
||||
LOCALE__VIETNAMESE: "(beta) Tiếng Việt (Vietnamese)",
|
||||
|
||||
# --------- In Progress Languages -----------------------------------------------
|
||||
LOCALE__GREEK: "(incomplete) Ελληνικά (Greek)", # OpenSans includes Greek chars
|
||||
LOCALE__NORWEGIAN: "(incomplete) Norsk (Norwegian)",
|
||||
LOCALE__RUSSIAN: "(incomplete) русский (Russian)", # OpenSans includes cyrillic chars
|
||||
LOCALE__TURKISH: "(incomplete) Türkçe (Turkish)",
|
||||
|
||||
# --------- Placeholders / Coming soon ------------------------------------------
|
||||
# Commented out options require explicit additional font support.
|
||||
@@ -139,19 +154,15 @@ class SettingsConstants:
|
||||
LOCALE__ARABIC: "العربية (Arabic)",
|
||||
# LOCALE__BENGALI: "বাংলা (Bengali)",
|
||||
LOCALE__BULGARIAN: "български (Bulgarian)", # OpenSans includes cyrillic chars
|
||||
LOCALE__CZECH: "čeština",
|
||||
# LOCALE__CHINESE_TRADITIONAL: "繁體中文 (Chinese Traditional)",
|
||||
LOCALE__DANISH: "Dansk",
|
||||
LOCALE__ESTONIAN: "Eesti",
|
||||
# LOCALE__EGYPTIAN: "مصرى (Egyptian)",
|
||||
LOCALE__GAELIC: "Gaeilge",
|
||||
LOCALE__GREEK: "Ελληνικά (Greek)", # OpenSans includes Greek chars
|
||||
# LOCALE__GUJARATI: "ગુજરાતી (Gujarati)",
|
||||
LOCALE__HAUSA: "Hausa",
|
||||
# LOCALE__HEBREW: "עברית (Hebrew)",
|
||||
# LOCALE__HINDI: "हिन्दी (Hindi)",
|
||||
LOCALE__CROATIAN: "Hrvatski",
|
||||
LOCALE__INDONESIAN: "Indonesia",
|
||||
LOCALE__JAVANESE: "Jawa (Javanese)",
|
||||
# LOCALE__LAO: "ລາວ (Lao)",
|
||||
LOCALE__LATVIAN: "Latviešu",
|
||||
@@ -160,14 +171,9 @@ class SettingsConstants:
|
||||
LOCALE__MALAY: "Melayu",
|
||||
LOCALE__MALTESE: "Malti",
|
||||
# LOCALE__MARATHI: "मराठी (Marathi)",
|
||||
LOCALE__NORWEGIAN: "Norsk",
|
||||
LOCALE__PERSIAN: "فارسی (Persian)",
|
||||
LOCALE__POLISH: "Polski",
|
||||
LOCALE__PORTUGUESE_BR: "Português (Brasil)",
|
||||
LOCALE__PORTUGUESE_PT: "Português (Portugal)",
|
||||
# LOCALE__PUNJABI: "ਪੰਜਾਬੀ (Punjabi)",
|
||||
LOCALE__ROMANIAN: "Română",
|
||||
LOCALE__RUSSIAN: "русский (Russian)", # OpenSans includes cyrillic chars
|
||||
LOCALE__SLOVAK: "Slovenčina",
|
||||
LOCALE__SLOVENIAN: "Slovenščina",
|
||||
LOCALE__FINNISH: "Suomi",
|
||||
@@ -175,12 +181,11 @@ class SettingsConstants:
|
||||
LOCALE__TAGALOG: "Tagalog",
|
||||
# LOCALE__TAMIL: "தமிழ் (Tamil)",
|
||||
# LOCALE__TELUGU: "తెలుగు (Telugu)",
|
||||
LOCALE__TURKISH: "Türkçe",
|
||||
LOCALE__UKRANIAN: "українська (Ukranian)", # OpenSans includes cyrillic chars
|
||||
# LOCALE__URDU: "اردو (Urdu)",
|
||||
LOCALE__VIETNAMESE: "Tiếng Việt (Vietnamese)",
|
||||
}
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_detected_languages(cls) -> list[tuple[str, str]]:
|
||||
"""
|
||||
@@ -188,22 +193,20 @@ class SettingsConstants:
|
||||
|
||||
Scans the filesystem to autodiscover which language codes are onboard.
|
||||
"""
|
||||
# Will normally be the launch dir (where main.py is located)...
|
||||
cwd = os.getcwd()
|
||||
|
||||
# ...except when running the tests which happens one dir higher
|
||||
if "src" not in cwd:
|
||||
cwd = os.path.join(cwd, "src")
|
||||
# Back out from the models/ dir to reach the seedsigner root
|
||||
models_dir = pathlib.Path(__file__).parent.resolve()
|
||||
seedsigner_root = models_dir.parent.resolve()
|
||||
|
||||
# Pre-load English since there's no "en" entry in the translations folder; also
|
||||
# it should always appear first in the list anyway.
|
||||
detected_languages = [(cls.LOCALE__ENGLISH, cls.ALL_LOCALES[cls.LOCALE__ENGLISH])]
|
||||
|
||||
locales_present = set()
|
||||
for root, dirs, files in os.walk(os.path.join(cwd, "seedsigner", "resources", "seedsigner-translations", "l10n")):
|
||||
for root, dirs, files in os.walk(os.path.join(seedsigner_root, "resources", "seedsigner-translations", "l10n")):
|
||||
for file in [f for f in files if f.endswith(".mo")]:
|
||||
# `root` will be [...]seedsigner/resources/seedsigner-translations/l10n/pt_BR/LC_MESSAGES
|
||||
locales_present.add(root.split(f"l10n{ os.sep }")[1].split(os.sep)[0])
|
||||
# Isolate the language code from the path
|
||||
locales_present.add(root.rsplit(os.sep, 2)[-2])
|
||||
|
||||
for locale in cls.ALL_LOCALES.keys():
|
||||
if locale in locales_present:
|
||||
@@ -297,9 +300,11 @@ class SettingsConstants:
|
||||
MICROSD_TOAST_TIMER_FIVE_SECONDS = "E"
|
||||
MICROSD_TOAST_TIMER_FOREVER = "inf"
|
||||
ALL_MICROSD_TOAST_TIMERS = [
|
||||
(MICROSD_TOAST_TIMER_DISABLED, "Disabled"),
|
||||
(MICROSD_TOAST_TIMER_FIVE_SECONDS, "5 seconds"),
|
||||
(MICROSD_TOAST_TIMER_FOREVER, "Until SD removed")
|
||||
(MICROSD_TOAST_TIMER_DISABLED, _mft("Disabled")),
|
||||
# TRANSLATOR_NOTE: MicroSD notification duration setting - Display notification for 5 seconds
|
||||
(MICROSD_TOAST_TIMER_FIVE_SECONDS, _mft("5 seconds")),
|
||||
# TRANSLATOR_NOTE: MicroSD notification duration setting - Display notification until the SD card is removed
|
||||
(MICROSD_TOAST_TIMER_FOREVER, _mft("Until SD removed"))
|
||||
]
|
||||
|
||||
WORDLIST_LANGUAGE__ENGLISH = "en"
|
||||
@@ -326,7 +331,7 @@ class SettingsConstants:
|
||||
SETTING__LOCALE = "locale"
|
||||
SETTING__WORDLIST_LANGUAGE = "wordlist_language"
|
||||
SETTING__PERSISTENT_SETTINGS = "persistent_settings"
|
||||
SETTING__COORDINATORS = "coordinators"
|
||||
SETTING__XPUB_QR_FORMAT = "xpub_qr"
|
||||
SETTING__BTC_DENOMINATION = "denomination"
|
||||
|
||||
SETTING__DISPLAY_CONFIGURATION = "display_config"
|
||||
@@ -334,7 +339,6 @@ class SettingsConstants:
|
||||
|
||||
SETTING__NETWORK = "network"
|
||||
SETTING__QR_DENSITY = "qr_density"
|
||||
SETTING__XPUB_EXPORT = "xpub_export"
|
||||
SETTING__SIG_TYPES = "sig_types"
|
||||
SETTING__SCRIPT_TYPES = "script_types"
|
||||
SETTING__XPUB_DETAILS = "xpub_details"
|
||||
@@ -451,6 +455,9 @@ class SettingsEntry:
|
||||
elif type(self.default_value) == tuple:
|
||||
self.default_value = self.default_value[0]
|
||||
|
||||
if not self.abbreviated_name:
|
||||
self.abbreviated_name = self.attr_name
|
||||
|
||||
|
||||
@property
|
||||
def selection_options_display_names(self) -> List[str]:
|
||||
@@ -569,19 +576,6 @@ class SettingsDefinition:
|
||||
help_text=SettingsConstants.PERSISTENT_SETTINGS__SD_INSERTED__HELP_TEXT,
|
||||
default_value=SettingsConstants.OPTION__DISABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__WALLET,
|
||||
attr_name=SettingsConstants.SETTING__COORDINATORS,
|
||||
abbreviated_name="coords",
|
||||
display_name=_mft("Coordinator software"),
|
||||
type=SettingsConstants.TYPE__MULTISELECT,
|
||||
selection_options=SettingsConstants.ALL_COORDINATORS,
|
||||
default_value=[
|
||||
SettingsConstants.COORDINATOR__BLUE_WALLET,
|
||||
SettingsConstants.COORDINATOR__NUNCHUK,
|
||||
SettingsConstants.COORDINATOR__SPARROW,
|
||||
SettingsConstants.COORDINATOR__SPECTER_DESKTOP,
|
||||
]),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
|
||||
attr_name=SettingsConstants.SETTING__BTC_DENOMINATION,
|
||||
abbreviated_name="denom",
|
||||
@@ -608,12 +602,6 @@ class SettingsDefinition:
|
||||
selection_options=SettingsConstants.ALL_DENSITIES,
|
||||
default_value=SettingsConstants.DENSITY__MEDIUM),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__XPUB_EXPORT,
|
||||
display_name=_mft("Xpub export"),
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__ENABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__SIG_TYPES,
|
||||
abbreviated_name="sigs",
|
||||
@@ -632,6 +620,17 @@ class SettingsDefinition:
|
||||
selection_options=SettingsConstants.ALL_SCRIPT_TYPES,
|
||||
default_value=[SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT, SettingsConstants.TAPROOT]),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__XPUB_QR_FORMAT,
|
||||
display_name=_mft("Xpub QR format"),
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
type=SettingsConstants.TYPE__MULTISELECT,
|
||||
selection_options=SettingsConstants.ALL_XPUB_QR_FORMATS,
|
||||
default_value=[
|
||||
SettingsConstants.XPUB_QR_FORMAT__UR_CRYPTO_ACCOUNT,
|
||||
SettingsConstants.XPUB_QR_FORMAT__STATIC,
|
||||
]),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__XPUB_DETAILS,
|
||||
display_name=_mft("Show xpub details"),
|
||||
@@ -678,7 +677,7 @@ class SettingsDefinition:
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__MICROSD_TOAST_TIMER,
|
||||
display_name="MicroSD toast timer",
|
||||
display_name=_mft("MicroSD notification duration"),
|
||||
type=SettingsConstants.TYPE__SELECT_1,
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
selection_options=SettingsConstants.ALL_MICROSD_TOAST_TIMERS,
|
||||
@@ -782,15 +781,25 @@ class SettingsDefinition:
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_defaults(cls) -> dict:
|
||||
def get_defaults(cls, use_abbreviated_name: bool = False, skip_hidden: bool = False) -> dict:
|
||||
"""
|
||||
* use_abbreviated_name: Only used by the test suite.
|
||||
* skip_hidden: Only used by the test suite.
|
||||
"""
|
||||
as_dict = {}
|
||||
for entry in SettingsDefinition.settings_entries:
|
||||
if skip_hidden and entry.visibility == SettingsConstants.VISIBILITY__HIDDEN:
|
||||
continue
|
||||
if not use_abbreviated_name:
|
||||
attr_name = entry.attr_name
|
||||
else:
|
||||
attr_name = entry.abbreviated_name
|
||||
if type(entry.default_value) == list:
|
||||
# Must copy the default_value list, otherwise we'll inadvertently change
|
||||
# defaults when updating these attrs
|
||||
as_dict[entry.attr_name] = list(entry.default_value)
|
||||
as_dict[attr_name] = list(entry.default_value)
|
||||
else:
|
||||
as_dict[entry.attr_name] = entry.default_value
|
||||
as_dict[attr_name] = entry.default_value
|
||||
return as_dict
|
||||
|
||||
|
||||
@@ -803,19 +812,3 @@ class SettingsDefinition:
|
||||
output["settings_entries"].append(settings_entry.to_dict())
|
||||
|
||||
return output
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
import os
|
||||
|
||||
hostname = os.uname()[1]
|
||||
|
||||
if hostname == "seedsigner-os":
|
||||
output_file = "/mnt/microsd/settings_definition.json"
|
||||
else:
|
||||
output_file = "settings_definition.json"
|
||||
|
||||
with open(output_file, 'w') as json_file:
|
||||
json.dump(SettingsDefinition.to_dict(), json_file, indent=4)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Submodule src/seedsigner/resources/seedsigner-translations updated: 9f528ae075...708961a416
@@ -26,7 +26,7 @@ class PSBTSelectSeedView(View):
|
||||
|
||||
if self.controller.psbt_seed:
|
||||
if PSBTParser.has_matching_input_fingerprint(psbt=self.controller.psbt, seed=self.controller.psbt_seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)):
|
||||
# skip the seed prompt if a seed was previous selected and has matching input fingerprint
|
||||
# skip the seed prompt if a seed was previously selected and has matching input fingerprint
|
||||
return Destination(PSBTOverviewView)
|
||||
|
||||
seeds = self.controller.storage.seeds
|
||||
|
||||
@@ -11,7 +11,7 @@ from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconCo
|
||||
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen,
|
||||
WarningScreen, DireWarningScreen, seed_screens)
|
||||
from seedsigner.gui.screens.screen import ButtonOption, ButtonOptionWithoutTranslation
|
||||
from seedsigner.models.encode_qr import CompactSeedQrEncoder, GenericStaticQrEncoder, SeedQrEncoder, SpecterXPubQrEncoder, StaticXpubQrEncoder, UrXpubQrEncoder
|
||||
from seedsigner.models.encode_qr import CompactSeedQrEncoder, GenericStaticQrEncoder, SeedQrEncoder, SpecterLegacyXPubQrEncoder, StaticXpubQrEncoder, UrXpubQrEncoder
|
||||
from seedsigner.models.qr_type import QRType
|
||||
from seedsigner.models.seed import Seed
|
||||
from seedsigner.models.settings import Settings, SettingsConstants
|
||||
@@ -226,11 +226,13 @@ class SeedMnemonicEntryView(View):
|
||||
)
|
||||
|
||||
if ret == RET_CODE__BACK_BUTTON:
|
||||
if self.cur_word_index > 0:
|
||||
return Destination(BackStackView)
|
||||
else:
|
||||
# This handles two possible scenarios:
|
||||
# 1. Backing out of the first word cancels the mnemonic entry process;
|
||||
# return to whichever `View` routed us here initially.
|
||||
# 2. Backing out of the current word returns to the previous word.
|
||||
if self.cur_word_index == 0:
|
||||
self.controller.storage.discard_pending_mnemonic()
|
||||
return Destination(MainMenuView)
|
||||
return Destination(BackStackView)
|
||||
|
||||
# ret will be our new mnemonic word
|
||||
self.controller.storage.update_pending_mnemonic(ret, self.cur_word_index)
|
||||
@@ -305,8 +307,9 @@ class SeedFinalizeView(View):
|
||||
super().__init__()
|
||||
self.seed = self.controller.storage.get_pending_seed()
|
||||
|
||||
if self.seed.get_fingerprint == "":
|
||||
# Expected normal user flow
|
||||
if not self.seed.has_passphrase:
|
||||
# Expected normal user flow. A freshly-loaded seed has no passphrase yet, so
|
||||
# we can just get the fingerprint directly.
|
||||
self.fingerprint = self.seed.get_fingerprint(network=self.settings.get_value(SettingsConstants.SETTING__NETWORK))
|
||||
|
||||
else:
|
||||
@@ -339,9 +342,6 @@ class SeedFinalizeView(View):
|
||||
elif button_data[selected_menu_num] == self.PASSPHRASE:
|
||||
return Destination(SeedAddPassphraseView)
|
||||
|
||||
elif selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
|
||||
|
||||
class SeedAddPassphraseView(View):
|
||||
@@ -572,8 +572,7 @@ class SeedOptionsView(View):
|
||||
|
||||
button_data.append(self.SCAN_PSBT)
|
||||
|
||||
if self.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED:
|
||||
button_data.append(self.EXPORT_XPUB)
|
||||
button_data.append(self.EXPORT_XPUB)
|
||||
|
||||
button_data.append(self.EXPLORER)
|
||||
button_data.append(self.BACKUP)
|
||||
@@ -720,7 +719,7 @@ class SeedExportXpubScriptTypeView(View):
|
||||
del args["sig_type"]
|
||||
return Destination(ToolsAddressExplorerAddressTypeView, view_args=args, skip_current_view=True)
|
||||
else:
|
||||
return Destination(SeedExportXpubCoordinatorView, view_args=args, skip_current_view=True)
|
||||
return Destination(SeedExportXpubQRFormatView, view_args=args, skip_current_view=True)
|
||||
|
||||
title = _("Export Xpub")
|
||||
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
|
||||
@@ -755,7 +754,7 @@ class SeedExportXpubScriptTypeView(View):
|
||||
del args["sig_type"]
|
||||
return Destination(ToolsAddressExplorerAddressTypeView, view_args=args)
|
||||
else:
|
||||
return Destination(SeedExportXpubCoordinatorView, view_args=args)
|
||||
return Destination(SeedExportXpubQRFormatView, view_args=args)
|
||||
|
||||
|
||||
|
||||
@@ -786,7 +785,7 @@ class SeedExportXpubCustomDerivationView(View):
|
||||
return Destination(ToolsAddressExplorerAddressTypeView, view_args=dict(seed_num=self.seed_num, script_type=self.script_type, custom_derivation=custom_derivation))
|
||||
|
||||
return Destination(
|
||||
SeedExportXpubCoordinatorView,
|
||||
SeedExportXpubQRFormatView,
|
||||
view_args={
|
||||
"seed_num": self.seed_num,
|
||||
"sig_type": self.sig_type,
|
||||
@@ -797,7 +796,7 @@ class SeedExportXpubCustomDerivationView(View):
|
||||
|
||||
|
||||
|
||||
class SeedExportXpubCoordinatorView(View):
|
||||
class SeedExportXpubQRFormatView(View):
|
||||
def __init__(self, seed_num: int, sig_type: str, script_type: str, custom_derivation: str = None):
|
||||
super().__init__()
|
||||
self.seed_num = seed_num
|
||||
@@ -813,41 +812,39 @@ class SeedExportXpubCoordinatorView(View):
|
||||
"script_type": self.script_type,
|
||||
"custom_derivation": self.custom_derivation,
|
||||
}
|
||||
if len(self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)) == 1:
|
||||
if len(self.settings.get_value(SettingsConstants.SETTING__XPUB_QR_FORMAT)) == 1:
|
||||
# Nothing to select; skip this screen
|
||||
args["coordinator"] = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[0]
|
||||
args["xpub_qr_format"] = self.settings.get_value(SettingsConstants.SETTING__XPUB_QR_FORMAT)[0]
|
||||
return Destination(SeedExportXpubWarningView, view_args=args, skip_current_view=True)
|
||||
|
||||
button_data = []
|
||||
for display_name, setting_option in zip(self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS), self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)):
|
||||
for display_name, setting_option in zip(self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__XPUB_QR_FORMAT), self.settings.get_value(SettingsConstants.SETTING__XPUB_QR_FORMAT)):
|
||||
button_data.append(ButtonOption(display_name, return_data=setting_option))
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title=_("Export Xpub"),
|
||||
title=_("Xpub QR Format"),
|
||||
is_button_text_centered=False,
|
||||
button_data=button_data,
|
||||
is_bottom_list=True,
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
# coordinators_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__COORDINATORS)
|
||||
# selected_display_name = button_data[selected_menu_num]
|
||||
# args["coordinator"] = coordinators_settings_entry.get_selection_option_value_by_display_name(selected_display_name)
|
||||
args["coordinator"] = button_data[selected_menu_num].return_data
|
||||
args["xpub_qr_format"] = button_data[selected_menu_num].return_data
|
||||
|
||||
return Destination(SeedExportXpubWarningView, view_args=args)
|
||||
|
||||
|
||||
|
||||
class SeedExportXpubWarningView(View):
|
||||
def __init__(self, seed_num: int, sig_type: str, script_type: str, coordinator: str, custom_derivation: str):
|
||||
def __init__(self, seed_num: int, sig_type: str, script_type: str, xpub_qr_format: str, custom_derivation: str):
|
||||
super().__init__()
|
||||
self.seed_num = seed_num
|
||||
self.sig_type = sig_type
|
||||
self.script_type = script_type
|
||||
self.coordinator = coordinator
|
||||
self.xpub_qr_format = xpub_qr_format
|
||||
self.custom_derivation = custom_derivation
|
||||
|
||||
|
||||
@@ -858,7 +855,7 @@ class SeedExportXpubWarningView(View):
|
||||
"seed_num": self.seed_num,
|
||||
"sig_type": self.sig_type,
|
||||
"script_type": self.script_type,
|
||||
"coordinator": self.coordinator,
|
||||
"xpub_qr_format": self.xpub_qr_format,
|
||||
"custom_derivation": self.custom_derivation,
|
||||
},
|
||||
skip_current_view=True, # Prevent going BACK to WarningViews
|
||||
@@ -888,11 +885,11 @@ class SeedExportXpubDetailsView(View):
|
||||
Collects the user input from all the previous screens leading up to this and
|
||||
finally calculates the xpub and displays the summary view to the user.
|
||||
"""
|
||||
def __init__(self, seed_num: int, sig_type: str, script_type: str, coordinator: str, custom_derivation: str):
|
||||
def __init__(self, seed_num: int, sig_type: str, script_type: str, xpub_qr_format: str, custom_derivation: str):
|
||||
super().__init__()
|
||||
self.sig_type = sig_type
|
||||
self.script_type = script_type
|
||||
self.coordinator = coordinator
|
||||
self.xpub_qr_format = xpub_qr_format
|
||||
self.custom_derivation = custom_derivation
|
||||
|
||||
self.seed_num = seed_num
|
||||
@@ -955,7 +952,7 @@ class SeedExportXpubDetailsView(View):
|
||||
return Destination(
|
||||
SeedExportXpubQRDisplayView,
|
||||
dict(seed_num=self.seed_num,
|
||||
coordinator=self.coordinator,
|
||||
xpub_qr_format=self.xpub_qr_format,
|
||||
derivation_path=derivation_path,
|
||||
sig_type=self.sig_type
|
||||
)
|
||||
@@ -967,7 +964,7 @@ class SeedExportXpubDetailsView(View):
|
||||
|
||||
|
||||
class SeedExportXpubQRDisplayView(View):
|
||||
def __init__(self, seed_num: int, coordinator: str, derivation_path: str, sig_type: str = SettingsConstants.SINGLE_SIG):
|
||||
def __init__(self, seed_num: int, xpub_qr_format: str, derivation_path: str, sig_type: str = SettingsConstants.SINGLE_SIG):
|
||||
super().__init__()
|
||||
self.seed = self.controller.get_seed(seed_num)
|
||||
|
||||
@@ -979,14 +976,14 @@ class SeedExportXpubQRDisplayView(View):
|
||||
sig_type=sig_type
|
||||
)
|
||||
|
||||
if coordinator == SettingsConstants.COORDINATOR__SPECTER_DESKTOP:
|
||||
self.qr_encoder = SpecterXPubQrEncoder(**encoder_args)
|
||||
|
||||
elif coordinator in [SettingsConstants.COORDINATOR__BLUE_WALLET,
|
||||
SettingsConstants.COORDINATOR__KEEPER]:
|
||||
if xpub_qr_format == SettingsConstants.XPUB_QR_FORMAT__STATIC:
|
||||
self.qr_encoder = StaticXpubQrEncoder(**encoder_args)
|
||||
|
||||
elif xpub_qr_format == SettingsConstants.XPUB_QR_FORMAT__SPECTER_LEGACY:
|
||||
self.qr_encoder = SpecterLegacyXPubQrEncoder(**encoder_args)
|
||||
|
||||
else:
|
||||
# Default: UR crypto-address
|
||||
self.qr_encoder = UrXpubQrEncoder(**encoder_args)
|
||||
|
||||
|
||||
@@ -1555,7 +1552,8 @@ class SeedTranscribeSeedQRZoomedInView(View):
|
||||
self.seedqr_format = seedqr_format
|
||||
self.seed = self.controller.get_seed(seed_num)
|
||||
self.initial_zone_x = initial_zone_x
|
||||
self.initial_zone_y = initial_zone_y
|
||||
self.initial_zone_y = initial_zone_y
|
||||
self.is_screensaver_allowed = False
|
||||
|
||||
|
||||
def run(self):
|
||||
@@ -2070,8 +2068,10 @@ class MultisigWalletDescriptorView(View):
|
||||
fingerprint = hexlify(key.fingerprint).decode()
|
||||
fingerprints.append(fingerprint)
|
||||
|
||||
policy = descriptor.brief_policy.split("multisig")[0].strip()
|
||||
# policy = " / ".join(policy.split(" of ")) # i18n w/o l10n since coming from non-l10n embit
|
||||
from seedsigner.helpers.embit_utils import get_multisig_policy
|
||||
threshold, n = get_multisig_policy(descriptor)
|
||||
# TRANSLATOR_NOTE: Multisig policy. For a "2 of 3" policy, "threshold" = 2; "n" = 3
|
||||
policy = _("{threshold} of {n}").format(threshold=threshold, n=n)
|
||||
|
||||
button_data = [self.OK]
|
||||
if self.controller.resume_main_flow:
|
||||
|
||||
@@ -212,6 +212,13 @@ class SettingsEntryUpdateSelectionView(View):
|
||||
)
|
||||
|
||||
if ret_value == RET_CODE__BACK_BUTTON:
|
||||
if self.settings_entry.type == SettingsConstants.TYPE__MULTISELECT:
|
||||
# After the user finishes toggling multiselect options, initial_value will
|
||||
# have their final selections when they hit BACK to exit. All current
|
||||
# multiselect settings require at least one option to be selected.
|
||||
if not initial_value:
|
||||
return Destination(SettingsSelectionRequiredWarningView, view_args={"attr_name": self.settings_entry.attr_name})
|
||||
|
||||
if self.blocking_view:
|
||||
return Destination(self.blocking_view, clear_history=True)
|
||||
return settings_menu_view_destination
|
||||
@@ -269,6 +276,37 @@ class SettingsEntryUpdateSelectionView(View):
|
||||
|
||||
|
||||
|
||||
class SettingsSelectionRequiredWarningView(View):
|
||||
def __init__(self, attr_name: str):
|
||||
super().__init__()
|
||||
self.settings_entry = SettingsDefinition.get_settings_entry(attr_name)
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.screen import WarningScreen
|
||||
|
||||
# TRANSLATOR_NOTE: Title of a warning dialog when configuring a setting that requires at least one option to be selected.
|
||||
title = _("Selection Required")
|
||||
|
||||
# TRANSLATOR_NOTE: The name of the setting being configured (e.g. "Script types") will be inserted.
|
||||
text = _("At least one option must be selected for \"{}\".").format(self.settings_entry.display_name)
|
||||
|
||||
# TRANSLATOR_NOTE: Text for the button that returns the user to the setting configuration screen.
|
||||
button_text = _("Return to setting")
|
||||
|
||||
self.run_screen(
|
||||
WarningScreen,
|
||||
title=title,
|
||||
status_headline=None,
|
||||
text=text,
|
||||
button_data=[ButtonOption(button_text)],
|
||||
show_back_button=False,
|
||||
)
|
||||
|
||||
return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=self.settings_entry.attr_name))
|
||||
|
||||
|
||||
|
||||
class SettingsIngestSettingsQRView(View):
|
||||
def __init__(self, data: str):
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
from gettext import gettext as _
|
||||
@@ -148,9 +147,12 @@ class ToolsImageEntropyMnemonicLengthView(View):
|
||||
|
||||
# Build in some hardware-level uniqueness via CPU unique Serial num
|
||||
try:
|
||||
stream = os.popen("cat /proc/cpuinfo | grep Serial")
|
||||
output = stream.read()
|
||||
serial_num = output.split(":")[-1].strip().encode('utf-8')
|
||||
serial_num = b''
|
||||
with open("/proc/cpuinfo", "r") as f:
|
||||
for line in f:
|
||||
if "Serial" in line:
|
||||
serial_num = line.split(":")[-1].strip().encode('utf-8')
|
||||
break
|
||||
serial_hash = hashlib.sha256(serial_num)
|
||||
hash_bytes = serial_hash.digest()
|
||||
except Exception as e:
|
||||
@@ -599,8 +601,12 @@ class ToolsAddressExplorerAddressTypeView(View):
|
||||
|
||||
wallet_descriptor_display_name = None
|
||||
if "wallet_descriptor" in data:
|
||||
wallet_descriptor_display_name = data["wallet_descriptor"].brief_policy.replace(" (sorted)", "")
|
||||
wallet_descriptor_display_name = " / ".join(wallet_descriptor_display_name.split(" of ")) # i18n w/o l10n since coming from non-l10n embit
|
||||
from seedsigner.helpers.embit_utils import get_multisig_policy
|
||||
threshold, n = get_multisig_policy(data["wallet_descriptor"])
|
||||
# TRANSLATOR_NOTE: Multisig policy. For a "2 / 3 multisig" policy, "threshold" = 2; "n" = 3
|
||||
wallet_descriptor_display_name = _("{threshold} / {n} multisig").format(
|
||||
threshold=threshold, n=n
|
||||
)
|
||||
|
||||
script_type = data["script_type"] if "script_type" in data else None
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ class View:
|
||||
self.screen = None
|
||||
|
||||
self._redirect: 'Destination' = None
|
||||
self.is_screensaver_allowed = True
|
||||
|
||||
|
||||
def __init__(self):
|
||||
@@ -349,10 +350,12 @@ class NetworkMismatchErrorView(ErrorView):
|
||||
self.next_destination = Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=SettingsConstants.SETTING__NETWORK), clear_history=True)
|
||||
super().__post_init__()
|
||||
|
||||
# TRANSLATOR_NOTE: Inserts mainnet/testnet/regtest and derivation path
|
||||
self.text = _("Current network setting ({}) doesn't match {}.").format(
|
||||
self.settings.get_value_display_name(SettingsConstants.SETTING__NETWORK),
|
||||
self.derivation_path,
|
||||
network = _(self.settings.get_value_display_name(SettingsConstants.SETTING__NETWORK))
|
||||
|
||||
# TRANSLATOR_NOTE: "network" will be mainnet/testnet/regtest.
|
||||
self.text = _("Current network setting ({network}) doesn't match {derivation_path}.").format(
|
||||
network=network,
|
||||
derivation_path=self.derivation_path,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -247,8 +247,8 @@ def generate_screenshots(locale):
|
||||
)
|
||||
add_settings_entries(SettingsConstants.VISIBILITY__HARDWARE)
|
||||
|
||||
settingsqr_data_persistent = f"settings::v1 name=English_noob_mode persistent=E coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E locale={locale}"
|
||||
settingsqr_data_not_persistent = f"settings::v1 name=Mode_Ephemeral persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E locale={locale}"
|
||||
settingsqr_data_persistent = f"settings::v1 name=English_noob_mode persistent=E xpub_qr=urca,sta denom=thr network=M qr_density=M sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E locale={locale}"
|
||||
settingsqr_data_not_persistent = f"settings::v1 name=Mode_Ephemeral persistent=D xpub_qr=urca,sta denom=thr network=M qr_density=M sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E locale={locale}"
|
||||
|
||||
# Set up mocks to provide whatever temporary data/state a particular screenshot
|
||||
# might need.
|
||||
@@ -376,13 +376,13 @@ def generate_screenshots(locale):
|
||||
|
||||
ScreenshotConfig(seed_views.SeedOptionsView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedBackupView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubSigTypeView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubScriptTypeView, dict(seed_num=0, sig_type="msig")),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubSigTypeView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubScriptTypeView, dict(seed_num=0, sig_type="msig")),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubCustomDerivationView, dict(seed_num=0, sig_type="ss", script_type="")),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubCoordinatorView, dict(seed_num=0, sig_type="ss", script_type="nat")),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubWarningView, dict(seed_num=0, sig_type="msig", script_type="nes", coordinator="spd", custom_derivation="")),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubDetailsView, dict(seed_num=0, sig_type="ss", script_type="nat", coordinator="bw", custom_derivation="")),
|
||||
ScreenshotConfig(SeedExportXpubQR_ScreenBrightnessView, dict(seed_num=0, coordinator="bw", derivation_path="m/84'/0'/0'")),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubQRFormatView, dict(seed_num=0, sig_type="ss", script_type="nat")),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubWarningView, dict(seed_num=0, sig_type="msig", script_type="nes", xpub_qr_format="urca", custom_derivation="")),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubDetailsView, dict(seed_num=0, sig_type="ss", script_type="nat", xpub_qr_format="urca", custom_derivation="")),
|
||||
ScreenshotConfig(SeedExportXpubQR_ScreenBrightnessView, dict(seed_num=0, xpub_qr_format="urca", derivation_path="m/84'/0'/0'")),
|
||||
|
||||
ScreenshotConfig(seed_views.SeedWordsWarningView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedWordsView, dict(seed_num=0)),
|
||||
@@ -474,6 +474,7 @@ def generate_screenshots(locale):
|
||||
ScreenshotConfig(settings_views.VersionView, screenshot_name="VersionView_current_git_state"),
|
||||
ScreenshotConfig(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_persistent), screenshot_name="SettingsIngestSettingsQRView_persistent"),
|
||||
ScreenshotConfig(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_not_persistent), screenshot_name="SettingsIngestSettingsQRView_not_persistent"),
|
||||
ScreenshotConfig(settings_views.SettingsSelectionRequiredWarningView, dict(attr_name=SettingsConstants.SETTING__SCRIPT_TYPES)),
|
||||
],
|
||||
"Misc Error Views": [
|
||||
ScreenshotConfig(NotYetImplementedView),
|
||||
|
||||
@@ -4,7 +4,6 @@ import pytest
|
||||
from base import BaseTest
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
|
||||
|
||||
class TestController(BaseTest):
|
||||
@@ -80,38 +79,3 @@ class TestController(BaseTest):
|
||||
# ...get a new copy of the instance and confirm change
|
||||
controller = Controller.get_instance()
|
||||
assert controller.unverified_address == "123abc"
|
||||
|
||||
|
||||
def test_missing_settings_get_defaults(self):
|
||||
""" Should gracefully handle all missing fields from `settings.json` """
|
||||
|
||||
controller = Controller.get_instance()
|
||||
|
||||
# Settings defaults
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__LOCALE) == SettingsConstants.LOCALE__ENGLISH
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) == SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__COORDINATORS) == [i for i,j in SettingsConstants.ALL_COORDINATORS if i!="kpr"]
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__BTC_DENOMINATION) == SettingsConstants.BTC_DENOMINATION__THRESHOLD
|
||||
|
||||
# Advanced Settings defaults
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__NETWORK) == SettingsConstants.MAINNET
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__QR_DENSITY) == SettingsConstants.DENSITY__MEDIUM
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__SIG_TYPES) == [i for i,j in SettingsConstants.ALL_SIG_TYPES]
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES) == [SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT, SettingsConstants.TAPROOT]
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__ENABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__ENABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__CAMERA_ROTATION) == SettingsConstants.CAMERA_ROTATION__180
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__ENABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__DISABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__MESSAGE_SIGNING) == SettingsConstants.OPTION__DISABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__PRIVACY_WARNINGS) == SettingsConstants.OPTION__ENABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__ENABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS_TIPS) == SettingsConstants.OPTION__ENABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__PARTNER_LOGOS) == SettingsConstants.OPTION__ENABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__MICROSD_TOAST_TIMER) == SettingsConstants.MICROSD_TOAST_TIMER_FIVE_SECONDS
|
||||
|
||||
# Hidden Settings defaults
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS) == 62
|
||||
|
||||
|
||||
@@ -345,6 +345,35 @@ def test_get_multisig_address():
|
||||
func(descriptor=descriptor, index=args[1], is_change=args[2], embit_network=args[3])
|
||||
|
||||
|
||||
def test_get_multisig_policy():
|
||||
"""
|
||||
tests seedsigner.helpers.embit_utils.get_multisig_policy()
|
||||
"""
|
||||
from embit.descriptor import Descriptor
|
||||
|
||||
# Reuses the same 2-of-3 multisig descriptors from test_get_multisig_address
|
||||
vectors_descriptor_expected = {
|
||||
# native segwit 2-of-3
|
||||
"wsh(sortedmulti(2,[8d55ff0d/48h/1h/0h/2h]tpubDDxNVWk924RTUhdkVB2uLHw1hGMPNMGufpZefhkkswjbZppVZcuMdjYKQN4ewUog9vbL6RBLFPRWcgTGT7kYP79N6thyJ43ELUs4N2szXMg/{0,1}/*,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/{0,1}/*,[0be174ee/48h/1h/0h/2h]tpubDEsePyLPkbxbrDiZSTTWdsviiNtiQjrvvzZnkLtG72QYLBygEsXePRsTdXi8DeMA7taCuuvoEBjUAfFrsNZeQJqfvG9fFoujYWbFPYUn7ux/{0,1}/*))#zw6cnrlk": ("2", "3"),
|
||||
# nested segwit 2-of-3
|
||||
"sh(wsh(sortedmulti(2,[73c5da0a/48h/1h/1h/0h/1h]tpubDFH9dgzveyD8yHQb8VrpG8FYAuwcLMHMje2CCcbBo1FpaGzYVtJeYYxcYgRqSTta5utUFts8nPPHs9C2bqoxrey5jia6Dwf9mpwrPq7YvcJ/{0,1}/*,[0be174ee/48h/1h/0h/1h]tpubDEsePyLPkbxbnj6XuKvWwdERHaKkikZxaGJ9sJqmM7okbZXgkNSFiGU6GX6qEes6kD8f9Z9FosYB9UEnBSgBEyEwwJhj4uUcFE1WE8VtKoh/{0,1}/*,[8d55ff0d/48h/1h/0h/1h]tpubDDxNVWk924RTT3vyGLHdSDoZ2JUVX7jUsPcwCQ9MrKHAtJrW5zECTF9rFHCvqu526E4PjHp61hBknts2c5aGexvX7hvCZ8TGPvQFdzxxy59/{0,1}/*)))#2ujlfp73": ("2", "3"),
|
||||
# legacy p2sh 2-of-3
|
||||
"sh(sortedmulti(2,[8d55ff0d/45h]tpubDANogJ2yfnizHwX7fSi5kUVzybyuPXDhgHB2TR9TUvkSLZFW73cRq4STKFDpx7qjJJiisyq82tbu4CeiYtmKEmT1xoCq9P8BPvXV31HUh6d/{0,1}/*,[0be174ee/45h]tpubDBkeVF2tDNT1Pz7L47iJeBB6RokU12LX6x4E6Ph8T89hmjQfB77q1AMyGwL8qpREVGq9sCJEbWwmnemwNTxnpxGn1di7BGy8jx9wEi5Vahu/{0,1}/*,[73c5da0a/45h]tpubDBKsGC1UqBDNvx9aivFmxZNgeZTUnmsCFGhWrqkLzucUCDePvbWWm3n8tAaAwMmxBG2ihdKCG9fzBdUnMxKx5PrkiqSZFi6Vkv6msUs9ddN/{0,1}/*))#p5t8sa8c": ("2", "3"),
|
||||
}
|
||||
|
||||
for desc_str, (expected_threshold, expected_n) in vectors_descriptor_expected.items():
|
||||
descriptor = Descriptor.from_string(desc_str)
|
||||
threshold, n = embit_utils.get_multisig_policy(descriptor)
|
||||
assert threshold == expected_threshold
|
||||
assert n == expected_n
|
||||
|
||||
# Non-multisig descriptor should raise ValueError
|
||||
with pytest.raises(ValueError):
|
||||
embit_utils.get_multisig_policy(Descriptor.from_string(
|
||||
"wpkh([73c5da0a/84h/1h/0h]tpubDC5FSnBiZDMmhiuCmWAYsLwgLYrrT9rAqvTySfuCCrgsWz8wxMXUS9Tb9iVMvcRbvFcAHGkMD5Kx8koh4GquNGNTfohfk7pgjhaPCdXpoba/{0,1}/*)#2aj6cvca"
|
||||
))
|
||||
|
||||
|
||||
def test_parse_derivation_path():
|
||||
# Shouldn't care if input uses "'" or "h"
|
||||
derivation_path = "m/84'/0'/0'/0/0"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from seedsigner.models.encode_qr import CompactSeedQrEncoder, SeedQrEncoder, SpecterXPubQrEncoder, StaticXpubQrEncoder, UrPsbtQrEncoder, UrXpubQrEncoder
|
||||
from seedsigner.models.encode_qr import CompactSeedQrEncoder, SeedQrEncoder, SpecterLegacyXPubQrEncoder, StaticXpubQrEncoder, UrPsbtQrEncoder, UrXpubQrEncoder
|
||||
from embit import psbt
|
||||
from binascii import a2b_base64
|
||||
|
||||
@@ -54,7 +54,7 @@ def test_xpub_qr():
|
||||
def test_specter_xpub_qr():
|
||||
mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash"
|
||||
|
||||
e = SpecterXPubQrEncoder(seed=Seed(mnemonic.split(" "), passphrase="pass"), network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW)
|
||||
e = SpecterLegacyXPubQrEncoder(seed=Seed(mnemonic.split(" "), passphrase="pass"), network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW)
|
||||
|
||||
assert e.next_part() == "p1of4 [c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDN"
|
||||
assert e.next_part() == "p2of4 c5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBo"
|
||||
|
||||
+117
-25
@@ -182,7 +182,7 @@ class TestSeedFlows(FlowTest):
|
||||
"""
|
||||
Selecting "Export XPUB" from the SeedOptionsView should enter the Export XPUB flow and end at the MainMenuView
|
||||
"""
|
||||
def flowtest_standard_xpub(sig_tuple, script_tuple, coord_tuple):
|
||||
def flowtest_standard_xpub(sig_tuple, script_tuple, xpub_qr_tuple):
|
||||
if sig_tuple[0] == SettingsConstants.SINGLE_SIG:
|
||||
sig_selection = seed_views.SeedExportXpubSigTypeView.SINGLE_SIG
|
||||
else:
|
||||
@@ -193,7 +193,7 @@ class TestSeedFlows(FlowTest):
|
||||
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB),
|
||||
FlowStep(seed_views.SeedExportXpubSigTypeView, button_data_selection=sig_selection),
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=ButtonOption(script_tuple[1], return_data=script_tuple[0])),
|
||||
FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=ButtonOption(coord_tuple[1], return_data=coord_tuple[0])),
|
||||
FlowStep(seed_views.SeedExportXpubQRFormatView, button_data_selection=ButtonOption(xpub_qr_tuple[1], return_data=xpub_qr_tuple[0])),
|
||||
FlowStep(seed_views.SeedExportXpubWarningView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0),
|
||||
@@ -209,17 +209,17 @@ class TestSeedFlows(FlowTest):
|
||||
# these are lists of (constant_value, display_name) tuples
|
||||
sig_types: list[tuple[str, str]] = SettingsConstants.ALL_SIG_TYPES
|
||||
script_types: list[tuple[str, str]] = SettingsConstants.ALL_SCRIPT_TYPES
|
||||
coordinators: list[tuple[str, str]] = SettingsConstants.ALL_COORDINATORS
|
||||
xpub_qr_formats: list[tuple[str, str]] = SettingsConstants.ALL_XPUB_QR_FORMATS
|
||||
|
||||
# enable non-defaults so they're available in views
|
||||
self.settings.set_value(SettingsConstants.SETTING__SIG_TYPES, [x for x,y in sig_types])
|
||||
self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [x for x,y in script_types])
|
||||
self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [x for x,y in coordinators])
|
||||
self.settings.set_value(SettingsConstants.SETTING__XPUB_QR_FORMAT, [x for x,y in xpub_qr_formats])
|
||||
|
||||
# exhaustively test flows thru standard sig_types, script_types, and coordinators
|
||||
# exhaustively test flows thru standard sig_types, script_types, and xpub_qr_formats
|
||||
for sig_tuple in sig_types:
|
||||
for script_tuple in script_types:
|
||||
for coord_tuple in coordinators:
|
||||
for xpub_qr_tuple in xpub_qr_formats:
|
||||
# skip custom derivation
|
||||
if script_tuple[0] == SettingsConstants.CUSTOM_DERIVATION:
|
||||
continue
|
||||
@@ -227,13 +227,13 @@ class TestSeedFlows(FlowTest):
|
||||
elif sig_tuple[0] == SettingsConstants.MULTISIG and script_tuple[0] == SettingsConstants.TAPROOT:
|
||||
continue
|
||||
else:
|
||||
print('\n\ntest_standard_xpubs(%s, %s, %s)' % (sig_tuple, script_tuple, coord_tuple))
|
||||
flowtest_standard_xpub(sig_tuple, script_tuple, coord_tuple)
|
||||
print('\n\ntest_standard_xpubs(%s, %s, %s)' % (sig_tuple, script_tuple, xpub_qr_tuple))
|
||||
flowtest_standard_xpub(sig_tuple, script_tuple, xpub_qr_tuple)
|
||||
|
||||
|
||||
def test_export_xpub_disabled_not_available_flow(self):
|
||||
"""
|
||||
If sig_type/script_type/coordinator disabled, then these options are not available
|
||||
If sig_type/script_type/xpub_qr_format disabled, then these options are not available
|
||||
"""
|
||||
# Load a finalized Seed into the Controller
|
||||
mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split()
|
||||
@@ -243,17 +243,17 @@ class TestSeedFlows(FlowTest):
|
||||
# these are lists of (constant_value, display_name) tuples
|
||||
sig_types: list[tuple[str, str]] = SettingsConstants.ALL_SIG_TYPES
|
||||
script_types: list[tuple[str, str]] = SettingsConstants.ALL_SCRIPT_TYPES
|
||||
coordinators: list[tuple[str, str]] = SettingsConstants.ALL_COORDINATORS
|
||||
xpub_qr_formats: list[tuple[str, str]] = SettingsConstants.ALL_XPUB_QR_FORMATS
|
||||
|
||||
# these are the disabled types that we will be testing
|
||||
disabled_sig = SettingsConstants.MULTISIG
|
||||
disabled_script = SettingsConstants.TAPROOT
|
||||
disabled_coord = SettingsConstants.COORDINATOR__NUNCHUK
|
||||
disabled_xpub_qr_format = SettingsConstants.XPUB_QR_FORMAT__SPECTER_LEGACY
|
||||
|
||||
# enable all but our target disabled type
|
||||
self.settings.set_value(SettingsConstants.SETTING__SIG_TYPES, [x for x,y in sig_types if x!=disabled_sig])
|
||||
self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [x for x,y in script_types if x!=disabled_script])
|
||||
self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [x for x,y in coordinators if x!=disabled_coord])
|
||||
self.settings.set_value(SettingsConstants.SETTING__XPUB_QR_FORMAT, [x for x,y in xpub_qr_formats if x!=disabled_xpub_qr_format])
|
||||
|
||||
# If multisig isn't an option, then the sig type selection is skipped altogether
|
||||
self.run_sequence(
|
||||
@@ -284,7 +284,7 @@ class TestSeedFlows(FlowTest):
|
||||
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB),
|
||||
FlowStep(seed_views.SeedExportXpubSigTypeView, is_redirect=True),
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=disabled_coord),
|
||||
FlowStep(seed_views.SeedExportXpubQRFormatView, button_data_selection=disabled_xpub_qr_format),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -305,8 +305,8 @@ class TestSeedFlows(FlowTest):
|
||||
SettingsConstants.CUSTOM_DERIVATION
|
||||
])
|
||||
|
||||
# Ensure that all coordinators are enabled
|
||||
self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [x for x, y in SettingsConstants.ALL_COORDINATORS])
|
||||
# Ensure that all xpub_qr_formats are enabled
|
||||
self.settings.set_value(SettingsConstants.SETTING__XPUB_QR_FORMAT, [x for x, y in SettingsConstants.ALL_XPUB_QR_FORMATS])
|
||||
|
||||
# Set up button_data selections
|
||||
sig_type = seed_views.SeedExportXpubSigTypeView.SINGLE_SIG
|
||||
@@ -314,9 +314,9 @@ class TestSeedFlows(FlowTest):
|
||||
custom_derivation = SettingsConstants.CUSTOM_DERIVATION
|
||||
script_type = ButtonOption(self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES)[2], return_data=custom_derivation)
|
||||
|
||||
specter = SettingsConstants.COORDINATOR__SPECTER_DESKTOP
|
||||
assert SettingsConstants.ALL_COORDINATORS[3][0] == specter
|
||||
coordinator = ButtonOption(self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)[3], return_data=specter)
|
||||
specter_legacy = SettingsConstants.XPUB_QR_FORMAT__SPECTER_LEGACY
|
||||
assert SettingsConstants.ALL_XPUB_QR_FORMATS[2][0] == specter_legacy
|
||||
xpub_qr_format = ButtonOption(self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__XPUB_QR_FORMAT)[2], return_data=specter_legacy)
|
||||
|
||||
self.run_sequence(
|
||||
initial_destination_view_args=dict(seed_num=0),
|
||||
@@ -325,7 +325,7 @@ class TestSeedFlows(FlowTest):
|
||||
FlowStep(seed_views.SeedExportXpubSigTypeView, button_data_selection=sig_type),
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=script_type),
|
||||
FlowStep(seed_views.SeedExportXpubCustomDerivationView, screen_return_value="m/0'/0'"),
|
||||
FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=coordinator),
|
||||
FlowStep(seed_views.SeedExportXpubQRFormatView, button_data_selection=xpub_qr_format),
|
||||
FlowStep(seed_views.SeedExportXpubWarningView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0),
|
||||
@@ -336,18 +336,18 @@ class TestSeedFlows(FlowTest):
|
||||
|
||||
def test_export_xpub_skip_non_option_flow(self):
|
||||
"""
|
||||
Export XPUB flows w/o user choices when no other options for sig_types, script_types, and/or coordinators
|
||||
Export XPUB flows w/o user choices when no other options for sig_types, script_types, and/or xpub_qr_formats
|
||||
"""
|
||||
# Load a finalized Seed into the Controller
|
||||
mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split()
|
||||
self.controller.storage.set_pending_seed(Seed(mnemonic=mnemonic))
|
||||
self.controller.storage.finalize_pending_seed()
|
||||
|
||||
# exclusively set only one choice for each of sig_types, script_types and coordinators
|
||||
# exclusively set only one choice for each of sig_types, script_types and xpub_qr_formats
|
||||
self.settings.update({
|
||||
SettingsConstants.SETTING__SIG_TYPES: SettingsConstants.MULTISIG,
|
||||
SettingsConstants.SETTING__SCRIPT_TYPES: SettingsConstants.NESTED_SEGWIT,
|
||||
SettingsConstants.SETTING__COORDINATORS: SettingsConstants.COORDINATOR__SPECTER_DESKTOP,
|
||||
SettingsConstants.SETTING__XPUB_QR_FORMAT: SettingsConstants.XPUB_QR_FORMAT__UR_CRYPTO_ACCOUNT,
|
||||
})
|
||||
|
||||
self.run_sequence(
|
||||
@@ -356,7 +356,7 @@ class TestSeedFlows(FlowTest):
|
||||
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB),
|
||||
FlowStep(seed_views.SeedExportXpubSigTypeView, is_redirect=True),
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, is_redirect=True),
|
||||
FlowStep(seed_views.SeedExportXpubCoordinatorView, is_redirect=True),
|
||||
FlowStep(seed_views.SeedExportXpubQRFormatView, is_redirect=True),
|
||||
FlowStep(seed_views.SeedExportXpubWarningView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0),
|
||||
@@ -377,7 +377,7 @@ class TestSeedFlows(FlowTest):
|
||||
# Make sure all options are enabled
|
||||
self.settings.set_value(SettingsConstants.SETTING__SIG_TYPES, [x for x,y in SettingsConstants.ALL_SIG_TYPES])
|
||||
self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [x for x,y in SettingsConstants.ALL_SCRIPT_TYPES])
|
||||
self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [x for x,y in SettingsConstants.ALL_COORDINATORS])
|
||||
self.settings.set_value(SettingsConstants.SETTING__XPUB_QR_FORMAT, [x for x,y in SettingsConstants.ALL_XPUB_QR_FORMATS])
|
||||
|
||||
self.run_sequence(
|
||||
initial_destination_view_args=dict(seed_num=0),
|
||||
@@ -387,7 +387,7 @@ class TestSeedFlows(FlowTest):
|
||||
|
||||
# Skips past the script type options via redirect
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, is_redirect=True),
|
||||
FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=ButtonOption(self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)[0], return_data=SettingsConstants.ALL_COORDINATORS[0][0])),
|
||||
FlowStep(seed_views.SeedExportXpubQRFormatView, button_data_selection=ButtonOption(self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__XPUB_QR_FORMAT)[0], return_data=SettingsConstants.ALL_XPUB_QR_FORMATS[0][0])),
|
||||
FlowStep(seed_views.SeedExportXpubWarningView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0),
|
||||
@@ -474,6 +474,98 @@ class TestSeedFlows(FlowTest):
|
||||
FlowStep(seed_views.SeedOptionsView),
|
||||
])
|
||||
|
||||
def test_transcribe_seedqr_screensaver_startable_status(self):
|
||||
"""
|
||||
The controller should return False for screensaver startable status when SeedTranscribeSeedQRZoomedInView
|
||||
is active.
|
||||
"""
|
||||
# Load a finalized Seed into the Controller
|
||||
mnemonic = ["abandon"] * 11 + ["about"]
|
||||
self.controller.storage.set_pending_seed(Seed(mnemonic=mnemonic))
|
||||
self.controller.storage.finalize_pending_seed()
|
||||
|
||||
self.run_sequence(
|
||||
initial_destination_view_args={'num_modules': 21, 'seed_num': 0, 'seedqr_format': 'seed__seedqr'},
|
||||
sequence=[
|
||||
FlowStep(seed_views.SeedTranscribeSeedQRWholeQRView),
|
||||
FlowStep(seed_views.SeedTranscribeSeedQRZoomedInView, is_redirect=True), # Live interactive screens are a bit weird; not sure why `is_redirect` is necessary here
|
||||
])
|
||||
|
||||
assert self.controller.is_screensaver_start_allowed == False
|
||||
|
||||
|
||||
|
||||
class TestSeedEntryBackFlows(FlowTest):
|
||||
"""
|
||||
Tests for every BACK exit scenario from SeedMnemonicEntryView and related views.
|
||||
|
||||
A naive BackStackView swap can leave resume_main_flow dangling, causing
|
||||
auto-redirects on stale flow state. These tests verify that BACK navigation
|
||||
returns to the correct parent view AND that no flow state leaks.
|
||||
"""
|
||||
|
||||
def test_back_from_seed_entry_first_word(self):
|
||||
"""
|
||||
Pressing BACK on the first word of mnemonic entry should return to
|
||||
the View that initiated the mnemonic entry process.
|
||||
"""
|
||||
for seed_type in [seed_views.LoadSeedView.TYPE_12WORD, seed_views.LoadSeedView.TYPE_24WORD]:
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS),
|
||||
FlowStep(seed_views.SeedsMenuView, is_redirect=True), # No seeds loaded; auto-redirects to LoadSeedView
|
||||
FlowStep(seed_views.LoadSeedView, button_data_selection=seed_type),
|
||||
FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=RET_CODE__BACK_BUTTON),
|
||||
FlowStep(seed_views.LoadSeedView), # Should land here, NOT MainMenuView
|
||||
])
|
||||
BaseTest.reset_controller()
|
||||
|
||||
|
||||
def test_back_from_seed_entry_mid_word(self):
|
||||
"""
|
||||
Pressing BACK from a middle word (eg. word #2) should return to the
|
||||
previous SeedMnemonicEntryView (eg. word #1) via the back stack.
|
||||
"""
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS),
|
||||
FlowStep(seed_views.SeedsMenuView, is_redirect=True),
|
||||
FlowStep(seed_views.LoadSeedView, button_data_selection=seed_views.LoadSeedView.TYPE_12WORD),
|
||||
FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value="abandon"), # word #1
|
||||
FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=RET_CODE__BACK_BUTTON), # BACK from word #2
|
||||
FlowStep(seed_views.SeedMnemonicEntryView), # Returns to word #1
|
||||
])
|
||||
|
||||
# Verify we're back on word #1: word at index 0 should still be set
|
||||
# from the previous entry, while word at index 1 should be unset.
|
||||
assert self.controller.storage.get_pending_mnemonic_word(0) == "abandon"
|
||||
assert self.controller.storage.get_pending_mnemonic_word(1) is None
|
||||
|
||||
|
||||
def test_back_from_seed_entry_via_seed_select(self):
|
||||
"""
|
||||
Backing out of mnemonic entry during an active flow must preserve
|
||||
`resume_main_flow` so the user remains within that flow.
|
||||
"""
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
|
||||
self.settings.set_value(SettingsConstants.SETTING__MESSAGE_SIGNING, SettingsConstants.OPTION__ENABLED)
|
||||
|
||||
def load_signmessage_into_decoder(view):
|
||||
view.decoder.add_data("signmessage m/84h/0h/0h/0/0 ascii:test message")
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
|
||||
FlowStep(scan_views.ScanView, before_run=load_signmessage_into_decoder),
|
||||
FlowStep(seed_views.SeedSignMessageStartView, is_redirect=True),
|
||||
FlowStep(seed_views.SeedSelectSeedView, button_data_selection=seed_views.SeedSelectSeedView.TYPE_12WORD),
|
||||
FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=RET_CODE__BACK_BUTTON), # BACK on first word
|
||||
FlowStep(seed_views.SeedSelectSeedView), # Should return here, in the sign message flow
|
||||
])
|
||||
|
||||
# Verify resume_main_flow is still set — user is still in the sign message flow
|
||||
assert self.controller.resume_main_flow == Controller.FLOW__SIGN_MESSAGE
|
||||
|
||||
|
||||
|
||||
|
||||
class TestMessageSigningFlows(FlowTest):
|
||||
|
||||
@@ -37,16 +37,35 @@ class TestSettingsFlows(FlowTest):
|
||||
|
||||
|
||||
def test_multiselect(self):
|
||||
""" Multiselect Settings options should stay in-place; requires BACK to exit. """
|
||||
"""
|
||||
Multiselect Settings options should stay in-place; requires BACK to exit. If no
|
||||
selections are made, route to the warning screen and return the user to the
|
||||
settings entry until at least one option is selected.
|
||||
"""
|
||||
# Which option are we testing?
|
||||
settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__COORDINATORS)
|
||||
settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SIG_TYPES)
|
||||
|
||||
# Enable all options to start
|
||||
self.settings.set_value(settings_entry.attr_name, [option[0] for option in settings_entry.selection_options])
|
||||
|
||||
# Sanity check, we only expect two options for this setting
|
||||
assert len(settings_entry.selection_options) == 2
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS),
|
||||
FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_views.SettingsMenuView.ADVANCED),
|
||||
FlowStep(settings_views.SettingsMenuView, button_data_selection=ButtonOption(settings_entry.display_name)),
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=0), # select/deselect first option
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # select/deselect second option
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # select/deselect second option
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=0), # deselect first option
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # deselect second option
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # select second option
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # deselect second option
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=RET_CODE__BACK_BUTTON), # BACK to exit
|
||||
|
||||
# Both options were deselected, should route to the warning screen
|
||||
FlowStep(settings_views.SettingsSelectionRequiredWarningView),
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=0), # select first option
|
||||
|
||||
# Now we can exit
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=RET_CODE__BACK_BUTTON), # BACK to exit
|
||||
FlowStep(settings_views.SettingsMenuView),
|
||||
])
|
||||
@@ -78,11 +97,11 @@ class TestSettingsFlows(FlowTest):
|
||||
MainMenuView.
|
||||
"""
|
||||
def load_persistent_settingsqr_into_decoder(view: scan_views.ScanView):
|
||||
settingsqr_data_persistent: str = "settings::v1 name=Total_noob_mode persistent=E coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E"
|
||||
settingsqr_data_persistent: str = "settings::v1 name=Total_noob_mode persistent=E xpub_qr=urca,sta denom=thr network=M qr_density=M sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E"
|
||||
view.decoder.add_data(settingsqr_data_persistent)
|
||||
|
||||
def load_not_persistent_settingsqr_into_decoder(view: scan_views.ScanView):
|
||||
settingsqr_data_not_persistent: str = "settings::v1 name=Ephemeral_noob_mode persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E"
|
||||
settingsqr_data_not_persistent: str = "settings::v1 name=Ephemeral_noob_mode persistent=D xpub_qr=urca,sta denom=thr network=M qr_density=M sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E"
|
||||
view.decoder.add_data(settingsqr_data_not_persistent)
|
||||
|
||||
def _run_test(initial_setting_state: str, load_settingsqr_into_decoder: Callable, expected_setting_state: str):
|
||||
|
||||
@@ -30,13 +30,12 @@ class TestViewFlows(FlowTest):
|
||||
"""
|
||||
Basic flow from MainMenuView to PowerOffView
|
||||
"""
|
||||
with patch.object(Settings, 'HOSTNAME', return_value=Settings.SEEDSIGNER_OS):
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON),
|
||||
FlowStep(PowerOptionsView, button_data_selection=PowerOptionsView.POWER_OFF),
|
||||
FlowStep(PowerOffView), # returns BackStackView
|
||||
FlowStep(PowerOptionsView),
|
||||
])
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON),
|
||||
FlowStep(PowerOptionsView, button_data_selection=PowerOptionsView.POWER_OFF),
|
||||
FlowStep(PowerOffView), # returns BackStackView
|
||||
FlowStep(PowerOptionsView),
|
||||
])
|
||||
|
||||
|
||||
def test_not_yet_implemented_flow(self):
|
||||
|
||||
+129
-18
@@ -1,7 +1,10 @@
|
||||
import json
|
||||
import pytest
|
||||
|
||||
from base import BaseTest
|
||||
|
||||
from seedsigner.models.settings import InvalidSettingsQRData, Settings
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +17,10 @@ class TestSettings(BaseTest):
|
||||
|
||||
def test_reset_settings(self):
|
||||
""" BaseTest.reset_settings() should wipe out any previous Settings changes """
|
||||
settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__PERSISTENT_SETTINGS)
|
||||
assert settings_entry.default_value == SettingsConstants.OPTION__DISABLED
|
||||
|
||||
# Change the setting from its default
|
||||
settings = Settings.get_instance()
|
||||
settings.set_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS, SettingsConstants.OPTION__ENABLED)
|
||||
assert settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__ENABLED
|
||||
@@ -23,18 +30,110 @@ class TestSettings(BaseTest):
|
||||
assert settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED
|
||||
|
||||
|
||||
def test_settings_defaults(self):
|
||||
""" Settings should initialize to their default values """
|
||||
BaseTest.reset_settings()
|
||||
settings = Settings.get_instance()
|
||||
for settings_entry in SettingsDefinition.get_settings_entries():
|
||||
assert settings.get_value(settings_entry.attr_name) == settings_entry.default_value
|
||||
|
||||
|
||||
def test_load_persistent_settings(self):
|
||||
""" Settings should load previously saved persistent settings from disk, if any
|
||||
exist. """
|
||||
# Initial Settings will start with defaults
|
||||
settings = Settings.get_instance()
|
||||
|
||||
# Enable persistent settings and make another change
|
||||
settings.set_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS, SettingsConstants.OPTION__ENABLED)
|
||||
|
||||
assert settings.get_value(SettingsConstants.SETTING__QR_DENSITY) != SettingsConstants.DENSITY__HIGH
|
||||
settings.set_value(SettingsConstants.SETTING__QR_DENSITY, SettingsConstants.DENSITY__HIGH)
|
||||
|
||||
# Hold on to the settings.json content
|
||||
settings_json = None
|
||||
with open(Settings.SETTINGS_FILENAME) as settings_file:
|
||||
settings_json = json.loads(settings_file.read())
|
||||
|
||||
# Now wipe out the Settings singleton
|
||||
BaseTest.reset_settings()
|
||||
|
||||
# This also deletes settings.json, so recreate it
|
||||
with open(Settings.SETTINGS_FILENAME, "w") as settings_file:
|
||||
settings_file.write(json.dumps(settings_json))
|
||||
|
||||
# Now instantiate the Settings singleton again; it should load from disk
|
||||
settings = Settings.get_instance()
|
||||
|
||||
# Persistent setting change should have survived
|
||||
assert settings.get_value(SettingsConstants.SETTING__QR_DENSITY) == SettingsConstants.DENSITY__HIGH
|
||||
|
||||
|
||||
def test_load_empty_multiselect_settings(self):
|
||||
""" Empty multiselect settings should load defaults. """
|
||||
# Initial Settings will start with defaults
|
||||
settings = Settings.get_instance()
|
||||
|
||||
# Enable persistent settings to write settings.json to disk
|
||||
settings.set_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS, SettingsConstants.OPTION__ENABLED)
|
||||
|
||||
# Hold on to the settings.json content
|
||||
settings_dict = None
|
||||
with open(Settings.SETTINGS_FILENAME) as settings_file:
|
||||
settings_dict = json.loads(settings_file.read())
|
||||
|
||||
def _verify_defaults_loaded(attr_name: str):
|
||||
# Verify that the multiselect setting has loaded its defaults
|
||||
settings = Settings.get_instance()
|
||||
cur_setting_value = settings.get_value(attr_name)
|
||||
assert cur_setting_value == SettingsDefinition.get_settings_entry(attr_name).default_value
|
||||
|
||||
# Alter the settings to test against various empty values
|
||||
for empty_value in ["", ",", [], None]:
|
||||
settings_dict[SettingsConstants.SETTING__SIG_TYPES] = empty_value
|
||||
settings.update(settings_dict)
|
||||
_verify_defaults_loaded(SettingsConstants.SETTING__SIG_TYPES)
|
||||
|
||||
# One last test: remove the multiselect setting entirely
|
||||
del settings_dict[SettingsConstants.SETTING__SIG_TYPES]
|
||||
settings.update(settings_dict)
|
||||
_verify_defaults_loaded(SettingsConstants.SETTING__SIG_TYPES)
|
||||
|
||||
|
||||
|
||||
class SettingsQRBase(BaseTest):
|
||||
"""
|
||||
Reusable base test class for testing SettingsQR data.
|
||||
"""
|
||||
def setup_method(self):
|
||||
super().setup_method()
|
||||
settingsqr_dict = SettingsDefinition.get_defaults(use_abbreviated_name=True, skip_hidden=True)
|
||||
|
||||
# Build baseline SettingsQR config based on defaults
|
||||
self.settingsqr_prefix = "settings::v1"
|
||||
self.settingsqr_default_attrs_str = ""
|
||||
for abbreviated_attr_name, value in settingsqr_dict.items():
|
||||
if isinstance(value, list):
|
||||
value_str = ",".join(value)
|
||||
else:
|
||||
value_str = str(value)
|
||||
self.settingsqr_default_attrs_str += f" {abbreviated_attr_name}={value_str}"
|
||||
|
||||
|
||||
|
||||
class TestSettingsQRParser(SettingsQRBase):
|
||||
def test_parse_settingsqr_data(self):
|
||||
"""
|
||||
SettingsQR parser should successfully parse a valid settingsqr input string and
|
||||
return the resulting config_name and formatted settings_update_dict.
|
||||
"""
|
||||
settings_name = "Test SettingsQR"
|
||||
settingsqr_data = f"""settings::v1 name={ settings_name.replace(" ", "_") } persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss,ms scripts=nat,nes,tr xpub_details=E passphrase=E camera=180 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E"""
|
||||
settingsqr_data = f"""{self.settingsqr_prefix} name={ settings_name.replace(" ", "_") } {self.settingsqr_default_attrs_str}"""
|
||||
|
||||
# First explicitly set settings that differ from the settingsqr_data
|
||||
self.settings.set_value(SettingsConstants.SETTING__COMPACT_SEEDQR, SettingsConstants.OPTION__DISABLED)
|
||||
self.settings.set_value(SettingsConstants.SETTING__DIRE_WARNINGS, SettingsConstants.OPTION__DISABLED)
|
||||
self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [SettingsConstants.COORDINATOR__BLUE_WALLET, SettingsConstants.COORDINATOR__SPARROW])
|
||||
self.settings.set_value(SettingsConstants.SETTING__XPUB_QR_FORMAT, [SettingsConstants.XPUB_QR_FORMAT__STATIC, SettingsConstants.XPUB_QR_FORMAT__SPECTER_LEGACY])
|
||||
|
||||
# Now parse the settingsqr_data
|
||||
config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data)
|
||||
@@ -45,43 +144,45 @@ class TestSettings(BaseTest):
|
||||
assert self.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__ENABLED
|
||||
assert self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__ENABLED
|
||||
|
||||
coordinators = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)
|
||||
assert SettingsConstants.COORDINATOR__BLUE_WALLET not in coordinators
|
||||
assert SettingsConstants.COORDINATOR__SPARROW in coordinators
|
||||
assert SettingsConstants.COORDINATOR__SPECTER_DESKTOP in coordinators
|
||||
xpub_qr_formats = self.settings.get_value(SettingsConstants.SETTING__XPUB_QR_FORMAT)
|
||||
assert SettingsConstants.XPUB_QR_FORMAT__UR_CRYPTO_ACCOUNT in xpub_qr_formats
|
||||
assert SettingsConstants.XPUB_QR_FORMAT__STATIC in xpub_qr_formats
|
||||
assert SettingsConstants.XPUB_QR_FORMAT__SPECTER_LEGACY not in xpub_qr_formats
|
||||
|
||||
|
||||
def test_settingsqr_version(self):
|
||||
""" SettingsQR parser should accept SettingsQR v1 and reject any others """
|
||||
settingsqr_data = "settings::v1 name=Foo"
|
||||
settingsqr_data = f"{self.settingsqr_prefix} {self.settingsqr_default_attrs_str}"
|
||||
config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data)
|
||||
|
||||
# Accepts update with no Exceptions
|
||||
self.settings.update(new_settings=settings_update_dict)
|
||||
|
||||
settingsqr_data = "settings::v2 name=Foo"
|
||||
settingsqr_data = f"settings::v2 {self.settingsqr_default_attrs_str}"
|
||||
with pytest.raises(InvalidSettingsQRData) as e:
|
||||
Settings.parse_settingsqr(settingsqr_data)
|
||||
assert "Unsupported SettingsQR version" in str(e.value)
|
||||
|
||||
# Should also fail if version omitted
|
||||
settingsqr_data = "settings name=Foo"
|
||||
settingsqr_data = f"settings {self.settingsqr_default_attrs_str}"
|
||||
with pytest.raises(InvalidSettingsQRData) as e:
|
||||
Settings.parse_settingsqr(settingsqr_data)
|
||||
|
||||
# And if "settings" is omitted entirely
|
||||
settingsqr_data = "name=Foo"
|
||||
settingsqr_data = self.settingsqr_default_attrs_str
|
||||
with pytest.raises(InvalidSettingsQRData) as e:
|
||||
Settings.parse_settingsqr(settingsqr_data)
|
||||
|
||||
|
||||
def test_settingsqr_ignores_unrecognized_setting(self):
|
||||
""" SettingsQR parser should ignore unrecognized settings """
|
||||
settingsqr_data = "settings::v1 name=Foo favorite_food=bacon xpub_export=D"
|
||||
settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__NETWORK)
|
||||
unknown_attr = "favorite_food"
|
||||
settingsqr_data = f"{self.settingsqr_prefix} {unknown_attr}=bacon {settings_entry.abbreviated_name}={settings_entry.default_value}"
|
||||
config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data)
|
||||
|
||||
assert "favorite_food" not in settings_update_dict
|
||||
assert "xpub_export" in settings_update_dict
|
||||
assert unknown_attr not in settings_update_dict
|
||||
assert settings_entry.attr_name in settings_update_dict
|
||||
|
||||
# Accepts update with no Exceptions
|
||||
self.settings.update(new_settings=settings_update_dict)
|
||||
@@ -89,18 +190,28 @@ class TestSettings(BaseTest):
|
||||
|
||||
def test_settingsqr_fails_unrecognized_option(self):
|
||||
""" SettingsQR parser should fail if a settings has an unrecognized option """
|
||||
settingsqr_data = "settings::v1 name=Foo xpub_export=Yep"
|
||||
settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__NETWORK)
|
||||
settingsqr_data = f"{self.settingsqr_prefix} {settings_entry.abbreviated_name}=fake_value"
|
||||
with pytest.raises(InvalidSettingsQRData) as e:
|
||||
Settings.parse_settingsqr(settingsqr_data)
|
||||
assert "xpub_export" in str(e.value)
|
||||
assert settings_entry.attr_name in str(e.value)
|
||||
|
||||
|
||||
def test_settingsqr_fails_empty_values(self):
|
||||
""" SettingsQR parser should fail if a setting is empty """
|
||||
settingsqr_data = "settings::v1 persistent=D sigs= camera=180"
|
||||
with pytest.raises(InvalidSettingsQRData) as e:
|
||||
Settings.parse_settingsqr(settingsqr_data)
|
||||
assert "sigs" in str(e.value)
|
||||
|
||||
|
||||
def test_settingsqr_parses_line_break_separators(self):
|
||||
""" SettingsQR parser should read line breaks as acceptable separators """
|
||||
settingsqr_data = "settings::v1\nname=Foo\nsigs=ss,ms\nscripts=nat,nes,tr\nxpub_export=E\n"
|
||||
attrs_with_line_breaks = self.settingsqr_default_attrs_str.replace(' ', '\n')
|
||||
settingsqr_data = f"{self.settingsqr_prefix}\n{attrs_with_line_breaks}"
|
||||
config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data)
|
||||
|
||||
assert len(settings_update_dict.keys()) == 3
|
||||
assert len(settings_update_dict.keys()) == self.settingsqr_default_attrs_str.count('=')
|
||||
|
||||
# Accepts update with no Exceptions
|
||||
self.settings.update(new_settings=settings_update_dict)
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
from test_settings import SettingsQRBase
|
||||
from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus
|
||||
|
||||
|
||||
|
||||
class TestSettingsQRDecoder:
|
||||
|
||||
class TestSettingsQRDecoder(SettingsQRBase):
|
||||
def test_decode_settingsqr(self):
|
||||
"""
|
||||
Assume the QR reader decodes the SettingsQR content correctly and begin this test
|
||||
with parsing the result.
|
||||
"""
|
||||
settings_name = "Test SettingsQR"
|
||||
settings_qr_str = f"""settings::v1 name={ settings_name.replace(" ", "_") } persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss,ms scripts=nat,nes,tr xpub_details=E passphrase=E camera=180 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E"""
|
||||
settings_qr_str = f"""{self.settingsqr_prefix} name={ settings_name.replace(" ", "_") } {self.settingsqr_default_attrs_str}"""
|
||||
|
||||
# Now parse the settings_qr_str
|
||||
decoder = DecodeQR()
|
||||
|
||||
Reference in New Issue
Block a user