diff --git a/.gitignore b/.gitignore
index e5df4958..f2651158 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
.DS_Store
__pycache__/
src/seedsigner.egg-info/
+.nova
diff --git a/docs/img/logo.svg b/docs/img/logo.svg
new file mode 100644
index 00000000..1b94c144
--- /dev/null
+++ b/docs/img/logo.svg
@@ -0,0 +1,203 @@
+
+
+
diff --git a/docs/qr_formats.md b/docs/qr_formats.md
index ececf0c9..ecdb930e 100644
--- a/docs/qr_formats.md
+++ b/docs/qr_formats.md
@@ -15,6 +15,7 @@ Static QR Formats:
- Base64 (if the PSBT byte size is too large, SS may have trouble scanning)
- Seed
- SeedSigner Seed QR format
+ - A 48 or 96 length string of numbers representing a bip39 wordlist (all wordlist languages supported). The numeric sequence is a concatination of four-digit zero padded segments. Each four-digit segment represents a bip39 word expressed by a zero-indexed position in the wordlist. For example "0000" is abandon in the english bip39 wordlist.
- English Bip39 Mnemonic words separated by a space. Currently only supports 12 and 24 word seeds.
- English Bip39 Mnemonic with only first 4 letters seperated by a space. Currently only supports 12 and 24 word seeds.
@@ -29,4 +30,5 @@ Animated QR Formats:
Static QR Formats:
- Seed
- - SeedSigner Seed QR format
\ No newline at end of file
+ - SeedSigner Seed QR format
+ - A 48 or 96 length string of numbers representing a bip39 wordlist (all wordlist languages supported). The numeric sequence is a concatination of four-digit zero padded segments. Each four-digit segment represents a bip39 word expressed by a zero-indexed position in the wordlist. For example "0000" is abandon in the english bip39 wordlist.
diff --git a/docs/recovery.md b/docs/recovery.md
index 931c897a..5725dc75 100644
--- a/docs/recovery.md
+++ b/docs/recovery.md
@@ -1,14 +1,35 @@
# SeedSigner Recovery Information
-SeedSigner creates extended public keys for the coordinator following [slip-0132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) to encode the extended public key. SeedSigner only supports native segwit for single sig (P2WPKH) and native segwit multisig (P2WSH).
+SeedSigner creates extended public keys for the coordinator following [slip-0132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) to encode the extended public key. SeedSigner supports native and nested segwit for single sig and multisig.
-SeedSigners default settings for "Script Policy" (2021-08-21) is Multi Sig Native Segwit. The second option is Single Sig Native Segwit.
+Derivation paths for standard script types for mainnet:
-- Single Sig Derivation Path: m/84'/0'/0'
-- Single Sig Public Key Encoding: 0x04b24746 - zpub
-- Multisig Dervivation Path: m/48'/0'/0'/2'
-- Multisig Public Key Encoding: 0x02aa7ed3 - Zpub
+- Single Sig
+ - Native Segwit
+ - Derivation Path: m/84'/0'/0'
+ - Script Type: P2WPKH
+ - Public Key Encoding: 0x04b24746 - zpub
+ - Nested Segwit
+ - Derivation Path: m/49'/0'/0'
+ - Script Type: P2WPKH in P2SH
+ - Public Key Encoding: 0x049d7cb2 - ypub
+- Multisig
+ - Native Segwit
+ - Derivation Path: m/48'/0'/0'/2'
+ - Script Type: P2WSH
+ - Public Key Encoding: 0x02aa7ed3 - Zpub
+ - Nested Segwit
+ - Derivation Path: m/48'/0'/0'/1'
+ - Script Type: P2WSH in P2SH
+ - Public Key Encoding: 0x0295b43f - Ypub
-Changing the network settings from main to test will change the public key encoding and derivation path following bip-0084, bip-0048 and slip-0132 standards.
+Custom derivation paths are also optional when generating an xpub from SeedSigner. The Public Key Encodings is detected based on the derivation path configured. Embit bitcoin library does the detection and is documented [here](https://github.com/diybitcoinhardware/embit/blob/master/docs/api/bip32.md#detect_version). For a video explination of these standards see a presentation by Stepan of Embit/Specter on this topic: https://youtube.com/watch?v=JCaC5DG2HTM
-Custom derivation paths will be optional in the future.
+Changing the network settings from main to test in SeedSigner will change the public key encoding and derivation path following [slip-0132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) standards.
+
+Related Standards:
+- [bip-0032](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)
+- [bip-0044](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki)
+- [bip-0048](https://github.com/bitcoin/bips/blob/master/bip-0048.mediawiki)
+- [bip-0049](https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki)
+- [bip-0084](https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki)
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index c9156ae1..702fce85 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-embit==0.4.5
+embit==0.4.10
numpy==1.21.1
picamera==1.13
Pillow==8.2.0
@@ -6,4 +6,4 @@ pyzbar==0.1.8
qrcode==6.1
RPi.GPIO==0.7.0
six==1.16.0
-spidev==3.5
\ No newline at end of file
+spidev==3.5
diff --git a/setup.py b/setup.py
index 66fa1c99..bfee1ee7 100644
--- a/setup.py
+++ b/setup.py
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
setuptools.setup(
name="seedsigner",
- version="0.4.1",
+ version="0.4.4",
author="SeedSigner",
author_email="author@example.com",
description="Build an offline, airgapped Bitcoin signing device for less than $50!",
diff --git a/src/default_settings.ini b/src/default_settings.ini
index 48d3e892..2aeffa0d 100644
--- a/src/default_settings.ini
+++ b/src/default_settings.ini
@@ -5,11 +5,11 @@ persistent_settings = False
[display]
text_color = ORANGE
+qr_background_color = FFFFFF
+camera_rotation = 0
[wallet]
network = main
software = Prompt
qr_density = 2
-script_policy = PKWSH
-custom_derivation_enabled = False
custom_derivation = m/0/0
\ No newline at end of file
diff --git a/src/seedsigner/camera.py b/src/seedsigner/camera.py
index e2af97be..edac1702 100644
--- a/src/seedsigner/camera.py
+++ b/src/seedsigner/camera.py
@@ -10,6 +10,16 @@ from seedsigner.helpers import PiVideoStream, Singleton
class Camera(Singleton):
_video_stream = None
_picamera = None
+ _camera_rotation = None
+
+ @classmethod
+ def get_instance(cls):
+ # This is the only way to access the one and only Controller
+ from seedsigner.models import Settings
+ if cls._instance is None:
+ cls._instance = cls.__new__(cls)
+ cls._instance._camera_rotation = Settings.get_instance().camera_rotation
+ return cls._instance
def start_video_stream_mode(self, resolution=(512, 384), framerate=12, format="bgr"):
@@ -28,7 +38,7 @@ class Camera(Singleton):
return frame
else:
if frame is not None:
- return Image.fromarray(frame.astype('uint8'), 'RGB').rotate(90)
+ return Image.fromarray(frame.astype('uint8'), 'RGB').rotate(90 + self._camera_rotation)
return None
@@ -59,37 +69,12 @@ class Camera(Singleton):
self._picamera.awb_mode = 'off'
self._picamera.awb_gains = g
- """
- When outputting to unencoded formats, the camera rounds the requested resolution.
- The horizontal resolution is rounded up to the nearest multiple of 32 pixels,
- while the vertical resolution is rounded up to the nearest multiple of 16 pixels.
- """
- # resolution = self._picamera.resolution
- # if resolution.width % 32 == 0:
- # raw_width = resolution.width
- # else:
- # raw_width = resolution.width - (resolution.width % 32) + 32
-
- # if resolution.height % 16 == 0:
- # raw_height = resolution.height
- # else:
- # raw_height = resolution.height + 16
-
- # print(resolution)
- # print(raw_width, raw_height)
- # np_array = numpy.empty((raw_width, raw_height, 3), dtype=numpy.uint8)
- # self._picamera.capture(np_array, format='rgb')
-
- # np_array.reshape((raw_width, raw_height, 3))
-
- # return Image.fromarray(np_array[:resolution.width, :resolution.height, :].astype('uint8'), 'RGB')
-
stream = io.BytesIO()
self._picamera.capture(stream, format='jpeg')
# "Rewind" the stream to the beginning so we can read its content
stream.seek(0)
- return Image.open(stream)
+ return Image.open(stream).rotate(90 + self._camera_rotation)
def stop_single_frame_mode(self):
diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py
index 29944f6e..9f601e8d 100644
--- a/src/seedsigner/controller.py
+++ b/src/seedsigner/controller.py
@@ -1,19 +1,20 @@
# External Dependencies
import time
+import re
from multiprocessing import Process, Queue
from subprocess import call
import os, sys
-from embit import bip39, bip32
+from embit import bip32, script, ec
from embit.networks import NETWORKS
+from embit.descriptor import Descriptor
from binascii import hexlify
from threading import Thread
# Internal file class dependencies
from .views import (View, MenuView, SeedToolsView,SigningToolsView,
- SettingsToolsView, IOTestView)
+ SettingsToolsView, IOTestView, OpeningSplashView, ScreensaverView)
from .helpers import Buttons, B, Path, Singleton
-from .models import (SeedStorage, Settings, DecodeQR, DecodeQRStatus,
- EncodeQRDensity, EncodeQR, PSBTParser, QRType)
+from .models import (EncodeQRDensity, QRType, Seed, SeedStorage, Settings, DecodeQR, DecodeQRStatus, EncodeQR, PSBTParser)
class Controller(Singleton):
"""
@@ -32,7 +33,7 @@ class Controller(Singleton):
Note: In many/most cases you'll need to do the Controller import within a method
rather than at the top in order avoid circular imports.
"""
- VERSION = "0.4.4"
+ VERSION = "0.4.5"
@classmethod
@@ -65,6 +66,7 @@ class Controller(Singleton):
# settings
controller.DEBUG = controller.settings.debug
controller.color = controller.settings.text_color
+ controller.current_bg_qr_color = controller.settings.qr_background_color
# Views
controller.menu_view = MenuView()
@@ -72,6 +74,10 @@ class Controller(Singleton):
controller.io_test_view = IOTestView()
controller.signing_tools_view = SigningToolsView(controller.storage)
controller.settings_tools_view = SettingsToolsView()
+ controller.screensaver = ScreensaverView(controller.buttons)
+
+ controller.screensaver_activation_ms = 120 * 1000
+
@property
def camera(self):
@@ -80,6 +86,9 @@ class Controller(Singleton):
def start(self) -> None:
+ opening_splash = OpeningSplashView()
+ opening_splash.start()
+
if self.DEBUG:
# Let Exceptions halt execution
try:
@@ -107,6 +116,10 @@ class Controller(Singleton):
self.menu_view.draw_modal(["Crashed ..."], "", "requires hard restart")
+ def start_screensaver(self):
+ self.screensaver.start()
+
+
### Menu
### Menu View handles navigation within the menu
### Sub Menu's like Seed Tools, Signing Tools, Settings are all in the Menu View
@@ -142,10 +155,10 @@ class Controller(Singleton):
ret_val = self.show_wallet_tool()
elif ret_val == Path.QR_DENSITY_SETTING:
ret_val = self.show_qr_density_tool()
- elif ret_val == Path.WALLET_POLICY:
- ret_val = self.show_wallet_policy_tool()
elif ret_val == Path.PERSISTENT_SETTINGS:
ret_val = self.show_persistent_settings_tool()
+ elif ret_val == Path.CAMERA_ROTATION:
+ ret_val = self.show_camera_rotation_tool()
elif ret_val == Path.DONATE:
ret_val = self.show_donate_tool()
elif ret_val == Path.RESET:
@@ -174,26 +187,26 @@ class Controller(Singleton):
### Generate Last Word 12 / 24 Menu
def show_generate_last_word_tool(self) -> int:
- seed_phrase = []
+ seed = Seed(wordlist=self.settings.wordlist)
ret_val = 0
while True:
# display menu to select 12 or 24 word seed for last word
ret_val = self.menu_view.display_12_24_word_menu("... [ Return to Seed Tools ]")
if ret_val == Path.SEED_WORD_12:
- seed_phrase = self.seed_tools_view.display_manual_seed_entry(11)
+ seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(11)
elif ret_val == Path.SEED_WORD_24:
- seed_phrase = self.seed_tools_view.display_manual_seed_entry(23)
+ seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(23)
else:
return Path.SEED_TOOLS_SUB_MENU
- if len(seed_phrase) > 0:
- completed_seed_phrase = self.seed_tools_view.display_last_word(seed_phrase)
+ if len(seed.mnemonic_list) > 0:
+ seed.mnemonic = self.seed_tools_view.display_last_word(seed.mnemonic_display_list)
break
# display seed phrase
while True:
- ret_val = self.seed_tools_view.display_seed_phrase(completed_seed_phrase, show_qr_option=True)
+ ret_val = self.seed_tools_view.display_seed_phrase(seed.mnemonic, show_qr_option=True)
if ret_val == True:
break
else:
@@ -206,7 +219,7 @@ class Controller(Singleton):
if r == 1: #Yes
slot_num = self.menu_view.display_saved_seed_menu(self.storage,2,None)
if slot_num in (1,2,3):
- self.storage.save_seed_phrase(completed_seed_phrase, slot_num)
+ self.storage.add_seed(seed, slot_num)
self.menu_view.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Main Menu")
input = self.buttons.wait_for([B.KEY_RIGHT])
@@ -215,19 +228,19 @@ class Controller(Singleton):
### Create a Seed w/ Dice Screen
def show_create_seed_with_dice_tool(self) -> int:
- seed_phrase = []
+ seed = Seed(wordlist=self.settings.wordlist)
ret_val = True
while True:
- seed_phrase = self.seed_tools_view.display_generate_seed_from_dice()
- if len(seed_phrase) > 0:
+ seed.mnemonic = self.seed_tools_view.display_generate_seed_from_dice()
+ if seed:
break
else:
return Path.SEED_TOOLS_SUB_MENU
# display seed phrase (24 words)
while True:
- ret_val = self.seed_tools_view.display_seed_phrase(seed_phrase, show_qr_option=True)
+ ret_val = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, show_qr_option=True)
if ret_val == True:
break
else:
@@ -240,30 +253,32 @@ class Controller(Singleton):
if r == 1: #Yes
slot_num = self.menu_view.display_saved_seed_menu(self.storage,2,None)
if slot_num in (1,2,3):
- self.storage.save_seed_phrase(seed_phrase, slot_num)
+ self.storage.add_seed(seed, slot_num)
self.menu_view.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Main Menu")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
+ ### Create a Seed w/ Image
+
def show_create_seed_with_image_tool(self) -> int:
- seed_phrase = []
+ seed = Seed(wordlist=self.settings.wordlist)
ret_val = True
while True:
- (reshoot, seed_phrase) = self.seed_tools_view.seed_phrase_from_camera_image()
+ (reshoot, seed.mnemonic) = self.seed_tools_view.seed_phrase_from_camera_image()
if reshoot:
# Relaunch into another image capture cycle
continue
- if len(seed_phrase) > 0:
+ if seed:
break
else:
return Path.SEED_TOOLS_SUB_MENU
# display seed phrase (24 words)
while True:
- ret_val = self.seed_tools_view.display_seed_phrase(seed_phrase, show_qr_option=True)
+ ret_val = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, show_qr_option=True)
if ret_val == True:
break
else:
@@ -276,15 +291,16 @@ class Controller(Singleton):
if r == 1: #Yes
slot_num = self.menu_view.display_saved_seed_menu(self.storage,2,None)
if slot_num in (1,2,3):
- self.storage.save_seed_phrase(seed_phrase, slot_num)
+ self.storage.add_seed(seed, slot_num)
self.menu_view.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Main Menu")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
+
### Store a seed (temp) Menu
def show_store_a_seed_tool(self):
- seed_phrase = []
+ seed = Seed(wordlist=self.settings.wordlist)
ret_val = 0
display_saved_seed = False
ret_val = self.menu_view.display_saved_seed_menu(self.storage, 1, "... [ Return to Seed Tools ]")
@@ -309,26 +325,25 @@ class Controller(Singleton):
# display menu to select 12 or 24 word seed for last word
ret_val = self.menu_view.display_qr_12_24_word_menu("... [ Return to Seed Tools ]")
if ret_val == Path.SEED_WORD_12:
- seed_phrase = self.seed_tools_view.display_manual_seed_entry(12)
+ seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(12)
elif ret_val == Path.SEED_WORD_24:
- seed_phrase = self.seed_tools_view.display_manual_seed_entry(24)
+ seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(24)
elif ret_val == Path.SEED_WORD_QR:
- seed_phrase = self.seed_tools_view.read_seed_phrase_qr()
+ seed.mnemonic = self.seed_tools_view.read_seed_phrase_qr()
else:
return Path.SEED_TOOLS_SUB_MENU
- if len(seed_phrase) == 0:
+ if not seed:
+ # seed is not valid
return Path.SEED_TOOLS_SUB_MENU
- if ret_val == Path.SEED_WORD_QR and len(seed_phrase) > 0:
+ if ret_val == Path.SEED_WORD_QR and seed:
show_qr_option = False
else:
show_qr_option = True
- self.menu_view.draw_modal(["Validating ..."])
- is_valid = self.storage.check_if_seed_valid(seed_phrase)
- if is_valid == False:
- # Exit if not valid with message
+ if not seed:
+ # Seed is not valid, Exit if not valid with message
self.menu_view.draw_modal(["Seed Invalid", "check seed phrase", "and try again", ""], "", "Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.SEED_TOOLS_SUB_MENU
@@ -337,15 +352,15 @@ class Controller(Singleton):
input = self.buttons.wait_for([B.KEY_RIGHT])
while display_saved_seed == False:
- r = self.seed_tools_view.display_seed_phrase(seed_phrase, show_qr_option=show_qr_option )
+ r = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, show_qr_option=show_qr_option )
if r == True:
break
else:
# no-op; can't back out of the seed phrase view
pass
- if is_valid:
- self.storage.save_seed_phrase(seed_phrase, slot_num)
+ if seed:
+ self.storage.add_seed(seed, slot_num)
self.menu_view.draw_modal(["", "Saved to Slot #" + str(slot_num)], "", "Right to Exit")
input = self.buttons.wait_for([B.KEY_RIGHT])
@@ -394,9 +409,9 @@ class Controller(Singleton):
self.storage.save_passphrase(passphrase, slot_num)
if r in (0,1):
- self.menu_view.draw_modal(["Passphrase Added", passphrase, "Added to Slot #" + str(slot_num)], "", "Right to Continue")
+ self.menu_view.draw_passphrase("Passphrase Added", passphrase, "Right to Continue")
elif r == 2:
- self.menu_view.draw_modal(["Passphrase Updated", passphrase, "Added to Slot #" + str(slot_num)], "", "Right to Continue")
+ self.menu_view.draw_passphrase("Passphrase Updated", passphrase, "Right to Continue")
self.buttons.wait_for([B.KEY_RIGHT])
return Path.SEED_TOOLS_SUB_MENU
@@ -408,8 +423,7 @@ class Controller(Singleton):
### Generate XPUB
def show_generate_xpub(self):
- seed_phrase = []
- passphrase = ""
+ seed = Seed(wordlist=self.settings.wordlist)
# If there is a saved seed, ask to use saved seed
if self.storage.num_of_saved_seeds() > 0:
@@ -418,29 +432,26 @@ class Controller(Singleton):
slot_num = self.menu_view.display_saved_seed_menu(self.storage,3,None)
if slot_num not in (1,2,3):
return Path.SEED_TOOLS_SUB_MENU
- seed_phrase = self.storage.get_seed_phrase(slot_num)
- passphrase = self.storage.get_passphrase(slot_num)
+ seed = self.storage.get_seed(slot_num)
- if len(seed_phrase) == 0:
- # gather seed phrase
+ if not seed:
+ # no valid seed, gather seed phrase
# display menu to select 12 or 24 word seed for last word
ret_val = self.menu_view.display_qr_12_24_word_menu("... [ Return to Sign Tools ]")
if ret_val == Path.SEED_WORD_12:
- seed_phrase = self.seed_tools_view.display_manual_seed_entry(12)
+ seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(12)
elif ret_val == Path.SEED_WORD_24:
- seed_phrase = self.seed_tools_view.display_manual_seed_entry(24)
+ seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(24)
elif ret_val == Path.SEED_WORD_QR:
- seed_phrase = self.seed_tools_view.read_seed_phrase_qr()
+ seed.mnemonic = self.seed_tools_view.read_seed_phrase_qr()
else:
return Path.SEED_TOOLS_SUB_MENU
- if len(seed_phrase) == 0:
+ if not seed:
return Path.SEED_TOOLS_SUB_MENU
# check if seed phrase is valid
- self.menu_view.draw_modal(["Validating ..."])
- is_valid = self.storage.check_if_seed_valid(seed_phrase)
- if is_valid == False:
+ if not seed:
self.menu_view.draw_modal(["Seed Invalid", "check seed phrase", "and try again"], "", "Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
@@ -448,41 +459,55 @@ class Controller(Singleton):
r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Add Seed Passphrase?")
if r == 1:
# display a tool to pick letters/numbers to make a passphrase
- passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry()
- if len(passphrase) == 0 or passphrase == "-1":
- passphrase = ""
+ seed.passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry()
+ if len(seed.passphrase) == 0:
self.menu_view.draw_modal(["No passphrase added", "to seed words"], "", "Left to Exit, Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT])
if input == B.KEY_LEFT:
return Path.MAIN_MENU
else:
- self.menu_view.draw_modal(["Optional passphrase", "added to seed words", passphrase], "", "Right to Continue")
+ self.menu_view.draw_modal(["Optional passphrase", "added to seed words", seed.passphrase], "", "Right to Continue")
self.buttons.wait_for([B.KEY_RIGHT])
# display seed phrase
while True:
- r = self.seed_tools_view.display_seed_phrase(seed_phrase, passphrase, "Right to Continue")
+ r = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, seed.passphrase, "Right to Continue")
if r == True:
break
else:
# Cancel
return Path.SEED_TOOLS_SUB_MENU
-
- self.signing_tools_view.draw_modal(["Loading xPub Info ..."])
-
- seed = bip39.mnemonic_to_seed((" ".join(seed_phrase)).strip(), passphrase)
- root = bip32.HDKey.from_seed(seed, version=NETWORKS[self.settings.network]["xprv"])
- fingerprint = hexlify(root.child(0).fingerprint).decode('utf-8')
- bip48_xprv = root.derive(self.settings.derivation)
- bip48_xpub = bip48_xprv.to_public()
- if self.settings.script_policy == "PKWPKH":
- xpub = bip48_xpub.to_base58(NETWORKS[self.settings.network]["zpub"])
+
+ # choose single sig or multisig wallet type
+ wallet_type = "multisig"
+ script_type = "native segwit"
+ derivation = self.settings.custom_derivation
+ r = self.menu_view.display_generic_selection_menu(["Single Sig", "Multisig"], "Wallet Type?")
+ if r == 1:
+ wallet_type = "single sig"
+ elif r == 2:
+ wallet_type = "multisig"
+
+ # choose derivation standard
+ r = self.menu_view.display_generic_selection_menu(["Native Segwit", "Nested Segwit", "Custom"], "Derivation Path?")
+ if r == 1:
+ script_type = "native segwit"
+ elif r == 2:
+ script_type = "nested segwit"
+ elif r == 3:
+ script_type = "custom"
+
+ # calculated derivation or get custom from keyboard entry
+ if script_type == "custom":
+ derivation = self.settings_tools_view.draw_derivation_keyboard_entry(existing_derivation=self.settings.custom_derivation)
+ self.settings.custom_derivation = derivation # save for next time
else:
- xpub = bip48_xpub.to_base58(NETWORKS[self.settings.network]["Zpub"])
-
- self.signing_tools_view.display_xpub_info(fingerprint, self.settings.derivation, xpub)
- self.buttons.wait_for([B.KEY_RIGHT])
-
+ derivation = Settings.calc_derivation(self.settings.network, wallet_type, script_type)
+
+ if derivation == "" or derivation == None:
+ self.menu_view.draw_modal(["Invalid Derivation", "try again"], "", "Right to Continue")
+ return Path.SEED_TOOLS_SUB_MENU
+
if self.settings.software == "Prompt":
lines = ["Specter Desktop", "Blue Wallet", "Sparrow"]
r = self.menu_view.display_generic_selection_menu(lines, "Which Wallet?")
@@ -490,28 +515,55 @@ class Controller(Singleton):
else:
qr_xpub_type = self.settings.qr_xpub_type
+ self.signing_tools_view.draw_modal(["Loading xPub Info ..."])
+
+ version = bip32.detect_version(derivation, default="xpub", network=NETWORKS[self.settings.network])
+ root = bip32.HDKey.from_seed(seed.seed, version=NETWORKS[self.settings.network]["xprv"])
+ fingerprint = hexlify(root.child(0).fingerprint).decode('utf-8')
+ xprv = root.derive(derivation)
+ xpub = xprv.to_public()
+ xpub_base58 = xpub.to_string(version=version)
+
+ self.signing_tools_view.display_xpub_info(fingerprint, derivation, xpub_base58)
+ self.buttons.wait_for([B.KEY_RIGHT])
+
self.signing_tools_view.draw_modal(["Generating xPub QR ..."])
- e = EncodeQR(seed_phrase=seed_phrase, passphrase=passphrase, derivation=self.settings.derivation, network=self.settings.network, policy=self.settings.script_policy, qr_type=qr_xpub_type, qr_density=self.settings.qr_density)
+ e = EncodeQR(seed_phrase=seed.mnemonic_list, passphrase=seed.passphrase, derivation=derivation, network=self.settings.network, qr_type=qr_xpub_type, qr_density=self.settings.qr_density, wordlist=self.settings.wordlist)
while e.totalParts() > 1:
- image = e.nextPartImage(240,240,2)
- View.DispShowImage(image)
- time.sleep(0.1)
- if self.buttons.check_for_low(B.KEY_RIGHT):
+
+ cur_time = int(time.time() * 1000)
+ if cur_time - self.buttons.last_input_time > self.screensaver_activation_ms and not self.screensaver.is_running:
+ self.start_screensaver()
+ self.buttons.update_last_input_time()
+ else:
+ image = e.nextPartImage(240,240,2,background=self.current_bg_qr_color)
+ View.DispShowImage(image)
+ time.sleep(0.1)
+ if self.buttons.check_for_low(B.KEY_RIGHT):
break
+ elif self.buttons.check_for_low(B.KEY_UP):
+ self.prev_qr_background_color()
+ elif self.buttons.check_for_low(B.KEY_DOWN):
+ self.next_qr_background_color()
- if e.totalParts() == 1:
- image = e.nextPartImage(240,240,1)
+ while e.totalParts() == 1:
+ image = e.nextPartImage(240,240,1,background=self.current_bg_qr_color)
View.DispShowImage(image)
- self.buttons.wait_for([B.KEY_RIGHT])
+ input = self.buttons.wait_for([B.KEY_RIGHT,B.KEY_UP,B.KEY_DOWN])
+ if input == B.KEY_RIGHT:
+ break
+ elif input == B.KEY_UP:
+ self.prev_qr_background_color()
+ elif input == B.KEY_DOWN:
+ self.next_qr_background_color()
return Path.MAIN_MENU
### Sign Transactions
def show_sign_transaction(self):
- seed_phrase = []
- passphrase = ""
+ seed = Seed(wordlist=self.settings.wordlist)
used_saved_seed = False
# reusable qr scan function
@@ -519,7 +571,7 @@ class Controller(Singleton):
# Scan QR using Camera
self.menu_view.draw_modal(["Initializing Camera"])
self.camera.start_video_stream_mode(resolution=(480, 480), framerate=12, format="rgb")
- decoder = DecodeQR()
+ decoder = DecodeQR(wordlist=self.settings.wordlist)
def live_preview(camera, decoder, scan_text):
while True:
@@ -527,8 +579,8 @@ class Controller(Singleton):
if frame is not None:
if decoder.getPercentComplete() > 0 and decoder.isPSBT():
scan_text = str(decoder.getPercentComplete()) + "% Complete"
- View.DispShowImageWithText(frame.resize((240,240)), scan_text, font=View.IMPACT22, text_color=View.color, text_background=(0,0,0,225))
- time.sleep(0.1) # turn this up or down to tune performace while decoding psbt
+ View.DispShowImageWithText(frame.resize((240,240)), scan_text, font=View.ASSISTANT22, text_color=View.color, text_background=(0,0,0,225))
+ time.sleep(0.1) # turn this up or down to tune performance while decoding psbt
if camera._video_stream is None:
break
@@ -567,10 +619,9 @@ class Controller(Singleton):
elif decoder.isComplete() and decoder.isSeed():
# first QR is Seed
self.menu_view.draw_modal(["Validating Seed"])
- seed_phrase = decoder.getSeedPhrase()
- is_valid = self.storage.check_if_seed_valid(seed_phrase)
- if is_valid == False:
- # Exit if not valid with message
+ seed.mnemonic = decoder.getSeedPhrase()
+ if not seed:
+ # seed is not valid, Exit if not valid with message
self.menu_view.draw_modal(["Seed Invalid", "check seed phrase", "and try again", ""], "", "Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
@@ -581,9 +632,8 @@ class Controller(Singleton):
r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Add Seed Passphrase?")
if r == 1:
# display a tool to pick letters/numbers to make a passphrase
- passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry()
- if len(passphrase) == 0 or passphrase == "-1":
- passphrase = ""
+ seed.passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry()
+ if len(seed.passphrase) == 0:
self.menu_view.draw_modal(["No passphrase added", "to seed words"], "", "Left to Exit, Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT])
if input == B.KEY_LEFT:
@@ -598,14 +648,13 @@ class Controller(Singleton):
if r == 1: #Yes
slot_num = self.menu_view.display_saved_seed_menu(self.storage,2,None)
if slot_num in (1,2,3):
- self.storage.save_seed_phrase(seed_phrase, slot_num)
- self.storage.save_passphrase(passphrase, slot_num)
+ self.storage.add_seed(seed, slot_num)
self.menu_view.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT])
# display seed phrase
while True:
- r = self.seed_tools_view.display_seed_phrase(seed_phrase, passphrase, "Right to Continue")
+ r = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, seed.passphrase, "Right to Continue")
if r == True:
break
else:
@@ -626,6 +675,208 @@ class Controller(Singleton):
else:
return Path.MAIN_MENU
+ elif decoder.isComplete() and decoder.isAddress():
+
+ address = decoder.getAddress()
+ address_type = decoder.getAddressType()
+
+ self.menu_view.draw_address(address)
+ self.buttons.wait_for([B.KEY_RIGHT])
+
+ validate_network = NETWORKS[self.settings.network]
+ validate_network_text = self.settings.network
+ if "main" in address_type:
+ validate_network = NETWORKS["main"]
+ validate_network_text = "main"
+ elif "test" in address_type:
+ validate_network = NETWORKS["test"]
+ validate_network_text = "test"
+
+ r = 0
+ if address_type in ("Bech32-main", "Bech32-test") and len(address) == 62:
+ r = 2
+ else:
+ # check single sig vs multi sig address
+ r = self.menu_view.display_generic_selection_menu(["Single Sig Seed", "JSON Wallet Desc"], "Validate Bitcoin Address")
+
+ if r == 1:
+
+ # validate single sig using seed
+
+ # No valid seed yet, If there is a saved seed, ask to use saved seed
+ if self.storage.num_of_saved_seeds() > 0:
+ r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Use Save Seed?")
+ if r == 1: #Yes
+ slot_num = self.menu_view.display_saved_seed_menu(self.storage,3,None)
+ if slot_num == 0:
+ return Path.MAIN_MENU
+ seed = self.storage.get_seed(slot_num)
+ used_saved_seed = True
+
+ if not seed:
+ # no valid seed yet, gather seed phrase
+ # display menu to select 12 or 24 word seed for last word
+ ret_val = self.menu_view.display_qr_12_24_word_menu("... [ Cancel ]")
+ if ret_val == Path.SEED_WORD_12:
+ seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(12)
+ elif ret_val == Path.SEED_WORD_24:
+ seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(24)
+ elif ret_val == Path.SEED_WORD_QR:
+ seed.mnemonic = self.seed_tools_view.read_seed_phrase_qr()
+ else:
+ return Path.MAIN_MENU
+
+ if not seed:
+ return Path.MAIN_MENU
+
+ # check if seed phrase is valid
+ self.menu_view.draw_modal(["Validating Seed ..."])
+ if not seed:
+ self.menu_view.draw_modal(["Seed Invalid", "check seed phrase", "and try again"], "", "Right to Continue")
+ input = self.buttons.wait_for([B.KEY_RIGHT])
+ return Path.MAIN_MENU
+
+ if len(seed.passphrase) == 0:
+ r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Add Seed Passphrase?")
+ if r == 1:
+ # display a tool to pick letters/numbers to make a passphrase
+ seed.passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry()
+ if len(seed.passphrase) == 0:
+ self.menu_view.draw_modal(["No passphrase added", "to seed words"], "", "Left to Exit, Right to Continue")
+ input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT])
+ if input == B.KEY_LEFT:
+ return Path.MAIN_MENU
+ else:
+ self.menu_view.draw_modal(["Optional passphrase", "added to seed words", seed.passphrase], "", "Right to Continue")
+ self.buttons.wait_for([B.KEY_RIGHT])
+
+ # display seed phrase
+ while True:
+ r = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, seed.passphrase, "Right to Continue")
+ if r == True:
+ break
+ else:
+ # Cancel
+ return Path.MAIN_MENU
+
+ # Ask to save seed
+ if self.storage.slot_avaliable() and used_saved_seed == False:
+ r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Save Seed?")
+ if r == 1: #Yes
+ slot_num = self.menu_view.display_saved_seed_menu(self.storage,2,None)
+ if slot_num in (1,2,3):
+ self.storage.add_seed(seed, slot_num)
+ self.menu_view.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Continue")
+ input = self.buttons.wait_for([B.KEY_RIGHT])
+
+ #
+ # Validate if address from seed
+ #
+
+ # choose derivation standard
+ r = self.menu_view.display_generic_selection_menu(["Native Segwit", "Nested Segwit", "Custom"], "Derivation Path?")
+ if r == 1:
+ script_type = "native segwit"
+ elif r == 2:
+ script_type = "nested segwit"
+ elif r == 3:
+ script_type = "custom"
+
+ # calculated derivation or get custom from keyboard entry
+ if script_type == "custom":
+ derivation = self.settings_tools_view.draw_derivation_keyboard_entry(existing_derivation=self.settings.custom_derivation)
+ self.settings.custom_derivation = derivation # save for next time
+ else:
+ derivation = Settings.calc_derivation(validate_network_text, "single sig", script_type)
+
+ if derivation == "" or derivation == None:
+ self.menu_view.draw_modal(["Invalid Derivation", "try again"], "", "Right to Continue")
+ return Path.SEED_TOOLS_SUB_MENU
+
+ self.menu_view.draw_modal(["Checking Address", ""], "", "")
+
+ version = bip32.detect_version(derivation, default="xpub", network=validate_network)
+ root = bip32.HDKey.from_seed(seed.seed, version=validate_network["xprv"])
+ fingerprint = hexlify(root.child(0).fingerprint).decode('utf-8')
+ xprv = root.derive(derivation)
+ xpub = xprv.to_public()
+
+ for i in range(500):
+ r_pubkey = xpub.derive([0,i]).key
+ c_pubkey = xpub.derive([1,i]).key
+
+ recieve_address = ""
+ change_address = ""
+
+ if "P2PKH" in address_type:
+ recieve_address = script.p2pkh(r_pubkey).address(network=validate_network)
+ change_address = script.p2pkh(c_pubkey).address(network=validate_network)
+ elif "Bech32" in address_type:
+ recieve_address = script.p2wpkh(r_pubkey).address(network=validate_network)
+ change_address = script.p2wpkh(c_pubkey).address(network=validate_network)
+ elif "P2SH" in address_type:
+ recieve_address = script.p2sh(script.p2wpkh(r_pubkey)).address(network=validate_network)
+ change_address = script.p2sh(script.p2wpkh(c_pubkey)).address(network=validate_network)
+
+ if address == recieve_address:
+ self.menu_view.draw_modal(["Receive Address "+str(i), "Verified"], "", "Right to Exit")
+ input = self.buttons.wait_for([B.KEY_RIGHT])
+ return Path.MAIN_MENU
+ if address == change_address:
+ self.menu_view.draw_modal(["Change Address "+str(i), "Verified"], "", "Right to Exit")
+ input = self.buttons.wait_for([B.KEY_RIGHT])
+ return Path.MAIN_MENU
+ else:
+ self.menu_view.draw_modal(["Checking Address "+str(i), "..."], "", "Right to Abort")
+ if self.buttons.check_for_low(B.KEY_RIGHT) or self.buttons.check_for_low(B.KEY_LEFT):
+ return Path.MAIN_MENU
+
+ if r == 2:
+
+ # second QR scan need PSBT now
+ decoder2 = scan_qr("Scan Backup JSON QR")
+
+ if decoder2.isComplete() and decoder2.isWalletDescriptor():
+ desc_str = decoder2.getWalletDescriptor()
+ desc_str = desc_str.replace("\n","").replace(" ","")
+ try:
+ if len(re.findall (r'\[([0-9,a-f,A-F]+?)(\/[0-9,\/,h\']+?)\].*?(\/0\/\*)', desc_str)) > 0:
+ p = re.compile(r'(\[[0-9,a-f,A-F]+?\/[0-9,\/,h\']+?\].*?)(\/0\/\*)')
+ desc_str = p.sub(r'\1/{0,1}/*', desc_str)
+ elif len(re.findall (r'(\[[0-9,a-f,A-F]+?\/[0-9,\/,h,\']+?\][a-z,A-Z,0-9]*?)([\,,\)])', desc_str)) > 0:
+ p = re.compile(r'(\[[0-9,a-f,A-F]+?\/[0-9,\/,h,\']+?\][a-z,A-Z,0-9]*?)([\,,\)])')
+ desc_str = p.sub(r'\1/{0,1}/*\2', desc_str)
+ except:
+ desc_str = decoder2.getWalletDescriptor()
+ print(desc_str)
+ desc = Descriptor.from_string(desc_str)
+ else:
+ return Path.MAIN_MENU
+
+ #
+ # Validate if address from descriptor
+ #
+
+ for i in range(1000):
+ r_derived = desc.derive(i,branch_index=0)
+ c_derived = desc.derive(i,branch_index=1)
+
+ recieve_address = r_derived.address(validate_network)
+ change_address = c_derived.address(validate_network)
+
+ if address == recieve_address:
+ self.menu_view.draw_modal(["Recieved Address "+str(i), "Verified"], "", "Right to Exit")
+ input = self.buttons.wait_for([B.KEY_RIGHT])
+ return Path.MAIN_MENU
+ if address == change_address:
+ self.menu_view.draw_modal(["Change Address "+str(i), "Verified"], "", "Right to Exit")
+ input = self.buttons.wait_for([B.KEY_RIGHT])
+ return Path.MAIN_MENU
+ else:
+ self.menu_view.draw_modal(["Checking Address "+str(i), "..."], "", "Right to Abort")
+ if self.buttons.check_for_low(B.KEY_RIGHT) or self.buttons.check_for_low(B.KEY_LEFT):
+ return Path.MAIN_MENU
+
elif ( decoder.isComplete() and not decoder.isPSBT() ) or decoder.isInvalid():
self.menu_view.draw_modal(["Not a valid PSBT QR"], "", "Right to Exit")
input = self.buttons.wait_for([B.KEY_RIGHT])
@@ -633,62 +884,57 @@ class Controller(Singleton):
else:
return Path.MAIN_MENU
-
- if len(seed_phrase) == 0:
-
- # If there is a saved seed, ask to use saved seed
+ if not seed:
+ # No valid seed yet, If there is a saved seed, ask to use saved seed
if self.storage.num_of_saved_seeds() > 0:
r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Use Save Seed?")
if r == 1: #Yes
slot_num = self.menu_view.display_saved_seed_menu(self.storage,3,None)
if slot_num == 0:
return Path.MAIN_MENU
- seed_phrase = self.storage.get_seed_phrase(slot_num)
- passphrase = self.storage.get_passphrase(slot_num)
+ seed = self.storage.get_seed(slot_num)
used_saved_seed = True
- if len(seed_phrase) == 0:
- # gather seed phrase
+ if not seed:
+ # no valid seed yet, gather seed phrase
# display menu to select 12 or 24 word seed for last word
ret_val = self.menu_view.display_qr_12_24_word_menu("... [ Return to Main Menu ]")
if ret_val == Path.SEED_WORD_12:
- seed_phrase = self.seed_tools_view.display_manual_seed_entry(12)
+ seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(12)
elif ret_val == Path.SEED_WORD_24:
- seed_phrase = self.seed_tools_view.display_manual_seed_entry(24)
+ seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(24)
elif ret_val == Path.SEED_WORD_QR:
- seed_phrase = self.seed_tools_view.read_seed_phrase_qr()
+ seed.mnemonic = self.seed_tools_view.read_seed_phrase_qr()
else:
return Path.MAIN_MENU
- if len(seed_phrase) == 0:
+ if not seed:
return Path.MAIN_MENU
# check if seed phrase is valid
self.menu_view.draw_modal(["Validating Seed ..."])
- is_valid = self.storage.check_if_seed_valid(seed_phrase)
- if is_valid == False:
+ if not seed:
self.menu_view.draw_modal(["Seed Invalid", "check seed phrase", "and try again"], "", "Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
- if len(passphrase) == 0:
+ if len(seed.passphrase) == 0:
r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Add Seed Passphrase?")
if r == 1:
# display a tool to pick letters/numbers to make a passphrase
- passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry()
- if len(passphrase) == 0 or passphrase == "-1":
- passphrase = ""
+ seed.passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry()
+ if len(seed.passphrase) == 0:
self.menu_view.draw_modal(["No passphrase added", "to seed words"], "", "Left to Exit, Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT])
if input == B.KEY_LEFT:
return Path.MAIN_MENU
else:
- self.menu_view.draw_modal(["Optional passphrase", "added to seed words", passphrase], "", "Right to Continue")
+ self.menu_view.draw_modal(["Optional passphrase", "added to seed words", seed.passphrase], "", "Right to Continue")
self.buttons.wait_for([B.KEY_RIGHT])
# display seed phrase
while True:
- r = self.seed_tools_view.display_seed_phrase(seed_phrase, passphrase, "Right to Continue")
+ r = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, seed.passphrase, "Right to Continue")
if r == True:
break
else:
@@ -701,14 +947,13 @@ class Controller(Singleton):
if r == 1: #Yes
slot_num = self.menu_view.display_saved_seed_menu(self.storage,2,None)
if slot_num in (1,2,3):
- self.storage.save_seed_phrase(seed_phrase, slot_num)
- self.storage.save_passphrase(passphrase, slot_num)
+ self.storage.add_seed(seed, slot_num)
self.menu_view.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT])
# show transaction information before sign
self.menu_view.draw_modal(["Parsing PSBT"])
- p = PSBTParser(psbt,seed_phrase,passphrase,self.settings.network)
+ p = PSBTParser(psbt,seed,self.settings.network)
self.signing_tools_view.display_transaction_information(p)
input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT], False)
if input == B.KEY_LEFT:
@@ -728,13 +973,22 @@ class Controller(Singleton):
# Display Animated QR Code
self.menu_view.draw_modal(["Generating PSBT QR ..."])
- e = EncodeQR(psbt=trimmed_psbt, qr_type=self.settings.qr_psbt_type, qr_density=self.settings.qr_density)
+ e = EncodeQR(psbt=trimmed_psbt, qr_type=self.settings.qr_psbt_type, qr_density=self.settings.qr_density, wordlist=self.settings.wordlist)
while True:
- image = e.nextPartImage(240,240,1)
- View.DispShowImage(image)
- time.sleep(0.05)
- if self.buttons.check_for_low(B.KEY_RIGHT):
+ cur_time = int(time.time() * 1000)
+ if cur_time - self.buttons.last_input_time > self.screensaver_activation_ms and not self.screensaver.is_running:
+ self.start_screensaver()
+ self.buttons.update_last_input_time()
+ else:
+ image = e.nextPartImage(240,240,1,background=self.current_bg_qr_color)
+ View.DispShowImage(image)
+ time.sleep(0.05)
+ if self.buttons.check_for_low(B.KEY_RIGHT):
break
+ elif self.buttons.check_for_low(B.KEY_UP):
+ self.prev_qr_background_color()
+ elif self.buttons.check_for_low(B.KEY_DOWN):
+ self.next_qr_background_color()
# Return to Main Menu
return Path.MAIN_MENU
@@ -780,15 +1034,6 @@ class Controller(Singleton):
return Path.SETTINGS_SUB_MENU
- ### Show Wallet Policy Tool
-
- def show_wallet_policy_tool(self):
- r = self.settings_tools_view.display_wallet_policy_selection()
- if r is not None:
- self.settings.script_policy = r
-
- return Path.SETTINGS_SUB_MENU
-
### Show Version Info
def show_version_info(self):
@@ -817,8 +1062,55 @@ class Controller(Singleton):
return Path.SETTINGS_SUB_MENU
+ ### next_qr_background_colors()
+
+ def next_qr_background_color(self):
+ if self.current_bg_qr_color == "FFFFFF":
+ self.current_bg_qr_color = "DDDDDD"
+ elif self.current_bg_qr_color == "DDDDDD":
+ self.current_bg_qr_color = "BBBBBB"
+ elif self.current_bg_qr_color == "BBBBBB":
+ self.current_bg_qr_color = "999999"
+ elif self.current_bg_qr_color == "999999":
+ self.current_bg_qr_color = "777777"
+ elif self.current_bg_qr_color == "777777":
+ self.current_bg_qr_color = "555555"
+ elif self.current_bg_qr_color == "555555":
+ self.current_bg_qr_color = "333333"
+ elif self.current_bg_qr_color == "333333":
+ self.current_bg_qr_color = "FFFFFF"
+
+ self.settings.qr_background_color = self.current_bg_qr_color
+
+ ### prev_qr_background_colors()
+
+ def prev_qr_background_color(self):
+ if self.current_bg_qr_color == "333333":
+ self.current_bg_qr_color = "555555"
+ elif self.current_bg_qr_color == "555555":
+ self.current_bg_qr_color = "777777"
+ elif self.current_bg_qr_color == "777777":
+ self.current_bg_qr_color = "999999"
+ elif self.current_bg_qr_color == "999999":
+ self.current_bg_qr_color = "BBBBBB"
+ elif self.current_bg_qr_color == "BBBBBB":
+ self.current_bg_qr_color = "DDDDDD"
+ elif self.current_bg_qr_color == "DDDDDD":
+ self.current_bg_qr_color = "FFFFFF"
+ elif self.current_bg_qr_color == "FFFFFF":
+ self.current_bg_qr_color = "333333"
+
+ self.settings.qr_background_color = self.current_bg_qr_color
+
### Show Donate Screen and QR
+ def show_camera_rotation_tool(self):
+ r = self.settings_tools_view.display_camera_rotation()
+ if r is not None:
+ self.settings.camera_rotation = r
+
+ return Path.SETTINGS_SUB_MENU ### Show Donate Screen and QR
+
def show_donate_tool(self):
self.settings_tools_view.display_donate_info_screen()
diff --git a/src/seedsigner/helpers/buttons.py b/src/seedsigner/helpers/buttons.py
index 80dd9d03..3956a5b0 100644
--- a/src/seedsigner/helpers/buttons.py
+++ b/src/seedsigner/helpers/buttons.py
@@ -40,10 +40,30 @@ class Buttons:
def wait_for(self, keys=[], check_release=True, release_keys=[]) -> int:
+ # TODO: Refactor to keep control in the Controller and not here
+ from seedsigner.controller import Controller
+ controller = Controller.get_instance()
+
if not release_keys:
release_keys = keys
self.override_ind = False
+
while True:
+ cur_time = int(time.time() * 1000)
+ if cur_time - self.last_input_time > controller.screensaver_activation_ms and not controller.screensaver.is_running:
+ # Start the screensaver. Will block execution until input detected.
+ controller.start_screensaver()
+
+ # We're back. Update last_input_time to now.
+ self.update_last_input_time()
+
+ # Freeze any further processing for a moment to avoid having the wakeup
+ # input register in the resumed UI.
+ time.sleep(self.next_repeat_threshold / 1000.0)
+
+ # Resume from a fresh loop
+ continue
+
for key in keys:
if not check_release or ((check_release and key in release_keys and B.release_lock) or check_release and key not in release_keys):
# when check release is False or the release lock is released (True)
@@ -61,8 +81,6 @@ class Buttons:
else:
# Still pressing the same input
- cur_time = int(time.time() * 1000)
-
if cur_time - self.last_input_time > self.next_repeat_threshold:
# Too much time has elapsed to consider this the same
# continuous input. Treat as a new separate press.
@@ -92,6 +110,10 @@ class Buttons:
time.sleep(0.01) # wait 10 ms to give CPU chance to do other things
+ def update_last_input_time(self):
+ self.last_input_time = int(time.time() * 1000)
+
+
def add_events(self, keys=[]):
for key in keys:
GPIO.add_event_detect(key, self.GPIO.RISING, callback=Buttons.rising_callback)
@@ -115,7 +137,18 @@ class Buttons:
return True
def check_for_low(self, key) -> bool:
- return self.GPIO.input(key) == self.GPIO.LOW
+ if self.GPIO.input(key) == self.GPIO.LOW:
+ self.update_last_input_time()
+ return True
+ else:
+ return False
+
+ def has_any_input(self) -> bool:
+ for key in B.ALL_KEYS:
+ if self.GPIO.input(key) == GPIO.LOW:
+ return True
+ return False
+
@@ -133,4 +166,15 @@ class B:
KEY3 = 16
OVERRIDE = 1000
+ ALL_KEYS = [
+ KEY_UP,
+ KEY_DOWN,
+ KEY_LEFT,
+ KEY_RIGHT,
+ KEY_PRESS,
+ KEY1,
+ KEY2,
+ KEY3,
+ ]
+
release_lock = True # released when True, locked when False
diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py
index 4a1c78cf..d7baf472 100644
--- a/src/seedsigner/helpers/mnemonic_generation.py
+++ b/src/seedsigner/helpers/mnemonic_generation.py
@@ -1,9 +1,10 @@
from embit import bip39
from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes
+import unicodedata
+import hashlib
-
-def calculate_checksum(partial_mnemonic: list):
+def calculate_checksum(partial_mnemonic: list, wordlist):
# Provide 11- or 23-word mnemonic, returns complete mnemonic w/checksum
if len(partial_mnemonic) not in [11, 23]:
raise Exception("Pass in a 11- or 23-word mnemonic")
@@ -13,7 +14,7 @@ def calculate_checksum(partial_mnemonic: list):
mnemonic_copy.append("abandon")
# Ignores the final checksum word and recalcs
- mnemonic_bytes = bip39.mnemonic_to_bytes(" ".join(mnemonic_copy), ignore_checksum=True)
+ mnemonic_bytes = bip39.mnemonic_to_bytes(unicodedata.normalize("NFKD", " ".join(mnemonic_copy)), ignore_checksum=True, wordlist=wordlist)
# Return as a list
return bip39.mnemonic_from_bytes(mnemonic_bytes).split()
@@ -27,8 +28,7 @@ def generate_mnemonic_from_bytes(entropy_bytes):
def generate_mnemonic_from_dice(roll_data: str):
- entropyinteger = int(roll_data, 6)
- entropy_bytes = entropyinteger.to_bytes(32, byteorder="little")
+ entropy_bytes = hashlib.sha256(roll_data.encode()).digest()
# Return as a list
return bip39.mnemonic_from_bytes(entropy_bytes).split()
diff --git a/src/seedsigner/helpers/path.py b/src/seedsigner/helpers/path.py
index 8658f3d3..f641c459 100644
--- a/src/seedsigner/helpers/path.py
+++ b/src/seedsigner/helpers/path.py
@@ -27,8 +27,8 @@ class Path:
DONATE = 63
WALLET = 64
QR_DENSITY_SETTING = 65
- WALLET_POLICY = 66
PERSISTENT_SETTINGS = 67
+ CAMERA_ROTATION = 69
RESET = 68
# Seed Slots
diff --git a/src/seedsigner/helpers/qr.py b/src/seedsigner/helpers/qr.py
index 38ad7bd0..1857201f 100644
--- a/src/seedsigner/helpers/qr.py
+++ b/src/seedsigner/helpers/qr.py
@@ -11,15 +11,15 @@ class QR:
qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=5, border=border )
qr.add_data(data)
qr.make(fit=True)
- return(qr.make_image(fill_color="black", back_color="white").resize((width,height)).convert('RGB'))
+ return(qr.make_image(fill_color="black", back_color="white").resize((width,height)).convert('RGBA'))
- def qrimage_io(self, data, width=240, height=240, border=3):
+ def qrimage_io(self, data, width=240, height=240, border=3, background="FFFFFF"):
if 1 <= border <= 10:
border_str = str(border)
else:
border_str = "3"
- cmd = 'qrencode -m '+border_str+' -s 3 -l L --foreground=000000 --background=FFFFFF -t PNG -o "/dev/shm/qrcode.png" "' + str(data) + '"'
+ cmd = 'qrencode -m '+border_str+' -s 3 -l L --foreground=000000 --background='+background+' -t PNG -o "/dev/shm/qrcode.png" "' + str(data) + '"'
rv = subprocess.call(cmd, shell=True)
# if qrencode fails, fall back to only encoder
diff --git a/src/seedsigner/models/__init__.py b/src/seedsigner/models/__init__.py
index f9a1945f..7e691d33 100644
--- a/src/seedsigner/models/__init__.py
+++ b/src/seedsigner/models/__init__.py
@@ -1,4 +1,6 @@
+from .seed import *
from .qr_type import *
+from .encode_qr_density import *
from .decode_qr import *
from .encode_qr import *
from .psbt_parser import *
diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py
index 31663996..07f90b9c 100644
--- a/src/seedsigner/models/decode_qr.py
+++ b/src/seedsigner/models/decode_qr.py
@@ -2,12 +2,13 @@ from pyzbar import pyzbar
from pyzbar.pyzbar import ZBarSymbol
from enum import IntEnum
import re
+import json
import base64
-from embit import bip39, psbt
+from embit import psbt
from binascii import a2b_base64, b2a_base64
from seedsigner.helpers.ur2.ur_decoder import URDecoder
from seedsigner.helpers.bcur import (cbor_decode, bc32decode)
-from seedsigner.models.qr_type import QRType
+from seedsigner.models import QRType, Seed
###
### DecodeQR Class
@@ -16,8 +17,6 @@ from seedsigner.models.qr_type import QRType
class DecodeQR:
- _4LETTER_WORDLIST = [word[:4].strip() for word in bip39.WORDLIST]
-
def __init__(self, **kwargs):
self.complete = False
self.qr_type = None
@@ -25,11 +24,21 @@ class DecodeQR:
self.specter_qr = SpecterDecodePSBTQR() # Specter Desktop PSBT QR base64 decoder
self.legacy_ur = LegacyURDecodeQR() # UR Legacy decoder
self.base64_qr = Base64DecodeQR() # Single Segments Base64
- self.seedqr = SeedQR()
+ self.base43_qr = Base43DecodeQR() # Single Segment Base43
+ self.address_qr = BitcoinAddressQR() # Single Segment bitcoin address
+ self.specter_wallet_qr = SpecterDecodeWalletQR() # Specter Desktop Wallet Export decoder
+ self.wordlist = None
for key, value in kwargs.items():
if key == "qr_type":
self.qr_type = value
+ if key == "wordlist":
+ self.wordlist = value
+
+ if self.wordlist == None:
+ raise Exception('Wordlist Required')
+
+ self.seedqr = SeedQR(wordlist=self.wordlist)
def addImage(self, image):
qr_str = DecodeQR.QR2Str(image)
@@ -42,7 +51,7 @@ class DecodeQR:
if qr_str == None:
return DecodeQRStatus.FALSE
- qr_type = DecodeQR.SegmentType(qr_str)
+ qr_type = DecodeQR.SegmentType(qr_str, wordlist=self.wordlist)
if self.qr_type == None:
self.qr_type = qr_type
@@ -77,6 +86,13 @@ class DecodeQR:
if rt == DecodeQRStatus.COMPLETE:
self.complete = True
return rt
+
+ elif self.qr_type == QRType.PSBTBASE43:
+
+ rt = self.base43_qr.add(qr_str)
+ if rt == DecodeQRStatus.COMPLETE:
+ self.complete = True
+ return rt
elif self.qr_type == QRType.SEEDSSQR:
rt = self.seedqr.add(qr_str, QRType.SEEDSSQR)
@@ -95,6 +111,18 @@ class DecodeQR:
if rt == DecodeQRStatus.COMPLETE:
self.complete = True
return rt
+
+ elif self.qr_type == QRType.BITCOINADDRESSQR:
+ rt = self.address_qr.add(qr_str, QRType.BITCOINADDRESSQR)
+ if rt == DecodeQRStatus.COMPLETE:
+ self.complete = True
+ return rt
+
+ elif self.qr_type == QRType.SPECTERWALLETQR:
+ rt = self.specter_wallet_qr.add(qr_str)
+ if rt == DecodeQRStatus.COMPLETE:
+ self.complete = True
+ return rt
else:
return DecodeQRStatus.INVALID
@@ -120,6 +148,8 @@ class DecodeQR:
return self.legacy_ur.getData()
elif self.qr_type == QRType.PSBTBASE64:
return self.base64_qr.getData()
+ elif self.qr_type == QRType.PSBTBASE43:
+ return self.base43_qr.getData()
return None
def getBase64PSBT(self):
@@ -135,6 +165,15 @@ class DecodeQR:
def getSeedPhrase(self):
return self.seedqr.getSeedPhrase()
+
+ def getAddress(self):
+ return self.address_qr.getAddress()
+
+ def getAddressType(self):
+ return self.address_qr.getAddressType()
+
+ def getWalletDescriptor(self):
+ return self.specter_wallet_qr.getWalletDescriptor()
def getPercentComplete(self) -> int:
if self.qr_type == QRType.PSBTUR2:
@@ -152,6 +191,11 @@ class DecodeQR:
return 100
else:
return 0
+ elif self.qr_type == QRType.PSBTBASE43:
+ if self.base43_qr.complete:
+ return 100
+ else:
+ return 0
else:
return 0
@@ -164,7 +208,7 @@ class DecodeQR:
return False
def isPSBT(self) -> bool:
- if self.qr_type in (QRType.PSBTUR2, QRType.PSBTSPECTER, QRType.PSBTURLEGACY, QRType.PSBTBASE64):
+ if self.qr_type in (QRType.PSBTUR2, QRType.PSBTSPECTER, QRType.PSBTURLEGACY, QRType.PSBTBASE64, QRType.PSBTBASE43):
return True
return False
@@ -172,6 +216,16 @@ class DecodeQR:
if self.qr_type in (QRType.SEEDSSQR, QRType.SEEDUR2, QRType.SEEDMNEMONIC, QRType.SEED4LETTERMNEMONIC):
return True
return False
+
+ def isAddress(self):
+ if self.qr_type == QRType.BITCOINADDRESSQR:
+ return True
+ return False
+
+ def isWalletDescriptor(self):
+ if self.qr_type in (QRType.SPECTERWALLETQR, QRType.URWALLETQR, QRType.BLUEWALLETQR):
+ return True
+ return False
def qrType(self):
return self.qr_type
@@ -188,26 +242,52 @@ class DecodeQR:
return barcode.data.decode("utf-8")
@staticmethod
- def SegmentType(s):
+ def SegmentType(s, wordlist=None):
+ # PSBT
if re.search("^UR:CRYPTO-PSBT/", s, re.IGNORECASE):
return QRType.PSBTUR2
- elif re.search(r'^p(\d+)of(\d+) ', s, re.IGNORECASE):
+ elif re.search(r'^p(\d+)of(\d+) ([A-Za-z0-9+\/=]+$)', s, re.IGNORECASE): #must be base64 characters only in segment
return QRType.PSBTSPECTER
elif re.search("^UR:BYTES/", s, re.IGNORECASE):
return QRType.PSBTURLEGACY
- elif re.search(r'\d{48,96}', s):
- return QRType.SEEDSSQR
- elif all(x in bip39.WORDLIST for x in s.strip().split(" ")):
- # checks if all words in list are in bip39 word list
- return QRType.SEEDMNEMONIC
- elif all(x in DecodeQR._4LETTER_WORDLIST for x in s.strip().split(" ")):
- # checks if all 4 letter words are in list are in 4 letter bip39 word list
- return QRType.SEED4LETTERMNEMONIC
elif DecodeQR.isBase64PSBT(s):
return QRType.PSBTBASE64
- else:
- return QRType.INVALID
+
+ # Wallet Descriptor
+ desc_str = s.replace("\n","").replace(" ","")
+ if re.search(r'^p(\d+)of(\d+) ', s, re.IGNORECASE):
+ # when not a SPECTER Base64 PSBT from above, assume it's json
+ return QRType.SPECTERWALLETQR
+ elif re.search(r'^\{\"label\".*\"descriptor\"\:.*', desc_str, re.IGNORECASE):
+ # if json starting with label and contains descriptor, assume specter wallet json
+ return QRType.SPECTERWALLETQR
+
+ # create 4 letter wordlist only if not PSBT (performance gain)
+ try:
+ _4LETTER_WORDLIST = [word[:4].strip() for word in wordlist]
+ except:
+ _4LETTER_WORDLIST = []
+
+ # Seed
+ if re.search(r'\d{48,96}', s):
+ return QRType.SEEDSSQR
+
+ # Bitcoin Address
+ elif DecodeQR.isBitcoinAddress(s):
+ return QRType.BITCOINADDRESSQR
+
+ # Seed
+ elif all(x in wordlist for x in s.strip().split(" ")):
+ # checks if all words in list are in bip39 word list
+ return QRType.SEEDMNEMONIC
+ elif all(x in _4LETTER_WORDLIST for x in s.strip().split(" ")):
+ # checks if all 4 letter words are in list are in 4 letter bip39 word list
+ return QRType.SEED4LETTERMNEMONIC
+ elif DecodeQR.isBase43PSBT(s):
+ return QRType.PSBTBASE43
+
+ return QRType.INVALID
@staticmethod
def isBase64(s):
@@ -226,6 +306,61 @@ class DecodeQR:
return False
return False
+ @staticmethod
+ def isBase43PSBT(s):
+ try:
+ psbt.PSBT.parse(DecodeQR.base43Decode(s))
+ return True
+ except Exception:
+ return False
+ return False
+
+ @staticmethod
+ def base43Decode(s):
+ chars = b'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ$*+-./:' #base43 chars
+
+ if isinstance(s, bytes):
+ v = s
+ if isinstance(s, str):
+ v = s.encode('ascii')
+ elif isinstance(s, bytearray):
+ v = bytes(s)
+
+ long_value = 0
+ power_of_base = 1
+ for c in v[::-1]:
+ digit = chars.find(bytes([c]))
+ if digit == -1:
+ raise BaseDecodeError('Forbidden character {} for base {}'.format(c, 43))
+ # naive but slow variant: long_value += digit * (base**i)
+ long_value += digit * power_of_base
+ power_of_base *= 43
+ result = bytearray()
+ while long_value >= 256:
+ div, mod = divmod(long_value, 256)
+ result.append(mod)
+ long_value = div
+ result.append(long_value)
+ nPad = 0
+ for c in v:
+ if c == chars[0]:
+ nPad += 1
+ else:
+ break
+ result.extend(b'\x00' * nPad)
+ result.reverse()
+ return bytes(result)
+
+ @staticmethod
+ def isBitcoinAddress(s):
+
+ if re.search(r'^bitcoin\:.*', s, re.IGNORECASE):
+ return True
+ elif re.search(r'^((bc1|tb1|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,62})$', s):
+ return True
+ else:
+ return False
+
###
### SpecterDecodePSBTQR Class
### Purpose: used in DecodePSBTQR to decode Specter Desktop Animated QR PSBT encoding
@@ -367,7 +502,7 @@ class LegacyURDecodeQR:
###
### Base64DecodeQR Class
### Purpose: used in DecodeQR to decode single frame base64 encoded qr image
-### does not support animated qr because no idicator or segments or thier order
+### does not support animated qr because no indicator or segments or their order
###
class Base64DecodeQR:
@@ -410,23 +545,67 @@ class Base64DecodeQR:
return segment
###
-### SeedQR Class
-### Purpose: used in DecodeQR to decode single frame representing a seed
-### SeedSigner numeric respresentation of a seed as a qr is supported
-### Mnemonic seed phrase respresentation of a seed as a qr is supported
-### does not support animated qr because no idicator or segments or thier order
+### Base43DecodeQR Class
+### Purpose: used in DecodeQR to decode single frame base43 encoded qr image
+### does not support animated qr because no indicator or segments or their order
###
-class SeedQR:
+class Base43DecodeQR:
def __init__(self):
+ self.total_segments = 1
+ self.collected_segments = 0
+ self.complete = False
+ self.data = None
+
+ def add(self, segment):
+ if DecodeQR.isBase43PSBT(segment):
+ self.complete = True
+ self.data = DecodeQR.base43Decode(segment)
+ self.collected_segments = 1
+ return DecodeQRStatus.COMPLETE
+
+ return DecodeQRStatus.INVALID
+
+ def getData(self):
+ return self.data
+
+ @staticmethod
+ def currentSegmentNum(segment) -> int:
+ return self.collected_segments
+
+ @staticmethod
+ def totalSegmentNum(segment) -> int:
+ return self.total_segments
+
+ @staticmethod
+ def parseSegment(segment) -> str:
+ return segment
+
+###
+### SeedQR Class
+### Purpose: used in DecodeQR to decode single frame representing a seed
+### SeedSigner numeric representation of a seed as a qr is supported
+### Mnemonic seed phrase representation of a seed as a qr is supported
+### does not support animated qr because no indicator or segments or their order
+###
+
+class SeedQR:
+
+ def __init__(self, wordlist=None):
self.total_segments = 1
self.collected_segments = 0
self.complete = False
self.seed_phrase = []
+ self.wordlist = wordlist
+
+ if self.wordlist == None:
+ raise Exception('Wordlist Required')
def add(self, segment, qr_type=QRType.SEEDSSQR):
+ _4LETTER_WORDLIST = [word[:4].strip() for word in self.wordlist]
+
if qr_type == QRType.SEEDSSQR:
try:
@@ -436,7 +615,7 @@ class SeedQR:
num_words = int(len(segment) / 4)
for i in range(0, num_words):
index = int(segment[i * 4: (i*4) + 4])
- word = bip39.WORDLIST[index]
+ word = self.wordlist[index]
self.seed_phrase.append(word)
if len(self.seed_phrase) > 0:
if self.is1224Phrase() == False:
@@ -453,7 +632,10 @@ class SeedQR:
try:
# embit mnemonic code to validate
- bip39.mnemonic_to_seed(segment.strip())
+ seed = Seed(segment, passphrase="", wordlist=self.wordlist)
+ if not seed:
+ # seed is not valid, return invalid
+ return DecodeQRStatus.INVALID
self.seed_phrase = segment.strip().split(" ")
if self.is1224Phrase() == False:
return DecodeQRStatus.INVALID
@@ -469,10 +651,13 @@ class SeedQR:
sl = segment.strip().split(" ")
words = []
for s in sl:
- words.append(bip39.WORDLIST[DecodeQR._4LETTER_WORDLIST.index(s)])
+ words.append(self.wordlist[_4LETTER_WORDLIST.index(s)])
# embit mnemonic code to validate
- bip39.mnemonic_to_seed(" ".join(words).strip())
+ seed = Seed(words, passphrase="", wordlist=self.wordlist)
+ if not seed:
+ # seed is not valid, return invalid
+ return DecodeQRStatus.INVALID
self.seed_phrase = words
if self.is1224Phrase() == False:
return DecodeQRStatus.INVALID
@@ -496,6 +681,149 @@ class SeedQR:
return True
return False
+###
+### BitcoinAddressQR Class
+### Purpose: used in DecodeQR to decode single frame representing a bitcoin address
+###
+
+class BitcoinAddressQR:
+
+ def __init__(self, wordlist=None):
+ self.total_segments = 1
+ self.collected_segments = 0
+ self.complete = False
+ self.address = None
+ self.address_type = None
+
+ def add(self, segment, qr_type=QRType.BITCOINADDRESSQR):
+
+ r = re.search(r'((bc1|tb1|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,62})', segment)
+ if r != None:
+ self.address = r.group(1)
+
+ if re.search(r'^((bc1|tb1|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,62})$', self.address) != None:
+ self.complete = True
+ self.collected_segments = 1
+
+ # get address type
+ r = re.search(r'^((bc1|tb1|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,62})$', self.address)
+ if r != None:
+ r = r.group(2)
+
+ if r == "1":
+ self.address_type = "P2PKH-main" # Legacy
+ elif r == "m" or r == "n":
+ self.address_type = "P2PKH-test" # Legacy
+ elif r == "3":
+ self.address_type = "P2SH-main" # Nested Segwit Single Sig (P2WPKH in P2SH) or Multisig (P2WSH in P2SH)
+ elif r == "2":
+ self.address_type = "P2SH-test" # Nested Segwit Single Sig (P2WPKH in P2SH) or Multisig (P2WSH in P2SH)
+ elif r == "bc1":
+ self.address_type = "Bech32-main"
+ elif r == "tb1":
+ self.address_type = "Bech32-test"
+
+ return DecodeQRStatus.COMPLETE
+
+ return DecodeQRStatus.INVALID
+
+ def getAddress(self):
+ if self.address != None:
+ return self.address
+ return None
+
+ def getAddressType(self):
+ if self.address != None:
+ if self.address_type != None:
+ return self.address_type
+ else:
+ return "Unknown"
+ return None
+
+###
+### SpecterDecodeWalletQR Class
+### Purpose: used in DecodeQR to decode animated frames to get a wallet descriptor from Specter Desktop
+###
+
+class SpecterDecodeWalletQR:
+
+ def __init__(self):
+ self.total_segments = None
+ self.collected_segments = 0
+ self.complete = False
+ self.segments = []
+
+ def add(self, segment):
+ if self.total_segments == None:
+ self.total_segments = SpecterDecodeWalletQR.totalSegmentNum(segment)
+ self.segments = [None] * self.total_segments
+ elif self.total_segments != SpecterDecodeWalletQR.totalSegmentNum(segment):
+ raise Exception('Specter Desktop segment total changed unexpectedly')
+
+ if self.segments[SpecterDecodeWalletQR.currentSegmentNum(segment) - 1] == None:
+ self.segments[SpecterDecodeWalletQR.currentSegmentNum(segment) - 1] = SpecterDecodeWalletQR.parseSegment(segment)
+ self.collected_segments += 1
+ if self.total_segments == self.collected_segments:
+ if self.validateWalletDescriptor():
+ self.complete = True
+ return DecodeQRStatus.COMPLETE
+ else:
+ return DecodeQRStatus.INVALID
+ return DecodeQRStatus.PART_COMPLETE # new segment added
+
+ return DecodeQRStatus.PART_EXISTING # segment not added because it's already been added
+
+ def validateJson(self) -> str:
+ try:
+ j = "".join(self.segments)
+ json.loads(j)
+ except json.decoder.JSONDecodeError:
+ return False
+ return True
+
+ def validateWalletDescriptor(self):
+ if self.validateJson():
+ j = "".join(self.segments)
+ data = json.loads(j)
+ if "descriptor" in data:
+ return True
+ return False
+
+ def getWalletDescriptor(self) -> str:
+ if self.validateWalletDescriptor():
+ j = "".join(self.segments)
+ data = json.loads(j)
+ return data['descriptor']
+ return None
+
+ def is_complete(self) -> bool:
+ if self.complete and self.validateWalletDescriptor():
+ return True
+ return False
+
+ @staticmethod
+ def currentSegmentNum(segment) -> int:
+ if DecodeQR.SegmentType(segment) == QRType.SPECTERWALLETQR:
+ if re.search(r'^p(\d+)of(\d+) ', segment, re.IGNORECASE) != None:
+ return int(re.search(r'^p(\d+)of(\d+) ', segment, re.IGNORECASE).group(1))
+ else:
+ return 1
+
+ @staticmethod
+ def totalSegmentNum(segment) -> int:
+ if DecodeQR.SegmentType(segment) == QRType.SPECTERWALLETQR:
+ if re.search(r'^p(\d+)of(\d+) ', segment, re.IGNORECASE) != None:
+ return int(re.search(r'^p(\d+)of(\d+) ', segment, re.IGNORECASE).group(2))
+ else:
+ return 1
+
+ @staticmethod
+ def parseSegment(segment) -> str:
+ try:
+ return re.search(r'^p(\d+)of(\d+) (.+$)', segment, re.IGNORECASE).group(3)
+ except:
+ return segment
+
###
### DecodeQRStatus Class IntEum
### Purpose: used in DecodeQR to communicate status of adding qr frame/segment
diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py
index 6931aa13..394113e2 100644
--- a/src/seedsigner/models/encode_qr.py
+++ b/src/seedsigner/models/encode_qr.py
@@ -1,5 +1,5 @@
from enum import IntEnum
-from embit import psbt, bip39, bip32
+from embit import psbt, bip32
from embit.networks import NETWORKS
from binascii import b2a_base64, hexlify
from seedsigner.helpers.ur2.ur_encoder import UREncoder
@@ -7,7 +7,7 @@ from seedsigner.helpers.ur2.cbor_lite import CBOREncoder
from seedsigner.helpers.ur2.ur import UR
from seedsigner.helpers.bcur import (bc32encode, cbor_encode, bcur_encode)
from seedsigner.helpers.qr import QR
-from seedsigner.models.qr_type import QRType
+from seedsigner.models import Seed, QRType, EncodeQRDensity
###
### EncodeQR Class
@@ -16,16 +16,18 @@ from seedsigner.models.qr_type import QRType
class EncodeQR:
+ WORDLIST = None
+
def __init__(self, **kwargs):
self.psbt = None
self.seed_phrase = None
self.passphrase = None
self.derivation = None
self.network = None
- self.policy = None
self.qr_type = None
self.qr_density = None
self.qr = QR()
+ self.wordlist = None
for key, value in kwargs.items():
if key == "psbt":
@@ -38,12 +40,15 @@ class EncodeQR:
self.derivation = value
elif key == "network":
self.network = value
- elif key == "policy":
- self.policy = value
elif key == "qr_type":
self.qr_type = value
elif key == "qr_density":
self.qr_density = value
+ elif key == "wordlist":
+ self.wordlist = value
+
+ if self.wordlist == None:
+ raise Exception('Wordlist Required')
if self.qr_type == None:
raise Exception('Encoder Type Required')
@@ -57,11 +62,11 @@ class EncodeQR:
elif self.qr_type == QRType.PSBTUR2:
self.encoder = UREncodePSBTQR(self.psbt, self.qr_density)
elif self.qr_type == QRType.SEEDSSQR:
- self.encoder = SeedSSQR(self.seed_phrase)
+ self.encoder = SeedSSQR(self.seed_phrase, self.wordlist)
elif self.qr_type == QRType.XPUBQR:
- self.encoder = XPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.policy)
+ self.encoder = XPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.wordlist)
elif self.qr_type == QRType.SPECTERXPUBQR:
- self.encoder = SpecterXPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.policy, self.qr_density)
+ self.encoder = SpecterXPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.qr_density, self.wordlist)
else:
raise Exception('Encoder Type not Supported')
@@ -74,9 +79,12 @@ class EncodeQR:
def part2Image(self, part, width=240, height=240, border=3):
return self.qr.qrimage_io(part, width, height, border)
- def nextPartImage(self, width=240, height=240, border=3):
+ def nextPartImage(self, width=240, height=240, border=3, background="FFFFFF"):
part = self.nextPart()
- return self.qr.qrimage_io(part, width, height, border)
+ if self.qr_type == QRType.SEEDSSQR:
+ return self.qr.qrimage(part, width, height, border)
+ else:
+ return self.qr.qrimage_io(part, width, height, border, background=background)
def isComplete(self):
return self.encoder.isComplete()
@@ -184,8 +192,12 @@ class SpecterEncodePSBTQR:
class SeedSSQR:
- def __init__(self, seed_phrase):
+ def __init__(self, seed_phrase, wordlist):
self.seed_phrase = seed_phrase
+ self.wordlist = wordlist
+
+ if self.wordlist == None:
+ raise Exception('Wordlist Required')
def seqLen(self):
return 1
@@ -193,7 +205,7 @@ class SeedSSQR:
def nextPart(self):
data = ""
for word in self.seed_phrase:
- index = bip39.WORDLIST.index(word)
+ index = self.wordlist.index(word)
data += str("%04d" % index)
return data
@@ -203,29 +215,27 @@ class SeedSSQR:
class XPubQR:
- def __init__(self, seed_phrase, passphrase, derivation, network, policy):
+ def __init__(self, seed_phrase, passphrase, derivation, network, wordlist):
self.seed_phrase = seed_phrase
self.passphrase = passphrase
self.derivation = derivation
+ self.wordlist = wordlist
self.parts = []
self.part_num_sent = 0
self.sent_complete = False
- self.network = network
- self.seed = bip39.mnemonic_to_seed((" ".join(self.seed_phrase)).strip(), self.passphrase)
- self.root = bip32.HDKey.from_seed(self.seed, version=NETWORKS[self.network]["xprv"])
+ if self.wordlist == None:
+ raise Exception('Wordlist Required')
+
+ version = bip32.detect_version(self.derivation, default="xpub", network=NETWORKS[network])
+ print(self.derivation)
+ self.seed = Seed(mnemonic=self.seed_phrase, passphrase=self.passphrase, wordlist=self.wordlist)
+ self.root = bip32.HDKey.from_seed(self.seed.seed, version=NETWORKS[network]["xprv"])
self.fingerprint = self.root.child(0).fingerprint
- self.bip48_xprv = self.root.derive(self.derivation)
- self.bip48_xpub = self.bip48_xprv.to_public()
-
- xpub_base58 = ""
-
- if policy == "PKWPKH":
- self.xpub_base58 = self.bip48_xpub.to_base58(NETWORKS[self.network]["zpub"])
- elif policy == "PKWSH":
- self.xpub_base58 = self.bip48_xpub.to_base58(NETWORKS[self.network]["Zpub"])
- else:
- raise Exception('Policy Type not Supported')
+ self.xprv = self.root.derive(self.derivation)
+ self.xpub = self.xprv.to_public()
+ self.xpub_base58 = self.xpub.to_string(version=version)
+ self.network = network
self.xpubstring = "[%s%s]%s" % (hexlify(self.fingerprint).decode('utf-8'),self.derivation[1:],self.xpub_base58)
@@ -249,7 +259,7 @@ class XPubQR:
class SpecterXPubQR(XPubQR):
- def __init__(self, seed_phrase, passphrase, derivation, network, policy, qr_density):
+ def __init__(self, seed_phrase, passphrase, derivation, network, qr_density, wordlist):
self.qr_max_fragement_size = 65
if qr_density == EncodeQRDensity.LOW:
self.qr_max_fragement_size = 40
@@ -258,7 +268,7 @@ class SpecterXPubQR(XPubQR):
elif qr_density == EncodeQRDensity.HIGH:
self.qr_max_fragement_size = 90
- XPubQR.__init__(self, seed_phrase, passphrase, derivation, network, policy)
+ XPubQR.__init__(self, seed_phrase, passphrase, derivation, network, wordlist)
self.__createParts()
def __createParts(self):
@@ -303,9 +313,4 @@ class SpecterXPubQR(XPubQR):
return len(self.parts)
def isComplete(self):
- return self.sent_complete
-
-class EncodeQRDensity(IntEnum):
- LOW = 1
- MEDIUM = 2
- HIGH = 3
\ No newline at end of file
+ return self.sent_complete
\ No newline at end of file
diff --git a/src/seedsigner/models/encode_qr_density.py b/src/seedsigner/models/encode_qr_density.py
new file mode 100644
index 00000000..20da4a27
--- /dev/null
+++ b/src/seedsigner/models/encode_qr_density.py
@@ -0,0 +1,11 @@
+from enum import IntEnum
+
+###
+### QRType Class IntEum
+### Purpose: used with DecodeQR and EncodeQR to communicate qr encoding type
+###
+
+class EncodeQRDensity(IntEnum):
+ LOW = 1
+ MEDIUM = 2
+ HIGH = 3
\ No newline at end of file
diff --git a/src/seedsigner/models/psbt_parser.py b/src/seedsigner/models/psbt_parser.py
index 52ea6926..8e6f8382 100644
--- a/src/seedsigner/models/psbt_parser.py
+++ b/src/seedsigner/models/psbt_parser.py
@@ -1,12 +1,12 @@
from embit import psbt, script, ec, bip32, bip39
from embit.networks import NETWORKS
from io import BytesIO
+from seedsigner.models import Seed
class PSBTParser():
- def __init__(self, p, seed_phrase=[], passphrase="", network="main"):
- self.seed_phrase = seed_phrase
- self.passphrase = passphrase
+ def __init__(self, p, seed: Seed, network="main"):
+ self.seed = seed
self.network = network
self.psbt = p
@@ -18,17 +18,15 @@ class PSBTParser():
self.destination_addresses = []
self.self_addresses = []
- self.seed = None
self.root = None
- if self.seed_phrase != None:
- self.parse(self.psbt,self.seed_phrase,self.passphrase,self.network)
+ if self.seed:
+ self.parse(self.psbt,self.seed,self.network)
- def __setSeedRoot(self, seed_phrase, passphrase, network):
- self.seed = bip39.mnemonic_to_seed(" ".join(seed_phrase).strip(), passphrase)
- self.root = bip32.HDKey.from_seed(self.seed, version=NETWORKS[network]["xprv"])
+ def __setRoot(self, seed: Seed, network):
+ self.root = bip32.HDKey.from_seed(seed.seed, version=NETWORKS[network]["xprv"])
- def parse(self, p, seed_phrase=[], passphrase="", network="main"):
+ def parse(self, p, seed: Seed, network="main"):
is_psbt_empty = False
try:
if p == None:
@@ -39,10 +37,10 @@ class PSBTParser():
if is_psbt_empty:
return False
- if len(seed_phrase) == 0:
+ if not seed:
return False
- self.__setSeedRoot(seed_phrase,passphrase,network)
+ self.__setRoot(seed, network)
rt = self.__parseInputs()
if rt == False:
@@ -57,13 +55,14 @@ class PSBTParser():
def __parseInputs(self):
self.input_amount = 0
for inp in self.psbt.inputs:
- self.input_amount += inp.witness_utxo.value
- inp_policy = PSBTParser.__get_policy(inp, inp.witness_utxo.script_pubkey, self.psbt.xpubs)
- if self.policy == None:
- self.policy = inp_policy
- else:
- if self.policy != inp_policy:
- raise RuntimeError("Mixed inputs in the transaction")
+ if inp.witness_utxo:
+ self.input_amount += inp.witness_utxo.value
+ inp_policy = PSBTParser.__get_policy(inp, inp.witness_utxo.script_pubkey, self.psbt.xpubs)
+ if self.policy == None:
+ self.policy = inp_policy
+ else:
+ if self.policy != inp_policy:
+ raise RuntimeError("Mixed inputs in the transaction")
def __parseOutputs(self):
self.spend_amount = 0
diff --git a/src/seedsigner/models/qr_type.py b/src/seedsigner/models/qr_type.py
index a3eb52ca..984e5d4c 100644
--- a/src/seedsigner/models/qr_type.py
+++ b/src/seedsigner/models/qr_type.py
@@ -9,6 +9,7 @@ class QRType(IntEnum):
PSBTBASE64 = 1
PSBTSPECTER = 2
PSBTURLEGACY = 3
+ PSBTBASE43 = 4
PSBTUR2 = 5
SEEDSSQR = 6
SEEDUR2 = 7
@@ -16,4 +17,8 @@ class QRType(IntEnum):
SEED4LETTERMNEMONIC = 9
XPUBQR = 20
SPECTERXPUBQR = 21
+ BITCOINADDRESSQR = 30
+ SPECTERWALLETQR = 40
+ URWALLETQR = 41
+ BLUEWALLETQR = 42
INVALID = 100
\ No newline at end of file
diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py
new file mode 100644
index 00000000..9009c7bb
--- /dev/null
+++ b/src/seedsigner/models/seed.py
@@ -0,0 +1,108 @@
+# External Dependencies
+from embit import bip39, bip32
+import unicodedata
+
+class Seed:
+
+ def __init__(self, mnemonic = None, passphrase = "", wordlist=None) -> None:
+ self._init_complete = False
+ self._valid = False
+ self.passphrase = passphrase
+ self.mnemonic = mnemonic
+ self._wordlist = wordlist
+
+ if self._wordlist == None:
+ raise Exception('Wordlist Required')
+
+ self._valid = self._generate_seed()
+ self._init_complete = True
+
+ def _generate_seed(self) -> bool:
+ self.seed = None
+ try:
+ self.seed = bip39.mnemonic_to_seed(self.mnemonic, password=self.passphrase, wordlist=self._wordlist)
+ return True
+ except Exception as e:
+ return False
+
+
+ ### getters and setters
+
+ @property
+ def mnemonic(self):
+ return self._mnemonic
+
+ @property
+ def mnemonic_list(self):
+ return self._mnemonic.split()
+
+ @property
+ def mnemonic_display(self):
+ return unicodedata.normalize("NFC", self._mnemonic)
+
+ @property
+ def mnemonic_display_list(self):
+ return unicodedata.normalize("NFC", self._mnemonic).split()
+
+ @mnemonic.setter
+ def mnemonic(self, value):
+ if isinstance(value, list):
+ self._mnemonic = unicodedata.normalize("NFKD", " ".join(value).strip())
+ elif isinstance(value, str):
+ self._mnemonic = unicodedata.normalize("NFKD", value.strip())
+
+ if self._init_complete:
+ self._valid = self._generate_seed()
+
+ @property
+ def passphrase(self):
+ return self._passphrase
+
+ @property
+ def passphrase_display(self):
+ return unicodedata.normalize("NFC", self._passphrase)
+
+ @passphrase.setter
+ def passphrase(self, value):
+ if isinstance(value, str):
+ self._passphrase = unicodedata.normalize("NFKD", value)
+
+ if self._init_complete:
+ self._valid = self._generate_seed()
+
+ @property
+ def wordlist(self):
+ return self._wordlist
+
+ @wordlist.setter
+ def wordlist(self, value):
+ previous_wordlist = self._wordlist
+
+ if isinstance(value, list):
+ if len(value) == 2048:
+ self._wordlist = value
+ else:
+ raise Exception('Invalid Wordlist')
+ else:
+ raise Exception('Wordlist Must be List Type')
+
+ if self.mnemonic != None:
+ # if has mnemonic, convert to new wordlist
+ previous_mnemonic_str = self._mnemonic
+ previous_mnemonic_list = previous_mnemonic_str.split()
+ new_mnemonic_list = []
+ for word in previous_mnemonic_list:
+ idx = previous_wordlist.index(word)
+ new_mnemonic_list.append(self._wordlist[idx])
+
+ self._mnemonic = " ".join(new_mnemonic_list)
+
+ ### override operators
+
+ def __eq__(self, other):
+ if isinstance(other, Seed):
+ return self.seed == other.seed
+ return False
+
+ def __bool__(self):
+ return self._valid
\ No newline at end of file
diff --git a/src/seedsigner/models/seed_storage.py b/src/seedsigner/models/seed_storage.py
index 87f55273..0ffd654b 100644
--- a/src/seedsigner/models/seed_storage.py
+++ b/src/seedsigner/models/seed_storage.py
@@ -1,151 +1,182 @@
# External Dependencies
-from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes
-from embit import bip39
+from seedsigner.models import Seed
class SeedStorage:
- SEEDWORDS = bip39.WORDLIST
+ def __init__(self) -> None:
+ self.seeds = []
+
+ # to be removed once the concepts of slots is removed
+ # this is a list of 3 indexes to the actual seed/mnemonic/passphrase data
+ # -1 means the slot is empty
+ self.slots = [-1,-1,-1]
+
+ # only adds a seed if it's valid and does not already exist
+ def add_mnemonic(self, mnemonic, passphrase: str = "", slot_num = -1) -> bool:
+ self.seeds.append(Seed(mnemonic, passphrase))
- def __init__(self) -> None:
+ # used to track virtual slot number, TODO: remove concept in UI of slots
+ if slot_num > 0:
+ self.slots[slot_num - 1] = len(self.seeds) - 1
+
+ def add_seed(self, seed: Seed, slot_num = -1):
+ self.seeds.append(seed)
+
+ # used to track virtual slot number, TODO: remove concept in UI of slots
+ if slot_num > 0:
+ self.slots[slot_num - 1] = len(self.seeds) - 1
+
+ def add_passphrase_to_seed(self, seed, passphrase: str = ""):
+ if not isinstance(passphrase, str):
+ raise ValueError("Invalid passphrase format")
+
+ # find existing seed to add passphrase to
+ if seed in self.seeds:
+ idx = self.seeds.index(seed)
+ seed.passphrase = passphrase
+ return True
+ else:
+ return False
+
+ def remove_passphrase_from_seed(self, seed):
+ return self.add_passphrase_to_seed(seed, "")
+
+ # validates mnemonic part of a seed in list or string format
+ def validate_mnemonic(self, seed_phrase) -> bool:
+ try:
+ seed = Seed(seed_phrase)
+ except Exception as e:
+ return False
+
+ return True
+
+ def seed_count(self):
+ return len(self.seeds)
- self.saved_seeds = [[],[],[]]
- self.passphrase = ["","",""]
+ #
+ # Slot related methods to be removed once the concepts of slots is removed
+ # TODO: remove concept in UI of slots
+ #
+
+ def get_mnemonic_from_slot(self, slot_num = 0):
+ if self.slots[slot_num - 1] == -1:
+ raise ValueError("Slot is unexpectedly empty")
+
+ return self.seeds[self.slots[slot_num - 1]].mnemonic
- ###
- ### Seed Related Methods
- ###
+ def get_passphrase_from_slot(self, slot_num = 0):
+ if self.slots[slot_num - 1] == -1:
+ raise ValueError("Slot is unexpectedly empty")
+
+ return self.seeds[self.slots[slot_num - 1]].passphrase
+
+ def add_passphrase_to_slot(self, passphrase: str = "", slot_num = -1):
+ if self.slots[slot_num - 1] == -1:
+ raise ValueError("Slot is unexpectedly empty")
+
+ return self.add_passphrase_to_seed(self.seeds[self.slots[slot_num - 1]], passphrase)
- def save_seed_phrase(self, seed_phrase = [], slot_num = 0) -> bool:
- slot_idx = slot_num - 1
- if slot_idx in (0,1,2):
- self.saved_seeds[slot_idx] = seed_phrase[:]
- self.passphrase[slot_idx] = ""
- return True
- else:
- return False
+ def remove_passphrase_from_slot(self, slot_num = -1):
+ if self.slots[slot_num - 1] == -1:
+ raise ValueError("Slot is unexpectedly empty")
+
+ return self.remove_passphrase_from_seed(self.seeds[self.slots[slot_num - 1]])
- def save_passphrase(self, passphrase, slot_num = 0) -> bool:
- slot_idx = slot_num - 1
- if slot_idx in (0,1,2):
- self.passphrase[slot_idx] = passphrase
- return True
- else:
- return False
+ def get_first_seed_slot(self):
+ if self.slots[0] != -1:
+ return self.slots[0] + 1
+ elif self.slots[1] != -1:
+ return self.slots[1] + 1
+ elif self.slots[2] != -1:
+ return self.slots[2] + 1
- def get_seed_phrase(self, slot_num) -> []:
- slot_idx = slot_num - 1
- if len(self.saved_seeds[slot_idx]) > 0:
- return self.saved_seeds[slot_idx]
- else:
- return []
+ def check_slot_1(self) -> bool:
+ if self.slots[0] != -1:
+ return True
+ else:
+ return False
+
+ def check_slot_2(self) -> bool:
+ if self.slots[1] != -1:
+ return True
+ else:
+ return False
+
+ def check_slot_3(self) -> bool:
+ if self.slots[2] != -1:
+ return True
+ else:
+ return False
- def get_passphrase(self, slot_num) -> str:
- slot_idx = slot_num - 1
- if len(self.passphrase[slot_idx]) > 0:
- return self.passphrase[slot_idx]
- else:
- return ""
+ def check_slot(self, slot_num) -> bool:
+ if self.slots[slot_num-1] != -1:
+ return True
+ else:
+ return False
- def delete_passphrase(self, slot_num) -> bool:
- slot_idx = slot_num - 1
- self.passphrase[slot_idx] = ""
+ def check_slot_passphrase(self, slot_num) -> bool:
+ if self.slots[slot_num-1] != -1:
+ if self.seeds[self.slots[slot_num-1]].passphrase != "":
+ return True
+ return False
+
+ def slot_avaliable(self) -> bool:
+ if self.slots[0] == -1 or self.slots[1] == -1 or self.slots[2] == -1:
+ return True
+ return True
+
+ def num_of_saved_seeds(self) -> int:
+ count = 0
+ if self.slots[0] != -1:
+ count += 1
+ if self.slots[1] != -1:
+ count += 1
+ if self.slots[2] != -1:
+ count += 1
- return True
+ return count
- def valid_seed_structure(self, seed_phrase) -> bool:
- if isinstance(seed_phrase, list):
- if len(seed_phrase) in (12,24):
- if all(word in seed_phrase for word in SEEDWORDS):
- return True
- else:
- return False
- else:
- return False # Required to be 12 or 24 word seed phrase
- else:
- return False # Not list, not valid seed_phrase
+ def num_of_free_slots(self) -> int:
+ count = 0
+ if self.slots[0] == -1:
+ count += 1
+ if self.slots[1] == -1:
+ count += 1
+ if self.slots[2] == -1:
+ count += 1
- def check_slot_1(self) -> bool:
- if len(self.saved_seeds[0]) > 0:
- return True
- else:
- return False
+ return count
- def check_slot_2(self) -> bool:
- if len(self.saved_seeds[1]) > 0:
- return True
- else:
- return False
+ def save_seed_phrase(self, seed_phrase = [], slot_num = 0) -> bool:
+ return self.add_mnemonic(seed_phrase,slot_num=slot_num)
- def check_slot_3(self) -> bool:
- if len(self.saved_seeds[2]) > 0:
- return True
- else:
- return False
+ def save_passphrase(self, passphrase, slot_num = 0) -> bool:
+ return self.add_passphrase_to_slot(passphrase, slot_num=slot_num)
- def check_slot(self, slot_num) -> bool:
- if len(self.saved_seeds[slot_num-1]) > 0:
- return True
- else:
- return False
+ def get_seed_phrase(self, slot_num) -> []:
+ if self.slots[slot_num - 1] == -1:
+ return []
- def check_slot_passphrase(self, slot_num) -> bool:
- if len(self.passphrase[slot_num-1]) > 0:
- return True
- else:
- return False
+ return self.seeds[self.slots[slot_num - 1]].mnemonic.split()
- def check_if_seed_valid(self, seed_phrase) -> bool:
- try:
- bip39.mnemonic_to_seed((" ".join(seed_phrase)).strip())
- except ValueError:
- return False
+ def get_passphrase(self, slot_num) -> str:
+ if self.slots[slot_num - 1] == -1:
+ return ""
- return True
+ return self.seeds[self.slots[slot_num - 1]].passphrase
- def slot_avaliable(self) -> bool:
- if len(self.saved_seeds[0]) > 0 and len(self.saved_seeds[1]) > 0 and len(self.saved_seeds[2]) > 0:
- return False
- return True
+ def get_seed(self, slot_num) -> Seed:
+ if self.slots[slot_num - 1] == -1:
+ return Seed()
- def num_of_saved_seeds(self) -> int:
- count = 0
- if len(self.saved_seeds[0]) > 0:
- count += 1
- if len(self.saved_seeds[1]) > 0:
- count += 1
- if len(self.saved_seeds[2]) > 0:
- count += 1
+ return self.seeds[self.slots[slot_num - 1]]
- return count
+ def delete_passphrase(self, slot_num) -> bool:
+ if self.slots[slot_num - 1] == -1:
+ return False
- def num_of_passphrase_seeds(self) -> int:
- count = 0
- if len(self.saved_seeds[0]) > 0 and len(self.passphrase[0]) > 0:
- count += 1
- if len(self.saved_seeds[1]) > 0 and len(self.passphrase[1]) > 0:
- count += 1
- if len(self.saved_seeds[2]) > 0 and len(self.passphrase[2]) > 0:
- count += 1
+ return self.remove_passphrase_from_seed(self.seeds[self.slots[slot_num - 1]])
- return count
-
- def get_first_seed_slot(self) -> int:
- if len(self.saved_seeds[0]) > 0:
- return 1
- elif len(self.saved_seeds[1]) > 0:
- return 2
- elif len(self.saved_seeds[2]) > 0:
- return 3
- else:
- return None
-
- def num_of_free_slots(self) -> int:
- count = 0
- if len(self.saved_seeds[0]) == 0:
- count += 1
- if len(self.saved_seeds[1]) == 0:
- count += 1
- if len(self.saved_seeds[2]) == 0:
- count += 1
-
- return count
\ No newline at end of file
+ def check_if_seed_valid(self, seed_phrase) -> bool:
+ return self.validate_mnemonic(seed_phrase)
+
\ No newline at end of file
diff --git a/src/seedsigner/models/settings.py b/src/seedsigner/models/settings.py
index b15f54b4..586f3040 100644
--- a/src/seedsigner/models/settings.py
+++ b/src/seedsigner/models/settings.py
@@ -1,8 +1,9 @@
from seedsigner.helpers import Singleton
-from .encode_qr import EncodeQRDensity
from .qr_type import QRType
+from .encode_qr_density import EncodeQRDensity
import configparser
+from embit import bip39
class Settings(Singleton):
@@ -12,7 +13,7 @@ class Settings(Singleton):
if cls._instance:
raise Exception("Instance already configured")
- # Instantiate the one and only Controller instance
+ # Instantiate the one and only instance
settings = cls.__new__(cls)
cls._instance = settings
@@ -21,17 +22,18 @@ class Settings(Singleton):
'system': {
'debug': False,
'default_language': "en",
- 'persistent_settings': False
+ 'persistent_settings': False,
+ 'wordlist': bip39.WORDLIST
},
'display': {
'text_color': "ORANGE",
+ 'qr_background_color': "FFFFFF",
+ 'camera_rotation': 0
},
'wallet': {
'network': "main",
'software': "Specter Desktop",
'qr_density': EncodeQRDensity.MEDIUM,
- 'script_policy': "PKWSH",
- 'custom_derivation_enabled': False,
'custom_derivation': 'm/0/0'
}
}
@@ -49,12 +51,12 @@ class Settings(Singleton):
self._data["system"]["debug"] = config.getboolean("system", "debug")
self._data["system"]["default_language"] = config["system"]["default_language"]
self._data["display"]["text_color"] = config["display"]["text_color"]
+ self._data["display"]["camera_rotation"] = int(config["display"]["camera_rotation"])
self.network = config["wallet"]["network"]
self.software = config["wallet"]["software"]
self.qr_density = int(config["wallet"]["qr_density"])
- self.script_policy = config["wallet"]["script_policy"]
- self.custom_derivation_enabled = config.getboolean("wallet", "custom_derivation_enabled")
self.custom_derivation = config["wallet"]["custom_derivation"]
+ self.qr_background_color = config["display"]["qr_background_color"]
### persistent settings handling
@@ -116,12 +118,37 @@ class Settings(Singleton):
@property
def language(self):
return self._data["system"]["default_language"]
+
+ @property
+ def wordlist(self):
+ return self._data["system"]["wordlist"]
### display
@property
def text_color(self):
return self._data["display"]["text_color"]
+
+ @property
+ def qr_background_color(self):
+ return self._data["display"]["qr_background_color"]
+
+ @qr_background_color.setter
+ def qr_background_color(self, value):
+ self._data["display"]["qr_background_color"] = value
+ self.__writeConfig()
+
+ @property
+ def camera_rotation(self):
+ return self._data["display"]["camera_rotation"]
+
+ @camera_rotation.setter
+ def camera_rotation(self, value):
+ if value in [0, 90, 180, 270]:
+ self._data["display"]["camera_rotation"] = value
+ self.__writeConfig()
+ else:
+ raise Exception("Unexpected display.camera_rotation settings.ini value")
### wallet
@@ -190,59 +217,50 @@ class Settings(Singleton):
else:
return "Unknown"
- @property
- def script_policy(self):
- return self._data["wallet"]["script_policy"]
-
- @script_policy.setter
- def script_policy(self, value):
- if value in ("PKWSH", "PKWPKH"):
- self._data["wallet"]["script_policy"] = value
- self.__writeConfig()
- else:
- raise Exception("Unexpected wallet.script_policy settings.ini value")
-
- @property
- def policy_name(self):
- if self.script_policy == "PKWSH":
- return "Multi Sig"
- elif self.script_policy == "PKWPKH":
- return "Single Sig"
- else:
- raise Exception("Unexpected Wallet policy value")
-
@property
def custom_derivation(self):
- if self._data["wallet"]["custom_derivation_enabled"]:
- return self._data["wallet"]["custom_derivation"]
- else:
- return None
-
- @property
- def derivation(self):
- if self.script_policy == "PKWSH" and self.network == "main":
- return "m/48'/0'/0'/2'"
- elif self.script_policy == "PKWSH" and self.network == "test":
- return "m/48'/1'/0'/2'"
- elif self.script_policy == "PKWPKH" and self.network == "main":
- return "m/84'/0'/0'"
- elif self.script_policy == "PKWPKH" and self.network == "test":
- return "m/84'/1'/0'"
- else:
- raise Exception("Unsupported Derivation Path or Policy")
+ return self._data["wallet"]["custom_derivation"]
@custom_derivation.setter
def custom_derivation(self, value):
# TODO: parse and validate custom derivation path
self._data["wallet"]["custom_derivation"] = value
-
- @property
- def custom_derivation_enabled(self):
- return self._data["wallet"]["custom_derivation_enabled"]
-
- @custom_derivation_enabled.setter
- def custom_derivation_enabled(self, value):
- if type(value) == bool:
- self._data["wallet"]["custom_derivation_enabled"] = value
+
+ @staticmethod
+ def calc_derivation(network, wallet_type, script_type):
+ if network == "main":
+ if wallet_type.lower() == "single sig":
+ if script_type.lower() == "native segwit":
+ return "m/84'/0'/0'"
+ elif script_type.lower() == "nested segwit":
+ return "m/49'/0'/0'"
+ else:
+ raise Exception("Unexpected script type")
+ elif wallet_type.lower() == "multisig":
+ if script_type.lower() == "native segwit":
+ return "m/48'/0'/0'/2'"
+ elif script_type.lower() == "nested segwit":
+ return "m/48'/0'/0'/1'"
+ else:
+ raise Exception("Unexpected script type")
+ else:
+ raise Exception("Unexpected wallet type")
+ elif network == "test":
+ if wallet_type.lower() == "single sig":
+ if script_type.lower() == "native segwit":
+ return "m/84'/1'/0'"
+ elif script_type.lower() == "nested segwit":
+ return "m/49'/1'/0'"
+ else:
+ raise Exception("Unexpected script type")
+ elif wallet_type.lower() == "multisig":
+ if script_type.lower() == "native segwit":
+ return "m/48'/1'/0'/2'"
+ elif script_type.lower() == "nested segwit":
+ return "m/48'/1'/0'/1'"
+ else:
+ raise Exception("Unexpected script type")
+ else:
+ raise Exception("Unexpected wallet type")
else:
- raise Exception("Unexpected wallet.custom_derivation_enabled settings.ini value")
\ No newline at end of file
+ raise Exception("Unexpected network type")
\ No newline at end of file
diff --git a/src/seedsigner/resources/fonts/Anton.ttf b/src/seedsigner/resources/fonts/Anton.ttf
new file mode 100644
index 00000000..d8dad257
Binary files /dev/null and b/src/seedsigner/resources/fonts/Anton.ttf differ
diff --git a/src/seedsigner/resources/fonts/Assistant-Bold.ttf b/src/seedsigner/resources/fonts/Assistant-Bold.ttf
new file mode 100644
index 00000000..b231abef
Binary files /dev/null and b/src/seedsigner/resources/fonts/Assistant-Bold.ttf differ
diff --git a/src/seedsigner/resources/fonts/Assistant-Medium.ttf b/src/seedsigner/resources/fonts/Assistant-Medium.ttf
new file mode 100644
index 00000000..83efe1ab
Binary files /dev/null and b/src/seedsigner/resources/fonts/Assistant-Medium.ttf differ
diff --git a/src/seedsigner/resources/fonts/Assistant-OFL.txt b/src/seedsigner/resources/fonts/Assistant-OFL.txt
new file mode 100644
index 00000000..a754f0e4
--- /dev/null
+++ b/src/seedsigner/resources/fonts/Assistant-OFL.txt
@@ -0,0 +1,95 @@
+Copyright 2020 The Assistant Project Authors (https://github.com/hafontia/Assistant).
+Copyright 2010 The Source Sans Pro Authors (https://github.com/adobe-fonts/source-sans-pro), with Reserved Font Name 'Source'.
+Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/src/seedsigner/resources/fonts/Cour.ttf b/src/seedsigner/resources/fonts/Cour.ttf
new file mode 100644
index 00000000..2c99e08c
Binary files /dev/null and b/src/seedsigner/resources/fonts/Cour.ttf differ
diff --git a/src/seedsigner/resources/fonts/RobotoMono-LICENSE.txt b/src/seedsigner/resources/fonts/RobotoMono-LICENSE.txt
new file mode 100644
index 00000000..75b52484
--- /dev/null
+++ b/src/seedsigner/resources/fonts/RobotoMono-LICENSE.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/src/seedsigner/resources/fonts/RobotoMono-Medium.ttf b/src/seedsigner/resources/fonts/RobotoMono-Medium.ttf
new file mode 100644
index 00000000..8461be77
Binary files /dev/null and b/src/seedsigner/resources/fonts/RobotoMono-Medium.ttf differ
diff --git a/src/seedsigner/resources/logo_black_240.png b/src/seedsigner/resources/logo_black_240.png
new file mode 100644
index 00000000..68e3043f
Binary files /dev/null and b/src/seedsigner/resources/logo_black_240.png differ
diff --git a/src/seedsigner/views/__init__.py b/src/seedsigner/views/__init__.py
index 47932cd0..bcf195a7 100644
--- a/src/seedsigner/views/__init__.py
+++ b/src/seedsigner/views/__init__.py
@@ -4,3 +4,4 @@ from .menu_view import *
from .seed_tools_view import *
from .settings_tools_view import *
from .signing_tools_view import *
+from .screensaver import OpeningSplashView, ScreensaverView
\ No newline at end of file
diff --git a/src/seedsigner/views/io_test_view.py b/src/seedsigner/views/io_test_view.py
index d334f689..ca3a6f3a 100644
--- a/src/seedsigner/views/io_test_view.py
+++ b/src/seedsigner/views/io_test_view.py
@@ -23,6 +23,9 @@ class IOTestView(View):
self.draw_modal(["Initializing I/O Test"])
print("Initializing I/O Test")
self.qr_text = "Scan ANY QR Code"
+ self.redraw = False
+ self.redraw_complete = False
+ self.exit = False
try:
self.controller.get_instance().camera.start_video_stream_mode()
@@ -75,7 +78,7 @@ class IOTestView(View):
self.redraw_complete = False
self.redraw = False
self.draw.rectangle((0,0,View.canvas_width, View.canvas_height), outline=0, fill=0)
- self.draw.text((45, 5), "Input/Output Check:", fill=View.color, font=View.IMPACT18)
+ self.draw.text((45, 5), "Input/Output Check:", fill=View.color, font=View.ASSISTANT18)
self.draw.polygon([(61, 89), (80, 46), (99, 89)], outline=View.color, fill=0)
self.draw.polygon([(51, 100), (8, 119), (51, 138)], outline=View.color, fill=0)
self.draw.polygon([(109, 100), (152, 119), (109, 138)], outline=View.color, fill=0)
@@ -83,10 +86,10 @@ class IOTestView(View):
self.draw.ellipse([(61, 99), (99, 141)], outline=View.color, fill=0)
self.draw.ellipse([(198, 40), (238, 80)], outline=View.color, fill=0)
self.draw.ellipse([(198, 95), (238, 135)], outline=View.color, fill=0)
- self.draw.text((200, 160), "EXIT", fill=View.color, font=View.IMPACT18)
+ self.draw.text((200, 160), "EXIT", fill=View.color, font=View.ASSISTANT18)
self.draw.rectangle((30, 205, 210, 235), outline=View.color, fill="BLACK")
- tw, th = self.draw.textsize(self.qr_text, font=View.IMPACT22)
- self.draw.text(((240 - tw) / 2, 205), self.qr_text, fill=View.color, font=View.IMPACT22)
+ tw, th = self.draw.textsize(self.qr_text, font=View.ASSISTANT22)
+ self.draw.text(((240 - tw) / 2, 205), self.qr_text, fill=View.color, font=View.ASSISTANT22)
View.DispShowImage()
self.redraw_complete = True
@@ -141,8 +144,8 @@ class IOTestView(View):
self.qr_text = "QR Scanned"
if self.redraw == False and self.redraw_complete == True:
self.draw.rectangle((30, 205, 210, 235), outline=View.color, fill=View.color)
- tw, th = self.draw.textsize(self.qr_text, font=View.IMPACT22)
- self.draw.text(((240 - tw) / 2, 205), self.qr_text, fill="BLACK", font=View.IMPACT22)
+ tw, th = self.draw.textsize(self.qr_text, font=View.ASSISTANT22)
+ self.draw.text(((240 - tw) / 2, 205), self.qr_text, fill="BLACK", font=View.ASSISTANT22)
View.DispShowImage()
self.redraw = True
\ No newline at end of file
diff --git a/src/seedsigner/views/menu_view.py b/src/seedsigner/views/menu_view.py
index fcf0f0b9..6fcbd1a9 100644
--- a/src/seedsigner/views/menu_view.py
+++ b/src/seedsigner/views/menu_view.py
@@ -1,7 +1,7 @@
# Internal file class dependencies
from . import View
from seedsigner.helpers import B, Path
-from seedsigner.models import SeedStorage, Settings
+from seedsigner.models import SeedStorage, Settings, Seed
# External Dependencies
import time
@@ -124,14 +124,13 @@ class MenuView(View):
### Settings Menu
def display_settings_menu(self) -> int:
- lines = ["... [ Return to Main ]", "Wallet: ", "Script Policy: ", "Network: ", "QR Density: ", "Input / Output Tests", "Persistent Settings: ", "Version Info", "Donate to SeedSigner", "Reset SeedSigner"]
+ lines = ["... [ Return to Main ]", "Wallet: ", "Network: ", "QR Density: ", "Input / Output Tests", "Persistent Settings: ", "Camera Rotation", "Version Info", "Donate to SeedSigner", "Reset SeedSigner"]
input = 0
lines[1] = lines[1].replace("", Settings.get_instance().software)
- lines[2] = lines[2].replace("", Settings.get_instance().policy_name)
- lines[3] = lines[3].replace("", Settings.get_instance().network)
- lines[4] = lines[4].replace("", Settings.get_instance().qr_density_name)
- lines[6] = lines[6].replace("", Settings.get_instance().persistent_display)
+ lines[2] = lines[2].replace("", Settings.get_instance().network)
+ lines[3] = lines[3].replace("", Settings.get_instance().qr_density_name)
+ lines[5] = lines[5].replace("", Settings.get_instance().persistent_display)
# Draw Menu
self.selected_menu_num = 1
@@ -150,15 +149,15 @@ class MenuView(View):
elif self.selected_menu_num == 2:
return Path.WALLET
elif self.selected_menu_num == 3:
- return Path.WALLET_POLICY
- elif self.selected_menu_num == 4:
return Path.CURRENT_NETWORK
- elif self.selected_menu_num == 5:
+ elif self.selected_menu_num == 4:
return Path.QR_DENSITY_SETTING
- elif self.selected_menu_num == 6:
+ elif self.selected_menu_num == 5:
return Path.IO_TEST_TOOL
- elif self.selected_menu_num == 7:
+ elif self.selected_menu_num == 6:
return Path.PERSISTENT_SETTINGS
+ elif self.selected_menu_num == 7:
+ return Path.CAMERA_ROTATION
elif self.selected_menu_num == 8:
return Path.VERSION_INFO
elif self.selected_menu_num == 9:
@@ -311,8 +310,8 @@ class MenuView(View):
#Menu has changed, redraw
View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0)
- tw, th = View.draw.textsize(t, font=View.IMPACT22)
- View.draw.text(((240 - tw) / 2, 2), t, fill=View.color, font=View.IMPACT22)
+ tw, th = View.draw.textsize(t, font=View.ASSISTANT22)
+ View.draw.text(((240 - tw) / 2, 2), t, fill=View.color, font=View.ASSISTANT22)
num_of_lines = len(lines)
@@ -350,8 +349,8 @@ class MenuView(View):
if num_of_lines >= 15:
self.draw_menu_text(15, 175, lines[14], (True if selected_menu_num == 15 else False))
- tw, th = View.draw.textsize(b, font=View.IMPACT18)
- View.draw.text(((240 - tw) / 2, 210), b, fill=View.color, font=View.IMPACT18)
+ tw, th = View.draw.textsize(b, font=View.ASSISTANT18)
+ View.draw.text(((240 - tw) / 2, 210), b, fill=View.color, font=View.ASSISTANT18)
View.DispShowImage()
# saved update menu lines and selection
@@ -378,8 +377,8 @@ class MenuView(View):
def draw_menu_text(self, x, y, line, selected) -> None:
if selected == True:
View.draw.rectangle((5, y-3, 235, y+28), outline=0, fill=View.color)
- View.draw.text((x, y) , line, fill="BLACK", font=View.IMPACT20)
+ View.draw.text((x, y) , line, fill="BLACK", font=View.ASSISTANT20BOLD)
else:
- View.draw.text((x, y) , line, fill=View.color, font=View.IMPACT20)
+ View.draw.text((x, y) , line, fill=View.color, font=View.ASSISTANT20)
return
diff --git a/src/seedsigner/views/screensaver.py b/src/seedsigner/views/screensaver.py
new file mode 100644
index 00000000..3f4afa0d
--- /dev/null
+++ b/src/seedsigner/views/screensaver.py
@@ -0,0 +1,136 @@
+import os
+import random
+import time
+
+from PIL import Image, ImageDraw
+
+from . import View
+
+from seedsigner.helpers import B
+
+
+class LogoView(View):
+ def __init__(self):
+ dirname = os.path.dirname(__file__)
+ logo_url = os.path.join(dirname, "../../", "seedsigner", "resources", "logo_black_240.png")
+ self.logo = Image.open(logo_url)
+
+
+
+class OpeningSplashView(LogoView):
+ def start(self):
+ from seedsigner.controller import Controller
+ controller = Controller.get_instance()
+
+ # Fade in alpha
+ for i in range(250, -1, -25):
+ self.logo.putalpha(255 - i)
+ background = Image.new("RGBA", self.logo.size, (0,0,0))
+ View.disp.ShowImage(Image.alpha_composite(background, self.logo), 0, 0)
+
+ # Display version num and hold for a few seconds
+ font = View.ROBOTOCONDENSED_REGULAR_22
+ version = f"v{controller.VERSION}"
+ tw, th = font.getsize(version)
+ x = int((View.canvas_width - tw) / 2)
+ y = int(View.canvas_height / 2) + 40
+
+ draw = ImageDraw.Draw(self.logo)
+ draw.text((x, y), version, fill="orange", font=font)
+ View.DispShowImage(self.logo)
+ time.sleep(3)
+
+
+
+class ScreensaverView(LogoView):
+ def __init__(self, buttons):
+ super().__init__()
+
+ self.buttons = buttons
+
+ # Paste the logo in a bigger image that is 2x the size of the logo
+ self.image = Image.new("RGB", (2 * self.logo.size[0], 2 * self.logo.size[1]), (0,0,0))
+ self.image.paste(self.logo, (int(self.logo.size[0] / 2), int(self.logo.size[1] / 2)))
+
+ self.min_coords = (0, 0)
+ self.max_coords = (self.logo.size[0], self.logo.size[1])
+
+ max_increment = 25
+ self.increment_x = self.rand_increment()
+ self.increment_y = self.rand_increment()
+ self.cur_x = int(self.logo.size[0] / 2)
+ self.cur_y = int(self.logo.size[1] / 2)
+
+ self._is_running = False
+ self.last_screen = None
+
+
+
+ @property
+ def is_running(self):
+ return self._is_running
+
+
+ def rand_increment(self):
+ max_increment = 10.0
+ min_increment = 1.0
+ increment = random.uniform(min_increment, max_increment)
+ if random.uniform(-1.0, 1.0) < 0.0:
+ return -1.0 * increment
+ return increment
+
+
+ def start(self):
+ if self.is_running:
+ return
+
+ self._is_running = True
+
+ # Store the current screen in order to restore it later
+ self.last_screen = View.canvas.copy()
+
+ screensaver_start = int(time.time() * 1000)
+
+ while True:
+ if self.buttons.has_any_input():
+ return self.stop()
+
+ # Must crop the image to the exact display size
+ crop = self.image.crop((
+ self.cur_x, self.cur_y,
+ self.cur_x + View.canvas_width, self.cur_y + View.canvas_height))
+ View.disp.ShowImage(crop, 0, 0)
+
+ self.cur_x += self.increment_x
+ self.cur_y += self.increment_y
+
+ if self.cur_x < self.min_coords[0]:
+ self.cur_x = self.min_coords[0]
+ self.increment_x = self.rand_increment()
+ if self.increment_x < 0.0:
+ self.increment_x *= -1.0
+ elif self.cur_x > self.max_coords[0]:
+ self.cur_x = self.max_coords[0]
+ self.increment_x = self.rand_increment()
+ if self.increment_x > 0.0:
+ self.increment_x *= -1.0
+
+ if self.cur_y < self.min_coords[1]:
+ self.cur_y = self.min_coords[1]
+ self.increment_y = self.rand_increment()
+ if self.increment_y < 0.0:
+ self.increment_y *= -1.0
+ elif self.cur_y > self.max_coords[1]:
+ self.cur_y = self.max_coords[1]
+ self.increment_y = self.rand_increment()
+ if self.increment_y > 0.0:
+ self.increment_y *= -1.0
+
+
+ def stop(self):
+ # Restore the original screen
+ View.DispShowImage(self.last_screen)
+
+ self._is_running = False
+
+
diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py
index c79e7f42..263bb855 100644
--- a/src/seedsigner/views/seed_tools_view.py
+++ b/src/seedsigner/views/seed_tools_view.py
@@ -1,6 +1,4 @@
# External Dependencies
-from embit import bip39
-from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes
from PIL import ImageDraw, Image
from PIL.ImageOps import autocontrast
import hashlib
@@ -11,13 +9,11 @@ import time
# Internal file class dependencies
from . import View
from seedsigner.helpers import B, QR, Keyboard, TextEntryDisplay, mnemonic_generation
-from seedsigner.models import DecodeQR, DecodeQRStatus, QRType, EncodeQR
-
+from seedsigner.models import DecodeQR, DecodeQRStatus, QRType, EncodeQR, Settings, Seed
+from seedsigner.models import SeedStorage
class SeedToolsView(View):
- SEEDWORDS = ["abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident", "account", "accuse", "achieve", "acid", "acoustic", "acquire", "across", "act", "action", "actor", "actress", "actual", "adapt", "add", "addict", "address", "adjust", "admit", "adult", "advance", "advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent", "agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album", "alcohol", "alert", "alien", "all", "alley", "allow", "almost", "alone", "alpha", "already", "also", "alter", "always", "amateur", "amazing", "among", "amount", "amused", "analyst", "anchor", "ancient", "anger", "angle", "angry", "animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique", "anxiety", "any", "apart", "apology", "appear", "apple", "approve", "april", "arch", "arctic", "area", "arena", "argue", "arm", "armed", "armor", "army", "around", "arrange", "arrest", "arrive", "arrow", "art", "artefact", "artist", "artwork", "ask", "aspect", "assault", "asset", "assist", "assume", "asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction", "audit", "august", "aunt", "author", "auto", "autumn", "average", "avocado", "avoid", "awake", "aware", "away", "awesome", "awful", "awkward", "axis", "baby", "bachelor", "bacon", "badge", "bag", "balance", "balcony", "ball", "bamboo", "banana", "banner", "bar", "barely", "bargain", "barrel", "base", "basic", "basket", "battle", "beach", "bean", "beauty", "because", "become", "beef", "before", "begin", "behave", "behind", "believe", "below", "belt", "bench", "benefit", "best", "betray", "better", "between", "beyond", "bicycle", "bid", "bike", "bind", "biology", "bird", "birth", "bitter", "black", "blade", "blame", "blanket", "blast", "bleak", "bless", "blind", "blood", "blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body", "boil", "bomb", "bone", "bonus", "book", "boost", "border", "boring", "borrow", "boss", "bottom", "bounce", "box", "boy", "bracket", "brain", "brand", "brass", "brave", "bread", "breeze", "brick", "bridge", "brief", "bright", "bring", "brisk", "broccoli", "broken", "bronze", "broom", "brother", "brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", "bulb", "bulk", "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus", "business", "busy", "butter", "buyer", "buzz", "cabbage", "cabin", "cable", "cactus", "cage", "cake", "call", "calm", "camera", "camp", "can", "canal", "cancel", "candy", "cannon", "canoe", "canvas", "canyon", "capable", "capital", "captain", "car", "carbon", "card", "cargo", "carpet", "carry", "cart", "case", "cash", "casino", "castle", "casual", "cat", "catalog", "catch", "category", "cattle", "caught", "cause", "caution", "cave", "ceiling", "celery", "cement", "census", "century", "cereal", "certain", "chair", "chalk", "champion", "change", "chaos", "chapter", "charge", "chase", "chat", "cheap", "check", "cheese", "chef", "cherry", "chest", "chicken", "chief", "child", "chimney", "choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar", "cinnamon", "circle", "citizen", "city", "civil", "claim", "clap", "clarify", "claw", "clay", "clean", "clerk", "clever", "click", "client", "cliff", "climb", "clinic", "clip", "clock", "clog", "close", "cloth", "cloud", "clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut", "code", "coffee", "coil", "coin", "collect", "color", "column", "combine", "come", "comfort", "comic", "common", "company", "concert", "conduct", "confirm", "congress", "connect", "consider", "control", "convince", "cook", "cool", "copper", "copy", "coral", "core", "corn", "correct", "cost", "cotton", "couch", "country", "couple", "course", "cousin", "cover", "coyote", "crack", "cradle", "craft", "cram", "crane", "crash", "crater", "crawl", "crazy", "cream", "credit", "creek", "crew", "cricket", "crime", "crisp", "critic", "crop", "cross", "crouch", "crowd", "crucial", "cruel", "cruise", "crumble", "crunch", "crush", "cry", "crystal", "cube", "culture", "cup", "cupboard", "curious", "current", "curtain", "curve", "cushion", "custom", "cute", "cycle", "dad", "damage", "damp", "dance", "danger", "daring", "dash", "daughter", "dawn", "day", "deal", "debate", "debris", "decade", "december", "decide", "decline", "decorate", "decrease", "deer", "defense", "define", "defy", "degree", "delay", "deliver", "demand", "demise", "denial", "dentist", "deny", "depart", "depend", "deposit", "depth", "deputy", "derive", "describe", "desert", "design", "desk", "despair", "destroy", "detail", "detect", "develop", "device", "devote", "diagram", "dial", "diamond", "diary", "dice", "diesel", "diet", "differ", "digital", "dignity", "dilemma", "dinner", "dinosaur", "direct", "dirt", "disagree", "discover", "disease", "dish", "dismiss", "disorder", "display", "distance", "divert", "divide", "divorce", "dizzy", "doctor", "document", "dog", "doll", "dolphin", "domain", "donate", "donkey", "donor", "door", "dose", "double", "dove", "draft", "dragon", "drama", "drastic", "draw", "dream", "dress", "drift", "drill", "drink", "drip", "drive", "drop", "drum", "dry", "duck", "dumb", "dune", "during", "dust", "dutch", "duty", "dwarf", "dynamic", "eager", "eagle", "early", "earn", "earth", "easily", "east", "easy", "echo", "ecology", "economy", "edge", "edit", "educate", "effort", "egg", "eight", "either", "elbow", "elder", "electric", "elegant", "element", "elephant", "elevator", "elite", "else", "embark", "embody", "embrace", "emerge", "emotion", "employ", "empower", "empty", "enable", "enact", "end", "endless", "endorse", "enemy", "energy", "enforce", "engage", "engine", "enhance", "enjoy", "enlist", "enough", "enrich", "enroll", "ensure", "enter", "entire", "entry", "envelope", "episode", "equal", "equip", "era", "erase", "erode", "erosion", "error", "erupt", "escape", "essay", "essence", "estate", "eternal", "ethics", "evidence", "evil", "evoke", "evolve", "exact", "example", "excess", "exchange", "excite", "exclude", "excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", "exit", "exotic", "expand", "expect", "expire", "explain", "expose", "express", "extend", "extra", "eye", "eyebrow", "fabric", "face", "faculty", "fade", "faint", "faith", "fall", "false", "fame", "family", "famous", "fan", "fancy", "fantasy", "farm", "fashion", "fat", "fatal", "father", "fatigue", "fault", "favorite", "feature", "february", "federal", "fee", "feed", "feel", "female", "fence", "festival", "fetch", "fever", "few", "fiber", "fiction", "field", "figure", "file", "film", "filter", "final", "find", "fine", "finger", "finish", "fire", "firm", "first", "fiscal", "fish", "fit", "fitness", "fix", "flag", "flame", "flash", "flat", "flavor", "flee", "flight", "flip", "float", "flock", "floor", "flower", "fluid", "flush", "fly", "foam", "focus", "fog", "foil", "fold", "follow", "food", "foot", "force", "forest", "forget", "fork", "fortune", "forum", "forward", "fossil", "foster", "found", "fox", "fragile", "frame", "frequent", "fresh", "friend", "fringe", "frog", "front", "frost", "frown", "frozen", "fruit", "fuel", "fun", "funny", "furnace", "fury", "future", "gadget", "gain", "galaxy", "gallery", "game", "gap", "garage", "garbage", "garden", "garlic", "garment", "gas", "gasp", "gate", "gather", "gauge", "gaze", "general", "genius", "genre", "gentle", "genuine", "gesture", "ghost", "giant", "gift", "giggle", "ginger", "giraffe", "girl", "give", "glad", "glance", "glare", "glass", "glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue", "goat", "goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip", "govern", "gown", "grab", "grace", "grain", "grant", "grape", "grass", "gravity", "great", "green", "grid", "grief", "grit", "grocery", "group", "grow", "grunt", "guard", "guess", "guide", "guilt", "guitar", "gun", "gym", "habit", "hair", "half", "hammer", "hamster", "hand", "happy", "harbor", "hard", "harsh", "harvest", "hat", "have", "hawk", "hazard", "head", "health", "heart", "heavy", "hedgehog", "height", "hello", "helmet", "help", "hen", "hero", "hidden", "high", "hill", "hint", "hip", "hire", "history", "hobby", "hockey", "hold", "hole", "holiday", "hollow", "home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital", "host", "hotel", "hour", "hover", "hub", "huge", "human", "humble", "humor", "hundred", "hungry", "hunt", "hurdle", "hurry", "hurt", "husband", "hybrid", "ice", "icon", "idea", "identify", "idle", "ignore", "ill", "illegal", "illness", "image", "imitate", "immense", "immune", "impact", "impose", "improve", "impulse", "inch", "include", "income", "increase", "index", "indicate", "indoor", "industry", "infant", "inflict", "inform", "inhale", "inherit", "initial", "inject", "injury", "inmate", "inner", "innocent", "input", "inquiry", "insane", "insect", "inside", "inspire", "install", "intact", "interest", "into", "invest", "invite", "involve", "iron", "island", "isolate", "issue", "item", "ivory", "jacket", "jaguar", "jar", "jazz", "jealous", "jeans", "jelly", "jewel", "job", "join", "joke", "journey", "joy", "judge", "juice", "jump", "jungle", "junior", "junk", "just", "kangaroo", "keen", "keep", "ketchup", "key", "kick", "kid", "kidney", "kind", "kingdom", "kiss", "kit", "kitchen", "kite", "kitten", "kiwi", "knee", "knife", "knock", "know", "lab", "label", "labor", "ladder", "lady", "lake", "lamp", "language", "laptop", "large", "later", "latin", "laugh", "laundry", "lava", "law", "lawn", "lawsuit", "layer", "lazy", "leader", "leaf", "learn", "leave", "lecture", "left", "leg", "legal", "legend", "leisure", "lemon", "lend", "length", "lens", "leopard", "lesson", "letter", "level", "liar", "liberty", "library", "license", "life", "lift", "light", "like", "limb", "limit", "link", "lion", "liquid", "list", "little", "live", "lizard", "load", "loan", "lobster", "local", "lock", "logic", "lonely", "long", "loop", "lottery", "loud", "lounge", "love", "loyal", "lucky", "luggage", "lumber", "lunar", "lunch", "luxury", "lyrics", "machine", "mad", "magic", "magnet", "maid", "mail", "main", "major", "make", "mammal", "man", "manage", "mandate", "mango", "mansion", "manual", "maple", "marble", "march", "margin", "marine", "market", "marriage", "mask", "mass", "master", "match", "material", "math", "matrix", "matter", "maximum", "maze", "meadow", "mean", "measure", "meat", "mechanic", "medal", "media", "melody", "melt", "member", "memory", "mention", "menu", "mercy", "merge", "merit", "merry", "mesh", "message", "metal", "method", "middle", "midnight", "milk", "million", "mimic", "mind", "minimum", "minor", "minute", "miracle", "mirror", "misery", "miss", "mistake", "mix", "mixed", "mixture", "mobile", "model", "modify", "mom", "moment", "monitor", "monkey", "monster", "month", "moon", "moral", "more", "morning", "mosquito", "mother", "motion", "motor", "mountain", "mouse", "move", "movie", "much", "muffin", "mule", "multiply", "muscle", "museum", "mushroom", "music", "must", "mutual", "myself", "mystery", "myth", "naive", "name", "napkin", "narrow", "nasty", "nation", "nature", "near", "neck", "need", "negative", "neglect", "neither", "nephew", "nerve", "nest", "net", "network", "neutral", "never", "news", "next", "nice", "night", "noble", "noise", "nominee", "noodle", "normal", "north", "nose", "notable", "note", "nothing", "notice", "novel", "now", "nuclear", "number", "nurse", "nut", "oak", "obey", "object", "oblige", "obscure", "observe", "obtain", "obvious", "occur", "ocean", "october", "odor", "off", "offer", "office", "often", "oil", "okay", "old", "olive", "olympic", "omit", "once", "one", "onion", "online", "only", "open", "opera", "opinion", "oppose", "option", "orange", "orbit", "orchard", "order", "ordinary", "organ", "orient", "original", "orphan", "ostrich", "other", "outdoor", "outer", "output", "outside", "oval", "oven", "over", "own", "owner", "oxygen", "oyster", "ozone", "pact", "paddle", "page", "pair", "palace", "palm", "panda", "panel", "panic", "panther", "paper", "parade", "parent", "park", "parrot", "party", "pass", "patch", "path", "patient", "patrol", "pattern", "pause", "pave", "payment", "peace", "peanut", "pear", "peasant", "pelican", "pen", "penalty", "pencil", "people", "pepper", "perfect", "permit", "person", "pet", "phone", "photo", "phrase", "physical", "piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot", "pink", "pioneer", "pipe", "pistol", "pitch", "pizza", "place", "planet", "plastic", "plate", "play", "please", "pledge", "pluck", "plug", "plunge", "poem", "poet", "point", "polar", "pole", "police", "pond", "pony", "pool", "popular", "portion", "position", "possible", "post", "potato", "pottery", "poverty", "powder", "power", "practice", "praise", "predict", "prefer", "prepare", "present", "pretty", "prevent", "price", "pride", "primary", "print", "priority", "prison", "private", "prize", "problem", "process", "produce", "profit", "program", "project", "promote", "proof", "property", "prosper", "protect", "proud", "provide", "public", "pudding", "pull", "pulp", "pulse", "pumpkin", "punch", "pupil", "puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle", "pyramid", "quality", "quantum", "quarter", "question", "quick", "quit", "quiz", "quote", "rabbit", "raccoon", "race", "rack", "radar", "radio", "rail", "rain", "raise", "rally", "ramp", "ranch", "random", "range", "rapid", "rare", "rate", "rather", "raven", "raw", "razor", "ready", "real", "reason", "rebel", "rebuild", "recall", "receive", "recipe", "record", "recycle", "reduce", "reflect", "reform", "refuse", "region", "regret", "regular", "reject", "relax", "release", "relief", "rely", "remain", "remember", "remind", "remove", "render", "renew", "rent", "reopen", "repair", "repeat", "replace", "report", "require", "rescue", "resemble", "resist", "resource", "response", "result", "retire", "retreat", "return", "reunion", "reveal", "review", "reward", "rhythm", "rib", "ribbon", "rice", "rich", "ride", "ridge", "rifle", "right", "rigid", "ring", "riot", "ripple", "risk", "ritual", "rival", "river", "road", "roast", "robot", "robust", "rocket", "romance", "roof", "rookie", "room", "rose", "rotate", "rough", "round", "route", "royal", "rubber", "rude", "rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness", "safe", "sail", "salad", "salmon", "salon", "salt", "salute", "same", "sample", "sand", "satisfy", "satoshi", "sauce", "sausage", "save", "say", "scale", "scan", "scare", "scatter", "scene", "scheme", "school", "science", "scissors", "scorpion", "scout", "scrap", "screen", "script", "scrub", "sea", "search", "season", "seat", "second", "secret", "section", "security", "seed", "seek", "segment", "select", "sell", "seminar", "senior", "sense", "sentence", "series", "service", "session", "settle", "setup", "seven", "shadow", "shaft", "shallow", "share", "shed", "shell", "sheriff", "shield", "shift", "shine", "ship", "shiver", "shock", "shoe", "shoot", "shop", "short", "shoulder", "shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side", "siege", "sight", "sign", "silent", "silk", "silly", "silver", "similar", "simple", "since", "sing", "siren", "sister", "situate", "six", "size", "skate", "sketch", "ski", "skill", "skin", "skirt", "skull", "slab", "slam", "sleep", "slender", "slice", "slide", "slight", "slim", "slogan", "slot", "slow", "slush", "small", "smart", "smile", "smoke", "smooth", "snack", "snake", "snap", "sniff", "snow", "soap", "soccer", "social", "sock", "soda", "soft", "solar", "soldier", "solid", "solution", "solve", "someone", "song", "soon", "sorry", "sort", "soul", "sound", "soup", "source", "south", "space", "spare", "spatial", "spawn", "speak", "special", "speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin", "spirit", "split", "spoil", "sponsor", "spoon", "sport", "spot", "spray", "spread", "spring", "spy", "square", "squeeze", "squirrel", "stable", "stadium", "staff", "stage", "stairs", "stamp", "stand", "start", "state", "stay", "steak", "steel", "stem", "step", "stereo", "stick", "still", "sting", "stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street", "strike", "strong", "struggle", "student", "stuff", "stumble", "style", "subject", "submit", "subway", "success", "such", "sudden", "suffer", "sugar", "suggest", "suit", "summer", "sun", "sunny", "sunset", "super", "supply", "supreme", "sure", "surface", "surge", "surprise", "surround", "survey", "suspect", "sustain", "swallow", "swamp", "swap", "swarm", "swear", "sweet", "swift", "swim", "swing", "switch", "sword", "symbol", "symptom", "syrup", "system", "table", "tackle", "tag", "tail", "talent", "talk", "tank", "tape", "target", "task", "taste", "tattoo", "taxi", "teach", "team", "tell", "ten", "tenant", "tennis", "tent", "term", "test", "text", "thank", "that", "theme", "then", "theory", "there", "they", "thing", "this", "thought", "three", "thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger", "tilt", "timber", "time", "tiny", "tip", "tired", "tissue", "title", "toast", "tobacco", "today", "toddler", "toe", "together", "toilet", "token", "tomato", "tomorrow", "tone", "tongue", "tonight", "tool", "tooth", "top", "topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist", "toward", "tower", "town", "toy", "track", "trade", "traffic", "tragic", "train", "transfer", "trap", "trash", "travel", "tray", "treat", "tree", "trend", "trial", "tribe", "trick", "trigger", "trim", "trip", "trophy", "trouble", "truck", "true", "truly", "trumpet", "trust", "truth", "try", "tube", "tuition", "tumble", "tuna", "tunnel", "turkey", "turn", "turtle", "twelve", "twenty", "twice", "twin", "twist", "two", "type", "typical", "ugly", "umbrella", "unable", "unaware", "uncle", "uncover", "under", "undo", "unfair", "unfold", "unhappy", "uniform", "unique", "unit", "universe", "unknown", "unlock", "until", "unusual", "unveil", "update", "upgrade", "uphold", "upon", "upper", "upset", "urban", "urge", "usage", "use", "used", "useful", "useless", "usual", "utility", "vacant", "vacuum", "vague", "valid", "valley", "valve", "van", "vanish", "vapor", "various", "vast", "vault", "vehicle", "velvet", "vendor", "venture", "venue", "verb", "verify", "version", "very", "vessel", "veteran", "viable", "vibrant", "vicious", "victory", "video", "view", "village", "vintage", "violin", "virtual", "virus", "visa", "visit", "visual", "vital", "vivid", "vocal", "voice", "void", "volcano", "volume", "vote", "voyage", "wage", "wagon", "wait", "walk", "wall", "walnut", "want", "warfare", "warm", "warrior", "wash", "wasp", "waste", "water", "wave", "way", "wealth", "weapon", "wear", "weasel", "weather", "web", "wedding", "weekend", "weird", "welcome", "west", "wet", "whale", "what", "wheat", "wheel", "when", "where", "whip", "whisper", "wide", "width", "wife", "wild", "will", "win", "window", "wine", "wing", "wink", "winner", "winter", "wire", "wisdom", "wise", "wish", "witness", "wolf", "woman", "wonder", "wood", "wool", "word", "work", "world", "worry", "worth", "wrap", "wreck", "wrestle", "wrist", "write", "wrong", "yard", "year", "yellow", "you", "young", "youth", "zebra", "zero", "zone", "zoo"]
-
ALPHABET = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
def __init__(self) -> None:
@@ -669,17 +665,18 @@ class SeedToolsView(View):
###
def display_last_word(self, partial_seed_phrase) -> list:
- finalseed = mnemonic_generation.calculate_checksum(partial_seed_phrase)
+ finalseed = mnemonic_generation.calculate_checksum(partial_seed_phrase, wordlist=self.controller.settings.wordlist)
last_word = finalseed[-1]
self.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0)
- tw, th = self.draw.textsize("The final word is :", font=View.IMPACT23)
- self.draw.text(((240 - tw) / 2, 60), "The final word is :", fill=View.color, font=View.IMPACT23)
- tw, th = self.draw.textsize(last_word, font=View.IMPACT50)
- self.draw.text(((240 - tw) / 2, 90), last_word, fill=View.color, font=View.IMPACT50)
+ tw, th = self.draw.textsize("The final word is :", font=View.ASSISTANT23)
+ self.draw.text(((240 - tw) / 2, 60), "The final word is :", fill=View.color, font=View.ASSISTANT23)
+ tw, th = self.draw.textsize(last_word, font=View.ASSISTANT50)
+ self.draw.text(((240 - tw) / 2, 90), last_word, fill=View.color, font=View.ASSISTANT50)
+
+ tw, th = View.draw.textsize("Right to Continue", font=View.ASSISTANT18)
+ View.draw.text(((240 - tw) / 2, 210), "Right to Continue", fill=View.color, font=View.ASSISTANT18)
- tw, th = View.draw.textsize("Right to Continue", font=View.IMPACT18)
- View.draw.text(((240 - tw) / 2, 210), "Right to Continue", fill=View.color, font=View.IMPACT18)
View.DispShowImage()
@@ -817,7 +814,7 @@ class SeedToolsView(View):
def draw_dice(self, dice_selected):
self.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0)
- self.draw.text((45, 5), "Dice roll: " + str(self.roll_number) + "/99", fill=View.color, font=View.IMPACT26)
+ self.draw.text((45, 5), "Dice roll: " + str(self.roll_number) + "/99", fill=View.color, font=View.ASSISTANT26)
# when dice is selected, rect fill will be orange and ellipse will be black, ellipse outline will be the black
# when dice is not selected, rect will will be black and ellipse will be orange, ellipse outline will be orange
@@ -901,7 +898,7 @@ class SeedToolsView(View):
self.draw.ellipse([(208, 174), (220, 186)], outline=View.color, fill=View.color)
# bottom text
- self.draw.text((18, 210), "Press Control Stick to Select", fill=View.color, font=View.IMPACT18)
+ self.draw.text((18, 210), "Press Control Stick to Select", fill=View.color, font=View.ASSISTANT18)
View.DispShowImage()
self.dice_selected = dice_selected
@@ -936,19 +933,19 @@ class SeedToolsView(View):
title = "Seed Phrase (2/2)"
word_index_offset = 12 # Skip ahead one page worth of words
- tw, th = View.draw.textsize(title, font=View.IMPACT18)
- draw.text(((240 - tw) / 2, 2), title, fill=View.color, font=View.IMPACT18)
+ tw, th = View.draw.textsize(title, font=View.ASSISTANT18)
+ draw.text(((240 - tw) / 2, 2), title, fill=View.color, font=View.ASSISTANT18)
for i in range(0, max_range):
- draw.text(word_positions[i], f"{i + 1 + word_index_offset}: " + seed_phrase[i + word_index_offset] , fill=View.color, font=View.IMPACT22)
+ draw.text(word_positions[i], f"{i + 1 + word_index_offset}: " + seed_phrase[i + word_index_offset] , fill=View.color, font=View.ASSISTANT22)
if passphrase and ((len(seed_phrase) > 12 and page_num == 2) or (len(seed_phrase) <= 12 and page_num == 1)):
disp_passphrase = "Passphrase: ************"
- tw, th = View.draw.textsize(disp_passphrase, font=View.IMPACT18)
- draw.text(((240 - tw) / 2, 185), disp_passphrase, fill=View.color, font=View.IMPACT18)
+ tw, th = View.draw.textsize(disp_passphrase, font=View.ASSISTANT18)
+ draw.text(((240 - tw) / 2, 185), disp_passphrase, fill=View.color, font=View.ASSISTANT18)
- tw, th = View.draw.textsize(bottom, font=View.IMPACT18)
- draw.text(((240 - tw) / 2, 212), bottom, fill=View.color, font=View.IMPACT18)
+ tw, th = View.draw.textsize(bottom, font=View.ASSISTANT18)
+ draw.text(((240 - tw) / 2, 212), bottom, fill=View.color, font=View.ASSISTANT18)
View.DispShowImage()
@@ -1011,9 +1008,9 @@ class SeedToolsView(View):
def seed_phrase_as_qr(self, seed_phrase):
- e = EncodeQR(seed_phrase=seed_phrase, qr_type=QRType.SEEDSSQR)
+ e = EncodeQR(seed_phrase=seed_phrase, qr_type=QRType.SEEDSSQR, wordlist=self.controller.settings.wordlist)
image = e.nextPartImage(240, 240, 3)
- View.DispShowImageWithText(image, "click to zoom, right to exit", font=View.IMPACT18, text_color="BLACK", text_background="ORANGE")
+ View.DispShowImageWithText(image, "click to zoom, right to exit", font=View.ASSISTANT18, text_color="BLACK", text_background="ORANGE")
input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_PRESS])
if input == B.KEY_RIGHT:
@@ -1058,8 +1055,8 @@ class SeedToolsView(View):
draw.line((mask_width, View.canvas_height - mask_height, View.canvas_width - mask_width, View.canvas_height - mask_height), fill=View.color)
msg = "click to exit"
- tw, th = draw.textsize(msg, font=View.IMPACT18)
- draw.text(((View.canvas_width - tw) / 2, View.canvas_height - th - 2), msg, fill=View.color, font=View.IMPACT18)
+ tw, th = draw.textsize(msg, font=View.ASSISTANT18)
+ draw.text(((View.canvas_width - tw) / 2, View.canvas_height - th - 2), msg, fill=View.color, font=View.ASSISTANT18)
def draw_block_labels(cur_block_x, cur_block_y):
# Create overlay for block labels (e.g. "D-5")
@@ -1071,10 +1068,12 @@ class SeedToolsView(View):
draw.rectangle((mask_width, 0, View.canvas_width - mask_width, pixels_per_block), fill=View.color)
draw.rectangle((0, mask_height, pixels_per_block, View.canvas_height - mask_height), fill=View.color)
- label_font = View.COURIERNEW24
+ label_font = View.ASSISTANT26
x_label = block_labels_x[cur_block_x]
tw, th = draw.textsize(x_label, font=label_font)
- draw.text(((View.canvas_width - tw) / 2, (pixels_per_block - th) / 2), x_label, fill="BLACK", font=label_font)
+
+ # note: have to nudge the y-coord up (the extra "- 4") for some reason
+ draw.text(((View.canvas_width - tw) / 2, ((pixels_per_block - th) / 2) - 4), x_label, fill="BLACK", font=label_font)
y_label = block_labels_y[cur_block_y]
tw, th = draw.textsize(y_label, font=label_font)
@@ -1098,7 +1097,7 @@ class SeedToolsView(View):
)
while True:
- # View.draw_text_over_image("click to exit", font=View.IMPACT18, text_color="BLACK", text_background="ORANGE")
+ # View.draw_text_over_image("click to exit", font=View.ASSISTANT18, text_color="BLACK", text_background="ORANGE")
input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT, B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS])
if input == B.KEY_RIGHT:
@@ -1145,11 +1144,11 @@ class SeedToolsView(View):
self.draw_modal(["Scanning..."], "Seed QR" ,"Right to Exit")
try:
self.controller.camera.start_video_stream_mode(resolution=(480, 480), framerate=12, format="rgb")
- decoder = DecodeQR()
+ decoder = DecodeQR(wordlist=self.controller.settings.wordlist)
while True:
frame = self.controller.camera.read_video_stream(as_image=True)
if frame is not None:
- View.DispShowImageWithText(frame.resize((240,240)), "Scan Seed QR", font=View.IMPACT22, text_color=View.color, text_background=(0,0,0,225))
+ View.DispShowImageWithText(frame.resize((240,240)), "Scan Seed QR", font=View.ASSISTANT22, text_color=View.color, text_background=(0,0,0,225))
status = decoder.addImage(frame)
if status in (DecodeQRStatus.COMPLETE, DecodeQRStatus.INVALID):
@@ -1193,11 +1192,15 @@ class SeedToolsView(View):
preview_images.append(frame)
if self.buttons.check_for_low(B.KEY_LEFT):
+ # Have to manually update last input time since we're not in a wait_for loop
+ self.buttons.update_last_input_time()
self.words = []
self.controller.camera.stop_video_stream_mode()
return (reshoot, self.words)
elif self.buttons.check_for_low(B.KEY_PRESS):
+ # Have to manually update last input time since we're not in a wait_for loop
+ self.buttons.update_last_input_time()
self.controller.camera.stop_video_stream_mode()
break
@@ -1212,8 +1215,6 @@ class SeedToolsView(View):
display_version = autocontrast(
seed_entropy_image,
cutoff=2
- ).rotate(
- 90
).crop(
(120, 0, 600, 480)
).resize(
@@ -1301,5 +1302,5 @@ class SeedToolsView(View):
def calc_possible_words(self):
- self.possible_words = [i for i in SeedToolsView.SEEDWORDS if i.startswith("".join(self.letters).strip())]
+ self.possible_words = [i for i in Settings.get_instance().wordlist if i.startswith("".join(self.letters).strip())]
self.selected_possible_words_index = 0
diff --git a/src/seedsigner/views/settings_tools_view.py b/src/seedsigner/views/settings_tools_view.py
index 18f33003..36cef358 100644
--- a/src/seedsigner/views/settings_tools_view.py
+++ b/src/seedsigner/views/settings_tools_view.py
@@ -1,10 +1,8 @@
# SeedSigner file class dependencies
from . import View
-from seedsigner.helpers import B, QR
+from seedsigner.helpers import B, QR, Keyboard, TextEntryDisplay
from seedsigner.models import EncodeQRDensity
-
-
class SettingsToolsView(View):
def __init__(self) -> None:
@@ -12,6 +10,7 @@ class SettingsToolsView(View):
self.qr = QR()
self.donate_image = None
+ self.derivation = None
### Donate Menu Item
@@ -64,24 +63,6 @@ class SettingsToolsView(View):
else:
return None
- ### Display Wallet Policy Selection
-
- def display_wallet_policy_selection(self) -> str:
-
- lines = ["... [ Return to Settings ]"]
- lines.append("Multi Sig Native Segwit")
- lines.append("Single Sig Native Segwit")
-
- r = self.controller.menu_view.display_generic_selection_menu(lines, "Which Wallet Policy?")
- if r == 1:
- return None
- elif lines[r-1] == "Multi Sig Native Segwit":
- return "PKWSH"
- elif lines[r-1] == "Single Sig Native Segwit":
- return "PKWPKH"
- else:
- return None
-
def display_persistent_settings(self) -> bool:
lines = ["... [ Return to Settings ]"]
@@ -98,6 +79,28 @@ class SettingsToolsView(View):
else:
return None
+
+ def display_camera_rotation(self) -> int:
+ lines = ["... [ Return to Settings ]"]
+ lines.append("0 (default)")
+ lines.append("90")
+ lines.append("180")
+ lines.append("270")
+
+ r = self.controller.menu_view.display_generic_selection_menu(lines, "Camera Rotation")
+ if r == 1:
+ return None
+ elif r == 2:
+ return 0
+ elif r == 3:
+ return 90
+ elif r == 4:
+ return 180
+ elif r == 5:
+ return 270
+ else:
+ return None
+
###
### Version Info
###
@@ -109,10 +112,165 @@ class SettingsToolsView(View):
line3 = "(Joystick RIGHT to EXIT)"
View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0)
- tw, th = View.draw.textsize(line1, font=View.IMPACT22)
- View.draw.text(((240 - tw) / 2, 20), line1, fill=View.color, font=View.IMPACT22)
- tw, th = View.draw.textsize(line2, font=View.IMPACT22)
- View.draw.text(((240 - tw) / 2, 55), line2, fill=View.color, font=View.IMPACT22)
- tw, th = View.draw.textsize(line3, font=View.IMPACT18)
- View.draw.text(((240 - tw) / 2, 210), line3, fill=View.color, font=View.IMPACT18)
+ tw, th = View.draw.textsize(line1, font=View.ASSISTANT22)
+ View.draw.text(((240 - tw) / 2, 20), line1, fill=View.color, font=View.ASSISTANT22)
+ tw, th = View.draw.textsize(line2, font=View.ASSISTANT22)
+ View.draw.text(((240 - tw) / 2, 55), line2, fill=View.color, font=View.ASSISTANT22)
+ tw, th = View.draw.textsize(line3, font=View.ASSISTANT18)
+ View.draw.text(((240 - tw) / 2, 210), line3, fill=View.color, font=View.ASSISTANT18)
View.DispShowImage()
+
+ ###
+ ### Custom Derivation Path
+ ###
+
+ def draw_derivation_keyboard_entry(self, existing_derivation = "m/"):
+ def render_right_panel():
+ row_height = 28
+ right_button_left_margin = 10
+ right_button_width = 60
+ font_padding_right = 2
+ font_padding_top = 1
+ key_x = View.canvas_width - right_button_width
+ key_y = int(View.canvas_height - row_height) / 2 - 1 - 60
+ font = View.ROBOTOCONDENSED_BOLD_24
+ background_color = "#111"
+ font_color = View.color
+ button3_text = "Save"
+ tw, th = font.getsize(button3_text)
+ key_y = int(View.canvas_height - row_height) / 2 - 1 + 60
+ View.draw.rounded_rectangle((key_x, key_y, 250, key_y + row_height), outline=View.color, fill=background_color, radius=5, width=1)
+ View.draw.text((View.canvas_width - tw - font_padding_right, key_y + font_padding_top), font=font, text=button3_text, fill=font_color)
+
+ # Clear the screen
+ View.draw.rectangle((0,0, View.canvas_width,View.canvas_height), fill="black")
+
+ self.render_previous_button()
+ previous_button_is_active = False
+
+ # Have to ensure that we don't carry any effects from a previous run
+ # TODO: This shouldn't be a member var
+ if existing_derivation:
+ self.derivation = existing_derivation
+ else:
+ self.derivation = "m/"
+
+ # Set up the keyboard params
+ right_panel_buttons_width = 60
+
+ # render top title banner
+ font = View.ROBOTOCONDENSED_REGULAR_20
+ title = "Enter Derivation"
+ title_top_padding = 0
+ title_bottom_padding = 10
+ tw, th = font.getsize(title)
+ View.draw.text((int(View.canvas_width - tw) / 2, title_top_padding), text=title, font=font, fill=View.color)
+ title_height = th + title_top_padding + title_bottom_padding
+
+ # Render the live text entry display
+ font = View.ROBOTOCONDENSED_REGULAR_28
+ tw, th = font.getsize("m/1234567890") # All possible chars for max range
+ text_entry_side_padding = 0
+ text_entry_top_padding = 1
+ text_entry_bottom_padding = 10
+ text_entry_top_y = title_height + text_entry_top_padding
+ text_entry_bottom_y = text_entry_top_y + 3 + th + 3
+ text_entry_display = TextEntryDisplay(
+ View.draw,
+ rect=(text_entry_side_padding,text_entry_top_y, View.canvas_width - right_panel_buttons_width - 1, text_entry_bottom_y),
+ font=font,
+ font_color=View.color,
+ cursor_mode=TextEntryDisplay.CURSOR_MODE__BLOCK,
+ is_centered=False,
+ has_outline=True,
+ cur_text=''.join(self.derivation)
+ )
+ text_entry_display.render()
+ cursor_position = len(self.derivation)
+
+ keyboard_start_y = text_entry_bottom_y + text_entry_bottom_padding
+ keyboard_digits = Keyboard(
+ View.draw,
+ charset="/'0123456789",
+ rows=3,
+ cols=6,
+ rect=(0, keyboard_start_y, View.canvas_width - right_panel_buttons_width, View.canvas_height),
+ auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT],
+ render_now=False
+ )
+ keyboard_digits.set_selected_key(selected_letter="/")
+ keyboard_digits.render_keys()
+ render_right_panel()
+
+ text_entry_display.render(self.derivation)
+ View.DispShowImage()
+
+ # Start the interactive update loop
+ while True:
+ input = View.buttons.wait_for(
+ [B.KEY_UP, B.KEY_DOWN, B.KEY_RIGHT, B.KEY_LEFT, B.KEY_PRESS, B.KEY3],
+ check_release=True,
+ release_keys=[B.KEY_PRESS, B.KEY3]
+ )
+
+ # Check our two possible exit conditions
+ if input == B.KEY3:
+ # Save!
+ if len(self.derivation) > 0:
+ return self.derivation.strip()
+
+ elif input == B.KEY_PRESS and previous_button_is_active:
+ # Prev button clicked; return empty string to signal cancel.
+ return ""
+
+ # Process normal input
+ if input in [B.KEY_UP, B.KEY_DOWN] and previous_button_is_active:
+ # We're navigating off the previous button
+ previous_button_is_active = False
+ self.render_previous_button(highlight=False)
+
+ # Override the actual input w/an ENTER signal for the Keyboard
+ if input == B.KEY_DOWN:
+ input = Keyboard.ENTER_TOP
+ else:
+ input = Keyboard.ENTER_BOTTOM
+ elif input in [B.KEY_LEFT, B.KEY_RIGHT] and previous_button_is_active:
+ # ignore
+ continue
+
+ ret_val = keyboard_digits.update_from_input(input)
+
+ # Now process the result from the keyboard
+ if ret_val in Keyboard.EXIT_DIRECTIONS:
+ self.render_previous_button(highlight=True)
+ previous_button_is_active = True
+
+ elif ret_val in Keyboard.ADDITIONAL_KEYS and input == B.KEY_PRESS:
+ if ret_val == Keyboard.KEY_BACKSPACE["code"]:
+ if len(self.derivation) <= 2:
+ pass
+ elif cursor_position == len(self.derivation):
+ self.derivation = self.derivation[:-1]
+ cursor_position -= 1
+ else:
+ self.derivation = self.derivation[:cursor_position - 1] + self.derivation[cursor_position:]
+ cursor_position -= 1
+
+ elif input == B.KEY_PRESS and ret_val not in Keyboard.ADDITIONAL_KEYS:
+ # User has locked in the current letter
+ if cursor_position == len(self.derivation):
+ self.derivation += ret_val
+ else:
+ self.derivation = self.derivation[:cursor_position] + ret_val + self.derivation[cursor_position:]
+ cursor_position += 1
+
+ elif input in [B.KEY_RIGHT, B.KEY_LEFT, B.KEY_UP, B.KEY_DOWN]:
+ # Live joystick movement; haven't locked this new letter in yet.
+ # Leave current spot blank for now. Only update the active keyboard keys
+ # when a selection has been locked in (KEY_PRESS) or removed ("del").
+ pass
+
+ # Render the text entry display and cursor block
+ text_entry_display.render(self.derivation)
+
+ View.DispShowImage()
\ No newline at end of file
diff --git a/src/seedsigner/views/signing_tools_view.py b/src/seedsigner/views/signing_tools_view.py
index 09c4dde0..854aa8fe 100644
--- a/src/seedsigner/views/signing_tools_view.py
+++ b/src/seedsigner/views/signing_tools_view.py
@@ -28,15 +28,15 @@ class SigningToolsView(View):
def display_transaction_information(self, p) -> None:
self.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0)
- tw, th = self.draw.textsize("Confirm Tx Details", font=View.IMPACT25)
- self.draw.text(((240 - tw) / 2, 3), "Confirm Tx Details", fill=View.color, font=View.IMPACT25)
+ tw, th = self.draw.textsize("Confirm Tx Details", font=View.ASSISTANT25)
+ self.draw.text(((240 - tw) / 2, 3), "Confirm Tx Details", fill=View.color, font=View.ASSISTANT25)
in_fee_outs_str = str(len(p.psbt.inputs))
in_fee_outs_str += " inputs - fee = " if len(p.psbt.inputs) > 1 else " input - fee = "
in_fee_outs_str += str(len(p.psbt.outputs))
in_fee_outs_str += " outs" if len(p.psbt.outputs) > 1 else " out"
- tw, th = self.draw.textsize(in_fee_outs_str, font=View.IMPACT22)
- self.draw.text(((240 - tw) / 2, 40), in_fee_outs_str, fill=View.color, font=View.IMPACT22)
+ tw, th = self.draw.textsize(in_fee_outs_str, font=View.ASSISTANT22)
+ self.draw.text(((240 - tw) / 2, 40), in_fee_outs_str, fill=View.color, font=View.ASSISTANT22)
receiving_addr_str1 = ""
receiving_addr_str2 = ""
@@ -45,45 +45,45 @@ class SigningToolsView(View):
receiving_addr_str2 += "receiving addresses"
elif len(p.destination_addresses) == 1:
receiving_addr_str1 += "receiving address"
- receiving_addr_str2 += "last 13: ..." + p.destination_addresses[0][-13:]
+ receiving_addr_str2 += "last 13: ... " + p.destination_addresses[0][-13:]
else:
receiving_addr_str1 += "Self-Transfer"
- tw, th = self.draw.textsize(receiving_addr_str1, font=View.IMPACT22)
- self.draw.text(((240 - tw) / 2, 75), receiving_addr_str1, fill=View.color, font=View.IMPACT22)
+ tw, th = self.draw.textsize(receiving_addr_str1, font=View.ASSISTANT22)
+ self.draw.text(((240 - tw) / 2, 75), receiving_addr_str1, fill=View.color, font=View.ASSISTANT22)
if len(receiving_addr_str2) > 0:
- tw, th = self.draw.textsize(receiving_addr_str2, font=View.IMPACT22)
- self.draw.text(((240 - tw) / 2, 100), receiving_addr_str2, fill=View.color, font=View.IMPACT22)
+ tw, th = self.draw.textsize(receiving_addr_str2, font=View.ROBOTOCONDENSED_BOLD_18)
+ self.draw.text(((240 - tw) / 2, 105), receiving_addr_str2, fill=View.color, font=View.ROBOTOCONDENSED_BOLD_18)
if p.spend_amount > 0:
spending_str = "Spend: " + str(p.spend_amount) + " sats"
- tw, th = self.draw.textsize(spending_str, font=View.IMPACT22)
- self.draw.text(((240 - tw) / 2, 130), spending_str, fill=View.color, font=View.IMPACT22)
+ tw, th = self.draw.textsize(spending_str, font=View.ASSISTANT22)
+ self.draw.text(((240 - tw) / 2, 130), spending_str, fill=View.color, font=View.ASSISTANT22)
if p.change_amount > 0 and len(p.destination_addresses) == 0:
change_str = "Amount: " + str(p.change_amount) + " sats"
- tw, th = self.draw.textsize(change_str, font=View.IMPACT22)
- self.draw.text(((240 - tw) / 2, 155), change_str, fill=View.color, font=View.IMPACT22)
+ tw, th = self.draw.textsize(change_str, font=View.ASSISTANT22)
+ self.draw.text(((240 - tw) / 2, 155), change_str, fill=View.color, font=View.ASSISTANT22)
elif p.change_amount > 0:
change_str = "Change: " + str(p.change_amount) + " sats"
- tw, th = self.draw.textsize(change_str, font=View.IMPACT22)
- self.draw.text(((240 - tw) / 2, 155), change_str, fill=View.color, font=View.IMPACT22)
+ tw, th = self.draw.textsize(change_str, font=View.ASSISTANT22)
+ self.draw.text(((240 - tw) / 2, 155), change_str, fill=View.color, font=View.ASSISTANT22)
fee_str = "Fee: " + str(p.fee_amount) + " sats"
- tw, th = self.draw.textsize(fee_str, font=View.IMPACT22)
- self.draw.text(((240 - tw) / 2, 180), fee_str, fill=View.color, font=View.IMPACT22)
+ tw, th = self.draw.textsize(fee_str, font=View.ASSISTANT22)
+ self.draw.text(((240 - tw) / 2, 180), fee_str, fill=View.color, font=View.ASSISTANT22)
- tw, th = self.draw.textsize("Left to Exit, Right to Continue", font=View.IMPACT18)
- self.draw.text(((240 - tw) / 2, 215), "Left to Exit, Right to Continue", fill=View.color, font=View.IMPACT18)
+ tw, th = self.draw.textsize("Left to Exit, Right to Continue", font=View.ASSISTANT18)
+ self.draw.text(((240 - tw) / 2, 215), "Left to Exit, Right to Continue", fill=View.color, font=View.ASSISTANT18)
View.DispShowImage()
def qr_gen_status(percentage):
View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0)
- tw, th = View.draw.textsize("QR Generation", font=View.IMPACT25)
- View.draw.text(((240 - tw) / 2, 90), "QR Generation", fill=View.color, font=View.IMPACT25)
- tw, th = View.draw.textsize(str(round(percentage)) + "% Complete", font=View.IMPACT25)
- View.draw.text(((240 - tw) / 2, 125), str(round(percentage)) + "% Complete", fill=View.color, font=View.IMPACT25)
+ tw, th = View.draw.textsize("QR Generation", font=View.ASSISTANT25)
+ View.draw.text(((240 - tw) / 2, 90), "QR Generation", fill=View.color, font=View.ASSISTANT25)
+ tw, th = View.draw.textsize(str(round(percentage)) + "% Complete", font=View.ASSISTANT25)
+ View.draw.text(((240 - tw) / 2, 125), str(round(percentage)) + "% Complete", fill=View.color, font=View.ASSISTANT25)
View.DispShowImage()
diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py
index f11e4f30..e4dd6694 100644
--- a/src/seedsigner/views/view.py
+++ b/src/seedsigner/views/view.py
@@ -16,25 +16,35 @@ class View:
WIDTH = 240
HEIGHT = 240
-
- # Define necessary fonts
- IMPACT16 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 16)
- IMPACT18 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 18)
- IMPACT20 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 20)
- IMPACT21 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 21)
- IMPACT22 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 22)
- IMPACT23 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 23)
- IMPACT25 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 25)
- IMPACT26 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 26)
- IMPACT35 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 35)
- IMPACT50 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 50)
- COURIERNEW14 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 14)
- COURIERNEW24 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 24)
- COURIERNEW38 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 38)
- COURIERNEW30 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 30)
- COURIERNEW20 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 20)
-
+
font_path = os.path.join(pathlib.Path(__file__).parent.resolve(), "..", "resources", "fonts")
+
+ # Define necessary fonts
+ ASSISTANT16 = ImageFont.truetype(os.path.join(font_path, 'Assistant-Medium.ttf'), 16)
+ ASSISTANT18 = ImageFont.truetype(os.path.join(font_path, 'Assistant-Medium.ttf'), 18)
+ ASSISTANT20 = ImageFont.truetype(os.path.join(font_path, 'Assistant-Medium.ttf'), 20)
+ ASSISTANT21 = ImageFont.truetype(os.path.join(font_path, 'Assistant-Medium.ttf'), 21)
+ ASSISTANT22 = ImageFont.truetype(os.path.join(font_path, 'Assistant-Medium.ttf'), 22)
+ ASSISTANT23 = ImageFont.truetype(os.path.join(font_path, 'Assistant-Medium.ttf'), 23)
+ ASSISTANT25 = ImageFont.truetype(os.path.join(font_path, 'Assistant-Medium.ttf'), 25)
+ ASSISTANT26 = ImageFont.truetype(os.path.join(font_path, 'Assistant-Medium.ttf'), 26)
+ ASSISTANT35 = ImageFont.truetype(os.path.join(font_path, 'Assistant-Medium.ttf'), 35)
+ ASSISTANT50 = ImageFont.truetype(os.path.join(font_path, 'Assistant-Medium.ttf'), 50)
+ ASSISTANT16BOLD = ImageFont.truetype(os.path.join(font_path, 'Assistant-Bold.ttf'), 15)
+ ASSISTANT18BOLD = ImageFont.truetype(os.path.join(font_path, 'Assistant-Bold.ttf'), 17)
+ ASSISTANT20BOLD = ImageFont.truetype(os.path.join(font_path, 'Assistant-Bold.ttf'), 19)
+ ASSISTANT21BOLD = ImageFont.truetype(os.path.join(font_path, 'Assistant-Bold.ttf'), 20)
+ ASSISTANT22BOLD = ImageFont.truetype(os.path.join(font_path, 'Assistant-Bold.ttf'), 21)
+ ASSISTANT23BOLD = ImageFont.truetype(os.path.join(font_path, 'Assistant-Bold.ttf'), 22)
+ ASSISTANT25BOLD = ImageFont.truetype(os.path.join(font_path, 'Assistant-Bold.ttf'), 25)
+ ASSISTANT26BOLD = ImageFont.truetype(os.path.join(font_path, 'Assistant-Bold.ttf'), 26)
+ ASSISTANT35BOLD = ImageFont.truetype(os.path.join(font_path, 'Assistant-Bold.ttf'), 35)
+ ASSISTANT50BOLD = ImageFont.truetype(os.path.join(font_path, 'Assistant-Bold.ttf'), 50)
+ ROBOTOMONO14 = ImageFont.truetype(os.path.join(font_path, 'RobotoMono-Medium.ttf'), 14)
+ ROBOTOMONO24 = ImageFont.truetype(os.path.join(font_path, 'RobotoMono-Medium.ttf'), 24)
+ ROBOTOMONO38 = ImageFont.truetype(os.path.join(font_path, 'RobotoMono-Medium.ttf'), 38)
+ ROBOTOMONO30 = ImageFont.truetype(os.path.join(font_path, 'RobotoMono-Medium.ttf'), 30)
+ ROBOTOMONO20 = ImageFont.truetype(os.path.join(font_path, 'RobotoMono-Medium.ttf'), 20)
ROBOTOCONDENSED_BOLD_16 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 16)
ROBOTOCONDENSED_BOLD_18 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 18)
@@ -67,6 +77,7 @@ class View:
disp = None
previous_button_width = None
+
def __init__(self) -> None:
# Import here to avoid circular imports
from seedsigner.controller import Controller
@@ -88,13 +99,23 @@ class View:
self.queue = Queue()
+
def DispShowImage(image=None, alpha_overlay=None):
if image == None:
image = View.canvas
+ else:
+ # Always keep a copy of the current display in the canvas
+ View.canvas.paste(image)
+
if alpha_overlay:
image = Image.alpha_composite(image, alpha_overlay)
+
+ # Always keep a copy of the current display in the canvas
+ View.canvas.paste(image)
+
View.disp.ShowImage(image, 0, 0)
+
def disp_show_image_pan(image, start_x, start_y, end_x, end_y, rate, alpha_overlay=None):
cur_x = start_x
cur_y = start_y
@@ -123,6 +144,9 @@ class View:
if alpha_overlay:
crop = Image.alpha_composite(crop, alpha_overlay)
+ # Always keep a copy of the current display in the canvas
+ View.canvas.paste(crop)
+
View.disp.ShowImage(crop, 0, 0)
@@ -134,11 +158,12 @@ class View:
text_overlay = Image.new("RGBA", (View.canvas_width, View.canvas_height), (255,255,255,0))
text_overlay_draw = ImageDraw.Draw(text_overlay)
if not font:
- font = View.COURIERNEW14
+ font = View.ASSISTANT18
tw, th = text_overlay_draw.textsize(text, font=font)
if text_background:
text_overlay_draw.rectangle(((240 - tw) / 2 - 3, 240 - th, (240 - tw) / 2 + tw + 3, 240), fill=text_background)
text_overlay_draw.text(((240 - tw) / 2, 240 - th - 1), text, fill=text_color, font=font)
+
View.DispShowImage(image_copy, alpha_overlay=text_overlay)
@@ -146,92 +171,152 @@ class View:
View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0)
if len(title) > 0:
- tw, th = View.draw.textsize(title, font=View.IMPACT22)
- View.draw.text(((240 - tw) / 2, 2), title, fill=View.color, font=View.IMPACT22)
+ tw, th = View.draw.textsize(title, font=View.ASSISTANT22)
+ View.draw.text(((240 - tw) / 2, 2), title, fill=View.color, font=View.ASSISTANT22)
if len(bottom) > 0:
- tw, th = View.draw.textsize(bottom, font=View.IMPACT18)
- View.draw.text(((240 - tw) / 2, 210), bottom, fill=View.color, font=View.IMPACT18)
+ tw, th = View.draw.textsize(bottom, font=View.ASSISTANT18)
+ View.draw.text(((240 - tw) / 2, 210), bottom, fill=View.color, font=View.ASSISTANT18)
if len(lines) == 1:
- tw, th = View.draw.textsize(lines[0], font=View.IMPACT26)
- View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.IMPACT26)
+ tw, th = View.draw.textsize(lines[0], font=View.ASSISTANT26)
+ View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.ASSISTANT26)
elif len(lines) == 2:
- tw, th = View.draw.textsize(lines[0], font=View.IMPACT22)
- View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.IMPACT22)
- tw, th = View.draw.textsize(lines[1], font=View.IMPACT22)
- View.draw.text(((240 - tw) / 2, 125), lines[1], fill=View.color, font=View.IMPACT22)
+ tw, th = View.draw.textsize(lines[0], font=View.ASSISTANT22)
+ View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.ASSISTANT22)
+ tw, th = View.draw.textsize(lines[1], font=View.ASSISTANT22)
+ View.draw.text(((240 - tw) / 2, 125), lines[1], fill=View.color, font=View.ASSISTANT22)
elif len(lines) == 3:
- tw, th = View.draw.textsize(lines[0], font=View.IMPACT26)
- View.draw.text(((240 - tw) / 2, 55), lines[0], fill=View.color, font=View.IMPACT26)
- tw, th = View.draw.textsize(lines[1], font=View.IMPACT22)
- View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.IMPACT22)
- tw, th = View.draw.textsize(lines[2], font=View.IMPACT22)
- View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.IMPACT22)
+ tw, th = View.draw.textsize(lines[0], font=View.ASSISTANT26)
+ View.draw.text(((240 - tw) / 2, 55), lines[0], fill=View.color, font=View.ASSISTANT26)
+ tw, th = View.draw.textsize(lines[1], font=View.ASSISTANT22)
+ View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.ASSISTANT22)
+ tw, th = View.draw.textsize(lines[2], font=View.ASSISTANT22)
+ View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.ASSISTANT22)
elif len(lines) == 4:
- tw, th = View.draw.textsize(lines[0], font=View.IMPACT22)
- View.draw.text(((240 - tw) / 2, 55), lines[0], fill=View.color, font=View.IMPACT22)
- tw, th = View.draw.textsize(lines[1], font=View.IMPACT22)
- View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.IMPACT22)
- tw, th = View.draw.textsize(lines[2], font=View.IMPACT22)
- View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.IMPACT22)
- tw, th = View.draw.textsize(lines[3], font=View.IMPACT22)
- View.draw.text(((240 - tw) / 2, 160), lines[3], fill=View.color, font=View.IMPACT22)
+ tw, th = View.draw.textsize(lines[0], font=View.ASSISTANT22)
+ View.draw.text(((240 - tw) / 2, 55), lines[0], fill=View.color, font=View.ASSISTANT22)
+ tw, th = View.draw.textsize(lines[1], font=View.ASSISTANT22)
+ View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.ASSISTANT22)
+ tw, th = View.draw.textsize(lines[2], font=View.ASSISTANT22)
+ View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.ASSISTANT22)
+ tw, th = View.draw.textsize(lines[3], font=View.ASSISTANT22)
+ View.draw.text(((240 - tw) / 2, 160), lines[3], fill=View.color, font=View.ASSISTANT22)
View.DispShowImage()
return
+ def draw_address(self, address) -> None:
+
+ if address.startswith(tuple(['1','2','3'])):
+ dividby = 14
+ else:
+ dividby = 18
+ address_parts = [address[i:i+dividby] for i in range(0, len(address), dividby)]
+ address_part_1 = address_parts[0]
+ address_part_2 = address_parts[1] if len(address_parts) >= 2 else ""
+ address_part_3 = address_parts[2] if len(address_parts) >= 3 else ""
+ address_part_4 = address_parts[3] if len(address_parts) >= 4 else ""
+
+ View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0)
+
+ tw, th = View.draw.textsize("Right to Continue", font=View.ASSISTANT18)
+ View.draw.text(((240 - tw) / 2, 210), "Right to Continue", fill=View.color, font=View.ASSISTANT18)
+
+ tw, th = View.draw.textsize("Bitcoin Address", font=View.ASSISTANT22)
+ View.draw.text(((240 - tw) / 2, 35), "Bitcoin Address", fill=View.color, font=View.ASSISTANT22)
+ tw, th = View.draw.textsize(address_part_1, font=View.ROBOTOCONDENSED_REGULAR_22)
+ View.draw.text(((240 - tw) / 2, 70), address_part_1, fill=View.color, font=View.ROBOTOCONDENSED_REGULAR_22)
+ tw, th = View.draw.textsize(address_part_2, font=View.ROBOTOCONDENSED_REGULAR_22)
+ View.draw.text(((240 - tw) / 2, 105), address_part_2, fill=View.color, font=View.ROBOTOCONDENSED_REGULAR_22)
+ tw, th = View.draw.textsize(address_part_3, font=View.ROBOTOCONDENSED_REGULAR_22)
+ View.draw.text(((240 - tw) / 2, 140), address_part_3, fill=View.color, font=View.ROBOTOCONDENSED_REGULAR_22)
+ tw, th = View.draw.textsize(address_part_4, font=View.ROBOTOCONDENSED_REGULAR_22)
+ View.draw.text(((240 - tw) / 2, 175), address_part_4, fill=View.color, font=View.ROBOTOCONDENSED_REGULAR_22)
+
+ View.DispShowImage()
+
+ return
+
+ def draw_passphrase(self, title, passphrase, bottom) -> None:
+ View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0)
+
+ tw, th = View.draw.textsize(bottom, font=View.ASSISTANT18)
+ View.draw.text(((240 - tw) / 2, 210), bottom, fill=View.color, font=View.ASSISTANT18)
+
+ passphrase_part_1 = passphrase[0:20]
+ passphrase_part_2 = passphrase[20:40]
+ passphrase_part_3 = passphrase[40:60]
+ passphrase_part_4 = passphrase[60:1000]
+
+ tw, th = View.draw.textsize(title, font=View.ASSISTANT26)
+ View.draw.text(((240 - tw) / 2, 30), title, fill=View.color, font=View.ASSISTANT26)
+
+ tw, th = View.draw.textsize(passphrase_part_1, font=View.ROBOTOCONDENSED_REGULAR_20)
+ View.draw.text(((240 - tw) / 2, 70), passphrase_part_1, fill=View.color, font=View.ROBOTOCONDENSED_REGULAR_20)
+ tw, th = View.draw.textsize(passphrase_part_2, font=View.ROBOTOCONDENSED_REGULAR_20)
+ View.draw.text(((240 - tw) / 2, 100), passphrase_part_2, fill=View.color, font=View.ROBOTOCONDENSED_REGULAR_20)
+ tw, th = View.draw.textsize(passphrase_part_3, font=View.ROBOTOCONDENSED_REGULAR_20)
+ View.draw.text(((240 - tw) / 2, 130), passphrase_part_3, fill=View.color, font=View.ROBOTOCONDENSED_REGULAR_20)
+ tw, th = View.draw.textsize(passphrase_part_4, font=View.ROBOTOCONDENSED_REGULAR_20)
+ View.draw.text(((240 - tw) / 2, 160), passphrase_part_4, fill=View.color, font=View.ROBOTOCONDENSED_REGULAR_20)
+
+ View.DispShowImage()
+
+ return
+
def draw_prompt_yes_no(self, lines = [], title = "", bottom = "") -> None:
self.draw_prompt_custom("", "Yes ", "No ", lines, title, bottom)
return
+
def draw_prompt_custom(self, a_txt, b_txt, c_txt, lines = [], title = "", bottom = "") -> None:
View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0)
if len(title) > 0:
- tw, th = View.draw.textsize(title, font=View.IMPACT22)
- View.draw.text(((240 - tw) / 2, 2), title, fill=View.color, font=View.IMPACT22)
+ tw, th = View.draw.textsize(title, font=View.ASSISTANT22)
+ View.draw.text(((240 - tw) / 2, 2), title, fill=View.color, font=View.ASSISTANT22)
if len(bottom) > 0:
- tw, th = View.draw.textsize(bottom, font=View.IMPACT18)
- View.draw.text(((240 - tw) / 2, 210), bottom, fill=View.color, font=View.IMPACT18)
+ tw, th = View.draw.textsize(bottom, font=View.ASSISTANT18)
+ View.draw.text(((240 - tw) / 2, 210), bottom, fill=View.color, font=View.ASSISTANT18)
if len(lines) == 1:
- tw, th = View.draw.textsize(lines[0], font=View.IMPACT26)
- View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.IMPACT26)
+ tw, th = View.draw.textsize(lines[0], font=View.ASSISTANT26)
+ View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.ASSISTANT26)
elif len(lines) == 2:
- tw, th = View.draw.textsize(lines[0], font=View.IMPACT22)
- View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.IMPACT22)
- tw, th = View.draw.textsize(lines[1], font=View.IMPACT22)
- View.draw.text(((240 - tw) / 2, 125), lines[1], fill=View.color, font=View.IMPACT22)
+ tw, th = View.draw.textsize(lines[0], font=View.ASSISTANT22)
+ View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.ASSISTANT22)
+ tw, th = View.draw.textsize(lines[1], font=View.ASSISTANT22)
+ View.draw.text(((240 - tw) / 2, 125), lines[1], fill=View.color, font=View.ASSISTANT22)
elif len(lines) == 3:
- tw, th = View.draw.textsize(lines[0], font=View.IMPACT26)
- View.draw.text(((240 - tw) / 2, 20), lines[0], fill=View.color, font=View.IMPACT26)
- tw, th = View.draw.textsize(lines[1], font=View.IMPACT22)
- View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.IMPACT22)
- tw, th = View.draw.textsize(lines[2], font=View.IMPACT22)
- View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.IMPACT22)
+ tw, th = View.draw.textsize(lines[0], font=View.ASSISTANT26)
+ View.draw.text(((240 - tw) / 2, 20), lines[0], fill=View.color, font=View.ASSISTANT26)
+ tw, th = View.draw.textsize(lines[1], font=View.ASSISTANT22)
+ View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.ASSISTANT22)
+ tw, th = View.draw.textsize(lines[2], font=View.ASSISTANT22)
+ View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.ASSISTANT22)
elif len(lines) == 4:
- tw, th = View.draw.textsize(lines[0], font=View.IMPACT22)
- View.draw.text(((240 - tw) / 2, 20), lines[0], fill=View.color, font=View.IMPACT22)
- tw, th = View.draw.textsize(lines[1], font=View.IMPACT22)
- View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.IMPACT22)
- tw, th = View.draw.textsize(lines[2], font=View.IMPACT22)
- View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.IMPACT22)
- tw, th = View.draw.textsize(lines[3], font=View.IMPACT22)
- View.draw.text(((240 - tw) / 2, 160), lines[3], fill=View.color, font=View.IMPACT22)
+ tw, th = View.draw.textsize(lines[0], font=View.ASSISTANT22)
+ View.draw.text(((240 - tw) / 2, 20), lines[0], fill=View.color, font=View.ASSISTANT22)
+ tw, th = View.draw.textsize(lines[1], font=View.ASSISTANT22)
+ View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.ASSISTANT22)
+ tw, th = View.draw.textsize(lines[2], font=View.ASSISTANT22)
+ View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.ASSISTANT22)
+ tw, th = View.draw.textsize(lines[3], font=View.ASSISTANT22)
+ View.draw.text(((240 - tw) / 2, 160), lines[3], fill=View.color, font=View.ASSISTANT22)
- a_x_offset = 240 - View.IMPACT25.getsize(a_txt)[0]
- View.draw.text((a_x_offset, 39 + 0), a_txt, fill=View.color, font=View.IMPACT25)
+ a_x_offset = 240 - View.ASSISTANT25.getsize(a_txt)[0]
+ View.draw.text((a_x_offset, 39 + 0), a_txt, fill=View.color, font=View.ASSISTANT25)
- b_x_offset = 240 - View.IMPACT25.getsize(b_txt)[0]
- View.draw.text((b_x_offset , 39 + 60), b_txt, fill=View.color, font=View.IMPACT25)
+ b_x_offset = 240 - View.ASSISTANT25.getsize(b_txt)[0]
+ View.draw.text((b_x_offset , 39 + 60), b_txt, fill=View.color, font=View.ASSISTANT25)
- c_x_offset = 240 - View.IMPACT25.getsize(c_txt)[0]
- View.draw.text((c_x_offset , 39 + 120), c_txt, fill=View.color, font=View.IMPACT25)
+ c_x_offset = 240 - View.ASSISTANT25.getsize(c_txt)[0]
+ View.draw.text((c_x_offset , 39 + 120), c_txt, fill=View.color, font=View.ASSISTANT25)
View.DispShowImage()
@@ -251,14 +336,14 @@ class View:
line3 = "30 seconds before"
line4 = "disconnecting power."
- tw, th = View.draw.textsize(line1, font=View.IMPACT22)
- View.draw.text(((240-tw)/2, 45), line1, fill=View.color, font=View.IMPACT22)
- tw, th = View.draw.textsize(line2, font=View.IMPACT20)
- View.draw.text(((240-tw)/2, 100), line2, fill=View.color, font=View.IMPACT20)
- tw, th = View.draw.textsize(line3, font=View.IMPACT20)
- View.draw.text(((240-tw)/2, 130), line3, fill=View.color, font=View.IMPACT20)
- tw, th = View.draw.textsize(line4, font=View.IMPACT20)
- View.draw.text(((240-tw)/2, 160), line4, fill=View.color, font=View.IMPACT20)
+ tw, th = View.draw.textsize(line1, font=View.ASSISTANT22)
+ View.draw.text(((240-tw)/2, 45), line1, fill=View.color, font=View.ASSISTANT22)
+ tw, th = View.draw.textsize(line2, font=View.ASSISTANT20)
+ View.draw.text(((240-tw)/2, 100), line2, fill=View.color, font=View.ASSISTANT20)
+ tw, th = View.draw.textsize(line3, font=View.ASSISTANT20)
+ View.draw.text(((240-tw)/2, 130), line3, fill=View.color, font=View.ASSISTANT20)
+ tw, th = View.draw.textsize(line4, font=View.ASSISTANT20)
+ View.draw.text(((240-tw)/2, 160), line4, fill=View.color, font=View.ASSISTANT20)
View.DispShowImage()
diff --git a/src/settings.ini b/src/settings.ini
index 48d3e892..2aeffa0d 100644
--- a/src/settings.ini
+++ b/src/settings.ini
@@ -5,11 +5,11 @@ persistent_settings = False
[display]
text_color = ORANGE
+qr_background_color = FFFFFF
+camera_rotation = 0
[wallet]
network = main
software = Prompt
qr_density = 2
-script_policy = PKWSH
-custom_derivation_enabled = False
custom_derivation = m/0/0
\ No newline at end of file
diff --git a/tests/test_controller.py b/tests/test_controller.py
index efafa713..96757eff 100644
--- a/tests/test_controller.py
+++ b/tests/test_controller.py
@@ -6,37 +6,43 @@ from seedsigner.controller import Controller
def test_singleton_init_fails():
- """ The Controller should not allow any code to instantiate it via Controller() """
- with pytest.raises(Exception):
- c = Controller()
+ """ The Controller should not allow any code to instantiate it via Controller() """
+ with pytest.raises(Exception):
+ c = Controller()
def test_singleton_get_instance_without_configure_fails():
- """ Calling get_instance() without first calling configure_instance() should fail """
- with pytest.raises(Exception):
- c = Controller.get_instance()
+ """ Calling get_instance() without first calling configure_instance() should fail """
+ with pytest.raises(Exception):
+ c = Controller.get_instance()
def test_singleton_get_instance_preserves_state():
- """ Changes to the Controller singleton should be preserved across calls to get_instance() """
- settings = """
- [system]
- DEBUG = True
- DEFAULT_LANGUAGE = en
+ """ Changes to the Controller singleton should be preserved across calls to get_instance() """
+ settings = """
+ [system]
+ debug = False
+ default_language = en
+ persistent_settings = False
- [display]
- BACKGROUND_COLOR = #000
- TEXT_COLOR = orange
- """
- config = configparser.ConfigParser()
- config.read_string(settings)
+ [display]
+ text_color = ORANGE
- # Initialize the instance and verify that it read the config settings
- Controller.configure_instance(config)
- controller = Controller.get_instance()
- assert controller.color == "orange"
+ [wallet]
+ network = main
+ software = Prompt
+ qr_density = 2
+ custom_derivation = m/0/0
+ """
+ config = configparser.ConfigParser()
+ config.read_string(settings)
- # Change a value in the instance...
- controller.color = "purple"
+ # Initialize the instance and verify that it read the config settings
+ Controller.configure_instance(config)
+ controller = Controller.get_instance()
+ assert controller.color == "ORANGE"
- # ...get a new copy of the instance and confirm change
- controller = Controller.get_instance()
- assert controller.color == "purple"
+ # Change a value in the instance...
+ controller.color = "purple"
+
+ # ...get a new copy of the instance and confirm change
+ controller = Controller.get_instance()
+ assert controller.color == "purple"
diff --git a/tests/test_decodepsbtqr.py b/tests/test_decodepsbtqr.py
index 45290756..270ae0f7 100644
--- a/tests/test_decodepsbtqr.py
+++ b/tests/test_decodepsbtqr.py
@@ -1,8 +1,7 @@
import pytest
from mock import MagicMock
-from seedsigner.models.decode_qr import DecodeQR, QRType, DecodeQRStatus
-from seedsigner.models.psbt_parser import PSBTParser
-from embit import psbt
+from seedsigner.models import Seed, DecodeQR, DecodeQRStatus, QRType, EncodeQRDensity, PSBTParser
+from embit import psbt, bip39
# this is an of this bug: https://github.com/Foundation-Devices/foundation-ur-py/issues/3
@@ -40,7 +39,7 @@ def test_ur_qr_decode_multisig():
, "UR:CRYPTO-PSBT/26-29/LPCSCYCSCACFCMCPCYBBRSPTSKHDSSKSKGDYBYDMPEYTWFDIROBZCHGWCEFMFECKZEDYAEAELAAEAEAELAAEAEAELAAOAEAELAADAEAEAEAEAEAEAECPAOAXREAMJNKKZCESYKSWJOJPTBRDEOKGBSHHDTIYPSBEPSVWKSYKJPCPDKUYIMPEOSFECEQZEOVTMDDYAEAELAAEAEAELAAEAEAELAAOAEAELAADAEAEAEAEAEAEAECPAOAXWTSOSKSKFNWYBYLBNSHTEEEEECTORFLRBYCWROTKVTEMBDPDQDTEVALECAWNWDVYCELPDNDYMYDYAEAELAAEAEAELAAEAEAELAAOAEAELAADAEAEAEAEAEAEAEAEADADTKGHCLAOOEWLYATYBAOYLKOLJESGHTGYJLCLRHSGLFKPBDCLSEHYMT"
]
- d = DecodeQR()
+ d = DecodeQR(wordlist=bip39.WORDLIST)
for i in qrcodes:
d.addString(i)
assert d.qrType() == QRType.PSBTUR2
@@ -62,7 +61,7 @@ def test_base64_single_frame_singlsig():
"cHNidP8BAHICAAAAAQDo5ey+2HIrNUkExsFhsImv1OK1cYA9x/bRjYQD+0UaAQAAAAD9////Apg6AAAAAAAAF6kUVuVZEcdpQ2zgABa9dRUNYHD4VuaHgSYAAAAAAAAWABQaLE4t0JbDRg4pNnmcf+cAWIcyawAAAAAAAQEfqGEAAAAAAAAWABRyuw9od6yuS0yiZljV0X12wG9e5CIGA/ZlEZvQubb6PmcnK+vlnd8aftYnrQ8wHYSxsD8tDp61GIshjoFUAACAAQAAgAAAAIAAAAAAAAAAAAAAAA=="
]
- d = DecodeQR()
+ d = DecodeQR(wordlist=bip39.WORDLIST)
for i in qrcodes:
d.addString(i)
assert d.qrType() == QRType.PSBTBASE64
@@ -80,7 +79,13 @@ def test_base64_single_frame_singlsig():
mnemonic = "height demise useless trap grow lion found off key clown transfer enroll"
pw = ""
- pp = PSBTParser(tx,mnemonic.split(" "),pw,"test")
+ seed = Seed(mnemonic, passphrase=pw, wordlist=bip39.WORDLIST)
+
+ assert seed.mnemonic == mnemonic
+
+ assert seed.seed != None
+
+ pp = PSBTParser(tx,seed,"test")
assert tx.inputs[0].witness_utxo.value == 25000 # input amount in psbt
@@ -101,7 +106,7 @@ def test_ur2_sparrow_singlesig_to_self():
, "UR:CRYPTO-PSBT/443-3/LPCFADRKAXCFAOOECYMSTBHDCSHDVYAXADCTPDIARTHKYLIDWSLTAASGSARSUECLSANEPDFZEHDLRNWSGLNNHTYTBNGRYLVYWDCPCLTDAEAEAEAEADDKAXTBCHOSVEHPAHTESNYAUYOYCKRNDRTBGWIHIYTLWFUERDNYWTMSLSROHKRPKELPLONDKOUEJSKBKGIHKNDNAYHLWENSMEGSSSHNAEEEFZSRNSKEGSVYWNEEAATAPYIMPASNHPFWMETEAYBYHFRSBBZCDSRDTTLNADLBEMFTTSSOAHENPFNTVSVEBDHNDTWYKBBDEYTSLPFRJSLBBTVADPDTBEDAKIZEZMZMAOPKPEAOAEAECPAOAXWKGRNLJEVSZMWSIDSOTTEHHETYFTFXCASBMKGTURBWJLIMTLLUFWSTSSKIZMLAFXBALUECZTFTHPISKTDWPEGRGSCPIYHDTLGYKIKORTJLHLVEMNKPCTAERKWPCMJN"
]
- d = DecodeQR()
+ d = DecodeQR(wordlist=bip39.WORDLIST)
for i in qrcodes:
d.addString(i)
assert d.qrType() == QRType.PSBTUR2
@@ -121,7 +126,7 @@ def test_base64_2_input_p2wsh():
base64_psbt = "cHNidP8BALICAAAAAq1DhxRK+mUH4T6uUNob8bUaZ7MP+44MW4+Y9bOxpjhZAAAAAAD9////aWclWQ+45HKrI07r878E2UrAupT2paT4QurbmtNjYNQBAAAAAP3///8CQEIPAAAAAAAiACCpkDPDhmIzPlkJrjw9A71xjbIUWf3VUB7ooFJhTVm04tjSIQEAAAAAIgAgjQKFDauIXsV5u23LBdYgOwX1FwGGrLiQfWzBtFKZ7dIAAAAATwEENYfPBD5i336AAAACQStJhNVJul7vHKbo83VdmuAW2m0WaXLKDlFANn7dUNoCNbhLMdw4Knz7Q7o6exdL6UFhQegW9nJb0SUStbLEpawUAgjLdzAAAIABAACAAAAAgAIAAIBPAQQ1h88EnbHQAIAAAAI/2Nc7x7iMpJNapTe/OJTV4oifqzQcYY9KV2+PGRjCdQJoww1WnSNqfcxXGyux0q1PqfmzUqgJNqKJCpmqI9t47BQmu4PEMAAAgAEAAIAAAACAAgAAgE8BBDWHzwS6wUg5gAAAAh1Pvr3ZZ+GvcUwJl9OPz2cLXOnTAcBEC7zDtqIOt3IcA1aOofNgUZFu0baQw54SqOcGA7KAvTDOXygfKRilU2OqFHPF2gowAACAAQAAgAAAAIACAACAAAEBK4CWmAAAAAAAIgAgiYAxcG7dnrEiZ4VHFVHOo18XCalvhZYuMqBr9n7HESQBBWlSIQJOjQgMfX26XEf+trHIEk3rYkEX5Y2NfrFKQARPcd2X8iEDBWHUgq25PfHvE+hlcBryJG7wo2y8jKUSPY7sd85OOMchA2iVcuKLD+2p1pgcAjfZ5d7b/sFt5xQ/aAoC7V0Vn3WHU64iBgJOjQgMfX26XEf+trHIEk3rYkEX5Y2NfrFKQARPcd2X8hwmu4PEMAAAgAEAAIAAAACAAgAAgAAAAAABAAAAIgYDBWHUgq25PfHvE+hlcBryJG7wo2y8jKUSPY7sd85OOMccAgjLdzAAAIABAACAAAAAgAIAAIAAAAAAAQAAACIGA2iVcuKLD+2p1pgcAjfZ5d7b/sFt5xQ/aAoC7V0Vn3WHHHPF2gowAACAAQAAgAAAAIACAACAAAAAAAEAAAAAAQErgJaYAAAAAAAiACAzd60wM9EFnPHSNbsSJfyipL8myVLVP2/vwzotVUSNxQEFaVIhAiKCMRLlzIhLkRbLIUIMx5KYJM0v6LcjW/mS6K7eFGwiIQKDzUflU23LeecRgzDo5IBCEvaWGfHW7JkNxzXvuc7FdCEDC5DtLoa61/Kk/pdpu0F9e6nKoRJIB9v7Ni377rZefgFTriIGAiKCMRLlzIhLkRbLIUIMx5KYJM0v6LcjW/mS6K7eFGwiHAIIy3cwAACAAQAAgAAAAIACAACAAAAAAAAAAAAiBgKDzUflU23LeecRgzDo5IBCEvaWGfHW7JkNxzXvuc7FdBwmu4PEMAAAgAEAAIAAAACAAgAAgAAAAAAAAAAAIgYDC5DtLoa61/Kk/pdpu0F9e6nKoRJIB9v7Ni377rZefgEcc8XaCjAAAIABAACAAAAAgAIAAIAAAAAAAAAAAAABAWlSIQKtIdmtKKuZrH7f2R4iIU8RWVOrCdHVWBCS+0e9pZJy/iEDoH074LrWPIA10hyXtBCJDT06GdLkA6+z/PxoJqomPHYhA6GoQ/otQdk71nUpYZFfbkSKdBkkSj4CuPTPYrzGp6JrU64iAgKtIdmtKKuZrH7f2R4iIU8RWVOrCdHVWBCS+0e9pZJy/hwCCMt3MAAAgAEAAIAAAACAAgAAgAEAAAAAAAAAIgIDoH074LrWPIA10hyXtBCJDT06GdLkA6+z/PxoJqomPHYcc8XaCjAAAIABAACAAAAAgAIAAIABAAAAAAAAACICA6GoQ/otQdk71nUpYZFfbkSKdBkkSj4CuPTPYrzGp6JrHCa7g8QwAACAAQAAgAAAAIACAACAAQAAAAAAAAAAAA=="
- d = DecodeQR()
+ d = DecodeQR(wordlist=bip39.WORDLIST)
d.addString(base64_psbt)
assert d.qrType() == QRType.PSBTBASE64
@@ -130,7 +135,7 @@ def test_base64_2_input_p2wsh():
assert str(tx) == base64_psbt
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
- pp = PSBTParser(tx,mnemonic.split(" "),"","test")
+ pp = PSBTParser(tx,Seed(mnemonic, wordlist=bip39.WORDLIST),"test")
assert tx.inputs[0].witness_utxo.value == 10000000 # input amount 1 in psbt
@@ -148,7 +153,7 @@ def test_base64_1_input_p2sh_p2wsh():
base64_psbt = "cHNidP8BAHICAAAAAR30J629i3Y/R8woRpLQ9JUa31rKxyM+Ny4NEsme48GWAAAAAAD9////Atw5XQUAAAAAF6kUdSESczdYagEyToVUSXyT8VTNz+OHgJaYAAAAAAAWABTmav7/w4OOcfCiewfjsA7eaujhYAAAAABPAQQ1h88EPmLffoAAAAHdXEj2dn8EYJ+rRdXEYu5laq6lJI5Mp+3t63ckwty05QKrJBNPewhwQaGPYRif6+XaxozFXvTXn7pU24H6fRy1FxQCCMt3MAAAgAEAAIAAAACAAQAAgE8BBDWHzwSdsdAAgAAAAeeOv56oeaaFTrNonMKDHk1C8brbWGFvdlecVue+v0/RAn/g4yI3oYsyen7OOcT7caYl4Mn7nQbyonHcusUR+GhzFCa7g8QwAACAAQAAgAAAAIABAACATwEENYfPBLrBSDmAAAABpzrb4oeEh2NNy/w/fr3osfyZTx7AaGDPAcP+LqeR3bYC5ioqmXPuazp69HwimlvecLylm9BLuyl/VpPXqiVrl20Uc8XaCjAAAIABAACAAAAAgAEAAIAAAQEgAOH1BQAAAAAXqRSv3gkn8731qcPbSDu4TJOlJJZ/PocBBCIAIOeiFBX5x0vX6CacrAUVovrs1DDCcKJS5qptFS3sjpDpAQVpUiECZ+pFYkOTVjB+eG+vQFA3MNjZWiA6DjRcs1Wl36A/zgMhA2Ygckuwjah29wiVRgA6wFx51+6ayrzeCIQ2eE4zfxPtIQOnUlBn22cn2CPCZkMSI6cDaZK2SlLV20rT6pqMoQCJsFOuIgYCZ+pFYkOTVjB+eG+vQFA3MNjZWiA6DjRcs1Wl36A/zgMcc8XaCjAAAIABAACAAAAAgAEAAIAAAAAAAAAAACIGA2Ygckuwjah29wiVRgA6wFx51+6ayrzeCIQ2eE4zfxPtHCa7g8QwAACAAQAAgAAAAIABAACAAAAAAAAAAAAiBgOnUlBn22cn2CPCZkMSI6cDaZK2SlLV20rT6pqMoQCJsBwCCMt3MAAAgAEAAIAAAACAAQAAgAAAAAAAAAAAAAEAIgAgZBwTq05RkpqKv6FV6LQjuM07Qv0/bYfWVc9NUQOFvwQBAWlSIQI32jVSdTgeu7+YZKrWfgOZ2J/LV36c5rBoApTzhrNlDCEC+hEqm3XmRt862AFFeyJ7p1m8A+V7czj6OajUNgCfg4EhA/MUVfxGh4k3Po3LB8CmMRsvIHcGNO0elUgETaITZA3UU64iAgI32jVSdTgeu7+YZKrWfgOZ2J/LV36c5rBoApTzhrNlDBwCCMt3MAAAgAEAAIAAAACAAQAAgAEAAAAAAAAAIgIC+hEqm3XmRt862AFFeyJ7p1m8A+V7czj6OajUNgCfg4EcJruDxDAAAIABAACAAAAAgAEAAIABAAAAAAAAACICA/MUVfxGh4k3Po3LB8CmMRsvIHcGNO0elUgETaITZA3UHHPF2gowAACAAQAAgAAAAIABAACAAQAAAAAAAAAAAA=="
- d = DecodeQR()
+ d = DecodeQR(wordlist=bip39.WORDLIST)
d.addString(base64_psbt)
assert d.qrType() == QRType.PSBTBASE64
@@ -157,7 +162,7 @@ def test_base64_1_input_p2sh_p2wsh():
assert str(tx) == base64_psbt
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
- pp = PSBTParser(tx,mnemonic.split(" "),"","test")
+ pp = PSBTParser(tx,Seed(mnemonic, wordlist=bip39.WORDLIST),"test")
assert tx.inputs[0].witness_utxo.value == 100000000 # input amount 1 in psbt
@@ -201,7 +206,7 @@ def test_ur_legacy():
, "UR:BYTES/8OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/E0DKKUXEJDPX589HZPQ6SVQQQZQQZQQQSQQQQQYQQGQQPQQPQQQQQPQQQQQZYQSZ56D86Z6EZJEJALSG9KYCEJD4ES9TQR59AR7VDQQ9FKL44LSKUNL3E38ZAMFNQQQQSQQSQQYQQQQQPQQZQQQGQQGQQQQQGQQQQQQQ0P6SEH"
]
- d = DecodeQR()
+ d = DecodeQR(wordlist=bip39.WORDLIST)
for i in qrcodes:
if d.addString(i) == DecodeQRStatus.COMPLETE:
break
@@ -213,7 +218,7 @@ def test_ur_legacy():
tx = d.getPSBT()
mnemonic = "zone zone zone zone zone abandon ability able abandon ability able abstract"
- pp = PSBTParser(tx,mnemonic.split(" "),"","test")
+ pp = PSBTParser(tx,Seed(mnemonic, wordlist=bip39.WORDLIST),"test")
assert pp.input_amount == 200000
@@ -235,7 +240,7 @@ def test_specter_multisig_animated_qr():
, "p1of5 cHNidP8BAKQCAAAAA6DLc9RAdKwWQb/7Nrq1FyAtDQ3e0w5E4LkLpcrwBL0aAAAAAAD9////78NG2vqjywKaa0QHfo6C44o1+odXeWZ43FfHy4bLdxABAAAAAP3///8rhueJha90HzgJ6alnJ1uvi0Zbq0JQoXnwDb0Cjn79DwAAAAAA/f///wHWCBAAAAAAABYAFDraKFKubXzlSvRQy8V4X+TwKhLXAAAAAE8BBDWHzwQJeplqgAAAAqmHnnoRR+XJd7gJt/PhLazKeWAfCBLJ4/OF/s0PqqF1AzK8fctt"
]
- d = DecodeQR()
+ d = DecodeQR(wordlist=bip39.WORDLIST)
for i in qrcodes:
if d.addString(i) == DecodeQRStatus.COMPLETE:
break
@@ -247,7 +252,7 @@ def test_specter_multisig_animated_qr():
tx = d.getPSBT()
mnemonic = "zone zone zone zone zone abandon ability able abandon ability able abstract"
- pp = PSBTParser(tx,mnemonic.split(" "),"","test")
+ pp = PSBTParser(tx,Seed(mnemonic, wordlist=bip39.WORDLIST),"test")
assert pp.input_amount == 1052818
@@ -273,7 +278,7 @@ def test_specter_multisig_animated_qr():
, "p2of5 OUsU2k9a6Valj6kLb/Bh2gB7YuxenNz7LY5PFJkKc60wAACAAQAAgAAAAIACAACATwEENYfPBEgFouKAAAACOeERUyCmIJuSdQNDB1mxkahw4tc2UCtjxJQoQGnRCtQCZUSXwQF+nA8dl/RG6U1b9ViWJxkzNrDCCagYEWch72cUrnhJ4jAAAIABAACAAAAAgAIAAIAAAQErckUPAAAAAAAiACDwHjagvyTvVDVn+RqxW0xaMLODBm2IVCwDBQm5uK8rDQEFR1IhAi/hk/Tz0aHcNwsVyv86bqQj30sUexPl"
]
- d2 = DecodeQR()
+ d2 = DecodeQR(wordlist=bip39.WORDLIST)
for i in qrcodes2:
if d2.addString(i) == DecodeQRStatus.COMPLETE:
break
@@ -285,7 +290,7 @@ def test_specter_multisig_animated_qr():
tx2 = d2.getPSBT()
mnemonic2 = "able bacon cable able bacon cable abandon abandon abandon abandon abandon access"
- pp2 = PSBTParser(tx2,mnemonic2.split(" "),"","test")
+ pp2 = PSBTParser(tx2,Seed(mnemonic2, wordlist=bip39.WORDLIST),"test")
assert pp2.input_amount == 1052818
@@ -306,30 +311,133 @@ def test_specter_multisig_animated_qr():
def test_mnemonic_qr():
mnemonic = "height demise useless trap grow lion found off key clown transfer enroll"
- d = DecodeQR()
+ d = DecodeQR(wordlist=bip39.WORDLIST)
assert d.addString(mnemonic) == DecodeQRStatus.COMPLETE
assert d.getSeedPhrase() == mnemonic.split()
bad_mnemonic = "height useless trap grow lion found off key clown transfer enroll"
- d2 = DecodeQR()
+ d2 = DecodeQR(wordlist=bip39.WORDLIST)
assert d2.addString(bad_mnemonic) == DecodeQRStatus.INVALID
bad2_mnemonic = "words not in the list of bip39"
- d3 = DecodeQR()
+ d3 = DecodeQR(wordlist=bip39.WORDLIST)
assert d3.addString(bad2_mnemonic) == DecodeQRStatus.INVALID
def test_short_4_letter_mnemonic_qr():
short_nm = "heig demi usel trap grow lion foun off key clow tran enro"
- d = DecodeQR()
+ d = DecodeQR(wordlist=bip39.WORDLIST)
d.addString(short_nm)
assert d.isComplete() == True
assert d.getSeedPhrase() == ["height", "demise", "useless", "trap", "grow", "lion", "found", "off", "key", "clown", "transfer", "enroll"]
+def test_bitcoin_address():
+
+ bad1 = "loremipsum"
+ bad2 = "0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae"
+ bad3 = "121802020768124106400009195602431595117715840445"
+
+ legacy_address1 = "1KFHE7w8BhaENAswwryaoccDb6qcT6DbYY"
+ legacy_address2 = "16ftSEQ4ctQFDtVZiUBusQUjRrGhM3JYwe"
+
+ main_bech32_address = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
+ test_bech32_address = "tb1qkurj377gtlmu0j5flcykcsh2xagexh9h3jk06a"
+
+ main_nested_segwit_address = "3Nu78Cqcf6hsD4sUBAN9nP13tYiHU9QPFX"
+ test_nested_segwit_address = "2N6JbrvPMMwbBhu2KxqXyyHUQz3XKspvyfm"
+
+ main_bech32_address2 = "bitcoin:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?amount=12000"
+ main_bech32_address3 = "BITCOIN:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?junk"
+
+ d = DecodeQR(wordlist=bip39.WORDLIST)
+ d.addString(bad1)
+
+ assert d.qrType() == QRType.INVALID
+
+ d = DecodeQR(wordlist=bip39.WORDLIST)
+ d.addString(legacy_address1)
+
+ assert d.getAddress() == legacy_address1
+ assert d.getAddressType() == "P2PKH-main"
+
+ d = DecodeQR(wordlist=bip39.WORDLIST)
+ d.addString(legacy_address2)
+
+ assert d.getAddress() == legacy_address2
+ assert d.getAddressType() == "P2PKH-main"
+
+ d = DecodeQR(wordlist=bip39.WORDLIST)
+ d.addString(main_bech32_address)
+
+ assert d.getAddress() == main_bech32_address
+ assert d.getAddressType() == "Bech32-main"
+
+ d = DecodeQR(wordlist=bip39.WORDLIST)
+ d.addString(test_bech32_address)
+
+ assert d.getAddress() == test_bech32_address
+ assert d.getAddressType() == "Bech32-test"
+
+ d = DecodeQR(wordlist=bip39.WORDLIST)
+ d.addString(main_nested_segwit_address)
+
+ assert d.getAddress() == main_nested_segwit_address
+ assert d.getAddressType() == "P2SH-main"
+
+ d = DecodeQR(wordlist=bip39.WORDLIST)
+ d.addString(test_nested_segwit_address)
+
+ assert d.getAddress() == test_nested_segwit_address
+ assert d.getAddressType() == "P2SH-test"
+
+ d = DecodeQR(wordlist=bip39.WORDLIST)
+ d.addString(main_bech32_address2)
+
+ assert d.getAddress() == "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
+ assert d.getAddressType() == "Bech32-main"
+
+ d = DecodeQR(wordlist=bip39.WORDLIST)
+ d.addString(main_bech32_address3)
+
+ assert d.getAddress() == "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
+ assert d.getAddressType() == "Bech32-main"
-
-
-
+def test_seed_qr():
+ seed = "121802020768124106400009195602431595117715840445"
+
+ d = DecodeQR(wordlist=bip39.WORDLIST)
+ d.addString(seed)
+
+ assert d.qrType() == QRType.SEEDSSQR
+ assert d.getSeedPhrase() == "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash".split()
+
+def test_specter_wallet_json():
+
+ parts = [
+ 'p1of3 {"label": "SeedSigner Dev Funds", "blockheight": 692143, "descriptor": "wsh(sortedmulti(4,[e0811b6b/48h/0h/0h/2h]xpub6E8v7uy63pCeJvHe5W8ea8zTnCtKMFgMRb5bueWWcUFMw6sWmUwTqxM8cFiKQRWkA2Fxth9HJZufJwjWTTvU1UGZNpTrh9khrswYMgeHiCt/0/*,[852b308f/48h/0h/0h/2h]xpub6ErhgAWfnEqW7xDBm1iLq5JjNyUS65YUFnjHLrRv9zmdDEtuE75bpWQ8o6bSBnpT6AkrrsA8eA5SmEFArZn11KEPaZJzx9mHTXPWZCsxLyh/0/*,[7edf9c59/48h/0h/0h/2h]xpub6DaFfKoe7Wpofr'
+ , 'p2of3 bYeNo3Wv2AiLUMeyrPwotXfukFxUHbK4JxaLHTd5394QtH5wnjFzBgr2YnJpHhXv25Zsqv2APmMFvH1DsKHj5LCr3pmXs/0/*,[b433e095/48h/0h/0h/2h]xpub6EF51itHko2YhGTjVeuYbBgJjVbTzzpYzn2a3JwZHpDrMePRVgXGBHMx2Yv1KwgLsUn9i7ExcAo8uqMx4pDjVRY9J7qnceFAwRRj16dd5AS/0/*,[184d07eb/48h/0h/0h/2h]xpub6EEoTpcQu7N4R8D84pJjZ69j3mi'
+ , 'p3of3 nevnYLDDoo2HBzYBXTQ4rGVf4XGTyCYFwJuZdsF9MyFYJNzYEjg5LGMA1ubTGWuDnjHAZz6ficVRDTSy/0/*,[3e451efe/48h/0h/0h/2h]xpub6ExQPvQxGBMaPxr8Fv7Vq91ztJFFX3VWvtpvex6UPZ1AptTeuAiJGCtKkgwJkrwpMZMagh9ex6rL4sM8axfFcdQbERoFCRUKTJxrBkJh56g/0/*))#c44hel9e", "devices": [{"type": "other", "label": "Keith"}, {"type": "other", "label": "Nick"}, {"type": "other", "label": "Richard"}, {"type": "other", "label": "Stephan"}, {"type": "other", "label": "SeedSigner 1"}, {"type": "other", "label": "SeedSigner 2"}]}'
+ ]
+
+ d = DecodeQR(wordlist=bip39.WORDLIST)
+ d.addString(parts[1])
+ d.addString(parts[2])
+ d.addString(parts[0])
+
+ assert d.qrType() == QRType.SPECTERWALLETQR
+ assert d.isComplete() == True
+ assert d.getWalletDescriptor() == "wsh(sortedmulti(4,[e0811b6b/48h/0h/0h/2h]xpub6E8v7uy63pCeJvHe5W8ea8zTnCtKMFgMRb5bueWWcUFMw6sWmUwTqxM8cFiKQRWkA2Fxth9HJZufJwjWTTvU1UGZNpTrh9khrswYMgeHiCt/0/*,[852b308f/48h/0h/0h/2h]xpub6ErhgAWfnEqW7xDBm1iLq5JjNyUS65YUFnjHLrRv9zmdDEtuE75bpWQ8o6bSBnpT6AkrrsA8eA5SmEFArZn11KEPaZJzx9mHTXPWZCsxLyh/0/*,[7edf9c59/48h/0h/0h/2h]xpub6DaFfKoe7WpofrbYeNo3Wv2AiLUMeyrPwotXfukFxUHbK4JxaLHTd5394QtH5wnjFzBgr2YnJpHhXv25Zsqv2APmMFvH1DsKHj5LCr3pmXs/0/*,[b433e095/48h/0h/0h/2h]xpub6EF51itHko2YhGTjVeuYbBgJjVbTzzpYzn2a3JwZHpDrMePRVgXGBHMx2Yv1KwgLsUn9i7ExcAo8uqMx4pDjVRY9J7qnceFAwRRj16dd5AS/0/*,[184d07eb/48h/0h/0h/2h]xpub6EEoTpcQu7N4R8D84pJjZ69j3minevnYLDDoo2HBzYBXTQ4rGVf4XGTyCYFwJuZdsF9MyFYJNzYEjg5LGMA1ubTGWuDnjHAZz6ficVRDTSy/0/*,[3e451efe/48h/0h/0h/2h]xpub6ExQPvQxGBMaPxr8Fv7Vq91ztJFFX3VWvtpvex6UPZ1AptTeuAiJGCtKkgwJkrwpMZMagh9ex6rL4sM8axfFcdQbERoFCRUKTJxrBkJh56g/0/*))#c44hel9e"
+
+def test_specter_wallet_json2():
+
+ 1_part = '{"label": "Testnet Single Zone", "blockheight": 2090512, "descriptor": "wpkh([990a73ad/84h/1h/0h]tpubDDHQMDnFdan2GyHBsG32VW9qiygbhVizGRTjiS3H79M49FSvpsvLXqLgp1yC7r43dXVHozWavi2Fc4WHUpZmQYmzoQbit28qJhLjScbAQWU/0/*)#ujr0xunp","devices": [{"type": "seedsigner", "label": "Single Seed Zone Testnet"}]}'
+
+ d = DecodeQR(wordlist=bip39.WORDLIST)
+ d.addString(1_part)
+
+ assert d.qrType() == QRType.SPECTERWALLETQR
+ assert d.isComplete() == True
+
diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py
index b38239d1..c5b4048b 100644
--- a/tests/test_encodepsbtqr.py
+++ b/tests/test_encodepsbtqr.py
@@ -1,8 +1,7 @@
import pytest
from mock import MagicMock
-from seedsigner.models.decode_qr import QRType
-from seedsigner.models.encode_qr import EncodeQR, EncodeQRDensity
-from embit import psbt
+from seedsigner.models import EncodeQR, QRType, EncodeQRDensity
+from embit import psbt, bip39
from binascii import a2b_base64
def test_ur_qr_encode():
@@ -11,7 +10,7 @@ def test_ur_qr_encode():
tx = psbt.PSBT.parse(a2b_base64(base64_psbt))
- e = EncodeQR(psbt=tx, qr_type=QRType.PSBTUR2)
+ e = EncodeQR(psbt=tx, qr_type=QRType.PSBTUR2, wordlist=bip39.WORDLIST)
cnt = 0
while cnt <= 10:
@@ -26,7 +25,7 @@ def test_specter_qr_encode():
tx = psbt.PSBT.parse(a2b_base64(base64_psbt))
- e = EncodeQR(psbt=tx, qr_type=QRType.PSBTSPECTER)
+ e = EncodeQR(psbt=tx, qr_type=QRType.PSBTSPECTER, wordlist=bip39.WORDLIST)
cnt = 0
while cnt <= 10:
@@ -61,7 +60,7 @@ def test_seedsigner_qr():
mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash"
- e = EncodeQR(seed_phrase=mnemonic.split(" "), qr_type=QRType.SEEDSSQR)
+ e = EncodeQR(seed_phrase=mnemonic.split(" "), qr_type=QRType.SEEDSSQR, wordlist=bip39.WORDLIST)
print(e.nextPart())
@@ -71,19 +70,19 @@ def test_xpub_qr():
mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash"
- e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.XPUBQR, network="test", policy="PKWPKH", derivation="m/48h/1h/0h/2h")
+ e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.XPUBQR, network="test", derivation="m/48h/1h/0h/2h", wordlist=bip39.WORDLIST)
- assert e.nextPart() == "[c49122a5/48h/1h/0h/2h]vpub5adb6xr5X1yqx2v7qoo5uR32BBCQsj9tRe9zbMRsH8X6tYGE6CzuiF6k3njCyHrU5JWPMsoPwNZd9Bk9Kaqy29izB8PbuU6smtv8HLcTEAv"
+ assert e.nextPart() == "[c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDNc5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBoMxfrVggXj5NJmaNEToWpax8GMMucozvAdqf1bW1JsZsfdBzsK3VUC5"
def test_specter_xpub_qr():
mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash"
- e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.SPECTERXPUBQR, network="test", policy="PKWPKH", derivation="m/48h/1h/0h/2h", qr_density=EncodeQRDensity.LOW)
+ e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.SPECTERXPUBQR, network="test", derivation="m/48h/1h/0h/2h", qr_density=EncodeQRDensity.LOW, wordlist=bip39.WORDLIST)
- assert e.nextPart() == "p1of4 [c49122a5/48h/1h/0h/2h]vpub5adb6xr5X1yqx"
- assert e.nextPart() == "p2of4 2v7qoo5uR32BBCQsj9tRe9zbMRsH8X6tYGE6Czui"
- assert e.nextPart() == "p3of4 F6k3njCyHrU5JWPMsoPwNZd9Bk9Kaqy29izB8Pbu"
- assert e.nextPart() == "p4of4 U6smtv8HLcTEAv"
+ assert e.nextPart() == "p1of4 [c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDN"
+ assert e.nextPart() == "p2of4 c5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBo"
+ assert e.nextPart() == "p3of4 MxfrVggXj5NJmaNEToWpax8GMMucozvAdqf1bW1J"
+ assert e.nextPart() == "p4of4 sZsfdBzsK3VUC5"
diff --git a/tests/test_mnemonic_generation.py b/tests/test_mnemonic_generation.py
index 65f002a8..67ecaf9a 100644
--- a/tests/test_mnemonic_generation.py
+++ b/tests/test_mnemonic_generation.py
@@ -23,10 +23,10 @@ def test_calculate_checksum():
"""
# Test mnemonics from https://iancoleman.io/bip39/
partial_mnemonic = "crawl focus rescue cable view pledge rather dinner cousin unfair day"
- mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "))
+ mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist=bip39.WORDLIST)
assert bip39.mnemonic_is_valid(" ".join(mnemonic))
partial_mnemonic = "bubble father debate ankle injury fence mesh evolve section wet coyote violin pyramid flower rent arrow round clutch myth safe base skin mobile"
- mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "))
+ mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist=bip39.WORDLIST)
assert bip39.mnemonic_is_valid(" ".join(mnemonic))
diff --git a/tests/test_psbt_parser.py b/tests/test_psbt_parser.py
new file mode 100644
index 00000000..7fb84c60
--- /dev/null
+++ b/tests/test_psbt_parser.py
@@ -0,0 +1,8 @@
+import pytest
+from mock import MagicMock
+from seedsigner.models import PSBTParser
+from embit import psbt
+
+def test_sig_count():
+ print("test")
+
\ No newline at end of file
diff --git a/tests/test_seed.py b/tests/test_seed.py
new file mode 100644
index 00000000..a03f2f87
--- /dev/null
+++ b/tests/test_seed.py
@@ -0,0 +1,46 @@
+import pytest
+from mock import MagicMock
+from seedsigner.models import Seed
+from embit import bip39
+
+spanish_wordlist = ["ábaco", "abdomen", "abeja", "abierto", "abogado", "abono", "aborto", "abrazo", "abrir", "abuelo", "abuso", "acabar", "academia", "acceso", "acción", "aceite", "acelga", "acento", "aceptar", "ácido", "aclarar", "acné", "acoger", "acoso", "activo", "acto", "actriz", "actuar", "acudir", "acuerdo", "acusar", "adicto", "admitir", "adoptar", "adorno", "aduana", "adulto", "aéreo", "afectar", "afición", "afinar", "afirmar", "ágil", "agitar", "agonía", "agosto", "agotar", "agregar", "agrio", "agua", "agudo", "águila", "aguja", "ahogo", "ahorro", "aire", "aislar", "ajedrez", "ajeno", "ajuste", "alacrán", "alambre", "alarma", "alba", "álbum", "alcalde", "aldea", "alegre", "alejar", "alerta", "aleta", "alfiler", "alga", "algodón", "aliado", "aliento", "alivio", "alma", "almeja", "almíbar", "altar", "alteza", "altivo", "alto", "altura", "alumno", "alzar", "amable", "amante", "amapola", "amargo", "amasar", "ámbar", "ámbito", "ameno", "amigo", "amistad", "amor", "amparo", "amplio", "ancho", "anciano", "ancla", "andar", "andén", "anemia", "ángulo", "anillo", "ánimo", "anís", "anotar", "antena", "antiguo", "antojo", "anual", "anular", "anuncio", "añadir", "añejo", "año", "apagar", "aparato", "apetito", "apio", "aplicar", "apodo", "aporte", "apoyo", "aprender", "aprobar", "apuesta", "apuro", "arado", "araña", "arar", "árbitro", "árbol", "arbusto", "archivo", "arco", "arder", "ardilla", "arduo", "área", "árido", "aries", "armonía", "arnés", "aroma", "arpa", "arpón", "arreglo", "arroz", "arruga", "arte", "artista", "asa", "asado", "asalto", "ascenso", "asegurar", "aseo", "asesor", "asiento", "asilo", "asistir", "asno", "asombro", "áspero", "astilla", "astro", "astuto", "asumir", "asunto", "atajo", "ataque", "atar", "atento", "ateo", "ático", "atleta", "átomo", "atraer", "atroz", "atún", "audaz", "audio", "auge", "aula", "aumento", "ausente", "autor", "aval", "avance", "avaro", "ave", "avellana", "avena", "avestruz", "avión", "aviso", "ayer", "ayuda", "ayuno", "azafrán", "azar", "azote", "azúcar", "azufre", "azul", "baba", "babor", "bache", "bahía", "baile", "bajar", "balanza", "balcón", "balde", "bambú", "banco", "banda", "baño", "barba", "barco", "barniz", "barro", "báscula", "bastón", "basura", "batalla", "batería", "batir", "batuta", "baúl", "bazar", "bebé", "bebida", "bello", "besar", "beso", "bestia", "bicho", "bien", "bingo", "blanco", "bloque", "blusa", "boa", "bobina", "bobo", "boca", "bocina", "boda", "bodega", "boina", "bola", "bolero", "bolsa", "bomba", "bondad", "bonito", "bono", "bonsái", "borde", "borrar", "bosque", "bote", "botín", "bóveda", "bozal", "bravo", "brazo", "brecha", "breve", "brillo", "brinco", "brisa", "broca", "broma", "bronce", "brote", "bruja", "brusco", "bruto", "buceo", "bucle", "bueno", "buey", "bufanda", "bufón", "búho", "buitre", "bulto", "burbuja", "burla", "burro", "buscar", "butaca", "buzón", "caballo", "cabeza", "cabina", "cabra", "cacao", "cadáver", "cadena", "caer", "café", "caída", "caimán", "caja", "cajón", "cal", "calamar", "calcio", "caldo", "calidad", "calle", "calma", "calor", "calvo", "cama", "cambio", "camello", "camino", "campo", "cáncer", "candil", "canela", "canguro", "canica", "canto", "caña", "cañón", "caoba", "caos", "capaz", "capitán", "capote", "captar", "capucha", "cara", "carbón", "cárcel", "careta", "carga", "cariño", "carne", "carpeta", "carro", "carta", "casa", "casco", "casero", "caspa", "castor", "catorce", "catre", "caudal", "causa", "cazo", "cebolla", "ceder", "cedro", "celda", "célebre", "celoso", "célula", "cemento", "ceniza", "centro", "cerca", "cerdo", "cereza", "cero", "cerrar", "certeza", "césped", "cetro", "chacal", "chaleco", "champú", "chancla", "chapa", "charla", "chico", "chiste", "chivo", "choque", "choza", "chuleta", "chupar", "ciclón", "ciego", "cielo", "cien", "cierto", "cifra", "cigarro", "cima", "cinco", "cine", "cinta", "ciprés", "circo", "ciruela", "cisne", "cita", "ciudad", "clamor", "clan", "claro", "clase", "clave", "cliente", "clima", "clínica", "cobre", "cocción", "cochino", "cocina", "coco", "código", "codo", "cofre", "coger", "cohete", "cojín", "cojo", "cola", "colcha", "colegio", "colgar", "colina", "collar", "colmo", "columna", "combate", "comer", "comida", "cómodo", "compra", "conde", "conejo", "conga", "conocer", "consejo", "contar", "copa", "copia", "corazón", "corbata", "corcho", "cordón", "corona", "correr", "coser", "cosmos", "costa", "cráneo", "cráter", "crear", "crecer", "creído", "crema", "cría", "crimen", "cripta", "crisis", "cromo", "crónica", "croqueta", "crudo", "cruz", "cuadro", "cuarto", "cuatro", "cubo", "cubrir", "cuchara", "cuello", "cuento", "cuerda", "cuesta", "cueva", "cuidar", "culebra", "culpa", "culto", "cumbre", "cumplir", "cuna", "cuneta", "cuota", "cupón", "cúpula", "curar", "curioso", "curso", "curva", "cutis", "dama", "danza", "dar", "dardo", "dátil", "deber", "débil", "década", "decir", "dedo", "defensa", "definir", "dejar", "delfín", "delgado", "delito", "demora", "denso", "dental", "deporte", "derecho", "derrota", "desayuno", "deseo", "desfile", "desnudo", "destino", "desvío", "detalle", "detener", "deuda", "día", "diablo", "diadema", "diamante", "diana", "diario", "dibujo", "dictar", "diente", "dieta", "diez", "difícil", "digno", "dilema", "diluir", "dinero", "directo", "dirigir", "disco", "diseño", "disfraz", "diva", "divino", "doble", "doce", "dolor", "domingo", "don", "donar", "dorado", "dormir", "dorso", "dos", "dosis", "dragón", "droga", "ducha", "duda", "duelo", "dueño", "dulce", "dúo", "duque", "durar", "dureza", "duro", "ébano", "ebrio", "echar", "eco", "ecuador", "edad", "edición", "edificio", "editor", "educar", "efecto", "eficaz", "eje", "ejemplo", "elefante", "elegir", "elemento", "elevar", "elipse", "élite", "elixir", "elogio", "eludir", "embudo", "emitir", "emoción", "empate", "empeño", "empleo", "empresa", "enano", "encargo", "enchufe", "encía", "enemigo", "enero", "enfado", "enfermo", "engaño", "enigma", "enlace", "enorme", "enredo", "ensayo", "enseñar", "entero", "entrar", "envase", "envío", "época", "equipo", "erizo", "escala", "escena", "escolar", "escribir", "escudo", "esencia", "esfera", "esfuerzo", "espada", "espejo", "espía", "esposa", "espuma", "esquí", "estar", "este", "estilo", "estufa", "etapa", "eterno", "ética", "etnia", "evadir", "evaluar", "evento", "evitar", "exacto", "examen", "exceso", "excusa", "exento", "exigir", "exilio", "existir", "éxito", "experto", "explicar", "exponer", "extremo", "fábrica", "fábula", "fachada", "fácil", "factor", "faena", "faja", "falda", "fallo", "falso", "faltar", "fama", "familia", "famoso", "faraón", "farmacia", "farol", "farsa", "fase", "fatiga", "fauna", "favor", "fax", "febrero", "fecha", "feliz", "feo", "feria", "feroz", "fértil", "fervor", "festín", "fiable", "fianza", "fiar", "fibra", "ficción", "ficha", "fideo", "fiebre", "fiel", "fiera", "fiesta", "figura", "fijar", "fijo", "fila", "filete", "filial", "filtro", "fin", "finca", "fingir", "finito", "firma", "flaco", "flauta", "flecha", "flor", "flota", "fluir", "flujo", "flúor", "fobia", "foca", "fogata", "fogón", "folio", "folleto", "fondo", "forma", "forro", "fortuna", "forzar", "fosa", "foto", "fracaso", "frágil", "franja", "frase", "fraude", "freír", "freno", "fresa", "frío", "frito", "fruta", "fuego", "fuente", "fuerza", "fuga", "fumar", "función", "funda", "furgón", "furia", "fusil", "fútbol", "futuro", "gacela", "gafas", "gaita", "gajo", "gala", "galería", "gallo", "gamba", "ganar", "gancho", "ganga", "ganso", "garaje", "garza", "gasolina", "gastar", "gato", "gavilán", "gemelo", "gemir", "gen", "género", "genio", "gente", "geranio", "gerente", "germen", "gesto", "gigante", "gimnasio", "girar", "giro", "glaciar", "globo", "gloria", "gol", "golfo", "goloso", "golpe", "goma", "gordo", "gorila", "gorra", "gota", "goteo", "gozar", "grada", "gráfico", "grano", "grasa", "gratis", "grave", "grieta", "grillo", "gripe", "gris", "grito", "grosor", "grúa", "grueso", "grumo", "grupo", "guante", "guapo", "guardia", "guerra", "guía", "guiño", "guion", "guiso", "guitarra", "gusano", "gustar", "haber", "hábil", "hablar", "hacer", "hacha", "hada", "hallar", "hamaca", "harina", "haz", "hazaña", "hebilla", "hebra", "hecho", "helado", "helio", "hembra", "herir", "hermano", "héroe", "hervir", "hielo", "hierro", "hígado", "higiene", "hijo", "himno", "historia", "hocico", "hogar", "hoguera", "hoja", "hombre", "hongo", "honor", "honra", "hora", "hormiga", "horno", "hostil", "hoyo", "hueco", "huelga", "huerta", "hueso", "huevo", "huida", "huir", "humano", "húmedo", "humilde", "humo", "hundir", "huracán", "hurto", "icono", "ideal", "idioma", "ídolo", "iglesia", "iglú", "igual", "ilegal", "ilusión", "imagen", "imán", "imitar", "impar", "imperio", "imponer", "impulso", "incapaz", "índice", "inerte", "infiel", "informe", "ingenio", "inicio", "inmenso", "inmune", "innato", "insecto", "instante", "interés", "íntimo", "intuir", "inútil", "invierno", "ira", "iris", "ironía", "isla", "islote", "jabalí", "jabón", "jamón", "jarabe", "jardín", "jarra", "jaula", "jazmín", "jefe", "jeringa", "jinete", "jornada", "joroba", "joven", "joya", "juerga", "jueves", "juez", "jugador", "jugo", "juguete", "juicio", "junco", "jungla", "junio", "juntar", "júpiter", "jurar", "justo", "juvenil", "juzgar", "kilo", "koala", "labio", "lacio", "lacra", "lado", "ladrón", "lagarto", "lágrima", "laguna", "laico", "lamer", "lámina", "lámpara", "lana", "lancha", "langosta", "lanza", "lápiz", "largo", "larva", "lástima", "lata", "látex", "latir", "laurel", "lavar", "lazo", "leal", "lección", "leche", "lector", "leer", "legión", "legumbre", "lejano", "lengua", "lento", "leña", "león", "leopardo", "lesión", "letal", "letra", "leve", "leyenda", "libertad", "libro", "licor", "líder", "lidiar", "lienzo", "liga", "ligero", "lima", "límite", "limón", "limpio", "lince", "lindo", "línea", "lingote", "lino", "linterna", "líquido", "liso", "lista", "litera", "litio", "litro", "llaga", "llama", "llanto", "llave", "llegar", "llenar", "llevar", "llorar", "llover", "lluvia", "lobo", "loción", "loco", "locura", "lógica", "logro", "lombriz", "lomo", "lonja", "lote", "lucha", "lucir", "lugar", "lujo", "luna", "lunes", "lupa", "lustro", "luto", "luz", "maceta", "macho", "madera", "madre", "maduro", "maestro", "mafia", "magia", "mago", "maíz", "maldad", "maleta", "malla", "malo", "mamá", "mambo", "mamut", "manco", "mando", "manejar", "manga", "maniquí", "manjar", "mano", "manso", "manta", "mañana", "mapa", "máquina", "mar", "marco", "marea", "marfil", "margen", "marido", "mármol", "marrón", "martes", "marzo", "masa", "máscara", "masivo", "matar", "materia", "matiz", "matriz", "máximo", "mayor", "mazorca", "mecha", "medalla", "medio", "médula", "mejilla", "mejor", "melena", "melón", "memoria", "menor", "mensaje", "mente", "menú", "mercado", "merengue", "mérito", "mes", "mesón", "meta", "meter", "método", "metro", "mezcla", "miedo", "miel", "miembro", "miga", "mil", "milagro", "militar", "millón", "mimo", "mina", "minero", "mínimo", "minuto", "miope", "mirar", "misa", "miseria", "misil", "mismo", "mitad", "mito", "mochila", "moción", "moda", "modelo", "moho", "mojar", "molde", "moler", "molino", "momento", "momia", "monarca", "moneda", "monja", "monto", "moño", "morada", "morder", "moreno", "morir", "morro", "morsa", "mortal", "mosca", "mostrar", "motivo", "mover", "móvil", "mozo", "mucho", "mudar", "mueble", "muela", "muerte", "muestra", "mugre", "mujer", "mula", "muleta", "multa", "mundo", "muñeca", "mural", "muro", "músculo", "museo", "musgo", "música", "muslo", "nácar", "nación", "nadar", "naipe", "naranja", "nariz", "narrar", "nasal", "natal", "nativo", "natural", "náusea", "naval", "nave", "navidad", "necio", "néctar", "negar", "negocio", "negro", "neón", "nervio", "neto", "neutro", "nevar", "nevera", "nicho", "nido", "niebla", "nieto", "niñez", "niño", "nítido", "nivel", "nobleza", "noche", "nómina", "noria", "norma", "norte", "nota", "noticia", "novato", "novela", "novio", "nube", "nuca", "núcleo", "nudillo", "nudo", "nuera", "nueve", "nuez", "nulo", "número", "nutria", "oasis", "obeso", "obispo", "objeto", "obra", "obrero", "observar", "obtener", "obvio", "oca", "ocaso", "océano", "ochenta", "ocho", "ocio", "ocre", "octavo", "octubre", "oculto", "ocupar", "ocurrir", "odiar", "odio", "odisea", "oeste", "ofensa", "oferta", "oficio", "ofrecer", "ogro", "oído", "oír", "ojo", "ola", "oleada", "olfato", "olivo", "olla", "olmo", "olor", "olvido", "ombligo", "onda", "onza", "opaco", "opción", "ópera", "opinar", "oponer", "optar", "óptica", "opuesto", "oración", "orador", "oral", "órbita", "orca", "orden", "oreja", "órgano", "orgía", "orgullo", "oriente", "origen", "orilla", "oro", "orquesta", "oruga", "osadía", "oscuro", "osezno", "oso", "ostra", "otoño", "otro", "oveja", "óvulo", "óxido", "oxígeno", "oyente", "ozono", "pacto", "padre", "paella", "página", "pago", "país", "pájaro", "palabra", "palco", "paleta", "pálido", "palma", "paloma", "palpar", "pan", "panal", "pánico", "pantera", "pañuelo", "papá", "papel", "papilla", "paquete", "parar", "parcela", "pared", "parir", "paro", "párpado", "parque", "párrafo", "parte", "pasar", "paseo", "pasión", "paso", "pasta", "pata", "patio", "patria", "pausa", "pauta", "pavo", "payaso", "peatón", "pecado", "pecera", "pecho", "pedal", "pedir", "pegar", "peine", "pelar", "peldaño", "pelea", "peligro", "pellejo", "pelo", "peluca", "pena", "pensar", "peñón", "peón", "peor", "pepino", "pequeño", "pera", "percha", "perder", "pereza", "perfil", "perico", "perla", "permiso", "perro", "persona", "pesa", "pesca", "pésimo", "pestaña", "pétalo", "petróleo", "pez", "pezuña", "picar", "pichón", "pie", "piedra", "pierna", "pieza", "pijama", "pilar", "piloto", "pimienta", "pino", "pintor", "pinza", "piña", "piojo", "pipa", "pirata", "pisar", "piscina", "piso", "pista", "pitón", "pizca", "placa", "plan", "plata", "playa", "plaza", "pleito", "pleno", "plomo", "pluma", "plural", "pobre", "poco", "poder", "podio", "poema", "poesía", "poeta", "polen", "policía", "pollo", "polvo", "pomada", "pomelo", "pomo", "pompa", "poner", "porción", "portal", "posada", "poseer", "posible", "poste", "potencia", "potro", "pozo", "prado", "precoz", "pregunta", "premio", "prensa", "preso", "previo", "primo", "príncipe", "prisión", "privar", "proa", "probar", "proceso", "producto", "proeza", "profesor", "programa", "prole", "promesa", "pronto", "propio", "próximo", "prueba", "público", "puchero", "pudor", "pueblo", "puerta", "puesto", "pulga", "pulir", "pulmón", "pulpo", "pulso", "puma", "punto", "puñal", "puño", "pupa", "pupila", "puré", "quedar", "queja", "quemar", "querer", "queso", "quieto", "química", "quince", "quitar", "rábano", "rabia", "rabo", "ración", "radical", "raíz", "rama", "rampa", "rancho", "rango", "rapaz", "rápido", "rapto", "rasgo", "raspa", "rato", "rayo", "raza", "razón", "reacción", "realidad", "rebaño", "rebote", "recaer", "receta", "rechazo", "recoger", "recreo", "recto", "recurso", "red", "redondo", "reducir", "reflejo", "reforma", "refrán", "refugio", "regalo", "regir", "regla", "regreso", "rehén", "reino", "reír", "reja", "relato", "relevo", "relieve", "relleno", "reloj", "remar", "remedio", "remo", "rencor", "rendir", "renta", "reparto", "repetir", "reposo", "reptil", "res", "rescate", "resina", "respeto", "resto", "resumen", "retiro", "retorno", "retrato", "reunir", "revés", "revista", "rey", "rezar", "rico", "riego", "rienda", "riesgo", "rifa", "rígido", "rigor", "rincón", "riñón", "río", "riqueza", "risa", "ritmo", "rito", "rizo", "roble", "roce", "rociar", "rodar", "rodeo", "rodilla", "roer", "rojizo", "rojo", "romero", "romper", "ron", "ronco", "ronda", "ropa", "ropero", "rosa", "rosca", "rostro", "rotar", "rubí", "rubor", "rudo", "rueda", "rugir", "ruido", "ruina", "ruleta", "rulo", "rumbo", "rumor", "ruptura", "ruta", "rutina", "sábado", "saber", "sabio", "sable", "sacar", "sagaz", "sagrado", "sala", "saldo", "salero", "salir", "salmón", "salón", "salsa", "salto", "salud", "salvar", "samba", "sanción", "sandía", "sanear", "sangre", "sanidad", "sano", "santo", "sapo", "saque", "sardina", "sartén", "sastre", "satán", "sauna", "saxofón", "sección", "seco", "secreto", "secta", "sed", "seguir", "seis", "sello", "selva", "semana", "semilla", "senda", "sensor", "señal", "señor", "separar", "sepia", "sequía", "ser", "serie", "sermón", "servir", "sesenta", "sesión", "seta", "setenta", "severo", "sexo", "sexto", "sidra", "siesta", "siete", "siglo", "signo", "sílaba", "silbar", "silencio", "silla", "símbolo", "simio", "sirena", "sistema", "sitio", "situar", "sobre", "socio", "sodio", "sol", "solapa", "soldado", "soledad", "sólido", "soltar", "solución", "sombra", "sondeo", "sonido", "sonoro", "sonrisa", "sopa", "soplar", "soporte", "sordo", "sorpresa", "sorteo", "sostén", "sótano", "suave", "subir", "suceso", "sudor", "suegra", "suelo", "sueño", "suerte", "sufrir", "sujeto", "sultán", "sumar", "superar", "suplir", "suponer", "supremo", "sur", "surco", "sureño", "surgir", "susto", "sutil", "tabaco", "tabique", "tabla", "tabú", "taco", "tacto", "tajo", "talar", "talco", "talento", "talla", "talón", "tamaño", "tambor", "tango", "tanque", "tapa", "tapete", "tapia", "tapón", "taquilla", "tarde", "tarea", "tarifa", "tarjeta", "tarot", "tarro", "tarta", "tatuaje", "tauro", "taza", "tazón", "teatro", "techo", "tecla", "técnica", "tejado", "tejer", "tejido", "tela", "teléfono", "tema", "temor", "templo", "tenaz", "tender", "tener", "tenis", "tenso", "teoría", "terapia", "terco", "término", "ternura", "terror", "tesis", "tesoro", "testigo", "tetera", "texto", "tez", "tibio", "tiburón", "tiempo", "tienda", "tierra", "tieso", "tigre", "tijera", "tilde", "timbre", "tímido", "timo", "tinta", "tío", "típico", "tipo", "tira", "tirón", "titán", "títere", "título", "tiza", "toalla", "tobillo", "tocar", "tocino", "todo", "toga", "toldo", "tomar", "tono", "tonto", "topar", "tope", "toque", "tórax", "torero", "tormenta", "torneo", "toro", "torpedo", "torre", "torso", "tortuga", "tos", "tosco", "toser", "tóxico", "trabajo", "tractor", "traer", "tráfico", "trago", "traje", "tramo", "trance", "trato", "trauma", "trazar", "trébol", "tregua", "treinta", "tren", "trepar", "tres", "tribu", "trigo", "tripa", "triste", "triunfo", "trofeo", "trompa", "tronco", "tropa", "trote", "trozo", "truco", "trueno", "trufa", "tubería", "tubo", "tuerto", "tumba", "tumor", "túnel", "túnica", "turbina", "turismo", "turno", "tutor", "ubicar", "úlcera", "umbral", "unidad", "unir", "universo", "uno", "untar", "uña", "urbano", "urbe", "urgente", "urna", "usar", "usuario", "útil", "utopía", "uva", "vaca", "vacío", "vacuna", "vagar", "vago", "vaina", "vajilla", "vale", "válido", "valle", "valor", "válvula", "vampiro", "vara", "variar", "varón", "vaso", "vecino", "vector", "vehículo", "veinte", "vejez", "vela", "velero", "veloz", "vena", "vencer", "venda", "veneno", "vengar", "venir", "venta", "venus", "ver", "verano", "verbo", "verde", "vereda", "verja", "verso", "verter", "vía", "viaje", "vibrar", "vicio", "víctima", "vida", "vídeo", "vidrio", "viejo", "viernes", "vigor", "vil", "villa", "vinagre", "vino", "viñedo", "violín", "viral", "virgo", "virtud", "visor", "víspera", "vista", "vitamina", "viudo", "vivaz", "vivero", "vivir", "vivo", "volcán", "volumen", "volver", "voraz", "votar", "voto", "voz", "vuelo", "vulgar", "yacer", "yate", "yegua", "yema", "yerno", "yeso", "yodo", "yoga", "yogur", "zafiro", "zanja", "zapato", "zarza", "zona", "zorro", "zumo", "zurdo"]
+
+def test_seed():
+
+ seed = Seed(mnemonic="obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash", wordlist=bip39.WORDLIST)
+
+ # valid seed?
+ assert seed
+
+ assert seed.seed == b'q\xb3\xd1i\x0c\x9b\x9b\xdf\xa7\xd9\xd97H\xa8,\xa7\xd9>\xeck\xc2\xf5ND?, \x88-\x07\x9aa\xc5\xee\xb7\xbf\xc4x\xd6\x07 X\xb6}?M\xaa\x05\xa6\xa7(>\xbf\x03\xb0\x9d\xef\xed":\xdf\x88w7'
+
+ assert seed.mnemonic == "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash"
+
+ assert seed.passphrase == ""
+
+ seed.wordlist = spanish_wordlist
+
+ assert seed.mnemonic == "natural ayuda futuro nivel espejo abuelo vago bien repetir moreno relevo conga"
+
+ seed.wordlist = bip39.WORDLIST
+
+ seed.mnemonic = "height demise useless trap grow lion found off key clown transfer enroll"
+
+ assert seed.mnemonic == "height demise useless trap grow lion found off key clown transfer enroll"
+
+ seed.wordlist = spanish_wordlist
+
+ assert seed.mnemonic == "hebilla cría truco tigre gris llenar folio negocio laico casa tieso eludir"
+
+ seed.passphrase = "test"
+
+ assert seed.seed == b'\xdd\r\xcb\x0b V\xb4@\xee+\x01`\xabem\xc1B\xfd\x8fba0\xab;[\xab\xc9\xf9\xba[F\x0c5,\x7fd8\xebI\x90"\xb8\x86C\x821\x01\xdb\xbe\xf3\xbc\x1cBH"%\x18\xc2{\x04\x08a]\xa5'
+
+ assert seed.passphrase == "test"
+
+
+
+
+
+
+
\ No newline at end of file