diff --git a/.coveragerc b/.coveragerc
deleted file mode 100644
index 73637522..00000000
--- a/.coveragerc
+++ /dev/null
@@ -1,40 +0,0 @@
-[run]
-branch = True
-
-[report]
-skip_empty = True
-skip_covered = True
-
-# Omit; need a different approach to test modules with hardware dependencies
-omit =
- */__init__.py
- */tests/*
- */pyzbar/*
- */gui/*
-
-# Regexes for lines to exclude from consideration
-exclude_lines =
- # Have to re-enable the standard pragma
- pragma: no cover
-
- # Don't complain about missing debug-only code:
- def __repr__
- def __str__
- if self\.debug
-
- # Don't complain if tests don't hit defensive assertion code:
- raise AssertionError
- raise NotImplementedError
-
- # Don't complain if non-runnable code isn't run:
- if 0:
- if __name__ == .__main__.:
-
- # Don't complain about abstract methods, they aren't run:
- @(abc\.)?abstractmethod
-
-
-[html]
-directory = coverage_html_report
-skip_empty = True
-skip_covered = False
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index cb7c0575..294e20be 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -21,7 +21,9 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ["3.10"]
+ # 3.10: currently used by Seedsigner
+ # 3.12: latest stable Python as upper test bound
+ python-version: ["3.10", "3.12"]
steps:
- uses: actions/checkout@v3
@@ -34,7 +36,7 @@ jobs:
sudo apt-get install libzbar0
python -m pip install --upgrade pip
pip install -r requirements.txt -r tests/requirements.txt
- python setup.py install
+ pip install .
- name: Test with pytest
run: |
mkdir artifacts
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 4b597e4e..00000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1 +0,0 @@
-recursive-include src/seedsigner/resources *
diff --git a/README.md b/README.md
index 318ff485..59fe0d80 100644
--- a/README.md
+++ b/README.md
@@ -44,6 +44,7 @@ If you have specific questions about the project, our [Telegram Group](https://t
* Optimized seed word entry interface
* Support for Bitcoin Mainnet & Testnet
* Support for custom user-defined derivation paths
+* Support for loading Electrum Segwit seed phrases with feature limitations: [Electrum support info](docs/electrum.md)
* On-demand receive address verification
* Address Explorer for single sig and multisig wallets
* User-configurable QR code display density
diff --git a/docs/electrum.md b/docs/electrum.md
new file mode 100644
index 00000000..6c07ca31
--- /dev/null
+++ b/docs/electrum.md
@@ -0,0 +1,15 @@
+# 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.
+
+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:
+
+- BIP-85 child seeds
+ - Not applicable for Electrum seed types
+- 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
+ - User-chosen custom derivations are thus not supported for Electrum seeds
diff --git a/docs/seed_qr/README.md b/docs/seed_qr/README.md
index 1d2a1a10..40beaf8d 100644
--- a/docs/seed_qr/README.md
+++ b/docs/seed_qr/README.md
@@ -1,8 +1,8 @@
# SeedQR Format Specification
-[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 Bitcoin private key each time they use it.
+[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 private key 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.
@@ -20,7 +20,7 @@ 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 private key is to store it as a [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) mnemonic seed phrase that consists of 12 or 24 words.
+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.
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.
@@ -150,7 +150,7 @@ But here the unit being described isn't alphanumeric characters or numeric digit
1 byte = 8 bits
```
-Rather than having the QR format interpret our data as numbers or characters, we can directly encode the relevant bits that determine our Bitcoin private key.
+Rather than having the QR format interpret our data as numbers or characters, we can directly encode the relevant bits that determine our mnemonic seed phrase.
We can extract exactly those bits from our mnemonic seed phrase digit stream that we generated above.
@@ -311,7 +311,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 Bitcoin private key. But using a QR code as your key loader--or even as your permanent backup etched in metal--has other advantages.
+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.
diff --git a/enclosures/pushcase/3mf/Bottom - Push Case - SeedSigner.3mf b/enclosures/pushcase/3mf/Bottom - Push Case - SeedSigner.3mf
new file mode 100644
index 00000000..c8303e29
Binary files /dev/null and b/enclosures/pushcase/3mf/Bottom - Push Case - SeedSigner.3mf differ
diff --git a/enclosures/pushcase/3mf/Buttons - Push Case - SeedSigner.3mf b/enclosures/pushcase/3mf/Buttons - Push Case - SeedSigner.3mf
new file mode 100644
index 00000000..07f40bfc
Binary files /dev/null and b/enclosures/pushcase/3mf/Buttons - Push Case - SeedSigner.3mf differ
diff --git a/enclosures/pushcase/3mf/Middle - Push Case - SeedSigner.3mf b/enclosures/pushcase/3mf/Middle - Push Case - SeedSigner.3mf
new file mode 100644
index 00000000..88fb6ced
Binary files /dev/null and b/enclosures/pushcase/3mf/Middle - Push Case - SeedSigner.3mf differ
diff --git a/enclosures/pushcase/3mf/Soldering Support - Push Case - SeedSigner.3mf b/enclosures/pushcase/3mf/Soldering Support - Push Case - SeedSigner.3mf
new file mode 100644
index 00000000..bafe0dd1
Binary files /dev/null and b/enclosures/pushcase/3mf/Soldering Support - Push Case - SeedSigner.3mf differ
diff --git a/enclosures/pushcase/3mf/Top - Push Case - SeedSigner.3mf b/enclosures/pushcase/3mf/Top - Push Case - SeedSigner.3mf
new file mode 100644
index 00000000..38a0c50a
Binary files /dev/null and b/enclosures/pushcase/3mf/Top - Push Case - SeedSigner.3mf differ
diff --git a/enclosures/pushcase/README.md b/enclosures/pushcase/README.md
new file mode 100644
index 00000000..0f26b4fb
--- /dev/null
+++ b/enclosures/pushcase/README.md
@@ -0,0 +1,130 @@
+# The Push Case
+
+
+
+## Motivation
+
+The motivation for this design was to get a thin, screwless and good looking enclosure with minimal secondary hardware requirements.
+
+## Characterisics
+
+- No screws needed
+- Soldering required
+- Mini-HDMI port not accessible
+- Removeable memory card accessible
+- Data-enabled USB port not accessible
+
+## Hardware restrictions
+
+- Raspberry Pi Zero without pre-installed GPIO pins required.
+- Raspberry Pi Camera in smaller "Zero" style version required.
+
+## Secondary hardware requirements
+
+- 40x pins (6mm / 0.04in)
+- Soldering iron and solder
+
+## 3D prints and screwless design
+
+Please note that the prints must be accurate in order to fit tightly without screws and for the buttons to perform smoothly. The prints are well tested with a Prusa MK3s (.3mf files attached), Prusament PLA and hardware components from early 2024. With this setup, the enclosure holds strongly together, but can still be opened again with a little effort (although this is not recommended, as it may not hold together as tightly afterwards). Other printers or newer or older hardware may produce results that do not fit and in worst case damage the hardware.
+
+## Known issues
+
+- Bambu Studio Slicer: There is a strange problem when slicing the buttons. A few layers are missing. As a workaround you can use another slicer, e.g. the Prusa Slicer.
+
+## Assembly
+
+### 1. Get all the parts
+
+
+
+- Raspberry Pi Zero v.1.3
+- Waveshare LCD Display (240x240)
+- Camera for Raspberry Pi Zero
+- 40 pins (6mm / 0.04in, cut them to size if you can only find larger ones)
+- Soldering iron and solder (not on picture)
+- Soldering support (3D print)
+- Top, middle, back and buttons (3D prints)
+- Tweezers (if available - not on picture)
+
+### 2. Place the display and the Raspberry Pi on the soldering support
+
+
+
+
+
+
+
+
+- Remove the screen protection of the LCD display.
+- Place the display face down on the soldering support.
+- Place the middle support on the display.
+- Place the Raspberry Pi face down on the middle support.
+
+### 3. Solder the pins
+
+
+
+
+
+
+
+- Insert the 40 pins with the help of tweezers and push them all the way down.
+- Solder the pins (please pay attention not to melt the orange support elements!).
+- Optional: Pull out the Raspberry Pi to check that the pins are holding properly after soldering.
+
+### 4. Attach camera
+
+
+
+
+
+
+
+- Carefully pull out the camera cable mount (ideally with the support of a tool).
+- Plug in the camera cable (it doesn't go very deep).
+- Close the camera cable mount to fix the cable.
+
+### 5. Stick on the back part
+
+
+
+
+
+
+
+- Remove the camera protection.
+- Bend the camera cable into position (do not use the adhesive tape!).
+- Place the back part on the signer, but do not push it in completely yet.
+- Align the camera precisely with the help of a tool.
+- Push the back part in completely (the camera should fit perfectly and have no more play).
+
+### 6. Stick on the front part
+
+
+
+
+
+
+
+
+- Remove the soldering support.
+- Insert the 3 buttons into the front.
+- Press the front completely into the signer (hold the device upside down so that the buttons do not fall out).
+- Plug in the thumbstick.
+
+### 7. Flash a MicroSD card
+
+- Please follow the documentation on [SeedSigner.com](https://seedsigner.com).
+
+## Contact
+
+Get in touch if you have any questions or suggestions. Success stories are also welcome. :) You can reach me on [X (@kayth21)](https://x.com/kayth21) and [Telegram (@Kayth_21)](https://t.me/kayth_21).
+
+## Donation
+
+If you like my work and want to honor it, I appreciate any small donation to my Lightning Address kayth@blink.sv. But even more important, please consider a donation to the [Seedsigner Development Fund](https://seedsigner.com/donate-now/).
+
+## Shop
+
+You can also get a push case, or a fully assembled SeedSigner, from my store [bitcoin-store.org](https://bitcoin-store.org).
\ No newline at end of file
diff --git a/enclosures/pushcase/images/back1.png b/enclosures/pushcase/images/back1.png
new file mode 100644
index 00000000..2bbd3675
Binary files /dev/null and b/enclosures/pushcase/images/back1.png differ
diff --git a/enclosures/pushcase/images/back2.png b/enclosures/pushcase/images/back2.png
new file mode 100644
index 00000000..b5852250
Binary files /dev/null and b/enclosures/pushcase/images/back2.png differ
diff --git a/enclosures/pushcase/images/camera1.png b/enclosures/pushcase/images/camera1.png
new file mode 100644
index 00000000..3c2316ce
Binary files /dev/null and b/enclosures/pushcase/images/camera1.png differ
diff --git a/enclosures/pushcase/images/camera2.png b/enclosures/pushcase/images/camera2.png
new file mode 100644
index 00000000..240f082c
Binary files /dev/null and b/enclosures/pushcase/images/camera2.png differ
diff --git a/enclosures/pushcase/images/camera3.png b/enclosures/pushcase/images/camera3.png
new file mode 100644
index 00000000..149f7601
Binary files /dev/null and b/enclosures/pushcase/images/camera3.png differ
diff --git a/enclosures/pushcase/images/camera4.png b/enclosures/pushcase/images/camera4.png
new file mode 100644
index 00000000..17e79cda
Binary files /dev/null and b/enclosures/pushcase/images/camera4.png differ
diff --git a/enclosures/pushcase/images/camera5.png b/enclosures/pushcase/images/camera5.png
new file mode 100644
index 00000000..3c642afe
Binary files /dev/null and b/enclosures/pushcase/images/camera5.png differ
diff --git a/enclosures/pushcase/images/components.png b/enclosures/pushcase/images/components.png
new file mode 100644
index 00000000..01603d24
Binary files /dev/null and b/enclosures/pushcase/images/components.png differ
diff --git a/enclosures/pushcase/images/front1.png b/enclosures/pushcase/images/front1.png
new file mode 100644
index 00000000..4bd4ac78
Binary files /dev/null and b/enclosures/pushcase/images/front1.png differ
diff --git a/enclosures/pushcase/images/front2.png b/enclosures/pushcase/images/front2.png
new file mode 100644
index 00000000..4fc4547f
Binary files /dev/null and b/enclosures/pushcase/images/front2.png differ
diff --git a/enclosures/pushcase/images/front3.png b/enclosures/pushcase/images/front3.png
new file mode 100644
index 00000000..2d6ecbc8
Binary files /dev/null and b/enclosures/pushcase/images/front3.png differ
diff --git a/enclosures/pushcase/images/front4.png b/enclosures/pushcase/images/front4.png
new file mode 100644
index 00000000..6fe04dd7
Binary files /dev/null and b/enclosures/pushcase/images/front4.png differ
diff --git a/enclosures/pushcase/images/pushcase.png b/enclosures/pushcase/images/pushcase.png
new file mode 100644
index 00000000..50881b35
Binary files /dev/null and b/enclosures/pushcase/images/pushcase.png differ
diff --git a/enclosures/pushcase/images/solder1.png b/enclosures/pushcase/images/solder1.png
new file mode 100644
index 00000000..adf42626
Binary files /dev/null and b/enclosures/pushcase/images/solder1.png differ
diff --git a/enclosures/pushcase/images/solder2.png b/enclosures/pushcase/images/solder2.png
new file mode 100644
index 00000000..7321b060
Binary files /dev/null and b/enclosures/pushcase/images/solder2.png differ
diff --git a/enclosures/pushcase/images/solder3.png b/enclosures/pushcase/images/solder3.png
new file mode 100644
index 00000000..82640eb5
Binary files /dev/null and b/enclosures/pushcase/images/solder3.png differ
diff --git a/enclosures/pushcase/images/stack1.png b/enclosures/pushcase/images/stack1.png
new file mode 100644
index 00000000..5a34e05f
Binary files /dev/null and b/enclosures/pushcase/images/stack1.png differ
diff --git a/enclosures/pushcase/images/stack2.png b/enclosures/pushcase/images/stack2.png
new file mode 100644
index 00000000..af4482bb
Binary files /dev/null and b/enclosures/pushcase/images/stack2.png differ
diff --git a/enclosures/pushcase/images/stack3.png b/enclosures/pushcase/images/stack3.png
new file mode 100644
index 00000000..04fef4f8
Binary files /dev/null and b/enclosures/pushcase/images/stack3.png differ
diff --git a/enclosures/pushcase/images/stack4.png b/enclosures/pushcase/images/stack4.png
new file mode 100644
index 00000000..a5025c66
Binary files /dev/null and b/enclosures/pushcase/images/stack4.png differ
diff --git a/enclosures/pushcase/stl/Bottom - Push Case - SeedSigner.stl b/enclosures/pushcase/stl/Bottom - Push Case - SeedSigner.stl
new file mode 100644
index 00000000..9f885b17
Binary files /dev/null and b/enclosures/pushcase/stl/Bottom - Push Case - SeedSigner.stl differ
diff --git a/enclosures/pushcase/stl/Button - Push Case - SeedSigner.stl b/enclosures/pushcase/stl/Button - Push Case - SeedSigner.stl
new file mode 100644
index 00000000..bbaf8113
Binary files /dev/null and b/enclosures/pushcase/stl/Button - Push Case - SeedSigner.stl differ
diff --git a/enclosures/pushcase/stl/Middle - Push Case - SeedSigner.stl b/enclosures/pushcase/stl/Middle - Push Case - SeedSigner.stl
new file mode 100644
index 00000000..410ddb81
Binary files /dev/null and b/enclosures/pushcase/stl/Middle - Push Case - SeedSigner.stl differ
diff --git a/enclosures/pushcase/stl/Soldering Support - Push Case - SeedSigner.stl b/enclosures/pushcase/stl/Soldering Support - Push Case - SeedSigner.stl
new file mode 100644
index 00000000..60d1adbf
Binary files /dev/null and b/enclosures/pushcase/stl/Soldering Support - Push Case - SeedSigner.stl differ
diff --git a/enclosures/pushcase/stl/Thumbstick - Push Case - SeedSigner.stl b/enclosures/pushcase/stl/Thumbstick - Push Case - SeedSigner.stl
new file mode 100644
index 00000000..1488ad3c
Binary files /dev/null and b/enclosures/pushcase/stl/Thumbstick - Push Case - SeedSigner.stl differ
diff --git a/enclosures/pushcase/stl/Top - Push Case - SeedSigner.stl b/enclosures/pushcase/stl/Top - Push Case - SeedSigner.stl
new file mode 100644
index 00000000..36f19ff7
Binary files /dev/null and b/enclosures/pushcase/stl/Top - Push Case - SeedSigner.stl differ
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..002bfe2c
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,69 @@
+[build-system]
+build-backend = "setuptools.build_meta"
+requires = ["setuptools", "wheel"]
+
+[project]
+authors = [{name = "SeedSigner", email = "author@example.com"}]
+classifiers = [
+ "Programming Language :: Python :: 3",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent"
+]
+description = "Build an offline, airgapped Bitcoin signing device for less than $50!"
+name = "seedsigner"
+readme = "README.md"
+requires-python = ">=3.10"
+version = "0.8.0-rc1"
+
+[project.urls]
+"Bug Tracker" = "https://github.com/SeedSigner/seedsigner/issues"
+Homepage = "https://seedsigner.com/"
+Repository = "https://github.com/SeedSigner/seedsigner"
+
+[tool.coverage.html]
+directory = "coverage_html_report"
+skip_covered = false
+skip_empty = true
+
+[tool.coverage.report]
+# Regexes for lines to exclude from consideration
+exclude_lines = [
+ # Have to re-enable the standard pragma
+ "pragma: no cover",
+ # Don't complain about missing debug-only code:
+ "def __repr__",
+ "def __str__",
+ "if self\\.debug",
+ # Don't complain if tests don't hit defensive assertion code:
+ "raise AssertionError",
+ "raise NotImplementedError",
+ # Don't complain if non-runnable code isn't run:
+ "if 0:",
+ "if __name__ == .__main__.:",
+ # Don't complain about abstract methods, they aren't run:
+ "@(abc\\.)?abstractmethod"
+]
+# Omit; need a different approach to test modules with hardware dependencies
+omit = [
+ "*/__init__.py",
+ "*/tests/*",
+ "*/pyzbar/*",
+ "*/gui/*"
+]
+skip_covered = true
+skip_empty = true
+
+[tool.coverage.run]
+branch = true
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+
+[tool.setuptools]
+include-package-data = true
+
+[tool.setuptools.package-data]
+"seedsigner.resources" = ["**"]
+
+[tool.setuptools.packages.find]
+where = ["src"]
diff --git a/pytest.ini b/pytest.ini
deleted file mode 100644
index de19c9f0..00000000
--- a/pytest.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-[pytest]
-testpaths = tests
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index f86231b0..89a1a589 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
-embit==0.7.0
-Pillow==9.4.0
+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
diff --git a/setup.py b/setup.py
deleted file mode 100644
index deb7c594..00000000
--- a/setup.py
+++ /dev/null
@@ -1,27 +0,0 @@
-import setuptools
-
-with open("README.md", "r", encoding="utf-8") as fh:
- long_description = fh.read()
-
-setuptools.setup(
- name="seedsigner",
- version="0.7.0",
- author="SeedSigner",
- author_email="author@example.com",
- description="Build an offline, airgapped Bitcoin signing device for less than $50!",
- long_description=long_description,
- long_description_content_type="text/markdown",
- url="https://github.com/SeedSigner/seedsigner",
- project_urls={
- "Bug Tracker": "https://github.com/SeedSigner/seedsigner/issues",
- },
- classifiers=[
- "Programming Language :: Python :: 3",
- "License :: OSI Approved :: MIT License",
- "Operating System :: OS Independent",
- ],
- package_dir={"": "src"},
- packages=setuptools.find_packages(where="src"),
- include_package_data=True,
- python_requires=">=3.10",
-)
diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py
index f0a95287..37d80e47 100644
--- a/src/seedsigner/controller.py
+++ b/src/seedsigner/controller.py
@@ -99,7 +99,7 @@ class Controller(Singleton):
rather than at the top in order avoid circular imports.
"""
- VERSION = "0.7.0"
+ VERSION = "0.8.0-rc1"
# Declare class member vars with type hints to enable richer IDE support throughout
# the code.
diff --git a/src/seedsigner/gui/screens/psbt_screens.py b/src/seedsigner/gui/screens/psbt_screens.py
index 330742d5..502c3921 100644
--- a/src/seedsigner/gui/screens/psbt_screens.py
+++ b/src/seedsigner/gui/screens/psbt_screens.py
@@ -1,4 +1,5 @@
from dataclasses import dataclass
+import math
from PIL import Image, ImageDraw, ImageFilter
from typing import List
import time
@@ -21,6 +22,7 @@ class PSBTOverviewScreen(ButtonListScreen):
num_self_transfer_outputs: int = 0
num_change_outputs: int = 0
destination_addresses: List[str] = None
+ has_op_return: bool = False
def __post_init__(self):
@@ -143,6 +145,9 @@ class PSBTOverviewScreen(ButtonListScreen):
destination_column.append(f"fee")
+ if self.has_op_return:
+ destination_column.append("OP_RETURN")
+
if self.num_change_outputs > 0:
for i in range(0, self.num_change_outputs):
destination_column.append("change")
@@ -681,6 +686,57 @@ class PSBTChangeDetailsScreen(ButtonListScreen):
+@dataclass
+class PSBTOpReturnScreen(ButtonListScreen):
+ op_return_data: bytes = None
+
+ def __post_init__(self):
+ # Customize defaults
+ self.is_bottom_list = True
+
+ super().__post_init__()
+
+ try:
+ # Simple case: display human-readable text
+ self.components.append(TextArea(
+ text=self.op_return_data.decode(errors="strict"), # "strict" is a good enough heuristic to decide if it's human readable
+ font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE,
+ is_text_centered=True,
+ allow_text_overflow=True,
+ screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
+ height=self.buttons[0].screen_y - self.top_nav.height - 2*GUIConstants.COMPONENT_PADDING,
+ ))
+ return
+ except UnicodeDecodeError:
+ # Contains data that can't be converted to UTF-8; probably encoded and not
+ # meant to be human readable.
+ font = Fonts.get_font(GUIConstants.FIXED_WIDTH_FONT_NAME, size=GUIConstants.BODY_FONT_SIZE)
+ (left, top, right, bottom) = font.getbbox("X", anchor="ls")
+ chars_per_line = int((self.canvas_width - 2*GUIConstants.EDGE_PADDING) / (right - left))
+ decoded_str = self.op_return_data.hex()
+ num_lines = math.ceil(len(decoded_str) / chars_per_line)
+ text = ""
+ for i in range(num_lines):
+ text += (decoded_str[i*chars_per_line:(i+1)*chars_per_line]) + "\n"
+ text = text[:-1]
+
+ label = TextArea(
+ text="raw hex data",
+ font_color=GUIConstants.LABEL_FONT_COLOR,
+ font_size=GUIConstants.LABEL_FONT_SIZE,
+ screen_y=self.top_nav.height,
+ )
+ self.components.append(label)
+
+ self.components.append(TextArea(
+ text=text,
+ font_name=GUIConstants.FIXED_WIDTH_FONT_NAME,
+ font_size=GUIConstants.BODY_FONT_SIZE,
+ screen_y=label.screen_y + label.height + GUIConstants.COMPONENT_PADDING,
+ ))
+
+
+
@dataclass
class PSBTFinalizeScreen(ButtonListScreen):
def __post_init__(self):
diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py
index a4b3a742..a77997ab 100644
--- a/src/seedsigner/gui/screens/screen.py
+++ b/src/seedsigner/gui/screens/screen.py
@@ -670,7 +670,7 @@ class QRDisplayScreen(BaseScreen):
self.tips_start_time = tips_start_time
- def add_brightness_tips(self, image: Image.Image) -> None:
+ def render_brightness_tip(self, image: Image.Image) -> None:
# TODO: Refactor ToastOverlay to support two lines of icon + text and use
# that instead of this more manual approach.
@@ -745,19 +745,29 @@ class QRDisplayScreen(BaseScreen):
from seedsigner.models.settings import Settings
settings = Settings.get_instance()
cur_brightness_setting = settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS_TIPS)
- show_brightness_tips = cur_brightness_setting == SettingsConstants.OPTION__ENABLED
+ is_brightness_tip_enabled = cur_brightness_setting == SettingsConstants.OPTION__ENABLED
+ pending_encoder_restart = False
# Loop whether the QR is a single frame or animated; each loop might adjust
# brightness setting.
while self.keep_running:
# convert the self.qr_brightness integer (31-255) into hex triplets
hex_color = (hex(self.qr_brightness.cur_count).split('x')[1]) * 3
- image = self.qr_encoder.next_part_image(240, 240, border=2, background_color=hex_color)
# Display the brightness tips toast
duration = 10 ** 9 * 1.2 # 1.2 seconds
- if show_brightness_tips and time.time_ns() - self.tips_start_time.cur_count < duration:
- self.add_brightness_tips(image)
+ if is_brightness_tip_enabled and time.time_ns() - self.tips_start_time.cur_count < duration:
+ image = self.qr_encoder.part_to_image(self.qr_encoder.cur_part(), 240, 240, border=2, background_color=hex_color)
+ self.render_brightness_tip(image)
+ pending_encoder_restart = True
+ else:
+ # Only advance the QR animation when the brightness tip is not displayed
+ if pending_encoder_restart:
+ # Animated QRs should restart their frame sequence after the
+ # brightness tip is stowed.
+ self.qr_encoder.restart()
+ pending_encoder_restart = False
+ image = self.qr_encoder.next_part_image(240, 240, border=2, background_color=hex_color)
with self.renderer.lock:
self.renderer.show_image(image)
diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py
index f657716d..c552a377 100644
--- a/src/seedsigner/gui/screens/seed_screens.py
+++ b/src/seedsigner/gui/screens/seed_screens.py
@@ -1512,6 +1512,8 @@ class SeedSignMessageConfirmMessageScreen(ButtonListScreen):
end_y = renderer.canvas_height - GUIConstants.EDGE_PADDING - GUIConstants.BUTTON_HEIGHT - GUIConstants.COMPONENT_PADDING
message_height = end_y - start_y
+ # TODO: Pass the full message in from the View so that this Screen doesn't need to
+ # interact with the Controller here.
self.sign_message_data = Controller.get_instance().sign_message_data
if "paged_message" not in self.sign_message_data:
paged = reflow_text_into_pages(
diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py
index c368c05e..269a8f89 100644
--- a/src/seedsigner/models/encode_qr.py
+++ b/src/seedsigner/models/encode_qr.py
@@ -85,16 +85,15 @@ class BaseStaticQrEncoder(BaseQrEncoder):
@dataclass
class SeedQrEncoder(BaseStaticQrEncoder):
- mnemonic: List[str] = None
- wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
+ seed : Seed = None
def __post_init__(self):
- self.wordlist = Seed.get_wordlist(self.wordlist_language_code)
+ self.wordlist = Seed.get_wordlist(self.seed.wordlist_language_code)
super().__post_init__()
self.data = ""
# Output as Numeric data format
- for word in self.mnemonic:
+ for word in self.seed.mnemonic_list:
index = self.wordlist.index(word)
self.data += str("%04d" % index)
@@ -109,18 +108,18 @@ class CompactSeedQrEncoder(SeedQrEncoder):
def next_part(self):
# Output as binary data format
binary_str = ""
- for word in self.mnemonic:
+ for word in self.seed.mnemonic_list:
index = self.wordlist.index(word)
# Convert index to binary, strip out '0b' prefix; zero-pad to 11 bits
binary_str += bin(index).split('b')[1].zfill(11)
# We can exclude the checksum bits at the end
- if len(self.mnemonic) == 24:
+ if len(self.seed.mnemonic_list) == 24:
# 8 checksum bits in a 24-word seed
binary_str = binary_str[:-8]
- elif len(self.mnemonic) == 12:
+ elif len(self.seed.mnemonic_list) == 12:
# 4 checksum bits in a 12-word seed
binary_str = binary_str[:-4]
@@ -149,22 +148,14 @@ class BaseXpubQrEncoder(BaseQrEncoder):
"""
Base Xpub QrEncoder for static and animated formats
"""
- mnemonic: list = None
- passphrase: str = None
+ seed: Seed = None
derivation: str = None
network: str = SettingsConstants.MAINNET
- wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
+ sig_type : str = None
def prep_xpub(self):
- self.wordlist = Seed.get_wordlist(self.wordlist_language_code)
-
- if self.wordlist == None:
- raise Exception('Wordlist Required')
- version = bip32.detect_version(self.derivation, default="xpub", network=NETWORKS[SettingsConstants.map_network_to_embit(self.network)])
- self.seed = Seed(mnemonic=self.mnemonic,
- passphrase=self.passphrase,
- wordlist_language_code=self.wordlist_language_code)
+ version = self.seed.detect_version(self.derivation, self.network, self.sig_type)
self.root = bip32.HDKey.from_seed(self.seed.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]["xprv"])
self.fingerprint = self.root.child(0).fingerprint
self.xprv = self.root.derive(self.derivation)
diff --git a/src/seedsigner/models/psbt_parser.py b/src/seedsigner/models/psbt_parser.py
index a20c13cc..8faba3fe 100644
--- a/src/seedsigner/models/psbt_parser.py
+++ b/src/seedsigner/models/psbt_parser.py
@@ -10,6 +10,11 @@ from seedsigner.models.seed import Seed
from seedsigner.models.settings import SettingsConstants
+class OPCODES:
+ OP_RETURN = 106
+ OP_PUSHDATA1 = 76
+
+
class PSBTParser():
def __init__(self, p: PSBT, seed: Seed, network: str = SettingsConstants.MAINNET):
@@ -26,6 +31,7 @@ class PSBTParser():
self.num_inputs = 0
self.destination_addresses = []
self.destination_amounts = []
+ self.op_return_data: bytes = None
self.root = None
@@ -169,7 +175,11 @@ class PSBTParser():
if sc.data == self.psbt.tx.vout[i].script_pubkey.data:
is_change = True
- if is_change:
+ if self.psbt.tx.vout[i].script_pubkey.data[0] == OPCODES.OP_RETURN:
+ # The data is written as: OP_RETURN + OP_PUSHDATA1 + len(payload) + payload
+ self.op_return_data = self.psbt.tx.vout[i].script_pubkey.data[3:]
+
+ elif is_change:
addr = self.psbt.tx.vout[i].script_pubkey.address(NETWORKS[SettingsConstants.map_network_to_embit(self.network)])
fingerprints = []
derivation_paths = []
@@ -252,21 +262,22 @@ class PSBTParser():
# expected multisig
script = None
- if "p2wsh" in script_type and scope.witness_script is not None:
- script = scope.witness_script
+ if script_type:
+ if "p2wsh" in script_type and scope.witness_script is not None:
+ script = scope.witness_script
- elif "p2sh" in script_type and scope.redeem_script is not None:
- script = scope.redeem_script
+ elif "p2sh" in script_type and scope.redeem_script is not None:
+ script = scope.redeem_script
- if script is not None:
- m, n, pubkeys = PSBTParser._parse_multisig(script)
-
- # check pubkeys are derived from cosigners
- try:
- cosigners = PSBTParser._get_cosigners(pubkeys, scope.bip32_derivations, xpubs)
- policy.update({"m": m, "n": n, "cosigners": cosigners})
- except:
- policy.update({"m": m, "n": n})
+ if script is not None:
+ m, n, pubkeys = PSBTParser._parse_multisig(script)
+
+ # check pubkeys are derived from cosigners
+ try:
+ cosigners = PSBTParser._get_cosigners(pubkeys, scope.bip32_derivations, xpubs)
+ policy.update({"m": m, "n": n, "cosigners": cosigners})
+ except:
+ policy.update({"m": m, "n": n})
return policy
diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py
index 51eb8555..6164ff84 100644
--- a/src/seedsigner/models/seed.py
+++ b/src/seedsigner/models/seed.py
@@ -1,4 +1,6 @@
import unicodedata
+import hashlib
+import hmac
from binascii import hexlify
from embit import bip39, bip32, bip85
@@ -18,7 +20,7 @@ class Seed:
mnemonic: List[str] = None,
passphrase: str = "",
wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> None:
- self.wordlist_language_code = wordlist_language_code
+ self._wordlist_language_code = wordlist_language_code
if not mnemonic:
raise Exception("Must initialize a Seed with a mnemonic List[str]")
@@ -56,7 +58,11 @@ class Seed:
@property
def mnemonic_list(self) -> List[str]:
return self._mnemonic
-
+
+
+ @property
+ def wordlist_language_code(self) -> str:
+ return self._wordlist_language_code
@property
def mnemonic_display_str(self) -> str:
@@ -100,6 +106,28 @@ class Seed:
# TODO: Support other BIP-39 wordlist languages!
raise Exception("Not yet implemented!")
+ @property
+ def script_override(self) -> list:
+ return None
+
+ def derivation_override(self, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str:
+ return None
+
+ def detect_version(self, derivation_path: str, network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str:
+ embit_network = NETWORKS[SettingsConstants.map_network_to_embit(network)]
+ return bip32.detect_version(derivation_path, default="xpub", network=embit_network)
+
+ @property
+ def passphrase_label(self) -> str:
+ return SettingsConstants.LABEL__BIP39_PASSPHRASE
+
+ @property
+ def seedqr_supported(self) -> bool:
+ return True
+
+ @property
+ def bip85_supported(self) -> bool:
+ return True
def get_fingerprint(self, network: str = SettingsConstants.MAINNET) -> str:
root = bip32.HDKey.from_seed(self.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(network)]["xprv"])
@@ -125,3 +153,65 @@ class Seed:
if isinstance(other, Seed):
return self.seed_bytes == other.seed_bytes
return False
+
+
+
+class ElectrumSeed(Seed):
+
+
+ def _generate_seed(self) -> bool:
+ if len(self._mnemonic) != 12:
+ return False
+ s = hmac.digest(b"Seed version", self.mnemonic_str.encode('utf8'), hashlib.sha512).hex()
+ prefix = s[0:3]
+ # only support Electrum Segwit version for now
+ if SettingsConstants.ELECTRUM_SEED_SEGWIT == prefix:
+ self.seed_bytes=hashlib.pbkdf2_hmac('sha512', self.mnemonic_str.encode('utf-8'), b'electrum' + self._passphrase.encode('utf-8'), iterations = SettingsConstants.ELECTRUM_PBKDF2_ROUNDS)
+ return True
+ else:
+ raise InvalidSeedException("Unsupported electrum seed input")
+ return False
+
+ def set_passphrase(self, passphrase: str, regenerate_seed: bool = True):
+ if passphrase:
+ self._passphrase = ElectrumSeed.normalize_electrum_passphrase(passphrase)
+ else:
+ # Passphrase must always have a string value, even if it's just the empty
+ # string.
+ self._passphrase = ""
+
+ if regenerate_seed:
+ # Regenerate the internal seed since passphrase changes the result
+ self._generate_seed()
+
+ @staticmethod
+ def normalize_electrum_passphrase(passphrase : str) -> str:
+ passphrase = unicodedata.normalize('NFKD', passphrase)
+ # lower
+ passphrase = passphrase.lower()
+ # normalize whitespaces
+ passphrase = u' '.join(passphrase.split())
+ return passphrase
+
+ @property
+ def script_override(self) -> list:
+ return [SettingsConstants.NATIVE_SEGWIT]
+
+ def derivation_override(self, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str:
+ return "m/0h" if SettingsConstants.SINGLE_SIG == wallet_type else "m/1h"
+
+ def detect_version(self, derivation_path: str, network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str:
+ embit_network = NETWORKS[SettingsConstants.map_network_to_embit(network)]
+ return embit_network["zpub"] if SettingsConstants.SINGLE_SIG == wallet_type else embit_network["Zpub"]
+
+ @property
+ def passphrase_label(self) -> str:
+ return SettingsConstants.LABEL__CUSTOM_EXTENSION
+
+ @property
+ def seedqr_supported(self) -> bool:
+ return False
+
+ @property
+ def bip85_supported(self) -> bool:
+ return False
diff --git a/src/seedsigner/models/seed_storage.py b/src/seedsigner/models/seed_storage.py
index 5b6c3049..85feb49c 100644
--- a/src/seedsigner/models/seed_storage.py
+++ b/src/seedsigner/models/seed_storage.py
@@ -1,5 +1,5 @@
from typing import List
-from seedsigner.models.seed import Seed, InvalidSeedException
+from seedsigner.models.seed import Seed, ElectrumSeed, InvalidSeedException
from seedsigner.models.settings_definition import SettingsConstants
@@ -9,6 +9,7 @@ class SeedStorage:
self.seeds: List[Seed] = []
self.pending_seed: Seed = None
self._pending_mnemonic: List[str] = []
+ self._pending_is_electrum : bool = False
def set_pending_seed(self, seed: Seed):
@@ -58,8 +59,9 @@ class SeedStorage:
return len(self._pending_mnemonic)
- def init_pending_mnemonic(self, num_words:int = 12):
+ def init_pending_mnemonic(self, num_words:int = 12, is_electrum:bool = False):
self._pending_mnemonic = [None] * num_words
+ self._pending_is_electrum = is_electrum
def update_pending_mnemonic(self, word: str, index: int):
@@ -81,16 +83,23 @@ class SeedStorage:
def get_pending_mnemonic_fingerprint(self, network: str = SettingsConstants.MAINNET) -> str:
try:
- seed = Seed(self._pending_mnemonic)
+ if self._pending_is_electrum:
+ seed = ElectrumSeed(self._pending_mnemonic)
+ else:
+ seed = Seed(self._pending_mnemonic)
return seed.get_fingerprint(network)
except InvalidSeedException:
return None
def convert_pending_mnemonic_to_pending_seed(self):
- self.pending_seed = Seed(self._pending_mnemonic)
+ if self._pending_is_electrum:
+ self.pending_seed = ElectrumSeed(self._pending_mnemonic)
+ else:
+ self.pending_seed = Seed(self._pending_mnemonic)
self.discard_pending_mnemonic()
def discard_pending_mnemonic(self):
self._pending_mnemonic = []
+ self._pending_is_electrum = False
diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py
index c329d82e..8f33ee6b 100644
--- a/src/seedsigner/models/settings_definition.py
+++ b/src/seedsigner/models/settings_definition.py
@@ -160,6 +160,7 @@ class SettingsConstants:
SETTING__CAMERA_ROTATION = "camera_rotation"
SETTING__COMPACT_SEEDQR = "compact_seedqr"
SETTING__BIP85_CHILD_SEEDS = "bip85_child_seeds"
+ SETTING__ELECTRUM_SEEDS = "electrum_seeds"
SETTING__MESSAGE_SIGNING = "message_signing"
SETTING__PRIVACY_WARNINGS = "privacy_warnings"
SETTING__DIRE_WARNINGS = "dire_warnings"
@@ -198,6 +199,15 @@ class SettingsConstants:
TYPE__ENABLED_DISABLED_PROMPT_REQUIRED,
]
+ # Electrum seed constants
+ ELECTRUM_SEED_STANDARD = "01"
+ ELECTRUM_SEED_SEGWIT = "100"
+ ELECTRUM_SEED_2FA = "101"
+ ELECTRUM_PBKDF2_ROUNDS=2048
+
+ # Label strings
+ LABEL__BIP39_PASSPHRASE = "BIP-39 Passphrase"
+ LABEL__CUSTOM_EXTENSION = "Custom Extension"
@dataclass
class SettingsEntry:
@@ -458,6 +468,13 @@ class SettingsDefinition:
visibility=SettingsConstants.VISIBILITY__ADVANCED,
default_value=SettingsConstants.OPTION__DISABLED),
+ SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
+ attr_name=SettingsConstants.SETTING__ELECTRUM_SEEDS,
+ abbreviated_name="Electrum",
+ display_name="Electrum seed support (Native Segwit only)",
+ visibility=SettingsConstants.VISIBILITY__ADVANCED,
+ default_value=SettingsConstants.OPTION__DISABLED),
+
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__MESSAGE_SIGNING,
display_name="Message signing",
diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py
index 18e40312..b4019e33 100644
--- a/src/seedsigner/views/psbt_views.py
+++ b/src/seedsigner/views/psbt_views.py
@@ -7,7 +7,7 @@ from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconCo
from seedsigner.models.encode_qr import UrPsbtQrEncoder
from seedsigner.models.psbt_parser import PSBTParser
from seedsigner.models.settings import SettingsConstants
-from seedsigner.gui.screens.psbt_screens import PSBTOverviewScreen, PSBTMathScreen, PSBTAddressDetailsScreen, PSBTChangeDetailsScreen, PSBTFinalizeScreen
+from seedsigner.gui.screens.psbt_screens import PSBTOpReturnScreen, PSBTOverviewScreen, PSBTMathScreen, PSBTAddressDetailsScreen, PSBTChangeDetailsScreen, PSBTFinalizeScreen
from seedsigner.gui.screens.screen import (RET_CODE__BACK_BUTTON, ButtonListScreen, WarningScreen, DireWarningScreen, QRDisplayScreen)
from seedsigner.views.view import BackStackView, MainMenuView, NotYetImplementedView, View, Destination
@@ -17,6 +17,7 @@ class PSBTSelectSeedView(View):
SCAN_SEED = ("Scan a seed", SeedSignerIconConstants.QRCODE)
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
+ TYPE_ELECTRUM = ("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD)
def run(self):
@@ -44,6 +45,8 @@ class PSBTSelectSeedView(View):
button_data.append(self.SCAN_SEED)
button_data.append(self.TYPE_12WORD)
button_data.append(self.TYPE_24WORD)
+ if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED:
+ button_data.append(self.TYPE_ELECTRUM)
selected_menu_num = self.run_screen(
ButtonListScreen,
@@ -75,6 +78,18 @@ class PSBTSelectSeedView(View):
self.controller.storage.init_pending_mnemonic(num_words=24)
return Destination(SeedMnemonicEntryView)
+ elif button_data[selected_menu_num] == self.TYPE_ELECTRUM:
+ self.run_screen(
+ WarningScreen,
+ title="Electrum warning",
+ status_headline=None,
+ text=f"Some features disabled for Electrum seeds",
+ show_back_button=False,
+ )
+ from seedsigner.views.seed_views import SeedMnemonicEntryView
+ self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)
+ return Destination(SeedMnemonicEntryView)
+
class PSBTOverviewView(View):
@@ -137,7 +152,8 @@ class PSBTOverviewView(View):
num_inputs=psbt_parser.num_inputs,
num_self_transfer_outputs=num_self_transfer_outputs,
num_change_outputs=num_change_outputs,
- destination_addresses=psbt_parser.destination_addresses
+ destination_addresses=psbt_parser.destination_addresses,
+ has_op_return=psbt_parser.op_return_data is not None,
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
@@ -276,12 +292,14 @@ class PSBTAddressDetailsView(View):
# Move on to display change
return Destination(PSBTChangeDetailsView, view_args={"change_address_num": 0})
+ elif psbt_parser.op_return_data:
+ return Destination(PSBTOpReturnView)
+
else:
# There's no change output to verify. Move on to sign the PSBT.
return Destination(PSBTFinalizeView)
-
class PSBTChangeDetailsView(View):
NEXT = "Next"
SKIP_VERIFICATION = "Skip Verificiation"
@@ -417,6 +435,10 @@ class PSBTChangeDetailsView(View):
if self.change_address_num < psbt_parser.num_change_outputs - 1:
return Destination(PSBTChangeDetailsView, view_args={"change_address_num": self.change_address_num + 1})
+
+ elif psbt_parser.op_return_data:
+ return Destination(PSBTOpReturnView)
+
else:
# There's no more change to verify. Move on to sign the PSBT.
return Destination(PSBTFinalizeView)
@@ -456,6 +478,33 @@ class PSBTAddressVerificationFailedView(View):
return Destination(MainMenuView, clear_history=True)
+class PSBTOpReturnView(View):
+ """
+ Shows the OP_RETURN data
+ """
+ def run(self):
+ psbt_parser: PSBTParser = self.controller.psbt_parser
+
+ if not psbt_parser:
+ # Should not be able to get here
+ raise Exception("Routing error")
+
+ title = "OP_RETURN"
+ button_data = ["Next"]
+
+ selected_menu_num = self.run_screen(
+ PSBTOpReturnScreen,
+ title=title,
+ button_data=button_data,
+ op_return_data=psbt_parser.op_return_data,
+ )
+
+ if selected_menu_num == RET_CODE__BACK_BUTTON:
+ return Destination(BackStackView)
+
+ return Destination(PSBTFinalizeView)
+
+
class PSBTFinalizeView(View):
"""
diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py
index a9bea1a8..c4fbc4ed 100644
--- a/src/seedsigner/views/seed_views.py
+++ b/src/seedsigner/views/seed_views.py
@@ -79,6 +79,7 @@ class SeedSelectSeedView(View):
SCAN_SEED = ("Scan a seed", SeedSignerIconConstants.QRCODE)
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
+ TYPE_ELECTRUM = ("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD)
def __init__(self, flow: str = Controller.FLOW__VERIFY_SINGLESIG_ADDR):
@@ -118,6 +119,8 @@ class SeedSelectSeedView(View):
button_data.append(self.SCAN_SEED)
button_data.append(self.TYPE_12WORD)
button_data.append(self.TYPE_24WORD)
+ if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED:
+ button_data.append(self.TYPE_ELECTRUM)
selected_menu_num = self.run_screen(
seed_screens.SeedSelectSeedScreen,
@@ -154,6 +157,19 @@ class SeedSelectSeedView(View):
self.controller.storage.init_pending_mnemonic(num_words=24)
return Destination(SeedMnemonicEntryView)
+ elif button_data[selected_menu_num] == self.TYPE_ELECTRUM:
+ self.run_screen(
+ WarningScreen,
+ title="Electrum warning",
+ status_headline=None,
+ text=f"Some features disabled for Electrum seeds",
+ show_back_button=False,
+ )
+
+ from seedsigner.views.seed_views import SeedMnemonicEntryView
+ self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)
+ return Destination(SeedMnemonicEntryView)
+
"""****************************************************************************
@@ -163,6 +179,7 @@ class LoadSeedView(View):
SEED_QR = (" Scan a SeedQR", SeedSignerIconConstants.QRCODE)
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
+ TYPE_ELECTRUM = ("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD)
CREATE = (" Create a seed", SeedSignerIconConstants.PLUS)
def run(self):
@@ -172,6 +189,9 @@ class LoadSeedView(View):
self.TYPE_24WORD,
self.CREATE,
]
+ if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED:
+ button_data.insert(len(button_data)-1, self.TYPE_ELECTRUM)
+
selected_menu_num = self.run_screen(
ButtonListScreen,
title="Load A Seed",
@@ -194,6 +214,17 @@ class LoadSeedView(View):
self.controller.storage.init_pending_mnemonic(num_words=24)
return Destination(SeedMnemonicEntryView)
+ elif button_data[selected_menu_num] == self.TYPE_ELECTRUM:
+ self.run_screen(
+ WarningScreen,
+ title="Electrum warning",
+ status_headline=None,
+ text=f"Some features disabled for Electrum seeds",
+ show_back_button=False,
+ )
+ self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)
+ return Destination(SeedMnemonicEntryView)
+
elif button_data[selected_menu_num] == self.CREATE:
from .tools_views import ToolsMenuView
return Destination(ToolsMenuView)
@@ -288,7 +319,6 @@ class SeedMnemonicInvalidView(View):
class SeedFinalizeView(View):
FINALIZE = "Done"
- PASSPHRASE = "BIP-39 Passphrase"
def __init__(self):
super().__init__()
@@ -298,8 +328,9 @@ class SeedFinalizeView(View):
def run(self):
button_data = [self.FINALIZE]
+ passphrase_button = self.seed.passphrase_label
if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) != SettingsConstants.OPTION__DISABLED:
- button_data.append(self.PASSPHRASE)
+ button_data.append(passphrase_button)
selected_menu_num = self.run_screen(
seed_screens.SeedFinalizeScreen,
@@ -311,7 +342,7 @@ class SeedFinalizeView(View):
seed_num = self.controller.storage.finalize_pending_seed()
return Destination(SeedOptionsView, view_args={"seed_num": seed_num}, clear_history=True)
- elif button_data[selected_menu_num] == self.PASSPHRASE:
+ elif button_data[selected_menu_num] == passphrase_button:
return Destination(SeedAddPassphraseView)
@@ -323,7 +354,8 @@ class SeedAddPassphraseView(View):
def run(self):
- ret_dict = self.run_screen(seed_screens.SeedAddPassphraseScreen, passphrase=self.seed.passphrase)
+ passphrase_title=self.seed.passphrase_label
+ ret_dict = self.run_screen(seed_screens.SeedAddPassphraseScreen, passphrase=self.seed.passphrase, title=passphrase_title)
# The new passphrase will be the return value; it might be empty.
self.seed.set_passphrase(ret_dict["passphrase"])
@@ -522,7 +554,7 @@ class SeedOptionsView(View):
if self.settings.get_value(SettingsConstants.SETTING__MESSAGE_SIGNING) == SettingsConstants.OPTION__ENABLED:
button_data.append(self.SIGN_MESSAGE)
- if self.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__ENABLED:
+ if self.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__ENABLED and self.seed.bip85_supported:
button_data.append(self.BIP85_CHILD_SEED)
button_data.append(self.DISCARD)
@@ -581,7 +613,9 @@ class SeedBackupView(View):
def run(self):
- button_data = [self.VIEW_WORDS, self.EXPORT_SEEDQR]
+ button_data = [self.VIEW_WORDS]
+ if self.seed.seedqr_supported:
+ button_data.append(self.EXPORT_SEEDQR)
selected_menu_num = self.run_screen(
ButtonListScreen,
@@ -647,9 +681,11 @@ class SeedExportXpubScriptTypeView(View):
def run(self):
from .tools_views import ToolsAddressExplorerAddressTypeView
args = {"seed_num": self.seed_num, "sig_type": self.sig_type}
- if len(self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)) == 1:
+ seed = self.controller.storage.seeds[self.seed_num]
+ script_types = seed.script_override if seed.script_override else self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)
+ if len(script_types) == 1:
# Nothing to select; skip this screen
- args["script_type"] = self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)[0]
+ args["script_type"] = script_types[0]
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
del args["sig_type"]
@@ -833,8 +869,11 @@ class SeedExportXpubDetailsView(View):
def run(self):
+ seed_derivation_override = self.seed.derivation_override(self.sig_type)
if self.script_type == SettingsConstants.CUSTOM_DERIVATION:
derivation_path = self.custom_derivation
+ elif seed_derivation_override:
+ derivation_path = seed_derivation_override
else:
derivation_path = embit_utils.get_standard_derivation_path(
network=self.settings.get_value(SettingsConstants.SETTING__NETWORK),
@@ -854,10 +893,10 @@ class SeedExportXpubDetailsView(View):
try:
embit_network = NETWORKS[SettingsConstants.map_network_to_embit(self.settings.get_value(SettingsConstants.SETTING__NETWORK))]
- version = embit.bip32.detect_version(
+ version = self.seed.detect_version(
derivation_path,
- default="xpub",
- network=embit_network
+ self.settings.get_value(SettingsConstants.SETTING__NETWORK),
+ self.sig_type
)
root = embit.bip32.HDKey.from_seed(
self.seed.seed_bytes,
@@ -885,6 +924,7 @@ class SeedExportXpubDetailsView(View):
dict(seed_num=self.seed_num,
coordinator=self.coordinator,
derivation_path=derivation_path,
+ sig_type=self.sig_type
)
)
@@ -894,16 +934,16 @@ class SeedExportXpubDetailsView(View):
class SeedExportXpubQRDisplayView(View):
- def __init__(self, seed_num: int, coordinator: str, derivation_path: str):
+ def __init__(self, seed_num: int, coordinator: str, derivation_path: str, sig_type: str = SettingsConstants.SINGLE_SIG):
super().__init__()
self.seed = self.controller.get_seed(seed_num)
encoder_args = dict(
- mnemonic=self.seed.mnemonic_list,
- passphrase=self.seed.passphrase,
+ seed=self.seed,
derivation=derivation_path,
network=self.settings.get_value(SettingsConstants.SETTING__NETWORK),
- qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY)
+ qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY),
+ sig_type=sig_type
)
if coordinator == SettingsConstants.COORDINATOR__SPECTER_DESKTOP:
@@ -1416,8 +1456,7 @@ class SeedTranscribeSeedQRWholeQRView(View):
def run(self):
- encoder_args = dict(mnemonic=self.seed.mnemonic_list,
- wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE))
+ encoder_args = dict(seed=self.seed)
if self.seedqr_format == QRType.SEED__SEEDQR:
e = SeedQrEncoder(**encoder_args)
elif self.seedqr_format == QRType.SEED__COMPACTSEEDQR:
@@ -1453,8 +1492,7 @@ class SeedTranscribeSeedQRZoomedInView(View):
def run(self):
- encoder_args = dict(mnemonic=self.seed.mnemonic_list,
- wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE))
+ encoder_args = dict(seed=self.seed)
if self.seedqr_format == QRType.SEED__SEEDQR:
e = SeedQrEncoder(**encoder_args)
elif self.seedqr_format == QRType.SEED__COMPACTSEEDQR:
@@ -1676,15 +1714,17 @@ class SeedAddressVerificationView(View):
super().__init__()
self.seed_num = seed_num
self.is_multisig = self.controller.unverified_address["sig_type"] == SettingsConstants.MULTISIG
+ self.seed_derivation_override = ""
if not self.is_multisig:
if seed_num is None:
raise Exception("Can't validate a single sig addr without specifying a seed")
self.seed_num = seed_num
self.seed = self.controller.get_seed(seed_num)
+ self.seed_derivation_override = self.seed.derivation_override(wallet_type=SettingsConstants.SINGLE_SIG)
else:
self.seed = None
self.address = self.controller.unverified_address["address"]
- self.derivation_path = self.controller.unverified_address["derivation_path"]
+ self.derivation_path = self.seed_derivation_override if self.seed_derivation_override else self.controller.unverified_address["derivation_path"]
self.script_type = self.controller.unverified_address["script_type"]
self.sig_type = self.controller.unverified_address["sig_type"]
self.network = self.controller.unverified_address["network"]
diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py
index 02c34f23..ad2940be 100644
--- a/src/seedsigner/views/tools_views.py
+++ b/src/seedsigner/views/tools_views.py
@@ -9,7 +9,7 @@ from PIL.ImageOps import autocontrast
from seedsigner.controller import Controller
from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants, SeedSignerIconConstants
-from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen)
+from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, WarningScreen)
from seedsigner.gui.screens.tools_screens import (ToolsCalcFinalWordDoneScreen, ToolsCalcFinalWordFinalizePromptScreen,
ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen,
ToolsImageEntropyLivePreviewScreen, ToolsAddressExplorerAddressTypeScreen)
@@ -440,6 +440,7 @@ class ToolsAddressExplorerSelectSourceView(View):
SCAN_DESCRIPTOR = ("Scan wallet descriptor", SeedSignerIconConstants.QRCODE)
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
+ TYPE_ELECTRUM = ("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD)
def run(self):
@@ -449,6 +450,8 @@ class ToolsAddressExplorerSelectSourceView(View):
button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
button_data.append((button_str, SeedSignerIconConstants.FINGERPRINT))
button_data = button_data + [self.SCAN_SEED, self.SCAN_DESCRIPTOR, self.TYPE_12WORD, self.TYPE_24WORD]
+ if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED:
+ button_data.append(self.TYPE_ELECTRUM)
selected_menu_num = self.run_screen(
ButtonListScreen,
@@ -492,6 +495,18 @@ class ToolsAddressExplorerSelectSourceView(View):
self.controller.storage.init_pending_mnemonic(num_words=24)
return Destination(SeedMnemonicEntryView)
+ elif button_data[selected_menu_num] == self.TYPE_ELECTRUM:
+ self.run_screen(
+ WarningScreen,
+ title="Electrum warning",
+ status_headline=None,
+ text=f"Some features disabled for Electrum seeds",
+ show_back_button=False,
+ )
+ from seedsigner.views.seed_views import SeedMnemonicEntryView
+ self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)
+ return Destination(SeedMnemonicEntryView)
+
class ToolsAddressExplorerAddressTypeView(View):
@@ -525,9 +540,12 @@ class ToolsAddressExplorerAddressTypeView(View):
if self.seed_num is not None:
self.seed = self.controller.storage.seeds[seed_num]
data["seed_num"] = self.seed
+ seed_derivation_override = self.seed.derivation_override(wallet_type=SettingsConstants.SINGLE_SIG)
if self.script_type == SettingsConstants.CUSTOM_DERIVATION:
derivation_path = self.custom_derivation
+ elif seed_derivation_override:
+ derivation_path = seed_derivation_override
else:
derivation_path = embit_utils.get_standard_derivation_path(
network=self.settings.get_value(SettingsConstants.SETTING__NETWORK),
diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py
index a5c4800c..a7c83d49 100644
--- a/src/seedsigner/views/view.py
+++ b/src/seedsigner/views/view.py
@@ -1,4 +1,4 @@
-from dataclasses import dataclass
+from dataclasses import dataclass, field
from typing import Type
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants
@@ -313,7 +313,7 @@ class ErrorView(View):
status_headline: str = None
text: str = None
button_text: str = None
- next_destination: Destination = Destination(MainMenuView, clear_history=True)
+ next_destination: Destination = field(default_factory=lambda: Destination(MainMenuView, clear_history=True))
def run(self):
diff --git a/tests/screenshot_generator/generator.py b/tests/screenshot_generator/generator.py
index 6b7effe1..810e5d6f 100644
--- a/tests/screenshot_generator/generator.py
+++ b/tests/screenshot_generator/generator.py
@@ -1,11 +1,17 @@
import embit
import os
+import random
import sys
import time
from unittest.mock import Mock, patch, MagicMock
from seedsigner.helpers import embit_utils
-from seedsigner.models.settings import Settings
+from embit import compact
+from embit.psbt import PSBT, OutputScope
+from embit.script import Script
+
+from seedsigner.helpers import embit_utils
+from seedsigner.models.psbt_parser import OPCODES, PSBTParser
# Prevent importing modules w/Raspi hardware dependencies.
@@ -22,8 +28,6 @@ sys.modules['seedsigner.hardware.microsd'] = MagicMock()
from seedsigner.controller import Controller
from seedsigner.gui.renderer import Renderer
from seedsigner.gui.toast import BaseToastOverlayManagerThread, RemoveSDCardToastManagerThread, SDCardStateChangeToastManagerThread
-from seedsigner.hardware.buttons import HardwareButtons
-from seedsigner.hardware.camera import Camera
from seedsigner.hardware.microsd import MicroSD
from seedsigner.models.decode_qr import DecodeQR
from seedsigner.models.qr_type import QRType
@@ -86,6 +90,34 @@ def test_generate_screenshots(target_locale):
controller.psbt = decoder.get_psbt()
controller.psbt_seed = seed_12b
+ def add_op_return_to_psbt(psbt: PSBT, raw_payload_data: bytes):
+ data = (compact.to_bytes(OPCODES.OP_RETURN) +
+ compact.to_bytes(OPCODES.OP_PUSHDATA1) +
+ compact.to_bytes(len(raw_payload_data)) +
+ raw_payload_data)
+ script = Script(data)
+ output = OutputScope()
+ output.script_pubkey = script
+ output.value = 0
+ psbt.outputs.append(output)
+ return psbt.to_string()
+
+ # Prep a PSBT with a human-readable OP_RETURN
+ raw_payload_data = "Chancellor on the brink of third bailout for banks".encode()
+ psbt = PSBT.from_base64(BASE64_PSBT_1)
+
+ # Simplify the output side
+ output = psbt.outputs[-1]
+ psbt.outputs.clear()
+ psbt.outputs.append(output)
+ assert len(psbt.outputs) == 1
+ BASE64_PSBT_WITH_OP_RETURN_TEXT = add_op_return_to_psbt(psbt, raw_payload_data)
+
+ # Prep a PSBT with a (repeatably) random 80-byte OP_RETURN
+ random.seed(6102)
+ BASE64_PSBT_WITH_OP_RETURN_RAW_BYTES = add_op_return_to_psbt(PSBT.from_base64(BASE64_PSBT_1), random.randbytes(80))
+
+
# Multisig wallet descriptor for the multisig in the above PSBT
MULTISIG_WALLET_DESCRIPTOR = """wsh(sortedmulti(1,[22bde1a9/48h/1h/0h/2h]tpubDFfsBrmpj226ZYiRszYi2qK6iGvh2vkkghfGB2YiRUVY4rqqedHCFEgw12FwDkm7rUoVtq9wLTKc6BN2sxswvQeQgp7m8st4FP8WtP8go76/{0,1}/*,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/{0,1}/*))#3jhtf6yx"""
controller.multisig_wallet_descriptor = embit.descriptor.Descriptor.from_string(MULTISIG_WALLET_DESCRIPTOR)
@@ -194,6 +226,11 @@ def test_generate_screenshots(target_locale):
(NotYetImplementedView, {}, "PSBTChangeDetailsView_multisig_unverified"), # Must manually re-run this below
(psbt_views.PSBTChangeDetailsView, dict(change_address_num=0), "PSBTChangeDetailsView_multisig_verified"),
+
+ (NotYetImplementedView, {}, "PSBTOverviewView_op_return"), # Placeholder
+ (NotYetImplementedView, {}, "PSBTOpReturnView_text"), # Placeholder
+ (NotYetImplementedView, {}, "PSBTOpReturnView_raw_hex_data"), # Placeholder
+
(psbt_views.PSBTAddressVerificationFailedView, dict(is_change=True, is_multisig=False), "PSBTAddressVerificationFailedView_singlesig_change"),
(psbt_views.PSBTAddressVerificationFailedView, dict(is_change=False, is_multisig=False), "PSBTAddressVerificationFailedView_singlesig_selftransfer"),
(psbt_views.PSBTAddressVerificationFailedView, dict(is_change=True, is_multisig=True), "PSBTAddressVerificationFailedView_multisig_change"),
@@ -242,7 +279,9 @@ def test_generate_screenshots(target_locale):
readme = f"""# SeedSigner Screenshots\n"""
- def screencap_view(view_cls: View, view_name: str, view_args: dict={}, toast_thread: BaseToastOverlayManagerThread = None):
+ def screencap_view(view_cls: View, view_args: dict = {}, view_name: str = None, toast_thread: BaseToastOverlayManagerThread = None):
+ if not view_name:
+ view_name = view_cls.__name__
screenshot_renderer.set_screenshot_filename(f"{view_name}.png")
try:
print(f"Running {view_name}")
@@ -267,7 +306,6 @@ def test_generate_screenshots(target_locale):
if toast_thread:
toast_thread.stop()
-
for section_name, screenshot_list in screenshot_sections.items():
subdir = section_name.lower().replace(" ", "_")
screenshot_renderer.set_screenshot_path(os.path.join(screenshot_root, subdir))
@@ -290,25 +328,41 @@ def test_generate_screenshots(target_locale):
view_name = view_cls.__name__
toast_thread = None
- screencap_view(view_cls, view_name, view_args, toast_thread=toast_thread)
+ screencap_view(view_cls, view_args=view_args, view_name=view_name, toast_thread=toast_thread)
readme += """ """
readme += f"""{view_name}
 |
"""
readme += """
\n"""
readme += ""
- # many screens don't work, leaving a missing image, re-run here for now
+ # Re-render some screens that require more manual intervention / setup than the above
+ # scripting can support.
screenshot_renderer.set_screenshot_path(os.path.join(screenshot_root, "psbt_views"))
+ # Render the PSBTChangeDetailsView_multisig_unverified screenshot
decoder = DecodeQR()
decoder.add_data(BASE64_PSBT_1)
controller.psbt = decoder.get_psbt()
controller.psbt_seed = seed_12b
controller.multisig_wallet_descriptor = None
- screencap_view(psbt_views.PSBTChangeDetailsView, 'PSBTChangeDetailsView_multisig_unverified', dict(change_address_num=0))
+ screencap_view(psbt_views.PSBTChangeDetailsView, view_name='PSBTChangeDetailsView_multisig_unverified', view_args=dict(change_address_num=0))
controller.psbt_seed = None
- screencap_view(psbt_views.PSBTSelectSeedView, 'PSBTSelectSeedView', {})
+ screencap_view(psbt_views.PSBTSelectSeedView, view_name='PSBTSelectSeedView')
+
+ # Render OP_RETURN screens for real
+ controller.psbt_seed = seed_12b
+ decoder = DecodeQR()
+ decoder.add_data(BASE64_PSBT_WITH_OP_RETURN_TEXT)
+ controller.psbt = decoder.get_psbt()
+ controller.psbt_parser = PSBTParser(p=controller.psbt, seed=seed_12b)
+ screencap_view(psbt_views.PSBTOverviewView, view_name='PSBTOverviewView_op_return')
+ screencap_view(psbt_views.PSBTOpReturnView, view_name="PSBTOpReturnView_text")
+
+ decoder.add_data(BASE64_PSBT_WITH_OP_RETURN_RAW_BYTES)
+ controller.psbt = decoder.get_psbt()
+ controller.psbt_parser = PSBTParser(p=controller.psbt, seed=seed_12b)
+ screencap_view(psbt_views.PSBTOpReturnView, view_name="PSBTOpReturnView_raw_hex_data")
with open(os.path.join(screenshot_root, "README.md"), 'w') as readme_file:
readme_file.write(readme)
diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py
index 8a8ec368..34f28631 100644
--- a/tests/test_encodepsbtqr.py
+++ b/tests/test_encodepsbtqr.py
@@ -3,6 +3,7 @@ from embit import psbt
from binascii import a2b_base64
from seedsigner.models.settings import SettingsConstants
+from seedsigner.models.seed import Seed
@@ -24,18 +25,19 @@ def test_ur_psbt_qr_encode():
def test_seedsigner_qr():
# test vector 1 from the SeedQR docs
mnemonic = "attack pizza motion avocado network gather crop fresh patrol unusual wild holiday candy pony ranch winter theme error hybrid van cereal salon goddess expire".split()
- e = SeedQrEncoder(mnemonic=mnemonic)
+ e = SeedQrEncoder(seed=Seed(mnemonic))
assert e.next_part() == "011513251154012711900771041507421289190620080870026613431420201617920614089619290300152408010643"
- e = CompactSeedQrEncoder(mnemonic=mnemonic)
+ e = CompactSeedQrEncoder(seed=Seed(mnemonic))
assert e.next_part() == b'\x0et\xb6A\x07\xf9L\xc0\xcc\xfa\xe6\xa1=\xcb\xec6b\x15O\xecg\xe0\xe0\t\x99\xc0x\x92Y}\x19\n'
# test vector 4 from the SeedQR docs
mnemonic="forum undo fragile fade shy sign arrest garment culture tube off merit".split()
- e = SeedQrEncoder(mnemonic=mnemonic)
+ seed = Seed(mnemonic)
+ e = SeedQrEncoder(seed=seed)
assert e.next_part() == "073318950739065415961602009907670428187212261116"
- e = CompactSeedQrEncoder(mnemonic=mnemonic)
+ e = CompactSeedQrEncoder(seed=seed)
assert e.next_part() == b'[\xbd\x9dq\xa8\xecy\x90\x83\x1a\xff5\x9dBeE'
@@ -43,8 +45,7 @@ def test_seedsigner_qr():
def test_xpub_qr():
mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash"
- e = StaticXpubQrEncoder(mnemonic=mnemonic.split(),
- passphrase="pass",
+ e = StaticXpubQrEncoder(seed=Seed(mnemonic.split(), passphrase="pass"),
derivation="m/48h/1h/0h/2h",
network=SettingsConstants.TESTNET)
assert e.next_part() == "[c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDNc5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBoMxfrVggXj5NJmaNEToWpax8GMMucozvAdqf1bW1JsZsfdBzsK3VUC5"
@@ -54,7 +55,7 @@ def test_xpub_qr():
def test_specter_xpub_qr():
mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash"
- e = SpecterXPubQrEncoder(mnemonic=mnemonic.split(" "), passphrase="pass", network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW)
+ e = SpecterXPubQrEncoder(seed=Seed(mnemonic.split(" "), passphrase="pass"), network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW)
assert e.next_part() == "p1of4 [c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDN"
assert e.next_part() == "p2of4 c5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBo"
@@ -67,8 +68,7 @@ def test_ur_xpub_qr():
mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash"
e = UrXpubQrEncoder(
- mnemonic=mnemonic.split(),
- passphrase="pass",
+ seed=Seed(mnemonic.split(), passphrase="pass"),
network=SettingsConstants.TESTNET,
derivation="m/48h/1h/0h/2h",
qr_density=SettingsConstants.DENSITY__MEDIUM
diff --git a/tests/test_flows_psbt.py b/tests/test_flows_psbt.py
index 67c5149e..c328fe24 100644
--- a/tests/test_flows_psbt.py
+++ b/tests/test_flows_psbt.py
@@ -2,7 +2,10 @@ from base import FlowTest, FlowStep
from seedsigner.views.view import MainMenuView
from seedsigner.views import scan_views, seed_views, psbt_views
+from seedsigner.models.settings import SettingsConstants
+
+# TODO: Cleanup: convert TAB spacing to SPACE
class TestPSBTFlows(FlowTest):
def test_scan_psbt_first_then_correct_seedqr_flow(self):
@@ -53,6 +56,7 @@ class TestPSBTFlows(FlowTest):
FlowStep(psbt_views.PSBTSignedQRDisplayView),
FlowStep(MainMenuView)
])
+
def test_scan_multisig_psbt_seed_already_signed_flow(self):
@@ -77,7 +81,7 @@ class TestPSBTFlows(FlowTest):
FlowStep(psbt_views.PSBTSigningErrorView, button_data_selection=psbt_views.PSBTSigningErrorView.SELECT_DIFF_SEED),
FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.SCAN_SEED),
FlowStep(scan_views.ScanSeedQRView, before_run=load_seed_into_decoder),
- FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE),
+ FlowStep(seed_views.SeedFinalizeView, button_data_selection=SettingsConstants.LABEL__BIP39_PASSPHRASE),
FlowStep(seed_views.SeedAddPassphraseView, screen_return_value=dict(passphrase="abc")),
FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.DONE),
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
@@ -89,4 +93,46 @@ class TestPSBTFlows(FlowTest):
FlowStep(psbt_views.PSBTSignedQRDisplayView),
FlowStep(MainMenuView),
])
-
+
+
+ def test_parse_and_display_op_return_content(self):
+ """
+ PSBTs that include an OP_RETURN should be able to be parsed like any other
+ PSBT and route to the dedicated OP_RETURN View to display the content
+ """
+ def load_psbt_into_decoder(view: scan_views.ScanView):
+ """
+ PSBT Tx and Wallet Details
+ - Single Sig Wallet P2WPKH (Native Segwit) with no passphrase
+ - Regtest 0fb882ff m/84'/1'/0' tpubDCfk37PqcQx6nFtFVuYHvRLJHxvYj33NjHkKRyRmWyCjyJ64sYBXyVjsTHaLBp5GLhM91VBgJ8nKDWDu52J2xVRy64c7ybEjjyWQJuQGLcg
+ - 1 Input
+ - 99,992,460 sats
+ - 2 Outputs
+ - 1 Output back to self (bcrt1qvwkhakqhz7m7kmz6332avatsmdy32m644g86vv) of 99,992,296 sats
+ - 1 OP_RETURN: "Chancellor on the brink of third bailout"
+ - Fee 164 sats
+ """
+ view.decoder.add_data("cHNidP8BAIYCAAAAATpQ10o+gKdZ8ThpKsbfHiHYn3NhvUrQ5DvW0ZWX8jKLAAAAAAD9////AujC9QUAAAAAFgAUY61+2BcXt+tsWoxV1nVw20kVb1UAAAAAAAAAACtqTChDaGFuY2VsbG9yIG9uIHRoZSBicmluayBvZiB0aGlyZCBiYWlsb3V0aQAAAE8BBDWHzwNXmUmVgAAAANRFa7R5gYD84Wbha3d1QnjgfYPOBw87on6cXS32WoyqAsPFtPxB7PRTdbujUnBPUVDh9YUBtwrl4nc0OcRNGvIyEA+4gv9UAACAAQAAgAAAAIAAAQB0AgAAAAGNFK/1X0fP5q+nu5XX7Tk2VRa0EL+jkGI9CHiJvsjZCgAAAAAA/f///wKMw/UFAAAAABYAFIpZMNnUU6cQt8Q0YpZ0pnvsSA5fAAAAAAAAAAAZakwWYml0Y29pbiBpcyBmcmVlIHNwZWVjaGgAAAABAR+Mw/UFAAAAABYAFIpZMNnUU6cQt8Q0YpZ0pnvsSA5fAQMEAQAAACIGAvxDI0eNI1oQ2AU69R7A0jf+hUdilWCgrWHgdzkqlaXMGA+4gv9UAACAAQAAgAAAAIAAAAAAAQAAAAAiAgK9qKtzGWyiRrpmupdA99NVLriz3GQy6cENbyD19sfl/hgPuIL/VAAAgAEAAIAAAACAAAAAAAIAAAAAAA==")
+
+ def load_seed_into_decoder(view: scan_views.ScanView):
+ view.decoder.add_data("114006021552133507590698063102151531110102551496")
+
+ self.run_sequence([
+ FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
+ FlowStep(scan_views.ScanView, before_run=load_psbt_into_decoder), # simulate read PSBT; ret val is ignored
+ FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.SCAN_SEED),
+ FlowStep(scan_views.ScanSeedQRView, before_run=load_seed_into_decoder),
+ FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
+ FlowStep(seed_views.SeedOptionsView, is_redirect=True),
+ FlowStep(psbt_views.PSBTOverviewView),
+ FlowStep(psbt_views.PSBTMathView),
+ FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT),
+
+ # Should route to display OP_RETURN content
+ FlowStep(psbt_views.PSBTOpReturnView, button_data_selection=0),
+
+ # Should be able to sign the psbt
+ FlowStep(psbt_views.PSBTFinalizeView, button_data_selection=psbt_views.PSBTFinalizeView.APPROVE_PSBT),
+ FlowStep(psbt_views.PSBTSignedQRDisplayView),
+ FlowStep(MainMenuView)
+ ])
diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py
index 4210543d..63230319 100644
--- a/tests/test_flows_seed.py
+++ b/tests/test_flows_seed.py
@@ -40,10 +40,10 @@ class TestSeedFlows(FlowTest):
self.run_sequence([
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder), # simulate read SeedQR; ret val is ignored
- FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE),
+ FlowStep(seed_views.SeedFinalizeView, button_data_selection=SettingsConstants.LABEL__BIP39_PASSPHRASE),
FlowStep(seed_views.SeedAddPassphraseView, screen_return_value=dict(passphrase="muhpassphrase", is_back_button=True)),
FlowStep(seed_views.SeedAddPassphraseExitDialogView, button_data_selection=seed_views.SeedAddPassphraseExitDialogView.DISCARD),
- FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE),
+ FlowStep(seed_views.SeedFinalizeView, button_data_selection=SettingsConstants.LABEL__BIP39_PASSPHRASE),
FlowStep(seed_views.SeedAddPassphraseView, screen_return_value=dict(passphrase="muhpassphrase", is_back_button=True)),
FlowStep(seed_views.SeedAddPassphraseExitDialogView, button_data_selection=seed_views.SeedAddPassphraseExitDialogView.EDIT),
FlowStep(seed_views.SeedAddPassphraseView, screen_return_value=dict(passphrase="muhpassphrase")),
diff --git a/tests/test_flows_tools.py b/tests/test_flows_tools.py
index 20ed8238..38d8325c 100644
--- a/tests/test_flows_tools.py
+++ b/tests/test_flows_tools.py
@@ -64,7 +64,7 @@ class TestToolsFlows(FlowTest):
# Finalize the new seed w/passphrase
self.run_sequence(
sequence=[
- FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE),
+ FlowStep(seed_views.SeedFinalizeView, button_data_selection=SettingsConstants.LABEL__BIP39_PASSPHRASE),
FlowStep(seed_views.SeedAddPassphraseView, screen_return_value=dict(passphrase="mypassphrase")),
FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.DONE),
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
diff --git a/tests/test_psbt_parser.py b/tests/test_psbt_parser.py
index cf6f8db2..99b20c23 100644
--- a/tests/test_psbt_parser.py
+++ b/tests/test_psbt_parser.py
@@ -112,3 +112,46 @@ def test_p2sh_legacy_multisig():
# And the self-transfer receive addr
assert psbt_parser.verify_multisig_output(descriptor, 1)
+
+
+def test_parse_op_return_content():
+ """
+ Should successfully parse the OP_RETURN content from a PSBT.
+
+ PSBT Tx and Wallet Details
+ - Single Sig Wallet P2WPKH (Native Segwit) with no passphrase
+ - Regtest 0fb882ff m/84'/1'/0' tpubDCfk37PqcQx6nFtFVuYHvRLJHxvYj33NjHkKRyRmWyCjyJ64sYBXyVjsTHaLBp5GLhM91VBgJ8nKDWDu52J2xVRy64c7ybEjjyWQJuQGLcg
+ - 1 Input
+ - 99,992,460 sats
+ - 2 Outputs
+ - 1 Output back to self (bcrt1qvwkhakqhz7m7kmz6332avatsmdy32m644g86vv) of 99,992,296 sats
+ - 1 OP_RETURN: "Chancellor on the brink of third bailout"
+ - Fee 164 sats
+ """
+ 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)
+
+ mnemonic = "model ensure search plunge galaxy firm exclude brain satoshi meadow cable roast".split()
+ pw = ""
+ seed = Seed(mnemonic, passphrase=pw)
+
+ psbt_parser = PSBTParser(p=tx, seed=seed, network=SettingsConstants.REGTEST)
+
+ # Remember to do the comparison as bytes
+ assert psbt_parser.op_return_data == "Chancellor on the brink of third bailout".encode()
+
+ # PSBT is an internal self-spend to the its own receive addr, but the parser categorizes it as "change"
+ assert psbt_parser.change_data == [
+ {
+ 'output_index': 0,
+ 'address': 'bcrt1qvwkhakqhz7m7kmz6332avatsmdy32m644g86vv',
+ 'amount': 99992296,
+ 'fingerprint': ['0fb882ff'],
+ 'derivation_path': ["m/84h/1h/0h/0/2"]}
+ ]
+ assert psbt_parser.spend_amount == 0 # This is a self-spend; no value being spent, other than the tx fee
+ assert psbt_parser.change_amount == 99992296
+ assert psbt_parser.destination_addresses == []
+ assert psbt_parser.destination_amounts == []
diff --git a/tests/test_seed.py b/tests/test_seed.py
index a7e082ee..04291f65 100644
--- a/tests/test_seed.py
+++ b/tests/test_seed.py
@@ -1,4 +1,4 @@
-from seedsigner.models.seed import Seed
+from seedsigner.models.seed import Seed, ElectrumSeed
from seedsigner.models.settings import SettingsConstants
@@ -36,6 +36,10 @@ def test_seed():
# assert seed.passphrase == "test"
-
-
-
\ No newline at end of file
+def test_electrum_seed():
+ seed = ElectrumSeed(mnemonic="regular reject rare profit once math fringe chase until ketchup century escape".split())
+
+ intended_seed = b'\xcan|\xf8\x8a\x8d\xf78=Pq\xc4_\xe6\x02\x91\xfcs\xb2[\xed*\xdc\xc7%\xb6[_-(~D\xe5\x1e\x85%N\x9c\x03\x9dh\xafX}\x16\xb1\x99,\xbe\xc4\x11\xfaW\x0f\xb0\x89yD\xf4\x0f\xd5?\x8eA'
+
+ assert seed.seed_bytes == intended_seed
+
diff --git a/tests/test_seedqr.py b/tests/test_seedqr.py
index 826cd941..2202af0d 100644
--- a/tests/test_seedqr.py
+++ b/tests/test_seedqr.py
@@ -4,6 +4,7 @@ from seedsigner.helpers.qr import QR
from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus
from seedsigner.models.encode_qr import SeedQrEncoder, CompactSeedQrEncoder
from seedsigner.models.qr_type import QRType
+from seedsigner.models.seed import Seed
@@ -13,9 +14,9 @@ def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type):
assert len(mnemonic) == mnemonic_length
if qr_type == QRType.SEED__SEEDQR:
- e = SeedQrEncoder(mnemonic=mnemonic)
+ e = SeedQrEncoder(seed=Seed(mnemonic))
elif qr_type == QRType.SEED__COMPACTSEEDQR:
- e = CompactSeedQrEncoder(mnemonic=mnemonic)
+ e = CompactSeedQrEncoder(seed=Seed(mnemonic))
data = e.next_part()
print(data)