mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-06 07:28:24 +00:00
Merge pull request #494 from kdmukai/qr_encoding_refactor
QR Encoder refactor + fountain encoder `restart()`
This commit is contained in:
@@ -10,6 +10,7 @@ from seedsigner.gui.components import (GUIConstants,
|
||||
from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
from seedsigner.hardware.buttons import HardwareButtonsConstants, HardwareButtons
|
||||
from seedsigner.models.encode_qr import BaseQrEncoder
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
|
||||
|
||||
@@ -657,10 +658,10 @@ class LargeButtonScreen(BaseTopNavScreen):
|
||||
|
||||
@dataclass
|
||||
class QRDisplayScreen(BaseScreen):
|
||||
qr_encoder: 'EncodeQR' = None
|
||||
qr_encoder: BaseQrEncoder = None
|
||||
|
||||
class QRDisplayThread(BaseThread):
|
||||
def __init__(self, qr_encoder: 'EncodeQR', qr_brightness: ThreadsafeCounter, renderer: Renderer,
|
||||
def __init__(self, qr_encoder: BaseQrEncoder, qr_brightness: ThreadsafeCounter, renderer: Renderer,
|
||||
tips_start_time: ThreadsafeCounter):
|
||||
super().__init__()
|
||||
self.qr_encoder = qr_encoder
|
||||
|
||||
@@ -90,6 +90,7 @@ class FountainEncoder:
|
||||
self.fragment_len = FountainEncoder.find_nominal_fragment_length(self.message_len, min_fragment_len, max_fragment_len)
|
||||
self.fragments = FountainEncoder.partition_message(message, self.fragment_len)
|
||||
self.seq_num = first_seq_num
|
||||
self.current_part: Part = None
|
||||
|
||||
@staticmethod
|
||||
def find_nominal_fragment_length(message_len, min_fragment_len, max_fragment_len):
|
||||
@@ -143,7 +144,17 @@ class FountainEncoder:
|
||||
indexes = choose_fragments(self.seq_num, self.seq_len(), self.checksum)
|
||||
mixed = self.mix(indexes)
|
||||
data = bytes(mixed)
|
||||
return Part(self.seq_num, self.seq_len(), self.message_len, self.checksum, data)
|
||||
self.current_part = Part(self.seq_num, self.seq_len(), self.message_len, self.checksum, data)
|
||||
return self.current_part
|
||||
|
||||
|
||||
def restart(self):
|
||||
"""
|
||||
Restart from the beginning; each cycle's first n frames are full data frames
|
||||
(not XOR composites).
|
||||
"""
|
||||
self.seq_num = 0
|
||||
|
||||
|
||||
def mix(self, indexes):
|
||||
result = [0] * self.fragment_len
|
||||
|
||||
@@ -34,12 +34,26 @@ class UREncoder:
|
||||
def is_single_part(self):
|
||||
return self.fountain_encoder.is_single_part()
|
||||
|
||||
def next_part(self):
|
||||
part = self.fountain_encoder.next_part()
|
||||
def next_part(self) -> str:
|
||||
if self.is_single_part():
|
||||
return UREncoder.encode(self.ur)
|
||||
else:
|
||||
part = self.fountain_encoder.next_part()
|
||||
return UREncoder.encode_part(self.ur.type, part)
|
||||
|
||||
def current_part(self) -> str:
|
||||
if self.is_single_part():
|
||||
return UREncoder.encode(self.ur)
|
||||
else:
|
||||
part = self.fountain_encoder.current_part
|
||||
if not part:
|
||||
part = self.fountain_encoder.next_part()
|
||||
return UREncoder.encode_part(self.ur.type, part)
|
||||
|
||||
|
||||
def restart(self):
|
||||
self.fountain_encoder.restart()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def encode_part(type, part):
|
||||
|
||||
+222
-361
@@ -2,7 +2,7 @@ import math
|
||||
|
||||
from embit import bip32
|
||||
from embit.networks import NETWORKS
|
||||
from binascii import b2a_base64, hexlify
|
||||
from binascii import hexlify
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
from embit import bip32
|
||||
@@ -11,7 +11,6 @@ from embit.psbt import PSBT
|
||||
from seedsigner.helpers.ur2.ur_encoder import UREncoder
|
||||
from seedsigner.helpers.ur2.ur import UR
|
||||
from seedsigner.helpers.qr import QR
|
||||
from seedsigner.models.qr_type import QRType
|
||||
from seedsigner.models.seed import Seed
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
|
||||
@@ -20,149 +19,62 @@ from urtypes.crypto import Account, HDKey, Output, Keypath, PathComponent, SCRIP
|
||||
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class EncodeQR:
|
||||
"""
|
||||
Encode psbt for displaying as qr image
|
||||
"""
|
||||
# TODO: Refactor so that this is a base class with implementation classes for each
|
||||
# QR type. No reason exterior code can't directly instantiate the encoder it needs.
|
||||
|
||||
# Dataclass input vars on __init__()
|
||||
psbt: PSBT = None
|
||||
seed_phrase: List[str] = None
|
||||
passphrase: str = None
|
||||
derivation: str = None
|
||||
network: str = SettingsConstants.MAINNET
|
||||
qr_type: str = None
|
||||
class BaseQrEncoder:
|
||||
qr_density: str = SettingsConstants.DENSITY__MEDIUM
|
||||
wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
|
||||
bitcoin_address: str = None
|
||||
signed_message: str = None
|
||||
|
||||
|
||||
def __post_init__(self):
|
||||
self.qr = QR()
|
||||
|
||||
if not self.qr_type:
|
||||
raise Exception('qr_type is required')
|
||||
|
||||
if self.qr_density == None:
|
||||
self.qr_density = SettingsConstants.DENSITY__MEDIUM
|
||||
|
||||
self.encoder: BaseQrEncoder = None
|
||||
|
||||
# PSBT formats
|
||||
if self.qr_type == QRType.PSBT__SPECTER:
|
||||
self.encoder = SpecterPsbtQrEncoder(psbt=self.psbt, qr_density=self.qr_density)
|
||||
|
||||
elif self.qr_type == QRType.PSBT__UR2:
|
||||
self.encoder = UrPsbtQrEncoder(psbt=self.psbt, qr_density=self.qr_density)
|
||||
|
||||
# XPUB formats
|
||||
elif self.qr_type == QRType.XPUB:
|
||||
self.encoder = XpubQrEncoder(
|
||||
seed_phrase=self.seed_phrase,
|
||||
passphrase=self.passphrase,
|
||||
derivation=self.derivation,
|
||||
network=self.network,
|
||||
wordlist_language_code=self.wordlist_language_code
|
||||
)
|
||||
|
||||
elif self.qr_type == QRType.XPUB__UR:
|
||||
self.encoder = UrXpubQrEncoder(
|
||||
qr_density=self.qr_density,
|
||||
seed_phrase=self.seed_phrase,
|
||||
passphrase=self.passphrase,
|
||||
derivation=self.derivation,
|
||||
network=self.network,
|
||||
wordlist_language_code=self.wordlist_language_code
|
||||
)
|
||||
|
||||
elif self.qr_type == QRType.XPUB__SPECTER:
|
||||
self.encoder = SpecterXPubQrEncoder(
|
||||
qr_density=self.qr_density,
|
||||
seed_phrase=self.seed_phrase,
|
||||
passphrase=self.passphrase,
|
||||
derivation=self.derivation,
|
||||
network=self.network,
|
||||
wordlist_language_code=self.wordlist_language_code
|
||||
)
|
||||
|
||||
|
||||
# SeedQR formats
|
||||
elif self.qr_type == QRType.SEED__SEEDQR:
|
||||
self.encoder = SeedQrEncoder(seed_phrase=self.seed_phrase,
|
||||
wordlist_language_code=self.wordlist_language_code)
|
||||
|
||||
elif self.qr_type == QRType.SEED__COMPACTSEEDQR:
|
||||
self.encoder = CompactSeedQrEncoder(seed_phrase=self.seed_phrase,
|
||||
wordlist_language_code=self.wordlist_language_code)
|
||||
|
||||
# Misc formats
|
||||
elif self.qr_type == QRType.BITCOIN_ADDRESS:
|
||||
self.encoder = BitcoinAddressEncoder(address=self.bitcoin_address)
|
||||
|
||||
elif self.qr_type == QRType.SIGN_MESSAGE:
|
||||
self.encoder = SignedMessageEncoder(signed_message=self.signed_message)
|
||||
|
||||
else:
|
||||
raise Exception('QR Type not supported')
|
||||
|
||||
|
||||
def total_parts(self) -> int:
|
||||
return self.encoder.seq_len()
|
||||
|
||||
|
||||
def next_part(self):
|
||||
return self.encoder.next_part()
|
||||
|
||||
|
||||
def part_to_image(self, part, width=240, height=240, border=3):
|
||||
return self.qr.qrimage_io(part, width, height, border)
|
||||
|
||||
|
||||
def next_part_image(self, width=240, height=240, border=3, background_color="bdbdbd"):
|
||||
part = self.next_part()
|
||||
if self.qr_type == QRType.SEED__SEEDQR:
|
||||
return self.qr.qrimage(part, width, height, border)
|
||||
else:
|
||||
return self.qr.qrimage_io(part, width, height, border, background_color=background_color)
|
||||
|
||||
|
||||
# TODO: Make these properties?
|
||||
def is_complete(self):
|
||||
return self.encoder.is_complete
|
||||
|
||||
|
||||
def get_qr_density(self):
|
||||
return self.qr_density
|
||||
|
||||
|
||||
def get_qr_type(self):
|
||||
return self.qr_type
|
||||
|
||||
|
||||
|
||||
class BaseQrEncoder:
|
||||
def seq_len(self):
|
||||
raise Exception("Not implemented in child class")
|
||||
|
||||
def next_part(self) -> str:
|
||||
raise Exception("Not implemented in child class")
|
||||
|
||||
@property
|
||||
def is_complete(self):
|
||||
raise Exception("Not implemented in child class")
|
||||
|
||||
@property
|
||||
def qr_max_fragment_size(self):
|
||||
raise Exception("Not implemented in child class")
|
||||
|
||||
def seq_len(self):
|
||||
raise Exception("Not implemented in child class")
|
||||
|
||||
def next_part(self) -> str:
|
||||
raise Exception("Not implemented in child class")
|
||||
|
||||
def cur_part(self) -> str:
|
||||
raise Exception("Not implemented in child class")
|
||||
|
||||
def restart(self):
|
||||
# only used by animated QR encoders
|
||||
pass
|
||||
|
||||
def _create_parts(self):
|
||||
raise Exception("Not implemented in child class")
|
||||
|
||||
|
||||
def part_to_image(self, part, width, height, border: int = 3, background_color: str = "ffffff"):
|
||||
return self.qr.qrimage_io(part, width, height, border, background_color=background_color)
|
||||
|
||||
|
||||
def next_part_image(self, width=240, height=240, border=3, background_color="bdbdbd"):
|
||||
part = self.next_part()
|
||||
return self.part_to_image(part, width, height, border, background_color=background_color)
|
||||
|
||||
|
||||
|
||||
|
||||
"""**************************************************************************************
|
||||
STATIC QR encoders
|
||||
**************************************************************************************"""
|
||||
@dataclass
|
||||
class BaseStaticQrEncoder(BaseQrEncoder):
|
||||
def seq_len(self):
|
||||
return 1
|
||||
|
||||
def cur_part(self) -> str:
|
||||
""" static QRs only have a single part, which `next_part` always returns """
|
||||
return self.next_part()
|
||||
|
||||
|
||||
@property
|
||||
@@ -171,86 +83,130 @@ class BaseStaticQrEncoder(BaseQrEncoder):
|
||||
|
||||
|
||||
|
||||
class BasePsbtQrEncoder(BaseQrEncoder):
|
||||
def __init__(self, psbt: PSBT):
|
||||
self.psbt = psbt
|
||||
@dataclass
|
||||
class SeedQrEncoder(BaseStaticQrEncoder):
|
||||
mnemonic: List[str] = None
|
||||
wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
|
||||
|
||||
def __post_init__(self):
|
||||
self.wordlist = Seed.get_wordlist(self.wordlist_language_code)
|
||||
super().__post_init__()
|
||||
|
||||
self.data = ""
|
||||
# Output as Numeric data format
|
||||
for word in self.mnemonic:
|
||||
index = self.wordlist.index(word)
|
||||
self.data += str("%04d" % index)
|
||||
|
||||
|
||||
def next_part(self):
|
||||
return self.data
|
||||
|
||||
|
||||
|
||||
class UrPsbtQrEncoder(BasePsbtQrEncoder):
|
||||
def __init__(self, psbt, qr_density):
|
||||
super().__init__(psbt)
|
||||
self.qr_max_fragment_size = 20
|
||||
@dataclass
|
||||
class CompactSeedQrEncoder(SeedQrEncoder):
|
||||
def next_part(self):
|
||||
# Output as binary data format
|
||||
binary_str = ""
|
||||
for word in self.mnemonic:
|
||||
index = self.wordlist.index(word)
|
||||
|
||||
# Convert index to binary, strip out '0b' prefix; zero-pad to 11 bits
|
||||
binary_str += bin(index).split('b')[1].zfill(11)
|
||||
|
||||
# We can exclude the checksum bits at the end
|
||||
if len(self.mnemonic) == 24:
|
||||
# 8 checksum bits in a 24-word seed
|
||||
binary_str = binary_str[:-8]
|
||||
|
||||
elif len(self.mnemonic) == 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))
|
||||
|
||||
qr_ur_bytes = UR("crypto-psbt", UR_PSBT(self.psbt.serialize()).to_cbor())
|
||||
|
||||
if qr_density == SettingsConstants.DENSITY__LOW:
|
||||
self.qr_max_fragment_size = 10
|
||||
elif qr_density == SettingsConstants.DENSITY__MEDIUM:
|
||||
self.qr_max_fragment_size = 30
|
||||
elif qr_density == SettingsConstants.DENSITY__HIGH:
|
||||
self.qr_max_fragment_size = 120
|
||||
|
||||
self.ur2_encode = UREncoder(ur=qr_ur_bytes, max_fragment_len=self.qr_max_fragment_size)
|
||||
# Must return data as `bytes` for `qrcode` to properly recognize it as byte data
|
||||
return bytes(as_bytes)
|
||||
|
||||
|
||||
def seq_len(self):
|
||||
return self.ur2_encode.fountain_encoder.seq_len()
|
||||
|
||||
@dataclass
|
||||
class GenericStaticQrEncoder(BaseStaticQrEncoder):
|
||||
data: str = None
|
||||
|
||||
def next_part(self):
|
||||
return self.data
|
||||
|
||||
|
||||
def next_part(self) -> str:
|
||||
return self.ur2_encode.next_part().upper()
|
||||
|
||||
@dataclass
|
||||
class BaseXpubQrEncoder(BaseQrEncoder):
|
||||
"""
|
||||
Base Xpub QrEncoder for static and animated formats
|
||||
"""
|
||||
mnemonic: list = None
|
||||
passphrase: str = None
|
||||
derivation: str = None
|
||||
network: str = SettingsConstants.MAINNET
|
||||
wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
|
||||
|
||||
def prep_xpub(self):
|
||||
self.wordlist = Seed.get_wordlist(self.wordlist_language_code)
|
||||
|
||||
if self.wordlist == None:
|
||||
raise Exception('Wordlist Required')
|
||||
|
||||
version = bip32.detect_version(self.derivation, default="xpub", network=NETWORKS[SettingsConstants.map_network_to_embit(self.network)])
|
||||
self.seed = Seed(mnemonic=self.mnemonic,
|
||||
passphrase=self.passphrase,
|
||||
wordlist_language_code=self.wordlist_language_code)
|
||||
self.root = bip32.HDKey.from_seed(self.seed.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]["xprv"])
|
||||
self.fingerprint = self.root.child(0).fingerprint
|
||||
self.xprv = self.root.derive(self.derivation)
|
||||
self.xpub = self.xprv.to_public()
|
||||
self.xpub_base58 = self.xpub.to_string(version=version)
|
||||
|
||||
self.xpubstring = "[{}{}]{}".format(
|
||||
hexlify(self.fingerprint).decode('utf-8'),
|
||||
self.derivation[1:],
|
||||
self.xpub_base58
|
||||
)
|
||||
|
||||
|
||||
|
||||
class StaticXpubQrEncoder(BaseXpubQrEncoder, BaseStaticQrEncoder):
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
self.prep_xpub()
|
||||
|
||||
|
||||
def next_part(self):
|
||||
self.prep_xpub()
|
||||
return self.xpubstring
|
||||
|
||||
|
||||
|
||||
"""**************************************************************************************
|
||||
Simple animated QR encoders
|
||||
**************************************************************************************"""
|
||||
@dataclass
|
||||
class BaseSimpleAnimatedQREncoder(BaseQrEncoder):
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
self.parts = []
|
||||
self.part_num_sent = 0
|
||||
self.sent_complete = False
|
||||
self._create_parts()
|
||||
|
||||
|
||||
@property
|
||||
def is_complete(self):
|
||||
return self.ur2_encode.is_complete()
|
||||
|
||||
|
||||
|
||||
class SpecterPsbtQrEncoder(BasePsbtQrEncoder):
|
||||
def __init__(self, psbt, qr_density):
|
||||
super().__init__(psbt)
|
||||
self.qr_max_fragement_size = 65
|
||||
self.parts = []
|
||||
self.part_num_sent = 0
|
||||
self.sent_complete = False
|
||||
|
||||
if qr_density == SettingsConstants.DENSITY__LOW:
|
||||
self.qr_max_fragement_size = 40
|
||||
elif qr_density == SettingsConstants.DENSITY__MEDIUM:
|
||||
self.qr_max_fragement_size = 65
|
||||
elif qr_density == SettingsConstants.DENSITY__HIGH:
|
||||
self.qr_max_fragement_size = 90
|
||||
|
||||
self._create_parts()
|
||||
|
||||
|
||||
def _create_parts(self):
|
||||
base64_psbt = b2a_base64(self.psbt.serialize())
|
||||
|
||||
if base64_psbt[-1:] == b"\n":
|
||||
base64_psbt = base64_psbt[:-1]
|
||||
|
||||
base64_psbt = base64_psbt.decode('utf-8')
|
||||
|
||||
start = 0
|
||||
stop = self.qr_max_fragement_size
|
||||
qr_cnt = ((len(base64_psbt)-1) // self.qr_max_fragement_size) + 1
|
||||
|
||||
if qr_cnt == 1:
|
||||
self.parts.append(base64_psbt[start:stop])
|
||||
|
||||
cnt = 0
|
||||
while cnt < qr_cnt and qr_cnt != 1:
|
||||
part = "p" + str(cnt+1) + "of" + str(qr_cnt) + " " + base64_psbt[start:stop]
|
||||
self.parts.append(part)
|
||||
|
||||
start = start + self.qr_max_fragement_size
|
||||
stop = stop + self.qr_max_fragement_size
|
||||
if stop > len(base64_psbt):
|
||||
stop = len(base64_psbt)
|
||||
cnt += 1
|
||||
return self.sent_complete
|
||||
|
||||
|
||||
def seq_len(self):
|
||||
@@ -274,158 +230,34 @@ class SpecterPsbtQrEncoder(BasePsbtQrEncoder):
|
||||
return part
|
||||
|
||||
|
||||
@property
|
||||
def is_complete(self):
|
||||
return self.sent_complete
|
||||
def cur_part(self) -> str:
|
||||
if self.part_num_sent == 0:
|
||||
# Rewind all the way back to the end
|
||||
self.part_num_sent = len(self.parts) - 1
|
||||
else:
|
||||
self.part_num_sent -= 1
|
||||
return self.next_part()
|
||||
|
||||
|
||||
|
||||
class SeedQrEncoder(BaseStaticQrEncoder):
|
||||
def __init__(self, seed_phrase: List[str], wordlist_language_code: str):
|
||||
super().__init__()
|
||||
self.seed_phrase = seed_phrase
|
||||
self.wordlist = Seed.get_wordlist(wordlist_language_code)
|
||||
|
||||
if self.wordlist == None:
|
||||
raise Exception('Wordlist Required')
|
||||
|
||||
|
||||
def next_part(self):
|
||||
data = ""
|
||||
# Output as Numeric data format
|
||||
for word in self.seed_phrase:
|
||||
index = self.wordlist.index(word)
|
||||
data += str("%04d" % index)
|
||||
return data
|
||||
|
||||
|
||||
|
||||
class CompactSeedQrEncoder(SeedQrEncoder):
|
||||
def next_part(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 BitcoinAddressEncoder(BaseStaticQrEncoder):
|
||||
def __init__(self, address: str):
|
||||
super().__init__()
|
||||
self.address = address
|
||||
|
||||
|
||||
def next_part(self):
|
||||
return self.address
|
||||
|
||||
|
||||
|
||||
class SignedMessageEncoder(BaseStaticQrEncoder):
|
||||
"""
|
||||
Assumes that a signed message will fit in a single-frame QR
|
||||
"""
|
||||
def __init__(self, signed_message: str):
|
||||
super().__init__()
|
||||
self.signed_message = signed_message
|
||||
|
||||
|
||||
def next_part(self):
|
||||
return self.signed_message
|
||||
|
||||
|
||||
|
||||
class XpubQrEncoder(BaseQrEncoder):
|
||||
def __init__(self, seed_phrase, passphrase, derivation, network, wordlist_language_code):
|
||||
self.seed_phrase = seed_phrase
|
||||
self.passphrase = passphrase
|
||||
self.derivation = derivation
|
||||
self.network = network
|
||||
self.wordlist = Seed.get_wordlist(wordlist_language_code)
|
||||
self.parts = []
|
||||
def restart(self) -> str:
|
||||
self.part_num_sent = 0
|
||||
self.sent_complete = False
|
||||
|
||||
if self.wordlist == None:
|
||||
raise Exception('Wordlist Required')
|
||||
|
||||
version = bip32.detect_version(self.derivation, default="xpub", network=NETWORKS[SettingsConstants.map_network_to_embit(self.network)])
|
||||
self.seed = Seed(mnemonic=self.seed_phrase,
|
||||
passphrase=self.passphrase,
|
||||
wordlist_language_code=wordlist_language_code)
|
||||
self.root = bip32.HDKey.from_seed(self.seed.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]["xprv"])
|
||||
self.fingerprint = self.root.child(0).fingerprint
|
||||
self.xprv = self.root.derive(self.derivation)
|
||||
self.xpub = self.xprv.to_public()
|
||||
self.xpub_base58 = self.xpub.to_string(version=version)
|
||||
|
||||
self.xpubstring = "[{}{}]{}".format(
|
||||
hexlify(self.fingerprint).decode('utf-8'),
|
||||
self.derivation[1:],
|
||||
self.xpub_base58
|
||||
)
|
||||
|
||||
self._create_parts()
|
||||
|
||||
|
||||
def _create_parts(self):
|
||||
self.parts = []
|
||||
self.parts.append(self.xpubstring)
|
||||
|
||||
|
||||
def next_part(self) -> str:
|
||||
if len(self.parts) > 0:
|
||||
self.sent_complete = True
|
||||
return self.parts[0]
|
||||
|
||||
|
||||
def seq_len(self):
|
||||
return len(self.parts)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SpecterXPubQrEncoder(BaseSimpleAnimatedQREncoder, BaseXpubQrEncoder):
|
||||
@property
|
||||
def is_complete(self):
|
||||
return self.sent_complete
|
||||
|
||||
|
||||
|
||||
class SpecterXPubQrEncoder(XpubQrEncoder):
|
||||
def __init__(self, qr_density, **kwargs):
|
||||
# Must set up qr_max_fragment_size before calling super().__init__()
|
||||
self.qr_max_fragment_size = 65
|
||||
if qr_density == SettingsConstants.DENSITY__LOW:
|
||||
self.qr_max_fragment_size = 40
|
||||
elif qr_density == SettingsConstants.DENSITY__MEDIUM:
|
||||
self.qr_max_fragment_size = 65
|
||||
elif qr_density == SettingsConstants.DENSITY__HIGH:
|
||||
self.qr_max_fragment_size = 90
|
||||
|
||||
super().__init__(**kwargs)
|
||||
def qr_max_fragment_size(self):
|
||||
density_mapping = {
|
||||
SettingsConstants.DENSITY__LOW: 40,
|
||||
SettingsConstants.DENSITY__MEDIUM: 65,
|
||||
SettingsConstants.DENSITY__HIGH: 90,
|
||||
}
|
||||
return density_mapping.get(self.qr_density, 65)
|
||||
|
||||
|
||||
def _create_parts(self):
|
||||
self.parts = []
|
||||
|
||||
self.prep_xpub()
|
||||
start = 0
|
||||
stop = self.qr_max_fragment_size
|
||||
qr_cnt = ((len(self.xpubstring)-1) // self.qr_max_fragment_size) + 1
|
||||
@@ -445,34 +277,60 @@ class SpecterXPubQrEncoder(XpubQrEncoder):
|
||||
cnt += 1
|
||||
|
||||
|
||||
|
||||
"""**************************************************************************************
|
||||
Fountain encoded animated QR encoders
|
||||
**************************************************************************************"""
|
||||
@dataclass
|
||||
class BaseFountainQrEncoder(BaseQrEncoder):
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
|
||||
self.ur2_encode: UREncoder = None
|
||||
|
||||
|
||||
@property
|
||||
def is_complete(self):
|
||||
return self.ur2_encode.is_complete()
|
||||
|
||||
|
||||
@property
|
||||
def qr_max_fragment_size(self):
|
||||
density_mapping = {
|
||||
SettingsConstants.DENSITY__LOW: 10,
|
||||
SettingsConstants.DENSITY__MEDIUM: 30,
|
||||
SettingsConstants.DENSITY__HIGH: 120,
|
||||
}
|
||||
return density_mapping.get(self.qr_density, 30)
|
||||
|
||||
|
||||
def _create_parts(self):
|
||||
""" parts are dynamically generated by the fountain encoder """
|
||||
pass
|
||||
|
||||
|
||||
def seq_len(self):
|
||||
return self.ur2_encode.fountain_encoder.seq_len()
|
||||
|
||||
|
||||
def next_part(self) -> str:
|
||||
# if part num sent is gt number of parts, start at 0
|
||||
if self.part_num_sent > (len(self.parts) - 1):
|
||||
self.part_num_sent = 0
|
||||
return self.ur2_encode.next_part().upper()
|
||||
|
||||
part = self.parts[self.part_num_sent]
|
||||
|
||||
# when parts sent eq num of parts in list
|
||||
if self.part_num_sent == (len(self.parts) - 1):
|
||||
self.sent_complete = True
|
||||
def cur_part(self) -> str:
|
||||
return self.ur2_encode.current_part().upper()
|
||||
|
||||
|
||||
# increment to next part
|
||||
self.part_num_sent += 1
|
||||
|
||||
return part
|
||||
def restart(self):
|
||||
self.ur2_encode.fountain_encoder.restart()
|
||||
|
||||
|
||||
|
||||
class UrXpubQrEncoder(XpubQrEncoder):
|
||||
def __init__(self, qr_density, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if qr_density == SettingsConstants.DENSITY__LOW:
|
||||
self.qr_max_fragment_size = 10
|
||||
elif qr_density == SettingsConstants.DENSITY__MEDIUM:
|
||||
self.qr_max_fragment_size = 30
|
||||
elif qr_density == SettingsConstants.DENSITY__HIGH:
|
||||
self.qr_max_fragment_size = 120
|
||||
@dataclass
|
||||
class UrXpubQrEncoder(BaseFountainQrEncoder, BaseXpubQrEncoder):
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
self.prep_xpub()
|
||||
|
||||
def derivation_to_keypath(path: str) -> list:
|
||||
arr = path.split("/")
|
||||
@@ -530,9 +388,12 @@ class UrXpubQrEncoder(XpubQrEncoder):
|
||||
self.ur2_encode = UREncoder(ur=qr_ur_bytes, max_fragment_len=self.qr_max_fragment_size)
|
||||
|
||||
|
||||
def seq_len(self):
|
||||
return self.ur2_encode.fountain_encoder.seq_len()
|
||||
|
||||
@dataclass
|
||||
class UrPsbtQrEncoder(BaseFountainQrEncoder):
|
||||
psbt: PSBT = None
|
||||
|
||||
def next_part(self) -> str:
|
||||
return self.ur2_encode.next_part().upper()
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
qr_ur_bytes = UR("crypto-psbt", UR_PSBT(self.psbt.serialize()).to_cbor())
|
||||
self.ur2_encode = UREncoder(ur=qr_ur_bytes, max_fragment_len=self.qr_max_fragment_size)
|
||||
|
||||
@@ -4,9 +4,8 @@ from embit.networks import NETWORKS
|
||||
from seedsigner.controller import Controller
|
||||
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants
|
||||
from seedsigner.models.encode_qr import EncodeQR
|
||||
from seedsigner.models.encode_qr import UrPsbtQrEncoder
|
||||
from seedsigner.models.psbt_parser import PSBTParser
|
||||
from seedsigner.models.qr_type import QRType
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
from seedsigner.gui.screens.psbt_screens import PSBTOverviewScreen, PSBTMathScreen, PSBTAddressDetailsScreen, PSBTChangeDetailsScreen, PSBTFinalizeScreen
|
||||
from seedsigner.gui.screens.screen import (RET_CODE__BACK_BUTTON, ButtonListScreen, WarningScreen, DireWarningScreen, QRDisplayScreen)
|
||||
@@ -498,11 +497,9 @@ class PSBTFinalizeView(View):
|
||||
|
||||
class PSBTSignedQRDisplayView(View):
|
||||
def run(self):
|
||||
qr_encoder = EncodeQR(
|
||||
qr_encoder = UrPsbtQrEncoder(
|
||||
psbt=self.controller.psbt,
|
||||
qr_type=QRType.PSBT__UR2, # All coordinators (as of 2022-08) use this format
|
||||
qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY),
|
||||
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE),
|
||||
)
|
||||
self.run_screen(QRDisplayScreen, qr_encoder=qr_encoder)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen,
|
||||
from seedsigner.gui.screens.screen import LargeIconStatusScreen, QRDisplayScreen
|
||||
from seedsigner.helpers import embit_utils
|
||||
from seedsigner.models.decode_qr import DecodeQR
|
||||
from seedsigner.models.encode_qr import EncodeQR
|
||||
from seedsigner.models.encode_qr import CompactSeedQrEncoder, GenericStaticQrEncoder, SeedQrEncoder, SpecterXPubQrEncoder, StaticXpubQrEncoder, UrXpubQrEncoder
|
||||
from seedsigner.models.psbt_parser import PSBTParser
|
||||
from seedsigner.models.qr_type import QRType
|
||||
from seedsigner.models.seed import InvalidSeedException, Seed
|
||||
@@ -863,35 +863,25 @@ class SeedExportXpubQRDisplayView(View):
|
||||
super().__init__()
|
||||
self.seed = self.controller.get_seed(seed_num)
|
||||
|
||||
qr_density = self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY)
|
||||
if coordinator == SettingsConstants.COORDINATOR__SPECTER_DESKTOP:
|
||||
qr_type = QRType.XPUB__SPECTER
|
||||
|
||||
elif coordinator == SettingsConstants.COORDINATOR__BLUE_WALLET:
|
||||
qr_type = QRType.XPUB
|
||||
|
||||
elif coordinator == SettingsConstants.COORDINATOR__KEEPER:
|
||||
qr_type = QRType.XPUB
|
||||
|
||||
elif coordinator == SettingsConstants.COORDINATOR__NUNCHUK:
|
||||
qr_type = QRType.XPUB__UR
|
||||
|
||||
# As of 2022-03-02 Nunchuk doesn't seem to support animated QRs for Xpub import
|
||||
qr_density = SettingsConstants.DENSITY__HIGH
|
||||
|
||||
else:
|
||||
qr_type = QRType.XPUB__UR
|
||||
|
||||
self.qr_encoder = EncodeQR(
|
||||
seed_phrase=self.seed.mnemonic_list,
|
||||
encoder_args = dict(
|
||||
mnemonic=self.seed.mnemonic_list,
|
||||
passphrase=self.seed.passphrase,
|
||||
derivation=derivation_path,
|
||||
network=self.settings.get_value(SettingsConstants.SETTING__NETWORK),
|
||||
qr_type=qr_type,
|
||||
qr_density=qr_density,
|
||||
wordlist_language_code=self.seed.wordlist_language_code
|
||||
qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY)
|
||||
)
|
||||
|
||||
if coordinator == SettingsConstants.COORDINATOR__SPECTER_DESKTOP:
|
||||
self.qr_encoder = SpecterXPubQrEncoder(**encoder_args)
|
||||
|
||||
elif coordinator in [SettingsConstants.COORDINATOR__BLUE_WALLET,
|
||||
SettingsConstants.COORDINATOR__KEEPER,
|
||||
SettingsConstants.COORDINATOR__NUNCHUK]:
|
||||
self.qr_encoder = StaticXpubQrEncoder(**encoder_args)
|
||||
|
||||
else:
|
||||
self.qr_encoder = UrXpubQrEncoder(**encoder_args)
|
||||
|
||||
|
||||
def run(self):
|
||||
self.run_screen(
|
||||
@@ -1391,11 +1381,13 @@ class SeedTranscribeSeedQRWholeQRView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
e = EncodeQR(
|
||||
seed_phrase=self.seed.mnemonic_list,
|
||||
qr_type=self.seedqr_format,
|
||||
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
|
||||
)
|
||||
encoder_args = dict(mnemonic=self.seed.mnemonic_list,
|
||||
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE))
|
||||
if self.seedqr_format == QRType.SEED__SEEDQR:
|
||||
e = SeedQrEncoder(**encoder_args)
|
||||
elif self.seedqr_format == QRType.SEED__COMPACTSEEDQR:
|
||||
e = CompactSeedQrEncoder(**encoder_args)
|
||||
|
||||
data = e.next_part()
|
||||
|
||||
ret = seed_screens.SeedTranscribeSeedQRWholeQRScreen(
|
||||
@@ -1426,11 +1418,13 @@ class SeedTranscribeSeedQRZoomedInView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
e = EncodeQR(
|
||||
seed_phrase=self.seed.mnemonic_list,
|
||||
qr_type=self.seedqr_format,
|
||||
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
|
||||
)
|
||||
encoder_args = dict(mnemonic=self.seed.mnemonic_list,
|
||||
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE))
|
||||
if self.seedqr_format == QRType.SEED__SEEDQR:
|
||||
e = SeedQrEncoder(**encoder_args)
|
||||
elif self.seedqr_format == QRType.SEED__COMPACTSEEDQR:
|
||||
e = CompactSeedQrEncoder(**encoder_args)
|
||||
|
||||
data = e.next_part()
|
||||
|
||||
if len(self.seed.mnemonic_list) == 24:
|
||||
@@ -2071,7 +2065,7 @@ class SeedSignMessageSignedMessageQRView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
qr_encoder = EncodeQR(qr_type=QRType.SIGN_MESSAGE, signed_message=self.signed_message)
|
||||
qr_encoder = GenericStaticQrEncoder(data=self.signed_message)
|
||||
|
||||
self.run_screen(
|
||||
QRDisplayScreen,
|
||||
|
||||
@@ -14,8 +14,7 @@ from seedsigner.gui.screens.tools_screens import (ToolsCalcFinalWordDoneScreen,
|
||||
ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen,
|
||||
ToolsImageEntropyLivePreviewScreen, ToolsAddressExplorerAddressTypeScreen)
|
||||
from seedsigner.helpers import embit_utils, mnemonic_generation
|
||||
from seedsigner.models.encode_qr import EncodeQR
|
||||
from seedsigner.models.qr_type import QRType
|
||||
from seedsigner.models.encode_qr import GenericStaticQrEncoder
|
||||
from seedsigner.models.seed import Seed
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
from seedsigner.views.seed_views import SeedDiscardView, SeedFinalizeView, SeedMnemonicEntryView, SeedOptionsView, SeedWordsWarningView, SeedExportXpubScriptTypeView
|
||||
@@ -695,7 +694,7 @@ class ToolsAddressExplorerAddressView(View):
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.screen import QRDisplayScreen
|
||||
qr_encoder = EncodeQR(qr_type=QRType.BITCOIN_ADDRESS, bitcoin_address=self.address)
|
||||
qr_encoder = GenericStaticQrEncoder(data=self.address)
|
||||
self.run_screen(
|
||||
QRDisplayScreen,
|
||||
qr_encoder=qr_encoder,
|
||||
|
||||
+23
-53
File diff suppressed because one or more lines are too long
@@ -1,6 +1,5 @@
|
||||
from base import FlowTest, FlowStep
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.views.view import MainMenuView
|
||||
from seedsigner.views import scan_views, seed_views, psbt_views
|
||||
|
||||
|
||||
+9
-11
@@ -1,23 +1,22 @@
|
||||
import os
|
||||
import pyzbar
|
||||
from embit import bip39
|
||||
from seedsigner.helpers.qr import QR
|
||||
from seedsigner.helpers.ur2.bytewords import decode
|
||||
from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus
|
||||
from seedsigner.models.encode_qr import EncodeQR
|
||||
from seedsigner.models.encode_qr import SeedQrEncoder, CompactSeedQrEncoder
|
||||
from seedsigner.models.qr_type import QRType
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
|
||||
|
||||
|
||||
def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type):
|
||||
""" Helper method to re-run multiple variations of the same encode/decode test """
|
||||
print(entropy)
|
||||
seed_phrase = bip39.mnemonic_from_bytes(entropy).split()
|
||||
print(seed_phrase)
|
||||
assert len(seed_phrase) == mnemonic_length
|
||||
mnemonic = bip39.mnemonic_from_bytes(entropy).split()
|
||||
assert len(mnemonic) == mnemonic_length
|
||||
|
||||
if qr_type == QRType.SEED__SEEDQR:
|
||||
e = SeedQrEncoder(mnemonic=mnemonic)
|
||||
elif qr_type == QRType.SEED__COMPACTSEEDQR:
|
||||
e = CompactSeedQrEncoder(mnemonic=mnemonic)
|
||||
|
||||
e = EncodeQR(seed_phrase=seed_phrase, qr_type=qr_type)
|
||||
data = e.next_part()
|
||||
print(data)
|
||||
|
||||
@@ -34,8 +33,7 @@ def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type):
|
||||
assert status == DecodeQRStatus.COMPLETE
|
||||
|
||||
decoded_seed_phrase = decoder.get_seed_phrase()
|
||||
print(decoded_seed_phrase)
|
||||
assert seed_phrase == decoded_seed_phrase
|
||||
assert mnemonic == decoded_seed_phrase
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user