Merge dev into passphrase-exit-dialog and resolved conflicts
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
recursive-include src/seedsigner/resources *
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
# The Push Case
|
||||
|
||||
<img src="images/pushcase.png">
|
||||
|
||||
## 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
|
||||
|
||||
<img src="images/components.png">
|
||||
|
||||
- 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
|
||||
|
||||
<p float="left">
|
||||
<img src="images/stack1.png" height="200">
|
||||
<img src="images/stack2.png" height="200">
|
||||
<img src="images/stack3.png" height="200">
|
||||
<img src="images/stack4.png" height="200">
|
||||
</p>
|
||||
|
||||
- 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
|
||||
|
||||
<p float="left">
|
||||
<img src="images/solder1.png" height="180">
|
||||
<img src="images/solder2.png" height="180">
|
||||
<img src="images/solder3.png" height="180">
|
||||
</p>
|
||||
|
||||
- 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
|
||||
|
||||
<p float="left">
|
||||
<img src="images/camera1.png" height="200">
|
||||
<img src="images/camera2.png" height="200">
|
||||
<img src="images/camera3.png" height="200">
|
||||
</p>
|
||||
|
||||
- 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
|
||||
|
||||
<p float="left">
|
||||
<img src="images/camera4.png" height="200">
|
||||
<img src="images/back1.png" height="200">
|
||||
<img src="images/back2.png" height="200">
|
||||
</p>
|
||||
|
||||
- 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
|
||||
|
||||
<p float="left">
|
||||
<img src="images/front1.png" height="140">
|
||||
<img src="images/front2.png" height="140">
|
||||
<img src="images/front3.png" height="140">
|
||||
<img src="images/front4.png" height="140">
|
||||
</p>
|
||||
|
||||
- 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).
|
||||
|
After Width: | Height: | Size: 362 KiB |
|
After Width: | Height: | Size: 570 KiB |
|
After Width: | Height: | Size: 372 KiB |
|
After Width: | Height: | Size: 594 KiB |
|
After Width: | Height: | Size: 739 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 5.0 MiB |
|
After Width: | Height: | Size: 400 KiB |
|
After Width: | Height: | Size: 477 KiB |
|
After Width: | Height: | Size: 453 KiB |
|
After Width: | Height: | Size: 382 KiB |
|
After Width: | Height: | Size: 341 KiB |
|
After Width: | Height: | Size: 832 KiB |
|
After Width: | Height: | Size: 487 KiB |
|
After Width: | Height: | Size: 476 KiB |
|
After Width: | Height: | Size: 469 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 292 KiB |
@@ -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"]
|
||||
@@ -1,2 +0,0 @@
|
||||
[pytest]
|
||||
testpaths = tests
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 += """ <table align="left" style="border: 1px solid gray;">"""
|
||||
readme += f"""<tr><td align="center">{view_name}<br/><br/><img src="{subdir}/{view_name}.png"></td></tr>"""
|
||||
readme += """</table>\n"""
|
||||
|
||||
readme += "</td></tr></table>"
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
])
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 == []
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||