mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
Merge branch 'dev' into qr-brightness-tips
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
|
||||
# Project Summary
|
||||
|
||||
The goal of SeedSigner is to lower the cost and complexity of Bitcoin multi-signature wallet use. To accomplish this goal, SeedSigner offers anyone the opportunity to build a verifiably air-gapped, stateless Bitcoin signing device using inexpensive, publicly available hardware components (usually < $50). SeedSigner helps users save with Bitcoin by assisting with trustless private key generation and multi-signature wallet setup, and helps users transact with Bitcoin via a secure, air-gapped QR-exchange signing model.
|
||||
The goal of SeedSigner is to lower the cost and complexity of Bitcoin multi-signature wallet use. To accomplish this goal, SeedSigner offers anyone the opportunity to build a verifiably air-gapped, stateless Bitcoin signing device using inexpensive, publicly available hardware components (usually < $50). SeedSigner helps users save with Bitcoin by assisting with trustless private key generation and multisignature (aka "multisig") wallet setup, and helps users transact with Bitcoin via a secure, air-gapped QR-exchange signing model.
|
||||
|
||||
Additional information about the project can be found at [SeedSigner.com](https://seedsigner.com).
|
||||
|
||||
@@ -26,20 +26,22 @@ You can follow [@SeedSigner](https://twitter.com/SeedSigner) on Twitter for the
|
||||
If you have specific questions about the project, our [Telegram Group](https://t.me/joinchat/GHNuc_nhNQjLPWsS) is a great place to ask them.
|
||||
|
||||
### Feature Highlights:
|
||||
* Calculate word 12/24 of a BIP39 seed phrase
|
||||
* Create a 24-word BIP39 seed phrase with 99 dice rolls
|
||||
* Create a 24-word BIP39 seed phrase by taking a digital photo
|
||||
* Temporarily store up to 3 seed phrases while device is powered
|
||||
* Guided interface to manually create a SeedQR for instant input [(demo video here)](https://youtu.be/c1-PqTNx1vc)
|
||||
* BIP39 passphrase / word 25 support
|
||||
* Native Segwit Multisig XPUB generation w/ QR display
|
||||
* Scan and parse transaction data from animated QR codes
|
||||
* Calculate the final word (aka checksum) of a 12- or 24-word BIP39 seed phrase
|
||||
* Create a 24-word BIP39 seed phrase with 99 dice rolls or a 12-word with 50 rolls [(Verifying dice seed generation)](docs/dice_verification.md)
|
||||
* Create a 12- or 24-word BIP39 seed phrase via image entropy from the onboard camera
|
||||
* Temporarily stores seeds in memory while the device is powered; all memory is wiped when power is removed
|
||||
* SD card removable after boot to ensure no secret data can be written to it
|
||||
* Guided interface to manually transcribe a seed to the SeedQR format for instant seed loading [(demo video here)](https://youtu.be/c1-PqTNx1vc)
|
||||
* BIP39 passphrase (aka "word 25") support
|
||||
* Native Segwit Multisig XPUB generation
|
||||
* PSBT-compliant; scan and parse transaction data from animated QR codes
|
||||
* Sign transactions & transfer XPUB data using animated QR codes [(demo video here)](https://youtu.be/LPqvdQ2gSzs)
|
||||
* Live preview during photo-to-seed and QR scanning UX
|
||||
* Live preview during image entropy seed generation and QR scanning UX
|
||||
* Optimized seed word entry interface
|
||||
* Support for Bitcoin Mainnet & Testnet
|
||||
* Support for custom user-defined derivation paths
|
||||
* On-demand receive address verification
|
||||
* Address Explorer for single sig and multisig wallets
|
||||
* User-configurable QR code display density
|
||||
* Responsive, event-driven user interface
|
||||
|
||||
@@ -52,10 +54,10 @@ If you have specific questions about the project, our [Telegram Group](https://t
|
||||
* If you think SeedSigner adds value to the Bitcoin ecosystem, please help us spread the word! (tweets, pics, videos, etc.)
|
||||
|
||||
### Planned Upcoming Improvements / Functionality:
|
||||
* Single-sig and multi-sig change address verification
|
||||
* Re-imagined, graphically-focused user interface
|
||||
* Multi-language support
|
||||
* Customized Linux live-boot OS to allow MicroSD card removal
|
||||
* Significantly faster boot time
|
||||
* Reproducible builds
|
||||
* Port to MicroPython to broaden the range of compatible hardware to include low-cost microcontrollers
|
||||
* Other optimizations based on user feedback!
|
||||
|
||||
---------------
|
||||
|
||||
+165
-73
@@ -15,11 +15,12 @@ For the following steps you'll need to either connect a keyboard & monitor to th
|
||||
|
||||
### Configure the Pi
|
||||
First things first, verify that you are using the correct version of the Raspberry Pi Lite operating system by typing the command:
|
||||
```
|
||||
```bash
|
||||
cat /etc/os-release
|
||||
```
|
||||
|
||||
The output of this command should match the following text:
|
||||
```
|
||||
```bash
|
||||
PRETTY_NAME="Raspbian GNU/Linux 10 (buster)"
|
||||
NAME="Raspbian GNU/Linux"
|
||||
VERSION_ID="10"
|
||||
@@ -33,7 +34,7 @@ BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs"
|
||||
```
|
||||
|
||||
Now launch the Raspberry Pi's System Configuration tool using the command:
|
||||
```
|
||||
```bash
|
||||
sudo raspi-config
|
||||
```
|
||||
|
||||
@@ -45,6 +46,8 @@ Set the following:
|
||||
* `Locale`: arrow up and down through the list and select or deselect languages with the spacebar.
|
||||
* Deselect the default language option that is selected
|
||||
* Select `en_US.UTF-8 UTF-8` for US English
|
||||
* Use the `TAB` button to select `Ok` and press `ENTER`
|
||||
* On the next screen select `en_US.UTF-8` for the default locale
|
||||
* You will also need to configure the WiFi settings if you are using the #1 option above to connect to the internet
|
||||
|
||||
When you exit the System Configuration tool, you will be prompted to reboot the system; allow the system to reboot and continue with these instructions.
|
||||
@@ -52,18 +55,55 @@ When you exit the System Configuration tool, you will be prompted to reboot the
|
||||
|
||||
### Change the default password
|
||||
Change the system's default password from the default "raspberry". Run the command:
|
||||
```
|
||||
```bash
|
||||
passwd
|
||||
```
|
||||
You will be prompted to enter the current password ("raspberry") and then to enter a new password twice. In our prepared release image, the password used is `AirG@pped!`.
|
||||
|
||||
|
||||
### Install dependencies
|
||||
Copy this entire box and run it as one command (will take 15-20min to complete):
|
||||
### Install python3.10
|
||||
```bash
|
||||
# install compiler dependencies; takes ~1 minute on a Pi Zero 1.3
|
||||
# * openssl, libssl-dev: ssl support when pip fetches packages
|
||||
# * libsqlite3-dev: required by `coverage`
|
||||
sudo apt update && sudo apt install -y build-essential zlib1g-dev \
|
||||
libncurses5-dev libgdbm-dev libnss3-dev openssl libssl-dev \
|
||||
libreadline-dev libffi-dev wget libsqlite3-dev
|
||||
|
||||
# Grab the python3.10 source
|
||||
wget https://www.python.org/ftp/python/3.10.10/Python-3.10.10.tgz
|
||||
tar -xzvf Python-3.10.10.tgz
|
||||
cd Python-3.10.10
|
||||
|
||||
# Takes ~6 minutes on a Pi Zero 1.3 to check what is available
|
||||
./configure --enable-optimizations
|
||||
|
||||
# compiling takes ~80 minutes(!!) on a Pi Zero 1.3
|
||||
sudo make altinstall
|
||||
|
||||
# cleanup
|
||||
cd ..
|
||||
sudo rm -rf Python-3.10.10*
|
||||
|
||||
# Make python3.10 the default version
|
||||
sudo update-alternatives --install /usr/bin/python python /usr/local/bin/python3.10 1
|
||||
sudo update-alternatives --install /usr/bin/python3 python3 /usr/local/bin/python3.10 1
|
||||
```
|
||||
sudo apt-get update && sudo apt-get install -y wiringpi python3-pip \
|
||||
python3-numpy python-pil libopenjp2-7 git python3-opencv \
|
||||
python3-picamera libatlas-base-dev qrencode
|
||||
|
||||
Manually re-install `python3-apt` to avoid error messages in later steps (though, ironically, you will see the "ModuleNotFoundError: No module named 'apt_pkg'" error message during the `apt remove` step):
|
||||
```bash
|
||||
sudo apt remove --purge python3-apt -y
|
||||
sudo apt autoremove -y
|
||||
sudo apt install python3-apt -y
|
||||
```
|
||||
|
||||
|
||||
### Install dependencies
|
||||
Copy this entire box and run it as one command (will take a while to complete):
|
||||
```bash
|
||||
sudo apt update && sudo apt install -y wiringpi python3-pip \
|
||||
python3-numpy python-pil libjpeg-dev zlib1g-dev libopenjp2-7 \
|
||||
git python3-opencv python3-picamera libatlas-base-dev qrencode
|
||||
```
|
||||
|
||||
### Install `zbar`
|
||||
@@ -72,17 +112,17 @@ sudo apt-get update && sudo apt-get install -y wiringpi python3-pip \
|
||||
SeedSigner requires `zbar` at 0.23.x or higher.
|
||||
|
||||
Download the binary:
|
||||
```
|
||||
```bash
|
||||
curl -L http://raspbian.raspberrypi.org/raspbian/pool/main/z/zbar/libzbar0_0.23.90-1_armhf.deb --output libzbar0_0.23.90-1_armhf.deb
|
||||
```
|
||||
|
||||
And then install it:
|
||||
```
|
||||
```bash
|
||||
sudo apt install ./libzbar0_0.23.90-1_armhf.deb
|
||||
```
|
||||
|
||||
Cleanup:
|
||||
```
|
||||
```bash
|
||||
rm libzbar0_0.23.90-1_armhf.deb
|
||||
```
|
||||
|
||||
@@ -90,7 +130,7 @@ rm libzbar0_0.23.90-1_armhf.deb
|
||||
This library "provides functions for reading digital inputs and setting digital outputs, using SPI and I2C, and for accessing the system timers."
|
||||
|
||||
Run each of the following individual steps:
|
||||
```
|
||||
```bash
|
||||
wget http://www.airspayce.com/mikem/bcm2835/bcm2835-1.60.tar.gz
|
||||
tar zxvf bcm2835-1.60.tar.gz
|
||||
cd bcm2835-1.60/
|
||||
@@ -102,88 +142,82 @@ sudo rm -rf bcm2835-1.60
|
||||
```
|
||||
|
||||
### Set up `virtualenv`
|
||||
```
|
||||
pip3 install virtualenvwrapper
|
||||
```bash
|
||||
python -m pip install virtualenvwrapper
|
||||
```
|
||||
|
||||
Edit your bash profile with the command `nano ~/.profile` and add the following to the end:
|
||||
```
|
||||
```bash
|
||||
export WORKON_HOME=$HOME/.envs
|
||||
export VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3
|
||||
source /home/pi/.local/bin/virtualenvwrapper.sh
|
||||
```
|
||||
Then `CTRL-X` and `y` to exit and save changes.
|
||||
|
||||
Now create the python virtualenv for SeedSigner with these two commands:
|
||||
```
|
||||
Now create the virtualenv for SeedSigner:
|
||||
```bash
|
||||
source ~/.profile
|
||||
mkvirtualenv --python=python3 seedsigner-env
|
||||
mkvirtualenv seedsigner-env
|
||||
```
|
||||
|
||||
For convenience you can configure your `.profile` to auto-activate the SeedSigner virtualenv when you ssh in. Once again `nano ~/.profile` and add at the end:
|
||||
```
|
||||
```bash
|
||||
workon seedsigner-env
|
||||
```
|
||||
|
||||
Optional: If you're going to be testing new code on the SeedSigner, you'll find yourself often needing to kill the SeedSigner code that automatically runs at startup (we'll be configuring this further down). As an extra convenience you can list the process id so that you can then kill it from the terminal:
|
||||
```
|
||||
ps aux | grep main.py
|
||||
```
|
||||
|
||||
Save your changes with `CTRL-X` and `y`.
|
||||
|
||||
Now when you `ssh` in you'll see something like:
|
||||
```
|
||||
pi 297 65.4 9.7 74096 36736 ? Rsl 09:26 10:29 /home/pi/.envs/seedsigner-env/bin/python main.py
|
||||
pi 857 0.0 0.4 7332 1876 pts/0 S+ 09:42 0:00 grep --color=auto main.py
|
||||
```
|
||||
|
||||
The top line is our SeedSigner code running. To stop it, run:
|
||||
```
|
||||
kill 297
|
||||
```
|
||||
Where `297` is the process id listed in the output above (it'll be different each time).
|
||||
|
||||
|
||||
### Download the SeedSigner code:
|
||||
```
|
||||
```bash
|
||||
git clone https://github.com/SeedSigner/seedsigner
|
||||
cd seedsigner
|
||||
```
|
||||
|
||||
If you want to run a specific branch within the main SeedSigner repo, switch to it with:
|
||||
```
|
||||
git checkout yourtargetbranch
|
||||
```
|
||||
|
||||
And if you want to test a pull request (PR), for example PR #123:
|
||||
```
|
||||
git fetch origin pull/123/head:pr_123
|
||||
git checkout pr_123
|
||||
```
|
||||
|
||||
where `pr_123` is any name you want to give to the new branch in your local repo that will hold the PR.
|
||||
|
||||
|
||||
### Install Python `pip` dependencies:
|
||||
```
|
||||
pip3 install -r requirements.txt
|
||||
```bash
|
||||
# Takes 1hr 45min on a Pi Zero 1.3
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements-raspi.txt
|
||||
```
|
||||
|
||||
#### `pyzbar`
|
||||
Note: The `requirements.txt` installs a fork of the python `pyzbar` repo (for now pointing to the fork in Keith's `kdmukai` github account [https://github.com/kdmukai/pyzbar](https://github.com/kdmukai/pyzbar)).
|
||||
Note: The `requirements.txt` installs a fork of the python `pyzbar` repo.
|
||||
|
||||
The fork is required because the main `pyzbar` repo has been abandoned. This [github issue](https://github.com/NaturalHistoryMuseum/pyzbar/issues/124#issuecomment-971967091) discusses the changes needed in order to support reading binary data from `zbar`, which is required for our `CompactSeedQR` format which writes byte data instead of strings. The changes specifically reference the following PRs which have already been merged into Keith's fork:
|
||||
* [PR 76](https://github.com/NaturalHistoryMuseum/pyzbar/pull/76/files): enables scanning to continue even when a null byte (`x\00`) is found.
|
||||
* [PR 82](https://github.com/NaturalHistoryMuseum/pyzbar/pull/82): enable `zbar`'s new binary mode. Note that this PR has a trivial bug that was fixed in Keith's fork.
|
||||
* [PR 82](https://github.com/NaturalHistoryMuseum/pyzbar/pull/82): enable `zbar`'s new binary mode. Note that this PR has a trivial bug that was fixed in our fork.
|
||||
|
||||
|
||||
### Finish configuring the virtualenv
|
||||
Set the SeedSigner `src/` directory as the project directory for the virtualenv (this is where the virtualenv will take you when you activate it):
|
||||
```bash
|
||||
cd src
|
||||
setvirtualenvproject
|
||||
```
|
||||
|
||||
Test it out:
|
||||
```bash
|
||||
# exit the virtualenv
|
||||
deactivate
|
||||
|
||||
# change dirs to somewhere else
|
||||
cd ~
|
||||
|
||||
# activate the virtualenv
|
||||
workon seedsigner-env
|
||||
|
||||
# you should now be back in the SeedSigner src/ directory
|
||||
pwd
|
||||
```
|
||||
|
||||
|
||||
### Configure `systemd` to run SeedSigner at boot:
|
||||
```
|
||||
```bash
|
||||
sudo nano /etc/systemd/system/seedsigner.service
|
||||
```
|
||||
|
||||
Add the following contents to the text file that was created:
|
||||
```
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Seedsigner
|
||||
|
||||
@@ -199,46 +233,98 @@ WantedBy=multi-user.target
|
||||
|
||||
_Note: For local dev you'll want to edit the `Restart=always` line to `Restart=no`. This way when your dev code crashes it won't keep trying to restart itself. Note that the UI "Reset" will no longer work when auto-restarts are disabled._
|
||||
|
||||
_Note: Debugging output is completely wiped via routing the output to `/dev/null 2>&1`. When working in local dev, you're better off disabling the `systemd` SeedSigner service and just directly running the app so you can see all the debugging output live._
|
||||
_Note: Debugging output is completely wiped via routing the output to `/dev/null 2>&1`. When working in local dev, you'll `kill` the `systemd` SeedSigner service and just directly run the code on demand so you can see all the debugging output live._
|
||||
|
||||
Use `CTRL-X` and `y` to exit and save changes.
|
||||
|
||||
Configure the service to start running (this will restart the seedsigner code automatically at startup and if it crashes):
|
||||
```
|
||||
```bash
|
||||
sudo systemctl enable seedsigner.service
|
||||
```
|
||||
|
||||
Now reboot the Raspberry Pi:
|
||||
```
|
||||
```bash
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
After the Raspberry Pi reboots, you should see the SeedSigner splash screen and the SeedSigner menu subsequently appear on the LCD screen (note that it can take up to 60 seconds for the menu to appear).
|
||||
|
||||
|
||||
#### Optional: kill `systemd` SeedSigner process on login
|
||||
If you're going to be testing new code on the device, you'll find yourself often needing to kill the SeedSigner instance that `systemd` automatically runs at startup.
|
||||
|
||||
You can configure your `~/.profile` to find and kill the SeedSigner process when you ssh in.
|
||||
|
||||
`nano ~/.profile` and add at the end:
|
||||
```bash
|
||||
# Find the SeedSigner process and kill it
|
||||
kill $(ps aux | grep '[m]ain.py' | awk '{print $2}')
|
||||
```
|
||||
|
||||
|
||||
### Further OS modifications
|
||||
Disable and remove the system's virtual memory / swap file with the commands:
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo apt remove dphys-swapfile -y
|
||||
sudo apt autoremove -y
|
||||
sudo rm /var/swap
|
||||
```
|
||||
|
||||
## Local testing and development
|
||||
For those who will use the SeedSigner installation for testing/development, it can be helpful to change the system's host name so it doesn't potentially conflict with other Raspberry Pis that may already be present on your network. (For those who don't plan to use the installation for testing or development, you can skip this portion of the process.) To change the host name first edit the "hostname" with the command:
|
||||
## Manually start the SeedSigner code
|
||||
```bash
|
||||
# activate the virtualenv if you haven't already
|
||||
workon seedsigner-env
|
||||
|
||||
# You should now be in the SeedSigner src/ directory. List its contents:
|
||||
ls
|
||||
|
||||
# You should see the main.py file. Run it:
|
||||
python main.py
|
||||
|
||||
# To kill the process, use CTRL-C
|
||||
```
|
||||
|
||||
|
||||
## Local testing and development
|
||||
|
||||
### Run specific branches or PRs
|
||||
The default branch is `dev`. If you want to run a specific release tag or a specific branch:
|
||||
```bash
|
||||
# release tag for v0.6.0:
|
||||
git checkout 0.6.0
|
||||
```
|
||||
|
||||
And if you want to test a pull request (PR), for example PR #123:
|
||||
```bash
|
||||
git fetch origin pull/123/head:pr_123
|
||||
git checkout pr_123
|
||||
```
|
||||
|
||||
where `pr_123` is any name you want to give to the new branch in your local repo that will hold the PR.
|
||||
|
||||
|
||||
### Change the host name
|
||||
For those who will use the SeedSigner installation for testing/development, it can be helpful to change the system's host name so it doesn't potentially conflict with other Raspberry Pis that may already be present on your network. (For those who don't plan to use the installation for testing or development, you can skip this portion of the process.) To change the host name first edit the "hostname" with the command:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/hostname
|
||||
```
|
||||
and change "raspberrypi" to "seedsigner" (or another name). Use `CTRL-X` and `y` to exit and save changes. You'll also need to edit the "hosts" file with the command:
|
||||
```
|
||||
|
||||
and change "raspberrypi" to "seedsigner" (or another name). Use `CTRL-X` and `y` to exit and save changes.
|
||||
|
||||
You'll also need to edit the "hosts" file with the command:
|
||||
```bash
|
||||
sudo nano /etc/hosts
|
||||
```
|
||||
|
||||
and change "raspberrypi" to "seedsigner" (or the other name you previously chose). Use `CTRL-X` and `y` to exit and save changes.
|
||||
|
||||
### Set a static IP
|
||||
Your local machine that `ssh`s into the SeedSigner can sometimes get confused if you're connecting to different SeedSigners that are all identified as `pi@seedsigner.local`. In this case it helps to set a static ip and just `ssh` directly to that instead.
|
||||
|
||||
First find your current `nameserver`:
|
||||
```
|
||||
```bash
|
||||
sudo cat /etc/resolv.conf
|
||||
```
|
||||
|
||||
@@ -260,21 +346,26 @@ static domain_name_servers=192.168.1.254
|
||||
`CTRL-X` and `y` to save changes.
|
||||
|
||||
After your next reboot, access this SeedSigner using its new static ip:
|
||||
```
|
||||
```bash
|
||||
# Use the static ip you set above:
|
||||
ssh pi@192.168.1.200
|
||||
|
||||
# But the hostname will still work, too:
|
||||
ssh pi@seedsigner.local
|
||||
```
|
||||
|
||||
### More convenient `ssh` access:
|
||||
Power SeedSigner devs will find themselves connecting to a lot of different SeedSigners. This can cause headaches with `ssh`'s built-in protections; a different device that uses the same `ssh` credentials is normally a potential spoofing attack. But we're doing this to ourselves on purpose and so we can carve out exceptions.
|
||||
|
||||
On your local machine, run `nano ~/.ssh/config` and add to the end:
|
||||
```
|
||||
```conf
|
||||
host seedsigner.local
|
||||
StrictHostKeyChecking no
|
||||
UserKnownHostsFile /dev/null
|
||||
User pi
|
||||
LogLevel QUIET
|
||||
|
||||
# Set this to the static ip you set above:
|
||||
host 192.168.1.200
|
||||
StrictHostKeyChecking no
|
||||
UserKnownHostsFile /dev/null
|
||||
@@ -293,7 +384,7 @@ The second entry does the same for a specific static ip; you'll want this if you
|
||||
You can also configure the SeedSigner so that you don't have to enter the `pi` password when you `ssh` in.
|
||||
|
||||
run `ssh-copy-id` with the same values that you connect via `ssh`:
|
||||
```
|
||||
```bash
|
||||
ssh-copy-id pi@seedsigner.local
|
||||
|
||||
# or if you're connecting over static ip, something like:
|
||||
@@ -307,7 +398,7 @@ _Note: If you don't have any ssh keys on your local machine, you'll need to crea
|
||||
|
||||
## Disable wifi/Bluetooth when using other Raspi boards
|
||||
If you plan to use your installation on a Raspberry Pi that is not a Zero version 1.3, but rather on a Raspberry Pi that has WiFi and Bluetooth capabilities, it is a good idea to disable the following WiFi & Bluetooth, as well as other relevant services (assuming you are not creating this installation for testing/development purposes). Enter the followiing commands to disable WiFi, Bluetooth, & other relevant services:
|
||||
```
|
||||
```bash
|
||||
sudo systemctl disable bluetooth.service
|
||||
sudo systemctl disable wpa_supplicant.service
|
||||
sudo systemctl disable dhcpcd.service
|
||||
@@ -316,12 +407,13 @@ sudo systemctl disable networking.service
|
||||
sudo systemctl disable dphys-swapfile.service
|
||||
sudo ifconfig wlan0 down
|
||||
```
|
||||
|
||||
Please note that if you are using WiFi to connect/interact with your Raspberry Pi, the last command will sever that connection.
|
||||
|
||||
You can now safely power the Raspberry Pi off from the SeedSigner main menu.
|
||||
|
||||
If you do not plan to use your installation for testing/development, it is also a good idea to disable WiFi and Bluetooth by editing the config.txt file found in the installation's "boot" partition. You can add the following text to the end of that file with any simple text editor (Windows: Notepad, Mac: TextEdit, Linux: nano):
|
||||
```
|
||||
```ini
|
||||
dtoverlay=disable-bt
|
||||
dtoverlay=pi3-disable-wifi
|
||||
```
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
picamera==1.13
|
||||
RPi.GPIO==0.7.0
|
||||
spidev==3.5
|
||||
@@ -1,10 +1,7 @@
|
||||
embit==0.7.0
|
||||
numpy==1.21.1
|
||||
picamera==1.13
|
||||
Pillow==8.2.0
|
||||
-e git+https://github.com/seedsigner/pyzbar.git@c3c237821c6a20b17953efe59b90df0b514a1c03#egg=pyzbar
|
||||
qrcode==7.3.1
|
||||
RPi.GPIO==0.7.0
|
||||
six==1.16.0
|
||||
spidev==3.5
|
||||
urtypes @ git+https://github.com/selfcustody/urtypes.git@7fb280eab3b3563dfc57d2733b0bf5cbc0a96a6a
|
||||
|
||||
@@ -32,6 +32,23 @@ class BackStack(List[Destination]):
|
||||
|
||||
|
||||
|
||||
class StopFlowBasedTest(Exception):
|
||||
"""
|
||||
This is a special exception that is only raised by the test suite to stop the
|
||||
Controller's main loop. It should not be raised by any other code.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class FlowBasedTestException(Exception):
|
||||
"""
|
||||
This is a special exception that is only raised by the test suite.
|
||||
It should not be raised by any other code.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class Controller(Singleton):
|
||||
"""
|
||||
The Controller is a globally available singleton that maintains SeedSigner state.
|
||||
@@ -185,7 +202,13 @@ class Controller(Singleton):
|
||||
self.back_stack = BackStack()
|
||||
|
||||
|
||||
def start(self) -> None:
|
||||
def start(self, initial_destination: Destination = None) -> None:
|
||||
"""
|
||||
The main loop of the application.
|
||||
|
||||
* initial_destination: The first View to run. If None, the MainMenuView is
|
||||
used. Only used by the test suite.
|
||||
"""
|
||||
from .views import MainMenuView, BackStackView
|
||||
from .views.screensaver import OpeningSplashScreen
|
||||
|
||||
@@ -218,6 +241,9 @@ class Controller(Singleton):
|
||||
View_cls(**init_args).run()
|
||||
"""
|
||||
try:
|
||||
if initial_destination:
|
||||
next_destination = initial_destination
|
||||
else:
|
||||
next_destination = Destination(MainMenuView)
|
||||
while True:
|
||||
# Destination(None) is a special case; render the Home screen
|
||||
@@ -240,10 +266,24 @@ class Controller(Singleton):
|
||||
print(f"back_stack: {self.back_stack}")
|
||||
|
||||
try:
|
||||
# Instantiate the View class and run it
|
||||
print(f"Executing {next_destination}")
|
||||
next_destination = next_destination.run()
|
||||
|
||||
except StopFlowBasedTest:
|
||||
# This is a special exception that is only raised by the test suite
|
||||
# to stop the Controller loop and exit the test.
|
||||
return
|
||||
|
||||
except FlowBasedTestException as e:
|
||||
# This is a special exception that is only raised by the test suite.
|
||||
# Re-raise so the test suite can handle it.
|
||||
raise e
|
||||
|
||||
except Exception as e:
|
||||
# Display user-friendly error screen w/debugging info
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
next_destination = self.handle_exception(e)
|
||||
|
||||
if not next_destination:
|
||||
@@ -309,7 +349,12 @@ class Controller(Singleton):
|
||||
# So we extract the Exception type and trim off any "foo.bar." namespacing:
|
||||
last_line = traceback.format_exc().splitlines()[-1]
|
||||
exception_type = last_line.split(":")[0].split(".")[-1]
|
||||
|
||||
# Extract the error message, if there is one
|
||||
if ":" in last_line:
|
||||
exception_msg = last_line.split(":")[1]
|
||||
else:
|
||||
exception_msg = ""
|
||||
|
||||
# Scan for the last debugging line that includes a line number reference
|
||||
line_info = None
|
||||
|
||||
@@ -379,7 +379,8 @@ class SeedMnemonicEntryScreen(BaseTopNavScreen):
|
||||
|
||||
self.keyboard.render_keys()
|
||||
|
||||
elif input in HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN:
|
||||
elif input in HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN \
|
||||
or input in (Keyboard.ENTER_TOP, Keyboard.ENTER_BOTTOM):
|
||||
if ret_val in self.possible_alphabet:
|
||||
# Live joystick movement; haven't locked this new letter in yet.
|
||||
# Replace the last letter w/the currently selected one. But don't
|
||||
|
||||
@@ -153,7 +153,7 @@ class ST7789(object):
|
||||
if imwidth != self.width or imheight != self.height:
|
||||
raise ValueError('Image must be same dimensions as display \
|
||||
({0}x{1}).' .format(self.width, self.height))
|
||||
# 24-bit RGB-8:8:8 becomes 16-bit BGR-5:6:5 plus endianness toggle.
|
||||
# convert 24-bit RGB-8:8:8 to gBRG-3:5:5:3; then per-pixel byteswap to 16-bit RGB-5:6:5
|
||||
arr = array.array("H", Image.convert("BGR;16").tobytes())
|
||||
arr.byteswap()
|
||||
pix = arr.tobytes()
|
||||
|
||||
@@ -218,7 +218,7 @@ class SettingsEntry:
|
||||
visibility: str = SettingsConstants.VISIBILITY__GENERAL
|
||||
type: str = SettingsConstants.TYPE__ENABLED_DISABLED
|
||||
help_text: str = None
|
||||
selection_options: List[str] = None
|
||||
selection_options: list[tuple[str | int], str] = None
|
||||
default_value: Any = None
|
||||
|
||||
def __post_init__(self):
|
||||
@@ -440,7 +440,7 @@ class SettingsDefinition:
|
||||
type=SettingsConstants.TYPE__SELECT_1,
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
selection_options=SettingsConstants.ALL_CAMERA_ROTATIONS,
|
||||
default_value=SettingsConstants.CAMERA_ROTATION__0),
|
||||
default_value=SettingsConstants.CAMERA_ROTATION__180),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__COMPACT_SEEDQR,
|
||||
|
||||
@@ -11,9 +11,8 @@ from seedsigner.models.encode_qr import EncodeQR
|
||||
from seedsigner.models.psbt_parser import PSBTParser
|
||||
from seedsigner.models.qr_type import QRType
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
from seedsigner.gui.screens import psbt_screens
|
||||
from seedsigner.gui.screens.screen import (RET_CODE__BACK_BUTTON, ButtonListScreen, DireWarningScreen,
|
||||
LoadingScreenThread, QRDisplayScreen, WarningScreen)
|
||||
from seedsigner.gui.screens.psbt_screens import PSBTOverviewScreen, PSBTMathScreen, PSBTAddressDetailsScreen, PSBTChangeDetailsScreen, PSBTFinalizeScreen
|
||||
from seedsigner.gui.screens.screen import (RET_CODE__BACK_BUTTON, ButtonListScreen, DireWarningScreen, QRDisplayScreen)
|
||||
|
||||
from .view import BackStackView, MainMenuView, NotYetImplementedView, View, Destination
|
||||
|
||||
@@ -22,6 +21,11 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PSBTSelectSeedView(View):
|
||||
SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE)
|
||||
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
button_data = []
|
||||
|
||||
def run(self):
|
||||
# Note: we can't just autoroute to the PSBT Overview because we might have a
|
||||
# multisig where we want to sign with more than one key on this device.
|
||||
@@ -31,32 +35,29 @@ class PSBTSelectSeedView(View):
|
||||
|
||||
seeds = self.controller.storage.seeds
|
||||
|
||||
SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE)
|
||||
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
button_data = []
|
||||
for seed in seeds:
|
||||
button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
|
||||
if not PSBTParser.has_matching_input_fingerprint(psbt=self.controller.psbt, seed=seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)):
|
||||
# Doesn't look like this seed can sign the current PSBT
|
||||
button_str += " (?)"
|
||||
|
||||
button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue"))
|
||||
self.button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue"))
|
||||
|
||||
button_data.append(SCAN_SEED)
|
||||
button_data.append(TYPE_12WORD)
|
||||
button_data.append(TYPE_24WORD)
|
||||
self.button_data.append(self.SCAN_SEED)
|
||||
self.button_data.append(self.TYPE_12WORD)
|
||||
self.button_data.append(self.TYPE_24WORD)
|
||||
|
||||
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
|
||||
return Destination(PSBTOverviewView)
|
||||
|
||||
selected_menu_num = ButtonListScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Select Signer",
|
||||
is_button_text_centered=False,
|
||||
button_data=button_data
|
||||
).display()
|
||||
button_data=self.button_data
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
@@ -69,13 +70,13 @@ class PSBTSelectSeedView(View):
|
||||
# The remaining flows are a sub-flow; resume PSBT flow once the seed is loaded.
|
||||
self.controller.resume_main_flow = Controller.FLOW__PSBT
|
||||
|
||||
if button_data[selected_menu_num] == SCAN_SEED:
|
||||
if self.button_data[selected_menu_num] == self.SCAN_SEED:
|
||||
from seedsigner.views.scan_views import ScanView
|
||||
return Destination(ScanView)
|
||||
|
||||
elif button_data[selected_menu_num] in [TYPE_12WORD, TYPE_24WORD]:
|
||||
elif button_data[selected_menu_num] in [self.TYPE_12WORD, self.TYPE_24WORD]:
|
||||
from seedsigner.views.seed_views import SeedMnemonicEntryView
|
||||
if button_data[selected_menu_num] == TYPE_12WORD:
|
||||
if button_data[selected_menu_num] == self.TYPE_12WORD:
|
||||
self.controller.storage.init_pending_mnemonic(num_words=12)
|
||||
else:
|
||||
self.controller.storage.init_pending_mnemonic(num_words=24)
|
||||
@@ -92,8 +93,10 @@ class PSBTOverviewView(View):
|
||||
if not self.controller.psbt_parser or self.controller.psbt_parser.seed != self.controller.psbt_seed:
|
||||
# The PSBTParser takes a while to read the PSBT. Run the loading screen while
|
||||
# we wait.
|
||||
from seedsigner.gui.screens.screen import LoadingScreenThread
|
||||
self.loading_screen = LoadingScreenThread(text="Parsing PSBT...")
|
||||
self.loading_screen.start()
|
||||
|
||||
try:
|
||||
self.controller.psbt_parser = PSBTParser(
|
||||
self.controller.psbt,
|
||||
@@ -128,23 +131,22 @@ class PSBTOverviewView(View):
|
||||
else:
|
||||
num_self_transfer_outputs += 1
|
||||
|
||||
screen = psbt_screens.PSBTOverviewScreen(
|
||||
# Everything is set. Stop the loading screen
|
||||
if self.loading_screen:
|
||||
self.loading_screen.stop()
|
||||
|
||||
# Run the overview screen
|
||||
selected_menu_num = self.run_screen(
|
||||
PSBTOverviewScreen,
|
||||
spend_amount=psbt_parser.spend_amount,
|
||||
change_amount=psbt_parser.change_amount,
|
||||
fee_amount=psbt_parser.fee_amount,
|
||||
num_inputs=psbt_parser.num_inputs,
|
||||
num_self_transfer_outputs=num_self_transfer_outputs,
|
||||
num_change_outputs=num_change_outputs,
|
||||
destination_addresses=psbt_parser.destination_addresses,
|
||||
destination_addresses=psbt_parser.destination_addresses
|
||||
)
|
||||
|
||||
# Everything is set. Stop the loading screen
|
||||
if self.loading_screen:
|
||||
self.loading_screen.stop()
|
||||
|
||||
# Run the overview screen
|
||||
selected_menu_num = screen.display()
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
self.controller.psbt_seed = None
|
||||
return Destination(BackStackView)
|
||||
@@ -214,14 +216,15 @@ class PSBTMathView(View):
|
||||
# Should not be able to get here
|
||||
return Destination(MainMenuView)
|
||||
|
||||
selected_menu_num = psbt_screens.PSBTMathScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
PSBTMathScreen,
|
||||
input_amount=psbt_parser.input_amount,
|
||||
num_inputs=psbt_parser.num_inputs,
|
||||
spend_amount=psbt_parser.spend_amount,
|
||||
num_recipients=psbt_parser.num_destinations,
|
||||
fee_amount=psbt_parser.fee_amount,
|
||||
change_amount=psbt_parser.change_amount,
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
@@ -238,7 +241,10 @@ class PSBTAddressDetailsView(View):
|
||||
"""
|
||||
Shows the recipient's address and amount they will receive
|
||||
"""
|
||||
def __init__(self, address_num, is_change=False):
|
||||
NEXT = "Next"
|
||||
button_data = []
|
||||
|
||||
def __init__(self, address_num):
|
||||
super().__init__()
|
||||
self.address_num = address_num
|
||||
|
||||
@@ -254,23 +260,27 @@ class PSBTAddressDetailsView(View):
|
||||
if psbt_parser.num_destinations > 1:
|
||||
title += f" (#{self.address_num + 1})"
|
||||
|
||||
button_data = []
|
||||
if self.address_num < psbt_parser.num_destinations - 1:
|
||||
button_data.append("Next Recipient")
|
||||
self.NEXT = "Next Recipient"
|
||||
else:
|
||||
button_data.append("Next")
|
||||
self.NEXT = "Next"
|
||||
self.button_data.append(self.NEXT)
|
||||
|
||||
selected_menu_num = psbt_screens.PSBTAddressDetailsScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
PSBTAddressDetailsScreen,
|
||||
title=title,
|
||||
button_data=button_data,
|
||||
button_data=self.button_data,
|
||||
address=psbt_parser.destination_addresses[self.address_num],
|
||||
amount=psbt_parser.destination_amounts[self.address_num],
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == 0:
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
if self.button_data[selected_menu_num] == self.NEXT:
|
||||
if self.address_num < len(psbt_parser.destination_addresses) - 1:
|
||||
# Show the next receive addr
|
||||
return Destination(PSBTAddressDetailsView, view_args={"address_num": self.address_num + 1, "is_change": False})
|
||||
return Destination(PSBTAddressDetailsView, view_args={"address_num": self.address_num + 1})
|
||||
|
||||
elif psbt_parser.change_amount > 0:
|
||||
# Move on to display change
|
||||
@@ -280,14 +290,13 @@ class PSBTAddressDetailsView(View):
|
||||
# There's no change output to verify. Move on to sign the PSBT.
|
||||
return Destination(PSBTFinalizeView)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
|
||||
|
||||
class PSBTChangeDetailsView(View):
|
||||
"""
|
||||
"""
|
||||
NEXT = "Next"
|
||||
VERIFY_MULTISIG = "Verify Multisig Change"
|
||||
button_data = [NEXT]
|
||||
|
||||
def __init__(self, change_address_num):
|
||||
super().__init__()
|
||||
self.change_address_num = change_address_num
|
||||
@@ -327,14 +336,12 @@ class PSBTChangeDetailsView(View):
|
||||
is_change_derivation_path = int(derivation_path.split("/")[-2]) == 1
|
||||
derivation_path_addr_index = int(derivation_path.split("/")[-1])
|
||||
|
||||
NEXT = "Next"
|
||||
|
||||
if is_change_derivation_path:
|
||||
title = "Your Change"
|
||||
VERIFY_MULTISIG = "Verify Multisig Change"
|
||||
self.VERIFY_MULTISIG = "Verify Multisig Change"
|
||||
else:
|
||||
title = "Self-Transfer"
|
||||
VERIFY_MULTISIG = "Verify Multisig Addr"
|
||||
self.VERIFY_MULTISIG = "Verify Multisig Addr"
|
||||
# if psbt_parser.num_change_outputs > 1:
|
||||
# title += f" (#{self.change_address_num + 1})"
|
||||
|
||||
@@ -343,11 +350,11 @@ class PSBTChangeDetailsView(View):
|
||||
# if the known-good multisig descriptor is already onboard:
|
||||
if self.controller.multisig_wallet_descriptor:
|
||||
is_change_addr_verified = psbt_parser.verify_multisig_output(self.controller.multisig_wallet_descriptor, change_num=self.change_address_num)
|
||||
button_data = [NEXT]
|
||||
self.button_data = [self.NEXT]
|
||||
|
||||
else:
|
||||
# Have the Screen offer to load in the multisig descriptor.
|
||||
button_data = [VERIFY_MULTISIG, NEXT]
|
||||
self.button_data = [self.VERIFY_MULTISIG, self.NEXT]
|
||||
|
||||
else:
|
||||
# Single sig
|
||||
@@ -356,6 +363,7 @@ class PSBTChangeDetailsView(View):
|
||||
loading_screen_text = "Verifying Change..."
|
||||
else:
|
||||
loading_screen_text = "Verifying Self-Transfer..."
|
||||
from seedsigner.gui.screens.screen import LoadingScreenThread
|
||||
loading_screen = LoadingScreenThread(text=loading_screen_text)
|
||||
loading_screen.start()
|
||||
|
||||
@@ -389,7 +397,7 @@ class PSBTChangeDetailsView(View):
|
||||
|
||||
if change_data["address"] == calc_address:
|
||||
is_change_addr_verified = True
|
||||
button_data = [NEXT]
|
||||
self.button_data = [self.NEXT]
|
||||
|
||||
finally:
|
||||
loading_screen.stop()
|
||||
@@ -397,9 +405,10 @@ class PSBTChangeDetailsView(View):
|
||||
if is_change_addr_verified == False and (not psbt_parser.is_multisig or self.controller.multisig_wallet_descriptor is not None):
|
||||
return Destination(PSBTAddressVerificationFailedView, view_args=dict(is_change=is_change_derivation_path, is_multisig=psbt_parser.is_multisig), clear_history=True)
|
||||
|
||||
selected_menu_num = psbt_screens.PSBTChangeDetailsScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
PSBTChangeDetailsScreen,
|
||||
title=title,
|
||||
button_data=button_data,
|
||||
button_data=self.button_data,
|
||||
address=change_data.get("address"),
|
||||
amount=change_data.get("amount"),
|
||||
is_multisig=psbt_parser.is_multisig,
|
||||
@@ -408,19 +417,19 @@ class PSBTChangeDetailsView(View):
|
||||
is_change_derivation_path=is_change_derivation_path,
|
||||
derivation_path_addr_index=derivation_path_addr_index,
|
||||
is_change_addr_verified=is_change_addr_verified,
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
elif button_data[selected_menu_num] == NEXT:
|
||||
elif self.button_data[selected_menu_num] == self.NEXT:
|
||||
if self.change_address_num < psbt_parser.num_change_outputs - 1:
|
||||
return Destination(PSBTChangeDetailsView, view_args={"change_address_num": self.change_address_num + 1})
|
||||
else:
|
||||
# There's no more change to verify. Move on to sign the PSBT.
|
||||
return Destination(PSBTFinalizeView)
|
||||
|
||||
elif button_data[selected_menu_num] == VERIFY_MULTISIG:
|
||||
elif self.button_data[selected_menu_num] == self.VERIFY_MULTISIG:
|
||||
from seedsigner.views.seed_views import LoadMultisigWalletDescriptorView
|
||||
self.controller.resume_main_flow = Controller.FLOW__PSBT
|
||||
return Destination(LoadMultisigWalletDescriptorView)
|
||||
@@ -459,6 +468,9 @@ class PSBTAddressVerificationFailedView(View):
|
||||
class PSBTFinalizeView(View):
|
||||
"""
|
||||
"""
|
||||
APPROVE_PSBT = "Approve PSBT"
|
||||
button_data = [APPROVE_PSBT]
|
||||
|
||||
def run(self):
|
||||
psbt_parser: PSBTParser = self.controller.psbt_parser
|
||||
psbt: PSBT = self.controller.psbt
|
||||
@@ -467,11 +479,12 @@ class PSBTFinalizeView(View):
|
||||
# Should not be able to get here
|
||||
return Destination(MainMenuView)
|
||||
|
||||
selected_menu_num = psbt_screens.PSBTFinalizeScreen(
|
||||
button_data=["Approve PSBT"]
|
||||
).display()
|
||||
selected_menu_num = self.run_screen(
|
||||
PSBTFinalizeScreen,
|
||||
button_data=self.button_data
|
||||
)
|
||||
|
||||
if selected_menu_num == 0:
|
||||
if self.button_data[selected_menu_num] == self.APPROVE_PSBT:
|
||||
# Sign PSBT
|
||||
sig_cnt = PSBTParser.sig_count(psbt)
|
||||
psbt.sign_with(psbt_parser.root)
|
||||
@@ -500,7 +513,7 @@ class PSBTSignedQRDisplayView(View):
|
||||
qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY),
|
||||
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE),
|
||||
)
|
||||
QRDisplayScreen(qr_encoder=qr_encoder).display()
|
||||
self.run_screen(QRDisplayScreen, qr_encoder=qr_encoder)
|
||||
|
||||
# We're done with this PSBT. Route back to MainMenuView which always
|
||||
# clears all ephemeral data (except in-memory seeds).
|
||||
|
||||
@@ -4,6 +4,7 @@ import re
|
||||
from embit.descriptor import Descriptor
|
||||
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
|
||||
from seedsigner.gui.screens import scan_screens
|
||||
from seedsigner.models import DecodeQR, Seed
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
|
||||
@@ -12,19 +13,24 @@ from .view import BackStackView, MainMenuView, NotYetImplementedView, View, Dest
|
||||
|
||||
|
||||
class ScanView(View):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# Set up the QR decoder here so we can inject data into it in the test suite's
|
||||
# `before_run`.
|
||||
self.wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
|
||||
self.decoder = DecodeQR(wordlist_language_code=self.wordlist_language_code)
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.scan_screens import ScanScreen
|
||||
|
||||
wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
|
||||
self.decoder = DecodeQR(wordlist_language_code=wordlist_language_code)
|
||||
|
||||
# Start the live preview and background QR reading
|
||||
ScanScreen(decoder=self.decoder).display()
|
||||
self.run_screen(scan_screens.ScanScreen, decoder=self.decoder)
|
||||
|
||||
# Handle the results
|
||||
if self.decoder.is_complete:
|
||||
if self.decoder.is_seed:
|
||||
seed_mnemonic = self.decoder.get_seed_phrase()
|
||||
|
||||
if not seed_mnemonic:
|
||||
# seed is not valid, Exit if not valid with message
|
||||
raise Exception("Not yet implemented!")
|
||||
@@ -33,7 +39,7 @@ class ScanView(View):
|
||||
# pending (might set a passphrase, SeedXOR, etc) until finalized.
|
||||
from .seed_views import SeedFinalizeView
|
||||
self.controller.storage.set_pending_seed(
|
||||
Seed(mnemonic=seed_mnemonic, wordlist_language_code=wordlist_language_code)
|
||||
Seed(mnemonic=seed_mnemonic, wordlist_language_code=self.wordlist_language_code)
|
||||
)
|
||||
if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__REQUIRED:
|
||||
from seedsigner.views.seed_views import SeedAddPassphraseView
|
||||
|
||||
+184
-128
@@ -13,7 +13,7 @@ from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustom
|
||||
from seedsigner.helpers import embit_utils
|
||||
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen,
|
||||
WarningScreen, DireWarningScreen, seed_screens)
|
||||
from seedsigner.gui.screens.screen import LargeIconStatusScreen, LoadingScreenThread, QRDisplayScreen
|
||||
from seedsigner.gui.screens.screen import LargeIconStatusScreen, QRDisplayScreen
|
||||
from seedsigner.models.decode_qr import DecodeQR
|
||||
from seedsigner.models.encode_qr import EncodeQR
|
||||
from seedsigner.models.psbt_parser import PSBTParser
|
||||
@@ -31,6 +31,8 @@ from .view import NotYetImplementedView, View, Destination, BackStackView, MainM
|
||||
|
||||
|
||||
class SeedsMenuView(View):
|
||||
LOAD = "Load a seed"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.seeds = []
|
||||
@@ -50,11 +52,12 @@ class SeedsMenuView(View):
|
||||
button_data.append((seed["fingerprint"], SeedSignerCustomIconConstants.FINGERPRINT, "blue"))
|
||||
button_data.append("Load a seed")
|
||||
|
||||
selected_menu_num = ButtonListScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="In-Memory Seeds",
|
||||
is_button_text_centered=False,
|
||||
button_data=button_data
|
||||
).display()
|
||||
)
|
||||
|
||||
if len(self.seeds) > 0 and selected_menu_num < len(self.seeds):
|
||||
return Destination(SeedOptionsView, view_args={"seed_num": selected_menu_num})
|
||||
@@ -71,40 +74,41 @@ class SeedsMenuView(View):
|
||||
Loading seeds, passphrases, etc
|
||||
****************************************************************************"""
|
||||
class LoadSeedView(View):
|
||||
def run(self):
|
||||
SEED_QR = (" Scan a SeedQR", FontAwesomeIconConstants.QRCODE)
|
||||
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
CREATE = (" Create a seed", FontAwesomeIconConstants.PLUS)
|
||||
button_data=[
|
||||
SEED_QR,
|
||||
TYPE_12WORD,
|
||||
TYPE_24WORD,
|
||||
CREATE,
|
||||
]
|
||||
|
||||
selected_menu_num = ButtonListScreen(
|
||||
def run(self):
|
||||
button_data = [
|
||||
self.SEED_QR,
|
||||
self.TYPE_12WORD,
|
||||
self.TYPE_24WORD,
|
||||
self.CREATE,
|
||||
]
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Load A Seed",
|
||||
is_button_text_centered=False,
|
||||
button_data=button_data
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
if button_data[selected_menu_num] == SEED_QR:
|
||||
if button_data[selected_menu_num] == self.SEED_QR:
|
||||
from .scan_views import ScanView
|
||||
return Destination(ScanView)
|
||||
|
||||
elif button_data[selected_menu_num] == TYPE_12WORD:
|
||||
elif button_data[selected_menu_num] == self.TYPE_12WORD:
|
||||
self.controller.storage.init_pending_mnemonic(num_words=12)
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
|
||||
elif button_data[selected_menu_num] == TYPE_24WORD:
|
||||
elif button_data[selected_menu_num] == self.TYPE_24WORD:
|
||||
self.controller.storage.init_pending_mnemonic(num_words=24)
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
|
||||
elif button_data[selected_menu_num] == CREATE:
|
||||
elif button_data[selected_menu_num] == self.CREATE:
|
||||
from .tools_views import ToolsMenuView
|
||||
return Destination(ToolsMenuView)
|
||||
|
||||
@@ -119,22 +123,16 @@ class SeedMnemonicEntryView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
ret = seed_screens.SeedMnemonicEntryScreen(
|
||||
ret = self.run_screen(
|
||||
seed_screens.SeedMnemonicEntryScreen,
|
||||
title=f"Seed Word #{self.cur_word_index + 1}", # Human-readable 1-indexing!
|
||||
initial_letters=list(self.cur_word) if self.cur_word else ["a"],
|
||||
wordlist=Seed.get_wordlist(wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)),
|
||||
).display()
|
||||
)
|
||||
|
||||
if ret == RET_CODE__BACK_BUTTON:
|
||||
if self.cur_word_index > 0:
|
||||
return Destination(BackStackView)
|
||||
# return Destination(
|
||||
# SeedMnemonicEntryView,
|
||||
# view_args={
|
||||
# "cur_word_index": self.cur_word_index - 1,
|
||||
# "is_calc_final_word": self.is_calc_final_word
|
||||
# }
|
||||
# )
|
||||
else:
|
||||
self.controller.storage.discard_pending_mnemonic()
|
||||
return Destination(MainMenuView)
|
||||
@@ -174,34 +172,38 @@ class SeedMnemonicEntryView(View):
|
||||
|
||||
|
||||
class SeedMnemonicInvalidView(View):
|
||||
EDIT = "Review & Edit"
|
||||
DISCARD = ("Discard", None, None, "red")
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.mnemonic: List[str] = self.controller.storage.pending_mnemonic
|
||||
|
||||
|
||||
def run(self):
|
||||
EDIT = "Review & Edit"
|
||||
DISCARD = ("Discard", None, None, "red")
|
||||
button_data = [EDIT, DISCARD]
|
||||
|
||||
selected_menu_num = WarningScreen(
|
||||
button_data = [self.EDIT, self.DISCARD]
|
||||
selected_menu_num = self.run_screen(
|
||||
WarningScreen,
|
||||
title="Invalid Mnemonic!",
|
||||
status_headline=None,
|
||||
text=f"Checksum failure; not a valid seed phrase.",
|
||||
show_back_button=False,
|
||||
button_data=button_data,
|
||||
).display()
|
||||
)
|
||||
|
||||
if button_data[selected_menu_num] == EDIT:
|
||||
if button_data[selected_menu_num] == self.EDIT:
|
||||
return Destination(SeedMnemonicEntryView, view_args={"cur_word_index": 0})
|
||||
|
||||
elif button_data[selected_menu_num] == DISCARD:
|
||||
elif button_data[selected_menu_num] == self.DISCARD:
|
||||
self.controller.storage.discard_pending_mnemonic()
|
||||
return Destination(MainMenuView)
|
||||
|
||||
|
||||
|
||||
class SeedFinalizeView(View):
|
||||
FINALIZE = "Done"
|
||||
PASSPHRASE = "BIP-39 Passphrase"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.seed = self.controller.storage.get_pending_seed()
|
||||
@@ -209,25 +211,21 @@ class SeedFinalizeView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
FINALIZE = "Done"
|
||||
PASSPHRASE = "BIP-39 Passphrase"
|
||||
button_data = []
|
||||
|
||||
button_data.append(FINALIZE)
|
||||
|
||||
button_data = [self.FINALIZE]
|
||||
if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) != SettingsConstants.OPTION__DISABLED:
|
||||
button_data.append(PASSPHRASE)
|
||||
button_data.append(self.PASSPHRASE)
|
||||
|
||||
selected_menu_num = seed_screens.SeedFinalizeScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
seed_screens.SeedFinalizeScreen,
|
||||
fingerprint=self.fingerprint,
|
||||
button_data=button_data,
|
||||
).display()
|
||||
)
|
||||
|
||||
if button_data[selected_menu_num] == FINALIZE:
|
||||
if button_data[selected_menu_num] == self.FINALIZE:
|
||||
seed_num = self.controller.storage.finalize_pending_seed()
|
||||
return Destination(SeedOptionsView, view_args={"seed_num": seed_num}, clear_history=True)
|
||||
|
||||
elif button_data[selected_menu_num] == PASSPHRASE:
|
||||
elif button_data[selected_menu_num] == self.PASSPHRASE:
|
||||
return Destination(SeedAddPassphraseView)
|
||||
|
||||
|
||||
@@ -239,7 +237,7 @@ class SeedAddPassphraseView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
ret = seed_screens.SeedAddPassphraseScreen(passphrase=self.seed.passphrase).display()
|
||||
ret = self.run_screen(seed_screens.SeedAddPassphraseScreen, passphrase=self.seed.passphrase)
|
||||
|
||||
if ret == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
@@ -257,16 +255,15 @@ class SeedReviewPassphraseView(View):
|
||||
"""
|
||||
Display the completed passphrase back to the user.
|
||||
"""
|
||||
EDIT = "Edit passphrase"
|
||||
DONE = "Done"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.seed = self.controller.storage.get_pending_seed()
|
||||
|
||||
|
||||
def run(self):
|
||||
EDIT = "Edit passphrase"
|
||||
DONE = "Done"
|
||||
button_data = [EDIT, DONE]
|
||||
|
||||
# Get the before/after fingerprints
|
||||
network = self.settings.get_value(SettingsConstants.SETTING__NETWORK)
|
||||
passphrase = self.seed.passphrase
|
||||
@@ -275,26 +272,32 @@ class SeedReviewPassphraseView(View):
|
||||
fingerprint_without = self.seed.get_fingerprint(network=network)
|
||||
self.seed.set_passphrase(passphrase)
|
||||
|
||||
# Because we have ane explicit "Edit" button, we disable "BACK" to keep the
|
||||
button_data = [self.EDIT, self.DONE]
|
||||
|
||||
# Because we have an explicit "Edit" button, we disable "BACK" to keep the
|
||||
# routing options sane.
|
||||
selected_menu_num = seed_screens.SeedReviewPassphraseScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
seed_screens.SeedReviewPassphraseScreen,
|
||||
fingerprint_without=fingerprint_without,
|
||||
fingerprint_with=fingerprint_with,
|
||||
passphrase=self.seed.passphrase,
|
||||
button_data=button_data,
|
||||
show_back_button=False,
|
||||
).display()
|
||||
)
|
||||
|
||||
if button_data[selected_menu_num] == EDIT:
|
||||
if button_data[selected_menu_num] == self.EDIT:
|
||||
return Destination(SeedAddPassphraseView)
|
||||
|
||||
elif button_data[selected_menu_num] == DONE:
|
||||
elif button_data[selected_menu_num] == self.DONE:
|
||||
seed_num = self.controller.storage.finalize_pending_seed()
|
||||
return Destination(SeedOptionsView, view_args={"seed_num": seed_num}, clear_history=True)
|
||||
|
||||
|
||||
|
||||
class SeedDiscardView(View):
|
||||
KEEP = "Keep Seed"
|
||||
DISCARD = ("Discard", None, None, "red")
|
||||
|
||||
def __init__(self, seed_num: int = None):
|
||||
super().__init__()
|
||||
self.seed_num = seed_num
|
||||
@@ -305,27 +308,26 @@ class SeedDiscardView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
KEEP = "Keep Seed"
|
||||
DISCARD = ("Discard", None, None, "red")
|
||||
button_data = [KEEP, DISCARD]
|
||||
button_data = [self.KEEP, self.DISCARD]
|
||||
|
||||
fingerprint = self.seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
|
||||
selected_menu_num = WarningScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
WarningScreen,
|
||||
title="Discard Seed?",
|
||||
status_headline=None,
|
||||
text=f"Wipe seed {fingerprint} from the device?",
|
||||
show_back_button=False,
|
||||
button_data=button_data,
|
||||
).display()
|
||||
)
|
||||
|
||||
if button_data[selected_menu_num] == KEEP:
|
||||
if button_data[selected_menu_num] == self.KEEP:
|
||||
# Use skip_current_view=True to prevent BACK from landing on this warning screen
|
||||
if self.seed_num is not None:
|
||||
return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num}, skip_current_view=True)
|
||||
else:
|
||||
return Destination(SeedFinalizeView, skip_current_view=True)
|
||||
|
||||
elif button_data[selected_menu_num] == DISCARD:
|
||||
elif button_data[selected_menu_num] == self.DISCARD:
|
||||
if self.seed_num is not None:
|
||||
self.controller.discard_seed(self.seed_num)
|
||||
else:
|
||||
@@ -338,6 +340,15 @@ class SeedDiscardView(View):
|
||||
Views for actions on individual seeds:
|
||||
****************************************************************************"""
|
||||
class SeedOptionsView(View):
|
||||
SCAN_PSBT = ("Scan PSBT", FontAwesomeIconConstants.QRCODE)
|
||||
VERIFY_ADDRESS = "Verify Addr"
|
||||
EXPORT_XPUB = "Export Xpub"
|
||||
EXPLORER = "Address Explorer"
|
||||
BACKUP = ("Backup Seed", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)
|
||||
BIP85_CHILD_SEED = "BIP-85 Child Seed"
|
||||
DISCARD = ("Discard Seed", None, None, "red")
|
||||
|
||||
|
||||
def __init__(self, seed_num: int):
|
||||
super().__init__()
|
||||
self.seed_num = seed_num
|
||||
@@ -347,31 +358,17 @@ class SeedOptionsView(View):
|
||||
def run(self):
|
||||
from seedsigner.views.psbt_views import PSBTOverviewView
|
||||
|
||||
SCAN_PSBT = ("Scan PSBT", FontAwesomeIconConstants.QRCODE)
|
||||
VERIFY_ADDRESS = "Verify Addr"
|
||||
EXPORT_XPUB = "Export Xpub"
|
||||
EXPLORER = "Address Explorer"
|
||||
BACKUP = ("Backup Seed", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)
|
||||
BIP85_CHILD_SEED = "BIP-85 Child Seed"
|
||||
DISCARD = ("Discard Seed", None, None, "red")
|
||||
|
||||
button_data = []
|
||||
|
||||
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
|
||||
# Jump straight back into the address explorer script type selection flow
|
||||
# But do ont cancel the `resume_main_flow` as we'll still need that after
|
||||
# derivation path is specified.
|
||||
return Destination(SeedExportXpubScriptTypeView, view_args=dict(seed_num=self.seed_num, sig_type=SettingsConstants.SINGLE_SIG), skip_current_view=True)
|
||||
|
||||
if self.controller.unverified_address:
|
||||
if self.controller.resume_main_flow == Controller.FLOW__VERIFY_SINGLESIG_ADDR:
|
||||
# Jump straight back into the single sig addr verification flow
|
||||
self.controller.resume_main_flow = None
|
||||
return Destination(SeedAddressVerificationView, view_args=dict(seed_num=self.seed_num), skip_current_view=True)
|
||||
|
||||
addr = self.controller.unverified_address["address"][:7]
|
||||
VERIFY_ADDRESS += f" {addr}"
|
||||
button_data.append(VERIFY_ADDRESS)
|
||||
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
|
||||
# Jump straight back into the address explorer script type selection flow
|
||||
# But don't cancel the `resume_main_flow` as we'll still need that after
|
||||
# derivation path is specified.
|
||||
return Destination(SeedExportXpubScriptTypeView, view_args=dict(seed_num=self.seed_num, sig_type=SettingsConstants.SINGLE_SIG), skip_current_view=True)
|
||||
|
||||
if self.controller.psbt:
|
||||
if PSBTParser.has_matching_input_fingerprint(self.controller.psbt, self.seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)):
|
||||
@@ -381,56 +378,67 @@ class SeedOptionsView(View):
|
||||
self.controller.psbt_seed = self.seed
|
||||
return Destination(PSBTOverviewView, skip_current_view=True)
|
||||
|
||||
button_data.append(SCAN_PSBT)
|
||||
button_data = []
|
||||
|
||||
if self.controller.unverified_address:
|
||||
addr = self.controller.unverified_address["address"][:7]
|
||||
self.VERIFY_ADDRESS += f" {addr}"
|
||||
button_data.append(self.VERIFY_ADDRESS)
|
||||
|
||||
button_data.append(self.SCAN_PSBT)
|
||||
|
||||
if self.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED:
|
||||
button_data.append(EXPORT_XPUB)
|
||||
button_data.append(self.EXPORT_XPUB)
|
||||
|
||||
button_data.append(EXPLORER)
|
||||
button_data.append(BACKUP)
|
||||
button_data.append(self.EXPLORER)
|
||||
button_data.append(self.BACKUP)
|
||||
|
||||
if self.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__ENABLED:
|
||||
button_data.append(BIP85_CHILD_SEED)
|
||||
button_data.append(self.BIP85_CHILD_SEED)
|
||||
|
||||
button_data.append(DISCARD)
|
||||
button_data.append(self.DISCARD)
|
||||
|
||||
selected_menu_num = seed_screens.SeedOptionsScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
seed_screens.SeedOptionsScreen,
|
||||
button_data=button_data,
|
||||
fingerprint=self.seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)),
|
||||
has_passphrase=self.seed.passphrase is not None,
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
# Force BACK to always return to the Main Menu
|
||||
return Destination(MainMenuView)
|
||||
|
||||
if button_data[selected_menu_num] == SCAN_PSBT:
|
||||
if button_data[selected_menu_num] == self.SCAN_PSBT:
|
||||
from seedsigner.views.scan_views import ScanView
|
||||
self.controller.psbt_seed = self.controller.get_seed(self.seed_num)
|
||||
return Destination(ScanView)
|
||||
|
||||
elif button_data[selected_menu_num] == VERIFY_ADDRESS:
|
||||
elif button_data[selected_menu_num] == self.VERIFY_ADDRESS:
|
||||
return Destination(SeedAddressVerificationView, view_args=dict(seed_num=self.seed_num))
|
||||
|
||||
elif button_data[selected_menu_num] == EXPORT_XPUB:
|
||||
elif button_data[selected_menu_num] == self.EXPORT_XPUB:
|
||||
return Destination(SeedExportXpubSigTypeView, view_args=dict(seed_num=self.seed_num))
|
||||
|
||||
elif button_data[selected_menu_num] == EXPLORER:
|
||||
elif button_data[selected_menu_num] == self.EXPLORER:
|
||||
self.controller.resume_main_flow = Controller.FLOW__ADDRESS_EXPLORER
|
||||
return Destination(SeedExportXpubScriptTypeView, view_args=dict(seed_num=self.seed_num, sig_type=SettingsConstants.SINGLE_SIG))
|
||||
|
||||
elif button_data[selected_menu_num] == BACKUP:
|
||||
elif button_data[selected_menu_num] == self.BACKUP:
|
||||
return Destination(SeedBackupView, view_args=dict(seed_num=self.seed_num))
|
||||
|
||||
elif button_data[selected_menu_num] == BIP85_CHILD_SEED:
|
||||
elif button_data[selected_menu_num] == self.BIP85_CHILD_SEED:
|
||||
return Destination(SeedBIP85ApplicationModeView, view_args={"seed_num": self.seed_num})
|
||||
|
||||
elif button_data[selected_menu_num] == DISCARD:
|
||||
elif button_data[selected_menu_num] == self.DISCARD:
|
||||
return Destination(SeedDiscardView, view_args=dict(seed_num=self.seed_num))
|
||||
|
||||
|
||||
|
||||
class SeedBackupView(View):
|
||||
VIEW_WORDS = "View Seed Words"
|
||||
EXPORT_SEEDQR = "Export as SeedQR"
|
||||
|
||||
def __init__(self, seed_num):
|
||||
super().__init__()
|
||||
self.seed_num = seed_num
|
||||
@@ -438,23 +446,22 @@ class SeedBackupView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
VIEW_WORDS = "View Seed Words"
|
||||
EXPORT_SEEDQR = "Export as SeedQR"
|
||||
button_data = [VIEW_WORDS, EXPORT_SEEDQR]
|
||||
button_data = [self.VIEW_WORDS, self.EXPORT_SEEDQR]
|
||||
|
||||
selected_menu_num = ButtonListScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Backup Seed",
|
||||
button_data=button_data,
|
||||
is_bottom_list=True,
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
elif button_data[selected_menu_num] == VIEW_WORDS:
|
||||
elif button_data[selected_menu_num] == self.VIEW_WORDS:
|
||||
return Destination(SeedWordsWarningView, view_args={"seed_num": self.seed_num})
|
||||
|
||||
elif button_data[selected_menu_num] == EXPORT_SEEDQR:
|
||||
elif button_data[selected_menu_num] == self.EXPORT_SEEDQR:
|
||||
return Destination(SeedTranscribeSeedQRFormatView, view_args={"seed_num": self.seed_num})
|
||||
|
||||
|
||||
@@ -463,6 +470,9 @@ class SeedBackupView(View):
|
||||
Export Xpub flow
|
||||
****************************************************************************"""
|
||||
class SeedExportXpubSigTypeView(View):
|
||||
SINGLE_SIG = "Single Sig"
|
||||
MULTISIG = "Multisig"
|
||||
|
||||
def __init__(self, seed_num: int):
|
||||
super().__init__()
|
||||
self.seed_num = seed_num
|
||||
@@ -473,22 +483,21 @@ class SeedExportXpubSigTypeView(View):
|
||||
# Nothing to select; skip this screen
|
||||
return Destination(SeedExportXpubScriptTypeView, view_args={"seed_num": self.seed_num, "sig_type": self.settings.get_value(SettingsConstants.SETTING__SIG_TYPES)[0]}, skip_current_view=True)
|
||||
|
||||
SINGLE_SIG = "Single Sig"
|
||||
MULTISIG = "Multisig"
|
||||
button_data=[SINGLE_SIG, MULTISIG]
|
||||
button_data=[self.SINGLE_SIG, self.MULTISIG]
|
||||
|
||||
selected_menu_num = ButtonListScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Export Xpub",
|
||||
button_data=button_data
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
if button_data[selected_menu_num] == SINGLE_SIG:
|
||||
if button_data[selected_menu_num] == self.SINGLE_SIG:
|
||||
return Destination(SeedExportXpubScriptTypeView, view_args={"seed_num": self.seed_num, "sig_type": SettingsConstants.SINGLE_SIG})
|
||||
|
||||
elif button_data[selected_menu_num] == MULTISIG:
|
||||
elif button_data[selected_menu_num] == self.MULTISIG:
|
||||
return Destination(SeedExportXpubScriptTypeView, view_args={"seed_num": self.seed_num, "sig_type": SettingsConstants.MULTISIG})
|
||||
|
||||
|
||||
@@ -513,20 +522,21 @@ class SeedExportXpubScriptTypeView(View):
|
||||
else:
|
||||
return Destination(SeedExportXpubCoordinatorView, view_args=args, skip_current_view=True)
|
||||
|
||||
button_data = []
|
||||
for script_type in self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES):
|
||||
button_data.append(script_type)
|
||||
|
||||
title = "Export Xpub"
|
||||
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
|
||||
title = "Address Explorer"
|
||||
|
||||
selected_menu_num = ButtonListScreen(
|
||||
button_data = []
|
||||
for script_type in self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES):
|
||||
button_data.append(script_type)
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title=title,
|
||||
is_button_text_centered=False,
|
||||
button_data=button_data,
|
||||
is_bottom_list=True,
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
# If previous view is SeedOptionsView then that should be where resume_main_flow started (otherwise it would have been skipped).
|
||||
@@ -558,10 +568,12 @@ class SeedExportXpubCustomDerivationView(View):
|
||||
self.script_type = script_type
|
||||
self.custom_derivation_path = "m/"
|
||||
|
||||
|
||||
def run(self):
|
||||
ret = seed_screens.SeedExportXpubCustomDerivationScreen(
|
||||
ret = self.run_screen(
|
||||
seed_screens.SeedExportXpubCustomDerivationScreen,
|
||||
initial_value=self.custom_derivation_path,
|
||||
).display()
|
||||
)
|
||||
|
||||
if ret == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
@@ -606,11 +618,12 @@ class SeedExportXpubCoordinatorView(View):
|
||||
args["coordinator"] = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[0]
|
||||
return Destination(SeedExportXpubWarningView, view_args=args, skip_current_view=True)
|
||||
|
||||
selected_menu_num = ButtonListScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Export Xpub",
|
||||
is_button_text_centered=False,
|
||||
button_data=self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS),
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num < len(self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)):
|
||||
args["coordinator"] = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[selected_menu_num]
|
||||
@@ -648,10 +661,11 @@ class SeedExportXpubWarningView(View):
|
||||
# Skip the WarningView entirely
|
||||
return destination
|
||||
|
||||
selected_menu_num = WarningScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
WarningScreen,
|
||||
status_headline="Privacy Leak!",
|
||||
text="""Xpub can be used to view all future transactions.""",
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == 0:
|
||||
# User clicked "I Understand"
|
||||
@@ -694,6 +708,7 @@ class SeedExportXpubDetailsView(View):
|
||||
|
||||
else:
|
||||
# The derivation calc takes a few moments. Run the loading screen while we wait.
|
||||
from seedsigner.gui.screens.screen import LoadingScreenThread
|
||||
self.loading_screen = LoadingScreenThread(text="Generating xpub...")
|
||||
self.loading_screen.start()
|
||||
|
||||
@@ -716,12 +731,13 @@ class SeedExportXpubDetailsView(View):
|
||||
finally:
|
||||
self.loading_screen.stop()
|
||||
|
||||
selected_menu_num = seed_screens.SeedExportXpubDetailsScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
seed_screens.SeedExportXpubDetailsScreen,
|
||||
fingerprint=fingerprint,
|
||||
has_passphrase=self.seed.passphrase is not None,
|
||||
derivation_path=derivation_path,
|
||||
xpub=xpub_base58,
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == 0:
|
||||
return Destination(
|
||||
@@ -773,7 +789,10 @@ class SeedExportXpubQRDisplayView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
QRDisplayScreen(qr_encoder=self.qr_encoder).display()
|
||||
self.run_screen(
|
||||
QRDisplayScreen,
|
||||
qr_encoder=self.qr_encoder
|
||||
)
|
||||
|
||||
return Destination(MainMenuView)
|
||||
|
||||
@@ -803,9 +822,10 @@ class SeedWordsWarningView(View):
|
||||
# Forward straight to showing the words
|
||||
return destination
|
||||
|
||||
selected_menu_num = DireWarningScreen(
|
||||
text="""You must keep your seed words private & away from all online devices.""",
|
||||
).display()
|
||||
selected_menu_num = self.run_screen(
|
||||
DireWarningScreen,
|
||||
text="""Never input your seed phrase into a device that connects to the internet.""",
|
||||
)
|
||||
|
||||
if selected_menu_num == 0:
|
||||
# User clicked "I Understand"
|
||||
@@ -942,6 +962,16 @@ class SeedBIP85SelectChildIndexView(View):
|
||||
if ret == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
if not 0 <= int(ret) < 2**31:
|
||||
return Destination(
|
||||
SeedBIP85InvalidChildIndexView,
|
||||
view_args=dict(
|
||||
seed_num=self.seed_num,
|
||||
num_words=self.num_words
|
||||
),
|
||||
skip_current_view=True
|
||||
)
|
||||
|
||||
return Destination(
|
||||
SeedWordsWarningView,
|
||||
view_args=dict(
|
||||
@@ -951,6 +981,32 @@ class SeedBIP85SelectChildIndexView(View):
|
||||
)
|
||||
|
||||
|
||||
class SeedBIP85InvalidChildIndexView(View):
|
||||
def __init__(self, seed_num: int, num_words: int):
|
||||
super().__init__()
|
||||
self.seed_num = seed_num
|
||||
self.num_words = num_words
|
||||
|
||||
|
||||
def run(self):
|
||||
DireWarningScreen(
|
||||
title="BIP-85 Index Error",
|
||||
show_back_button=False,
|
||||
status_headline=f"Invalid Child Index",
|
||||
text=f"BIP-85 Child Index must be between 0 and {2**31-1}.",
|
||||
button_data=["Try Again"]
|
||||
).display()
|
||||
|
||||
return Destination(
|
||||
SeedBIP85SelectChildIndexView,
|
||||
view_args=dict(
|
||||
seed_num=self.seed_num,
|
||||
num_words=self.num_words
|
||||
),
|
||||
skip_current_view=True
|
||||
)
|
||||
|
||||
|
||||
|
||||
"""****************************************************************************
|
||||
Seed Words Backup Test
|
||||
@@ -1206,7 +1262,7 @@ class SeedTranscribeSeedQRWarningView(View):
|
||||
|
||||
selected_menu_num = DireWarningScreen(
|
||||
status_headline="SeedQR is your private key!",
|
||||
text="""Never photograph it or scan it into an online device.""",
|
||||
text="""Never photograph or scan it into a device that connects to the internet.""",
|
||||
).display()
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustomIconConstants
|
||||
from seedsigner.models.decode_qr import DecodeQR
|
||||
from seedsigner.gui.components import SeedSignerCustomIconConstants
|
||||
|
||||
from .view import View, Destination, BackStackView, MainMenuView
|
||||
from .view import View, Destination, MainMenuView
|
||||
|
||||
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, settings_screens)
|
||||
from seedsigner.models.settings import SettingsConstants, SettingsDefinition
|
||||
@@ -9,6 +8,9 @@ from seedsigner.models.settings import SettingsConstants, SettingsDefinition
|
||||
|
||||
|
||||
class SettingsMenuView(View):
|
||||
IO_TEST = "I/O test"
|
||||
DONATE = "Donate"
|
||||
|
||||
def __init__(self, visibility: str = SettingsConstants.VISIBILITY__GENERAL, selected_attr: str = None, initial_scroll: int = 0):
|
||||
super().__init__()
|
||||
self.visibility = visibility
|
||||
@@ -19,9 +21,6 @@ class SettingsMenuView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
IO_TEST = "I/O test"
|
||||
DONATE = "Donate"
|
||||
|
||||
settings_entries = SettingsDefinition.get_settings_entries(
|
||||
visibility=self.visibility
|
||||
)
|
||||
@@ -39,34 +38,34 @@ class SettingsMenuView(View):
|
||||
|
||||
# Set up the next nested level of menuing
|
||||
button_data.append(("Advanced", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT))
|
||||
next = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__ADVANCED})
|
||||
next_destination = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__ADVANCED})
|
||||
|
||||
button_data.append(IO_TEST)
|
||||
button_data.append(DONATE)
|
||||
button_data.append(self.IO_TEST)
|
||||
button_data.append(self.DONATE)
|
||||
|
||||
elif self.visibility == SettingsConstants.VISIBILITY__ADVANCED:
|
||||
title = "Advanced"
|
||||
|
||||
# So far there are no real Developer options; disabling for now
|
||||
# button_data.append(("Developer Options", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT))
|
||||
# next = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__DEVELOPER})
|
||||
next = None
|
||||
# next_destination = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__DEVELOPER})
|
||||
next_destination = None
|
||||
|
||||
elif self.visibility == SettingsConstants.VISIBILITY__DEVELOPER:
|
||||
title = "Dev Options"
|
||||
next = None
|
||||
next_destination = None
|
||||
|
||||
screen = ButtonListScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title=title,
|
||||
is_button_text_centered=False,
|
||||
button_data=button_data,
|
||||
selected_button=selected_button,
|
||||
scroll_y_initial_offset=self.initial_scroll,
|
||||
)
|
||||
selected_menu_num = screen.display()
|
||||
|
||||
# Preserve our scroll position in this Screen so we can return
|
||||
initial_scroll = screen.buttons[0].scroll_y
|
||||
initial_scroll = self.screen.buttons[0].scroll_y
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
if self.visibility == SettingsConstants.VISIBILITY__GENERAL:
|
||||
@@ -77,12 +76,12 @@ class SettingsMenuView(View):
|
||||
return Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__ADVANCED})
|
||||
|
||||
elif selected_menu_num == len(settings_entries):
|
||||
return next
|
||||
return next_destination
|
||||
|
||||
elif len(button_data) > selected_menu_num and button_data[selected_menu_num] == IO_TEST:
|
||||
elif len(button_data) > selected_menu_num and button_data[selected_menu_num] == self.IO_TEST:
|
||||
return Destination(IOTestView)
|
||||
|
||||
elif len(button_data) > selected_menu_num and button_data[selected_menu_num] == DONATE:
|
||||
elif len(button_data) > selected_menu_num and button_data[selected_menu_num] == self.DONATE:
|
||||
return Destination(DonateView)
|
||||
|
||||
else:
|
||||
@@ -95,10 +94,10 @@ class SettingsEntryUpdateSelectionView(View):
|
||||
Handles changes to all selection-type settings (Multiselect, SELECT_1,
|
||||
Enabled/Disabled, etc).
|
||||
"""
|
||||
def __init__(self, attr_name: str, parent_initial_scroll: int = 0):
|
||||
def __init__(self, attr_name: str, parent_initial_scroll: int = 0, selected_button: int = None):
|
||||
super().__init__()
|
||||
self.settings_entry = SettingsDefinition.get_settings_entry(attr_name)
|
||||
self.selected_button = None
|
||||
self.selected_button = selected_button
|
||||
self.parent_initial_scroll = parent_initial_scroll
|
||||
|
||||
|
||||
@@ -111,7 +110,9 @@ class SettingsEntryUpdateSelectionView(View):
|
||||
value, display_name = value
|
||||
else:
|
||||
display_name = value
|
||||
|
||||
button_data.append(display_name)
|
||||
|
||||
if (type(initial_value) == list and value in initial_value) or value == initial_value:
|
||||
checked_buttons.append(i)
|
||||
|
||||
@@ -120,17 +121,18 @@ class SettingsEntryUpdateSelectionView(View):
|
||||
# selected option).
|
||||
self.selected_button = i
|
||||
|
||||
if not self.selected_button:
|
||||
if self.selected_button is None:
|
||||
self.selected_button = 0
|
||||
|
||||
ret_value = settings_screens.SettingsEntryUpdateSelectionScreen(
|
||||
ret_value = self.run_screen(
|
||||
settings_screens.SettingsEntryUpdateSelectionScreen,
|
||||
display_name=self.settings_entry.display_name,
|
||||
help_text=self.settings_entry.help_text,
|
||||
button_data=button_data,
|
||||
selected_button=self.selected_button,
|
||||
checked_buttons=checked_buttons,
|
||||
settings_entry_type=self.settings_entry.type,
|
||||
).display()
|
||||
)
|
||||
|
||||
destination = None
|
||||
settings_menu_view_destination = Destination(
|
||||
@@ -178,7 +180,8 @@ class SettingsEntryUpdateSelectionView(View):
|
||||
|
||||
# All selects stay in place; re-initialize where in the list we left off
|
||||
self.selected_button = ret_value
|
||||
return self.run()
|
||||
|
||||
return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=self.settings_entry.attr_name, parent_initial_scroll=self.parent_initial_scroll, selected_button=self.selected_button), skip_current_view=True)
|
||||
|
||||
|
||||
|
||||
@@ -187,7 +190,7 @@ class SettingsEntryUpdateSelectionView(View):
|
||||
****************************************************************************"""
|
||||
class IOTestView(View):
|
||||
def run(self):
|
||||
settings_screens.IOTestScreen().display()
|
||||
self.run_screen(settings_screens.IOTestScreen)
|
||||
|
||||
return Destination(SettingsMenuView)
|
||||
|
||||
@@ -195,6 +198,6 @@ class IOTestView(View):
|
||||
|
||||
class DonateView(View):
|
||||
def run(self):
|
||||
settings_screens.DonateScreen().display()
|
||||
self.run_screen(settings_screens.DonateScreen)
|
||||
|
||||
return Destination(SettingsMenuView)
|
||||
|
||||
@@ -7,8 +7,6 @@ from embit.descriptor import Descriptor
|
||||
from PIL import Image
|
||||
from PIL.ImageOps import autocontrast
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.gui.screens.screen import LoadingScreenThread, QRDisplayScreen
|
||||
|
||||
from seedsigner.hardware.camera import Camera
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants, SeedSignerCustomIconConstants
|
||||
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen)
|
||||
@@ -25,32 +23,34 @@ from .view import View, Destination, BackStackView
|
||||
|
||||
|
||||
class ToolsMenuView(View):
|
||||
def run(self):
|
||||
IMAGE = (" New seed", FontAwesomeIconConstants.CAMERA)
|
||||
DICE = ("New seed", FontAwesomeIconConstants.DICE)
|
||||
KEYBOARD = ("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD)
|
||||
EXPLORER = "Address Explorer"
|
||||
button_data = [IMAGE, DICE, KEYBOARD, EXPLORER]
|
||||
screen = ButtonListScreen(
|
||||
|
||||
def run(self):
|
||||
button_data = [self.IMAGE, self.DICE, self.KEYBOARD, self.EXPLORER]
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Tools",
|
||||
is_button_text_centered=False,
|
||||
button_data=button_data
|
||||
)
|
||||
selected_menu_num = screen.display()
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
elif button_data[selected_menu_num] == IMAGE:
|
||||
elif button_data[selected_menu_num] == self.IMAGE:
|
||||
return Destination(ToolsImageEntropyLivePreviewView)
|
||||
|
||||
elif button_data[selected_menu_num] == DICE:
|
||||
elif button_data[selected_menu_num] == self.DICE:
|
||||
return Destination(ToolsDiceEntropyMnemonicLengthView)
|
||||
|
||||
elif button_data[selected_menu_num] == KEYBOARD:
|
||||
elif button_data[selected_menu_num] == self.KEYBOARD:
|
||||
return Destination(ToolsCalcFinalWordNumWordsView)
|
||||
|
||||
elif button_data[selected_menu_num] == EXPLORER:
|
||||
elif button_data[selected_menu_num] == self.EXPLORER:
|
||||
return Destination(ToolsAddressExplorerSelectSourceView)
|
||||
|
||||
|
||||
@@ -232,28 +232,30 @@ class ToolsDiceEntropyEntryView(View):
|
||||
Calc final word Views
|
||||
****************************************************************************"""
|
||||
class ToolsCalcFinalWordNumWordsView(View):
|
||||
def run(self):
|
||||
TWELVE = "12 words"
|
||||
TWENTY_FOUR = "24 words"
|
||||
|
||||
button_data = [TWELVE, TWENTY_FOUR]
|
||||
selected_menu_num = ButtonListScreen(
|
||||
def run(self):
|
||||
button_data = [self.TWELVE, self.TWENTY_FOUR]
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Mnemonic Length",
|
||||
is_bottom_list=True,
|
||||
is_button_text_centered=True,
|
||||
button_data=button_data,
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
elif button_data[selected_menu_num] == TWELVE:
|
||||
elif button_data[selected_menu_num] == self.TWELVE:
|
||||
self.controller.storage.init_pending_mnemonic(12)
|
||||
|
||||
# return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
|
||||
return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
|
||||
|
||||
elif button_data[selected_menu_num] == TWENTY_FOUR:
|
||||
elif button_data[selected_menu_num] == self.TWENTY_FOUR:
|
||||
self.controller.storage.init_pending_mnemonic(24)
|
||||
|
||||
# return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
|
||||
@@ -423,29 +425,27 @@ class ToolsCalcFinalWordDoneView(View):
|
||||
Address Explorer Views
|
||||
****************************************************************************"""
|
||||
class ToolsAddressExplorerSelectSourceView(View):
|
||||
def run(self):
|
||||
SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE)
|
||||
SCAN_DESCRIPTOR = ("Scan wallet descriptor", FontAwesomeIconConstants.QRCODE)
|
||||
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
button_data = []
|
||||
|
||||
|
||||
def run(self):
|
||||
seeds = self.controller.storage.seeds
|
||||
button_data = []
|
||||
for seed in seeds:
|
||||
button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
|
||||
button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue"))
|
||||
button_data = button_data + [self.SCAN_SEED, self.SCAN_DESCRIPTOR, self.TYPE_12WORD, self.TYPE_24WORD]
|
||||
|
||||
button_data.append(SCAN_SEED)
|
||||
button_data.append(SCAN_DESCRIPTOR)
|
||||
button_data.append(TYPE_12WORD)
|
||||
button_data.append(TYPE_24WORD)
|
||||
|
||||
selected_menu_num = ButtonListScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Address Explorer",
|
||||
button_data=button_data,
|
||||
is_button_text_centered=False,
|
||||
is_bottom_list=True,
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
@@ -465,13 +465,13 @@ class ToolsAddressExplorerSelectSourceView(View):
|
||||
)
|
||||
)
|
||||
|
||||
elif button_data[selected_menu_num] in [SCAN_SEED, SCAN_DESCRIPTOR]:
|
||||
elif button_data[selected_menu_num] in [self.SCAN_SEED, self.SCAN_DESCRIPTOR]:
|
||||
from seedsigner.views.scan_views import ScanView
|
||||
return Destination(ScanView)
|
||||
|
||||
elif button_data[selected_menu_num] in [TYPE_12WORD, TYPE_24WORD]:
|
||||
elif button_data[selected_menu_num] in [self.TYPE_12WORD, self.TYPE_24WORD]:
|
||||
from seedsigner.views.seed_views import SeedMnemonicEntryView
|
||||
if button_data[selected_menu_num] == TYPE_12WORD:
|
||||
if button_data[selected_menu_num] == self.TYPE_12WORD:
|
||||
self.controller.storage.init_pending_mnemonic(num_words=12)
|
||||
else:
|
||||
self.controller.storage.init_pending_mnemonic(num_words=24)
|
||||
@@ -480,6 +480,10 @@ class ToolsAddressExplorerSelectSourceView(View):
|
||||
|
||||
|
||||
class ToolsAddressExplorerAddressTypeView(View):
|
||||
RECEIVE = "Receive Addresses"
|
||||
CHANGE = "Change Addresses"
|
||||
|
||||
|
||||
def __init__(self, seed_num: int = None, script_type: str = None, custom_derivation: str = None):
|
||||
"""
|
||||
If the explorer source is a seed, `seed_num` and `script_type` must be
|
||||
@@ -528,29 +532,28 @@ class ToolsAddressExplorerAddressTypeView(View):
|
||||
def run(self):
|
||||
data = self.controller.address_explorer_data
|
||||
|
||||
RECEIVE = "Receive Addresses"
|
||||
CHANGE = "Change Addresses"
|
||||
button_data = [RECEIVE, CHANGE]
|
||||
|
||||
wallet_descriptor_display_name = None
|
||||
if "wallet_descriptor" in data:
|
||||
wallet_descriptor_display_name = data["wallet_descriptor"].brief_policy.replace(" (sorted)", "")
|
||||
|
||||
script_type = data["script_type"] if "script_type" in data else None
|
||||
|
||||
selected_menu_num = ToolsAddressExplorerAddressTypeScreen(
|
||||
button_data = [self.RECEIVE, self.CHANGE]
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ToolsAddressExplorerAddressTypeScreen,
|
||||
button_data=button_data,
|
||||
fingerprint=self.seed.get_fingerprint() if self.seed_num is not None else None,
|
||||
wallet_descriptor_display_name=wallet_descriptor_display_name,
|
||||
script_type=script_type,
|
||||
custom_derivation_path=self.custom_derivation,
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
elif button_data[selected_menu_num] in [RECEIVE, CHANGE]:
|
||||
return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=button_data[selected_menu_num] == CHANGE))
|
||||
elif button_data[selected_menu_num] in [self.RECEIVE, self.CHANGE]:
|
||||
return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=button_data[selected_menu_num] == self.CHANGE))
|
||||
|
||||
|
||||
|
||||
@@ -565,7 +568,7 @@ class ToolsAddressExplorerAddressListView(View):
|
||||
|
||||
def run(self):
|
||||
self.loading_screen = None
|
||||
try:
|
||||
|
||||
addresses = []
|
||||
button_data = []
|
||||
data = self.controller.address_explorer_data
|
||||
@@ -576,10 +579,12 @@ class ToolsAddressExplorerAddressListView(View):
|
||||
addr_storage_key = "change_addrs"
|
||||
|
||||
if addr_storage_key in data and len(data[addr_storage_key]) >= self.start_index + addrs_per_screen:
|
||||
# We already calculated this range addresses; just retrieve them
|
||||
# We already calculated this range of addresses; just retrieve them
|
||||
addresses = data[addr_storage_key][self.start_index:self.start_index + addrs_per_screen]
|
||||
|
||||
else:
|
||||
try:
|
||||
from seedsigner.gui.screens.screen import LoadingScreenThread
|
||||
self.loading_screen = LoadingScreenThread(text="Calculating addrs...")
|
||||
self.loading_screen.start()
|
||||
|
||||
@@ -608,9 +613,15 @@ class ToolsAddressExplorerAddressListView(View):
|
||||
|
||||
else:
|
||||
raise Exception("Single sig descriptors not yet supported")
|
||||
finally:
|
||||
# Everything is set. Stop the loading screen
|
||||
self.loading_screen.stop()
|
||||
|
||||
for i, address in enumerate(addresses):
|
||||
cur_index = i + self.start_index
|
||||
|
||||
# Adjust the trailing addr display length based on available room
|
||||
# (the index number will push it out on each order of magnitude)
|
||||
if cur_index < 10:
|
||||
end_digits = -6
|
||||
elif cur_index < 100:
|
||||
@@ -621,7 +632,8 @@ class ToolsAddressExplorerAddressListView(View):
|
||||
|
||||
button_data.append(("Next {}".format(addrs_per_screen), None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT))
|
||||
|
||||
screen = ButtonListScreen(
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="{} Addrs".format("Receive" if not self.is_change else "Change"),
|
||||
button_data=button_data,
|
||||
button_font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
|
||||
@@ -631,12 +643,6 @@ class ToolsAddressExplorerAddressListView(View):
|
||||
selected_button=self.selected_button_index,
|
||||
scroll_y_initial_offset=self.initial_scroll,
|
||||
)
|
||||
finally:
|
||||
# Everything is set. Stop the loading screen
|
||||
if self.loading_screen:
|
||||
self.loading_screen.stop()
|
||||
|
||||
selected_menu_num = screen.display()
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
@@ -646,7 +652,7 @@ class ToolsAddressExplorerAddressListView(View):
|
||||
return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=self.is_change, start_index=self.start_index + addrs_per_screen))
|
||||
|
||||
# Preserve the list's current scroll so we can return to the same spot
|
||||
initial_scroll = screen.buttons[0].scroll_y
|
||||
initial_scroll = self.screen.buttons[0].scroll_y
|
||||
|
||||
index = selected_menu_num + self.start_index
|
||||
return Destination(ToolsAddressExplorerAddressView, view_args=dict(index=index, address=addresses[selected_menu_num], is_change=self.is_change, start_index=self.start_index, parent_initial_scroll=initial_scroll), skip_current_view=True)
|
||||
@@ -664,10 +670,12 @@ class ToolsAddressExplorerAddressView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.screen import QRDisplayScreen
|
||||
qr_encoder = EncodeQR(qr_type=QRType.BITCOIN_ADDRESS, bitcoin_address=self.address)
|
||||
QRDisplayScreen(
|
||||
self.run_screen(
|
||||
QRDisplayScreen,
|
||||
qr_encoder=qr_encoder,
|
||||
).display()
|
||||
)
|
||||
|
||||
# Exiting/Cancelling the QR display screen always returns to the list
|
||||
return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=self.is_change, start_index=self.start_index, selected_button_index=self.index - self.start_index, initial_scroll=self.parent_initial_scroll), skip_current_view=True)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
from typing import Type
|
||||
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants
|
||||
from seedsigner.gui.screens import RET_CODE__POWER_BUTTON
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, DireWarningScreen, LargeButtonScreen, PowerOffScreen, PowerOffNotRequiredScreen, ResetScreen, WarningScreen
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants
|
||||
from seedsigner.gui.screens import RET_CODE__POWER_BUTTON, RET_CODE__BACK_BUTTON
|
||||
from seedsigner.gui.screens.screen import BaseScreen, DireWarningScreen, LargeButtonScreen, PowerOffScreen, PowerOffNotRequiredScreen, ResetScreen, WarningScreen
|
||||
from seedsigner.models.threads import BaseThread
|
||||
from seedsigner.models import Settings
|
||||
|
||||
@@ -56,13 +56,19 @@ class View:
|
||||
self.canvas_width = self.renderer.canvas_width
|
||||
self.canvas_height = self.renderer.canvas_height
|
||||
|
||||
self.buttons = self.controller.buttons
|
||||
self.screen = None
|
||||
|
||||
|
||||
def run(self, **kwargs):
|
||||
if hasattr(self, "screen"):
|
||||
self.screen.display()
|
||||
else:
|
||||
def run_screen(self, Screen_cls: Type[BaseScreen], **kwargs) -> int | str:
|
||||
"""
|
||||
Instantiates the provided Screen_cls and runs its interactive display.
|
||||
Returns the user's input upon completion.
|
||||
"""
|
||||
self.screen = Screen_cls(**kwargs)
|
||||
return self.screen.display()
|
||||
|
||||
|
||||
def run(self, **kwargs) -> 'Destination':
|
||||
raise Exception("Must implement in the child class")
|
||||
|
||||
|
||||
@@ -73,7 +79,7 @@ class Destination:
|
||||
Basic struct to pass back to the Controller to tell it which View the user should
|
||||
be presented with next.
|
||||
"""
|
||||
View_cls: View # The target View to route to
|
||||
View_cls: Type[View] # The target View to route to
|
||||
view_args: dict = None # The input args required to instantiate the target View
|
||||
skip_current_view: bool = False # The current View is just forwarding; omit current View from history
|
||||
clear_history: bool = False # Optionally clears the back_stack to prevent "back"
|
||||
@@ -93,12 +99,22 @@ class Destination:
|
||||
return out
|
||||
|
||||
|
||||
def run(self):
|
||||
def _instantiate_view(self):
|
||||
if not self.view_args:
|
||||
# Can't unpack (**) None so we replace with an empty dict
|
||||
self.view_args = {}
|
||||
# Instantiate the `View_cls` and run() it with the `view_args` dict
|
||||
return self.View_cls(**self.view_args).run()
|
||||
|
||||
# Instantiate the `View_cls` with the `view_args` dict
|
||||
self.view = self.View_cls(**self.view_args)
|
||||
|
||||
|
||||
def _run_view(self):
|
||||
return self.view.run()
|
||||
|
||||
|
||||
def run(self):
|
||||
self._instantiate_view()
|
||||
return self._run_view()
|
||||
|
||||
|
||||
def __eq__(self, obj):
|
||||
@@ -121,53 +137,63 @@ class Destination:
|
||||
#
|
||||
#########################################################################################
|
||||
class MainMenuView(View):
|
||||
def run(self):
|
||||
from .seed_views import SeedsMenuView
|
||||
from .settings_views import SettingsMenuView
|
||||
from .scan_views import ScanView
|
||||
from .tools_views import ToolsMenuView
|
||||
from seedsigner.gui.screens import LargeButtonScreen
|
||||
menu_items = [
|
||||
(("Scan", FontAwesomeIconConstants.QRCODE), ScanView),
|
||||
(("Seeds", FontAwesomeIconConstants.KEY), SeedsMenuView),
|
||||
(("Tools", FontAwesomeIconConstants.SCREWDRIVER_WRENCH), ToolsMenuView),
|
||||
(("Settings", FontAwesomeIconConstants.GEAR), SettingsMenuView),
|
||||
]
|
||||
SCAN = ("Scan", FontAwesomeIconConstants.QRCODE)
|
||||
SEEDS = ("Seeds", FontAwesomeIconConstants.KEY)
|
||||
TOOLS = ("Tools", FontAwesomeIconConstants.SCREWDRIVER_WRENCH)
|
||||
SETTINGS = ("Settings", FontAwesomeIconConstants.GEAR)
|
||||
|
||||
screen = LargeButtonScreen(
|
||||
def run(self):
|
||||
button_data = [self.SCAN, self.SEEDS, self.TOOLS, self.SETTINGS]
|
||||
selected_menu_num = self.run_screen(
|
||||
LargeButtonScreen,
|
||||
title="Home",
|
||||
title_font_size=26,
|
||||
button_data=[entry[0] for entry in menu_items],
|
||||
button_data=button_data,
|
||||
show_back_button=False,
|
||||
show_power_button=True,
|
||||
)
|
||||
selected_menu_num = screen.display()
|
||||
|
||||
if selected_menu_num == RET_CODE__POWER_BUTTON:
|
||||
return Destination(PowerOptionsView)
|
||||
|
||||
return Destination(menu_items[selected_menu_num][1])
|
||||
if button_data[selected_menu_num] == self.SCAN:
|
||||
from .scan_views import ScanView
|
||||
return Destination(ScanView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.SEEDS:
|
||||
from .seed_views import SeedsMenuView
|
||||
return Destination(SeedsMenuView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.TOOLS:
|
||||
from .tools_views import ToolsMenuView
|
||||
return Destination(ToolsMenuView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.SETTINGS:
|
||||
from .settings_views import SettingsMenuView
|
||||
return Destination(SettingsMenuView)
|
||||
|
||||
|
||||
|
||||
class PowerOptionsView(View):
|
||||
def run(self):
|
||||
RESET = ("Restart", FontAwesomeIconConstants.ROTATE_RIGHT)
|
||||
POWER_OFF = ("Power Off", FontAwesomeIconConstants.POWER_OFF)
|
||||
button_data = [RESET, POWER_OFF]
|
||||
selected_menu_num = LargeButtonScreen(
|
||||
|
||||
def run(self):
|
||||
button_data = [self.RESET, self.POWER_OFF]
|
||||
selected_menu_num = self.run_screen(
|
||||
LargeButtonScreen,
|
||||
title="Reset / Power",
|
||||
show_back_button=True,
|
||||
button_data=button_data
|
||||
).display()
|
||||
)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
elif button_data[selected_menu_num] == RESET:
|
||||
elif button_data[selected_menu_num] == self.RESET:
|
||||
return Destination(RestartView)
|
||||
|
||||
elif button_data[selected_menu_num] == POWER_OFF:
|
||||
elif button_data[selected_menu_num] == self.POWER_OFF:
|
||||
return Destination(PowerOffView)
|
||||
|
||||
|
||||
@@ -176,7 +202,7 @@ class RestartView(View):
|
||||
def run(self):
|
||||
thread = RestartView.DoResetThread()
|
||||
thread.start()
|
||||
ResetScreen().display()
|
||||
self.run_screen(ResetScreen)
|
||||
|
||||
|
||||
class DoResetThread(BaseThread):
|
||||
@@ -200,12 +226,12 @@ class RestartView(View):
|
||||
class PowerOffView(View):
|
||||
def run(self):
|
||||
if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
|
||||
PowerOffNotRequiredScreen().display()
|
||||
self.run_screen(PowerOffNotRequiredScreen)
|
||||
return Destination(BackStackView)
|
||||
else:
|
||||
thread = PowerOffView.PowerOffThread()
|
||||
thread.start()
|
||||
PowerOffScreen().display()
|
||||
self.run_screen(PowerOffScreen)
|
||||
|
||||
|
||||
class PowerOffThread(BaseThread):
|
||||
@@ -223,30 +249,32 @@ class NotYetImplementedView(View):
|
||||
Temporary View to use during dev.
|
||||
"""
|
||||
def run(self):
|
||||
WarningScreen(
|
||||
self.run_screen(
|
||||
WarningScreen,
|
||||
title="Work In Progress",
|
||||
status_headline="Not Yet Implemented",
|
||||
text="This is still on our to-do list!",
|
||||
button_data=["Back to Main Menu"],
|
||||
).display()
|
||||
)
|
||||
|
||||
return Destination(MainMenuView)
|
||||
|
||||
|
||||
|
||||
class UnhandledExceptionView(View):
|
||||
def __init__(self, error: List[str]):
|
||||
def __init__(self, error: list[str]):
|
||||
self.error = error
|
||||
|
||||
|
||||
def run(self):
|
||||
DireWarningScreen(
|
||||
self.run_screen(
|
||||
DireWarningScreen,
|
||||
title="System Error",
|
||||
status_headline=self.error[0],
|
||||
text=self.error[1] + "\n" + self.error[2],
|
||||
button_data=["OK"],
|
||||
show_back_button=False,
|
||||
allow_text_overflow=True, # Fit what we can, let the rest go off the edges
|
||||
).display()
|
||||
)
|
||||
|
||||
return Destination(MainMenuView, clear_history=True)
|
||||
|
||||
+8
-2
@@ -1,7 +1,13 @@
|
||||
# Running Tests
|
||||
|
||||
You must install the test-runner dependencies:
|
||||
```
|
||||
The tests are designed to be run on non-Raspi hardware.
|
||||
|
||||
On your testing machine you'll have to install:
|
||||
```bash
|
||||
# general dependencies
|
||||
pip3 install -r requirements.txt
|
||||
|
||||
# test suite dependencies
|
||||
pip3 install -r tests/requirements.txt
|
||||
```
|
||||
|
||||
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from mock import MagicMock, patch
|
||||
from typing import Callable
|
||||
|
||||
# Prevent importing modules w/Raspi hardware dependencies.
|
||||
# These must precede any SeedSigner imports.
|
||||
sys.modules['seedsigner.gui.renderer'] = MagicMock()
|
||||
sys.modules['seedsigner.gui.screens.screensaver'] = MagicMock()
|
||||
sys.modules['seedsigner.views.screensaver'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.buttons'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.camera'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.microsd'] = MagicMock()
|
||||
|
||||
from seedsigner.controller import Controller, FlowBasedTestException, StopFlowBasedTest
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON
|
||||
from seedsigner.models import Settings
|
||||
from seedsigner.views.view import Destination, MainMenuView, View
|
||||
|
||||
|
||||
|
||||
class BaseTest:
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
# Ensure there are no on-disk artifacts after running tests.
|
||||
Settings.SETTINGS_FILENAME = "settings-test.json"
|
||||
|
||||
# Mock out the loading screen so it can't spawn. View classes must import locally!
|
||||
patch('seedsigner.gui.screens.screen.LoadingScreenThread').start()
|
||||
|
||||
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
pass
|
||||
|
||||
|
||||
@classmethod
|
||||
def reset_settings(cls):
|
||||
""" Wipe and re-initialize the Settings singleton """
|
||||
Settings._instance = None
|
||||
BaseTest.remove_settings()
|
||||
|
||||
|
||||
@classmethod
|
||||
def remove_settings(cls):
|
||||
""" If settings were written to disk, delete """
|
||||
import os
|
||||
try:
|
||||
if os.path.exists(Settings.SETTINGS_FILENAME):
|
||||
os.remove(Settings.SETTINGS_FILENAME)
|
||||
except:
|
||||
print(f"{Settings.SETTINGS_FILENAME} could not be removed")
|
||||
|
||||
|
||||
@classmethod
|
||||
def reset_controller(cls):
|
||||
""" Wipe and re-initialize the Controller singleton """
|
||||
Controller._instance = None
|
||||
Controller.configure_instance()
|
||||
|
||||
|
||||
def setup_method(self):
|
||||
""" Guarantee a clean/default Controller and Settings state for each test case """
|
||||
BaseTest.reset_controller()
|
||||
BaseTest.reset_settings()
|
||||
self.controller = Controller.get_instance()
|
||||
self.settings = Settings.get_instance()
|
||||
|
||||
|
||||
def teardown_method(self):
|
||||
BaseTest.remove_settings()
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class FlowStep:
|
||||
"""
|
||||
Trivial helper class to express FlowTest sequences below.
|
||||
|
||||
* expected_view: verify that the current step in the sequence instantiates the right View.
|
||||
* before_run: function that takes a View instance as an arg and modifies it before running the View.
|
||||
* screen_return_value: mocked Screen interaction result: raw return value as if from the Screen.
|
||||
* button_data_selection: mocked Screen interaction result: the View.button_data value of the desired option.
|
||||
* is_redirect: expects the Destination to specify `skip_current_view=True`.
|
||||
"""
|
||||
expected_view: type[View] = None
|
||||
before_run: Callable[[View], None] = None
|
||||
screen_return_value: int | str = None
|
||||
button_data_selection: str | tuple = None
|
||||
is_redirect: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
if self.screen_return_value is not None and self.button_data_selection is not None:
|
||||
raise Exception("Can't specify both `screen_return_value` and `button_data_selection`")
|
||||
|
||||
|
||||
|
||||
class FlowTestInvalidButtonDataSelectionException(FlowBasedTestException):
|
||||
""" The FlowStep's button_data_selection value was not found in the View's button_data """
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class FlowTestUnexpectedViewException(FlowBasedTestException):
|
||||
""" The Controller's current View did not match the expected View in the FlowStep sequence """
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class FlowTestRunScreenNotExecutedException(FlowBasedTestException):
|
||||
""" The View's run_screen() method was not called but the FlowStep expected it to need user input """
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class FlowTest(BaseTest):
|
||||
""" Base class for any tests that do flow-based testing """
|
||||
|
||||
def stop_test(self):
|
||||
""" Raise our special Exception to stop the test """
|
||||
raise StopFlowBasedTest()
|
||||
|
||||
|
||||
def run_sequence(self, sequence: list[FlowStep], initial_destination_view_args: dict = None) -> None:
|
||||
"""
|
||||
Run a pre-set sequence of Views w/manually-specified return values in order to test
|
||||
the Controller's flow control logic and the routing from View to View.
|
||||
"""
|
||||
with patch("seedsigner.views.view.Destination._run_view", autospec=True) as mock_run_view:
|
||||
with patch("seedsigner.views.view.View.run_screen", autospec=True) as mock_run_screen:
|
||||
def run_view(destination: Destination, *args, **kwargs):
|
||||
""" Replaces Destination._run_view() """
|
||||
if len(sequence) == 0:
|
||||
self.stop_test()
|
||||
|
||||
cur_flow_step = sequence[0]
|
||||
|
||||
# Verify that the View class specified in the test sequence matches the
|
||||
# View class that is being run.
|
||||
if destination.View_cls != cur_flow_step.expected_view:
|
||||
raise FlowTestUnexpectedViewException(f"Expected {cur_flow_step.expected_view}, got {destination.View_cls}")
|
||||
|
||||
# Run the optional pre-run function to modify the View.
|
||||
if cur_flow_step.before_run:
|
||||
cur_flow_step.before_run(destination.view)
|
||||
|
||||
if cur_flow_step.is_redirect:
|
||||
# The current View is going to auto-redirect without calling run_screen(),
|
||||
# so we need to remove the current step from the sequence before the
|
||||
# View.run() call below.
|
||||
sequence.pop(0)
|
||||
|
||||
# Some Views reach into their Screen's variables directly (e.g.
|
||||
# Screen.buttons to preserve the scroll position), so we need to mock out the
|
||||
# Screen instance that is created by the View.
|
||||
destination.view.screen = MagicMock()
|
||||
|
||||
prev_mock_run_screen_call_count = mock_run_screen.call_count
|
||||
|
||||
# Run the View (with our mocked run_screen) and get the next Destination that results
|
||||
destination = destination.view.run()
|
||||
|
||||
if (cur_flow_step.button_data_selection or cur_flow_step.screen_return_value is not None) and mock_run_screen.call_count == prev_mock_run_screen_call_count:
|
||||
# The FlowStep was expecting some kind of user interaction, but the View
|
||||
# never called run_screen().
|
||||
raise FlowTestRunScreenNotExecutedException(f"View.run_screen() was not run for {destination.View_cls.__name__}")
|
||||
|
||||
return destination
|
||||
|
||||
def run_screen(view: View, *args, **kwargs):
|
||||
""" Replaces View.run_screen() """
|
||||
# Return the return value specified in the test sequence and
|
||||
# remove the completed test step from the sequence.
|
||||
flow_step = sequence.pop(0)
|
||||
|
||||
if flow_step.button_data_selection:
|
||||
# We're mocking out the View.run_screen() method, so we'll get all of the
|
||||
# input args that are normally passed into the Screen.run() method,
|
||||
# including the button_data kwarg.
|
||||
if "button_data" in kwargs:
|
||||
if flow_step.button_data_selection not in kwargs.get("button_data") and flow_step.button_data_selection not in [RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON]:
|
||||
raise FlowTestInvalidButtonDataSelectionException(f"'{flow_step.button_data_selection}' not found in button_data: {kwargs.get('button_data')}")
|
||||
return kwargs.get("button_data").index(flow_step.button_data_selection)
|
||||
else:
|
||||
raise Exception(f"Can't specify `FlowStep.button_data_selection` if `button_data` isn't a kwarg in {view.__class__.__name__}'s run_screen()")
|
||||
|
||||
elif type(flow_step.screen_return_value) in [StopFlowBasedTest, FlowBasedTestException]:
|
||||
raise flow_step.screen_return_value
|
||||
|
||||
return flow_step.screen_return_value
|
||||
|
||||
# Mock out the Destination._run_view() method so we can verify the View class
|
||||
# that is specified in the test sequence and then run the View.
|
||||
mock_run_view.side_effect = run_view
|
||||
|
||||
# Mock out the View.run_screen() method so we can provide the
|
||||
# return value that is specified in the test sequence.
|
||||
mock_run_screen.side_effect = run_screen
|
||||
|
||||
# Start the Controller with the first View_cls specified in the test sequence
|
||||
if sequence[0].expected_view != MainMenuView:
|
||||
initial_destination = Destination(sequence[0].expected_view, view_args=initial_destination_view_args)
|
||||
else:
|
||||
initial_destination = None
|
||||
|
||||
# Start the Controller and run the sequence
|
||||
Controller.get_instance().start(initial_destination=initial_destination)
|
||||
+57
-19
@@ -1,34 +1,73 @@
|
||||
import configparser
|
||||
import pytest
|
||||
from mock import MagicMock
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
|
||||
# Must import this before the Controller
|
||||
from base import BaseTest
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
|
||||
|
||||
class TestController(BaseTest):
|
||||
|
||||
@pytest.fixture()
|
||||
def reset_controller():
|
||||
"""fixture to setup, then yield to run test, then tear down"""
|
||||
def test_reset_controller(self):
|
||||
""" The reset_controller util should completely reset the Controller singleton """
|
||||
controller = Controller.get_instance()
|
||||
controller.address_explorer_data = "foo"
|
||||
|
||||
# setup
|
||||
Controller.configure_instance(disable_hardware=True)
|
||||
|
||||
# yield to run a single test
|
||||
yield
|
||||
|
||||
# tear down
|
||||
MicroSD._instance = None
|
||||
Controller._instance = None
|
||||
BaseTest.reset_controller()
|
||||
controller = Controller.get_instance()
|
||||
assert controller.address_explorer_data is None
|
||||
|
||||
|
||||
def test_singleton_init_fails(reset_controller):
|
||||
def test_singleton_init_fails(self):
|
||||
""" The Controller should not allow any code to instantiate it via Controller() """
|
||||
with pytest.raises(Exception):
|
||||
c = Controller()
|
||||
|
||||
|
||||
def test_singleton_get_instance_preserves_state(reset_controller):
|
||||
def test_handle_exception(reset_controller):
|
||||
""" Handle exceptions that get caught by the controller """
|
||||
|
||||
def process_exception_asserting_valid_error(exception_type, exception_msg=None):
|
||||
"""
|
||||
Exceptions caught by the controller are forwarded to the
|
||||
UnhandledExceptionView with view_args["error"] being a list
|
||||
of three strings, ie: [exception_type, line_info, exception_msg]
|
||||
"""
|
||||
try:
|
||||
if exception_msg:
|
||||
raise exception_type(exception_msg)
|
||||
else:
|
||||
raise exception_type()
|
||||
except Exception as e:
|
||||
error = controller.handle_exception(e).view_args["error"]
|
||||
|
||||
# assert that error structure is valid
|
||||
assert len(error) == 3
|
||||
assert error[0] in str(exception_type)
|
||||
assert type(error[1]) == str
|
||||
if exception_msg:
|
||||
assert exception_msg in error[2]
|
||||
else:
|
||||
assert error[2] == ""
|
||||
|
||||
# Initialize the controller
|
||||
controller = Controller.get_instance()
|
||||
|
||||
exception_tests = [
|
||||
# exceptions with an exception_msg
|
||||
(Exception, "foo"),
|
||||
(KeyError, "key not found"),
|
||||
# exceptions without an exception_msg
|
||||
(Exception, ""),
|
||||
(Exception, None),
|
||||
]
|
||||
|
||||
for exception_type, exception_msg in exception_tests:
|
||||
process_exception_asserting_valid_error(exception_type, exception_msg)
|
||||
|
||||
|
||||
def test_singleton_get_instance_preserves_state(self):
|
||||
""" Changes to the Controller singleton should be preserved across calls to get_instance() """
|
||||
|
||||
# Initialize the instance and verify that it read the config settings
|
||||
@@ -43,7 +82,7 @@ def test_singleton_get_instance_preserves_state(reset_controller):
|
||||
assert controller.unverified_address == "123abc"
|
||||
|
||||
|
||||
def test_missing_settings_get_defaults(reset_controller):
|
||||
def test_missing_settings_get_defaults(self):
|
||||
""" Should gracefully handle all missing fields from `settings.json` """
|
||||
|
||||
controller = Controller.get_instance()
|
||||
@@ -68,7 +107,6 @@ def test_missing_settings_get_defaults(reset_controller):
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == 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
|
||||
|
||||
# Hidden Settings defaults
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
import pytest
|
||||
|
||||
from seedsigner.models.settings_definition import SettingsConstants as SC
|
||||
from seedsigner.helpers import embit_utils
|
||||
|
||||
|
||||
def test_get_standard_derivation_path():
|
||||
"""
|
||||
tests seedsigner.helpers.embit_utils.get_standard_derivation_path()
|
||||
"""
|
||||
|
||||
vectors_args_expected = {
|
||||
# single sig
|
||||
tuple(): "m/84'/0'/0'",
|
||||
(SC.MAINNET,): "m/84'/0'/0'",
|
||||
(SC.MAINNET, SC.SINGLE_SIG, ): "m/84'/0'/0'",
|
||||
|
||||
(SC.MAINNET, SC.SINGLE_SIG, SC.NATIVE_SEGWIT): "m/84'/0'/0'",
|
||||
(SC.TESTNET, SC.SINGLE_SIG, SC.NATIVE_SEGWIT): "m/84'/1'/0'",
|
||||
(SC.REGTEST, SC.SINGLE_SIG, SC.NATIVE_SEGWIT): "m/84'/1'/0'",
|
||||
|
||||
(SC.MAINNET, SC.SINGLE_SIG, SC.NESTED_SEGWIT): "m/49'/0'/0'",
|
||||
(SC.TESTNET, SC.SINGLE_SIG, SC.NESTED_SEGWIT): "m/49'/1'/0'",
|
||||
(SC.REGTEST, SC.SINGLE_SIG, SC.NESTED_SEGWIT): "m/49'/1'/0'",
|
||||
|
||||
(SC.MAINNET, SC.SINGLE_SIG, SC.TAPROOT): "m/86'/0'/0'",
|
||||
(SC.TESTNET, SC.SINGLE_SIG, SC.TAPROOT): "m/86'/1'/0'",
|
||||
(SC.REGTEST, SC.SINGLE_SIG, SC.TAPROOT): "m/86'/1'/0'",
|
||||
|
||||
|
||||
# multi sig
|
||||
(SC.MAINNET, SC.MULTISIG, SC.NATIVE_SEGWIT): "m/48'/0'/0'/2'",
|
||||
(SC.TESTNET, SC.MULTISIG, SC.NATIVE_SEGWIT): "m/48'/1'/0'/2'",
|
||||
(SC.REGTEST, SC.MULTISIG, SC.NATIVE_SEGWIT): "m/48'/1'/0'/2'",
|
||||
|
||||
(SC.MAINNET, SC.MULTISIG, SC.NESTED_SEGWIT): "m/48'/0'/0'/1'",
|
||||
(SC.TESTNET, SC.MULTISIG, SC.NESTED_SEGWIT): "m/48'/1'/0'/1'",
|
||||
(SC.REGTEST, SC.MULTISIG, SC.NESTED_SEGWIT): "m/48'/1'/0'/1'",
|
||||
|
||||
(SC.MAINNET, SC.MULTISIG, SC.TAPROOT): Exception,
|
||||
(SC.TESTNET, SC.MULTISIG, SC.TAPROOT): Exception,
|
||||
(SC.REGTEST, SC.MULTISIG, SC.TAPROOT): Exception,
|
||||
|
||||
# intentionally fall into exceptions
|
||||
(SC.MAINNET, SC.SINGLE_SIG, 'invalid'): Exception,
|
||||
(SC.MAINNET, SC.MULTISIG, 'invalid'): Exception,
|
||||
(SC.MAINNET, 'invalid', SC.NATIVE_SEGWIT): Exception,
|
||||
|
||||
# nonsense arguments
|
||||
("A",): Exception,
|
||||
("B", "A"): Exception,
|
||||
("C", "B", "A"): Exception,
|
||||
(True,): Exception,
|
||||
(False, True): Exception,
|
||||
(tuple(),): Exception,
|
||||
}
|
||||
func = embit_utils.get_standard_derivation_path
|
||||
|
||||
print()
|
||||
for args, expected in vectors_args_expected.items():
|
||||
|
||||
# test successful calls
|
||||
if type(expected) is str:
|
||||
# call with ordered params
|
||||
print(f"asserting {func.__name__}(*{args}) == {repr(expected)}")
|
||||
assert func(*args) == expected
|
||||
|
||||
# call with named params
|
||||
a_dict = {}
|
||||
if len(args) == 1: a_dict = {'network': args[0]}
|
||||
elif len(args) == 2: a_dict = {'network': args[0], 'wallet_type': args[1]}
|
||||
elif len(args) == 3: a_dict = {'network': args[0], 'wallet_type': args[1], 'script_type': args[2]}
|
||||
print(f"asserting {func.__name__}(**{a_dict}) == {repr(expected)}")
|
||||
assert func(**a_dict) == expected
|
||||
|
||||
# test exceptions
|
||||
else:
|
||||
# call with ordered params
|
||||
with pytest.raises(expected):
|
||||
print(f"asserting {func.__name__}(*{args}) raises Exception")
|
||||
func(*args)
|
||||
|
||||
# call with named params
|
||||
a_dict = {}
|
||||
if len(args) == 1: a_dict = {'network': args[0]}
|
||||
elif len(args) == 2: a_dict = {'network': args[0], 'wallet_type': args[1]}
|
||||
elif len(args) == 3: a_dict = {'network': args[0], 'wallet_type': args[1], 'script_type': args[2]}
|
||||
print(f"asserting {func.__name__}(**{a_dict}) raises Exception")
|
||||
with pytest.raises(expected):
|
||||
func(**a_dict)
|
||||
|
||||
|
||||
def test_get_xpub():
|
||||
"""
|
||||
tests seedsigner.helpers.embit_utils.get_xpub()
|
||||
"""
|
||||
|
||||
from binascii import unhexlify
|
||||
from embit import bip39, bip32
|
||||
|
||||
# test vectors originate from:
|
||||
# https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
|
||||
# https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki
|
||||
# https://github.com/satoshilabs/slips/blob/master/slip-0132.md
|
||||
# https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki
|
||||
# https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
|
||||
vector_seeds = (
|
||||
unhexlify("000102030405060708090a0b0c0d0e0f"),
|
||||
unhexlify("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"),
|
||||
unhexlify("4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be"),
|
||||
unhexlify("3ddd5602285899a946114506157c7997e5444528f3003f6134712147db19b678"),
|
||||
bip39.mnemonic_to_seed("abandon "*11 + "about"),
|
||||
)
|
||||
|
||||
vectors_args_expected = {
|
||||
# https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vector-1
|
||||
(vector_seeds[0], "m/", "main"): "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8",
|
||||
(vector_seeds[0], "m/0'", "main"): "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw",
|
||||
(vector_seeds[0], "m/0h/1", "main"): "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ",
|
||||
(vector_seeds[0], "m/0'/1/2h", "main"): "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5",
|
||||
(vector_seeds[0], "m/0'/1/2h/2", "main"): "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV",
|
||||
(vector_seeds[0], "m/0'/1/2h/2/1000000000", "main"): "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy",
|
||||
|
||||
# https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vector-2
|
||||
(vector_seeds[1], "m/", "main"): "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB",
|
||||
(vector_seeds[1], "m/0", "main"): "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH",
|
||||
(vector_seeds[1], "m/0/2147483647'", "main"): "xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a",
|
||||
(vector_seeds[1], "m/0/2147483647h/1", "main"): "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon",
|
||||
(vector_seeds[1], "m/0/2147483647'/1/2147483646h", "main"): "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL",
|
||||
(vector_seeds[1], "m/0/2147483647h/1/2147483646'/2", "main"): "xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt",
|
||||
|
||||
# https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vector-3
|
||||
(vector_seeds[2], "m/", "main"): "xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13",
|
||||
(vector_seeds[2], "m/0h", "main"): "xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y",
|
||||
|
||||
# https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vector-4
|
||||
(vector_seeds[3], "m/", "main"): "xpub661MyMwAqRbcGczjuMoRm6dXaLDEhW1u34gKenbeYqAix21mdUKJyuyu5F1rzYGVxyL6tmgBUAEPrEz92mBXjByMRiJdba9wpnN37RLLAXa",
|
||||
(vector_seeds[3], "m/0'", "main"): "xpub69AUMk3qDBi3uW1sXgjCmVjJ2G6WQoYSnNHyzkmdCHEhSZ4tBok37xfFEqHd2AddP56Tqp4o56AePAgCjYdvpW2PU2jbUPFKsav5ut6Ch1m",
|
||||
(vector_seeds[3], "m/0h/1'", "main"): "xpub6BJA1jSqiukeaesWfxe6sNK9CCGaujFFSJLomWHprUL9DePQ4JDkM5d88n49sMGJxrhpjazuXYWdMf17C9T5XnxkopaeS7jGk1GyyVziaMt",
|
||||
|
||||
#
|
||||
# embit_utils.get_xpub() returns the generic bip32 version "xpub", NOT the zpub/Zpub/ypub/Ypub extended versions
|
||||
#
|
||||
|
||||
# https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki#test-vectors
|
||||
# https://github.com/satoshilabs/slips/blob/master/slip-0132.md#bitcoin-test-vectors
|
||||
(vector_seeds[4], "m/84'/0'/0'", "main"):
|
||||
bip32.HDKey.from_string("zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs").to_base58(version=b'\x04\x88\xb2\x1e'),
|
||||
|
||||
# https://github.com/satoshilabs/slips/blob/master/slip-0132.md#bitcoin-test-vectors
|
||||
(vector_seeds[4], "m/49'/0'/0'", "main"):
|
||||
bip32.HDKey.from_string("ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP").to_base58(version=b'\x04\x88\xb2\x1e'),
|
||||
|
||||
# https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki#test-vectors
|
||||
(vector_seeds[4], "m/49'/1'/0'", "test"):
|
||||
bip32.HDKey.from_string("upub5EFU65HtV5TeiSHmZZm7FUffBGy8UKeqp7vw43jYbvZPpoVsgU93oac7Wk3u6moKegAEWtGNF8DehrnHtv21XXEMYRUocHqguyjknFHYfgY").to_base58(version=b'\x04\x35\x87\xcf'),
|
||||
|
||||
# https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki#test-vectors
|
||||
(vector_seeds[4], "m/86'/0'/0'", "main"): "xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ",
|
||||
|
||||
}
|
||||
func = embit_utils.get_xpub
|
||||
|
||||
print()
|
||||
for args, expected in vectors_args_expected.items():
|
||||
print("\nasserting...")
|
||||
|
||||
# call without optional params (default is "main")
|
||||
if args[2] == "main":
|
||||
print(f' {func.__name__}({args[0]}, "{args[1]}") == "{expected}"')
|
||||
assert str(func(args[0], args[1])) == expected
|
||||
|
||||
# call with ordered params
|
||||
print(f' {func.__name__}(*{args}) == "{expected}"')
|
||||
assert str(func(*args)) == expected
|
||||
|
||||
# call with named params
|
||||
print(f' {func.__name__}(seed_bytes={args[0]}, derivation_path="{args[1]}", embit_network="{args[2]}") == "{expected}"')
|
||||
assert str(func(seed_bytes=args[0], derivation_path=args[1], embit_network=args[2])) == expected
|
||||
|
||||
|
||||
def test_get_single_sig_address():
|
||||
"""
|
||||
tests seedsigner.helpers.embit_utils.get_single_sig_address()
|
||||
"""
|
||||
|
||||
from embit.bip32 import HDKey
|
||||
|
||||
# test vectors originate from:
|
||||
# https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki
|
||||
# https://github.com/satoshilabs/slips/blob/master/slip-0132.md
|
||||
# https://iancoleman.io/bip39/
|
||||
# https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
|
||||
|
||||
vectors_args_expected = {
|
||||
# https://github.com/satoshilabs/slips/blob/master/slip-0132.md#bitcoin-test-vectors (first payment address of native segwit on mainnet)
|
||||
(HDKey.from_string("zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs"), "nat", 0, False, "main"):
|
||||
"bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu",
|
||||
# jdlcdl: derived via iancoleman test vector for first change address of native segwit on mainnet
|
||||
(HDKey.from_string("zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs"), "nat", 0, True, "main"):
|
||||
"bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el",
|
||||
|
||||
# https://github.com/satoshilabs/slips/blob/master/slip-0132.md#bitcoin-test-vectors (first payment address of nested segwit on mainnet)
|
||||
(HDKey.from_string("ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP"), "nes", 0, False, "main"):
|
||||
"37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf",
|
||||
# jdlcdl: derived via iancoleman test vector for first change address of nested segwit on mainnet
|
||||
(HDKey.from_string("ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP"), "nes", 0, True, "main"):
|
||||
"34K56kSjgUCUSD8GTtuF7c9Zzwokbs6uZ7",
|
||||
|
||||
# https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki#test-vectors (first payment address of nested segwit on testnet)
|
||||
(HDKey.from_string("upub5EFU65HtV5TeiSHmZZm7FUffBGy8UKeqp7vw43jYbvZPpoVsgU93oac7Wk3u6moKegAEWtGNF8DehrnHtv21XXEMYRUocHqguyjknFHYfgY"), "nes", 0, False, "test"):
|
||||
"2Mww8dCYPUpKHofjgcXcBCEGmniw9CoaiD2",
|
||||
# jdlcdl: derived via iancoleman test vector for first change address of nested segwit on testnet
|
||||
(HDKey.from_string("upub5EFU65HtV5TeiSHmZZm7FUffBGy8UKeqp7vw43jYbvZPpoVsgU93oac7Wk3u6moKegAEWtGNF8DehrnHtv21XXEMYRUocHqguyjknFHYfgY"), "nes", 0, True, "test"):
|
||||
"2MvdUi5o3f2tnEFh9yGvta6FzptTZtkPJC8",
|
||||
|
||||
# https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki#test-vectors (first payment address of taproot on mainnet)
|
||||
(HDKey.from_string("xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ"), "tr", 0, False, "main"):
|
||||
"bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
|
||||
|
||||
# https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki#test-vectors (second payment address of taproot on mainnet)
|
||||
(HDKey.from_string("xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ"), "tr", 1, False, "main"):
|
||||
"bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh",
|
||||
|
||||
# https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki#test-vectors (first change address of taproot on mainnet)
|
||||
(HDKey.from_string("xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ"), "tr", 0, True, "main"):
|
||||
"bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7",
|
||||
|
||||
# jdlcdl: derived via electrum m/44'/1'/0 (first payment address p2pkh on testnet)
|
||||
(HDKey.from_string("tpubDC5FSnBiZDMmhiuCmWAYsLwgLYrrT9rAqvTySfuCCrgsWz8wxMXUS9Tb9iVMvcRbvFcAHGkMD5Kx8koh4GquNGNTfohfk7pgjhaPCdXpoba"), "leg", 0, False, "test"):
|
||||
"mkpZhYtJu2r87Js3pDiWJDmPte2NRZ8bJV",
|
||||
|
||||
# jdlcdl: derived via electrum m/44'/1'/0 (first change address p2pkh on testnet)
|
||||
(HDKey.from_string("tpubDC5FSnBiZDMmhiuCmWAYsLwgLYrrT9rAqvTySfuCCrgsWz8wxMXUS9Tb9iVMvcRbvFcAHGkMD5Kx8koh4GquNGNTfohfk7pgjhaPCdXpoba"), "leg", 0, True, "test"):
|
||||
"mi8nhzZgGZQthq6DQHbru9crMDerUdTKva",
|
||||
|
||||
# jdlcdl: nonsense script_type falls off end of function returning None. TODO: Would it be preferred to "else: raise ValueError"?
|
||||
(HDKey.from_string("tpubDC5FSnBiZDMmhiuCmWAYsLwgLYrrT9rAqvTySfuCCrgsWz8wxMXUS9Tb9iVMvcRbvFcAHGkMD5Kx8koh4GquNGNTfohfk7pgjhaPCdXpoba"), "NONSENSE", 0, True, "test"):
|
||||
"None",
|
||||
}
|
||||
func = embit_utils.get_single_sig_address
|
||||
|
||||
print()
|
||||
for args, expected in vectors_args_expected.items():
|
||||
print("\nasserting...")
|
||||
|
||||
# call without optional params (defaults: script_type="nat", index=0, is_change=False, embit_network="main")
|
||||
if args[1:5] == ("nat", 0, False, "main"):
|
||||
print(f' {func.__name__}(HDKey.from_string("{args[0]}")) == "{expected}"')
|
||||
assert str(func(args[0])) == expected
|
||||
|
||||
# call with ordered params
|
||||
print(f' {func.__name__}(HDKey.from_string("{args[0]}"), *{args[1:5]}) == "{expected}"')
|
||||
assert str(func(*args)) == expected
|
||||
|
||||
# call with named params
|
||||
print(f' {func.__name__}(xpub=HDKey.from_string("{args[0]}"), script_type="{args[1]}", index={args[2]}, is_change={args[3]}, embit_network="{args[4]}") == "{expected}"')
|
||||
assert str(func(xpub=args[0], script_type=args[1], index=args[2], is_change=args[3], embit_network=args[4])) == expected
|
||||
|
||||
|
||||
def test_get_multisig_address():
|
||||
"""
|
||||
tests seedsigner.helpers.embit_utils.get_multisig_address()
|
||||
"""
|
||||
|
||||
from embit.descriptor import Descriptor
|
||||
|
||||
# jdlcdl: these vectors created with electrum & sparrow as a 2 of 3 multisig based on bip39-bip32-standard-path wallets
|
||||
# keystore1 = 0x00*16 = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
|
||||
# keystore2 = 0x11*16 = 'baby mass dust captain baby mass dust captain baby mass dust casino'
|
||||
# keystore3 = 0x22*16 = 'captain baby mass dust captain baby mass dust captain baby mass dutch'
|
||||
|
||||
vector_args_expected = {
|
||||
# multisig native segwit on testnet, first payment and change addresses
|
||||
("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" , 0, False, "test"): "tb1q7tpecll8jhp77yqdeyt2t8q5swxmmqeh2v22cqpms5dxlp6p27dqlftet8",
|
||||
("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" , 0, True, "test"): "tb1q7h94ywhfjrpxdfzwl4dcawrg80r4rywswjrh447x4n3e5t3m0jms9jh7pm",
|
||||
|
||||
# multisig nested segwit on testnet, first payment and change addresses
|
||||
("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", 0, False, "test"): "2MtgJH28mZWNWU7VRU4ba6ciFbRRGYWZDt3",
|
||||
("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", 0, True, "test"): "2NAjjwUQqwD9XRGLeQ6TitSUyMHUz3cLiWm",
|
||||
|
||||
# legacy multisig p2sh on testnet, not supported
|
||||
("sh(sortedmulti(2,[8d55ff0d/45h]tpubDANogJ2yfnizHwX7fSi5kUVzybyuPXDhgHB2TR9TUvkSLZFW73cRq4STKFDpx7qjJJiisyq82tbu4CeiYtmKEmT1xoCq9P8BPvXV31HUh6d/{0,1}/*,[0be174ee/45h]tpubDBkeVF2tDNT1Pz7L47iJeBB6RokU12LX6x4E6Ph8T89hmjQfB77q1AMyGwL8qpREVGq9sCJEbWwmnemwNTxnpxGn1di7BGy8jx9wEi5Vahu/{0,1}/*,[73c5da0a/45h]tpubDBKsGC1UqBDNvx9aivFmxZNgeZTUnmsCFGhWrqkLzucUCDePvbWWm3n8tAaAwMmxBG2ihdKCG9fzBdUnMxKx5PrkiqSZFi6Vkv6msUs9ddN/{0,1}/*))#p5t8sa8c", 0, False, "test"): Exception,
|
||||
|
||||
# multisig taproot on testnet, not supported
|
||||
# TODO: find what a multisig-taproot descriptor would look like and add a test so we can fall into the last condition exception.
|
||||
|
||||
# some policy that is not supported:
|
||||
# TODO: find anything non supported so we can drop off the function: Would it be preferred to "else: raise ValueError()"?
|
||||
#("sh(multi(2,[8d55ff0d/45h]tpubDANogJ2yfnizHwX7fSi5kUVzybyuPXDhgHB2TR9TUvkSLZFW73cRq4STKFDpx7qjJJiisyq82tbu4CeiYtmKEmT1xoCq9P8BPvXV31HUh6d/{0,1}/*,[0be174ee/45h]tpubDBkeVF2tDNT1Pz7L47iJeBB6RokU12LX6x4E6Ph8T89hmjQfB77q1AMyGwL8qpREVGq9sCJEbWwmnemwNTxnpxGn1di7BGy8jx9wEi5Vahu/{0,1}/*,[73c5da0a/45h]tpubDBKsGC1UqBDNvx9aivFmxZNgeZTUnmsCFGhWrqkLzucUCDePvbWWm3n8tAaAwMmxBG2ihdKCG9fzBdUnMxKx5PrkiqSZFi6Vkv6msUs9ddN/{0,1}/*))#", 0, False, "test"): None,
|
||||
|
||||
}
|
||||
func = embit_utils.get_multisig_address
|
||||
|
||||
print()
|
||||
for args, expected in vector_args_expected.items():
|
||||
descriptor = Descriptor.from_string(args[0])
|
||||
|
||||
print("\nasserting...")
|
||||
|
||||
# test successful calls
|
||||
if type(expected) == str:
|
||||
# call with optional params (defaults: index=0, is_change=False, embit_network="main")
|
||||
if args[1:4] == (0, False, 'main'):
|
||||
print(f' {func.__name__}(Descriptor.from_string("{descriptor}")) == "{expected}"')
|
||||
assert func(descriptor) == expected
|
||||
|
||||
# call with ordered params
|
||||
print(f' {func.__name__}(Descriptor.from_string("{descriptor}"), *{args[1:4]}) == "{expected}"')
|
||||
assert func(descriptor, *args[1:4]) == expected
|
||||
|
||||
# call with named params
|
||||
print(f' {func.__name__}(descriptor=Descriptor.from_string("{descriptor}"), index={args[1]}, is_change={args[2]}, embit_network="{args[3]}") == "{expected}"')
|
||||
assert func(descriptor=descriptor, index=args[1], is_change=args[2], embit_network=args[3]) == expected
|
||||
|
||||
# test exceptions
|
||||
else:
|
||||
# call with ordered params
|
||||
with pytest.raises(expected):
|
||||
print(f' {func.__name__}(Descriptor.from_string("{descriptor}"), *{args[1:4]}) raises Exception')
|
||||
func(descriptor, *args[1:4])
|
||||
|
||||
# call with named params
|
||||
with pytest.raises(expected):
|
||||
print(f' {func.__name__}(descriptor=Descriptor.from_string("{descriptor}"), index={args[1]}, is_change={args[2]}, embit_network="{args[3]}") raises Exception"')
|
||||
func(descriptor=descriptor, index=args[1], is_change=args[2], embit_network=args[3])
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import pytest
|
||||
|
||||
# Must import test base before the Controller
|
||||
from base import FlowTest, FlowStep, FlowTestUnexpectedViewException, FlowTestInvalidButtonDataSelectionException, FlowTestRunScreenNotExecutedException
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON
|
||||
from seedsigner.models.seed import Seed
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
from seedsigner.views.seed_views import SeedBackupView, SeedMnemonicEntryView, SeedOptionsView, SeedWordsWarningView
|
||||
from seedsigner.views.view import MainMenuView, PowerOptionsView, UnhandledExceptionView
|
||||
from seedsigner.views.tools_views import ToolsMenuView, ToolsCalcFinalWordNumWordsView
|
||||
|
||||
|
||||
|
||||
class TestFlowTest(FlowTest):
|
||||
|
||||
def test_simple_flow(self):
|
||||
"""
|
||||
Basic test to ensure the FlowTest can flow through a sequence of Views and
|
||||
terminate via the StopControllerCommand.
|
||||
"""
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
|
||||
FlowStep(ToolsMenuView, button_data_selection=ToolsMenuView.KEYBOARD),
|
||||
FlowStep(ToolsCalcFinalWordNumWordsView, button_data_selection=ToolsCalcFinalWordNumWordsView.TWELVE),
|
||||
FlowStep(SeedMnemonicEntryView),
|
||||
])
|
||||
|
||||
|
||||
def test_FlowTestUnexpectedViewException(self):
|
||||
"""
|
||||
Ensure that the FlowTest will raise a FlowTestUnexpectedViewException if the next
|
||||
View in the sequence is not the expected View.
|
||||
"""
|
||||
with pytest.raises(FlowTestUnexpectedViewException):
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=RET_CODE__POWER_BUTTON),
|
||||
FlowStep(ToolsMenuView), # <-- Wrong target View! Should raise an AssertionError.
|
||||
])
|
||||
|
||||
|
||||
def test_FlowTestInvalidButtonDataSelectionException(self):
|
||||
"""
|
||||
Ensure that the FlowTest will raise a FlowTestUnexpectedViewException if the
|
||||
specified button_data_selection in invalid.
|
||||
"""
|
||||
with pytest.raises(FlowTestInvalidButtonDataSelectionException):
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection="this is not a real button option!"),
|
||||
])
|
||||
|
||||
|
||||
def test_FlowTestRunScreenNotExecutedException(self):
|
||||
"""
|
||||
Ensure that the FlowTest will raise a FlowTestRunScreenNotExecutedException if the next
|
||||
View in the sequence doesn't call its View.run_screen().
|
||||
"""
|
||||
# Disable dire warnings so that the SeedWordsWarningView won't execute its run_screen()
|
||||
self.settings.set_value(SettingsConstants.SETTING__DIRE_WARNINGS, SettingsConstants.OPTION__DISABLED)
|
||||
self.controller.storage.set_pending_seed(Seed(mnemonic=["bacon"] * 24))
|
||||
self.controller.storage.finalize_pending_seed()
|
||||
|
||||
with pytest.raises(FlowTestRunScreenNotExecutedException):
|
||||
self.run_sequence(
|
||||
initial_destination_view_args=dict(seed_num=0),
|
||||
sequence=[
|
||||
FlowStep(SeedOptionsView, button_data_selection=SeedOptionsView.BACKUP),
|
||||
FlowStep(SeedBackupView, button_data_selection=SeedBackupView.VIEW_WORDS),
|
||||
FlowStep(SeedWordsWarningView, screen_return_value=0),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def test_before_run_executes(self):
|
||||
"""
|
||||
Ensure that the FlowTest can execute a function before running a View.
|
||||
"""
|
||||
# TODO
|
||||
pass
|
||||
|
||||
|
||||
def test_back_button_flow(self):
|
||||
"""
|
||||
Ensure that the FlowTest works correctly with the Controller and its BackStack.
|
||||
"""
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON),
|
||||
FlowStep(PowerOptionsView, screen_return_value=RET_CODE__BACK_BUTTON),
|
||||
FlowStep(MainMenuView),
|
||||
])
|
||||
|
||||
|
||||
def test_initial_destination(self):
|
||||
"""
|
||||
Ensure that the FlowTest can start from a View other than MainMenuView.
|
||||
"""
|
||||
# Don't have to start at the MainMenuView; can jump straight in
|
||||
self.run_sequence([
|
||||
FlowStep(ToolsCalcFinalWordNumWordsView),
|
||||
])
|
||||
|
||||
# And again, but this time with a View that requires input view_args
|
||||
self.reset_controller()
|
||||
self.controller = Controller.get_instance()
|
||||
|
||||
# Load a seed into the Controller
|
||||
seed = Seed(mnemonic=["abandon "* 11 + "about"])
|
||||
self.controller.storage.set_pending_seed(seed)
|
||||
self.controller.storage.finalize_pending_seed()
|
||||
|
||||
self.run_sequence(
|
||||
initial_destination_view_args=dict(seed_num=0),
|
||||
sequence=[
|
||||
FlowStep(SeedOptionsView, button_data_selection=SeedOptionsView.BACKUP),
|
||||
FlowStep(SeedBackupView),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_raise_exception_via_screen_return_value(self):
|
||||
"""
|
||||
Ensure that the FlowTest can raise an exception via the screen_return_value.
|
||||
"""
|
||||
# A generic Exception should be caught by the Controller and routed to the
|
||||
# UnhandledExceptionView.
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, screen_return_value=Exception("Test exception")),
|
||||
FlowStep(UnhandledExceptionView),
|
||||
])
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
from base import FlowTest, FlowStep
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.views.view import MainMenuView
|
||||
from seedsigner.views import scan_views, seed_views, psbt_views
|
||||
|
||||
class TestPSBTFlows(FlowTest):
|
||||
|
||||
def test_scan_psbt_first_then_correct_seedqr_flow(self):
|
||||
"""
|
||||
Selecting "Scan" from the MainMenuView and scanning a PSBT should enter the PSBTSelectSeedView flow
|
||||
when Scan a Seed is selected from PSBTSelectSeedView it should enter the ScanView flow
|
||||
when a SeedQR is scanned it should enter the PSBTOverviewView flow
|
||||
since the PSBT has change no warning is displayed and it should enter the PSBTMathView flow
|
||||
since the PSBT is not a self transfer it should enter the PSBTAddressDetailsView flow
|
||||
"""
|
||||
def load_psbt_into_decoder(view: scan_views.ScanView):
|
||||
"""
|
||||
PSBT Tx and Wallet Details
|
||||
- Single Sig Wallet P2WPKH (Native Segwit) with no passphrase
|
||||
- Regtest c751dc07 m/84'/1'/0' tpubDDZBrnxMxbVzqt8EoEiABPxeKzFWma5pra5UEbg3Wst1hrwr6feuvcy7Sov7cpuYx94ypuy1PQ9NDNoQagFs37wGALzLb5Ei3FvyJWPPPKZ
|
||||
- 2 Inputs
|
||||
- 56,522,834 sats
|
||||
- 1,990,245,069 sats
|
||||
- 4 Outputs
|
||||
- 1 Output to another wallet (bcrt1q7cw0wzy8g6mq5qvkpvhnk5gsps5ncy3srp0n2j) of 123,456 sats
|
||||
- 3 Outputs change
|
||||
- 3 outputs to emulate a fake mix to increase privacy
|
||||
- Change addresses are index 1/7, 1/8, 1/9
|
||||
- 1/7 address bcrt1q53j0xwuskuf5gnvynadh0hlazyy8srydlucrhg with amount 123,456 sats
|
||||
- 1/8 address bcrt1q5gtw3zfp4cx67yk5q42q6j6rfza8aqcwpyyslv with amount 1,990,121,477 sats
|
||||
- 1/9 address bcrt1q9rrg7399m43cn0yg4tz0v0ate89jgf2d6kpz7v with amount 56,399,242 sats
|
||||
- Fee 272 sats
|
||||
"""
|
||||
view.decoder.add_data("cHNidP8BANgCAAAAAsTXZs3fz/dmGb6M80+jjvJZdYya+cw5bT/dGuhZFdSlAAAAAAD9////qo6xg/UZAvUkcbse1F+C9zbP/FeZNjThx7SCIn6eMCgBAAAAAP3///8EQOIBAAAAAAAWABSkZPM7kLcTRE2En1t33/0RCHgMjQXYnnYAAAAAFgAUKMaPRKXdY4m8iKrE9j+rycskJU1A4gEAAAAAABYAFPYc9wiHRrYKAZYLLztREAwpPBIwipVcAwAAAAAWABSiFuiJIa4NrxLUBVQNS0NIun6DDtoRAABPAQQ1h88DBcQGZIAAAAA+0J+jlNL3dpWwlnBi8Dx+Ipg4e6uvB3HdjzFPX7r9CAOOlAIxgII+/xCcj+XoEenKH7wj5s5wlu7Q7CCZWFLGLhA5Su0UVAAAgAEAAIAAAACAAAEA7QIAAAAEE6njX/fnvn7hbkKIRcxzNYFOSfbCdNeWnd7Fe/1UcQ0BAAAAAP3///8TqeNf9+e+fuFuQohFzHM1gU5J9sJ015ad3sV7/VRxDQMAAAAA/f///xOp41/3575+4W5CiEXMczWBTkn2wnTXlp3exXv9VHENBAAAAAD9////E6njX/fnvn7hbkKIRcxzNYFOSfbCdNeWnd7Fe/1UcQ0GAAAAAP3///8CUnheAwAAAAAWABRCfygPJ+Fjsx4BknYvvm3A3qKn2xJ/XQcAAAAAF6kU1I4TAst5nAj15ey7vwe5cM3OFq+HlhEAAAEBH1J4XgMAAAAAFgAUQn8oDyfhY7MeAZJ2L75twN6ip9sBAwQBAAAAIgYCo7sfm78RQY3B5n0ac/QF8VtMAzFnci+h5D1MtpgRY7oYOUrtFFQAAIABAACAAAAAgAEAAAAGAAAAAAEAcQIAAAABxY7wh0nsfJQfzWrD/9rN9BYsM+iOmPaO6I0ANFgO/PcAAAAAAP3///8CptiUAAAAAAAWABRIm4HhQY/TzOjeWSPRrbuJo9MlW826oHYAAAAAFgAU0z+0L2QSLGtyQTn8FhbCpcI7jbliAQAAAQEfzbqgdgAAAAAWABTTP7QvZBIsa3JBOfwWFsKlwjuNuQEDBAEAAAAiBgITHmebEANk81CraV4xZIpqkNjjw0tIvezl1Ism1NRH3Rg5Su0UVAAAgAEAAIAAAACAAQAAAAAAAAAAIgICuTT7WnuiUTpObjWnZFHzIeEvW9PTB+1LLVFNQJVFeIIYOUrtFFQAAIABAACAAAAAgAEAAAAHAAAAACICAk8f3hpc5C35chgSg+Pe2zZ9IhHREd4aKW2+yAMRIFeqGDlK7RRUAACAAQAAgAAAAIABAAAACQAAAAAAIgIDjt1CjvrnMMnjbmTNKUAYoKEDRbmKjNjbq+6Ppqj3bqQYOUrtFFQAAIABAACAAAAAgAEAAAAIAAAAAA==")
|
||||
|
||||
def load_seed_into_decoder(view: scan_views.ScanView):
|
||||
view.decoder.add_data("080115060387063104071857067618681125136207731354")
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
|
||||
FlowStep(scan_views.ScanView, before_run=load_psbt_into_decoder), # simulate read PSBT; ret val is ignored
|
||||
FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.SCAN_SEED),
|
||||
FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder),
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
|
||||
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
|
||||
FlowStep(psbt_views.PSBTOverviewView),
|
||||
FlowStep(psbt_views.PSBTMathView),
|
||||
FlowStep(psbt_views.PSBTAddressDetailsView, button_data_selection=psbt_views.PSBTAddressDetailsView.NEXT),
|
||||
FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT),
|
||||
FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT),
|
||||
FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT),
|
||||
FlowStep(psbt_views.PSBTFinalizeView, button_data_selection=psbt_views.PSBTFinalizeView.APPROVE_PSBT),
|
||||
FlowStep(psbt_views.PSBTSignedQRDisplayView),
|
||||
FlowStep(MainMenuView)
|
||||
])
|
||||
@@ -0,0 +1,290 @@
|
||||
# Must import test base before the Controller
|
||||
from base import BaseTest, FlowTest, FlowStep
|
||||
from base import FlowTestUnexpectedViewException, FlowTestRunScreenNotExecutedException, FlowTestInvalidButtonDataSelectionException
|
||||
|
||||
import pytest
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
from seedsigner.models.seed import Seed
|
||||
from seedsigner.views.view import MainMenuView
|
||||
from seedsigner.views import seed_views, scan_views
|
||||
|
||||
|
||||
|
||||
class TestSeedFlows(FlowTest):
|
||||
|
||||
def test_scan_seedqr_flow(self):
|
||||
"""
|
||||
Selecting "Scan" from the MainMenuView and scanning a SeedQR should enter the
|
||||
Finalize Seed flow and end at the SeedOptionsView.
|
||||
"""
|
||||
def load_seed_into_decoder(view: scan_views.ScanView):
|
||||
view.decoder.add_data("0000" * 11 + "0003")
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
|
||||
FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder), # simulate read SeedQR; ret val is ignored
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
|
||||
FlowStep(seed_views.SeedOptionsView),
|
||||
])
|
||||
|
||||
|
||||
def test_mnemonic_entry_flow(self):
|
||||
"""
|
||||
Manually entering a mnemonic should land at the Finalize Seed flow and end at
|
||||
the SeedOptionsView.
|
||||
"""
|
||||
def test_with_mnemonic(mnemonic):
|
||||
sequence = [
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS),
|
||||
FlowStep(seed_views.SeedsMenuView, is_redirect=True), # When no seeds are loaded it auto-redirects to LoadSeedView
|
||||
FlowStep(seed_views.LoadSeedView, button_data_selection=seed_views.LoadSeedView.TYPE_12WORD if len(mnemonic) == 12 else seed_views.LoadSeedView.TYPE_24WORD),
|
||||
]
|
||||
|
||||
# Now add each manual word entry step
|
||||
for word in mnemonic:
|
||||
sequence.append(
|
||||
FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word)
|
||||
)
|
||||
|
||||
# With the mnemonic completely entered, we land on the SeedFinalizeView
|
||||
sequence += [
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
|
||||
FlowStep(seed_views.SeedOptionsView),
|
||||
]
|
||||
|
||||
self.run_sequence(sequence)
|
||||
|
||||
# Test data from iancoleman.io; 12- and 24-word mnemonic
|
||||
test_with_mnemonic("tone flat shed cool census soul paddle boy flight fantasy stem social".split())
|
||||
|
||||
BaseTest.reset_controller()
|
||||
|
||||
test_with_mnemonic("cotton artefact spy mind wing there echo steak child oak awful host despair online bicycle divorce middle firm diamond rare execute chimney almost hollow".split())
|
||||
|
||||
|
||||
def test_invalid_mnemonic(self):
|
||||
""" Should be able to go back and edit or discard an invalid mnemonic """
|
||||
# Test data from iancoleman.io
|
||||
mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split()
|
||||
sequence = [
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS),
|
||||
FlowStep(seed_views.SeedsMenuView, is_redirect=True), # When no seeds are loaded it auto-redirects to LoadSeedView
|
||||
FlowStep(seed_views.LoadSeedView, button_data_selection=seed_views.LoadSeedView.TYPE_12WORD if len(mnemonic) == 12 else seed_views.LoadSeedView.TYPE_24WORD),
|
||||
]
|
||||
for word in mnemonic[:-1]:
|
||||
sequence.append(FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word))
|
||||
|
||||
sequence += [
|
||||
FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value="zoo"), # But finish with an INVALID checksum word
|
||||
FlowStep(seed_views.SeedMnemonicInvalidView, button_data_selection=seed_views.SeedMnemonicInvalidView.EDIT),
|
||||
]
|
||||
|
||||
# Restarts from first word
|
||||
for word in mnemonic[:-1]:
|
||||
sequence.append(FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word))
|
||||
|
||||
sequence += [
|
||||
FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value="zebra"), # provide yet another invalid checksum word
|
||||
FlowStep(seed_views.SeedMnemonicInvalidView, button_data_selection=seed_views.SeedMnemonicInvalidView.DISCARD),
|
||||
FlowStep(MainMenuView),
|
||||
]
|
||||
|
||||
self.run_sequence(sequence)
|
||||
|
||||
|
||||
def test_export_xpub_standard_flow(self):
|
||||
"""
|
||||
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):
|
||||
self.run_sequence(
|
||||
initial_destination_view_args=dict(seed_num=0),
|
||||
sequence=[
|
||||
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB),
|
||||
FlowStep(seed_views.SeedExportXpubSigTypeView, button_data_selection=sig_tuple[1]),
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=script_tuple[1]),
|
||||
FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=coord_tuple[1]),
|
||||
FlowStep(seed_views.SeedExportXpubWarningView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0),
|
||||
FlowStep(MainMenuView),
|
||||
]
|
||||
)
|
||||
|
||||
# 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()
|
||||
|
||||
# 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
|
||||
|
||||
# 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])
|
||||
|
||||
# exhaustively test flows thru standard sig_types, script_types, and coordinators
|
||||
for sig_tuple in sig_types:
|
||||
for script_tuple in script_types:
|
||||
for coord_tuple in coordinators:
|
||||
# skip custom derivation
|
||||
if script_tuple[0] == SettingsConstants.CUSTOM_DERIVATION:
|
||||
continue
|
||||
# skip multisig taproot
|
||||
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)
|
||||
|
||||
|
||||
def test_export_xpub_disabled_not_available_flow(self):
|
||||
"""
|
||||
If sig_type/script_type/coordinator 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()
|
||||
self.controller.storage.set_pending_seed(Seed(mnemonic=mnemonic))
|
||||
self.controller.storage.finalize_pending_seed()
|
||||
|
||||
# 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
|
||||
|
||||
# these are the disabled types that we will be testing
|
||||
disabled_sig = SettingsConstants.MULTISIG
|
||||
disabled_script = SettingsConstants.TAPROOT
|
||||
disabled_coord = SettingsConstants.COORDINATOR__NUNCHUK
|
||||
|
||||
# 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])
|
||||
|
||||
# test that multisig is not an option via exception raised when redirected to next step instead of having a choice
|
||||
with pytest.raises(FlowTestRunScreenNotExecutedException) as e:
|
||||
self.run_sequence(
|
||||
initial_destination_view_args=dict(seed_num=0),
|
||||
sequence=[
|
||||
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB),
|
||||
FlowStep(seed_views.SeedExportXpubSigTypeView, button_data_selection=disabled_sig),
|
||||
]
|
||||
)
|
||||
|
||||
# test that taproot is not an option via exception raised when choice is taproot
|
||||
with pytest.raises(FlowTestInvalidButtonDataSelectionException) as e:
|
||||
self.run_sequence(
|
||||
initial_destination_view_args=dict(seed_num=0),
|
||||
sequence=[
|
||||
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB),
|
||||
FlowStep(seed_views.SeedExportXpubSigTypeView, is_redirect=True),
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=disabled_script),
|
||||
]
|
||||
)
|
||||
|
||||
# test that nunchuk is not an option via exception raised when choice is nunchuk
|
||||
with pytest.raises(FlowTestInvalidButtonDataSelectionException) as e:
|
||||
self.run_sequence(
|
||||
initial_destination_view_args=dict(seed_num=0),
|
||||
sequence=[
|
||||
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),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_export_xpub_custom_derivation_flow(self):
|
||||
"""
|
||||
Export XPUB flow for custom derivation finishes at MainMenuView
|
||||
"""
|
||||
# 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()
|
||||
|
||||
# enable custom derivation script_type setting (plus at least one more for a choice)
|
||||
self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [
|
||||
SettingsConstants.NATIVE_SEGWIT,
|
||||
SettingsConstants.NESTED_SEGWIT,
|
||||
SettingsConstants.CUSTOM_DERIVATION
|
||||
])
|
||||
|
||||
# get display names to access button choices in the views (ugh: hardcoding, is there a better way?)
|
||||
sig_type = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SIG_TYPES)[0] # single sig
|
||||
script_type = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES)[2] # custom derivation
|
||||
coordinator = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)[3] # specter
|
||||
|
||||
self.run_sequence(
|
||||
initial_destination_view_args=dict(seed_num=0),
|
||||
sequence=[
|
||||
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB),
|
||||
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.SeedExportXpubWarningView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0),
|
||||
FlowStep(MainMenuView),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
# 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
|
||||
self.settings.update({
|
||||
SettingsConstants.SETTING__SIG_TYPES: SettingsConstants.MULTISIG,
|
||||
SettingsConstants.SETTING__SCRIPT_TYPES: SettingsConstants.NESTED_SEGWIT,
|
||||
SettingsConstants.SETTING__COORDINATORS: SettingsConstants.COORDINATOR__SPECTER_DESKTOP,
|
||||
}, disable_missing_entries=False)
|
||||
|
||||
self.run_sequence(
|
||||
initial_destination_view_args=dict(seed_num=0),
|
||||
sequence=[
|
||||
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.SeedExportXpubWarningView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0),
|
||||
FlowStep(MainMenuView),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_discard_seed_flow(self):
|
||||
"""
|
||||
Selecting "Discard Seed" from the SeedOptionsView should enter the Discard Seed flow and
|
||||
remove the in-memory seed from the Controller.
|
||||
"""
|
||||
# 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()
|
||||
|
||||
self.run_sequence(
|
||||
initial_destination_view_args=dict(seed_num=0),
|
||||
sequence=[
|
||||
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.DISCARD),
|
||||
FlowStep(seed_views.SeedDiscardView, button_data_selection=seed_views.SeedDiscardView.DISCARD),
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS),
|
||||
FlowStep(seed_views.SeedsMenuView, is_redirect=True), # When no seeds are loaded it auto-redirects to LoadSeedView
|
||||
FlowStep(seed_views.LoadSeedView),
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,70 @@
|
||||
import os
|
||||
|
||||
# Must import test base before the Controller
|
||||
from base import FlowTest, FlowStep
|
||||
|
||||
from seedsigner.models import SettingsDefinition
|
||||
from seedsigner.models.settings import Settings
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
|
||||
from seedsigner.views.view import MainMenuView
|
||||
from seedsigner.views import settings_views
|
||||
|
||||
|
||||
|
||||
class TestSettingsFlows(FlowTest):
|
||||
|
||||
def test_persistent_settings(self):
|
||||
""" Basic flow from MainMenuView to enable/disable persistent settings """
|
||||
# Which option are we testing?
|
||||
settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__PERSISTENT_SETTINGS)
|
||||
|
||||
# No settings file should exist before we enable persistent settings
|
||||
assert os.path.exists(Settings.SETTINGS_FILENAME) == False
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS),
|
||||
FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_entry.display_name),
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, button_data_selection=settings_entry.get_selection_option_display_name_by_value(SettingsConstants.OPTION__ENABLED)),
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=RET_CODE__BACK_BUTTON),
|
||||
FlowStep(settings_views.SettingsMenuView),
|
||||
])
|
||||
|
||||
# Settings file should now exist
|
||||
assert os.path.exists(Settings.SETTINGS_FILENAME) == True
|
||||
|
||||
|
||||
def test_multiselect(self):
|
||||
""" Multiselect Settings options should stay in-place; requires BACK to exit. """
|
||||
# Which option are we testing?
|
||||
settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__COORDINATORS)
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS),
|
||||
FlowStep(settings_views.SettingsMenuView, button_data_selection=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=RET_CODE__BACK_BUTTON), # BACK to exit
|
||||
FlowStep(settings_views.SettingsMenuView),
|
||||
])
|
||||
|
||||
|
||||
def test_io_test(self):
|
||||
""" Basic flow from MainMenuView to I/O Test View """
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS),
|
||||
FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_views.SettingsMenuView.IO_TEST),
|
||||
FlowStep(settings_views.IOTestView),
|
||||
FlowStep(settings_views.SettingsMenuView),
|
||||
])
|
||||
|
||||
|
||||
def test_donate(self):
|
||||
""" Basic flow from MainMenuView to Donate View """
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS),
|
||||
FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_views.SettingsMenuView.DONATE),
|
||||
FlowStep(settings_views.DonateView),
|
||||
FlowStep(settings_views.SettingsMenuView),
|
||||
])
|
||||
@@ -0,0 +1,72 @@
|
||||
# Must import test base before the Controller
|
||||
from base import FlowTest, FlowStep
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.models.seed import Seed
|
||||
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
|
||||
from seedsigner.views.view import MainMenuView
|
||||
from seedsigner.views import scan_views, seed_views, tools_views
|
||||
|
||||
|
||||
|
||||
class TestToolsFlows(FlowTest):
|
||||
|
||||
def test_addressexplorer_flow(self):
|
||||
"""
|
||||
Test the simplest AddressExplorer flow when a seed is already loaded.
|
||||
"""
|
||||
controller = Controller.get_instance()
|
||||
seed = Seed(mnemonic=["abandon "* 11 + "about"])
|
||||
controller.storage.set_pending_seed(seed)
|
||||
controller.storage.finalize_pending_seed()
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
|
||||
FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.EXPLORER),
|
||||
FlowStep(tools_views.ToolsAddressExplorerSelectSourceView, screen_return_value=0), # ret 1st onboard seed
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES).get_selection_option_display_name_by_value(SettingsConstants.NATIVE_SEGWIT)),
|
||||
FlowStep(tools_views.ToolsAddressExplorerAddressTypeView, button_data_selection=tools_views.ToolsAddressExplorerAddressTypeView.RECEIVE),
|
||||
FlowStep(tools_views.ToolsAddressExplorerAddressListView, screen_return_value=10), # ret NEXT page of addrs
|
||||
FlowStep(tools_views.ToolsAddressExplorerAddressListView, screen_return_value=4), # ret a specific addr from the list
|
||||
FlowStep(tools_views.ToolsAddressExplorerAddressView), # runs until dismissed; no ret value
|
||||
FlowStep(tools_views.ToolsAddressExplorerAddressListView),
|
||||
])
|
||||
|
||||
|
||||
def test_addressexplorer_loadseed_sideflow(self):
|
||||
"""
|
||||
Finalizing a seed during the Address Explorer flow should return to the next
|
||||
Address Explorer step upon completion.
|
||||
"""
|
||||
controller = Controller.get_instance()
|
||||
|
||||
def load_seed_into_decoder(view: scan_views.ScanView):
|
||||
view.decoder.add_data("0000" * 11 + "0003")
|
||||
|
||||
# Finalize the new seed w/out passphrase
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
|
||||
FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.EXPLORER),
|
||||
FlowStep(tools_views.ToolsAddressExplorerSelectSourceView, button_data_selection=tools_views.ToolsAddressExplorerSelectSourceView.SCAN_SEED),
|
||||
FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder), # simulate read SeedQR
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
|
||||
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView),
|
||||
])
|
||||
|
||||
assert controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER
|
||||
|
||||
# Reset
|
||||
controller.storage.seeds.clear()
|
||||
controller.storage.set_pending_seed(Seed(mnemonic=["abandon "* 11 + "about"]))
|
||||
|
||||
# Finalize the new seed w/passphrase
|
||||
self.run_sequence(
|
||||
sequence=[
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE),
|
||||
FlowStep(seed_views.SeedAddPassphraseView, screen_return_value="mypassphrase"),
|
||||
FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.DONE),
|
||||
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView),
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,74 @@
|
||||
from mock import patch
|
||||
|
||||
# Must import test base before the Controller
|
||||
from base import FlowTest, FlowStep
|
||||
|
||||
from seedsigner.gui.screens.screen import RET_CODE__POWER_BUTTON
|
||||
from seedsigner.models.settings import Settings
|
||||
from seedsigner.views.tools_views import ToolsCalcFinalWordNumWordsView, ToolsMenuView
|
||||
from seedsigner.views.view import MainMenuView, NotYetImplementedView, PowerOptionsView, PowerOffView, RestartView, UnhandledExceptionView, View
|
||||
|
||||
|
||||
|
||||
class TestViewFlows(FlowTest):
|
||||
|
||||
def test_restart_flow(self):
|
||||
"""
|
||||
Basic flow from MainMenuView to RestartView
|
||||
"""
|
||||
with patch('seedsigner.views.view.RestartView.DoResetThread'):
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON),
|
||||
FlowStep(PowerOptionsView, button_data_selection=PowerOptionsView.RESET),
|
||||
FlowStep(RestartView),
|
||||
])
|
||||
|
||||
|
||||
def test_power_off_flow(self):
|
||||
"""
|
||||
Basic flow from MainMenuView to PowerOffView
|
||||
"""
|
||||
with patch('seedsigner.views.view.PowerOffView.PowerOffThread'):
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON),
|
||||
FlowStep(PowerOptionsView, button_data_selection=PowerOptionsView.POWER_OFF),
|
||||
FlowStep(PowerOffView),
|
||||
])
|
||||
|
||||
# And again, but this time as if we were in the SeedSigner OS
|
||||
Settings.HOSTNAME = 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),
|
||||
])
|
||||
|
||||
|
||||
def test_not_yet_implemented_flow(self):
|
||||
"""
|
||||
Run an incomplete View that returns None and ensure that we get the NotYetImplementedView
|
||||
"""
|
||||
class IncompleteView(View):
|
||||
def run(self):
|
||||
self.run_screen(None)
|
||||
return None
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(IncompleteView),
|
||||
FlowStep(NotYetImplementedView),
|
||||
FlowStep(MainMenuView),
|
||||
])
|
||||
|
||||
|
||||
def test_unhandled_exception_flow(self):
|
||||
"""
|
||||
Basic flow from any arbitrary View to the UnhandledExceptionView
|
||||
"""
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
|
||||
FlowStep(ToolsMenuView, button_data_selection=ToolsMenuView.KEYBOARD),
|
||||
FlowStep(ToolsCalcFinalWordNumWordsView, screen_return_value=Exception("Test exception")), # <-- force an exception
|
||||
FlowStep(UnhandledExceptionView),
|
||||
FlowStep(MainMenuView),
|
||||
])
|
||||
@@ -1,8 +1,47 @@
|
||||
import pytest
|
||||
from mock import MagicMock
|
||||
from seedsigner.models import PSBTParser
|
||||
from seedsigner.models import PSBTParser, Seed
|
||||
from embit import psbt
|
||||
from binascii import a2b_base64
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
|
||||
def test_sig_count():
|
||||
print("test")
|
||||
def test_p2tr_change_detection():
|
||||
""" Should successfully detect change in a p2tr to p2tr psbt spend
|
||||
|
||||
PSBT Tx and Wallet Details
|
||||
- Single Sig Wallet P2TR (Taproot) with no passphrase
|
||||
- Regtest 394aed14 m/86'/1'/0' tpubDCawGrRg7YdHdFb9p4mmD8GBaZjJegL53FPFRrMkGoLcgLATJfksUs2y1Q7dVzixAkgecazsxEsUuyj3LyDw7eVVYHQyojwrc2hfesK4wXW
|
||||
- 1 Inputs
|
||||
- 3,190,493,401 sats
|
||||
- 2 Outputs
|
||||
- 1 Output spend to another wallet (bcrt1p6p00wazu4nnqac29fvky6vhjnnhku5u2g9njss62rvy7e0yuperq86f5ek) p2tr address
|
||||
- 1 Output change
|
||||
- addresss bcrt1prz4g6saush37epdwhvwpu78td3q7yfz3xxz37axlx7udck6wracq3rwq30)
|
||||
- amount 2,871,443,918 sats
|
||||
- Change addresses is index 1/1
|
||||
- Fee 155 sats
|
||||
"""
|
||||
|
||||
psbt_base64 = "cHNidP8BAIkCAAAAAf8upuiIWF1VTgC/Q8ZWRrameRigaXpRcQcBe8ye+TK3AQAAAAAXCgAAAs7BJqsAAAAAIlEgGKqNQ7yF4+yFrrscHnjrbEHiJFExhR903ze43FtOH3BwTgQTAAAAACJRINBe93RcrOYO4UVLLE0y8pzvblOKQWcoQ0obCey8nA5GAAAAAE8BBDWHzwNMUx9OgAAAAJdr+WtwWfVa6IPbpKZ4KgRC0clbm11Gl155IPA27n2FAvQCrFGH6Ac2U0Gcy1IH5f5ltgUBDz2+fe8iqL6JzZdgEDlK7RRWAACAAQAAgAAAAIAAAQB9AgAAAAGAKOOUFIzw9pbRDaZ7F0DYhLImrdMn//OSm++ff5VNdAAAAAAAAQAAAAKsjLwAAAAAABYAFKEcuxvXmB3rWHSqSviP5mrKMZoL2RArvgAAAAAiUSBGU0Lg5fx/ECsB1Z4ZUqXQFSLFnlmpm0rm5R2l599h2AAAAAABASvZECu+AAAAACJRIEZTQuDl/H8QKwHVnhlSpdAVIsWeWambSublHaXn32HYAQMEAAAAACEWF7hZVn7pIDR429kAn/WDeQiWjZey1iGHztsL1H83QLMZADlK7RRWAACAAQAAgAAAAIABAAAAAAAAAAEXIBe4WVZ+6SA0eNvZAJ/1g3kIlo2XstYhh87bC9R/N0CzACEHbJdqWyMxF2eOPr6YRXUJmry04HUbgKyeM2IZeG+NI9AZADlK7RRWAACAAQAAgAAAAIABAAAAAQAAAAEFIGyXalsjMRdnjj6+mEV1CZq8tOB1G4CsnjNiGXhvjSPQAAA="
|
||||
|
||||
raw = a2b_base64(psbt_base64)
|
||||
tx = psbt.PSBT.parse(raw)
|
||||
|
||||
mnemonic = "goddess rough corn exclude cream trial fee trumpet million prevent gaze power".split()
|
||||
pw = ""
|
||||
seed = Seed(mnemonic, passphrase=pw)
|
||||
|
||||
pp = PSBTParser(p=tx, seed=seed, network=SettingsConstants.REGTEST)
|
||||
|
||||
assert pp.change_data == [
|
||||
{
|
||||
'output_index': 0,
|
||||
'address': 'bcrt1prz4g6saush37epdwhvwpu78td3q7yfz3xxz37axlx7udck6wracq3rwq30',
|
||||
'amount': 2871443918,
|
||||
'fingerprint': ['394aed14'],
|
||||
'derivation_path': ['m/86h/1h/0h/1/1']}
|
||||
]
|
||||
assert pp.spend_amount == 319049328
|
||||
assert pp.change_amount == 2871443918
|
||||
assert pp.destination_addresses == ['bcrt1p6p00wazu4nnqac29fvky6vhjnnhku5u2g9njss62rvy7e0yuperq86f5ek']
|
||||
assert pp.destination_amounts == [319049328]
|
||||
@@ -0,0 +1,16 @@
|
||||
from base import BaseTest
|
||||
from seedsigner.models.settings import Settings
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
|
||||
|
||||
class TestSettings(BaseTest):
|
||||
|
||||
def test_reset_settings(self):
|
||||
""" BaseTest.reset_settings() should wipe out any previous Settings changes """
|
||||
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
|
||||
|
||||
BaseTest.reset_settings()
|
||||
settings = Settings.get_instance()
|
||||
assert settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED
|
||||
Reference in New Issue
Block a user