mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
Second attempt at a clean 0.4.6 merge
This commit is contained in:
@@ -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
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ class Path:
|
||||
QR_DENSITY_SETTING = 65
|
||||
PERSISTENT_SETTINGS = 67
|
||||
CAMERA_ROTATION = 69
|
||||
COMPACT_SEEDQR_ENABLED = 70
|
||||
RESET = 68
|
||||
|
||||
# Seed Slots
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user