Second attempt at a clean 0.4.6 merge

This commit is contained in:
kdmukai
2022-02-19 16:46:24 -06:00
parent 1730d8ecb6
commit b55ed9edc8
13 changed files with 457 additions and 139 deletions
+3 -2
View File
@@ -5,11 +5,12 @@ persistent_settings = False
[display]
text_color = ORANGE
qr_background_color = FFFFFF
qr_background_color = 555555
camera_rotation = 0
[wallet]
network = main
software = Prompt
qr_density = 2
custom_derivation = m/0/0
custom_derivation = m/0/0
compact_seedqr_enabled = False
+28 -5
View File
@@ -33,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.5"
VERSION = "0.4.6"
@classmethod
@@ -46,7 +46,17 @@ class Controller(Singleton):
@classmethod
def configure_instance(cls, config=None):
def configure_instance(cls, config=None, disable_hardware=False):
"""
- `disable_hardware` is only meant to be used by the test suite so that it
can keep re-initializing a Controller in however many tests it needs to. But
this is only possible if the hardware isn't already being reserved. Without
this you get:
RuntimeError: Conflicting edge detection already enabled for this GPIO channel
each time you try to re-initialize a Controller.
"""
# Must be called before the first get_instance() call
if cls._instance:
raise Exception("Instance already configured")
@@ -56,7 +66,10 @@ class Controller(Singleton):
cls._instance = controller
# Input Buttons
controller.buttons = Buttons()
if disable_hardware:
controller.buttons = None
else:
controller.buttons = Buttons()
# models
controller.storage = SeedStorage()
@@ -159,6 +172,8 @@ class Controller(Singleton):
ret_val = self.show_persistent_settings_tool()
elif ret_val == Path.CAMERA_ROTATION:
ret_val = self.show_camera_rotation_tool()
elif ret_val == Path.COMPACT_SEEDQR_ENABLED:
ret_val = self.show_compact_seedqr_enabled()
elif ret_val == Path.DONATE:
ret_val = self.show_donate_tool()
elif ret_val == Path.RESET:
@@ -705,7 +720,7 @@ class Controller(Singleton):
# 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?")
r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Use Saved Seed?")
if r == 1: #Yes
slot_num = self.menu_view.display_saved_seed_menu(self.storage,3,None)
if slot_num == 0:
@@ -887,7 +902,7 @@ class Controller(Singleton):
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?")
r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Use Saved Seed?")
if r == 1: #Yes
slot_num = self.menu_view.display_saved_seed_menu(self.storage,3,None)
if slot_num == 0:
@@ -1111,6 +1126,14 @@ class Controller(Singleton):
return Path.SETTINGS_SUB_MENU ### Show Donate Screen and QR
def show_compact_seedqr_enabled(self):
r = self.settings_tools_view.display_compact_seedqr_enabled()
if r is not None:
self.settings.compact_seedqr_enabled = r
return Path.SETTINGS_SUB_MENU ### Show Donate Screen and QR
def show_donate_tool(self):
self.settings_tools_view.display_donate_info_screen()
+1
View File
@@ -29,6 +29,7 @@ class Path:
QR_DENSITY_SETTING = 65
PERSISTENT_SETTINGS = 67
CAMERA_ROTATION = 69
COMPACT_SEEDQR_ENABLED = 70
RESET = 68
# Seed Slots
+26 -3
View File
@@ -1,17 +1,40 @@
import qrcode
from qrcode.image.styledpil import StyledPilImage
from qrcode.image.styles.moduledrawers import CircleModuleDrawer, GappedSquareModuleDrawer
from PIL import Image
import subprocess
class QR:
STYLE__DEFAULT = 1
STYLE__ROUNDED = 2
STYLE__GRID = 3
def __init__(self) -> None:
return
def qrimage(self, data, width=240, height=240, border=3):
def qrimage(self, data, width=240, height=240, border=3, style=None):
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('RGBA'))
if not style or style == QR.STYLE__DEFAULT:
return qr.make_image(fill_color="black", back_color="white").resize((width,height)).convert('RGBA')
else:
if style == QR.STYLE__ROUNDED:
return qr.make_image(
fill_color="black",
back_color="white",
image_factory=StyledPilImage,
module_drawer=CircleModuleDrawer()
).resize((width,height)).convert('RGBA')
elif style == QR.STYLE__GRID:
return qr.make_image(
fill_color="black",
back_color="white",
image_factory=StyledPilImage,
module_drawer=GappedSquareModuleDrawer()
).resize((width,height)).convert('RGBA')
def qrimage_io(self, data, width=240, height=240, border=3, background="FFFFFF"):
if 1 <= border <= 10:
@@ -27,4 +50,4 @@ class QR:
return self.qrimage(data,width,height,border)
img = Image.open("/dev/shm/qrcode.png").resize((width,height), Image.NEAREST).convert("RGBA")
return img
return img
+128 -76
View File
@@ -3,13 +3,19 @@ from pyzbar.pyzbar import ZBarSymbol
from enum import IntEnum
import re
import json
import logging
import base64
from embit import psbt
from embit import psbt, bip39
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 import QRType, Seed
logger = logging.getLogger(__name__)
from urtypes.crypto import PSBT as UR_PSBT
###
### DecodeQR Class
### Purpose: used to process images or string data from animated qr codes with psbt data to create binary/base64 psbt
@@ -41,23 +47,40 @@ class DecodeQR:
self.seedqr = SeedQR(wordlist=self.wordlist)
def addImage(self, image):
qr_str = DecodeQR.QR2Str(image)
if qr_str == None:
data = DecodeQR.extract_qr_data(image, is_binary=True)
if data == None:
return DecodeQRStatus.FALSE
return self.addString(qr_str)
return self.add_data(data)
def addString(self, qr_str):
if qr_str == None:
def add_data(self, data):
if data == None:
return DecodeQRStatus.FALSE
qr_type = DecodeQR.SegmentType(qr_str, wordlist=self.wordlist)
qr_type = DecodeQR.SegmentType(data, wordlist=self.wordlist)
if self.qr_type == None:
self.qr_type = qr_type
elif self.qr_type != qr_type:
raise Exception('QR Fragement Unexpected Type Change')
# Process the binary formats first
if self.qr_type == QRType.COMPACTSEEDQR:
rt = self.seedqr.add(data, QRType.COMPACTSEEDQR)
if rt == DecodeQRStatus.COMPLETE:
self.complete = True
return rt
# Convert to string data
if type(data) == bytes:
# Should always be bytes, but the test suite has some manual datasets that
# are strings.
# TODO: Convert the test suite rather than handle here?
qr_str = data.decode('utf-8')
else:
# it's already str data
qr_str = data
if self.qr_type == QRType.PSBTUR2:
self.ur_decoder.receive_part(qr_str)
@@ -94,8 +117,8 @@ class DecodeQR:
self.complete = True
return rt
elif self.qr_type == QRType.SEEDSSQR:
rt = self.seedqr.add(qr_str, QRType.SEEDSSQR)
elif self.qr_type == QRType.SEEDQR:
rt = self.seedqr.add(qr_str, QRType.SEEDQR)
if rt == DecodeQRStatus.COMPLETE:
self.complete = True
return rt
@@ -141,7 +164,8 @@ class DecodeQR:
if self.complete:
if self.qr_type == QRType.PSBTUR2:
cbor = self.ur_decoder.result_message().cbor
return cbor_decode(cbor)
return UR_PSBT.from_cbor(cbor).data
# return cbor_decode(cbor)
elif self.qr_type == QRType.PSBTSPECTER:
return self.specter_qr.getData()
elif self.qr_type == QRType.PSBTURLEGACY:
@@ -213,7 +237,7 @@ class DecodeQR:
return False
def isSeed(self):
if self.qr_type in (QRType.SEEDSSQR, QRType.SEEDUR2, QRType.SEEDMNEMONIC, QRType.SEED4LETTERMNEMONIC):
if self.qr_type in (QRType.SEEDQR, QRType.COMPACTSEEDQR, QRType.SEEDUR2, QRType.SEEDMNEMONIC, QRType.SEED4LETTERMNEMONIC):
return True
return False
@@ -231,62 +255,95 @@ class DecodeQR:
return self.qr_type
@staticmethod
def QR2Str(image) -> str:
def extract_qr_data(image, is_binary=False) -> str:
if image is None:
return None
barcodes = pyzbar.decode(image, symbols=[ZBarSymbol.QRCODE])
# Are there any cases where defaulting `binary` to True causes problems?
barcodes = pyzbar.decode(image, symbols=[ZBarSymbol.QRCODE], binary=is_binary)
# if barcodes:
# logger.debug("--------------- extract_qr_data ---------------")
# logger.debug(barcodes)
for barcode in barcodes:
# Only pull and return the first barcode
return barcode.data.decode("utf-8")
return barcode.data
@staticmethod
def SegmentType(s, wordlist=None):
# logger.debug("-------------- DecodeQR.SegmentType --------------")
# logger.debug(type(s))
# logger.debug(len(s))
# PSBT
if re.search("^UR:CRYPTO-PSBT/", s, re.IGNORECASE):
return QRType.PSBTUR2
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 DecodeQR.isBase64PSBT(s):
return QRType.PSBTBASE64
# 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
# Convert to str data
if type(s) == bytes:
# Should always be bytes, but the test suite has some manual datasets that
# are strings.
# TODO: Convert the test suite rather than handle here?
s = s.decode('utf-8')
# PSBT
if re.search("^UR:CRYPTO-PSBT/", s, re.IGNORECASE):
return QRType.PSBTUR2
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 DecodeQR.isBase64PSBT(s):
return QRType.PSBTBASE64
# 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 = []
# 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
# Seed
if re.search(r'\d{48,96}', s):
return QRType.SEEDQR
# 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
except UnicodeDecodeError:
# Probably this isn't meant to be string data; check if it's valid byte data
# below.
pass
# Is it byte data?
# 32 bytes for 24-word CompactSeedQR; 16 bytes for 12-word CompactSeedQR
if len(s) == 32 or len(s) == 16:
try:
bitstream = ""
for b in s:
bitstream += bin(b).lstrip('0b').zfill(8)
# logger.debug(bitstream)
return QRType.COMPACTSEEDQR
except Exception as e:
# Couldn't extract byte data; assume it's not a byte format
pass
return QRType.INVALID
@staticmethod
@@ -331,7 +388,7 @@ class DecodeQR:
for c in v[::-1]:
digit = chars.find(bytes([c]))
if digit == -1:
raise BaseDecodeError('Forbidden character {} for base {}'.format(c, 43))
raise Exception('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
@@ -532,14 +589,6 @@ class Base64DecodeQR:
return None
@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
@@ -570,14 +619,6 @@ class Base43DecodeQR:
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
@@ -602,11 +643,11 @@ class SeedQR:
if self.wordlist == None:
raise Exception('Wordlist Required')
def add(self, segment, qr_type=QRType.SEEDSSQR):
def add(self, segment, qr_type=QRType.SEEDQR):
# `segment` data will either be bytes or str, depending on the qr_type
logger.debug(qr_type)
_4LETTER_WORDLIST = [word[:4].strip() for word in self.wordlist]
if qr_type == QRType.SEEDSSQR:
if qr_type == QRType.SEEDQR:
try:
self.seed_phrase = []
@@ -628,8 +669,17 @@ class SeedQR:
except Exception as e:
return DecodeQRStatus.INVALID
elif qr_type == QRType.SEEDMNEMONIC:
if qr_type == QRType.COMPACTSEEDQR:
try:
self.seed_phrase = bip39.mnemonic_from_bytes(segment).split()
self.complete = True
self.collected_segments = 1
return DecodeQRStatus.COMPLETE
except Exception as e:
logger.exception(repr(e))
return DecodeQRStatus.INVALID
elif qr_type == QRType.SEEDMNEMONIC:
try:
# embit mnemonic code to validate
seed = Seed(segment, passphrase="", wordlist=self.wordlist)
@@ -651,6 +701,8 @@ class SeedQR:
sl = segment.strip().split(" ")
words = []
for s in sl:
# TODO: Pre-calculate this once on startup
_4LETTER_WORDLIST = [word[:4].strip() for word in self.wordlist]
words.append(self.wordlist[_4LETTER_WORDLIST.index(s)])
# embit mnemonic code to validate
+129 -12
View File
@@ -1,3 +1,5 @@
import math
from enum import IntEnum
from embit import psbt, bip32
from embit.networks import NETWORKS
@@ -9,6 +11,9 @@ from seedsigner.helpers.bcur import (bc32encode, cbor_encode, bcur_encode)
from seedsigner.helpers.qr import QR
from seedsigner.models import Seed, QRType, EncodeQRDensity
from urtypes.crypto import PSBT as UR_PSBT
from urtypes.crypto import Account, HDKey, Output, Keypath, PathComponent, SCRIPT_EXPRESSION_TAG_MAP
###
### EncodeQR Class
### Purpose: used to encode psbt for displaying as qr image
@@ -61,8 +66,12 @@ class EncodeQR:
self.encoder = SpecterEncodePSBTQR(self.psbt, self.qr_density)
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.wordlist)
elif self.qr_type == QRType.SEEDQR:
self.encoder = SeedQR(self.seed_phrase, self.wordlist)
elif self.qr_type == QRType.COMPACTSEEDQR:
self.encoder = CompactSeedQR(self.seed_phrase, self.wordlist)
elif self.qr_type == QRType.URXPUBQR:
self.encoder = URXPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.qr_density, self.wordlist)
elif self.qr_type == QRType.XPUBQR:
self.encoder = XPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.wordlist)
elif self.qr_type == QRType.SPECTERXPUBQR:
@@ -81,7 +90,7 @@ class EncodeQR:
def nextPartImage(self, width=240, height=240, border=3, background="FFFFFF"):
part = self.nextPart()
if self.qr_type == QRType.SEEDSSQR:
if self.qr_type == QRType.SEEDQR:
return self.qr.qrimage(part, width, height, border)
else:
return self.qr.qrimage_io(part, width, height, border, background=background)
@@ -100,10 +109,8 @@ class UREncodePSBTQR:
def __init__(self, p, qr_density):
self.psbt = p
self.qr_max_fragement_size = 20
cbor_encoder = CBOREncoder()
cbor_encoder.encodeBytes(self.psbt.serialize())
qr_ur_bytes = UR("crypto-psbt", cbor_encoder.get_bytes())
qr_ur_bytes = UR("crypto-psbt", UR_PSBT(self.psbt.serialize()).to_cbor())
if qr_density == EncodeQRDensity.LOW:
self.qr_max_fragement_size = 10
@@ -190,8 +197,9 @@ class SpecterEncodePSBTQR:
def isComplete(self):
return self.sent_complete
class SeedSSQR:
class SeedQR:
def __init__(self, seed_phrase, wordlist):
self.seed_phrase = seed_phrase
self.wordlist = wordlist
@@ -199,22 +207,57 @@ class SeedSSQR:
if self.wordlist == None:
raise Exception('Wordlist Required')
def seqLen(self):
return 1
def nextPart(self):
data = ""
# Output as Numeric data format
for word in self.seed_phrase:
index = self.wordlist.index(word)
data += str("%04d" % index)
return data
def isComplete(self):
return True
class XPubQR:
class CompactSeedQR(SeedQR):
def nextPart(self):
# Output as binary data format
binary_str = ""
for word in self.seed_phrase:
index = self.wordlist.index(word)
# Convert index to binary, strip out '0b' prefix; zero-pad to 11 bits
binary_str += bin(index).split('b')[1].zfill(11)
# We can exclude the checksum bits at the end
if len(self.seed_phrase) == 24:
# 8 checksum bits in a 24-word seed
binary_str = binary_str[:-8]
elif len(self.seed_phrase) == 12:
# 4 checksum bits in a 12-word seed
binary_str = binary_str[:-4]
# Now convert to bytes, 8 bits at a time
as_bytes = bytearray()
for i in range(0, math.ceil(len(binary_str) / 8)):
# int conversion reads byte data as a string prefixed with '0b'
as_bytes.append(int('0b' + binary_str[i*8:(i+1)*8], 2))
# Must return data as `bytes` for `qrcode` to properly recognize it as byte data
return bytes(as_bytes)
class XPubQR:
def __init__(self, seed_phrase, passphrase, derivation, network, wordlist):
self.seed_phrase = seed_phrase
self.passphrase = passphrase
@@ -228,7 +271,6 @@ class XPubQR:
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
@@ -313,4 +355,79 @@ class SpecterXPubQR(XPubQR):
return len(self.parts)
def isComplete(self):
return self.sent_complete
return self.sent_complete
class URXPubQR(XPubQR):
def __init__(self, seed_phrase, passphrase, derivation, network, qr_density, wordlist):
XPubQR.__init__(self, seed_phrase, passphrase, derivation, network, wordlist)
if qr_density == EncodeQRDensity.LOW:
self.qr_max_fragement_size = 10
elif qr_density == EncodeQRDensity.MEDIUM:
self.qr_max_fragement_size = 30
elif qr_density == EncodeQRDensity.HIGH:
self.qr_max_fragement_size = 120
def derivation_to_keypath(path: str) -> list:
arr = path.split("/")
if arr[0] == "m":
arr = arr[1:]
if len(arr) == 0:
return Keypath([],self.root.my_fingerprint, None)
if arr[-1] == "":
# trailing slash
arr = arr[:-1]
for i, e in enumerate(arr):
if e[-1] == "h" or e[-1] == "'":
arr[i] = PathComponent(int(e[:-1]), True)
else:
arr[i] = PathComponent(int(e), False)
return Keypath(arr, self.root.my_fingerprint, len(arr))
origin = derivation_to_keypath(derivation)
self.ur_hdkey = HDKey({ 'key': self.xpub.key.serialize(),
'chain_code': self.xpub.chain_code,
'origin': origin,
'parent_fingerprint': self.xpub.fingerprint})
ur_outputs = []
if len(origin.components) > 0:
if origin.components[0].index == 84: # Native Single Sig
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey))
elif origin.components[0].index == 49: # Nested Single Sig
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey))
elif origin.components[0].index == 48: # Multisig
if len(origin.components) >= 4:
if origin.components[3].index == 2: # Native Multisig
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey))
elif origin.components[3].index == 1: # Nested Multisig
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey))
# If empty, add all script types
if len(ur_outputs) == 0:
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey))
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey))
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey))
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey))
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[403]],self.ur_hdkey))
ur_account = Account(self.root.my_fingerprint, ur_outputs)
qr_ur_bytes = UR("crypto-account", ur_account.to_cbor())
self.ur2_encode = UREncoder(qr_ur_bytes, self.qr_max_fragement_size, 0)
def seqLen(self):
return self.ur2_encode.fountain_encoder.seq_len()
def nextPart(self) -> str:
return self.ur2_encode.next_part().upper()
def isComplete(self):
return self.ur2_encode.is_complete()
+3 -1
View File
@@ -11,12 +11,14 @@ class QRType(IntEnum):
PSBTURLEGACY = 3
PSBTBASE43 = 4
PSBTUR2 = 5
SEEDSSQR = 6
SEEDQR = 6
COMPACTSEEDQR = 10
SEEDUR2 = 7
SEEDMNEMONIC = 8
SEED4LETTERMNEMONIC = 9
XPUBQR = 20
SPECTERXPUBQR = 21
URXPUBQR = 22
BITCOINADDRESSQR = 30
SPECTERWALLETQR = 40
URWALLETQR = 41
+22 -4
View File
@@ -34,7 +34,8 @@ class Settings(Singleton):
'network': "main",
'software': "Specter Desktop",
'qr_density': EncodeQRDensity.MEDIUM,
'custom_derivation': 'm/0/0'
'custom_derivation': 'm/0/0',
'compact_seedqr_enabled': False,
}
}
@@ -47,16 +48,21 @@ class Settings(Singleton):
settings.init_complete = True
def __config_to_data(self, config):
# TODO: Make each get resilient to the possibility of the field being missing in
# the `settings.ini`
self.persistent = config.getboolean("system", "persistent_settings")
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.qr_background_color = config["display"]["qr_background_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.custom_derivation = config["wallet"]["custom_derivation"]
self.qr_background_color = config["display"]["qr_background_color"]
if "compact_seedqr_enabled"in config["wallet"]:
self.compact_seedqr_enabled = config.getboolean("wallet", "compact_seedqr_enabled")
### persistent settings handling
@@ -203,8 +209,10 @@ class Settings(Singleton):
def getXPubType(software):
if software == "Specter Desktop":
return QRType.SPECTERXPUBQR
else:
elif software == "Blue Wallet":
return QRType.XPUBQR
else:
return QRType.URXPUBQR
@property
def qr_density_name(self):
@@ -225,7 +233,17 @@ class Settings(Singleton):
def custom_derivation(self, value):
# TODO: parse and validate custom derivation path
self._data["wallet"]["custom_derivation"] = value
self.__writeConfig()
@property
def compact_seedqr_enabled(self):
return self._data["wallet"]["compact_seedqr_enabled"]
@compact_seedqr_enabled.setter
def compact_seedqr_enabled(self, value):
self._data["wallet"]["compact_seedqr_enabled"] = value
self.__writeConfig()
@staticmethod
def calc_derivation(network, wallet_type, script_type):
if network == "main":
+17 -8
View File
@@ -124,13 +124,20 @@ class MenuView(View):
### Settings Menu
def display_settings_menu(self) -> int:
lines = ["... [ Return to Main ]", "Wallet: <wallet>", "Network: <network>", "QR Density: <density>", "Input / Output Tests", "Persistent Settings: <persistent>", "Camera Rotation", "Version Info", "Donate to SeedSigner", "Reset SeedSigner"]
lines = [
"... [ Return to Main ]",
f"Wallet: {Settings.get_instance().software}",
f"Network: {Settings.get_instance().network}",
f"QR Density: {Settings.get_instance().qr_density_name}",
"Input / Output Tests",
f"Persistent Settings: {Settings.get_instance().persistent_display}",
f"Camera Rotation: {Settings.get_instance().camera_rotation}°",
f"Compact SeedQR: {'Enabled' if Settings.get_instance().compact_seedqr_enabled else 'Disabled'}",
"Version Info",
"Donate to SeedSigner",
"Reset SeedSigner"
]
input = 0
lines[1] = lines[1].replace("<wallet>", Settings.get_instance().software)
lines[2] = lines[2].replace("<network>", Settings.get_instance().network)
lines[3] = lines[3].replace("<density>", Settings.get_instance().qr_density_name)
lines[5] = lines[5].replace("<persistent>", Settings.get_instance().persistent_display)
# Draw Menu
self.selected_menu_num = 1
@@ -159,10 +166,12 @@ class MenuView(View):
elif self.selected_menu_num == 7:
return Path.CAMERA_ROTATION
elif self.selected_menu_num == 8:
return Path.VERSION_INFO
return Path.COMPACT_SEEDQR_ENABLED
elif self.selected_menu_num == 9:
return Path.DONATE
return Path.VERSION_INFO
elif self.selected_menu_num == 10:
return Path.DONATE
elif self.selected_menu_num == 11:
return Path.RESET
raise Exception("Unhandled case")
+74 -19
View File
@@ -1,6 +1,8 @@
# External Dependencies
from re import L
from PIL import ImageDraw, Image
from PIL.ImageOps import autocontrast
import hashlib
import math
import os
@@ -965,7 +967,10 @@ class SeedToolsView(View):
elif show_qr_option and ret_val == B.KEY_RIGHT:
# Show the resulting seed as a transcribable QR code
self.seed_phrase_as_qr(seed_phrase)
if self.controller.settings.compact_seedqr_enabled:
self.display_seed_qr_options(seed_phrase)
else:
self.seed_phrase_as_qr(seed_phrase)
# Signal success to move forward
return True
@@ -992,7 +997,10 @@ class SeedToolsView(View):
elif show_qr_option and ret_val == B.KEY_RIGHT:
# Show the resulting seed as a transcribable QR code
self.seed_phrase_as_qr(seed_phrase)
if self.controller.settings.compact_seedqr_enabled:
self.display_seed_qr_options(seed_phrase)
else:
self.seed_phrase_as_qr(seed_phrase)
# Signal success to move forward
return True
@@ -1004,9 +1012,38 @@ class SeedToolsView(View):
return True
def seed_phrase_as_qr(self, seed_phrase):
e = EncodeQR(seed_phrase=seed_phrase, qr_type=QRType.SEEDSSQR, wordlist=self.controller.settings.wordlist)
image = e.nextPartImage(240, 240, 3)
def display_seed_qr_options(self, seed_phrase) -> int:
if len(seed_phrase) == 24:
standard = "29x29"
compact = "25x25"
else:
standard = "25x25"
compact = "21x21"
r = self.controller.menu_view.display_generic_selection_menu(
["... [ Return to Seed Tools ]", f"Standard SeedQR: {standard}", f"Compact SeedQR: {compact}"],
"Transcribe SeedQR"
)
if r == 2:
return self.seed_phrase_as_qr(seed_phrase, is_compact_seedqr=False)
elif r == 3:
return self.seed_phrase_as_qr(seed_phrase, is_compact_seedqr=True)
else:
return None
def seed_phrase_as_qr(self, seed_phrase, is_compact_seedqr=False):
if is_compact_seedqr:
e = EncodeQR(seed_phrase=seed_phrase, qr_type=QRType.COMPACTSEEDQR, wordlist=self.controller.settings.wordlist)
else:
e = EncodeQR(seed_phrase=seed_phrase, qr_type=QRType.SEEDQR, wordlist=self.controller.settings.wordlist)
data = e.nextPart()
qr = QR()
image = qr.qrimage(
data=data,
width=240,
height=240,
border=3,
style=QR.STYLE__ROUNDED).convert("RGBA")
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])
@@ -1016,15 +1053,35 @@ class SeedToolsView(View):
elif input == B.KEY_PRESS:
# Render an oversized QR code that we can view up close
pixels_per_block = 24
qr_border = 4
width = (qr_border + 25 + qr_border) * pixels_per_block
height = width
# Border must accommodate the 3 blocks outside the center 5x5 mask plus up to
# 4 empty blocks inside the 5x5 mask (21x21 has a 1-block final col/row).
qr_border = 7
qr_blocks_per_zoom = 5
if len(seed_phrase) == 24:
width = (qr_border + 29 + qr_border) * pixels_per_block
height = width
if is_compact_seedqr:
num_modules = 25
else:
num_modules = 29
else:
if is_compact_seedqr:
num_modules = 21
# Optimize for 21x21
qr_blocks_per_zoom = 7
else:
num_modules = 25
width = (qr_border + num_modules + qr_border) * pixels_per_block
height = width
data = e.nextPart()
qr = QR()
image = qr.qrimage(data, width=width, height=height, border=qr_border).convert("RGBA")
image = qr.qrimage(
data,
width=width,
height=height,
border=qr_border,
style=QR.STYLE__ROUNDED).convert("RGBA")
# Render gridlines but leave the 1-block border as-is
draw = ImageDraw.Draw(image)
@@ -1037,8 +1094,8 @@ class SeedToolsView(View):
block_mask = Image.new("RGBA", (View.canvas_width, View.canvas_height), (255,255,255,0))
draw = ImageDraw.Draw(block_mask)
mask_width = int((View.canvas_width - 5 * pixels_per_block)/2)
mask_height = int((View.canvas_height - 5 * pixels_per_block)/2)
mask_width = int((View.canvas_width - qr_blocks_per_zoom * pixels_per_block)/2)
mask_height = int((View.canvas_height - qr_blocks_per_zoom * pixels_per_block)/2)
mask_rgba = (0, 0, 0, 226)
draw.rectangle((0, 0, View.canvas_width, mask_height), fill=mask_rgba)
draw.rectangle((0, View.canvas_height - mask_height - 1, View.canvas_width, View.canvas_height), fill=mask_rgba)
@@ -1094,29 +1151,27 @@ class SeedToolsView(View):
)
while True:
# 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:
next_x = cur_x + 5 * pixels_per_block
next_x = cur_x + qr_blocks_per_zoom * pixels_per_block
cur_block_x += 1
if next_x > width - View.canvas_width:
next_x = cur_x
cur_block_x -= 1
elif input == B.KEY_LEFT:
next_x = cur_x - 5 * pixels_per_block
next_x = cur_x - qr_blocks_per_zoom * pixels_per_block
cur_block_x -= 1
if next_x < 0:
next_x = cur_x
cur_block_x += 1
elif input == B.KEY_DOWN:
next_y = cur_y + 5 * pixels_per_block
next_y = cur_y + qr_blocks_per_zoom * pixels_per_block
cur_block_y += 1
if next_y > height - View.canvas_height:
next_y = cur_y
cur_block_y -= 1
elif input == B.KEY_UP:
next_y = cur_y - 5 * pixels_per_block
next_y = cur_y - qr_blocks_per_zoom * pixels_per_block
cur_block_y -= 1
if next_y < 0:
next_y = cur_y
+21 -7
View File
@@ -64,7 +64,6 @@ class SettingsToolsView(View):
return None
def display_persistent_settings(self) -> bool:
lines = ["... [ Return to Settings ]"]
lines.append("Yes")
lines.append("No")
@@ -82,10 +81,10 @@ class SettingsToolsView(View):
def display_camera_rotation(self) -> int:
lines = ["... [ Return to Settings ]"]
lines.append("0 (default)")
lines.append("90")
lines.append("180")
lines.append("270")
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:
@@ -123,7 +122,6 @@ class SettingsToolsView(View):
###
### Custom Derivation Path
###
def draw_derivation_keyboard_entry(self, existing_derivation = "m/"):
def render_right_panel():
row_height = 28
@@ -273,4 +271,20 @@ class SettingsToolsView(View):
# Render the text entry display and cursor block
text_entry_display.render(self.derivation)
View.DispShowImage()
View.DispShowImage()
def display_compact_seedqr_enabled(self) -> bool:
lines = ["... [ Return to Settings ]"]
lines.append("Enable")
lines.append("Disable")
r = self.controller.menu_view.display_generic_selection_menu(lines, "Compact SeedQR")
if r == 1:
return None
elif r == 2:
return True
elif r == 3:
return False
else:
return None
+2
View File
@@ -94,6 +94,8 @@ class View:
# 240x240 display with hardware SPI:
View.bus = 0
View.device = 0
# TODO: Add `disable_hardware` option for test suite here, too?
View.disp = ST7789(SPI.SpiDev(View.bus, View.device),View.RST, View.DC, View.BL)
View.disp.Init()
+3 -2
View File
@@ -5,11 +5,12 @@ persistent_settings = False
[display]
text_color = ORANGE
qr_background_color = FFFFFF
qr_background_color = 555555
camera_rotation = 0
[wallet]
network = main
software = Prompt
qr_density = 2
custom_derivation = m/0/0
custom_derivation = m/0/0
compact_seedqr_enabled = False