Merge dev into passphrase-exit-dialog and resolved conflicts

This commit is contained in:
alvroble
2024-07-11 23:37:23 +02:00
63 changed files with 766 additions and 178 deletions
-40
View File
@@ -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
+4 -2
View File
@@ -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
View File
@@ -1 +0,0 @@
recursive-include src/seedsigner/resources *
+1
View File
@@ -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
+15
View File
@@ -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
+5 -5
View File
@@ -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.
+130
View File
@@ -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).
Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

+69
View File
@@ -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"]
-2
View File
@@ -1,2 +0,0 @@
[pytest]
testpaths = tests
+2 -2
View File
@@ -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
-27
View File
@@ -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",
)
+1 -1
View File
@@ -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):
+15 -5
View File
@@ -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(
+9 -18
View File
@@ -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)
+25 -14
View File
@@ -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
+92 -2
View File
@@ -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
+13 -4
View File
@@ -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",
+52 -3
View File
@@ -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):
"""
+60 -20
View File
@@ -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"]
+19 -1
View File
@@ -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),
+2 -2
View File
@@ -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):
+63 -9
View File
@@ -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)
+9 -9
View File
@@ -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
+48 -2
View File
@@ -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)
])
+2 -2
View File
@@ -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")),
+1 -1
View File
@@ -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),
+43
View File
@@ -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 == []
+8 -4
View File
@@ -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
+3 -2
View File
@@ -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)