From c570285e9425bd174c84e5f739b297acc0f6bce0 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 15 Sep 2023 18:35:24 -0500 Subject: [PATCH 1/8] fix xpub export coordinator selection --- src/seedsigner/views/seed_views.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index afd70e10d..5426ee64a 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -718,20 +718,25 @@ def run(self): args["coordinator"] = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[0] return Destination(SeedExportXpubWarningView, view_args=args, skip_current_view=True) + button_data = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS) + selected_menu_num = self.run_screen( ButtonListScreen, title="Export Xpub", is_button_text_centered=False, - button_data=self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS), + button_data=button_data, ) - if selected_menu_num < len(self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)): - args["coordinator"] = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[selected_menu_num] - return Destination(SeedExportXpubWarningView, view_args=args) - - elif selected_menu_num == RET_CODE__BACK_BUTTON: + if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) + coordinators_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__COORDINATORS) + selected_display_name = button_data[selected_menu_num] + args["coordinator"] = coordinators_settings_entry.get_selection_option_value_by_display_name(selected_display_name) + + return Destination(SeedExportXpubWarningView, view_args=args) + + class SeedExportXpubWarningView(View): From 165f7b2a2724baa79cad50e96b21fe91af20a7b7 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 15 Sep 2023 18:15:12 -0500 Subject: [PATCH 2/8] interim commit --- src/seedsigner/gui/screens/screen.py | 10 +- src/seedsigner/helpers/qr_encoders.py | 443 ++++++++++++++ .../helpers/ur2/fountain_encoder.py | 4 +- src/seedsigner/helpers/ur2/ur_encoder.py | 14 +- src/seedsigner/models/encode_qr.py | 538 ------------------ src/seedsigner/views/psbt_views.py | 7 +- src/seedsigner/views/seed_views.py | 64 +-- src/seedsigner/views/tools_views.py | 5 +- tests/test_encodepsbtqr.py | 74 +-- tests/test_flows_psbt.py | 1 - tests/test_seedqr.py | 20 +- 11 files changed, 529 insertions(+), 651 deletions(-) create mode 100644 src/seedsigner/helpers/qr_encoders.py delete mode 100644 src/seedsigner/models/encode_qr.py diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index b229857da..ca9da429a 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -10,6 +10,7 @@ from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay from seedsigner.gui.renderer import Renderer from seedsigner.hardware.buttons import HardwareButtonsConstants, HardwareButtons +from seedsigner.helpers.qr_encoders import BaseQrEncoder from seedsigner.models.settings import SettingsConstants from seedsigner.models.threads import BaseThread, ThreadsafeCounter @@ -657,10 +658,10 @@ def swap_selected_button(new_selected_button: int): @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 @@ -751,12 +752,15 @@ def run(self): while self.keep_running: # convert the self.qr_brightness integer (31-255) into hex triplets hex_color = (hex(self.qr_brightness.cur_count).split('x')[1]) * 3 - image = self.qr_encoder.next_part_image(240, 240, border=2, background_color=hex_color) # Display the brightness tips toast duration = 10 ** 9 * 1.2 # 1.2 seconds if show_brightness_tips and time.time_ns() - self.tips_start_time.cur_count < duration: + image = self.qr_encoder.part_to_image(self.qr_encoder.cur_part(), 240, 240, border=2, background_color=hex_color) self.add_brightness_tips(image) + else: + # Only advance the QR animation when the brightness tip is not displayed + image = self.qr_encoder.next_part_image(240, 240, border=2, background_color=hex_color) with self.renderer.lock: self.renderer.show_image(image) diff --git a/src/seedsigner/helpers/qr_encoders.py b/src/seedsigner/helpers/qr_encoders.py new file mode 100644 index 000000000..0a72c764c --- /dev/null +++ b/src/seedsigner/helpers/qr_encoders.py @@ -0,0 +1,443 @@ +import math + +from embit import bip32 +from embit.networks import NETWORKS +from binascii import b2a_base64, hexlify +from dataclasses import dataclass +from typing import List +from embit import bip32 +from embit.networks import NETWORKS +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 + +from urtypes.crypto import PSBT as UR_PSBT +from urtypes.crypto import Account, HDKey, Output, Keypath, PathComponent, SCRIPT_EXPRESSION_TAG_MAP + + + +@dataclass +class BaseQrEncoder: + qr_density: str = SettingsConstants.DENSITY__MEDIUM + + + def __post_init__(self): + print("BaseQrEncoder.__post_init__") + self.qr = QR() + + + @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 _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"): + # 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) + 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 + def is_complete(self): + return True + + + +@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 + + + +@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)) + + # Must return data as `bytes` for `qrcode` to properly recognize it as byte data + return bytes(as_bytes) + + + +@dataclass +class GenericStaticQrEncoder(BaseStaticQrEncoder): + data: str = None + + def next_part(self): + return self.data + + + +@dataclass +class BaseXpubQrEncoder(BaseQrEncoder): + """ + Base Xpub QrEncoder for static and animated formats + """ + seed_phrase: list = None + passphrase: str = None + derivation: str = None + network: str = SettingsConstants.MAINNET + wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH + + def prep_xpub(self): + print("BaseXpubQrEncoder.prep_xpub") + 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.seed_phrase, + 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.sent_complete + + + def seq_len(self): + return len(self.parts) + + + 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 + + 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 + + # increment to next part + self.part_num_sent += 1 + + return part + + + 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() + + + +@dataclass +class SpecterXPubQrEncoder(BaseSimpleAnimatedQREncoder, BaseXpubQrEncoder): + @property + 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.prep_xpub() + start = 0 + stop = self.qr_max_fragment_size + qr_cnt = ((len(self.xpubstring)-1) // self.qr_max_fragment_size) + 1 + + if qr_cnt == 1: + self.parts.append(self.xpubstring[start:stop]) + + cnt = 0 + while cnt < qr_cnt and qr_cnt != 1: + part = "p" + str(cnt+1) + "of" + str(qr_cnt) + " " + self.xpubstring[start:stop] + self.parts.append(part) + + start = start + self.qr_max_fragment_size + stop = stop + self.qr_max_fragment_size + if stop > len(self.xpubstring): + stop = len(self.xpubstring) + cnt += 1 + + + +# !! This format is no longer used !! +# @dataclass +# class SpecterPsbtQrEncoder(BaseSimpleAnimatedQREncoder, BaseQrEncoder): +# psbt: PSBT +# qr_density: str + +# def __post_init__(self): +# super().__post_init__() +# if self.qr_density == SettingsConstants.DENSITY__LOW: +# self.qr_max_fragement_size = 40 +# elif self.qr_density == SettingsConstants.DENSITY__MEDIUM: +# self.qr_max_fragement_size = 65 +# elif self.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 + + + +"""************************************************************************************** + Fountain encoded animated QR encoders +**************************************************************************************""" +@dataclass +class BaseFountainQrEncoder(BaseQrEncoder): + def __post_init__(self): + super().__post_init__() + print("BaseFountainQrEncoder __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: + return self.ur2_encode.next_part().upper() + + + def cur_part(self) -> str: + return self.ur2_encode.current_part().upper() + + + +@dataclass +class UrXpubQrEncoder(BaseFountainQrEncoder, BaseXpubQrEncoder): + def __post_init__(self): + super().__post_init__() + print("UrXpubQrEncoder __post_init__") + + self.prep_xpub() + + 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(self.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)) + elif origin.components[0].index == 86: # P2TR + ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[409]],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(ur=qr_ur_bytes, max_fragment_len=self.qr_max_fragment_size) + + + +@dataclass +class UrPsbtQrEncoder(BaseFountainQrEncoder): + psbt: PSBT = None + + 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) diff --git a/src/seedsigner/helpers/ur2/fountain_encoder.py b/src/seedsigner/helpers/ur2/fountain_encoder.py index ac43bb62c..45888efd2 100644 --- a/src/seedsigner/helpers/ur2/fountain_encoder.py +++ b/src/seedsigner/helpers/ur2/fountain_encoder.py @@ -90,6 +90,7 @@ def __init__(self, message, max_fragment_len, first_seq_num = 0, min_fragment_le 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,8 @@ def next_part(self): 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 mix(self, indexes): result = [0] * self.fragment_len diff --git a/src/seedsigner/helpers/ur2/ur_encoder.py b/src/seedsigner/helpers/ur2/ur_encoder.py index ac1cbc7d0..f08364133 100644 --- a/src/seedsigner/helpers/ur2/ur_encoder.py +++ b/src/seedsigner/helpers/ur2/ur_encoder.py @@ -34,12 +34,22 @@ def is_complete(self): 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) + @staticmethod def encode_part(type, part): diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py deleted file mode 100644 index 6efa715db..000000000 --- a/src/seedsigner/models/encode_qr.py +++ /dev/null @@ -1,538 +0,0 @@ -import math - -from embit import bip32 -from embit.networks import NETWORKS -from binascii import b2a_base64, hexlify -from dataclasses import dataclass -from typing import List -from embit import bip32 -from embit.networks import NETWORKS -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 - -from urtypes.crypto import PSBT as UR_PSBT -from urtypes.crypto import Account, HDKey, Output, Keypath, PathComponent, SCRIPT_EXPRESSION_TAG_MAP - - - - -@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 - 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") - - def _create_parts(self): - raise Exception("Not implemented in child class") - - - -class BaseStaticQrEncoder(BaseQrEncoder): - def seq_len(self): - return 1 - - - @property - def is_complete(self): - return True - - - -class BasePsbtQrEncoder(BaseQrEncoder): - def __init__(self, psbt: PSBT): - self.psbt = psbt - - - -class UrPsbtQrEncoder(BasePsbtQrEncoder): - def __init__(self, psbt, qr_density): - super().__init__(psbt) - self.qr_max_fragment_size = 20 - - 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) - - - def seq_len(self): - return self.ur2_encode.fountain_encoder.seq_len() - - - def next_part(self) -> str: - return self.ur2_encode.next_part().upper() - - - @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 - - - def seq_len(self): - return len(self.parts) - - - 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 - - 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 - - # increment to next part - self.part_num_sent += 1 - - return part - - - @property - def is_complete(self): - return self.sent_complete - - - -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 = [] - 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) - - - @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 _create_parts(self): - self.parts = [] - - start = 0 - stop = self.qr_max_fragment_size - qr_cnt = ((len(self.xpubstring)-1) // self.qr_max_fragment_size) + 1 - - if qr_cnt == 1: - self.parts.append(self.xpubstring[start:stop]) - - cnt = 0 - while cnt < qr_cnt and qr_cnt != 1: - part = "p" + str(cnt+1) + "of" + str(qr_cnt) + " " + self.xpubstring[start:stop] - self.parts.append(part) - - start = start + self.qr_max_fragment_size - stop = stop + self.qr_max_fragment_size - if stop > len(self.xpubstring): - stop = len(self.xpubstring) - cnt += 1 - - - 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 - - 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 - - # increment to next part - self.part_num_sent += 1 - - return part - - - -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 - - 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(self.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)) - elif origin.components[0].index == 86: # P2TR - ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[409]],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(ur=qr_ur_bytes, max_fragment_len=self.qr_max_fragment_size) - - - def seq_len(self): - return self.ur2_encode.fountain_encoder.seq_len() - - - def next_part(self) -> str: - return self.ur2_encode.next_part().upper() diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index 9c0d9c60f..fdb94473d 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -4,9 +4,8 @@ from seedsigner.controller import Controller from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants -from seedsigner.models.encode_qr import EncodeQR +from seedsigner.helpers.qr_encoders 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 @@ def run(self): 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) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 5426ee64a..183b1a360 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -16,7 +16,7 @@ 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.helpers.qr_encoders 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 @@ def __init__(self, seed_num: int, coordinator: str, derivation_path: str): 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( + encoder_args = dict( seed_phrase=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 @@ def __init__(self, seed_num: int, seedqr_format: str, num_modules: int): 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 @@ def __init__(self, seed_num: int, seedqr_format: str): 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(seed_phrase=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 @@ def __init__(self): 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, diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 12a4493b3..47b71fa1a 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -14,8 +14,7 @@ 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.helpers.qr_encoders 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 @@ def __init__(self, index: int, address: str, is_change: bool, start_index: int, 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, diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py index 75ed796fb..c17653553 100644 --- a/tests/test_encodepsbtqr.py +++ b/tests/test_encodepsbtqr.py @@ -1,5 +1,4 @@ -from seedsigner.models.encode_qr import EncodeQR -from seedsigner.models.qr_type import QRType +from seedsigner.helpers.qr_encoders import CompactSeedQrEncoder, SeedQrEncoder, SpecterXPubQrEncoder, StaticXpubQrEncoder, UrPsbtQrEncoder, UrXpubQrEncoder from embit import psbt from binascii import a2b_base64 @@ -7,75 +6,47 @@ -def test_ur_qr_encode(): +def test_ur_psbt_qr_encode(): base64_psbt = "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" tx = psbt.PSBT.parse(a2b_base64(base64_psbt)) - e = EncodeQR(psbt=tx, qr_type=QRType.PSBT__UR2) + e = UrPsbtQrEncoder(psbt=tx, qr_density=SettingsConstants.DENSITY__MEDIUM) cnt = 0 while cnt <= 10: fragment = e.next_part() - img = e.part_to_image(fragment) - cnt += 1 - - - -def test_specter_qr_encode(): - base64_psbt = "cHNidP8BAIkCAAAAAaLlQ/VRNpx3IFtoRTOCnq2xfJwg/n7R9XB0TTTnlX/UHQAAAAD9////AtzQAwAAAAAAIgAgCwVSg4Ae1lGNHzy76jLN6GSQaSVnktnmNDByu/wkn7FQwwAAAAAAACIAIJyFZJe7xxQjXpoEBhb8mIkau9OhobDS7xbYxnRIjJUSAAAAAE8BBIiyHgQFgrfagAAAAqP8rWjHFRBmTEWK39AFjd6Wo1sw1UxlgIvROVHUOHbiAzre+t61zOqKFV1xXtDPuUcQRh3M92zh0Zar8rDLPJKQFH7fnFkwAACAAAAAgAAAAIACAACATwEEiLIeBFIg7+eAAAACubwMfJNby3zfn9owhFfgl/Xe/GiHciMMxxB9v6q7BWcCurV9rH+K8ucVU3w52mcEttDldz7kh5cS0xBtWs7wmTYU4IEbazAAAIAAAACAAAAAgAIAAIBPAQSIsh4EX+8GLoAAAALvSlncnGchVCfK7tnzHPVYcBRcck0JGQuspGFpcGP+YQIAXYODa8PIF3hOOnUeYHhlv4PQ+UZCYynQCOoKgVJRhhQYTQfrMAAAgAAAAIAAAACAAgAAgE8BBIiyHgRgkAVVgAAAAsgLKl/ahhLHvS/3Cth+9Hde12MHJO5PP8REKtbWkqONAvETqIlMPWJ/f1uBvSCGFm+zzDYnnEBtuAYjZiQrzj9mFLQz4JUwAACAAAAAgAAAAIACAACATwEEiLIeBLQlJwmAAAAC4IOLeQD9ojcPbh5QGsPVUt/g+dCiQrlZ1DvZK21ajf8CN4aND6VGGhYiFtI9NNyna/M03ovmM4PSg3nR7Df9jsoUhSswjzAAAIAAAACAAAAAgAIAAIBPAQSIsh4EwYVAaYAAAAKvbrl5PeuwgEBUqMQqBYTaTR+PUfKrOXzPQ87VbyLgXwMFEpYG8cv4ljYX+uebG0hJLXsD8K9Lc9K2RqaBmFOtyBQ+RR7+MAAAgAAAAIAAAACAAgAAgAABAP0DDAIAAAADnI5jmO6QLNrEFUwjGd8ZaVBeFqwJGZ3APH1mGpO+GU1CAAAAAP////8tMJlbqdEddNCzmBnmZXSdFFfNTTzD8fd0L2l15pJNWwIAAAAA/////+zKvZECNrGUsrUdWJZnB42n6r1Rhi1XkTyPs/nHuhyoFAAAAAD/////WlvbBAAAAAAAF6kUoY7q7sUcfiktUmzDPQGi//fFvXyHb44BAAAAAAAXqRTHugXLmX3w/lCFhTkfalnedRIHrIeGWAIAAAAAABl2qRQUGQLrLqWokxaHzt65bE2Qle3FQYishdwFAAAAAAAZdqkUX4m0fmwPCUO6pcw8Zbx2YkykKIyIrNACJwAAAAAAFgAUuej6+oAPU186R0ACxtFVG1po+oU7EQUAAAAAABl2qRTGnZD1MfBn92OncmMkRD2Ea+fToYisEksCAAAAAAAXqRS8oz8RN18jt3aeydd/y+/StoEsdYcLajcAAAAAABepFHeYIqSnDz5GuBfk0XbjLOlqNF6Dhy4IFwAAAAAAGXapFDomdwaFLA0OfjLfhXXxGYcjwi94iKyGWAIAAAAAABl2qRSdfDacqPqO9CPL8VtX8vldzlBo6YisSVgCAAAAAAAWABR0DohsF88/3DalzIZyF2ZToibTSxXkLQAAAAAAGXapFPmlzMsqjXuIMCBczer3vGR+GX7KiKwyQAIAAAAAABl2qRTt97UCSLs90250ctaRfDmj6KvZzYis9QgXAAAAAAAWABS526HCIlmm4yis1TxaDNbeCCGyHkakAQAAAAAAF6kUIv85Uai5pFu94QXUYU6YV6ZY/HmHR8gBAAAAAAAZdqkU4cbRwoLlhic5Mx6SdsH8m8bF1nqIrNBvBgAAAAAAGXapFFBUM1drdEjYVzE3ZQOodhobVeNBiKwdSwIAAAAAABl2qRRV48mXLau6y2HwytGPyw/YWXIDR4isD+UGAAAAAAAZdqkUhup0yRlrxdycP9543gF4HEdYVX6IrEiVBAAAAAAAF6kU0xyHPo/mQeDTWX3uHecVQr3QNwmHLjUDAAAAAAAZdqkUR4aV0HjC/bVmOwfjXsbcMzVsbSKIrOqKBAAAAAAAFgAUEx8rhKUzD3fHWdK2v6R9xHvFqHpMhQIAAAAAABl2qRRIg2u4Ow74IDCcGKYnssKpOi2VeYisN6YbAAAAAAAXqRRCSDT9kY3HvkNQI480GmDcu8fffocMQQMAAAAAABl2qRQ83W+Njx6fiXXbXE0fFR9QfA0zJoisdggXAAAAAAAXqRQ9/3PVGaBETVlM+auG6MBXkqNa6YeQt04AAAAAABl2qRRHmF4qf2fYqxhuYPulQCJIhkKyo4isfY4BAAAAAAAXqRQb2OkwwU0kbjeTepBie2hH9Nyhy4d4oAIAAAAAABepFLxzGvLxJoys+l4fvCfHyKNfzx5XhzeVBAAAAAAAIgAgWE9I4MhYpgx9StM1jKekhXHNQ8ohBTlx4N8wbBvDeil4QAIAAAAAABepFJhDXqvM/jN8FZw/lHXkusDCJRTgh2q4AgAAAAAAF6kUtcjnRyRtAOawAnBvMygHecHHRCiHIIkLAAAAAAAZdqkU7X7qc767ZmR51ucTsc5G3uu4XDCIrNBnAwAAAAAAF6kU1SVFH4lwDMpvZEe/g4OXPA/R9EeHcIMDAAAAAAAXqRSGSbWdefvglK8rcFv861TKX7H4Lod63AUAAAAAABepFDspcMlIbqM0IOnk4iOp+VVWsIeIh1zsAQAAAAAAGXapFHrgjevUcXAC9y3FSqtB4O6YHBf9iKxiyAQAAAAAABYAFJWB2pija792dDLax+k7ko3rc3MTmdUBAAAAAAAWABTEd9Lq4RQ5DqWrFEJG0yGwTyNGVmzIAQAAAAAAF6kUvQ1oX0X+EqSr9nm5yVgTEqqwbBKH/jEJAAAAAAAWABS5Q9n7WwpD8V+DoUr1PhtaPjEAzpw4AwAAAAAAGXapFJV07D6tUzUodx2WS8O5Co4x365viKxedB8AAAAAABl2qRQwCg19nxZttgRYVNtqm634kcvwI4islXUXAAAAAAAXqRQdpv7S8UHAtUzhN9UjDzbA1r8l3odrcAMAAAAAABepFFY77ZQ2QH8SBYqU4lswOS2SM3NphxXvCQAAAAAAF6kUQIs3Q5sPU0ubPufbIGTl5aforUWHXtACAAAAAAAZdqkUomTncvcva43IhQjLUn/gkddAA4+IrL1sAwAAAAAAF6kUF0IHuQXB2WY+UKhOt2Fe1PP0YKeHziuGAwAAAAAWABQmwHI2oSXayEsODm4irKumczJcu2hZDQAAAAAAF6kUkEVhUQ33XDK3OqdRZDvT9lpyh5CHoNMJAAAAAAAXqRSoGqG/VTHq7TmPlXD2YYU8ih0HQYdqfgsAAAAAABl2qRTJ1SEupAnvPWOSxpcXFnbfVCx2r4issK0BAAAAAAAXqRTb7/iq9K3zhZIGk0VpFBtYiVJ724cDdRcAAAAAABl2qRRHkxnD6L4pYcssWJdqkDrkja7kmYisokoCAAAAAAAXqRT75VCujkWKFY/ifu/0Orj3JUV0Z4ezjAQAAAAAABYAFBpX8ddXQJ95Vy/v3zi+yYZVi/yglrAEAAAAAAAXqRSLFecvMVMAuxjHz6iSn0XpfQ98gIdNTRgAAAAAABepFB3Wn0gQsayX8cOkCtmSF/NRy08zh5QUBwAAAAAAGXapFLbjX9PnCBzJKViLkLVzyMwtVwNUiKyJSQUAAAAAABepFNDbsKF4ZizxSBuY7NHYnOEuPemxh9LxWwAAAAAAF6kUquecjseAlaEpHxPc83v8kGUFdkuHLzQIAAAAAAAXqRRX8vVVucqqFgH6mGJPEK+/reJ2oYd03AUAAAAAABepFBsBZvNXH4r6Ro8ojq4rmGTcNtBih9maAQAAAAAAGXapFF9oCUBEmn9pA1ddXZGZjsfEFsMOiKzEzwUAAAAAABl2qRRxJyHXAGx4sfRS9WH4eyJLHi+Wd4isLfotAAAAAAAXqRSORo5USPBTLgHEvyfKgfjCqMhnm4csWAIAAAAAABl2qRSRQ+PgB7THRZz/rts1ZV1kB3xCU4ishkoCAAAAAAAZdqkU6tpAL4E1Y53hpyNDyup0NNkIWZ2IrHaaAQAAAAAAF6kUdCiIVs9RAe6mhTnBrZ9rXDmBUwqHMRUHAAAAAAAWABRWDPxl5JVG+87QXnn6mxroeokXegbVCgAAAAAAF6kUc1nZFyA2yQQlxjG7wC8EmcwSYAaHkooLAAAAAAAZdqkUbWMloEkzLZaPkqmvj48ayjP24pWIrKXBCQAAAAAAF6kUhor0BRMQSHMrs8huHLt3PzkmwY+HLTQCAAAAAAAXqRRu2+r5RZ97rALhlGzLcTqXL0qWBYdtdQIAAAAAABepFOvvX6e4KHStEF9gAeP5sueSWoj8h8xKAgAAAAAAF6kUtCOpxVPaoJX6I6x4sYcxi0FRkZuHHEsCAAAAAAAXqRR0jXC8f5rOvLMnaCqNbFhgYV1VI4eynwUAAAAAABepFKk9GBH39jPYAijN98mQiXQLwO6th6KVBAAAAAAAF6kUxAXYvAMMGpeUwbSehQ6yl7PfBKaHhAwGAAAAAAAXqRSgEqI18gMoa8oDed3Nmw0e0JjANodsEAIAAAAAABl2qRSZlcV+iJuoM2F5GdNLhAmJB8LZkYisD9QBAAAAAAAXqRTltOpfMjLJA3a0569jL3OdK96kLYeQXgIAAAAAABl2qRRJxL+Ewl1I7R0UVRYyhvyTdhGt+Yisg0oCAAAAAAAZdqkUqzUSwEEJDrGszAlQNOTOyiXHGc6IrH0qCQAAAAAAF6kUA9gkZnXrwD3nSird5PjY/mKrjrKHrZUEAAAAAAAXqRR2GK6PRPCUdeDBifrkXqVW6OjTVocldRcAAAAAABYAFIALieV/hlNyLSnLzygXuapZ5ZWOeFACAAAAAAAXqRQzJK44f4kGcK0Mr67rQIf8V6K004eNCBcAAAAAABepFEPC9GKHNg91b0VHjiGqN9jskJBnh3wYBgAAAAAAF6kUac+U//Z6fP0Sd1hF+7H2spE6W3uHAAAAAAEBKzeVBAAAAAAAIgAgWE9I4MhYpgx9StM1jKekhXHNQ8ohBTlx4N8wbBvDeikBBc9UIQI90obbwglkzCu7YY5szpmsifPSjmmkMWB2zirsF7i5JSECXtSG8zlgDJHpslDlTL+/MPiyMHW404co4O9XwhrFJD4hAsaYDVoTjPJ1xm5KIpmVjO8AerWFj+0ij7ti1GkxvyI/IQMNJ5G2tHM6GGX9OMrL1a5LLFjx3eyHE9dG8/00BGJ6+yEDW0BA9BSig0YYQcMhaCQ5EgJhYPx0HfMNsknOEzNVBfkhA4/77ELJ9rT3+zhaRN/L3lk81Eie5dlCI15SuNT45ZV+Vq4iBgI90obbwglkzCu7YY5szpmsifPSjmmkMWB2zirsF7i5JRw+RR7+MAAAgAAAAIAAAACAAgAAgAAAAAABAAAAIgYCXtSG8zlgDJHpslDlTL+/MPiyMHW404co4O9XwhrFJD4c4IEbazAAAIAAAACAAAAAgAIAAIAAAAAAAQAAACIGAsaYDVoTjPJ1xm5KIpmVjO8AerWFj+0ij7ti1GkxvyI/HIUrMI8wAACAAAAAgAAAAIACAACAAAAAAAEAAAAiBgMNJ5G2tHM6GGX9OMrL1a5LLFjx3eyHE9dG8/00BGJ6+xwYTQfrMAAAgAAAAIAAAACAAgAAgAAAAAABAAAAIgYDW0BA9BSig0YYQcMhaCQ5EgJhYPx0HfMNsknOEzNVBfkctDPglTAAAIAAAACAAAAAgAIAAIAAAAAAAQAAACIGA4/77ELJ9rT3+zhaRN/L3lk81Eie5dlCI15SuNT45ZV+HH7fnFkwAACAAAAAgAAAAIACAACAAAAAAAEAAAAAAQHPVCEC5eStpJd5y6MpbkWgUYRhL6Sta3BAtONOSEC2uIXXIcEhAw5hli91LeHlLHv5WR6/xjfFTjCsXxE9MtO0wV/a7mTnIQMT9IzdgTJDxQ0CO5Ka1HcnXfbBnCdLN9NZrDKMf3Z+WSEDn6BiNDZ7YI//rSuZjrNIY0k0C3h7MBEur/nzJ7gVF08hA7UGbXn9OfXGcHLWujN7D1wpZqwQrOV49XIiJNtqr6dFIQPwycXFPO4Rf5xaNDQ1zryEERu4z+A3C6iz0+aKHfHq4VauIgIC5eStpJd5y6MpbkWgUYRhL6Sta3BAtONOSEC2uIXXIcEcGE0H6zAAAIAAAACAAAAAgAIAAIABAAAAAAAAACICAw5hli91LeHlLHv5WR6/xjfFTjCsXxE9MtO0wV/a7mTnHOCBG2swAACAAAAAgAAAAIACAACAAQAAAAAAAAAiAgMT9IzdgTJDxQ0CO5Ka1HcnXfbBnCdLN9NZrDKMf3Z+WRx+35xZMAAAgAAAAIAAAACAAgAAgAEAAAAAAAAAIgIDn6BiNDZ7YI//rSuZjrNIY0k0C3h7MBEur/nzJ7gVF08cPkUe/jAAAIAAAACAAAAAgAIAAIABAAAAAAAAACICA7UGbXn9OfXGcHLWujN7D1wpZqwQrOV49XIiJNtqr6dFHLQz4JUwAACAAAAAgAAAAIACAACAAQAAAAAAAAAiAgPwycXFPO4Rf5xaNDQ1zryEERu4z+A3C6iz0+aKHfHq4RyFKzCPMAAAgAAAAIAAAACAAgAAgAEAAAAAAAAAAAEBz1QhAqLp+NQOoYyma8paUW8hucqCdQu2VAZmFGMbV79csI7jIQKtZYJ+sgBVWQwp/xCIeS/x+/SZXAD4VHf56HFmnK9fkyECrvaSdw5m5ZxvwhF7/EbFGJP5MGIDhdbdcILAGsept4shAwlGvi1FP2ybbd5xYnQhz7Cvh2gWaTn5yvMVWm+Ev5keIQPCy/yDc1y1RCJYDMEy6UYkduq4Eq1dyLOoInv5xwsitSED0sEPo41jUtW51+oiJDQPHFt0scWX6aPHivum+kT7WBhWriICAqLp+NQOoYyma8paUW8hucqCdQu2VAZmFGMbV79csI7jHIUrMI8wAACAAAAAgAAAAIACAACAAAAAAAIAAAAiAgKtZYJ+sgBVWQwp/xCIeS/x+/SZXAD4VHf56HFmnK9fkxzggRtrMAAAgAAAAIAAAACAAgAAgAAAAAACAAAAIgICrvaSdw5m5ZxvwhF7/EbFGJP5MGIDhdbdcILAGsept4scGE0H6zAAAIAAAACAAAAAgAIAAIAAAAAAAgAAACICAwlGvi1FP2ybbd5xYnQhz7Cvh2gWaTn5yvMVWm+Ev5keHH7fnFkwAACAAAAAgAAAAIACAACAAAAAAAIAAAAiAgPCy/yDc1y1RCJYDMEy6UYkduq4Eq1dyLOoInv5xwsitRy0M+CVMAAAgAAAAIAAAACAAgAAgAAAAAACAAAAIgID0sEPo41jUtW51+oiJDQPHFt0scWX6aPHivum+kT7WBgcPkUe/jAAAIAAAACAAAAAgAIAAIAAAAAAAgAAAAA=" - - tx = psbt.PSBT.parse(a2b_base64(base64_psbt)) - - e = EncodeQR(psbt=tx, qr_type=QRType.PSBT__SPECTER) - - cnt = 0 - while cnt <= 10: - fragment = e.next_part() - if (cnt+1) == 1: - assert fragment == "p1of117 cHNidP8BAIkCAAAAAaLlQ/VRNpx3IFtoRTOCnq2xfJwg/n7R9XB0TTTnlX/UHQAAA" - elif (cnt+1) == 2: - assert fragment == "p2of117 AD9////AtzQAwAAAAAAIgAgCwVSg4Ae1lGNHzy76jLN6GSQaSVnktnmNDByu/wkn7" - elif (cnt+1) == 3: - assert fragment == "p3of117 FQwwAAAAAAACIAIJyFZJe7xxQjXpoEBhb8mIkau9OhobDS7xbYxnRIjJUSAAAAAE8" - elif (cnt+1) == 4: - assert fragment == "p4of117 BBIiyHgQFgrfagAAAAqP8rWjHFRBmTEWK39AFjd6Wo1sw1UxlgIvROVHUOHbiAzre" - elif (cnt+1) == 5: - assert fragment == "p5of117 +t61zOqKFV1xXtDPuUcQRh3M92zh0Zar8rDLPJKQFH7fnFkwAACAAAAAgAAAAIACA" - elif (cnt+1) == 6: - assert fragment == "p6of117 ACATwEEiLIeBFIg7+eAAAACubwMfJNby3zfn9owhFfgl/Xe/GiHciMMxxB9v6q7BW" - elif (cnt+1) == 7: - assert fragment == "p7of117 cCurV9rH+K8ucVU3w52mcEttDldz7kh5cS0xBtWs7wmTYU4IEbazAAAIAAAACAAAA" - elif (cnt+1) == 8: - assert fragment == "p8of117 AgAIAAIBPAQSIsh4EX+8GLoAAAALvSlncnGchVCfK7tnzHPVYcBRcck0JGQuspGFp" - elif (cnt+1) == 9: - assert fragment == "p9of117 cGP+YQIAXYODa8PIF3hOOnUeYHhlv4PQ+UZCYynQCOoKgVJRhhQYTQfrMAAAgAAAA" - elif (cnt+1) == 10: - assert fragment == "p10of117 IAAAACAAgAAgE8BBIiyHgRgkAVVgAAAAsgLKl/ahhLHvS/3Cth+9Hde12MHJO5PP8" - elif (cnt+1) == 11: - assert fragment == "p11of117 REKtbWkqONAvETqIlMPWJ/f1uBvSCGFm+zzDYnnEBtuAYjZiQrzj9mFLQz4JUwAAC" - - img = e.part_to_image(fragment) + e.part_to_image(fragment, 240, 240) cnt += 1 def test_seedsigner_qr(): - mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" + # test vector 1 from the SeedQR docs + mnemonic = "attack pizza motion avocado network gather crop fresh patrol unusual wild holiday candy pony ranch winter theme error hybrid van cereal salon goddess expire".split() + e = SeedQrEncoder(mnemonic=mnemonic) + assert e.next_part() == "011513251154012711900771041507421289190620080870026613431420201617920614089619290300152408010643" - e = EncodeQR(seed_phrase=mnemonic.split(), qr_type=QRType.SEED__SEEDQR) + e = CompactSeedQrEncoder(mnemonic=mnemonic) + assert e.next_part() == b'\x0et\xb6A\x07\xf9L\xc0\xcc\xfa\xe6\xa1=\xcb\xec6b\x15O\xecg\xe0\xe0\t\x99\xc0x\x92Y}\x19\n' - print(e.next_part()) + # test vector 4 from the SeedQR docs + mnemonic="forum undo fragile fade shy sign arrest garment culture tube off merit".split() + e = SeedQrEncoder(mnemonic=mnemonic) + assert e.next_part() == "073318950739065415961602009907670428187212261116" - assert e.next_part() == "121802020768124106400009195602431595117715840445" + e = CompactSeedQrEncoder(mnemonic=mnemonic) + assert e.next_part() == b'[\xbd\x9dq\xa8\xecy\x90\x83\x1a\xff5\x9dBeE' def test_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = EncodeQR(seed_phrase=mnemonic.split(), passphrase="pass", qr_type=QRType.XPUB, network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h") - + e = StaticXpubQrEncoder(seed_phrase=mnemonic.split(), + passphrase="pass", + derivation="m/48h/1h/0h/2h", + network=SettingsConstants.TESTNET) assert e.next_part() == "[c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDNc5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBoMxfrVggXj5NJmaNEToWpax8GMMucozvAdqf1bW1JsZsfdBzsK3VUC5" @@ -83,7 +54,7 @@ def test_xpub_qr(): def test_specter_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.XPUB__SPECTER, network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW) + e = SpecterXPubQrEncoder(seed_phrase=mnemonic.split(" "), passphrase="pass", network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW) assert e.next_part() == "p1of4 [c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDN" assert e.next_part() == "p2of4 c5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBo" @@ -95,10 +66,9 @@ def test_specter_xpub_qr(): def test_ur_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = EncodeQR( + e = UrXpubQrEncoder( seed_phrase=mnemonic.split(), passphrase="pass", - qr_type=QRType.XPUB__UR, network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__MEDIUM diff --git a/tests/test_flows_psbt.py b/tests/test_flows_psbt.py index 02b714463..93d4847d8 100644 --- a/tests/test_flows_psbt.py +++ b/tests/test_flows_psbt.py @@ -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 diff --git a/tests/test_seedqr.py b/tests/test_seedqr.py index 7e2d63856..472af661d 100644 --- a/tests/test_seedqr.py +++ b/tests/test_seedqr.py @@ -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.helpers.qr_encoders 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 From 7f11bfc8133c87321e0b54ba9b50d4662031f5d1 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 22 Sep 2023 19:23:42 -0500 Subject: [PATCH 3/8] Fountain encoder `restart`; cleanup --- src/seedsigner/helpers/qr_encoders.py | 58 ++++++--------------------- 1 file changed, 12 insertions(+), 46 deletions(-) diff --git a/src/seedsigner/helpers/qr_encoders.py b/src/seedsigner/helpers/qr_encoders.py index 0a72c764c..5dda69783 100644 --- a/src/seedsigner/helpers/qr_encoders.py +++ b/src/seedsigner/helpers/qr_encoders.py @@ -46,6 +46,10 @@ def next_part(self) -> str: 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") @@ -242,6 +246,10 @@ def cur_part(self) -> str: return self.next_part() + def restart(self) -> str: + self.part_num_sent = 0 + + @dataclass class SpecterXPubQrEncoder(BaseSimpleAnimatedQREncoder, BaseXpubQrEncoder): @@ -277,52 +285,6 @@ def _create_parts(self): -# !! This format is no longer used !! -# @dataclass -# class SpecterPsbtQrEncoder(BaseSimpleAnimatedQREncoder, BaseQrEncoder): -# psbt: PSBT -# qr_density: str - -# def __post_init__(self): -# super().__post_init__() -# if self.qr_density == SettingsConstants.DENSITY__LOW: -# self.qr_max_fragement_size = 40 -# elif self.qr_density == SettingsConstants.DENSITY__MEDIUM: -# self.qr_max_fragement_size = 65 -# elif self.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 - - - """************************************************************************************** Fountain encoded animated QR encoders **************************************************************************************""" @@ -365,6 +327,10 @@ def next_part(self) -> str: def cur_part(self) -> str: return self.ur2_encode.current_part().upper() + + + def restart(self): + self.ur2_encode.fountain_encoder.restart() From fdbe7777446d148b4b98c22ed58cd8289bf672b3 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 22 Sep 2023 19:24:18 -0500 Subject: [PATCH 4/8] Fountain encoder `restart` --- src/seedsigner/helpers/ur2/fountain_encoder.py | 9 +++++++++ src/seedsigner/helpers/ur2/ur_encoder.py | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/src/seedsigner/helpers/ur2/fountain_encoder.py b/src/seedsigner/helpers/ur2/fountain_encoder.py index 45888efd2..af5dd3384 100644 --- a/src/seedsigner/helpers/ur2/fountain_encoder.py +++ b/src/seedsigner/helpers/ur2/fountain_encoder.py @@ -146,6 +146,15 @@ def next_part(self): data = bytes(mixed) 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 diff --git a/src/seedsigner/helpers/ur2/ur_encoder.py b/src/seedsigner/helpers/ur2/ur_encoder.py index f08364133..082ad17b2 100644 --- a/src/seedsigner/helpers/ur2/ur_encoder.py +++ b/src/seedsigner/helpers/ur2/ur_encoder.py @@ -49,6 +49,10 @@ def current_part(self) -> str: 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 From f8392e05318cfeb80a0708d2ef0c4147c910b858 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 22 Sep 2023 19:37:50 -0500 Subject: [PATCH 5/8] remove experimental animated qr pause on brightness tip UI --- src/seedsigner/gui/screens/screen.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index ca9da429a..59fd24d9c 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -752,15 +752,12 @@ def run(self): while self.keep_running: # convert the self.qr_brightness integer (31-255) into hex triplets hex_color = (hex(self.qr_brightness.cur_count).split('x')[1]) * 3 + image = self.qr_encoder.next_part_image(240, 240, border=2, background_color=hex_color) # Display the brightness tips toast duration = 10 ** 9 * 1.2 # 1.2 seconds if show_brightness_tips and time.time_ns() - self.tips_start_time.cur_count < duration: - image = self.qr_encoder.part_to_image(self.qr_encoder.cur_part(), 240, 240, border=2, background_color=hex_color) self.add_brightness_tips(image) - else: - # Only advance the QR animation when the brightness tip is not displayed - image = self.qr_encoder.next_part_image(240, 240, border=2, background_color=hex_color) with self.renderer.lock: self.renderer.show_image(image) From 03a0e8a549275a95b3161ddf91f2150f6af80a5d Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 22 Sep 2023 19:48:43 -0500 Subject: [PATCH 6/8] rename `seed_phrase` argument to `mnemonic` --- src/seedsigner/helpers/qr_encoders.py | 4 ++-- src/seedsigner/views/seed_views.py | 4 ++-- tests/test_encodepsbtqr.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/seedsigner/helpers/qr_encoders.py b/src/seedsigner/helpers/qr_encoders.py index 5dda69783..3c722fcdf 100644 --- a/src/seedsigner/helpers/qr_encoders.py +++ b/src/seedsigner/helpers/qr_encoders.py @@ -155,7 +155,7 @@ class BaseXpubQrEncoder(BaseQrEncoder): """ Base Xpub QrEncoder for static and animated formats """ - seed_phrase: list = None + mnemonic: list = None passphrase: str = None derivation: str = None network: str = SettingsConstants.MAINNET @@ -169,7 +169,7 @@ def prep_xpub(self): 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, + 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"]) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 183b1a360..0b2cb88ae 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -864,7 +864,7 @@ def __init__(self, seed_num: int, coordinator: str, derivation_path: str): self.seed = self.controller.get_seed(seed_num) encoder_args = dict( - seed_phrase=self.seed.mnemonic_list, + mnemonic=self.seed.mnemonic_list, passphrase=self.seed.passphrase, derivation=derivation_path, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK), @@ -1418,7 +1418,7 @@ def __init__(self, seed_num: int, seedqr_format: str): def run(self): - encoder_args = dict(seed_phrase=self.seed.mnemonic_list, + 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) diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py index c17653553..35153d7ee 100644 --- a/tests/test_encodepsbtqr.py +++ b/tests/test_encodepsbtqr.py @@ -43,7 +43,7 @@ def test_seedsigner_qr(): def test_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = StaticXpubQrEncoder(seed_phrase=mnemonic.split(), + e = StaticXpubQrEncoder(mnemonic=mnemonic.split(), passphrase="pass", derivation="m/48h/1h/0h/2h", network=SettingsConstants.TESTNET) @@ -54,7 +54,7 @@ def test_xpub_qr(): def test_specter_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = SpecterXPubQrEncoder(seed_phrase=mnemonic.split(" "), passphrase="pass", network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW) + e = SpecterXPubQrEncoder(mnemonic=mnemonic.split(" "), passphrase="pass", network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW) assert e.next_part() == "p1of4 [c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDN" assert e.next_part() == "p2of4 c5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBo" @@ -67,7 +67,7 @@ def test_ur_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" e = UrXpubQrEncoder( - seed_phrase=mnemonic.split(), + mnemonic=mnemonic.split(), passphrase="pass", network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", From 98e9ca8fd7e75edc87335396cf2f238d6abc47fc Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 3 Mar 2024 09:08:58 -0600 Subject: [PATCH 7/8] Reverting encode_qr.py file move and name change --- src/seedsigner/gui/screens/screen.py | 2 +- .../{helpers/qr_encoders.py => models/encode_qr.py} | 8 +------- src/seedsigner/views/psbt_views.py | 2 +- src/seedsigner/views/seed_views.py | 2 +- src/seedsigner/views/tools_views.py | 2 +- tests/test_encodepsbtqr.py | 2 +- tests/test_seedqr.py | 2 +- 7 files changed, 7 insertions(+), 13 deletions(-) rename src/seedsigner/{helpers/qr_encoders.py => models/encode_qr.py} (97%) diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index 59fd24d9c..a4b3a7422 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -10,7 +10,7 @@ from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay from seedsigner.gui.renderer import Renderer from seedsigner.hardware.buttons import HardwareButtonsConstants, HardwareButtons -from seedsigner.helpers.qr_encoders import BaseQrEncoder +from seedsigner.models.encode_qr import BaseQrEncoder from seedsigner.models.settings import SettingsConstants from seedsigner.models.threads import BaseThread, ThreadsafeCounter diff --git a/src/seedsigner/helpers/qr_encoders.py b/src/seedsigner/models/encode_qr.py similarity index 97% rename from src/seedsigner/helpers/qr_encoders.py rename to src/seedsigner/models/encode_qr.py index 3c722fcdf..159d2be52 100644 --- a/src/seedsigner/helpers/qr_encoders.py +++ b/src/seedsigner/models/encode_qr.py @@ -2,7 +2,7 @@ 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 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 @@ -26,7 +25,6 @@ class BaseQrEncoder: def __post_init__(self): - print("BaseQrEncoder.__post_init__") self.qr = QR() @@ -162,7 +160,6 @@ class BaseXpubQrEncoder(BaseQrEncoder): wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH def prep_xpub(self): - print("BaseXpubQrEncoder.prep_xpub") self.wordlist = Seed.get_wordlist(self.wordlist_language_code) if self.wordlist == None: @@ -292,7 +289,6 @@ def _create_parts(self): class BaseFountainQrEncoder(BaseQrEncoder): def __post_init__(self): super().__post_init__() - print("BaseFountainQrEncoder __post_init__") self.ur2_encode: UREncoder = None @@ -338,8 +334,6 @@ def restart(self): class UrXpubQrEncoder(BaseFountainQrEncoder, BaseXpubQrEncoder): def __post_init__(self): super().__post_init__() - print("UrXpubQrEncoder __post_init__") - self.prep_xpub() def derivation_to_keypath(path: str) -> list: diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index fdb94473d..69e12073a 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -4,7 +4,7 @@ from seedsigner.controller import Controller from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants -from seedsigner.helpers.qr_encoders import UrPsbtQrEncoder +from seedsigner.models.encode_qr import UrPsbtQrEncoder from seedsigner.models.psbt_parser import PSBTParser from seedsigner.models.settings import SettingsConstants from seedsigner.gui.screens.psbt_screens import PSBTOverviewScreen, PSBTMathScreen, PSBTAddressDetailsScreen, PSBTChangeDetailsScreen, PSBTFinalizeScreen diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 0b2cb88ae..0df0889e3 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -16,7 +16,7 @@ from seedsigner.gui.screens.screen import LargeIconStatusScreen, QRDisplayScreen from seedsigner.helpers import embit_utils from seedsigner.models.decode_qr import DecodeQR -from seedsigner.helpers.qr_encoders import CompactSeedQrEncoder, GenericStaticQrEncoder, SeedQrEncoder, SpecterXPubQrEncoder, StaticXpubQrEncoder, UrXpubQrEncoder +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 diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 47b71fa1a..162c874ae 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -14,7 +14,7 @@ ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsAddressExplorerAddressTypeScreen) from seedsigner.helpers import embit_utils, mnemonic_generation -from seedsigner.helpers.qr_encoders import GenericStaticQrEncoder +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 diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py index 35153d7ee..8a8ec3688 100644 --- a/tests/test_encodepsbtqr.py +++ b/tests/test_encodepsbtqr.py @@ -1,4 +1,4 @@ -from seedsigner.helpers.qr_encoders import CompactSeedQrEncoder, SeedQrEncoder, SpecterXPubQrEncoder, StaticXpubQrEncoder, UrPsbtQrEncoder, UrXpubQrEncoder +from seedsigner.models.encode_qr import CompactSeedQrEncoder, SeedQrEncoder, SpecterXPubQrEncoder, StaticXpubQrEncoder, UrPsbtQrEncoder, UrXpubQrEncoder from embit import psbt from binascii import a2b_base64 diff --git a/tests/test_seedqr.py b/tests/test_seedqr.py index 472af661d..826cd9415 100644 --- a/tests/test_seedqr.py +++ b/tests/test_seedqr.py @@ -2,7 +2,7 @@ from embit import bip39 from seedsigner.helpers.qr import QR from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus -from seedsigner.helpers.qr_encoders import SeedQrEncoder, CompactSeedQrEncoder +from seedsigner.models.encode_qr import SeedQrEncoder, CompactSeedQrEncoder from seedsigner.models.qr_type import QRType From ea106532b99afefabe34f18ae2024a02cf9ef7bd Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 3 Mar 2024 14:37:33 -0600 Subject: [PATCH 8/8] minor cleanup --- src/seedsigner/models/encode_qr.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index 159d2be52..c368c05e9 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -54,10 +54,6 @@ def _create_parts(self): def part_to_image(self, part, width, height, border: int = 3, background_color: str = "ffffff"): - # 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) return self.qr.qrimage_io(part, width, height, border, background_color=background_color)