Merge branch 'dev' into enable_persian

This commit is contained in:
Nick Klockenga
2025-09-05 16:14:46 -04:00
committed by GitHub
43 changed files with 971 additions and 422 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ This pull request is categorized as a:
## Checklist
- [ ] I’ve run `pytest` and made sure all unit tests pass before sumbitting the PR
- [ ] I’ve run `pytest` and made sure all unit tests pass before submitting the PR
If you modified or added functionality/workflow, did you add new unit tests?
-18
View File
@@ -1,18 +0,0 @@
name: GitHub Notify on Telegram
on:
pull_request_target:
branches:
- dev
types:
- closed
jobs:
if_merged:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Notify on Telegram
uses: EverythingSuckz/github-telegram-notify@main
with:
bot_token: '${{ secrets.BOT_TOKEN }}'
chat_id: '${{ secrets.CHAT_ID }}'
-3
View File
@@ -2,9 +2,6 @@ name: CI
on:
push:
branches:
- dev
- main
pull_request:
concurrency:
+41 -39
View File
@@ -33,7 +33,7 @@ If you have specific questions about the project, our [Telegram Group](https://t
* Stateless, air-gapped operation:
* 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.
* No wifi or Bluetooth hardware onboard.
* No WiFi or Bluetooth hardware onboard.
* Can only receive data via reading QR codes with its camera.
* Can only send data by displaying QR codes on its screen.
@@ -51,7 +51,7 @@ If you have specific questions about the project, our [Telegram Group](https://t
* Import any existing seed phrase via an optimized seed word entry interface.
* Partial support for Electrum Segwit seed phrases [(info)](docs/electrum.md).
* Wallet setup and transaction signing
* Wallet setup and transaction signing:
* Script types: Taproot, native segwit, nested segwit, legacy (p2pkh).
* Single sig and multisig xpub export.
* Support for user-defined custom derivation paths.
@@ -59,7 +59,7 @@ If you have specific questions about the project, our [Telegram Group](https://t
* Verify the PSBT's single sig or multisig change outputs or self-transfer outputs.
* Mainnet, testnet, and regtest.
* Additional utilities
* Additional utilities:
* [SettingsQR](https://github.com/SeedSigner/seedsigner-settings-generator) to instantly reconfigure a SeedSigner for beginners, advanced users, or tailored to your preferences.
* Scan a software wallet's receive or change address to verify that it's correct.
* Address Explorer for single sig and multisig wallets.
@@ -93,7 +93,7 @@ To build a SeedSigner, you will need:
Notes:
* You may need to solder the 40 GPIO pins (20 pins per row) to the Raspberry Pi Zero board. If you don't want to solder, most stores offer the board "with headers" already soldered on.
* The Pi Zero "W" or "2W" is often easier to find but has wifi/Bluetooth hardware. You can still use these boards and can optionally [disable the wifi/Bluetooth hardware](https://github.com/DesobedienteTecnologico/rpi_disable_wifi_and_bt_by_hardware).
* The Pi Zero "W" or "2W" is often easier to find but has WiFi/Bluetooth hardware. You can still use these boards and can optionally [disable the WiFi/Bluetooth hardware](https://github.com/DesobedienteTecnologico/rpi_disable_wifi_and_bt_by_hardware).
* Other cameras with the above sensor module should work, but may not fit in the Orange Pill enclosure.
* Choose the Waveshare screen carefully; they make a number of different boards that look very similar but ARE NOT COMPATIBLE! Make sure you purchase the model that has a resolution of 240x240 pixels.
* Raspberry Pi 1 is also compatible, but will require a [hardware modification to the Waveshare LCD Hat](./docs/legacy_hardware.md).
@@ -112,23 +112,24 @@ Instructions to build a SeedSigner OS image (using precisely the same process th
## Downloading the Software
Download the current Version (0.8.5) software image that is compatible with your Raspberry Pi Hardware. The Pi Zero 1.3 is the most common and recommended board.
Download the current Version (0.8.6) software image that is compatible with your Raspberry Pi Hardware. The Pi Zero 1.3 is the most common and recommended board.
| Board | Download Image Link/Name |
| --------------------- | --------------------------------- |
|**[Raspberry Pi Zero 1.3](https://www.raspberrypi.com/products/raspberry-pi-zero/)** |[`seedsigner_os.0.8.5.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.5/seedsigner_os.0.8.5.pi0.img) |
|[Raspberry Pi Zero W](https://www.raspberrypi.com/products/raspberry-pi-zero-w/) |[`seedsigner_os.0.8.5.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.5/seedsigner_os.0.8.5.pi0.img) |
|[Raspberry Pi Zero 2 W](https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/) |[`seedsigner_os.0.8.5.pi02w.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.5/seedsigner_os.0.8.5.pi02w.img) |
|[Raspberry Pi 1 Model B/B+](https://www.raspberrypi.com/products/raspberry-pi-1-model-b-plus/) |[`seedsigner_os.0.8.5.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.5/seedsigner_os.0.8.5.pi0.img) |
|[Raspberry Pi 2 Model B](https://www.raspberrypi.com/products/raspberry-pi-2-model-b/) |[`seedsigner_os.0.8.5.pi2.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.5/seedsigner_os.0.8.5.pi2.img) |
|[Raspberry Pi 3 Model B](https://www.raspberrypi.com/products/raspberry-pi-3-model-b/) |[`seedsigner_os.0.8.5.pi02w.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.5/seedsigner_os.0.8.5.pi02w.img) |
|[Raspberry Pi 4 Model B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/) |[`seedsigner_os.0.8.5.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.5/seedsigner_os.0.8.5.pi4.img) |
|[Raspberry Pi 400](https://www.raspberrypi.com/products/raspberry-pi-400-unit/) |[`seedsigner_os.0.8.5.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.5/seedsigner_os.0.8.5.pi4.img) |
|**[Raspberry Pi Zero 1.3](https://www.raspberrypi.com/products/raspberry-pi-zero/)** |[`seedsigner_os.0.8.6.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.6/seedsigner_os.0.8.6.pi0.img) |
|[Raspberry Pi Zero W](https://www.raspberrypi.com/products/raspberry-pi-zero-w/) |[`seedsigner_os.0.8.6.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.6/seedsigner_os.0.8.6.pi0.img) |
|[Raspberry Pi Zero 2 W](https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/) |[`seedsigner_os.0.8.6.pi02w.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.6/seedsigner_os.0.8.6.pi02w.img) |
|[Raspberry Pi 1 Model B/B+](https://www.raspberrypi.com/products/raspberry-pi-1-model-b-plus/) |[`seedsigner_os.0.8.6.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.6/seedsigner_os.0.8.6.pi0.img) |
|[Raspberry Pi 2 Model B](https://www.raspberrypi.com/products/raspberry-pi-2-model-b/) |[`seedsigner_os.0.8.6.pi2.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.6/seedsigner_os.0.8.6.pi2.img) |
|[Raspberry Pi 3 Model B](https://www.raspberrypi.com/products/raspberry-pi-3-model-b/) |[`seedsigner_os.0.8.6.pi02w.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.6/seedsigner_os.0.8.6.pi02w.img) |
|[Raspberry Pi 4 Model B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/) |[`seedsigner_os.0.8.6.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.6/seedsigner_os.0.8.6.pi4.img) |
|[Raspberry Pi 400](https://www.raspberrypi.com/products/raspberry-pi-400-unit/) |[`seedsigner_os.0.8.6.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.6/seedsigner_os.0.8.6.pi4.img) |
Note: If you have physically removed the WiFi component from your board, you will still use the image file of the original(un-modified) hardware. (Our files are compiled/based on the *processor* architecture). Although it is better to spend a few minutes upfront to determine which specific Pi hardware/model you have, if you are still unsure which hardware you have, you can try using the pi0.img file. Making an incorrect choice here will not ruin your board, because this is software, not firmware.
Note: If you have physically removed the WiFi component from your board, you will still use the image file of the original (un-modified) hardware. (Our files are compiled/based on the *processor* architecture). Although it is better to spend a few minutes upfront to determine which specific Pi hardware/model you have, if you are still unsure which hardware you have, you can try using the pi0.img file. Making an incorrect choice here will not ruin your board, because this is software, not firmware.
**Also download** these 2 signature verification files to the same folder
[The Plaintext manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.8.6/seedsigner.0.8.6.sha256.txt)
[The Signature of the manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.8.6/seedsigner.0.8.6.sha256.txt.sig)
**also download** these 2 signature verification files to the same folder
[The Plaintext manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.8.5/seedsigner.0.8.5.sha256.txt)
[The Signature of the manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.8.5/seedsigner.0.8.5.sha256.txt.sig)
Users familiar with older versions of the SeedSigner software might be surprised with how fast their software downloads now are, because since version 0.6.0 the software image files are now 100x smaller! Each image file is now under 42 Megabytes so your downloads and verifications will be very quick now (and might even seem *too* quick)!
@@ -147,7 +148,7 @@ We assume you are running the commands from a computer where both [GPG](https://
### Step 1. Verify that the signature (.sig) file is genuine:
Run GPG's *fetch-keys* command to import the SeedSigner projects public key from the popular online keyserver called *Keybase.io*, into your computers *keychain*.
Run GPG's *fetch-keys* command to import the SeedSigner project's public key from the popular online keyserver called *Keybase.io*, into your computer's *keychain*.
```
@@ -159,7 +160,7 @@ The result should confirm that 1 key was *either* imported or updated. *Ignore*
Next, you will run the *verify* command on the signature (.sig) file. (*Verify* must be run from inside the same folder that you downloaded the files into earlier.)
```
gpg --verify seedsigner.0.8.5.sha256.txt.sig
gpg --verify seedsigner.0.8.6.sha256.txt.sig
```
When the verify command completes successfully, it should display output like this:
@@ -169,7 +170,7 @@ The result must display "**Good signature**". Ignore any email addresses - *onl
<BR>
On the *last* output line, look at your *rightmost* 16 characters (the 4 blocks of 4).
**Crucially, we must now check WHO that Primary key fingerprint /ID belongs to.** We will start by looking at Keybase.io to see if it is the *SeedSigner project* 's public key or not.
**Crucially, we must now check WHO that Primary key fingerprint /ID belongs to.** We will start by looking at Keybase.io to see if it is the *SeedSigner project*'s public key or not.
<details><summary> About the warning message:</summary>
<p> Since you are about to match the outputted fingerprint/ID against the proofs at Keybase.io/SeedSigner, and thereby confirm who the pubkey really belongs to-, you can safely ignore this warning message:
@@ -184,16 +185,16 @@ On the *last* output line, look at your *rightmost* 16 characters (the 4 blocks
<details><summary> More about how the verify command works:</summary>
<p>
The verify command will attempt to decrypt the signature file (sha256.sig) by trying each public key already imported into your computer. If the public key we just imported (via fetch-keys), manages to: (a) successfully decrypt the .sig file , and (b), that result matches exactly to the clear-text equivalent (.sha256) of the .sig file, then its "a good signature"!
The verify command will attempt to decrypt the signature file (sha256.sig) by trying each public key already imported into your computer. If the public key we just imported (via fetch-keys), manages to: (a) successfully decrypt the .sig file , and (b), that result matches exactly to the clear-text equivalent (.sha256) of the .sig file, then it's "a good signature"!
Crucially, we must still manually check who *exactly* owns the Key ID which gave us that "Good signature". Thats what the warning message means- Who does the matching key really belong to? We will start by looking at keybase.io to see if it is "The SeedSigner project"'s public Key or not.
Crucially, we must still manually check who *exactly* owns the Key ID which gave us that "Good signature". That's what the warning message means- Who does the matching key really belong to? We will start by looking at keybase.io to see if it is "The SeedSigner project"'s public Key or not.
Note that it is the file hashes of .sig and .sha256 that *verify* compares, not their raw contents.
</p>
</details>
<br>
Now to determine ***who*** the Public key ID belongs to: Goto [Keybase.io/SeedSigner](https://keybase.io/seedsigner)
Now to determine ***who*** the Public key ID belongs to: Go to [Keybase.io/SeedSigner](https://keybase.io/seedsigner)
<BR>
![SS - Keybase Website PubKey visual matching1_Cropped-80pct](https://user-images.githubusercontent.com/91296549/215326193-97c84e35-5570-4e52-bf3f-e86d367c8908.jpg)
@@ -207,13 +208,13 @@ Now to determine ***who*** the Public key ID belongs to: Goto [Keybase.io/SeedSi
<details><summary>Learn more about how keybase.io helps you check that someone (online) is who they say they are:</summary>
<p>
Keybase.io allows you to independently verify that the public key saved on Keybase.io, is both authentic and that it belongs to the organization it claims to represent.
Keybase has already checked the three pubkey file locations cryptographically when they were saved there. You can further verify the key publications if you would like:
Keybase has already checked the three pubkey file locations cryptographically when they were saved there. You can further verify the key publications if you would like:
- *via Keybase*: By clicking on any of the three blue badges to see that the "proof" was published at that location. (The blue badge marked as tweet, is in the most human-readable form and it is also a bi-directional link on Twitter)
or,
- *without keybase (out-of-band)*: By using these 3 links directly: [Twitter](https://twitter.com/SeedSigner/status/1530555252373704707), [Github](https://gist.github.com/SeedSigner/5936fa1219b07e28a3672385b605b5d2) and [SeedSigner.com](https://seedsigner.com/keybase.txt). This method can be used if you would like to make an even deeper, independent inspection without relying on Keybase at all, or if the Keybase.io site is no longer valid or it is removed entirely.
Once you have used one of these methods, you will know if the Public Key stored on Keybase, is genuinely from the SeedSinger Project or not.
Once you have used one of these methods, you will know if the Public Key stored on Keybase, is genuinely from the SeedSigner Project or not.
</p>
</details>
<br>
@@ -224,29 +225,30 @@ If the two ID's do *not* match, then you must stop here immediately. Do not cont
### Step 2. Verifying that the *software images/binaries* are genuine
Now that you have confirmed that you do have the real SeedSigner Project's Public Key (ie the 16 characters match) - you can return to your terminal window. Running the *shasum* command, is the final verification step and will confirm (via file hashing) that the software code/image files, were also not altered since publication, or even during your download process.
(Prior to version 0.6.0 , your verify command will check the .zip file which contains the binary files.)
Now that you have confirmed that you do have the real SeedSigner Project's Public Key (ie the 16 characters match) - you can return to your terminal window. Running the *shasum* command, is the final verification step and will confirm (via file hashing) that the software code/image files, were also not altered since publication, or even during your download process.
(Prior to version 0.6.0, your verify command will check the .zip file which contains the binary files.)
**On Linux or OSX:** Run this command
```
shasum -a 256 --ignore-missing --check seedsigner.0.8.5.sha256.txt
shasum -a 256 --ignore-missing --check seedsigner.0.8.6.sha256.txt
```
Note: macOS versions earlier than v11 (Big Sur) do not support the `--ignore-missing` flag. You can omit it and disregard any missing file warnings.
**On Windows (inside Powershell):** Run this command
```
CertUtil -hashfile seedsigner_os.0.8.5.Insert_Your_Pi_Models_binary_here_For_Example_pi02w.img SHA256
CertUtil -hashfile seedsigner_os.0.8.6.Insert_Your_Pi_Models_binary_here_For_Example_pi02w.img SHA256
```
On Windows, you must then manually compare the resulting file hash value to the corresponding hash value shown inside the .SHA256 cleartext file.
<BR>
Wait up to 30 seconds for the command to complete, and it should display:
```
seedsigner_os.0.8.5.[Your_Pi_Model_For_Example:pi02w].img: OK
seedsigner_os.0.8.6.[Your_Pi_Model_For_Example:pi02w].img: OK
```
**If you receive the "OK" message** for your **seedsigner_os.0.8.5.[Your_Pi_Model_For_Example:pi02w].img file**, as shown above, then your verification is fully complete!
**If you receive the "OK" message** for your **seedsigner_os.0.8.6.[Your_Pi_Model_For_Example:pi02w].img file**, as shown above, then your verification is fully complete!
**All of your downloaded files have now been confirmed as both authentic and unaltered!** You can proceed to create/write your MicroSD card😄😄 !!
If your file result shows "FAILED", then you must stop here immediately. Do not continue. Contact us for assistance at the Telegram group address above.
If your file result shows "FAILED", then you must stop here immediately. Do not continue. Contact us for assistance at the Telegram group address above.
<BR>
@@ -265,19 +267,19 @@ To write the SeedSigner software onto your MicroSD card, there are a few options
| DD Command Line Utility | Built-in to Linux and MacOS, the DD (Data Duplicator) is a tool for advanced users. If not used carefully it can accidentally format the incorrect disk! | Built-in to Linux and MacOS |
Be sure to download the software from the genuine publisher.
Either of the Etcher or Pi Imager software is recommended. Some SeedSigner users have reported a better experience with one or the other. So, if the one application doesn’t work well for your particular machine, then please try the other one.
Either of the Etcher or Pi Imager software is recommended. Some SeedSigner users have reported a better experience with one or the other. So, if the one application doesn't work well for your particular machine, then please try the other one.
<BR>
### **General Considerations:**
The writing and verify steps are very quick from version 0.6.0 upwards, so please pay close attention to your screen.
Make sure to set any write-protection physical slider on the MicroSD Card Adapter to UN-locked.
You also don’t need to pre-format the MicroSD beforehand. You *dont* need to unzip any .zip file beforehand.
Current Etcher and Pi Imager software will perform a verify action (by default) to make sure the card was written successfully! Watching for that verify step to complete successfully, can save you a lot of headaches if you later need to troubleshoot issues where your SeedSigner device doesn’t boot up at power on.
Writing the MicroSd card is also known as flashing.
You also don't need to pre-format the MicroSD beforehand. You *don't* need to unzip any .zip file beforehand.
Current Etcher and Pi Imager software will perform a verify action (by default) to make sure the card was written successfully! Watching for that verify step to complete successfully, can save you a lot of headaches if you later need to troubleshoot issues where your SeedSigner device doesn't boot up at power on.
Writing the MicroSD card is also known as flashing.
It will overwrite everything on the MicroSD card.
If the one application fails for you, then please try again using our other recommended application.
Advanced users may want to try the Linux/MacOS *DD* command instead of using Etcher or Pi Imager, however, a reminder is given that DD can overwrite the wrong disk if you are not careful !
Advanced users may want to try the Linux/MacOS *DD* command instead of using Etcher or Pi Imager, however, a reminder is given that DD can overwrite the wrong disk if you are not careful!
#### **Specific considerations for Windows users:**
Use the Pi imager software as your first choice on Windows. Windows can sometimes flag the writing of a MicroSD as risky behaviour and hence it may prevent this activity. If this happens, your writing/flashing will fail, hang or wont even begin, in which case you should to try to run the Etcher/Pi-Imager app "As administrator", (right-click and choose that option). It can also be blocked by windows security in some cases, so If you have the (non-default) *Controlled Folder Access* option set to active, try turning that *off* temporarily.
Use the Pi imager software as your first choice on Windows. Windows can sometimes flag the writing of a MicroSD as risky behaviour and hence it may prevent this activity. If this happens, your writing/flashing will fail, hang or won't even begin, in which case you should try to run the Etcher/Pi-Imager app "As administrator", (right-click and choose that option). It can also be blocked by windows security in some cases, so If you have the (non-default) *Controlled Folder Access* option set to active, try turning that *off* temporarily.
@@ -288,7 +290,7 @@ Use the Pi imager software as your first choice on Windows. Windows can sometime
### Open Pill
The Open Pill enclosure design is all about quick, simple and inexpensive depoloyment of a SeedSigner device. The design does not require any additional hardware and can be printed using a standard FDM 3D printer in about 2 hours, no supports necessary. A video demonstrating the assembly process can be found [here](https://youtu.be/gXPFJygZobEa). To access the design file and printable model, click [here](https://github.com/SeedSigner/seedsigner/tree/main/enclosures/open_pill).
The Open Pill enclosure design is all about quick, simple and inexpensive deployment of a SeedSigner device. The design does not require any additional hardware and can be printed using a standard FDM 3D printer in about 2 hours, no supports necessary. A video demonstrating the assembly process can be found [here](https://youtu.be/gXPFJygZobEa). To access the design file and printable model, click [here](https://github.com/SeedSigner/seedsigner/tree/main/enclosures/open_pill).
### Orange Pill
+5 -9
View File
@@ -1,21 +1,17 @@
# Code Structure
SeedSigner roughly follows a Model-View-Controller approach. Like in a typical web app (e.g. Flask) the `View`s can be called as needed like individual web urls. After completing display and interaction with the user, the `View` then decides where to route the user next, analogous to a web app returning a `response.redirect(url)`.
The `Controller` then ends up being quite stripped down. For example, there's no need for a web app's `urls.py` since there are no mappings from url to `View` to maintain since we're not actually using a url/http routing approach.
`View`s have to handle user interaction so there are `while True` loops that cycle between waiting for user input, gathering data, and then updating the UI components accordingly. You wouldn't find this kind of cycle in a web app because this sort of interactive user input is handled in the browser at the html/css/js level.
SeedSigner roughly follows a Model-View-Controller approach. Like in a typical web app (e.g. Flask), the `View`s can be called as needed like individual web URLs. After completing display and interaction with the user, the `View` then decides where to route the user next, analogous to a web app returning a `response.redirect(URL)`.
The `Controller` then ends up being quite stripped down. For example, there's no need for a web app's `urls.py` since there are no mappings from URL to `View` to maintain since we're not actually using a URL/HTTP routing approach.
`View`s have to handle user interaction, so there are `while True` loops that cycle between waiting for user input, gathering data, and then updating the UI components accordingly. You wouldn't find this kind of cycle in a web app because this sort of interactive user input is handled in the browser at the HTML/CSS/JS level.
* `Model`s: Store the persistent settings, the in-memory seeds, current wallet information, etc.
* `Controller`: Manages the state of the world and controls access to global resources.
* `View`s: Implementation of each screen. Prepares relevant data for display. Must also instantiate the display objects that will actually render the UI.
* `gui.screens`: Re-usable formatted UI renderers.
* `gui.screens`: Reusable formatted UI renderers.
* `gui.components`: Basic individual UI elements that are used by the `templates` such as the top nav, buttons, button lists, text displays.
In an typical webserver context the `View` would send data to an html template (e.g. Jinja) which would then dynamically populate the page with html elements like `<input>`, `<button>`, `<img>`, etc. This is analgous to our `gui.screens` constructing a UI renderer by piecing together various `gui.components` as needed.
In a typical web server context, the `View` would send data to an HTML template (e.g. Jinja) which would then dynamically populate the page with HTML elements like `<input>`, `<button>`, `<img>`, etc. This is analogous to our `gui.screens` constructing a UI renderer by piecing together various `gui.components` as needed.
`Controller` is a global singleton that any `View` can access and update as needed.
+26 -18
View File
@@ -1,37 +1,45 @@
## Debugging a Crash for advanced (technical) users
## Debugging a Crash for Advanced (Technical) Users
These instructions are intended to help users of SeedSigner provide crash exception and traceback logs to developers to aid in troubleshooting and resolving bugs.
### Testnet vs Mainnet
Whenever possible recreate a crash in testnet. This will help avoid accidently revealing private information about yourself, your bitcoin transactions, or lose any funds.
Whenever possible, recreate a crash in testnet. This will help avoid accidentally revealing private information about yourself, your Bitcoin transactions, or losing any funds.
### Network connected SeedSigner
### Network-Connected SeedSigner
If you are using SeedSigner for development and testing, then I recommend network access via ssh to view crash logs. Follow [these](https://github.com/SeedSigner/seedsigner/blob/main/docs/usb_relay.md) instructions to setup a USB relay for internet access. You can also connect your SeedSigner to Wifi if you have a rasp pi zero w/ wifi.
If you are using SeedSigner for development and testing, then we recommend network access via SSH to view crash logs. Follow [these](https://github.com/SeedSigner/seedsigner/blob/main/docs/usb_relay.md) instructions to set up a USB relay for internet access. You can also connect your SeedSigner to WiFi if you have a Raspberry Pi Zero W with WiFi.
### Airgapped debugging setup
### Airgapped Debugging Setup
If you are using SeedSigner for mainnet transactions, then do not connect your device to a network or the internet. Instead connect your SeedSigner to a HDMI display (without internet) and a USB keyboard. This will require an HDMI adapter and micro USB to USB A adapter. Plug in the HDMI display and keyboard before powering on SeedSigner. The password for the SeedSigner pi user is `raspberry`.
If you are using SeedSigner for mainnet transactions, then do not connect your device to a network or the internet. Instead, connect your SeedSigner to an HDMI display (without internet) and a USB keyboard. This will require an HDMI adapter and a micro USB to USB A adapter. Plug in the HDMI display and keyboard before powering on SeedSigner. The password for the SeedSigner pi user is `raspberry`.
### Debugging steps
### Debugging Steps
At this point you should be signed into the pi user either on a HDMI display (via command line) or a ssh connection.
At this point, you should be signed into the pi user either on an HDMI display (via command line) or an SSH connection.
Follow these steps to setup a debug session.
Follow these steps to set up a debug session:
`cd seedsigner/src`
1. Navigate to the source directory:
```bash
cd seedsigner/src
```
`nano settings.ini`
2. Edit the settings file:
```bash
nano settings.ini
```
in nano editor change `debug = False` to `debug = True` (case sensitive). Save and exit settings.ini.
3. In the nano editor, change `debug = False` to `debug = True` (case sensitive). Save and exit settings.ini.
stop the seedsigner systemd process by running
4. Stop the SeedSigner systemd process:
```bash
sudo systemctl stop seedsigner.service
```
`sudo systemctl stop seedsigner.service`
now start the python app manually by running
`python3 main.py`
5. Start the Python app manually:
```bash
python3 main.py
```
SeedSigner should now be up and running. Keep it connected to the display and keyboard. Recreate the steps to cause the crash. The traceback log and exception will be displayed on the HDMI display.
+20 -15
View File
@@ -3,12 +3,16 @@
### Quickly generate a new seed to test with
Generate a new 12- or 24-word seed via [https://iancoleman.io/bip39/](https://iancoleman.io/bip39/).
Access a `python3` environment that has the `embit` library installed (e.g. your own local machine, ssh into the SeedSigner, etc)
Access a `python3` environment that has the `embit` library installed (e.g., your own local machine, SSH into the SeedSigner, etc.).
Start a python REPL session by just typing: `python3`
Paste in the following but insert your newly generated mnemonic:
Start a `python3` REPL session by typing:
```bash
python3
```
Paste in the following code (replace the seed phrase with your newly generated mnemonic):
```python
from embit import bip39
seed_phrase = "smoke chimney announce candy glory tongue refuse fatigue cricket once consider beef treat urge wing deny gym robot tobacco adult problem priority wheat diagram"
data = ""
@@ -19,7 +23,7 @@ for word in seed_phrase.split(" "):
print(data)
```
For the seed in the snippet, you should see:
For the example seed phrase above, you should see:
```
163803200074026607961827144306700411123603780160185419152013046908321497181700301371136719990487
```
@@ -28,20 +32,20 @@ Take the output and paste it into a [QR code generator](https://www.the-qrcode-g
Start up SeedSigner's UI to import a seed from a QR code. Scan the new QR code and you're good to go!
# Advanced developer notes
## Backup an SD card
You can back up and restore any size SD card but the process is a little inefficient so the bigger the source SD card, the bigger the backup will be and the longer it'll take to create (and even longer to image back to a new SD card), even if most of the card is blank.
You can back up and restore any size SD card, but the process is a little inefficient. The bigger the source SD card, the bigger the backup will be and the longer it'll take to create (and even longer to image back to a new SD card), even if most of the card is blank.
You can restore a backup image to an SD card of the same or larger size. So it's strongly recommended to do repetitive development work on a smaller card that's easier to backup and restore. Once the image is stabilized, then write it to a bigger card, if necessary (that being said, there's really no reason to use a large SD card for SeedSigner. An 8GB SD card is more than big enough).
You can restore a backup image to an SD card of the same or larger size. It's strongly recommended to do repetitive development work on a smaller card that's easier to backup and restore. Once the image is stabilized, then write it to a bigger card, if necessary (that being said, there's really no reason to use a large SD card for SeedSigner. An 8GB SD card is more than big enough).
Insert the SD card into a Mac/Linux machine and create a compressed img file.
### Steps to create a backup:
First verify the name of your SD card:
1. Insert the SD card into a Mac/Linux machine
2. Verify the name of your SD card:
```
```bash
# Mac:
diskutil list
@@ -49,14 +53,15 @@ diskutil list
sudo fdisk -l
```
It will most likely be `/dev/disk1` on most systems.
The device will most likely be `/dev/disk1` on most systems.
Now we use `dd` to clone and `gzip` to compress it. Note that we reference the SD card by adding an `r` in front of the disk name. This speeds up the cloning considerably.
3. Create a compressed backup using `dd` and `gzip`. Note that we reference the SD card by adding an `r` in front of the disk name to speed up the cloning process:
```
```bash
sudo dd if=/dev/rdisk1 conv=sparse bs=4m | gzip -9 > seedsigner.img.gz
```
The process should take about 15 minutes and will typically generate a roughly 1.1GB image.
To restore from your backup image, just use the Raspberry Pi Imager. Remember that you can only write it to an SD card of equal or greater size than the original SD card.
### Restoring from backup
To restore from your backup image, use the Raspberry Pi Imager. Remember that you can only write it to an SD card of equal or greater size than the original SD card.
+9 -10
View File
@@ -7,20 +7,19 @@ As usual: Don't Trust, Verify!<br>
<br><br>
**Note:**<br>
**Do NOT use this with any seed you want to use later with real funds. This exercise is only for checking that the independent codebases get to the same end result!**<br>
**However, if you do want to check your real seedphrases you should download the Iancoleman and/or Bitcoiner.Guide tools onto an airgapped, ephemeral computer (e.g. using tails-OS) and perform these tests on there. Destroy/abandon the TailsOS afterwards.**<br>
**However, if you do want to check your real seed phrases you should download the Ian Coleman and/or Bitcoiner.Guide tools onto an airgapped, ephemeral computer (e.g. using Tails-OS) and perform these tests on there. Destroy/abandon the Tails-OS afterwards.**<br>
**Never input seed phrases that you intend to use to store real funds onto an internet-connected computer!!!**
<br><br><br>
## 99 Dice Rolls / 24 Seed Words Example
The following 99 dice roll results are used in the verification steps as an example for a 24 words seed:<br>
The following 99 dice roll results are used in the verification steps as an example for a 24-word seed:<br>
> 655152231316521321611331544441236164664431121534415633526456254462245546236542364246312613322234612
The corresponding 24 seed words are:<br>
> eyebrow obvious such suggest poet seven breeze blame virtual frown dynamic donor harsh pigeon express broccoli easy apology scatter force recipe shadow claim radio
(Scroll down near the end to see result values for a 50 dice rolls / 12 seed words example)
<br><br><br>
## Creating seed via Dice rolls in SeedSigner (here v0.6.0)
@@ -56,7 +55,7 @@ Keep the SeedSigner open and the newly created seed still loaded as we will need
## Create new wallet from seed in Sparrow Wallet to see xpub/zpub and addresses
Go to https://www.sparrowwallet.com/download/ and download the release version supported by your operating system.<br><br>
Open Sparrow Wallet, go to 'File' menu and select 'New Wallet'. Enter a name (e.g. test), and click 'Create Wallet'.<br><br>
Open Sparrow Wallet, go to the 'File' menu and select 'New Wallet'. Enter a name (e.g. test), and click 'Create Wallet'.<br><br>
Click 'Airgapped Hardware Wallet' (1) and click on the 'Scan' button in the SeedSigner entry (2) which will open the camera scan screen:<br>
<kbd><img src="img/dicedoc/sparrow_wallet_1.png"></kbd>
@@ -84,7 +83,7 @@ Go to https://iancoleman.io/bip39 and check 'Show entropy details' (1):<br>
<br>
Make sure to check (1) 'Hex' and (2) '24 Words' as 'Mnemonic Length'.<br>
(Do not use 'dice' format because dice 6 will be replaced by 0).<br>
Then enter the 99 dices numbers in (3). The corresponding seed words are shown in (4):<br>
Then enter the 99 dice numbers in (3). The corresponding seed words are shown in (4):<br>
<kbd><img src="img/dicedoc/coleman_verify.png"></kbd>
<br><br>
The 24 seed words are the same in SeedSigner and the Ian Coleman tool.
@@ -144,7 +143,7 @@ Compare to zpub in Sparrow:<br>
<kbd><img src="img/dicedoc/sparrow_zpub.png"></kbd>
<br>
Zpub is the same as shown in SeedSigner, Sparrow and Ian Colemand tool.
Zpub is the same as shown in SeedSigner, Sparrow and Ian Coleman tool.
<br><br>
**Addresses:**<br>
Scroll down a little bit where the receive addresses are shown and compare to the ones generated in Sparrow ('Addresses' tab of the wallet):<br>
@@ -163,10 +162,10 @@ Check that the change addresses all match.
## 50 Dice Rolls / 12 Seed Words Example
SeedSigner supports the creation of mnenomic seeds both with 12 or 24 seed words corresponding to 50 or 99 dice rolls. Below are some example result values for using 50 dice rolls only.<br><br>
All the steps shown can be executed the same way, just select the '12 words (50 rolls)' option in SeedSigner and change the 'Mnenomic Length' dropdown boxes in both web tools to '12 Words'.<br><br>
SeedSigner supports the creation of mnemonic seeds both with 12 or 24 seed words corresponding to 50 or 99 dice rolls. Below are some example result values for using 50 dice rolls only.<br><br>
All the steps shown can be executed the same way, just select the '12 words (50 rolls)' option in SeedSigner and change the 'Mnemonic Length' dropdown boxes in both web tools to '12 Words'.<br><br>
50 dice roll results as an example for a 12 words seed:<br>
50 dice roll results as an example for a 12-word seed:<br>
> 65515223131652132161133154444123616466443112153441
The corresponding 12 seed words are:<br>
@@ -246,7 +245,7 @@ python3 mnemonic.py -h
python3 mnemonic.py coins 1111100111...
# 256 coin flips / 24-word mnemonic
python mnemonic.py coins 0010111010...
python3 mnemonic.py coins 0010111010...
# GENERATE 50 random dice rolls / 12-word mnemonic
python3 mnemonic.py dice rand12
+4 -4
View File
@@ -1,8 +1,8 @@
# SeedSigner Electrum seed phrase support
# SeedSigner Electrum Seed Phrase Support
SeedSigner supports loading of [Electrum's Segwit seed phrases](https://electrum.readthedocs.io/en/latest/seedphrase.html#electrum-seed-version-system). This is considered an Advanced feature that is disabled by default.
SeedSigner supports loading of [Electrum's SegWit seed phrases](https://electrum.readthedocs.io/en/latest/seedphrase.html#electrum-seed-version-system). This is considered an Advanced feature that is disabled by default.
To load an Electrum Segwit seed phrase, first enable Electrum seed support in Settings -> Advanced -> Electrum seed support. After this option is enabled, the user will now be able to enter an Electrum seed phrase by selecting "Enter Electrum seed" in the Load Seed screen.
To load an Electrum SegWit seed phrase, first enable Electrum seed support in Settings -> Advanced -> Electrum seed support. After this option is enabled, the user will now be able to enter an Electrum seed phrase by selecting "Enter Electrum seed" in the Load Seed screen.
Some SeedSigner functionality is deliberately disabled when using an Electrum mnemonic:
@@ -11,5 +11,5 @@ Some SeedSigner functionality is deliberately disabled when using an Electrum mn
- SeedQR backups
- Since Electrum seeds are not supported by other SeedQR implementations, it would be dangerous to use SeedQR as a backup tool for Electrum seeds and is thus disabled
- Custom derivations
- Hard coded derivation path and script types in SeedSigner to match Electrum wallet software. These are m/0h for single sig and m/1h for multisig
- Hard-coded derivation path and script types in SeedSigner to match Electrum wallet software. These are m/0h for single sig and m/1h for multisig
- User-chosen custom derivations are thus not supported for Electrum seeds
+12 -12
View File
@@ -6,28 +6,28 @@ Current focus: v0.5.0 preview releases.
## v0.5.0 Pre-Release 1.x
* Scan SeedQR / CompactSeedQR
* Scan SeedQR/CompactSeedQR
* Add/Edit passphrase
* View seed words w/configurable warnings
* Export xpub w/configurable warnings and flow determined by Settings
* View seed words with configurable warnings
* Export xpub with configurable warnings and flow determined by Settings
* Scan PSBT
* Full PSBT review screens
* "Full Spend" (no change) warning
* Fully verify PSBT change addrs
* Fully verify PSBT change addresses
* Send signed PSBT via QR
* QR display dimming/brightness UP/DOWN
* Subset of configurable Settings; persistent Settings storage
* SettingsQR integration proof-of-concept
Screens will be functional but not necessarily in their final presentation state (icons, text, positioning, etc).
Screens will be functional but not necessarily in their final presentation state (icons, text, positioning, etc.).
## v0.5.0 Pre-Release 2.x
* Existing screen refinement (visual presentation, text, etc)
* Existing screen refinement (visual presentation, text, etc.)
* Create new seed via image entropy
* Manual mnemonic seed word entry
* 12th/24th word calc
* SeedQR/CompactSeedQR manual transcription UI w/configurable UI style (dots vs grid)
* 12th/24th word calculation
* SeedQR/CompactSeedQR manual transcription UI with configurable UI style (dots vs grid)
* Single sig address scan and verification
* SettingsQR standalone UI refinement
* Fix broken tests
@@ -52,16 +52,16 @@ All of the above!
## Beyond v0.5.0
These features will not be included in the initial v0.5.0 release and will have varying degrees of priority for subsequent releases (or possibly not at all).
* Multisig wallet descriptor QR scan(?) and addr verification(?)
* Sign taproot txs
* Multisig wallet descriptor QR scan(?) and address verification(?)
* Sign taproot transactions
* Multi-language support (Transifex free for open source projects)
* Multisig: sign PSBT with multiple keys at once.
* Multisig: sign PSBT with multiple keys at once
* Custom OS, possibly with swappable SD card PSBT and multisig wallet descriptor storage
* Decoy game mode at launch (Snake, Tetris, Sudoku...?)
* BIP-39 wordlists in additional languages
* Address message signing
* UI color scheme customization
* Specify missing entropy for 12th/24th word calc
* Specify missing entropy for 12th/24th word calculation
# v0.6 and Beyond...?
+6 -6
View File
@@ -5,18 +5,18 @@ Older Raspberry Pi devices have a smaller GPIO header, 26 pin as opposed to the
## Hardware Changes
A suggested remapping can be found here:
![Remapped Pins Shematic](./img/legacy_hardware_remapped_pins.jpg)
![Remapped Pins Schematic](./img/legacy_hardware_remapped_pins.jpg)
This remapping can be done by soldering wires on to the Waveshare hat as below:
![Remapped Pins Photo](./img/legacy_hardware_remapped_pins_photo.jpg)
Alternatively, you could do this by connecting the LCD hat on with individual breadboard jumper wires.
Alternatively, you could do this by connecting the LCD hat with individual breadboard jumper wires.
Once you have re-mapped the pins, it is advised to do an IO Test to ensure that everything works.
Once you have re-mapped the pins, it is advised to do an I/O Test to ensure that everything works.
**Warning: Some of the GPIO pins on contain 5 volt output. Raspberry Pi GPIO pins are NOT 5V tolerant, meaning that if you accidentally connect a 5V supply pin to a GPIO input pin, you risk permanent damage.**
**Warning: Some of the GPIO pins contain 5 volt output. Raspberry Pi GPIO pins are NOT 5V tolerant, meaning that if you accidentally connect a 5V supply pin to a GPIO input pin, you risk permanent damage.**
## Software Changes
The Seedsigner software will automatically detect which hardware revision you are using and if the older hardware is detected, will remap the software to match the above modifications to the Waveshare hat.
The SeedSigner software will automatically detect which hardware revision you are using and if the older hardware is detected, will remap the software to match the above modifications to the Waveshare hat.
If you are using a pre-built Seedsigner image that hasn't yet has this incorporated, you can simply take the file "buttons.py" (/src/seedsigner/hardware/ int his repository) and overwrite same file on your Seedsigner SD card. (The easiest way to do this is to copy it on to the /boot/ partitition of the SD card then copy it over via the command line while connected to your Pi via monitor+keyboard) O
If you are using a pre-built SeedSigner image that hasn't yet had this incorporated, you can simply take the file "buttons.py" (/src/seedsigner/hardware/ in this repository) and overwrite the same file on your SeedSigner SD card. (The easiest way to do this is to copy it to the /boot/ partition of the SD card then copy it over via the command line while connected to your Pi via monitor+keyboard.)
+8 -8
View File
@@ -2,7 +2,7 @@
### Scanning QR Codes
SeedSigner supports scanning the following QR formats
SeedSigner supports scanning the following QR formats:
Animated QR Formats:
- PSBT
@@ -15,22 +15,22 @@ Static QR Formats:
- Base64 (if the PSBT byte size is too large, SS may have trouble scanning)
- Seed
- [SeedSigner SeedQR](seed_qr/README.md) format
- A 48 or 96 length string of numbers representing a bip39 wordlist (all wordlist languages supported). The numeric sequence is a concatenation of four-digit, zero-padded segments. Each four-digit segment represents a bip39 word expressed by a zero-indexed position in the wordlist. For example "0000" is abandon in the english bip39 wordlist.
- A 48 or 96 length string of numbers representing a BIP-39 wordlist (all wordlist languages supported). The numeric sequence is a concatenation of four-digit, zero-padded segments. Each four-digit segment represents a BIP-39 word expressed by a zero-indexed position in the wordlist. For example, "0000" is "abandon" in the English BIP-39 wordlist.
- [SeedSigner CompactSeedQR](seed_qr/README.md) format
- The 128- or 256-bit entropy encoded as a binary QR.
- English Bip39 Mnemonic words separated by a space. Currently only supports 12 and 24 word seeds.
- English Bip39 Mnemonic with only first 4 letters seperated by a space. Currently only supports 12 and 24 word seeds.
- The 128- or 256-bit entropy encoded as a binary QR
- English BIP-39 Mnemonic words separated by a space (currently only supports 12 and 24 word seeds)
- English BIP-39 Mnemonic with only first 4 letters separated by a space (currently only supports 12 and 24 word seeds)
### Displaying QR Codes
SeedSigner supports displaying QR's in the following formats
SeedSigner supports displaying QR codes in the following formats:
Animated QR Formats:
- PSBT
- Blockchain Commons [UR2 PSBT](https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-006-urtypes.md)
- Specter Desktop base64 segments animated
- Specter Desktop base64 segments animated
Static QR Formats:
- Seed
- SeedSigner Seed QR format
- A 48 or 96 length string of numbers representing a bip39 wordlist (all wordlist languages supported). The numeric sequence is a concatination of four-digit zero padded segments. Each four-digit segment represents a bip39 word expressed by a zero-indexed position in the wordlist. For example "0000" is abandon in the english bip39 wordlist.
- A 48 or 96 length string of numbers representing a BIP-39 wordlist (all wordlist languages supported). The numeric sequence is a concatenation of four-digit, zero-padded segments. Each four-digit segment represents a BIP-39 word expressed by a zero-indexed position in the wordlist. For example, "0000" is "abandon" in the English BIP-39 wordlist.
+29 -32
View File
@@ -1,23 +1,23 @@
# Raspberry Pi OS Local Dev Build Instructions
Since v0.6.0, official releases use our custom [SeedSigner OS](https://github.com/SeedSigner/seedsigner-os/) However, project contributors looking to do rapid development cycles typically use the older Raspberry Pi OS that we had previously built on prior to v0.6.0. If you're here to set up your SeedSigner for local development, continue reading.
Since v0.6.0, official releases use our custom [SeedSigner OS](https://github.com/SeedSigner/seedsigner-os/). However, project contributors looking to do rapid development cycles typically use the older Raspberry Pi OS that we had previously built on prior to v0.6.0. If you're here to set up your SeedSigner for local development, continue reading.
Begin by acquiring the latest 32-bit, Buster-based Raspberry Pi Lite operating system. This guide was tested using the version dated 2023-05-03; which can be found here:
Begin by acquiring the latest 32-bit, Buster-based Raspberry Pi Lite operating system. This guide was tested using the version dated 2023-05-03, which can be found here:
https://downloads.raspberrypi.org/raspios_oldstable_lite_armhf/images/raspios_oldstable_lite_armhf-2023-05-03/
SeedSigner does not work any of the more recent versions of Debian. This is a known limitation and there are open tickets to track the progress of this ([Debian 11 ticket](https://github.com/SeedSigner/seedsigner/issues/431), [Debian 12 ticket](https://github.com/SeedSigner/seedsigner/issues/430)). This guide does not work on the 64-bit versions of Buster, however pull requests to update it to be compatible are welcome.
SeedSigner does not work with any of the more recent versions of Debian. This is a known limitation and there are open tickets to track the progress of this ([Debian 11 ticket](https://github.com/SeedSigner/seedsigner/issues/431), [Debian 12 ticket](https://github.com/SeedSigner/seedsigner/issues/430)). This guide does not work on the 64-bit versions of Buster, however pull requests to update it to be compatible are welcome.
Best practice is to verify the downloaded file containing the Raspberry Pi Lite OS matches the published SHA256 hash of the file; for additional reference that hash is: 3d210e61b057de4de90eadb46e28837585a9b24247c221998f5bead04f88624c. After verifying the file's data integrity, you can decompress the .tar.xz file to obtain the operating system image that it contains. You can then use Balena's Etcher tool (https://www.balena.io/etcher/) to write the Raspberry Pi Lite software image to a memory card (4 GB or larger). It's important to note that an image authoring tool must be used (the operating system image cannot be simply copied into a file storage partition on the memory card).
The manual SeedSigner installation and configuration process requires an internet connection on the Pi to download the necessary libraries and code.
If your Pi does not have onboard wifi, you have two options:
If your Pi does not have onboard WiFi, you have two options:
1. Run these steps on a separate Raspberry Pi 2/3/4 or Zero W which does have onboard Wi-Fi to connect to the internet, and then move the SD card over to the non Wi-Fi enabled Pi when complete.
2. OR configure the non Wi-Fi enabled Pi directly by relaying through your computer's internet connection over USB. See instructions [here](usb_relay.md).
1. Run these steps on a separate Raspberry Pi 2/3/4 or Zero W which does have onboard WiFi to connect to the internet, and then move the SD card over to the non WiFi enabled Pi when complete.
2. OR configure the non WiFi enabled Pi directly by relaying through your computer's internet connection over USB. See instructions [here](usb_relay.md).
If your Pi does have onboard Wi-Fi, then using the Rasberry Pi Imager software will allow you to easily configure your Pi's Wi-Fi connection, as well as simultaneously write the image file. That will make your initial SSH into the Pi much easier.
Use the Pi's onboard Wi-Fi only if you are setting up a local development environment, never for real funds or binary image creation.
If your Pi does have onboard WiFi, then using the Raspberry Pi Imager software will allow you to easily configure your Pi's WiFi connection, as well as simultaneously write the image file. That will make your initial SSH into the Pi much easier.
Use the Pi's onboard WiFi only if you are setting up a local development environment, never for real funds or binary image creation.
For the following steps you'll need to either connect a keyboard & monitor to the network-connected Raspberry Pi you are working with, or SSH into the Pi if you're familiar with that process.
@@ -60,8 +60,7 @@ Set the following:
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.
Each command should be run individually,unless its specified as a multi-line command.
Each command should be run individually, unless it's specified as a multi-line command.
### Change the default password
Change the system's default password from the default "raspberry". Run the command:
```bash
@@ -69,7 +68,6 @@ 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 python3.10
```bash
# install compiler dependencies; takes ~1 minute on a Pi Zero 1.3
@@ -77,7 +75,7 @@ You will be prompted to enter the current password ("raspberry") and then to ent
# * 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
libreadline-dev libffi-dev wget libsqlite3-dev libraqm-dev
# Grab the python3.10 source
wget https://www.python.org/ftp/python/3.10.10/Python-3.10.10.tgz
@@ -106,7 +104,6 @@ sudo apt autoremove -y
sudo apt install python3-apt -y
```
### Install dependencies
Copy this entire box and run it as one command (~15 minutes on a Pi Zero 1.3):
```bash
@@ -189,7 +186,7 @@ The fork is required because the main `pyzbar` repo has been abandoned. This [gi
### Optional: increase spidev buffer size
This allows `ST7789.py` to update the LCD without performing multiple write operations because the default buffer size is 4096 bytes. The default can be changed via the `/boot/cmdline.txt` file. You will need to add `spidev.bufsiz=131072` to the end of this single lined file command.
This allows `ST7789.py` to update the LCD without performing multiple write operations because the default buffer size is 4096 bytes. The default can be changed via the `/boot/cmdline.txt` file. You will need to add `spidev.bufsiz=131072` to the end of this single lined file command.
Example `cmdline.txt` contents:
```
@@ -203,7 +200,7 @@ sudo nano /etc/systemd/system/seedsigner.service
```
Add the following contents to the text file that was created:
If you are not using the username pi, then replace `pi` in the service section below with your username. There are 3 lines to change.
If you are not using the username pi, then replace `pi` in the service section below with your username. There are 3 lines to change.
```ini
[Unit]
Description=Seedsigner
@@ -214,19 +211,19 @@ WorkingDirectory=/home/pi/seedsigner/src/
ExecStart=/usr/bin/python3 main.py
StandardOutput=null
ErrorOutput=null
Restart=always
Restart=no
[Install]
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: The line `Restart=no` ensures that when your dev code crashes it won't keep trying to restart itself._
_Note: Debugging output is completely wiped via routing the stdout and stderr to `/dev/null`. 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):
Configure the service to start running (this will restart the seedsigner code automatically at startup):
```bash
sudo systemctl enable seedsigner.service
```
@@ -313,9 +310,9 @@ First find your current `nameserver`:
sudo cat /etc/resolv.conf
```
This is the address of your local machine that is connected to your SeedSigner via usb (or it'll be the wifi router's address if you're using a Raspi with wifi and are keeping it enabled for `ssh` access).
This is the address of your local machine that is connected to your SeedSigner via USB (or it'll be the WiFi router's address if you're using a Raspberry Pi with WiFi and are keeping it enabled for `ssh` access).
Set a static ip: `sudo nano /etc/dhcpcd.conf` and add to the end:
Set a static IP: `sudo nano /etc/dhcpcd.conf` and add to the end:
```
interface usb0
static ip_address=192.168.1.200/24
@@ -323,16 +320,16 @@ static routers=192.168.1.254
static domain_name_servers=192.168.1.254
```
* `interface` will be `usb0` for usb connections; `wlan0` for wifi.
* `static ip_address` is the ip address you want the SeedSigner to use. It should match the `nameserver` ip you found above for all but the last part of the ip (note: the `/24` should always be included as-is).
* `static routers` should be your `nameserver` ip.
* `static domain_name_servers` should also be the `nameserver` ip.
* `interface` will be `usb0` for USB connections; `wlan0` for WiFi.
* `static ip_address` is the IP address you want the SeedSigner to use. It should match the `nameserver` IP you found above for all but the last part of the IP (note: the `/24` should always be included as-is).
* `static routers` should be your `nameserver` IP.
* `static domain_name_servers` should also be the `nameserver` IP.
`CTRL-X` and `y` to save changes.
After your next reboot, access this SeedSigner using its new static ip:
After your next reboot, access this SeedSigner using its new static IP:
```bash
# Use the static ip you set above:
# Use the static IP you set above:
ssh pi@192.168.1.200
# But the hostname will still work, too:
@@ -350,7 +347,7 @@ host seedsigner.local
User pi
LogLevel QUIET
# Set this to the static ip you set above:
# Set this to the static IP you set above:
host 192.168.1.200
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
@@ -360,7 +357,7 @@ host 192.168.1.200
The first entry prevents warnings for the default `pi@seedsigner.local` connections.
The second entry does the same for a specific static ip; you'll want this if you configure all your SeedSigners to use the same static ip.
The second entry does the same for a specific static IP; you'll want this if you configure all your SeedSigners to use the same static IP.
`CTRL-X` and `y` to save changes.
@@ -372,7 +369,7 @@ 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:
# or if you're connecting over static IP, something like:
ssh-copy-id pi@192.168.1.200
```
@@ -381,8 +378,8 @@ You'll be prompted to enter the password to complete it.
_Note: If you don't have any ssh keys on your local machine, you'll need to create a set with `ssh-keygen -t ed25519 -C "your_email@example.com"`. Then try running `ssh-copy-id` again._
## 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:
## Disable WiFi/Bluetooth when using other Raspberry Pi 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 following commands to disable WiFi, Bluetooth, & other relevant services:
```bash
sudo systemctl disable bluetooth.service
sudo systemctl disable wpa_supplicant.service
@@ -409,4 +406,4 @@ Please remember that it can take up to a minute for the GUI to appear when power
### Optional: Run the tests
see: [tests/README.md](../tests/README.md)
see: [tests/README.md](../tests/README.md)
+8 -6
View File
@@ -2,7 +2,7 @@
[SeedSigner](https://github.com/SeedSigner/seedsigner/) is an open source, DIY, fully-airgapped Bitcoin hardware wallet that wipes all private data from memory each time it's turned off. That means users need to re-enter their mnemonic seed phrase each time they use it.
To speed up this key entry process we have defined a way to encode a BIP-39 mnemonic seed phrase as a QR code that can be instantly scanned into a SeedSigner or potentially any other Bitcoin hardware wallet that has a camera.
To speed up this key entry process, we have defined a way to encode a BIP-39 mnemonic seed phrase as a QR code that can be instantly scanned into a SeedSigner or potentially any other Bitcoin hardware wallet that has a camera.
The approach is specifically designed to encode the minimum possible amount of data in order to keep the resulting QR code small enough that it can be transcribed *by hand*. This sounds ridiculous at first, but remember that this is secret data that should never be stored in any digital medium. And even printers present some additional risk vectors.
@@ -22,11 +22,13 @@ Specifications for each follow below, as well as discussion of the pros and cons
## Quick Review of BIP-39 Mnemonic Seed Phrases
The typical method for backing up a Bitcoin wallet is to store its [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) mnemonic seed phrase consisting of 12 or 24 words.
**SeedQR specifically assumes and recommends using the English BIP39 wordlist.** While BIP39 supports multiple language-specific wordlists (e.g., Japanese, Spanish, French), there is no mechanism to specify or detect which language wordlist was used when creating or scanning a SeedQR. Using a non-English wordlist may result in incorrect or invalid seeds.
Each word comes from a [list of 2048 words](https://github.com/bitcoin/bips/blob/master/bip-0039/english.txt). The words themselves are meaningless; all that matters is the word's position number (aka index) in the word list.
For example, "tomato" is the 1,825th word in the list.
But code always starts counting list items with zero. So the index of "tomato" is actually `1824` (if you're looking at the github wordlist line numbers, just remember to always subtract one).
But code always starts counting list items with zero. So the index of "tomato" is actually `1824` (if you're looking at the GitHub wordlist line numbers, just remember to always subtract one).
So we can transcode a 12-word seed into a series of indices:
@@ -264,7 +266,7 @@ But there are other tradeoffs to consider.
## Recoverability
If you lose your SeedSigner or somehow the project is abandoned or banned, how will you read back your SeedQR?
With the Standard SeedQR format this is trivial--any smartphone can decode the numeric digit stream. But the CompactSeedQR's raw byte data is not decipherable in the same way. Most QR readers today assume the data is either alphanumeric or human-readable numeric data. Because of this assumption, they misinterpret the binary format data:
With the Standard SeedQR format, this is trivial--any smartphone can decode the numeric digit stream. But the CompactSeedQR's raw byte data is not decipherable in the same way. Most QR readers today assume the data is either alphanumeric or human-readable numeric data. Because of this assumption, they misinterpret the binary format data:
<table align="center">
<tr>
@@ -283,7 +285,7 @@ It's just the above process in reverse:
# Separate out into 4-digit individual indices
1924 0222 0235 1743 0631 1124 0378 1770 0641 1980 1290 1210
# Look up each BIP-39 index number (index+1 if using the github list!)
# Look up each BIP-39 index number (index+1 if using the GitHub list!)
1. vacuum 1924
2. bridge 222
3. buddy 235
@@ -298,7 +300,7 @@ It's just the above process in reverse:
12. nuclear 1210
```
It would be much more difficult to manually recreate your seed from a CompactSeedQR. Tools like [zxing.org](https://zxing.org/w/decode.jspx) or [ZBar](https://zbar.sourceforge.net/) can help you get the binary data out as a hexidecimal string:
It would be much more difficult to manually recreate your seed from a CompactSeedQR. Tools like [zxing.org](https://zxing.org/w/decode.jspx) or [ZBar](https://zbar.sourceforge.net/) can help you get the binary data out as a hexadecimal string:
<img src="img/zxing_screenshot.png">
@@ -313,7 +315,7 @@ Conversely, having limited support for reading binary QR codes and the complicat
# Some Additional Notes on QR Codes
Our main use case is to be able to quickly initialize a SeedSigner with your mnemonic seed phrase. But using a QR code as your key loader--or even as your permanent backup etched in metal--has other advantages.
QR codes are ubiquitous now so plenty of hardware and software exists to read and generate them.
QR codes are ubiquitous now, so plenty of hardware and software exists to read and generate them.
QR codes have built-in error correction. The "L" error correction mode is described as having a roughly 7% correction rate.
+30 -32
View File
@@ -1,6 +1,6 @@
## Relaying internet access to the Pi Zero 1.3 over USB
Note that this is an optional, alternate way to initialize your SeedSigner. The default method is to work on a separate Raspberry Pi device that has internet access. Be aware that by enabling internet access over USB you are obviously creating a link to the outside world that this project seeks to avoid.
Note that this is an optional, alternate way to initialize your SeedSigner. The default method is to work on a separate Raspberry Pi device that has internet access. Be aware that by enabling internet access over USB, you are creating a link to the outside world that this project seeks to avoid.
If you use this setup route, we recommend that you disable internet access over USB when these steps are complete.
@@ -8,17 +8,17 @@ If you use this setup route, we recommend that you disable internet access over
### Get started
Insert the SD card with your Raspberry Pi OS image into a regular computer. Open a terminal window (macOS: Terminal; Windows: Command Prompt) and navigate to the SD card:
```
# mac/Linux:
```bash
# macOS/Linux:
cd /Volumes/boot
# Windows (alter with correct drive letter):
cd e:
```
We need to create an empty file called "ssh" to enable us to remotely terminal into the Pi via SSH.
```
# mac/Linux:
We need to create an empty file called `ssh` to enable remote terminal access to the Pi via SSH:
```bash
# macOS/Linux:
touch ssh
# Windows:
@@ -29,8 +29,8 @@ type nul > ssh
Now we have some incomprehensible configuration steps to set up the internet access relay.
Edit `config.txt`:
```
# mac/Linux:
```bash
# macOS/Linux:
nano config.txt
# Windows:
@@ -39,17 +39,15 @@ notepad config.txt
Add `dtoverlay=dwc2` to the end of `config.txt`. Exit and save changes (CTRL-X, then "y" in nano).
Alternatively, add to `config.txt` via the command line:
```
# mac/Linux/Windows:
```bash
# macOS/Linux/Windows:
echo dtoverlay=dwc2 >> config.txt
```
Next, edit `cmdline.txt`:
```
# mac/Linux:
```bash
# macOS/Linux:
nano cmdline.txt
# Windows:
@@ -68,7 +66,7 @@ If you are on Linux, you need to first [set a static IP](#set-a-static-ip-on-lin
Eject the SD card and insert it into your Pi Zero 1.3. Plug a USB cable into your computer and into the Pi's USB connector that is closer to the center. It will draw power from the USB cable and begin powering up.
The Pi will take a minute or so to boot up its OS. After waiting a bit, try to communicate with the Pi over SSH:
```
```bash
# Manual builds:
ssh pi@raspberrypi.local
@@ -83,7 +81,7 @@ ECDSA key fingerprint is SHA256:go4yVgii1GcvyxzhOe03atLn5bl2NhZlOR04tJHBo+k.
Are you sure you want to continue connecting (yes/no/[fingerprint])?
```
At the password prompt enter the Pi's default password: `raspberry`
At the password prompt, enter the Pi's default password: `raspberry`
If you now see the Pi's command prompt, you're in!
@@ -96,7 +94,7 @@ This is a security risk - please login as the 'pi' user and type 'passwd' to set
```
If someone savvy got access to your SeedSigner, they could sign into it and potentially upload malicious code. To add an extra layer of protection, change the default 'pi' user's password now by typing `passwd`:
```
```bash
pi@raspberrypi:~ $ passwd
Changing password for pi.
Current password:
@@ -105,7 +103,6 @@ Retype new password:
passwd: password updated successfully
```
## Configure host computer to share internet access with the Pi
### macOS
@@ -126,7 +123,7 @@ Now go to the "Sharing" system settings. Click on "Internet Sharing" and check t
Click the "Internet Sharing" checkbox to activate.
Back at your SSH terminal, test the connection:
```
```bash
ping 8.8.8.8
```
@@ -143,7 +140,7 @@ see: https://www.circuitbasics.com/raspberry-pi-zero-ethernet-gadget/
### Linux
To enable IP forwarding on your Host:
```
```bash
nano /etc/sysctl.conf
```
@@ -171,11 +168,12 @@ What you will need:
_Note: Remember to remove the brackets. For example, `ip_address=<rpi_ip>` should be written as: `ip_address=192.168.21.21`_
#### On the SD card
```
```bash
cd rootfs/etc
nano dhcpcd.conf
```
At the end of the file, add the following lines.
At the end of the file, add the following lines:
```
interface usb0
static ip_address=<rpi_ip>/24
@@ -192,7 +190,7 @@ Type `lsusb` to see if your Pi Zero is properly connected. Look for `Ethernet/RN
![Static IP on the seedsigner Interface](img/usb_relay_linux_01.png)
Set a name for your new Interface:
```
```bash
nano /etc/systemd/network/10-rename-rpi0.link
```
@@ -204,8 +202,8 @@ Property=ID_VENDOR_ID=0525 "ID_MODEL_ID=a4a2"
Name=seedsigner0
```
To give your Host a static ip:
```
To give your Host a static IP:
```bash
nano /etc/network/interfaces.d/seedsigner
```
@@ -235,8 +233,8 @@ Return to the main [README](../README.md) and complete the setup steps. But reme
Power down the SeedSigner and remove the SD card. Put the SD card back into your computer and use a terminal to navigate to the `boot` drive (same process as above).
Edit `config.txt`:
```
# mac/Linux:
```bash
# macOS/Linux:
nano config.txt
# Windows:
@@ -247,8 +245,8 @@ Delete the `dtoverlay=dwc2` line that we added to the end of the file. Exit and
Edit `cmdline.txt`:
```
# mac/Linux:
```bash
# macOS/Linux:
nano cmdline.txt
# Windows:
@@ -262,12 +260,12 @@ Eject the SD card and put it back in the SeedSigner. Connect the SeedSigner to y
SSH into the pi and try to `ping 8.8.8.8`. It should fail with no responses.
For full security, put the SD card back in your computer one last time and delete the `ssh` file from `boot`:
```
# mac/Linux:
```bash
# macOS/Linux:
rm ssh
# Windows:
del ssh
```
You're now good to go with a SeedSigner that is back to being fully air-gapped!
You're now good to go with a SeedSigner that is back to being fully air-gapped!
+1 -1
View File
@@ -13,7 +13,7 @@ description = "Build an offline, airgapped Bitcoin signing device for less than
name = "seedsigner"
readme = "README.md"
requires-python = ">=3.10"
version = "0.8.5"
version = "0.8.6"
[project.urls]
"Bug Tracker" = "https://github.com/SeedSigner/seedsigner/issues"
+1 -1
View File
@@ -2,4 +2,4 @@ embit==0.8.0
Pillow==10.0.1
pyzbar @ git+https://github.com/seedsigner/pyzbar.git@c3c237821c6a20b17953efe59b90df0b514a1c03
qrcode==7.3.1
urtypes @ git+https://github.com/selfcustody/urtypes.git@7fb280eab3b3563dfc57d2733b0bf5cbc0a96a6a
urtypes==1.0.1
+1 -1
View File
@@ -99,7 +99,7 @@ class Controller(Singleton):
rather than at the top in order avoid circular imports.
"""
VERSION = "0.8.5"
VERSION = "0.8.6"
# Declare class member vars with type hints to enable richer IDE support throughout
# the code.
+1 -16
View File
@@ -56,6 +56,7 @@ class GUIConstants:
SettingsConstants.LOCALE__JAPANESE: "NotoSansJP-Regular",
SettingsConstants.LOCALE__KOREAN: "NotoSansKR-Regular",
SettingsConstants.LOCALE__PERSIAN: "NotoSansAR-Regular", # Uses the Arabic font
SettingsConstants.LOCALE__THAI: "NotoSansTH-Regular",
}
TOP_NAV_TITLE_FONT_NAME = BASE_LOCALE_FONTS.copy()
@@ -434,10 +435,6 @@ class TextArea(BaseComponent):
self.text_y = self.text_height_above_baseline
self.visible_width = self.width - max(self.edge_padding, self.min_text_x) - self.edge_padding
if not ImageFont.core.HAVE_RAQM:
# Fudge factor for imprecise width calcs w/out libraqm
full_text_width = int(full_text_width * 1.05)
self.visible_width = int(self.visible_width * 1.05)
if self.is_horizontal_scrolling_enabled or not self.auto_line_break:
# Guaranteed to be a single line of text, possibly wider than self.width
@@ -591,10 +588,6 @@ class TextArea(BaseComponent):
self.rendered_text_img = img
if not ImageFont.core.HAVE_RAQM:
# At this point we need the visible_width to be the "actual" (yet still incorrect) width
self.visible_width = int(self.visible_width * 0.95)
self.horizontal_text_scroll_thread: TextArea.HorizontalTextScrollThread = None
if self.is_horizontal_scrolling_enabled:
self.horizontal_text_scroll_thread = TextArea.HorizontalTextScrollThread(
@@ -1845,10 +1838,6 @@ def reflow_text_for_width(text: str,
# Measure from left baseline ("ls")
(left, top, full_text_width, px_below_baseline) = font.getbbox(text, anchor="ls")
if not ImageFont.core.HAVE_RAQM:
# Fudge factor for imprecise width calcs w/out libraqm
full_text_width = int(full_text_width * 1.05)
# Assume we can break Asian text on any character
treat_chars_as_words = Settings.get_instance().get_value(SettingsConstants.SETTING__LOCALE) in [
SettingsConstants.LOCALE__CHINESE_SIMPLIFIED,
@@ -1882,10 +1871,6 @@ def reflow_text_for_width(text: str,
(left, top, right, px_below_baseline) = font.getbbox(word_spacer.join(words[0:index]), anchor="ls")
line_width = right - left
if not ImageFont.core.HAVE_RAQM:
# Fudge factor for imprecise width calcs w/out libraqm
line_width = int(line_width * 1.05)
if line_width >= width:
# Candidate line is still too long. Restrict search range down.
if min_index + 1 == index:
+2 -2
View File
@@ -31,7 +31,7 @@ class PSBTOverviewScreen(ButtonListScreen):
# Customize defaults
self.title = _("Review PSBT")
self.is_bottom_list = True
self.button_data = [ButtonOption("Review Details")]
self.button_data = [ButtonOption("Review details")]
# This screen can take a while to load while parsing the PSBT
self.show_loading_screen = True
@@ -479,7 +479,7 @@ class PSBTMathScreen(ButtonListScreen):
def __post_init__(self):
# Customize defaults
self.title = _("PSBT Math")
self.button_data = [ButtonOption("Review Recipients")]
self.button_data = [ButtonOption("Review recipients")]
self.is_bottom_list = True
super().__post_init__()
+1 -1
View File
@@ -284,5 +284,5 @@ class ScanScreen(BaseScreen):
if self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_RIGHT) or self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_LEFT):
self.camera.stop_video_stream_mode()
break
return False
+1 -1
View File
@@ -1032,7 +1032,7 @@ class WarningScreen(WarningEdgesMixin, LargeIconStatusScreen):
status_icon_name: str = SeedSignerIconConstants.WARNING
status_color: str = GUIConstants.WARNING_COLOR
status_headline: str = _mft("Privacy Leak!") # The colored text under the alert icon
button_data: list = field(default_factory=lambda: [ButtonOption("I Understand")])
button_data: list = field(default_factory=lambda: [ButtonOption("I understand")])
+5 -5
View File
@@ -31,7 +31,7 @@ class SeedMnemonicEntryScreen(BaseTopNavScreen):
self.possible_alphabet = "abcdefghijklmnopqrstuvwxyz"
# Measure the width required to display the longest word in the English bip39
# Measure the width required to display the longest word in the English BIP-39
# wordlist.
# TODO: If we ever support other wordlist languages, adjust accordingly.
matches_list_highlight_font_name = GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME
@@ -48,7 +48,7 @@ class SeedMnemonicEntryScreen(BaseTopNavScreen):
self.arrow_up_is_active = False
self.arrow_down_is_active = False
# TODO: support other BIP39 languages/charsets
# TODO: support other BIP-39 languages/charsets
self.keyboard = Keyboard(
draw=self.image_draw,
charset=self.possible_alphabet,
@@ -614,7 +614,7 @@ class SeedExportXpubDetailsScreen(WarningEdgesMixin, ButtonListScreen):
def __post_init__(self):
# Programmatically set up other args
self.button_data = [ButtonOption("Export Xpub")]
self.button_data = [ButtonOption("Export xpub")]
self.title = _("Xpub Details")
# Initialize the base class
@@ -624,7 +624,7 @@ class SeedExportXpubDetailsScreen(WarningEdgesMixin, ButtonListScreen):
self.fingerprint_line = IconTextLine(
icon_name=SeedSignerIconConstants.FINGERPRINT,
icon_color=GUIConstants.INFO_COLOR,
# TRANSLATOR_NOTE: Short for "BIP32 Master Fingerprint"
# TRANSLATOR_NOTE: Short for "BIP-32 Master Fingerprint"
label_text=_("Fingerprint"),
value_text=self.fingerprint,
screen_x=GUIConstants.COMPONENT_PADDING,
@@ -1676,7 +1676,7 @@ class SeedSignMessageConfirmAddressScreen(ButtonListScreen):
self.title = _("Confirm Address")
self.is_bottom_list = True
self.is_button_text_centered = True
self.button_data = [ButtonOption("Sign Message")]
self.button_data = [ButtonOption("Sign message")]
super().__post_init__()
derivation_path_display = IconTextLine(
+10 -5
View File
@@ -267,13 +267,17 @@ class ToolsCalcFinalWordScreen(ButtonListScreen):
super().__post_init__()
# First what's the total bit display width and where do the checksum bits start?
bit_font_size = GUIConstants.get_button_font_size() + 2
bit_font_size = GUIConstants.get_button_font_size(locale="default") + 2 # bit font size should not vary by locale
font = Fonts.get_font(GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, bit_font_size)
(left, top, bit_display_width, bit_font_height) = font.getbbox("0" * 11, anchor="lt")
(left, top, checksum_x, bottom) = font.getbbox("0" * (11 - len(self.checksum_bits)), anchor="lt")
bit_display_x = int((self.canvas_width - bit_display_width)/2)
checksum_x += bit_display_x
y_spacer = GUIConstants.COMPONENT_PADDING
if GUIConstants.get_body_font_size() > GUIConstants.get_body_font_size("default"):
y_spacer -= 1
# Display the user's additional entropy input
if self.selected_final_word:
selection_text = self.selected_final_word
@@ -300,7 +304,7 @@ class ToolsCalcFinalWordScreen(ButtonListScreen):
))
# ...and that entropy's associated 11 bits
screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING
screen_y = self.components[-1].screen_y + self.components[-1].height + y_spacer
first_bits_line = TextArea(
text=keeper_selected_bits,
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
@@ -327,18 +331,19 @@ class ToolsCalcFinalWordScreen(ButtonListScreen):
is_text_centered=False,
))
# Show the checksum..
# Show the checksum...
self.components.append(TextArea(
# TRANSLATOR_NOTE: A function of "x" to be used for detecting errors in "x"
text=_("Checksum"),
edge_padding=0,
screen_y=first_bits_line.screen_y + first_bits_line.height + 2*GUIConstants.COMPONENT_PADDING,
height_ignores_below_baseline=True, # Keep the next line (bits display) snugged up, regardless of text rendering below the baseline
))
# ...and its actual bits. Prepend spacers to keep vertical alignment
checksum_spacer = "_" * (11 - len(self.checksum_bits))
screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING
screen_y = self.components[-1].screen_y + self.components[-1].height + y_spacer
# This time we de-emphasize the prepended spacers that are irrelevant
self.components.append(TextArea(
@@ -375,7 +380,7 @@ class ToolsCalcFinalWordScreen(ButtonListScreen):
# Once again show the bits that came from the user's entropy...
num_checksum_bits = len(self.checksum_bits)
user_component = self.selected_final_bits[:11 - num_checksum_bits]
screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING
screen_y = self.components[-1].screen_y + self.components[-1].height + y_spacer
self.components.append(TextArea(
text=user_component,
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
+1 -1
View File
@@ -44,7 +44,7 @@ def get_standard_derivation_path(network: str = SettingsConstants.MAINNET, walle
elif wallet_type == SettingsConstants.MULTISIG:
if script_type == SettingsConstants.LEGACY_P2PKH:
return f"m/45'" #BIP45
return f"m/45'" #BIP-45
elif script_type == SettingsConstants.NESTED_SEGWIT:
return f"m/48'/{network_path}/0'/1'"
elif script_type == SettingsConstants.NATIVE_SEGWIT:
+71 -8
View File
@@ -2,6 +2,7 @@ import base64
import json
import logging
import re
import zlib
from binascii import a2b_base64, b2a_base64
from enum import IntEnum
@@ -11,6 +12,7 @@ from pyzbar.pyzbar import ZBarSymbol
from urtypes.crypto import PSBT as UR_PSBT
from urtypes.crypto import Account, Output
from urtypes.bytes import Bytes
from base64 import b32encode, b32decode
from seedsigner.helpers.ur2.ur_decoder import URDecoder
from seedsigner.models.qr_type import QRType
@@ -74,6 +76,9 @@ class DecodeQR:
elif self.qr_type == QRType.PSBT__BASE43:
self.decoder = Base43PsbtQrDecoder() # Single Segment Base43
elif self.qr_type == QRType.PSBT__BBQR:
self.decoder = BBQRPsbtQrDecoder() # BBQr Decoder
elif self.qr_type in [QRType.SEED__SEEDQR, QRType.SEED__COMPACTSEEDQR, QRType.SEED__MNEMONIC, QRType.SEED__FOUR_LETTER_MNEMONIC, QRType.SEED__UR2]:
self.decoder = SeedQrDecoder(wordlist_language_code=self.wordlist_language_code)
@@ -229,7 +234,7 @@ class DecodeQR:
if self.qr_type in [QRType.PSBT__UR2, QRType.OUTPUT__UR, QRType.ACCOUNT__UR, QRType.BYTES__UR]:
return int(self.decoder.estimated_percent_complete(weight_mixed_frames=weight_mixed_frames) * 100)
elif self.qr_type in [QRType.PSBT__SPECTER]:
elif self.qr_type in [QRType.PSBT__SPECTER, QRType.PSBT__BBQR]:
if self.decoder.total_segments == None:
return 0
return int((self.decoder.collected_segments / self.decoder.total_segments) * 100)
@@ -262,6 +267,7 @@ class DecodeQR:
QRType.PSBT__SPECTER,
QRType.PSBT__BASE64,
QRType.PSBT__BASE43,
QRType.PSBT__BBQR,
]
@@ -326,9 +332,6 @@ class DecodeQR:
@staticmethod
def detect_segment_type(s, wordlist_language_code=None):
# print("-------------- DecodeQR.detect_segment_type --------------")
# print(type(s))
# print(len(s))
try:
# Convert to str data
@@ -338,6 +341,9 @@ class DecodeQR:
# TODO: Convert the test suite rather than handle here?
s = s.decode('utf-8')
logger.debug(f"segment string: {s}")
logger.debug(f"segment string length: {len(s)}")
# PSBT
if re.search("^UR:CRYPTO-PSBT/", s, re.IGNORECASE):
return QRType.PSBT__UR2
@@ -357,6 +363,9 @@ class DecodeQR:
elif DecodeQR.is_base64_psbt(s):
return QRType.PSBT__BASE64
elif re.search(r"^B\$[2HZ]P[0-9A-Z]{4}", s): # https://github.com/coinkite/BBQr/blob/master/BBQr.md#spliting-the-data
return QRType.PSBT__BBQR
# Wallet Descriptor
desc_str = s.replace("\n","").replace(" ","")
if re.search(r'^p(\d+)of(\d+) ', s, re.IGNORECASE):
@@ -398,11 +407,11 @@ class DecodeQR:
_4LETTER_WORDLIST = []
if all(x in wordlist for x in s.strip().split(" ")):
# checks if all words in list are in bip39 word list
# checks if all words in list are in BIP-39 word list
return QRType.SEED__MNEMONIC
elif all(x in _4LETTER_WORDLIST for x in s.strip().split(" ")):
# checks if all 4 letter words are in list are in 4 letter bip39 word list
# checks if all 4 letter words are in list are in 4 letter BIP-39 word list
return QRType.SEED__FOUR_LETTER_MNEMONIC
elif DecodeQR.is_base43_psbt(s):
@@ -661,8 +670,9 @@ class BaseAnimatedQrDecoder(BaseQrDecoder):
elif self.total_segments != self.total_segment_nums(segment):
raise Exception('Segment total changed unexpectedly')
if self.segments[self.current_segment_num(segment) - 1] == None:
self.segments[self.current_segment_num(segment) - 1] = self.parse_segment(segment)
current_segment_num = self.current_segment_num(segment)
if self.segments[current_segment_num - 1] == None:
self.segments[current_segment_num - 1] = self.parse_segment(segment)
self.collected_segments += 1
if self.total_segments == self.collected_segments:
if self.is_valid:
@@ -711,6 +721,59 @@ class SpecterPsbtQrDecoder(BaseAnimatedQrDecoder):
class BBQRPsbtQrDecoder(BaseAnimatedQrDecoder):
"""
Used to decode BBQR Animated PSBT encoding.
"""
def __init__(self):
super().__init__()
self.encoding = None
def get_data(self) -> str:
logger.debug("BBQRPsbtQrDecoder get_data")
data = "".join(self.segments)
if self.complete and self.encoding:
if self.encoding == 'H':
return b''.join(bytes.fromhex(s) for s in self.segments)
# base32 decode, but insert padding for API
rv = b''
for p in self.segments:
padding = (8 - (len(p) % 8)) % 8
rv += b32decode(p + (padding*'='))
if self.encoding == 'Z':
# decompress
z = zlib.decompressobj(wbits=-10)
rv = z.decompress(rv)
rv += z.flush()
return rv
return None
def current_segment_num(self, segment) -> int:
current_segment = int(segment[6:8], 36) + 1
logger.debug(f"BBQRPsbtQrDecoder current_segment_num {current_segment}")
return current_segment
def total_segment_nums(self, segment) -> int:
total_segments = int(segment[4:6], 36)
logger.debug(f"BBQRPsbtQrDecoder total_segment_nums {total_segments}")
return total_segments
def parse_segment(self, segment) -> str:
self.encoding = segment[2]
file_type = segment[3]
data = segment[8:]
return data.strip()
class Base64PsbtQrDecoder(BaseSingleFrameQrDecoder):
"""
Decodes single frame base64 encoded qr image.
+3
View File
@@ -344,6 +344,9 @@ class UrXpubQrEncoder(BaseFountainQrEncoder, BaseXpubQrEncoder):
return Keypath(arr, self.root.my_fingerprint, len(arr))
origin = derivation_to_keypath(self.derivation)
# Implemts "use_info" member on HDKey class (urtypes/crypto packages-libs folder) construct,
# so if working on TESTNET, Xpub can be exported accordingly. Default case, MAINNET: None value.
self.use_info = None if self.network == SettingsConstants.MAINNET else CoinInfo(type=None, network=1)
self.ur_hdkey = HDKey({ 'key': self.xpub.key.serialize(),
+1
View File
@@ -6,6 +6,7 @@ class QRType:
PSBT__SPECTER = "psbt__specter"
PSBT__BASE43 = "psbt__base43"
PSBT__UR2 = "psbt__ur2"
PSBT__BBQR = "psbt__bbqr"
SEED__SEEDQR = "seed__seedqr"
SEED__COMPACTSEEDQR = "seed__compactseedqr"
+4 -4
View File
@@ -124,12 +124,14 @@ class SettingsConstants:
LOCALE__ENGLISH: "English",
LOCALE__SPANISH: "Español",
LOCALE__FRENCH: "Français",
LOCALE__ITALIAN: "Italiano",
LOCALE__DUTCH: "Nederlands",
# --------- Beta languages ------------------------------------------------------
LOCALE__CHINESE_SIMPLIFIED: "(beta) 简体中文 (Chinese Simplified)",
LOCALE__JAPANESE: "(beta) 日本語 (Japanese)",
LOCALE__KOREAN: "(beta) 한국어 (Korean)",
LOCALE__THAI: "(beta) ไทย (Thai)",
# --------- Placeholders / Coming soon ------------------------------------------
# Commented out options require explicit additional font support.
@@ -149,7 +151,6 @@ class SettingsConstants:
# LOCALE__HEBREW: "עברית (Hebrew)",
# LOCALE__HINDI: "हिन्दी (Hindi)",
LOCALE__CROATIAN: "Hrvatski",
LOCALE__ITALIAN: "Italiano",
LOCALE__INDONESIAN: "Indonesia",
LOCALE__JAVANESE: "Jawa (Javanese)",
# LOCALE__LAO: "ລາວ (Lao)",
@@ -174,7 +175,6 @@ class SettingsConstants:
LOCALE__TAGALOG: "Tagalog",
# LOCALE__TAMIL: "தமிழ் (Tamil)",
# LOCALE__TELUGU: "తెలుగు (Telugu)",
# LOCALE__THAI: "ไทย (Thai)",
LOCALE__TURKISH: "Türkçe",
LOCALE__UKRANIAN: "українська (Ukranian)", # OpenSans includes cyrillic chars
# LOCALE__URDU: "اردو (Urdu)",
@@ -395,7 +395,7 @@ class SettingsConstants:
# Label strings
LABEL__BIP39_PASSPHRASE = _mft("BIP-39 Passphrase")
# TRANSLATOR_NOTE: Terminology used by Electrum seeds; equivalent to bip39 passphrase
# TRANSLATOR_NOTE: Terminology used by Electrum seeds; equivalent to BIP-39 passphrase
custom_extension = _mft("Custom Extension")
LABEL__CUSTOM_EXTENSION = custom_extension
@@ -542,7 +542,7 @@ class SettingsDefinition:
selection_options=SettingsConstants.get_detected_languages(),
default_value=SettingsConstants.LOCALE__ENGLISH),
# TODO: Support other bip-39 wordlist languages! Until then, type == HIDDEN
# TODO: Support other BIP-39 wordlist languages! Until then, type == HIDDEN
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
attr_name=SettingsConstants.SETTING__WORDLIST_LANGUAGE,
abbreviated_name="wordlist_lang",
+13 -10
View File
@@ -166,11 +166,12 @@ class PSBTOverviewView(View):
class PSBTUnsupportedScriptTypeWarningView(View):
def run(self):
selected_menu_num = WarningScreen(
selected_menu_num = self.run_screen(
WarningScreen,
status_headline=_("Unsupported Script Type!"),
text=_("PSBT has unsupported input script type, please verify your change addresses."),
button_data=[ButtonOption("Continue")],
).display()
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
@@ -186,12 +187,13 @@ class PSBTUnsupportedScriptTypeWarningView(View):
class PSBTNoChangeWarningView(View):
def run(self):
selected_menu_num = WarningScreen(
selected_menu_num = self.run_screen(
WarningScreen,
# TRANSLATOR_NOTE: User will receive no change back; the inputs to this transaction are fully spent
status_headline=_("Full Spend!"),
text=_("This PSBT spends its entire input value. No change is coming back to your wallet."),
button_data=[ButtonOption("Continue")],
).display()
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
@@ -265,7 +267,7 @@ class PSBTAddressDetailsView(View):
button_data = []
if self.address_num < psbt_parser.num_destinations - 1:
button_data.append(ButtonOption("Next Recipient"))
button_data.append(ButtonOption("Next recipient"))
else:
# TRANSLATOR_NOTE: Short for "Next step"
button_data.append(ButtonOption("Next"))
@@ -300,8 +302,8 @@ class PSBTAddressDetailsView(View):
class PSBTChangeDetailsView(View):
NEXT = ButtonOption("Next")
SKIP_VERIFICATION = ButtonOption("Skip Verification")
VERIFY_MULTISIG = ButtonOption("Verify Multisig Change")
SKIP_VERIFICATION = ButtonOption("Skip verification")
VERIFY_MULTISIG = ButtonOption("Verify multisig change")
def __init__(self, change_address_num):
super().__init__()
@@ -466,13 +468,14 @@ class PSBTAddressVerificationFailedView(View):
# TRANSLATOR_NOTE: Variable is either "change" or "self-transfer".
text = _("PSBT's {} address could not be generated from your seed.").format(_("change") if self.is_change else _("self-transfer"))
DireWarningScreen(
self.run_screen(
DireWarningScreen,
title=_("Suspicious PSBT"),
status_headline=_("Address Verification Failed"),
text=text,
button_data=[ButtonOption("Discard PSBT")],
show_back_button=False,
).display()
)
# We're done with this PSBT. Route back to MainMenuView which always
# clears all ephemeral data (except in-memory seeds).
@@ -569,7 +572,7 @@ class PSBTSignedQRDisplayView(View):
class PSBTSigningErrorView(View):
SELECT_DIFF_SEED = ButtonOption("Select Diff Seed")
SELECT_DIFF_SEED = ButtonOption("Select diff seed")
def run(self):
psbt_parser: PSBTParser = self.controller.psbt_parser
+68 -55
View File
@@ -268,7 +268,7 @@ class SeedMnemonicEntryView(View):
class SeedMnemonicInvalidView(View):
EDIT = ButtonOption("Review & Edit")
EDIT = ButtonOption("Review & edit")
DISCARD = ButtonOption("Discard", button_label_color="red")
def __init__(self):
@@ -366,23 +366,18 @@ class SeedAddPassphraseView(View):
# The new passphrase will be the return value; it might be empty.
self.seed.set_passphrase(ret_dict["passphrase"])
if "is_back_button" in ret_dict:
if len(self.seed.passphrase) > 0:
return Destination(SeedAddPassphraseExitDialogView)
else:
return Destination(BackStackView)
elif len(self.seed.passphrase) > 0:
return Destination(SeedReviewPassphraseView)
if "is_back_button" in ret_dict or len(self.seed.passphrase) == 0:
return Destination(SeedAddPassphraseExitDialogView)
else:
return Destination(SeedFinalizeView)
return Destination(SeedReviewPassphraseView)
class SeedAddPassphraseExitDialogView(View):
EDIT = ButtonOption("Edit passphrase")
DISCARD = ButtonOption("Discard passphrase", button_label_color="red")
SKIP = ButtonOption("Skip passphrase") # NOT red since we're not throwing anything away
def __init__(self):
super().__init__()
@@ -390,13 +385,20 @@ class SeedAddPassphraseExitDialogView(View):
def run(self):
button_data = [self.EDIT, self.DISCARD]
if self.seed.passphrase:
title = _("Discard passphrase?")
message = _("Your current passphrase entry will be erased.")
button_data = [self.EDIT, self.DISCARD]
else:
title = _("Skip passphrase?")
message = _("You have not entered a passphrase yet.")
button_data = [self.EDIT, self.SKIP]
selected_menu_num = self.run_screen(
WarningScreen,
title=_("Discard passphrase?"),
title=title,
status_headline=None,
text=_("Your current passphrase entry will be erased"),
text=message,
show_back_button=False,
button_data=button_data,
)
@@ -404,7 +406,7 @@ class SeedAddPassphraseExitDialogView(View):
if button_data[selected_menu_num] == self.EDIT:
return Destination(SeedAddPassphraseView)
elif button_data[selected_menu_num] == self.DISCARD:
elif button_data[selected_menu_num] in [self.DISCARD, self.SKIP]:
self.seed.set_passphrase("")
return Destination(SeedFinalizeView)
@@ -454,7 +456,7 @@ class SeedReviewPassphraseView(View):
class SeedDiscardView(View):
KEEP = ButtonOption("Keep Seed")
KEEP = ButtonOption("Keep seed")
DISCARD = ButtonOption("Discard", button_label_color="red")
def __init__(self, seed_num: int = None):
@@ -523,13 +525,13 @@ class SeedElectrumMnemonicStartView(View):
****************************************************************************"""
class SeedOptionsView(View):
SCAN_PSBT = ButtonOption("Scan PSBT", SeedSignerIconConstants.QRCODE)
VERIFY_ADDRESS = ButtonOption("Verify Addr")
EXPORT_XPUB = ButtonOption("Export Xpub")
EXPLORER = ButtonOption("Address Explorer")
SIGN_MESSAGE = ButtonOption("Sign Message")
BACKUP = ButtonOption("Backup Seed", right_icon_name=SeedSignerIconConstants.CHEVRON_RIGHT)
BIP85_CHILD_SEED = ButtonOption("BIP-85 Child Seed")
DISCARD = ButtonOption("Discard Seed", button_label_color="red")
VERIFY_ADDRESS = ButtonOption("Verify addr")
EXPORT_XPUB = ButtonOption("Export xpub")
EXPLORER = ButtonOption("Address explorer")
SIGN_MESSAGE = ButtonOption("Sign message")
BACKUP = ButtonOption("Backup seed", right_icon_name=SeedSignerIconConstants.CHEVRON_RIGHT)
BIP85_CHILD_SEED = ButtonOption("BIP-85 child seed")
DISCARD = ButtonOption("Discard seed", button_label_color="red")
def __init__(self, seed_num: int):
@@ -635,7 +637,7 @@ class SeedOptionsView(View):
class SeedBackupView(View):
VIEW_WORDS = ButtonOption("View Seed Words")
VIEW_WORDS = ButtonOption("View seed words")
EXPORT_SEEDQR = ButtonOption("Export as SeedQR")
def __init__(self, seed_num):
@@ -1084,13 +1086,14 @@ class SeedWordsView(View):
else:
button_data.append(self.DONE)
selected_menu_num = seed_screens.SeedWordsScreen(
selected_menu_num = self.run_screen(
seed_screens.SeedWordsScreen,
title=f"{title}: {self.page_index+1}/{num_pages}",
words=words,
page_index=self.page_index,
num_pages=num_pages,
button_data=button_data,
).display()
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
@@ -1117,17 +1120,17 @@ class SeedWordsView(View):
"""****************************************************************************
BIP85 - Derive child mnemonic (seed) flow
BIP-85 - Derive child mnemonic (seed) flow
****************************************************************************"""
class SeedBIP85ApplicationModeView(View):
"""
* Ask the user the application type as defined in the BIP0085 spec.
* Ask the user the application type as defined in the BIP-85 spec.
* Currently only Word mode of 12, 24 words (Application number: 39')
* Possible future additions are
* WIF (HDSEED)
* XPRV (BIP32)
* XPRV (BIP-32)
"""
# TODO: Future enhancement to display WIF (HD-SEED) and XPRV (Bip32)?
# TODO: Future enhancement to display WIF (HD-SEED) and XPRV (BIP-32)?
WORDS_12 = ButtonOption("12 Words")
WORDS_24 = ButtonOption("24 Words")
@@ -1141,10 +1144,11 @@ class SeedBIP85ApplicationModeView(View):
def run(self):
button_data = [self.WORDS_12, self.WORDS_24]
selected_menu_num = ButtonListScreen(
selected_menu_num = self.run_screen(
ButtonListScreen,
title=_("BIP-85 Num Words"),
button_data=button_data
).display()
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
@@ -1171,7 +1175,7 @@ class SeedBIP85SelectChildIndexView(View):
def run(self):
# TODO: Change this later to use the generic Screen input keyboard
ret = seed_screens.SeedBIP85SelectChildIndexScreen().display()
ret = self.run_screen(seed_screens.SeedBIP85SelectChildIndexScreen)
if ret == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
@@ -1204,14 +1208,15 @@ class SeedBIP85InvalidChildIndexView(View):
def run(self):
DireWarningScreen(
self.run_screen(
DireWarningScreen,
title=_("BIP-85 Index Error"),
show_back_button=False,
status_icon_name=SeedSignerIconConstants.ERROR,
status_headline=_("Invalid Child Index"),
text=_("BIP-85 Child Index must be between 0 and 2^31-1."),
button_data=[ButtonOption("Try Again")]
).display()
button_data=[ButtonOption("Try again")]
)
return Destination(
SeedBIP85SelectChildIndexView,
@@ -1239,9 +1244,10 @@ class SeedWordsBackupTestPromptView(View):
def run(self):
button_data = [self.VERIFY, self.SKIP]
selected_menu_num = seed_screens.SeedWordsBackupTestPromptScreen(
selected_menu_num = self.run_screen(
seed_screens.SeedWordsBackupTestPromptScreen,
button_data=button_data,
).display()
)
if button_data[selected_menu_num] == self.VERIFY:
return Destination(
@@ -1305,13 +1311,14 @@ class SeedWordsBackupTestView(View):
# TRANSLATOR_NOTE: Inserts the word number (e.g. "Verify Word #1")
title = _("Verify Word #{}").format(self.cur_index + 1)
selected_menu_num = ButtonListScreen(
selected_menu_num = self.run_screen(
ButtonListScreen,
title=title,
show_back_button=False,
button_data=button_data,
is_bottom_list=True,
is_button_text_centered=True,
).display()
)
if button_data[selected_menu_num] == real_word:
self.confirmed_list.append(self.cur_index)
@@ -1344,8 +1351,8 @@ class SeedWordsBackupTestView(View):
class SeedWordsBackupTestMistakeView(View):
REVIEW = ButtonOption("Review Seed Words")
RETRY = ButtonOption("Try Again")
REVIEW = ButtonOption("Review seed words")
RETRY = ButtonOption("Try again")
def __init__(self, seed_num: int, bip85_data: dict = None, cur_index: int = None, wrong_word: str = None, confirmed_list: list[bool] = None):
super().__init__()
@@ -1365,14 +1372,15 @@ class SeedWordsBackupTestMistakeView(View):
# TRANSLATOR_NOTE: User selected the wrong word during the mnemonic backup test (e.g. incorrectly said the 5th word was "zoo")
status_headline = _("Wrong Word!")
selected_menu_num = DireWarningScreen(
selected_menu_num = self.run_screen(
DireWarningScreen,
title=_("Verification Error"),
show_back_button=False,
status_icon_name=SeedSignerIconConstants.ERROR,
status_headline=status_headline,
button_data=button_data,
text=text,
).display()
)
if button_data[selected_menu_num] == self.REVIEW:
return Destination(
@@ -1400,13 +1408,14 @@ class SeedWordsBackupTestSuccessView(View):
def run(self):
from seedsigner.gui.screens.screen import LargeIconStatusScreen
LargeIconStatusScreen(
self.run_screen(
LargeIconStatusScreen,
title=_("Backup Verified"),
show_back_button=False,
status_headline=_("Success!"),
text=_("All mnemonic backup words were successfully verified!"),
button_data=[ButtonOption("OK")]
).display()
)
if self.seed_num is not None:
return Destination(SeedOptionsView, view_args=dict(seed_num=self.seed_num), clear_history=True)
@@ -1591,12 +1600,13 @@ class SeedTranscribeSeedQRZoomedInView(View):
else:
num_modules = 25
seed_screens.SeedTranscribeSeedQRZoomedInScreen(
self.run_screen(
seed_screens.SeedTranscribeSeedQRZoomedInScreen,
qr_data=data,
num_modules=num_modules,
initial_zone_x=self.initial_zone_x,
initial_zone_y=self.initial_zone_y,
).display()
)
return Destination(SeedTranscribeSeedQRConfirmQRPromptView, view_args={"seed_num": self.seed_num})
@@ -1646,12 +1656,16 @@ class SeedTranscribeSeedQRConfirmScanView(View):
# Run the live preview and QR code capture process
# TODO: Does this belong in its own BaseThread?
self.run_screen(
scanning_done=self.run_screen(
ScanScreen,
decoder=self.decoder,
instructions_text=_("Scan your SeedQR")
)
# If the scanning was canceled because the back button was pressed, return to BackStackView (SeedTranscribeSeedQRConfirmQRPromptView).
if scanning_done==False:
return Destination(BackStackView, skip_current_view=False)
if self.decoder.is_complete:
if self.decoder.is_seed:
seed_mnemonic = self.decoder.get_seed_phrase()
@@ -1661,9 +1675,8 @@ class SeedTranscribeSeedQRConfirmScanView(View):
else:
return Destination(SeedTranscribeSeedQRConfirmSuccessView, view_args={"seed_num": self.seed_num})
else:
# Will this case ever happen? Will trigger if a different kind of QR code is scanned
return Destination(SeedTranscribeSeedQRConfirmInvalidQRView, skip_current_view=True)
# Will trigger if a different kind of QR code is scanned (non SeedQR)
return Destination(SeedTranscribeSeedQRConfirmInvalidQRView, skip_current_view=True)
@@ -2039,7 +2052,7 @@ class SeedAddressVerificationSuccessView(View):
class LoadMultisigWalletDescriptorView(View):
SCAN = ButtonOption("Scan Descriptor", SeedSignerIconConstants.QRCODE)
SCAN = ButtonOption("Scan descriptor", SeedSignerIconConstants.QRCODE)
CANCEL = ButtonOption("Cancel")
def run(self):
@@ -2065,8 +2078,8 @@ class LoadMultisigWalletDescriptorView(View):
class MultisigWalletDescriptorView(View):
RETURN = ButtonOption("Return to PSBT")
VERIFY_ADDR = ButtonOption("Verify Addr")
ADDRESS_EXPLORER = ButtonOption("Address Explorer")
VERIFY_ADDR = ButtonOption("Verify addr")
ADDRESS_EXPLORER = ButtonOption("Address explorer")
OK = ButtonOption("OK")
def run(self):
+74 -56
View File
@@ -23,8 +23,8 @@ class ToolsMenuView(View):
IMAGE = ButtonOption("New seed", FontAwesomeIconConstants.CAMERA)
DICE = ButtonOption("New seed", FontAwesomeIconConstants.DICE)
KEYBOARD = ButtonOption("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD)
ADDRESS_EXPLORER = ButtonOption("Address Explorer")
VERIFY_ADDRESS = ButtonOption("Verify Address")
ADDRESS_EXPLORER = ButtonOption("Address explorer")
VERIFY_ADDRESS = ButtonOption("Verify address")
def run(self):
button_data = [self.IMAGE, self.DICE, self.KEYBOARD, self.ADDRESS_EXPLORER, self.VERIFY_ADDRESS]
@@ -64,7 +64,7 @@ class ToolsImageEntropyLivePreviewView(View):
def run(self):
from seedsigner.gui.screens.tools_screens import ToolsImageEntropyLivePreviewScreen
self.controller.image_entropy_preview_frames = None
ret = ToolsImageEntropyLivePreviewScreen().display()
ret = self.run_screen(ToolsImageEntropyLivePreviewScreen)
if ret == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
@@ -104,9 +104,10 @@ class ToolsImageEntropyFinalImageView(View):
sampling_method=Image.Resampling.BICUBIC,
)
ret = ToolsImageEntropyFinalImageScreen(
ret = self.run_screen(
ToolsImageEntropyFinalImageScreen,
final_image=display_version
).display()
)
if ret == RET_CODE__BACK_BUTTON:
# Go back to live preview and reshoot
@@ -124,61 +125,73 @@ class ToolsImageEntropyMnemonicLengthView(View):
def run(self):
button_data = [self.TWELVE_WORDS, self.TWENTYFOUR_WORDS]
selected_menu_num = ButtonListScreen(
selected_menu_num = self.run_screen(
ButtonListScreen,
title=_("Mnemonic Length?"),
button_data=button_data,
).display()
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
mnemonic_length = button_data[selected_menu_num].return_data
preview_images = self.controller.image_entropy_preview_frames
seed_entropy_image = self.controller.image_entropy_final_image
# The entropy calculation can take time, especially with a full image buffer.
# Show a loading spinner to provide feedback during this delay.
from seedsigner.gui.screens.screen import LoadingScreenThread
self.loading_screen = LoadingScreenThread(text=_("Calculating..."))
self.loading_screen.start()
# Build in some hardware-level uniqueness via CPU unique Serial num
try:
stream = os.popen("cat /proc/cpuinfo | grep Serial")
output = stream.read()
serial_num = output.split(":")[-1].strip().encode('utf-8')
serial_hash = hashlib.sha256(serial_num)
hash_bytes = serial_hash.digest()
except Exception as e:
logger.info(repr(e), exc_info=True)
hash_bytes = b'0'
preview_images = self.controller.image_entropy_preview_frames
seed_entropy_image = self.controller.image_entropy_final_image
# Build in modest entropy via millis since power on
millis_hash = hashlib.sha256(hash_bytes + str(time.time()).encode('utf-8'))
hash_bytes = millis_hash.digest()
# Build in some hardware-level uniqueness via CPU unique Serial num
try:
stream = os.popen("cat /proc/cpuinfo | grep Serial")
output = stream.read()
serial_num = output.split(":")[-1].strip().encode('utf-8')
serial_hash = hashlib.sha256(serial_num)
hash_bytes = serial_hash.digest()
except Exception as e:
logger.info(repr(e), exc_info=True)
hash_bytes = b'0'
# Build in better entropy by chaining the preview frames
for frame in preview_images:
img_hash = hashlib.sha256(hash_bytes + frame.tobytes())
hash_bytes = img_hash.digest()
# Build in modest entropy via millis since power on
millis_hash = hashlib.sha256(hash_bytes + str(time.time()).encode('utf-8'))
hash_bytes = millis_hash.digest()
# Finally build in our headline entropy via the new full-res image
final_hash = hashlib.sha256(hash_bytes + seed_entropy_image.tobytes()).digest()
# Build in better entropy by chaining the preview frames
for frame in preview_images:
img_hash = hashlib.sha256(hash_bytes + frame.tobytes())
hash_bytes = img_hash.digest()
if mnemonic_length == 12:
# 12-word mnemonic only uses the first 128 bits / 16 bytes of entropy
final_hash = final_hash[:16]
# Finally build in our headline entropy via the new full-res image
final_hash = hashlib.sha256(hash_bytes + seed_entropy_image.tobytes()).digest()
# Generate the mnemonic
mnemonic = mnemonic_generation.generate_mnemonic_from_bytes(final_hash)
if mnemonic_length == 12:
# 12-word mnemonic only uses the first 128 bits / 16 bytes of entropy
final_hash = final_hash[:16]
# Image should never get saved nor stick around in memory
seed_entropy_image = None
preview_images = None
final_hash = None
hash_bytes = None
self.controller.image_entropy_preview_frames = None
self.controller.image_entropy_final_image = None
# Generate the mnemonic
mnemonic = mnemonic_generation.generate_mnemonic_from_bytes(final_hash)
# Image should never get saved nor stick around in memory
seed_entropy_image = None
preview_images = None
final_hash = None
hash_bytes = None
self.controller.image_entropy_preview_frames = None
self.controller.image_entropy_final_image = None
# Add the mnemonic as an in-memory Seed
seed = Seed(mnemonic, wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE))
self.controller.storage.set_pending_seed(seed)
finally:
# Stop spinner even if an error occurs
self.loading_screen.stop()
# Add the mnemonic as an in-memory Seed
seed = Seed(mnemonic, wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE))
self.controller.storage.set_pending_seed(seed)
# Cannot return BACK to this View
return Destination(SeedWordsWarningView, view_args={"seed_num": None}, clear_history=True)
@@ -201,12 +214,13 @@ class ToolsDiceEntropyMnemonicLengthView(View):
TWENTY_FOUR = ButtonOption(twenty_four, return_data=mnemonic_generation.DICE__NUM_ROLLS__24WORD)
button_data = [TWELVE, TWENTY_FOUR]
selected_menu_num = ButtonListScreen(
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)
@@ -227,9 +241,10 @@ class ToolsDiceEntropyEntryView(View):
def run(self):
from seedsigner.gui.screens.tools_screens import ToolsDiceEntropyEntryScreen
ret = ToolsDiceEntropyEntryScreen(
ret = self.run_screen(
ToolsDiceEntropyEntryScreen,
return_after_n_chars=self.total_rolls,
).display()
)
if ret == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
@@ -292,11 +307,12 @@ class ToolsCalcFinalWordFinalizePromptView(View):
num_entropy_bits = 3
button_data = [self.COIN_FLIPS, self.SELECT_WORD, self.ZEROS]
selected_menu_num = ToolsCalcFinalWordFinalizePromptScreen(
selected_menu_num = self.run_screen(
ToolsCalcFinalWordFinalizePromptScreen,
mnemonic_length=mnemonic_length,
num_entropy_bits=num_entropy_bits,
button_data=button_data,
).display()
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
@@ -328,9 +344,10 @@ class ToolsCalcFinalWordCoinFlipsView(View):
else:
total_flips = 3
ret_val = ToolsCoinFlipEntryScreen(
ret_val = self.run_screen(
ToolsCoinFlipEntryScreen,
return_after_n_chars=total_flips,
).display()
)
if ret_val == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
@@ -431,12 +448,13 @@ class ToolsCalcFinalWordDoneView(View):
button_data = [self.LOAD, self.DISCARD]
selected_menu_num = ToolsCalcFinalWordDoneScreen(
selected_menu_num = self.run_screen(
ToolsCalcFinalWordDoneScreen,
final_word=final_word,
mnemonic_word_length=mnemonic_word_length,
fingerprint=self.controller.storage.get_pending_mnemonic_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)),
button_data=button_data,
).display()
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
@@ -520,10 +538,10 @@ class ToolsAddressExplorerSelectSourceView(View):
class ToolsAddressExplorerAddressTypeView(View):
# TRANSLATOR_NOTE: label for addresses where others send us incoming payments
RECEIVE = ButtonOption("Receive Addresses")
RECEIVE = ButtonOption("Receive addresses")
# TRANSLATOR_NOTE: label for addresses that collect the change from our own outgoing payments
CHANGE = ButtonOption("Change Addresses")
CHANGE = ButtonOption("Change addresses")
def __init__(self, seed_num: int = None, script_type: str = None, custom_derivation: str = None):
+24 -13
View File
@@ -1,3 +1,4 @@
import logging
from dataclasses import dataclass
from gettext import gettext as _
from typing import Type
@@ -10,6 +11,9 @@ from seedsigner.models.settings import Settings, SettingsConstants
from seedsigner.models.settings_definition import SettingsDefinition
from seedsigner.models.threads import BaseThread
logger = logging.getLogger(__name__)
class BackStackView:
"""
@@ -221,7 +225,7 @@ class MainMenuView(View):
class PowerOptionsView(View):
RESET = ButtonOption("Restart", SeedSignerIconConstants.RESTART)
POWER_OFF = ButtonOption("Power Off", SeedSignerIconConstants.POWER)
POWER_OFF = ButtonOption("Power off", SeedSignerIconConstants.POWER)
def run(self):
button_data = [self.RESET, self.POWER_OFF]
@@ -242,30 +246,37 @@ class PowerOptionsView(View):
return Destination(PowerOffView)
@dataclass
class RestartView(View):
is_screenshot_renderer: bool = False
def run(self):
from seedsigner.gui.screens.screen import ResetScreen
thread = RestartView.DoResetThread()
thread.start()
if not self.is_screenshot_renderer:
# We don't want the screenshot generator to actually try to do the restart
RestartView.DoResetThread().start()
self.run_screen(ResetScreen)
class DoResetThread(BaseThread):
def run(self):
import os
import sys
import time
from subprocess import call
logger.info("Restarting SeedSigner")
# Give the screen just enough time to display the reset message before
# exiting.
time.sleep(0.25)
# Kill the SeedSigner process; Running the process again.
# `.*` is a wildcard to detect either `python`` or `python3`.
if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
call("kill $(pidof python*) & python /opt/src/main.py", shell=True)
else:
call("kill $(ps aux | grep '[p]ython.*main.py' | awk '{print $2}')", shell=True)
# Flush any buffered data.
sys.stdout.flush()
sys.stderr.flush()
# Replace the current process with a new one.
os.execv(sys.executable, [sys.executable] + sys.argv)
@@ -291,7 +302,7 @@ class NotYetImplementedView(View):
title=_("Work In Progress"),
status_headline=_("Not Yet Implemented"),
text=self.text,
button_data=[ButtonOption("Back to Main Menu")],
button_data=[ButtonOption("Back to main menu")],
)
return Destination(MainMenuView)
@@ -366,7 +377,7 @@ class UnhandledExceptionView(View):
@dataclass
class OptionDisabledView(View):
UPDATE_SETTING = ButtonOption("Update Setting")
UPDATE_SETTING = ButtonOption("Update setting")
DONE = ButtonOption("Done")
settings_attr: str
+128
View File
@@ -0,0 +1,128 @@
from binascii import a2b_base64, unhexlify
from io import BytesIO
from embit.networks import NETWORKS
from embit.psbt import PSBT, OutputScope
from seedsigner.models.seed import Seed
class PSBTTestData:
"""
Output data extracted from regtest psbts via the util helper methods below.
"""
seed = Seed("model ensure search plunge galaxy firm exclude brain satoshi meadow cable roast".split())
multisig_key_2 = Seed("better gown govern speak spawn vendor exercise item uncle odor sound cat".split())
multisig_key_3 = Seed("sign sword lift deer ocean insect web lazy sick pencil start select".split())
# Base psbts with various inputs for various wallet script types; outputs have been stripped
SINGLE_SIG_NATIVE_SEGWIT_1_INPUT = "cHNidP8BADMCAAAAAU4T/0aX9mmNZHyKh+0AHYY+EtdJxndMRra0gn4QPCZdAQAAAAD9////AHAAAABPAQQ1h88DV5lJlYAAAADURWu0eYGA/OFm4Wt3dUJ44H2DzgcPO6J+nF0t9lqMqgLDxbT8Qez0U3W7o1JwT1FQ4fWFAbcK5eJ3NDnETRryMhAPuIL/VAAAgAEAAIAAAACAAAEAiAIAAAAB1Tct22qlCW5iTOZDfXqVu6CKlwdTtrPhbPI7pcTQ/GUBAAAAFxYAFCPpAsYWUNJnTErOjkoChnQylGp6/f///wLNLhoeAQAAABYAFI4lU5EAZIlw/MpX6r3gh1zqazEOAOH1BQAAAAAWABRYjLo+w43NT+5rzgPCNuy79lZjDm4AAAABAR8A4fUFAAAAABYAFFiMuj7Djc1P7mvOA8I27Lv2VmMOAQMEAQAAACIGAvXbqnkmWDXPNEkHznP0l2VFFql2WpgX42yD3Z9fJY/3GA+4gv9UAACAAQAAgAAAAIAAAAAAAwAAAAA="
SINGLE_SIG_NESTED_SEGWIT_1_INPUT = "cHNidP8BADMCAAAAAdU3LdtqpQluYkzmQ316lbugipcHU7az4WzyO6XE0PxlAAAAAAD9////AHAAAABPAQQ1h88DIWg8n4AAAACkwPiG7M1QVnBPyAKQofuHpAZAXgXRNyJ15AYUNQRCFQIwD+a/8Dc8z2f/GXiC8z1fbvjC6VqIcciZQ51uU2tq9BAPuIL/MQAAgAEAAIAAAACAAAEAcwIAAAAB9EA7OurOd1aGyQJpQN+S0G+3IF31HO2UFAZnIpALsfQAAAAAAP3///8CAOH1BQAAAAAXqRQDW7vsMAXGa+tKARihpGq6ZwuDYYdxEBAkAQAAABepFGaYxdrjSngzRRSCeg3U/SKQOEfXhwAAAAABASAA4fUFAAAAABepFANbu+wwBcZr60oBGKGkarpnC4NhhwEDBAEAAAABBBYAFDriE9Kbby6bOWzXbXatODyyIRpcIgYDQD4mWFMSOO9mDOb+bwzEiaDf+1dWJICoG4BCig4K2awYD7iC/zEAAIABAACAAAAAgAAAAAAAAAAAAA=="
SINGLE_SIG_TAPROOT_1_INPUT = "cHNidP8BADMCAAAAAQljnRdfh8jB/p3Px/Qozd8x0+T4fxI6AwIOf7jQJm6FAAAAAAABAAAAAAAAAABPAQQ1h88DGQXiXoAAAACWEQ4AaLct5K3pnPzxRrHl/74twiazq4XLYi+gGu/5BgPEM2JVPwHaGssfGr25yECm8d73bfEpXMdNNBV3vMW72hAPuIL/VgAAgAEAAIAAAACAAAEA8wIAAAABrv27i6AMCio94q4NQzF6YKxuhgeRW1KQZm2+jWaKGDQBAAAAakcwRAIgM5nApqEOs/MiP9GMA5yKvV3a5qXp8sGfQ8edVizr/ZUCIHhDNQlYvBXCr8iTGycq1/XznJYav88tIJ5Cb7/pdMc8ASECtDcBLkIBPnoXcobfeX36XFp8nD+ZSj1DfxQbQy2JW6/9////AgDh9QUAAAAAIlEgcd/UDa6cM/La5HWyGkTvZOIFp6qTD3MlHZav5/CDmlsT5Vb6AAAAACJRIESp0pjyhqlKbwUtt6WOLKRUhX6Id49uLAK3cd0s+SWrcgAAAAEBKwDh9QUAAAAAIlEgcd/UDa6cM/La5HWyGkTvZOIFp6qTD3MlHZav5/CDmlsBAwQAAAAAIRYXKjsWmE7bdCAHODqvbHkLw5B2jIZj5C3QjXX00E6AdhkAD7iC/1YAAIABAACAAAAAgAAAAAAAAAAAARcgFyo7FphO23QgBzg6r2x5C8OQdoyGY+Qt0I119NBOgHYA"
SINGLE_SIG_LEGACY_P2PKH_1_INPUT = "cHNidP8BADMCAAAAAa79u4ugDAoqPeKuDUMxemCsboYHkVtSkGZtvo1mihg0AAAAAAD9////AHIAAABPAQQ1h88DZEKTEIAAAABFOKVFUmpvsmxCAlh1YH6xGPpkLNS0qGLYidlf2TMkHwLTyV38ld8evWpfC6fWTSUoGkkbCu6TfPqYFx9Hd/up4hAPuIL/LAAAgAEAAIAAAACAAAEAjgIAAAABu1uNN8B9NH+3zx1+QaZxkHPSX+7Osu4kTJ/RYweX3i8AAAAAFxYAFE6ANd2QMqwrq2YgFN/s7E9mbmHc/f///wIA4fUFAAAAABl2qRSFDsVqajk9QGakHH5tKXuflIWEBYisBsdMAAEAAAAZdqkUm0UFp//k5knGslifTh4z3o8s6/GIrHEAAAABAwQBAAAAIgYCENx0YsKdM+SNcY34QcTjuaZ9uYhS2tFoLA14o6OlJmwYD7iC/ywAAIABAACAAAAAgAAAAAAAAAAAAA=="
MULTISIG_NATIVE_SEGWIT_1_INPUT = "cHNidP8BADMCAAAAAbaPcIOkYKEWkbJpYrT/gbRQYzJ6CSFOJ5eeewflO6jJAAAAAAD9////AHAAAABPAQQ1h88EmoSnUYAAAAIQMkp0H2fIDDprJUDbKzC/+HxUkXMORn4E1dW9s6YYUQLOMo4uvEoFnOcNGcjrzHnZ64ZXsv22WoU7UMUfhSn2NxQPuIL/MAAAgAEAAIAAAACAAgAAgE8BBDWHzwRA+hbPgAAAAtCjPoabT6mduHms6sD7+rUniNXoDSlZvjkiYJpUC65FAqCWpcb6ZLoEcloJpk7vLpNRwwuOFNYyRAvzm28odTsIFAPNCiswAACAAQAAgAAAAIACAACATwEENYfPBMtS+WKAAAACIN1+5Cw5eeTBSKSY3g/NXrRBfXXDPEROQpFRHlmMlIECm0FA2Wj3AxUKxq9C1G9QJ2Ar5TvlMy3/VPUkfQS1HJ4UD4iQRDAAAIABAACAAAAAgAIAAIAAAQCJAgAAAAF1X+6fnEE2Mfa3mgKXygqWGWw2qQb3t1I4fdYJ3cIFowAAAAAA/f///wIA4fUFAAAAACIAIMpj8X3shRYalzfqac0USxW9AqloiW42GgwJjS3jLvSVWxAQJAEAAAAiUSBXg7Hc/rsd/34P6AqwBEDsuXzW+aNgcOsTxa+zGiHOXmsAAAABASsA4fUFAAAAACIAIMpj8X3shRYalzfqac0USxW9AqloiW42GgwJjS3jLvSVAQMEAQAAAAEFaVIhAlnU4yc1877LybCWrJke1kaZFO7WCoP2vDyEog2ERtyfIQKyV23E2SQWvjjaPhSNsRbfaZpPrORdY1Tr73Hm0Ub4nyEDy3sMBTuWlna6t45bdHdl/NzROoKeDVDMwLcIObQE9ZVTriIGAlnU4yc1877LybCWrJke1kaZFO7WCoP2vDyEog2ERtyfHA+4gv8wAACAAQAAgAAAAIACAACAAAAAAAAAAAAiBgKyV23E2SQWvjjaPhSNsRbfaZpPrORdY1Tr73Hm0Ub4nxwDzQorMAAAgAEAAIAAAACAAgAAgAAAAAAAAAAAIgYDy3sMBTuWlna6t45bdHdl/NzROoKeDVDMwLcIObQE9ZUcD4iQRDAAAIABAACAAAAAgAIAAIAAAAAAAAAAAAA="
MULTISIG_NESTED_SEGWIT_1_INPUT = "cHNidP8BADMCAAAAAbtbjTfAfTR/t88dfkGmcZBz0l/uzrLuJEyf0WMHl94vAQAAAAD9////AHEAAABPAQQ1h88EmoSnUYAAAAF+MfPe6kyaEEX91G1HKwksGaixXN6RMBTxIcHKjNYYoQJBzkFqUsm3ttoRsGfx+CMj/77pmyyjitqjWZPG4d3RrxQPuIL/MAAAgAEAAIAAAACAAQAAgE8BBDWHzwRA+hbPgAAAAcRAG6PxSUPbFp8IvKVuNlIQn4W5TK1ceLGdKkxdSfktA8QjWNuOjADhspq0onHkGp117FftE91lAYTev8snQsNRFAPNCiswAACAAQAAgAAAAIABAACATwEENYfPBMtS+WKAAAAB6inmZ+P+TS/JJhI/Fog5q8Rx2Nik9EJVEugTuPSDcI4CbvN4qeJalXMOlJpoKnL9Y64icQtz01M9NG6SOhcG8uQUD4iQRDAAAIABAACAAAAAgAEAAIAAAQBzAgAAAAHCfPUdyeETNxf9pkzBP2oOUfgSISrrZi4uj0boKN2PeQEAAAAA/f///wKwqEIGAQAAABepFOV0lUEDO1EMcpnbm8u5gOS//denhwDh9QUAAAAAF6kUURslNGksU91RDOgpxCM7IQOoQaSHAAAAAAEBIADh9QUAAAAAF6kUURslNGksU91RDOgpxCM7IQOoQaSHAQMEAQAAAAEEIgAgDHlipQ8OV+Wko64bycNh+v4LfTW5d8cTv3T1n4XDS/sBBWlSIQIsxclipQM/Gs3kdO+Mlg1gbcMRf1ukSklJhhQ8iMXpSSEDjvwJUpUWl14h2ma/DH5VeAEhM9PxTz70b6lOAgDiWrshA8FwdXOWiPDUIOtV4aCz/ZxfLr9fwrpHQLCUeAtGctJiU64iBgOO/AlSlRaXXiHaZr8MflV4ASEz0/FPPvRvqU4CAOJauxwPuIL/MAAAgAEAAIAAAACAAQAAgAAAAAAAAAAAIgYCLMXJYqUDPxrN5HTvjJYNYG3DEX9bpEpJSYYUPIjF6UkcA80KKzAAAIABAACAAAAAgAEAAIAAAAAAAAAAACIGA8FwdXOWiPDUIOtV4aCz/ZxfLr9fwrpHQLCUeAtGctJiHA+IkEQwAACAAQAAgAAAAIABAACAAAAAAAAAAAAA"
MULTISIG_LEGACY_P2SH_1_INPUT = "cHNidP8BADMCAAAAAarS4QMzScnPvNBT2B1I3h80UrSoNuKd9vZkjSl7j2WKAQAAAAD9////AHQAAABPAQQ1h88BD7iC/4AAAC16oZcoUbg2ksYGYWMICvKISPS51jTZLJ3tu4schhGxcAJ/o4LdLxGZHyVDceUz5n6ZtABk9X6nBk3yz/f+eXxkpQgPuIL/LQAAgE8BBDWHzwEDzQorgAAALaD15sHGuMCJCZiW09JfCCZEKGcn9WtB395d/8vj/WC7Am982IdFkEBytDXikxzoeLV5q3kpHg+bfmsmj7ncr1lgCAPNCistAACATwEENYfPAQ+IkESAAAAtcUAwLhCloJPpswRwhGdyG3KP1kY0VAetU6FdAmNrqQoDKFgD/CXYCi5ZHqx4HImYJZtoxpjx60Ki7kvK4Ean8FkID4iQRC0AAIAAAQBzAgAAAAFOE/9Gl/ZpjWR8ioftAB2GPhLXScZ3TEa2tIJ+EDwmXQAAAAAA/f///wI+TSQYAQAAABepFE+Jek+WpV0vCjnVpkLqKfoiLvPAhwDh9QUAAAAAF6kUdHD4u+bbshVEAVKkqRQxu+KR8FKHaAAAAAEDBAEAAAABBGlSIQLUf/ihLNEohwIQiCxVNGmPROLzi8t9FnV6DmfCuEhXHCEDK1QlMflvscOGZmSDWJi90FPUQvNFTQUbkkRdowbR0RghA2T1w+njATChE10XcsbguMj0J5RIzJ5TVN7sbVckbzUWU64iBgLUf/ihLNEohwIQiCxVNGmPROLzi8t9FnV6DmfCuEhXHBAPuIL/LQAAgAAAAAAAAAAAIgYDZPXD6eMBMKETXRdyxuC4yPQnlEjMnlNU3uxtVyRvNRYQA80KKy0AAIAAAAAAAAAAACIGAytUJTH5b7HDhmZkg1iYvdBT1ELzRU0FG5JEXaMG0dEYEA+IkEQtAACAAAAAAAAAAAAA"
SINGLE_SIG_INPUTS = [
SINGLE_SIG_NATIVE_SEGWIT_1_INPUT,
SINGLE_SIG_NESTED_SEGWIT_1_INPUT,
SINGLE_SIG_TAPROOT_1_INPUT,
SINGLE_SIG_LEGACY_P2PKH_1_INPUT,
]
MULTISIG_INPUTS = [
MULTISIG_NATIVE_SEGWIT_1_INPUT,
MULTISIG_NESTED_SEGWIT_1_INPUT,
MULTISIG_LEGACY_P2SH_1_INPUT
]
ALL_INPUTS = SINGLE_SIG_INPUTS + MULTISIG_INPUTS
MULTISIG_NATIVE_SEGWIT_DESCRIPTOR = "wsh(sortedmulti(2,[0fb882ff/48h/1h/0h/2h]tpubDF3QYaRazZ44jHz3jaSPRGCLVYj7D8j4mVVUTCr3CHsfuvoV2Z73eTcvHc8sP3Dj58yEfkG57iBpKTuHv3dNAUcFufCxx26SAbrWque5gts/<0;1>/*,[03cd0a2b/48h/1h/0h/2h]tpubDEPEYgTj1ddmZqDdpiq5Gjttx3CnNSFppSaUa5eHAUVNMD2FE1ihGA2EMP92mzmSUGJsTAgMhBTACd9xsRDB5K4GKJH8RzbRuFUrmVVLR15/<0;1>/*,[0f889044/48h/1h/0h/2h]tpubDFQDKbH2mDqNDPNaUVxM6R5mHhzC4u5F6mNnUkCf6gBMbcENMQ1ZGFLZc3QwgdEv2f34wkTvLMG5kD8AZEZRhat1HQDj42eVxQSxbcqxn31/<0;1>/*))#jsc5nnpk"
MULTISIG_NESTED_SEGWIT_DESCRIPTOR = "sh(wsh(sortedmulti(2,[03cd0a2b/48h/1h/0h/1h]tpubDEPEYgTj1ddmXAEnYZG8LjUxRcYNX8xw2QkVEkHj8HVsH3bt3QcS9tgXdfrm31SgNxFY7EfgYcc2BNxcMtg6cB4pdXjND9GjA895aJNsBQp/<0;1>/*,[0fb882ff/48h/1h/0h/1h]tpubDF3QYaRazZ44hqgAAHf16EbhRGBfokgLHcQ5pWB1iQsqLgtTK6rR7XHUBVmwAcv5iU4hQR78PLThbHf5427oAk1hz7Ph8K1r3PUgstnZBvx/<0;1>/*,[0f889044/48h/1h/0h/1h]tpubDFQDKbH2mDqNCqnWUKyj9N2GsGmfJ1mTaoz6BBJ1PRj8FMfd7f3XhFeGBadLtszMp3gRboRmmkWa8TCY8HJN85ouYUALSDnGCzg3vpVDLgd/<0;1>/*)))#per3463t"
MULTISIG_LEGACY_P2SH_DESCRIPTOR = "sh(sortedmulti(2,[0fb882ff/45h]tpubD8Nq4eMvojKe9yL3k7vVq42NdriZpkPMCduzQedPRe9Cy6kCfMiGD49oQvomKgrzk4qUb1NVsg6gJeP4MsVTKDUWaubTWHvqTwNg2GCpbZK/<0;1>/*,[0f889044/45h]tpubD8NkS3Gngj7L4FJRYrwojKhsx2seBhrNrXVdvqaUyvtVe1YDCVcziZVa9g3KouXz7FN5CkGBkoC16nmNu2HcG9ubTdtCbSW8DEXSMHmmu62/<0;1>/*,[03cd0a2b/45h]tpubD8HkLLgkdJkVitn1i9CN4HpFKJdom48iKm9PyiXYz5hivn1cGz6H3VeS6ncmCEgamvzQA2Qofu2YSTwWzvuaYWbJDEnvTUtj5R96vACdV6L/<0;1>/*))#8pfqnxa2"
# Internal self-transfer and change outputs
SINGLE_SIG_NATIVE_SEGWIT_CHANGE = "2202022c15efd08c822a6287f1b42b6c30ecced47f1c5484c3e7d72e7680a305893d27180fb882ff5400008001000080000000800100000000000000010308e8fcf105000000000104160014acc1340709e9b7babeff921806f0570c652bd05800"
SINGLE_SIG_NATIVE_SEGWIT_SELF_TRANSFER = "2202028d0b3faac6bd38acc2075191bec98612b87100047a7a8d77ed9ef4e3ae03dfc3180fb882ff540000800100008000000080000000000600000001030890d00300000000000104160014f8b609a3b82d8091fb2c99f4f0302bbb838e48ed00"
SINGLE_SIG_NESTED_SEGWIT_CHANGE = "01001600145f801cc93ce9357afef02087213775e98b95923d22020236cdb1b950a92dc2f72182c88af0c898a3d8db06dcf57c3ea662850beffc2793180fb882ff3100008001000080000000800100000000000000010308aef3c90500000000010417a9148f71f1b34439b80a1c37a9501d0facf4fa9bf8d38700"
SINGLE_SIG_NESTED_SEGWIT_SELF_TRANSFER = "0100160014b574a614ffb9a43ee85f7a08e6ba6f444908406622020212d8d9d112a7b45aae931f59fb459150b8e5fc5377d9efc176253e284e8443a0180fb882ff31000080010000800000008000000000010000000103084cd82b0000000000010417a914a89b66ac7d9de4d7d5da4dae71065147355643148700"
SINGLE_SIG_TAPROOT_CHANGE = "010308d50ff205000000000104225120f7fae84e379ba7acfef153518f35c109be2e69dd1658e3a5d2b445abd4b6cf730105200a891a9b5600abef50c78e1c9e5d1ebf95d515667c2033098f8f843fa7f459ae21070a891a9b5600abef50c78e1c9e5d1ebf95d515667c2033098f8f843fa7f459ae19000fb882ff560000800100008000000080010000000000000000"
SINGLE_SIG_TAPROOT_SELF_TRANSFER = "01030890d0030000000000010422512045d38d7d2f9b3b2135f93ea1d771495f02eea63deb0bd94c365d8387cf9f5c740105209b36fb4d174ca5ed1a4c0d118bcb75fa4bb5408002b242c773e23df4599f9e6c21079b36fb4d174ca5ed1a4c0d118bcb75fa4bb5408002b242c773e23df4599f9e6c19000fb882ff560000800100008000000080000000000100000000"
SINGLE_SIG_LEGACY_P2PKH_CHANGE = "220203b6390d6aa2516f1004fecb1439cfdb2038f6e1f2504376eae6df3dc1634ac191180fb882ff2c000080010000800000008001000000000000000103088e0ff2050000000001041976a9140de643ba8e5659f365b10da0c879a3999f4a348588ac00"
SINGLE_SIG_LEGACY_P2PKH_SELF_TRANSFER = "220202c168e3f31b9a7c31aa16b33b3ee47cd64ac5d30d31d9c709e7616bc9389d835e180fb882ff2c0000800100008000000080000000000100000001030890d003000000000001041976a914fef98f1e7c7d975710b8b8b0c12d5737be81e7a688ac00"
MULTISIG_NATIVE_SEGWIT_CHANGE = "01016952210234886728fde2bc0e8b9d7f6c4c452c700add80b2690bb237a3c3a6f4e7714c5221025485ce74baeeb49296f486e6780e1e2b46187943de4da3bb8f1a202cd6695393210355da420fc1b72af894a8532bc917528bd0e3bfbc435c33f8f0d9100671084b2353ae22020234886728fde2bc0e8b9d7f6c4c452c700add80b2690bb237a3c3a6f4e7714c521c0fb882ff3000008001000080000000800200008001000000040000002202025485ce74baeeb49296f486e6780e1e2b46187943de4da3bb8f1a202cd66953931c03cd0a2b30000080010000800000008002000080010000000400000022020355da420fc1b72af894a8532bc917528bd0e3bfbc435c33f8f0d9100671084b231c0f88904430000080010000800000008002000080010000000400000001030887f3f105000000000104220020751780ba145dd0a289a88bdccff824116518df23cb674e810654313520eb74db00"
MULTISIG_NATIVE_SEGWIT_SELF_TRANSFER = "010169522102519fb0e4b5de132efb1c603e37007baa8de848609627fb1c8af6670f97dba8162102523b7ebc26a2a23d5f863428bf6047cf146904a5ec33724c0be5444c22e2b9d5210302d297b3658cb2643df5bbb5b8edd8d015d96c0a1547e69ef440e6a72b3af70653ae22020302d297b3658cb2643df5bbb5b8edd8d015d96c0a1547e69ef440e6a72b3af7061c0fb882ff300000800100008000000080020000800000000002000000220202523b7ebc26a2a23d5f863428bf6047cf146904a5ec33724c0be5444c22e2b9d51c03cd0a2b300000800100008000000080020000800000000002000000220202519fb0e4b5de132efb1c603e37007baa8de848609627fb1c8af6670f97dba8161c0f88904430000080010000800000008002000080000000000200000001030890d0030000000000010422002074ef7b55285cc4fe433074ed87e54ffc3a2d2d1e89e411112b8869e28d480eaf00"
MULTISIG_NESTED_SEGWIT_CHANGE = "0100220020b10852265d3992de36ba959d0ffedac561941910f3c64305c55329c3eded6723010169522102ecf1ff790b21ac813fd16ae103adf49e8783d4e1c69efab0dc65ff21f9e2d0fc21038427b43c1e5009b51c4a52aba48157aa2cb0f3e650ee4abd26578767c2915f882103dad1dced2f90f7d9723a67afbde437ac1a4a3ab7e991502a62442212056bdea653ae220203dad1dced2f90f7d9723a67afbde437ac1a4a3ab7e991502a62442212056bdea61c0fb882ff3000008001000080000000800100008001000000000000002202038427b43c1e5009b51c4a52aba48157aa2cb0f3e650ee4abd26578767c2915f881c03cd0a2b300000800100008000000080010000800100000000000000220202ecf1ff790b21ac813fd16ae103adf49e8783d4e1c69efab0dc65ff21f9e2d0fc1c0f8890443000008001000080000000800100008001000000000000000103089a0ff20500000000010417a914e158e224d933585dcf9077e6f0556ddf0b82c3b18700"
MULTISIG_NESTED_SEGWIT_SELF_TRANSFER = "01002200201dc4bf10ca0b4a1106650e2459e8a917a231fd24b4ecdbf1711f4ec063a34df101016952210234361c2816115f8745e95d14243651f622fded6e1f3e30b32ac480e53e3fed3b2102cfd9087055c61d1226649885397872d5f0174186fb2919a7ce0e5149224f78c92103c68ecdc19777b9179b3185a8af96a72baaf3975b7abb125733532031d3b034ab53ae220202cfd9087055c61d1226649885397872d5f0174186fb2919a7ce0e5149224f78c91c0fb882ff300000800100008000000080010000800000000001000000220203c68ecdc19777b9179b3185a8af96a72baaf3975b7abb125733532031d3b034ab1c03cd0a2b30000080010000800000008001000080000000000100000022020234361c2816115f8745e95d14243651f622fded6e1f3e30b32ac480e53e3fed3b1c0f88904430000080010000800000008001000080000000000100000001030890d0030000000000010417a9149c28aee05bdbe48728adc4155c24ab180f899fce8700"
MULTISIG_LEGACY_P2SH_CHANGE = "0100695221031ce6ddb7a26336264b132c1b45c3e631e8502bc155226d46fc38d73d57f9e4ff21031d9e6e094413dc3a508518f729af23a35b3943ca79bafbc55afab631595e5ccf2103c3a6cb2786d860aa1ccff81fae79c1a973bc8a79938af6ff3c75fc4f54333d7353ae220203c3a6cb2786d860aa1ccff81fae79c1a973bc8a79938af6ff3c75fc4f54333d73100fb882ff2d00008001000000000000002202031d9e6e094413dc3a508518f729af23a35b3943ca79bafbc55afab631595e5ccf1003cd0a2b2d00008001000000000000002202031ce6ddb7a26336264b132c1b45c3e631e8502bc155226d46fc38d73d57f9e4ff100f8890442d0000800100000000000000010308000ff20500000000010417a91402e6f3cb4a98d88fbedf0e43869ce9e1a01e8d9f8700"
MULTISIG_LEGACY_P2SH_SELF_TRANSFER = "01006952210283a03b8fa4cce258e536514416f448cf20aca8ef42492413c149a329029adb4e210346b5d12c00554bce6920e60d647893db6555e0844b8583747bc0ca1ecaba3e1c2103bfc0f7151411ace2d2eef986efca7873dabdc0f6f1bbea7bd3c863be1f2e00de53ae220203bfc0f7151411ace2d2eef986efca7873dabdc0f6f1bbea7bd3c863be1f2e00de100fb882ff2d000080000000000100000022020346b5d12c00554bce6920e60d647893db6555e0844b8583747bc0ca1ecaba3e1c1003cd0a2b2d000080000000000100000022020283a03b8fa4cce258e536514416f448cf20aca8ef42492413c149a329029adb4e100f8890442d000080000000000100000001030890d0030000000000010417a914505efc1adc72f354e827da94b49dec46d80717068700"
# External receive outputs
recipient_seed = Seed("shove album flame dad equal cook spike cheap hollow exit great forest".split())
recipient_multisig_key_2 = Seed("grow curve arrive reflect alarm water black funny comfort match attend tired".split())
recipient_multisig_key_3 = Seed("hunt air season climb grocery shadow monitor please virtual cave young denial".split())
SINGLE_SIG_NATIVE_SEGWIT_RECEIVE = "01030890d00300000000000104160014fc243291eec14337ce81817778c251e92da7c45600"
SINGLE_SIG_NESTED_SEGWIT_RECEIVE = "01030890d0030000000000010417a914094a43bc6e1632bd092f1beb8a67dc63160be6ff8700"
SINGLE_SIG_TAPROOT_RECEIVE = "01030890d003000000000001042251200d30561bed15df986cafbfb58150a2208cf422b4efa6891d0534fba834bf06c600"
SINGLE_SIG_LEGACY_P2PKH_RECEIVE = "01030890d003000000000001041976a91467df58b6a9c4c0a2569595036fe6f928390ecd9588ac00"
MULTISIG_NATIVE_SEGWIT_RECEIVE = "01030890d003000000000001042200200936ff1943bbe5c037ad9e9839ca3effe24d8b22fd38cc2601c9007cc0f210a100"
MULTISIG_NESTED_SEGWIT_RECEIVE = "01030890d0030000000000010417a9141d3c080d4b05358b8cc439ef12534250563f34a08700"
MULTISIG_LEGACY_P2SH_RECEIVE = "01030890d0030000000000010417a914fa70ebb69e283b493770c5a8fa19ec76da321de68700"
ALL_EXTERNAL_OUTPUTS = [
SINGLE_SIG_NATIVE_SEGWIT_RECEIVE,
SINGLE_SIG_NESTED_SEGWIT_RECEIVE,
SINGLE_SIG_TAPROOT_RECEIVE,
SINGLE_SIG_LEGACY_P2PKH_RECEIVE,
MULTISIG_NATIVE_SEGWIT_RECEIVE,
MULTISIG_NESTED_SEGWIT_RECEIVE,
MULTISIG_LEGACY_P2SH_RECEIVE,
]
"""
Utils to generate the above PSBT data snippets
"""
def dump_outputs(psbt_base64: str):
psbt = PSBT.parse(a2b_base64(psbt_base64))
for output in psbt.outputs:
out_type = "external recipient"
if output.bip32_derivations:
pubkey, der = list(output.bip32_derivations.items())[0]
if der.derivation[-2] == 1:
out_type = "change"
else:
out_type = "self-transfer"
elif output.taproot_bip32_derivations:
pubkey, (leaf_hashes, der) = list(output.taproot_bip32_derivations.items())[0]
if der.derivation[-2] == 1:
out_type = "change"
else:
out_type = "self-transfer"
# Must specify `version=2` so script_pubkey is included
print(output.script_pubkey.address(NETWORKS["regtest"]), out_type, output.serialize(version=2).hex(), '\n')
def strip_psbt_outputs(psbt_base64: str):
psbt = PSBT.parse(a2b_base64(psbt_base64))
psbt.outputs.clear()
print(psbt)
def create_output(output_hex: str, value: int = None) -> OutputScope:
"""
Given a change, self-transfer, or receive output's serialized hex, create the
corresponding OutputScope that can be added to a PSBT.
Optionally override the output's `value`.
"""
output = OutputScope.read_from(BytesIO(unhexlify(output_hex)))
if value is not None:
output.value = value
return output
+19 -16
View File
@@ -6,6 +6,7 @@ import random
import sys
import time
from unittest.mock import Mock, patch, MagicMock
from PIL import ImageFont
from embit import compact
from embit.psbt import PSBT, OutputScope
@@ -21,9 +22,6 @@ sys.modules['RPi.GPIO'] = MagicMock()
sys.modules['seedsigner.hardware.camera'] = MagicMock()
sys.modules['seedsigner.hardware.microsd'] = MagicMock()
# Force the screenshots to mimic Pi Zero's output without libraqm
patch('PIL.ImageFont.core.HAVE_RAQM', False).start()
from seedsigner.controller import Controller
from seedsigner.gui.components import GUIConstants
from seedsigner.gui.renderer import Renderer
@@ -47,8 +45,6 @@ from .utils import ScreenshotComplete, ScreenshotConfig, ScreenshotRenderer
import warnings; warnings.warn = lambda *args, **kwargs: None
# Dynamically generate a pytest test run for each locale
@pytest.mark.parametrize("locale", [x for x, y in SettingsConstants.get_detected_languages()])
def test_generate_all(locale, target_locale):
@@ -60,6 +56,11 @@ def test_generate_all(locale, target_locale):
if target_locale and locale != target_locale:
pytest.skip(f"Skipping {locale}")
if not ImageFont.core.HAVE_RAQM:
# We can't generate pixel-perfect screenshots that match what gets rendered on
# the device if we don't have libraqm.
pytest.fail("libraqm is not installed.")
generate_screenshots(locale)
@@ -274,7 +275,7 @@ def generate_screenshots(locale):
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_DireWarningToast', toast_thread=DireWarningToast("This is a dire warning toast!", activation_delay=0, duration=0)),
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_ErrorToast', toast_thread=ErrorToast("This is an error toast!", activation_delay=0, duration=0)),
ScreenshotConfig(PowerOptionsView),
ScreenshotConfig(RestartView),
ScreenshotConfig(RestartView, dict(is_screenshot_renderer=True)),
ScreenshotConfig(PowerOffView),
],
"Seed Views": [
@@ -412,8 +413,16 @@ def generate_screenshots(locale):
try:
print(f"Running {screenshot_config.screenshot_name}")
try:
cur_count = screenshot_renderer.render_count
# Set up and run the target View
screenshot_config.run_callback_before()
screenshot_config.View_cls(**screenshot_config.view_kwargs).run()
if screenshot_renderer.render_count == cur_count:
# The View didn't actually render anything
raise Exception(f"{screenshot_config.screenshot_name} did not render a screenshot. Verify that its `run_screen()` is reachable by the screenshot generator.")
except ScreenshotComplete:
# The target View has run and its Screen has rendered what it needs to
if toast_thread is not None:
@@ -421,18 +430,10 @@ def generate_screenshots(locale):
controller.activate_toast(toast_thread)
while controller.toast_notification_thread.is_alive():
# Give the Toast a moment to complete its work
time.sleep(0.01)
# TODO: Necessary now that the lock is in place?
# Whenever possible, clean up toast thread HERE before killing the
# main thread with ScreenshotComplete.
toast_thread.stop()
toast_thread.join()
raise ScreenshotComplete()
except ScreenshotComplete:
# Slightly hacky way to exit ScreenshotRenderer as expected
print(f"Completed {screenshot_config.screenshot_name}")
print(f"Completed {screenshot_config.screenshot_name}")
except Exception as e:
# Something else went wrong
from traceback import print_exc
@@ -506,3 +507,5 @@ def generate_screenshots(locale):
with open(os.path.join(screenshot_root, "README.md"), 'w') as readme_file:
readme_file.write(main_readme)
print(f"Screenshots rendered: {screenshot_renderer.render_count}")
+9
View File
@@ -10,6 +10,10 @@ from seedsigner.views.view import View
class ScreenshotComplete(Exception):
"""
Slightly hacky way for the ScreenshotRenderer to intentionally break out of the
normal Controller flow in order to return control to the screenshot generator.
"""
pass
@@ -30,6 +34,8 @@ class ScreenshotRenderer(Renderer):
renderer.canvas = Image.new('RGB', (renderer.canvas_width, renderer.canvas_height))
renderer.draw = ImageDraw.Draw(renderer.canvas)
renderer.render_count = 0
def set_screenshot_filename(self, filename:str):
@@ -56,6 +62,9 @@ class ScreenshotRenderer(Renderer):
self.canvas.paste(image)
self.canvas.save(os.path.join(self.screenshot_path, self.screenshot_filename))
self.render_count += 1
# Break out of the normal Controller flow and return to the screenshot generator
raise ScreenshotComplete()
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -283,7 +283,7 @@ def test_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
# jdlcdl: these vectors created with electrum & sparrow as a 2 of 3 multisig based on BIP-39 and BIP-32 standard-path wallets
# keystore1 = 0x00*16 = 73c5da0a = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
# keystore2 = 0x11*16 = 0be174ee = 'baby mass dust captain baby mass dust captain baby mass dust casino'
# keystore3 = 0x22*16 = 8d55ff0d = 'captain baby mass dust captain baby mass dust captain baby mass dutch'
+2 -2
View File
@@ -167,14 +167,14 @@ class TestFlowTest(FlowTest):
def run(self):
self.run_screen(
ButtonListScreen,
button_data=[ButtonOption("this is fine"), "this is not"]
button_data=[ButtonOption("This is fine"), "This is not"]
)
class MyGoodButtonDataTestView(View):
def run(self):
self.run_screen(
ButtonListScreen,
button_data=[ButtonOption("this is fine"), ButtonOption("this is also fine")]
button_data=[ButtonOption("This is fine"), ButtonOption("This is also fine")]
)
return Destination(MainMenuView)
+13 -4
View File
@@ -35,7 +35,7 @@ class TestSeedFlows(FlowTest):
def test_passphrase_entry_flow(self):
"""
Opting to add a bip39 passphrase on the Finalize Seed screen should enter the
Opting to add a BIP-39 passphrase on the Finalize Seed screen should enter the
passphrase entry / review flow and end at the SeedOptionsView.
"""
self.run_sequence([
@@ -435,6 +435,9 @@ class TestSeedFlows(FlowTest):
def load_completely_wrong_qr_type_into_decoder(view: View):
view.decoder.add_data("I like cheese")
def load_recognized_qr_type_isnot_seed_into_decoder(view: View):
view.decoder.add_data("UR:CRYPTO-PSBT/HKADHEJOJKIDJYZMADAEGMAOAEAEAEADHHGLEYKGBDSBKEMTSARLVYSGIABEDELFSKWLLUDKLYESJOFDLPJPFXSOHHWZTYWEAEAEAEAEAEZCZMZMZMADBKBZJEDEHEAEAEAECMAEBBVEPFDWDMBBGSPFZTKIMYCLKPSOBDDTRDWTWPYKGAQDKNAEAEGWADAAECLTTKAXWSWTUTSNLAAEAEAEPKCTBSHGISSSRETIVEWSGYNEPTHTESNSWMLBTARYEMHTBTBTLNWSBKJLMKYNFGHLAXJYBBTTIDHKDICHAMLEHHDSRKATGDLYSBIYHNHDNEWPJKZSZMDKVETYNSGOCXKNDNBEOLFSRSNSGHAEAELAADAEAELAAEAEAELAAEADADCTKSBZJEDEHEAEAEAECMAEBBVEPFDWDMBBGSPFZTKIMYCLKPSOBDDTRDWTWPYKGAADAXAAADAEAEAECPAMAODSFTLDTYFRECSKVWYLKNBANNKKZCRTHYJTPSHLHNHKNBPDCLCFDMSOLPDKFXLFQZCSOLFSRSNSGHAEAELAADAEAELAAEAEAELAAEAEAEAEAEAEAEAEAECPAOAODSFTLDTYFRECSKVWYLKNBANNKKZCRTHYJTPSHLHNHKNBPDCLCFDMSOLPDKFXLFQZCSOLFSRSNSGHAEAELAADAEAELAAEAEAELAAEAEAEAEAEAEAEAEAELGMKFZCW")
def load_right_seed_into_decoder(view: View):
view.decoder.add_data("0000" * 11 + "0003")
@@ -446,20 +449,26 @@ class TestSeedFlows(FlowTest):
FlowStep(seed_views.SeedTranscribeSeedQRFormatView, button_data_selection=seed_views.SeedTranscribeSeedQRFormatView.STANDARD_12),
FlowStep(seed_views.SeedTranscribeSeedQRWarningView),
FlowStep(seed_views.SeedTranscribeSeedQRWholeQRView),
FlowStep(seed_views.SeedTranscribeSeedQRZoomedInView, is_redirect=True), # Live interactive screens are a bit weird; not sure why `is_redirect` is necessary here
FlowStep(seed_views.SeedTranscribeSeedQRZoomedInView),
FlowStep(seed_views.SeedTranscribeSeedQRConfirmQRPromptView, button_data_selection=seed_views.SeedTranscribeSeedQRConfirmQRPromptView.SCAN),
# Intentionally "scan" the wrong SeedQR
FlowStep(seed_views.SeedTranscribeSeedQRConfirmScanView, before_run=load_wrong_seed_into_decoder),
FlowStep(seed_views.SeedTranscribeSeedQRConfirmWrongSeedView),
FlowStep(seed_views.SeedTranscribeSeedQRZoomedInView, is_redirect=True), # Live interactive screens are still weird
FlowStep(seed_views.SeedTranscribeSeedQRZoomedInView),
# Intentionally scan QR data that makes no sense for this flow
FlowStep(seed_views.SeedTranscribeSeedQRConfirmQRPromptView, button_data_selection=seed_views.SeedTranscribeSeedQRConfirmQRPromptView.SCAN),
FlowStep(seed_views.SeedTranscribeSeedQRConfirmScanView, before_run=load_completely_wrong_qr_type_into_decoder),
FlowStep(seed_views.SeedTranscribeSeedQRConfirmInvalidQRView),
FlowStep(seed_views.SeedTranscribeSeedQRZoomedInView, is_redirect=True), # Live interactive screens are still weird
FlowStep(seed_views.SeedTranscribeSeedQRZoomedInView),
# Intentionally scan QR data that makes no sense for this flow because is another QR recognized but is not a SeedQR (e.g., bitcoin address, psbt)
FlowStep(seed_views.SeedTranscribeSeedQRConfirmQRPromptView, button_data_selection=seed_views.SeedTranscribeSeedQRConfirmQRPromptView.SCAN),
FlowStep(seed_views.SeedTranscribeSeedQRConfirmScanView, before_run=load_recognized_qr_type_isnot_seed_into_decoder),
FlowStep(seed_views.SeedTranscribeSeedQRConfirmInvalidQRView),
FlowStep(seed_views.SeedTranscribeSeedQRZoomedInView),
# Now scan the correct SeedQR
FlowStep(seed_views.SeedTranscribeSeedQRConfirmQRPromptView, button_data_selection=seed_views.SeedTranscribeSeedQRConfirmQRPromptView.SCAN),
FlowStep(seed_views.SeedTranscribeSeedQRConfirmScanView, before_run=load_right_seed_into_decoder),
+222 -5
View File
@@ -1,13 +1,223 @@
import pytest
import random
from binascii import a2b_base64
from embit import psbt
from embit import bip32
from embit.psbt import PSBT
from embit.descriptor import Descriptor
from seedsigner.models.psbt_parser import PSBTParser
from seedsigner.models.seed import Seed
from seedsigner.models.settings_definition import SettingsConstants
from psbt_testing_util import PSBTTestData, create_output
class TestPSBTParser:
"""
Exhaustively test all supported script input and output types.
"""
seed = PSBTTestData.seed
def run_basic_test(self, psbt_base64: str, change_data: str, self_transfer_data: str):
"""
Constructs a series of test psbts that use the specified `psbt_base64` for the input(s).
* A spend to each recipient type + specified `change_data`
* Self-transfer back to sender via the `self_transfer_data`
* A full spend (no change) to each recipient type
* 1 mega psbt with an output to each recipient type + specified `change_data`
"""
psbt: PSBT = PSBT.parse(a2b_base64(psbt_base64))
input_amount = sum([inp.utxo.value for inp in psbt.inputs])
recipient_amount = random.randint(200_000, 90_000_000)
fee_amount = 5_000
change_output = create_output(change_data, input_amount - recipient_amount - fee_amount)
# Spend the input(s) to each supported recipient type + change
for output in PSBTTestData.ALL_EXTERNAL_OUTPUTS:
psbt.outputs.clear()
psbt.outputs.append(create_output(output, recipient_amount))
psbt.outputs.append(change_output)
assert len(psbt.outputs) == 2
psbt_parser = PSBTParser(p=psbt, seed=self.seed, network=SettingsConstants.REGTEST)
assert psbt_parser.num_inputs == len(psbt.inputs)
assert psbt_parser.input_amount == input_amount
assert psbt_parser.num_destinations == 1
assert psbt_parser.num_change_outputs == 1
assert psbt_parser.spend_amount == recipient_amount
assert psbt_parser.change_amount == input_amount - recipient_amount - fee_amount
assert psbt_parser.fee_amount == fee_amount
assert psbt_parser.input_amount == psbt_parser.spend_amount + psbt_parser.change_amount + psbt_parser.fee_amount
# Internally cycle the input(s) back to sender via the `self_transfer_data`
psbt.outputs.clear()
psbt.outputs.append(create_output(self_transfer_data, input_amount - fee_amount))
assert len(psbt.outputs) == 1
psbt_parser = PSBTParser(p=psbt, seed=self.seed, network=SettingsConstants.REGTEST)
assert psbt_parser.num_inputs == len(psbt.inputs)
assert psbt_parser.input_amount == input_amount
assert psbt_parser.num_destinations == 0 # No external recipients == no destinations
assert psbt_parser.num_change_outputs == 1 # PSBTParser considers self-transfers == change
assert psbt_parser.spend_amount == 0 # No external recipients == nothing spent (ignores fee)
assert psbt_parser.change_amount == input_amount - fee_amount # PSBTParser considers self-transfers == change
assert psbt_parser.fee_amount == fee_amount
assert psbt_parser.input_amount == psbt_parser.spend_amount + psbt_parser.change_amount + psbt_parser.fee_amount
# Now do full spends with no change
fee_amount = random.randint(5_000, 100_000)
recipient_amount = input_amount - fee_amount
for output in PSBTTestData.ALL_EXTERNAL_OUTPUTS:
psbt.outputs.clear()
psbt.outputs.append(create_output(output, recipient_amount))
assert len(psbt.outputs) == 1
psbt_parser = PSBTParser(p=psbt, seed=self.seed, network=SettingsConstants.REGTEST)
assert psbt_parser.num_inputs == len(psbt.inputs)
assert psbt_parser.input_amount == input_amount
assert psbt_parser.num_destinations == 1
assert psbt_parser.num_change_outputs == 0
assert psbt_parser.spend_amount == recipient_amount
assert psbt_parser.change_amount == 0
assert psbt_parser.fee_amount == fee_amount
assert psbt_parser.input_amount == psbt_parser.spend_amount + psbt_parser.change_amount + psbt_parser.fee_amount
# Now try a single mega psbt with ALL the outputs at once
psbt.outputs.clear()
change_amount = input_amount - fee_amount
for output in PSBTTestData.ALL_EXTERNAL_OUTPUTS:
output_amount = random.randint(200_000, int(change_amount / 2))
psbt.outputs.append(create_output(output, output_amount))
change_amount -= output_amount
# Don't forget the change!
psbt.outputs.append(create_output(change_data, change_amount))
assert len(psbt.outputs) == len(PSBTTestData.ALL_EXTERNAL_OUTPUTS) + 1
psbt_parser = PSBTParser(p=psbt, seed=self.seed, network=SettingsConstants.REGTEST)
assert psbt_parser.num_inputs == len(psbt.inputs)
assert psbt_parser.input_amount == input_amount
assert psbt_parser.num_destinations == len(PSBTTestData.ALL_EXTERNAL_OUTPUTS)
assert psbt_parser.num_change_outputs == 1
assert psbt_parser.spend_amount == input_amount - change_amount - fee_amount
assert psbt_parser.change_amount == change_amount
assert psbt_parser.fee_amount == fee_amount
assert psbt_parser.input_amount == psbt_parser.spend_amount + psbt_parser.change_amount + psbt_parser.fee_amount
def test_singlesig_native_segwit(self):
self.run_basic_test(PSBTTestData.SINGLE_SIG_NATIVE_SEGWIT_1_INPUT, PSBTTestData.SINGLE_SIG_NATIVE_SEGWIT_CHANGE, PSBTTestData.SINGLE_SIG_NATIVE_SEGWIT_SELF_TRANSFER)
def test_singlesig_nested_segwit(self):
self.run_basic_test(PSBTTestData.SINGLE_SIG_NESTED_SEGWIT_1_INPUT, PSBTTestData.SINGLE_SIG_NESTED_SEGWIT_CHANGE, PSBTTestData.SINGLE_SIG_NESTED_SEGWIT_SELF_TRANSFER)
def test_singlesig_taproot(self):
self.run_basic_test(PSBTTestData.SINGLE_SIG_TAPROOT_1_INPUT, PSBTTestData.SINGLE_SIG_TAPROOT_CHANGE, PSBTTestData.SINGLE_SIG_TAPROOT_SELF_TRANSFER)
def test_singlesig_legacy_p2pkh(self):
self.run_basic_test(PSBTTestData.SINGLE_SIG_LEGACY_P2PKH_1_INPUT, PSBTTestData.SINGLE_SIG_LEGACY_P2PKH_CHANGE, PSBTTestData.SINGLE_SIG_LEGACY_P2PKH_SELF_TRANSFER)
def test_multisig_native_segwit(self):
self.run_basic_test(PSBTTestData.MULTISIG_NATIVE_SEGWIT_1_INPUT, PSBTTestData.MULTISIG_NATIVE_SEGWIT_CHANGE, PSBTTestData.MULTISIG_NATIVE_SEGWIT_SELF_TRANSFER)
def test_multisig_nested_segwit(self):
self.run_basic_test(PSBTTestData.MULTISIG_NESTED_SEGWIT_1_INPUT, PSBTTestData.MULTISIG_NESTED_SEGWIT_CHANGE, PSBTTestData.MULTISIG_NESTED_SEGWIT_SELF_TRANSFER)
def test_multisig_legacy_p2sh(self):
self.run_basic_test(PSBTTestData.MULTISIG_LEGACY_P2SH_1_INPUT, PSBTTestData.MULTISIG_LEGACY_P2SH_CHANGE, PSBTTestData.MULTISIG_LEGACY_P2SH_SELF_TRANSFER)
def test_has_matching_input_fingerprint(self):
"""
PSBTParser should correctly identify when a psbt contains an input that matches a
given Seed's fingerprint.
"""
wrong_seed = Seed(["bacon"] * 24)
for input in PSBTTestData.ALL_INPUTS:
psbt = PSBT.parse(a2b_base64(input))
assert PSBTParser.has_matching_input_fingerprint(psbt, PSBTTestData.seed)
assert PSBTParser.has_matching_input_fingerprint(psbt, wrong_seed) == False
# The other keys in the multisig inputs should also match
for input in PSBTTestData.MULTISIG_INPUTS:
psbt = PSBT.parse(a2b_base64(input))
assert PSBTParser.has_matching_input_fingerprint(psbt, PSBTTestData.multisig_key_2)
assert PSBTParser.has_matching_input_fingerprint(psbt, PSBTTestData.multisig_key_3)
def test_trim_and_sig_count(self):
"""
PSBTParser should correctly trim a psbt of all unnecessary data and count the number of
signatures in the psbt.
"""
output = create_output(PSBTTestData.SINGLE_SIG_NATIVE_SEGWIT_RECEIVE, 100_000)
for input in PSBTTestData.ALL_INPUTS:
psbt: PSBT = PSBT.parse(a2b_base64(input))
psbt.outputs.append(output)
psbt.sign_with(bip32.HDKey.from_seed(self.seed.seed_bytes))
assert PSBTParser.sig_count(psbt) == 1
# TODO: What can we test for before/after trimming?
PSBTParser.trim(psbt)
if input in PSBTTestData.MULTISIG_INPUTS:
psbt.sign_with(bip32.HDKey.from_seed(PSBTTestData.multisig_key_2.seed_bytes))
assert PSBTParser.sig_count(psbt) == 2
psbt.sign_with(bip32.HDKey.from_seed(PSBTTestData.multisig_key_3.seed_bytes))
assert PSBTParser.sig_count(psbt) == 3
def test_verify_multisig_output(self):
"""
PSBTParser should correctly verify multisig change and self-transfer outputs against the
provided descriptor or fail to verify if we provide the wrong descriptor.
"""
multisig_inputs = [
PSBTTestData.MULTISIG_NATIVE_SEGWIT_1_INPUT,
PSBTTestData.MULTISIG_NESTED_SEGWIT_1_INPUT,
PSBTTestData.MULTISIG_LEGACY_P2SH_1_INPUT
]
change_outputs = [
PSBTTestData.MULTISIG_NATIVE_SEGWIT_CHANGE,
PSBTTestData.MULTISIG_NESTED_SEGWIT_CHANGE,
PSBTTestData.MULTISIG_LEGACY_P2SH_CHANGE
]
self_transfer_outputs = [
PSBTTestData.MULTISIG_NATIVE_SEGWIT_SELF_TRANSFER,
PSBTTestData.MULTISIG_NESTED_SEGWIT_SELF_TRANSFER,
PSBTTestData.MULTISIG_LEGACY_P2SH_SELF_TRANSFER
]
descriptors = [
PSBTTestData.MULTISIG_NATIVE_SEGWIT_DESCRIPTOR,
PSBTTestData.MULTISIG_NESTED_SEGWIT_DESCRIPTOR,
PSBTTestData.MULTISIG_LEGACY_P2SH_DESCRIPTOR
]
for i, psbt_base64 in enumerate(multisig_inputs):
# Construct a psbt with change & self-transfer outputs of the same type as the input
psbt: PSBT = PSBT.parse(a2b_base64(psbt_base64))
psbt.outputs.append(create_output(change_outputs[i], 100_000))
psbt.outputs.append(create_output(self_transfer_outputs[i], 100_000))
psbt_parser = PSBTParser(p=psbt, seed=self.seed, network=SettingsConstants.REGTEST)
# Attempt to verify the change & self-transfer outputs using the right and wrong descriptors
for j, descriptor_str in enumerate(descriptors):
descriptor = Descriptor.from_string(descriptor_str.replace("<0;1>", "{0,1}"))
if i == j:
assert psbt_parser.verify_multisig_output(descriptor, change_num=0) == True
assert psbt_parser.verify_multisig_output(descriptor, change_num=1) == True # self-transfer is considered change
else:
assert psbt_parser.verify_multisig_output(descriptor, change_num=0) == False
assert psbt_parser.verify_multisig_output(descriptor, change_num=1) == False
# TODO: Refactor all tests to be in the TestPSBTParser class(?)
def test_p2tr_change_detection():
""" Should successfully detect change in a p2tr to p2tr psbt spend
@@ -28,7 +238,7 @@ def test_p2tr_change_detection():
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)
tx = PSBT.parse(raw)
mnemonic = "goddess rough corn exclude cream trial fee trumpet million prevent gaze power".split()
pw = ""
@@ -51,6 +261,7 @@ def test_p2tr_change_detection():
# TODO: Test no longer necessary now that we have exhaustive tests for all types above?
def test_p2sh_legacy_multisig():
"""
Should correctly parse a legacy multisig p2sh (m/45') psbt.
@@ -84,7 +295,7 @@ def test_p2sh_legacy_multisig():
descriptor = Descriptor.from_string("sh(sortedmulti(2,[0f889044/45h]tpubD8NkS3Gngj7L4FJRYrwojKhsx2seBhrNrXVdvqaUyvtVe1YDCVcziZVa9g3KouXz7FN5CkGBkoC16nmNu2HcG9ubTdtCbSW8DEXSMHmmu62/<0;1>/*,[03cd0a2b/45h]tpubD8HkLLgkdJkVitn1i9CN4HpFKJdom48iKm9PyiXYz5hivn1cGz6H3VeS6ncmCEgamvzQA2Qofu2YSTwWzvuaYWbJDEnvTUtj5R96vACdV6L/<0;1>/*,[769f695c/45h]tpubD98hRDKvtATTM8hy5Vvt5ZrvDXwJvrUZm1p1mTKDmd7FqUHY9Wj2k4X1CvxjjtTf3JoChWqYbnWjfkRJ65GQnpVJKbbMfjnGzCwoBUXafyM/<0;1>/*))#uardwtq4".replace("<0;1>", "{0,1}"))
psbt_base64 = "cHNidP8BALsCAAAAAk/6v0Yo0tvQSd45NaCoZQj0dS2RU35cF+KXp/RbBltsAAAAAAD9////HN9jZsT3CVXquPrSgGg7/H8DHsy18Ej8uCqaAo8UAsQAAAAAAP3///8DWeYAAAAAAAAXqRTsNEZFrVtk15AU60/MeTWjxGCZJIeQXwEAAAAAABepFIgB1fOQz3ajeGClCsf7Kn4BDG1Zh1DDAAAAAAAAFgAUOCnLFF5fXnue7LTJBtJne7SfW4xlCgAATwEENYfPAQPNCiuAAAAtoPXmwca4wIkJmJbT0l8IJkQoZyf1a0Hf3l3/y+P9YLsCb3zYh0WQQHK0NeKTHOh4tXmreSkeD5t+ayaPudyvWWAIA80KKy0AAIBPAQQ1h88BD4iQRIAAAC1xQDAuEKWgk+mzBHCEZ3Ibco/WRjRUB61ToV0CY2upCgMoWAP8JdgKLlkerHgciZglm2jGmPHrQqLuS8rgRqfwWQgPiJBELQAAgE8BBDWHzwF2n2lcgAAALXtkfUG4BFcO0mnNEFWpGBBvebmUn9Icjd9KVpKJF/MkA59Hw6Sxmpk0lp7SYIoBZJ8BFT3IVY9Ywu6NVn2JGfLmCHafaVwtAACAAAEAUwIAAAABLEtmpDrExA4GJ2itUuWqHQqVsr0WoamuwxKxFA+if3oDAAAAAP3///8BvIUBAAAAAAAXqRSO3FlqUGy1+B6q4UZU1uvY6aDX7YdkCgAAAQMEAQAAAAEEaVIhAhV0XDrvBSAO2pnyRtuyioVgPwb9fxQ7GwNSYKODA6XIIQKHsTdUi0B81JZaK9WASeMWb1ad2snk9iPJ8KKYGJDS+CEC6k1h+lULPMlXOd0x4bIBUwpoTr30vFfoHqr3gSKmlnlTriIGAoexN1SLQHzUllor1YBJ4xZvVp3ayeT2I8nwopgYkNL4EAPNCistAACAAAAAAAQAAAAiBgLqTWH6VQs8yVc53THhsgFTCmhOvfS8V+geqveBIqaWeRAPiJBELQAAgAAAAAAEAAAAIgYCFXRcOu8FIA7amfJG27KKhWA/Bv1/FDsbA1Jgo4MDpcgQdp9pXC0AAIAAAAAABAAAAAABAFMCAAAAASxLZqQ6xMQOBidorVLlqh0KlbK9FqGprsMSsRQPon96BAAAAAD9////ATGGAQAAAAAAF6kU7vgoQJrHpHs0uEBUzW4ogkY3VmuHYwoAAAEDBAEAAAABBGlSIQJMzyIV0BhlIAdtCFRC0nWcJ+qiowFHgStyQvx/Ov9lYSECo3z9DGK1zjn25m1n8NHEoQlcNOnsnF5UA2khAfUhxTUhA9IpGx2/u34tqOV/jRErjSguk6uQK3L743i2LgKpXB+VU64iBgJMzyIV0BhlIAdtCFRC0nWcJ+qiowFHgStyQvx/Ov9lYRADzQorLQAAgAAAAAADAAAAIgYD0ikbHb+7fi2o5X+NESuNKC6Tq5ArcvvjeLYuAqlcH5UQD4iQRC0AAIAAAAAAAwAAACIGAqN8/Qxitc459uZtZ/DRxKEJXDTp7JxeVANpIQH1IcU1EHafaVwtAACAAAAAAAMAAAAAAQBpUiEC7j3OSch6J9P+ZAcOiGeZ4Be3wS4zjzXyU6EzwixfEqQhAxzm3beiYzYmSxMsG0XD5jHoUCvBVSJtRvw41z1X+eT/IQMdnm4JRBPcOlCFGPcpryOjWzlDynm6+8Va+rYxWV5cz1OuIgIDHZ5uCUQT3DpQhRj3Ka8jo1s5Q8p5uvvFWvq2MVleXM8QA80KKy0AAIABAAAAAAAAACICAxzm3beiYzYmSxMsG0XD5jHoUCvBVSJtRvw41z1X+eT/EA+IkEQtAACAAQAAAAAAAAAiAgLuPc5JyHon0/5kBw6IZ5ngF7fBLjOPNfJToTPCLF8SpBB2n2lcLQAAgAEAAAAAAAAAAAEAaVIhAoETdqS+0tZtmj0auNDI9SxxCmUw5Iq9JJjvWjrpPGOCIQKD7KrnsR4fGz0vM67hRh17r9WznwE4JfSEJxSdJMVopyEDvLJhv9fUi2uoUAQN9AQ7fYeUFJMa/iRw2jKBYDn04zpTriICAoPsquexHh8bPS8zruFGHXuv1bOfATgl9IQnFJ0kxWinEAPNCistAACAAAAAAAUAAAAiAgKBE3akvtLWbZo9GrjQyPUscQplMOSKvSSY71o66TxjghAPiJBELQAAgAAAAAAFAAAAIgIDvLJhv9fUi2uoUAQN9AQ7fYeUFJMa/iRw2jKBYDn04zoQdp9pXC0AAIAAAAAABQAAAAAA"
raw = a2b_base64(psbt_base64)
tx = psbt.PSBT.parse(raw)
tx = PSBT.parse(raw)
# 03cd0a2b test seed
mnemonic = "better gown govern speak spawn vendor exercise item uncle odor sound cat".split()
@@ -113,6 +324,8 @@ def test_p2sh_legacy_multisig():
assert psbt_parser.verify_multisig_output(descriptor, 1)
# TODO: Test no longer necessary now that we have exhaustive tests for all types above?
def test_p2sh_p2wpkh_nested_segwit():
"""
Should correctly parse a nested segwit (m/49'/1'/0') psbt.
@@ -140,7 +353,7 @@ def test_p2sh_p2wpkh_nested_segwit():
descriptor = Descriptor.from_string("sh(wpkh([c751dc07/49h/1h/0h]tpubDDS23bf7c9mdfWpuvA61HHCYDusq25UtMNYsFagKPNMNWHSm8bvwmNNP2KSpivN3gQWAK8fhDFk3dzgoBn9rPoMncKxJuqNAv7sJMShbZ6i/<0;1>/*))#7sn8gf37".replace("<0;1>", "{0,1}"))
psbt_base64 = "cHNidP8BAH4CAAAAAXfY5crHl+bXtTvKvdo2MaFQeIXw+P+3kzZwBRgw84lFAQAAAAD9////AhjaAAAAAAAAF6kUSop8lEmO4FB1AyV1GJe2bygA7ASHSGsBAAAAAAAiACCHttDIHkeECumMJgZ2643Hhd8rXFnv0ZbDSYM8esN9UIouEwBPAQQ1h88Dv3UWAIAAAACfHgAYuw3ODwXCSP0valI9edAB1t3EInR2TXkbOd+F+AJgmJs8XUkZD5zQAgd3+/ijOqVphlWUMzxDnRorBQYEgxDHUdwHMQAAgAEAAIAAAACAAAEBIBFGAgAAAAAAF6kU7ijES3iWT8u0+44/blPlLfh9WkyHAQMEAQAAAAEEFgAUX7JspW1r0gC+WkUHwGABJ8DU9f8iBgO1/adRC+r8XJ/bjnfdwk3740n0m8gE3+xN8GHsNrxDUxjHUdwHMQAAgAEAAIAAAACAAQAAAAAAAAAAAQAWABT8V9vY29XR8niVYdVSF9H4zRTAbiICArH6DjPShnzXiaAnc2BR1f61QQliH0BOhqAvksByf3e9GMdR3AcxAACAAQAAgAAAAIABAAAAAQAAAAAA"
raw = a2b_base64(psbt_base64)
tx = psbt.PSBT.parse(raw)
tx = PSBT.parse(raw)
# 03cd0a2b test seed
mnemonic = "goddess rough corn exclude cream trial fee trumpet million prevent gaze power".split()
@@ -159,6 +372,10 @@ def test_p2sh_p2wpkh_nested_segwit():
assert psbt_parser.get_change_data(0)['address'] == '2Mz3MthXyM4YDjLPw1V4PAacKt4pD8Cz8N3'
assert psbt_parser.get_change_data(0)["amount"] == 55832
# We should be able to verify the change addr
assert psbt_parser.verify_multisig_output(descriptor, 0)
def test_parse_op_return_content():
"""
@@ -177,7 +394,7 @@ def test_parse_op_return_content():
psbt_base64 = "cHNidP8BAIYCAAAAATpQ10o+gKdZ8ThpKsbfHiHYn3NhvUrQ5DvW0ZWX8jKLAAAAAAD9////AujC9QUAAAAAFgAUY61+2BcXt+tsWoxV1nVw20kVb1UAAAAAAAAAACtqTChDaGFuY2VsbG9yIG9uIHRoZSBicmluayBvZiB0aGlyZCBiYWlsb3V0aQAAAE8BBDWHzwNXmUmVgAAAANRFa7R5gYD84Wbha3d1QnjgfYPOBw87on6cXS32WoyqAsPFtPxB7PRTdbujUnBPUVDh9YUBtwrl4nc0OcRNGvIyEA+4gv9UAACAAQAAgAAAAIAAAQB0AgAAAAGNFK/1X0fP5q+nu5XX7Tk2VRa0EL+jkGI9CHiJvsjZCgAAAAAA/f///wKMw/UFAAAAABYAFIpZMNnUU6cQt8Q0YpZ0pnvsSA5fAAAAAAAAAAAZakwWYml0Y29pbiBpcyBmcmVlIHNwZWVjaGgAAAABAR+Mw/UFAAAAABYAFIpZMNnUU6cQt8Q0YpZ0pnvsSA5fAQMEAQAAACIGAvxDI0eNI1oQ2AU69R7A0jf+hUdilWCgrWHgdzkqlaXMGA+4gv9UAACAAQAAgAAAAIAAAAAAAQAAAAAiAgK9qKtzGWyiRrpmupdA99NVLriz3GQy6cENbyD19sfl/hgPuIL/VAAAgAEAAIAAAACAAAAAAAIAAAAAAA=="
raw = a2b_base64(psbt_base64)
tx = psbt.PSBT.parse(raw)
tx = PSBT.parse(raw)
mnemonic = "model ensure search plunge galaxy firm exclude brain satoshi meadow cable roast".split()
pw = ""