From 946245fdc8e3df8458eda7eb47fd8d91808aecc7 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 30 Mar 2022 07:21:08 -0500 Subject: [PATCH 01/10] Denomination display options --- src/seedsigner/gui/components.py | 245 ++++++++++++++++++- src/seedsigner/gui/screens/psbt_screens.py | 45 +--- src/seedsigner/gui/screens/screen.py | 4 +- src/seedsigner/models/settings_definition.py | 22 +- 4 files changed, 276 insertions(+), 40 deletions(-) diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index d27640b9..ce723dce 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -1,12 +1,16 @@ import math +from operator import is_ import os import pathlib from dataclasses import dataclass +from decimal import Decimal from PIL import Image, ImageDraw, ImageFont, ImageFilter from typing import List, Tuple from seedsigner.models import Singleton +from seedsigner.models.settings import Settings +from seedsigner.models.settings_definition import SettingsConstants # TODO: Remove all pixel hard coding @@ -21,6 +25,8 @@ class GUIConstants: SUCCESS_COLOR = "#00dd00" BITCOIN_ORANGE = "#ff9416" ACCENT_COLOR = "orange" + TESTNET_COLOR = "#00f100" + REGTEST_COLOR = "#00caf1" ICON_FONT_NAME__FONT_AWESOME = "Font_Awesome_6_Free-Solid-900" ICON_FONT_NAME__SEEDSIGNER = "seedsigner-glyphs" @@ -203,8 +209,8 @@ class TextDoesNotFitException(Exception): @dataclass class BaseComponent: - image_draw: ImageDraw = None - canvas: Image = None + image_draw: ImageDraw.ImageDraw = None + canvas: Image.Image = None def __post_init__(self): from seedsigner.gui import Renderer @@ -533,6 +539,7 @@ class IconTextLine(BaseComponent): if self.icon_name: icon_y = self.screen_y + int((self.height - self.icon.height)/2) self.icon.screen_y = icon_y + self.height = self.icon.height if self.is_text_centered: if self.icon_name: @@ -546,6 +553,8 @@ class IconTextLine(BaseComponent): # self.label_textarea.screen_x = self.screen_x + int((self.canvas_width - self.screen_x - max_textarea_width + (max_textarea_width - self.label_textarea.width))/2) # self.value_textarea.screen_x = self.screen_x + int((self.canvas_width - self.screen_x - max_textarea_width + (max_textarea_width - self.value_textarea.width))/2) + self.width = self.canvas_width + def render(self): if self.label_textarea: @@ -729,6 +738,238 @@ class FormattedAddress(BaseComponent): +@dataclass +class BtcAmount(BaseComponent): + """ + Display btc value based on the SETTING__BTC_DENOMINATION Setting: + * btc: "B" icon + 8-decimal amount + "btc" (can truncate zero decimals to .0 or .09) + * sats: "B" icon + comma-separated amount + "sats" + * threshold: btc display at or above 0.01 btc; otherwise sats + * btcsatshybrd: "B" icon + 2-decimal amount + "|" + up to 6-digit, comma-separated sats + "sats" + """ + total_sats: int = None + icon_size: int = 34 + font_size: int = 24 + screen_x: int = 0 + screen_y: int = None + + + def __post_init__(self): + super().__post_init__() + self.sub_components: List[BaseComponent] = [] + self.paste_image: Image.Image = None + self.paste_coords = None + denomination = Settings.get_instance().get_value(SettingsConstants.SETTING__BTC_DENOMINATION) + network = Settings.get_instance().get_value(SettingsConstants.SETTING__NETWORK) + + print(self.total_sats) + + btc_unit = "tBtc" + sats_unit = "tSats" + if network == SettingsConstants.MAINNET: + btc_unit = "btc" + sats_unit = "sats" + btc_color = GUIConstants.ACCENT_COLOR + + elif network == SettingsConstants.TESTNET: + btc_color = GUIConstants.TESTNET_COLOR + + elif network == SettingsConstants.REGTEST: + btc_color = GUIConstants.REGTEST_COLOR + + digit_font = Fonts.get_font(font_name=GUIConstants.BODY_FONT_NAME, size=self.font_size) + smaller_digit_font = Fonts.get_font(font_name=GUIConstants.BODY_FONT_NAME, size=self.font_size - 2) + unit_font_size = GUIConstants.BUTTON_FONT_SIZE + 2 + + # Render to a temp surface + self.paste_image = Image.new(mode="RGB", size=(self.canvas_width, self.icon_size), color=GUIConstants.BACKGROUND_COLOR) + draw = ImageDraw.Draw(self.paste_image) + + # Render the circular Bitcoin icon + btc_icon = Icon( + image_draw=draw, + canvas=self.paste_image, + icon_name=SeedSignerCustomIconConstants.BITCOIN_LOGO, + icon_color=btc_color, + icon_size=self.icon_size, + screen_x=0, + screen_y=0, + ) + btc_icon.render() + cur_x = btc_icon.width + int(GUIConstants.COMPONENT_PADDING/4) + + if denomination == SettingsConstants.BTC_DENOMINATION__BTC or \ + (denomination == SettingsConstants.BTC_DENOMINATION__THRESHOLD and self.total_sats >= 1e6) or \ + (denomination == SettingsConstants.BTC_DENOMINATION__BTCSATSHYBRID and self.total_sats >= 1e6 and str(self.total_sats)[-6:] == "0" * 6) or \ + self.total_sats > 1e10: + decimal_btc = Decimal(self.total_sats / 1e8).quantize(Decimal("0.12345678")) + if str(self.total_sats)[-8:] == "0" * 8: + # Only whole btc units being displayed; truncate to a single decimal place + decimal_btc = decimal_btc.quantize(Decimal("0.1")) + + elif str(self.total_sats)[-6:] == "0" * 6: + # Bottom six digits are all zeroes; trucate to two decimal places + decimal_btc = decimal_btc.quantize(Decimal("0.12")) + + btc_text = f"{decimal_btc:,}" + + if len(btc_text) >= 12: + # This is a large btc value that won't fit; omit sats + btc_text = btc_text.split(".")[0] + "." + btc_text.split(".")[-1][:2] + "..." + + # Draw the btc side + font = digit_font + # if self.total_sats > 1e9: + # font = smaller_digit_font + + (left, top, text_width, bottom) = font.getbbox(btc_text, anchor="ls") + text_height = -1 * top + text_y = self.paste_image.height - int((self.paste_image.height - text_height)/2) + + draw.text( + xy=( + cur_x, + text_y + ), + font=font, + text=btc_text, + fill=GUIConstants.BODY_FONT_COLOR, + anchor="ls", + ) + cur_x += text_width + + unit_text = btc_unit + + elif denomination == SettingsConstants.BTC_DENOMINATION__SATS or \ + (denomination == SettingsConstants.BTC_DENOMINATION__THRESHOLD and self.total_sats < 1e6) or \ + (denomination == SettingsConstants.BTC_DENOMINATION__BTCSATSHYBRID and self.total_sats < 1e6): + # Draw the sats side + sats_text = f"{self.total_sats:,}" + + font = digit_font + if self.total_sats > 1e9: + font = smaller_digit_font + (left, top, text_width, bottom) = font.getbbox(sats_text, anchor="ls") + text_height = -1 * top + text_y = self.paste_image.height - int((self.paste_image.height - text_height)/2) + draw.text( + xy=( + cur_x, + text_y + ), + font=font, + text=sats_text, + fill=GUIConstants.BODY_FONT_COLOR, + anchor="ls", + ) + cur_x += text_width + + unit_text = sats_unit + + elif denomination == SettingsConstants.BTC_DENOMINATION__BTCSATSHYBRID: + decimal_btc = Decimal(self.total_sats / 1e8).quantize(Decimal("0.12345678")) + decimal_btc = Decimal(str(decimal_btc)[:-6]) + btc_text = f"{decimal_btc:,}" + sats_text = f"{self.total_sats:,}"[-7:] + while sats_text[0] == "0": + sats_text = sats_text[1:] + + btc_icon = Icon( + image_draw=draw, + canvas=self.paste_image, + icon_name=SeedSignerCustomIconConstants.BITCOIN_LOGO, + icon_color=btc_color, + icon_size=self.icon_size, + screen_x=0, + screen_y=0, + ) + btc_icon.render() + cur_x = btc_icon.width + int(GUIConstants.COMPONENT_PADDING/4) + + (left, top, text_width, bottom) = smaller_digit_font.getbbox(btc_text, anchor="ls") + text_height = -1 * top + text_y = self.paste_image.height - int((self.paste_image.height - text_height)/2) + + draw.text( + xy=( + cur_x, + text_y + ), + font=smaller_digit_font, + text=btc_text, + fill=GUIConstants.BODY_FONT_COLOR, + anchor="ls", + ) + cur_x += text_width - int(GUIConstants.COMPONENT_PADDING/2) + + # Draw the pipe separator + pipe_font = Fonts.get_font(font_name=GUIConstants.BODY_FONT_NAME, size=self.icon_size - 4) + (left, top, text_width, bottom) = pipe_font.getbbox("|", anchor="ls") + draw.text( + xy=( + cur_x, + text_y + ), + font=pipe_font, + text="|", + fill=btc_color, + anchor="ls", + ) + cur_x += text_width - int(GUIConstants.COMPONENT_PADDING/2) + + # Draw the sats side + (left, top, text_width, bottom) = smaller_digit_font.getbbox(sats_text, anchor="ls") + draw.text( + xy=( + cur_x, + text_y + ), + font=smaller_digit_font, + text=sats_text, + fill=GUIConstants.BODY_FONT_COLOR, + anchor="ls", + ) + cur_x += text_width + + unit_text = sats_unit + + # Draw the unit + unit_font = Fonts.get_font(font_name=GUIConstants.BODY_FONT_NAME, size=unit_font_size) + (left, top, unit_text_width, bottom) = unit_font.getbbox(unit_text, anchor="ls") + unit_font_height = -1 * top + + unit_textarea = TextArea( + image_draw=draw, + canvas=self.paste_image, + text=f" {unit_text}", + font_name=GUIConstants.BODY_FONT_NAME, + font_size=unit_font_size, + font_color=GUIConstants.BODY_FONT_COLOR, + supersampling_factor=2, + is_text_centered=False, + edge_padding=0, + screen_x=cur_x, + screen_y=text_y - unit_font_height, + ) + unit_textarea.render() + + final_x = cur_x + GUIConstants.COMPONENT_PADDING + unit_textarea.width + + self.paste_image = self.paste_image.crop((0, 0, final_x, self.paste_image.height)) + self.paste_coords = ( + int((self.canvas_width - final_x)/2), + self.screen_y + ) + + self.width = self.canvas_width + self.height = self.paste_image.height + + + def render(self): + self.canvas.paste(self.paste_image, self.paste_coords) + + + @dataclass class Button(BaseComponent): # TODO: Rename the seedsigner.helpers.Buttons class (to Inputs?) to reduce confusion diff --git a/src/seedsigner/gui/screens/psbt_screens.py b/src/seedsigner/gui/screens/psbt_screens.py index a51145e5..9cb482d2 100644 --- a/src/seedsigner/gui/screens/psbt_screens.py +++ b/src/seedsigner/gui/screens/psbt_screens.py @@ -7,7 +7,7 @@ from seedsigner.gui.renderer import Renderer from seedsigner.models.threads import BaseThread from .screen import ButtonListScreen, WarningScreen -from ..components import (Button, Icon, FontAwesomeIconConstants, IconTextLine, FormattedAddress, GUIConstants, Fonts, SeedSignerCustomIconConstants, TextArea, +from ..components import (BtcAmount, Button, Icon, FontAwesomeIconConstants, IconTextLine, FormattedAddress, GUIConstants, Fonts, SeedSignerCustomIconConstants, TextArea, calc_bezier_curve, linear_interp) @@ -45,17 +45,8 @@ class PSBTOverviewScreen(ButtonListScreen): else: spend_amount = self.spend_amount - if spend_amount <= 1e6: - amount_display = f"{spend_amount:,} sats" - else: - amount_display = f"{spend_amount/1e8:,} btc" - self.components.append(IconTextLine( - icon_name=SeedSignerCustomIconConstants.BITCOIN_LOGO, - icon_color=GUIConstants.ACCENT_COLOR, - icon_size=34, - is_text_centered=True, - value_text=f"{amount_display}", - font_size=24, + self.components.append(BtcAmount( + total_sats=spend_amount, screen_y=icon_text_lines_y, )) @@ -592,20 +583,10 @@ class PSBTAddressDetailsScreen(ButtonListScreen): center_img = Image.new("RGB", (self.canvas_width, center_img_height), GUIConstants.BACKGROUND_COLOR) draw = ImageDraw.Draw(center_img) - if self.amount <= 1e7: - amount_display = f"{self.amount:,} sats" - else: - amount_display = f"{self.amount/1e8:,} btc" - - icon_text_line = IconTextLine( + btc_amount = BtcAmount( image_draw=draw, canvas=center_img, - icon_name=SeedSignerCustomIconConstants.BITCOIN_LOGO, - icon_color=GUIConstants.ACCENT_COLOR, - icon_size=28, - is_text_centered=True, - value_text=f"{amount_display}", - font_size=22, + total_sats=self.amount, screen_y=int(GUIConstants.COMPONENT_PADDING/2), ) @@ -614,13 +595,13 @@ class PSBTAddressDetailsScreen(ButtonListScreen): canvas=center_img, width=self.canvas_width - 2*GUIConstants.EDGE_PADDING, screen_x=GUIConstants.EDGE_PADDING, - screen_y=icon_text_line.height + GUIConstants.COMPONENT_PADDING, + screen_y=btc_amount.height + GUIConstants.COMPONENT_PADDING, font_size=24, address=self.address, ) # Render each to the temp img we passed in - icon_text_line.render() + btc_amount.render() formatted_address.render() self.body_img = center_img.crop(( @@ -652,15 +633,11 @@ class PSBTChangeDetailsScreen(ButtonListScreen): self.is_bottom_list = True super().__post_init__() - self.components.append(IconTextLine( - icon_name=SeedSignerCustomIconConstants.BITCOIN_LOGO, - icon_color=GUIConstants.ACCENT_COLOR, - icon_size=28, - value_text=f"{self.amount} sats" if self.amount < 1e6 else f"{self.amount/1e8:0.8f} btc", - font_size=22, - is_text_centered=True, - screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING + self.components.append(BtcAmount( + total_sats=self.amount, + screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING, )) + self.components.append(FormattedAddress( screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING, address=self.address, diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index f173f2cc..dda658cf 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -7,7 +7,7 @@ from seedsigner.gui.renderer import Renderer from seedsigner.models.threads import BaseThread from seedsigner.models.encode_qr import EncodeQR -from seedsigner.models.settings import SettingsConstants +from seedsigner.models.settings import Settings, SettingsConstants from ..components import (GUIConstants, BaseComponent, Button, Icon, LargeIconButton, SeedSignerCustomIconConstants, TopNav, TextArea, load_image) @@ -390,8 +390,6 @@ class ButtonListScreen(BaseTopNavScreen): release_keys=HardwareButtonsConstants.KEYS__ANYCLICK ) - print(user_input) - with self.renderer.lock: if not self.top_nav.is_selected and ( user_input == HardwareButtonsConstants.KEY_LEFT or ( diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index 30b526a6..cf1f009a 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -45,6 +45,17 @@ class SettingsConstants: (LANGUAGE__ENGLISH, "English"), ] + BTC_DENOMINATION__BTC = "btc" + BTC_DENOMINATION__SATS = "sats" + BTC_DENOMINATION__THRESHOLD = "thr" + BTC_DENOMINATION__BTCSATSHYBRID = "hyb" + ALL_BTC_DENOMINATIONS = [ + (BTC_DENOMINATION__BTC, "Btc-only"), + (BTC_DENOMINATION__SATS, "Sats-only"), + (BTC_DENOMINATION__THRESHOLD, "Threshold at 0.01"), + (BTC_DENOMINATION__BTCSATSHYBRID, "Btc | Sats hybrid"), + ] + CAMERA_ROTATION__0 = 0 CAMERA_ROTATION__90 = 90 CAMERA_ROTATION__180 = 180 @@ -130,6 +141,7 @@ class SettingsConstants: SETTING__WORDLIST_LANGUAGE = "wordlist_language" SETTING__PERSISTENT_SETTINGS = "persistent_settings" SETTING__COORDINATORS = "coordinators" + SETTING__BTC_DENOMINATION = "denomination" SETTING__NETWORK = "network" SETTING__QR_DENSITY = "qr_density" @@ -334,7 +346,7 @@ class SettingsDefinition: visibility=SettingsConstants.VISIBILITY__HIDDEN, selection_options=SettingsConstants.ALL_WORDLIST_LANGUAGES, default_value=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH), - + SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM, attr_name=SettingsConstants.SETTING__PERSISTENT_SETTINGS, display_name="Persistent settings", @@ -348,6 +360,14 @@ class SettingsDefinition: selection_options=SettingsConstants.ALL_COORDINATORS, default_value=SettingsConstants.ALL_COORDINATORS), + SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM, + attr_name=SettingsConstants.SETTING__BTC_DENOMINATION, + display_name="Denomination display", + type=SettingsConstants.TYPE__SELECT_1, + selection_options=SettingsConstants.ALL_BTC_DENOMINATIONS, + default_value=SettingsConstants.BTC_DENOMINATION__THRESHOLD), + + # Advanced options SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, attr_name=SettingsConstants.SETTING__NETWORK, From 477d44f2a021d10b8cbd2d214dc1116bf0620000 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 1 Apr 2022 07:38:59 -0500 Subject: [PATCH 02/10] Test suite fixes; DonateView * Updated custom SeedSigner icon font pack * Removing debugging print statements * Default button handling for BaseTopNavScreen --- src/seedsigner/controller.py | 11 +- src/seedsigner/gui/components.py | 39 +++---- src/seedsigner/gui/renderer.py | 6 +- src/seedsigner/gui/screens/psbt_screens.py | 6 +- src/seedsigner/gui/screens/screen.py | 48 ++++++-- .../gui/screens/settings_screens.py | 23 +++- src/seedsigner/models/decode_qr.py | 6 +- src/seedsigner/models/psbt_parser.py | 4 +- src/seedsigner/models/settings_definition.py | 2 - .../resources/fonts/seedsigner-glyphs.otf | Bin 16092 -> 11808 bytes src/seedsigner/views/psbt_views.py | 4 +- src/seedsigner/views/seed_views.py | 14 +-- src/seedsigner/views/settings_views.py | 25 +++- src/seedsigner/views/tools_views.py | 9 +- tests/test_controller.py | 59 ++-------- tests/test_decodepsbtqr.py | 109 ++++-------------- tests/test_encodepsbtqr.py | 38 +++--- tests/test_seed.py | 9 +- tests/test_seedqr.py | 5 +- 19 files changed, 179 insertions(+), 238 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 5431f62c..8c91e9d4 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -49,7 +49,7 @@ class Controller(Singleton): rather than at the top in order avoid circular imports. """ - VERSION = "0.5.0 Pre-Release 3" + VERSION = "0.5.0" # Declare class member vars with type hints to enable richer IDE support throughout # the code. @@ -185,15 +185,6 @@ class Controller(Singleton): opening_splash = OpeningSplashView() opening_splash.start() - # TODO: Remove for v0.5.0 production release - WarningScreen( - title="Warning", - status_headline="Pre-Release Code", - text="Do not use this with real funds or to create new secure keys!", - show_back_button=False, - ).display() - - """ Class references can be stored as variables in python! This loop receives a View class to execute and stores it in the `View_cls` diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index ce723dce..18bff593 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -1,5 +1,4 @@ import math -from operator import is_ import os import pathlib @@ -33,7 +32,7 @@ class GUIConstants: ICON_FONT_SIZE = 22 ICON_INLINE_FONT_SIZE = 24 ICON_LARGE_BUTTON_SIZE = 36 - ICON_PRIMARY_SCREEN_SIZE = 44 + ICON_PRIMARY_SCREEN_SIZE = 60 TOP_NAV_TITLE_FONT_NAME = "OpenSans-SemiBold" TOP_NAV_TITLE_FONT_SIZE = 20 @@ -109,18 +108,19 @@ class FontAwesomeIconConstants: class SeedSignerCustomIconConstants: LARGE_CHEVRON_LEFT = "\ue900" SMALL_CHEVRON_RIGHT = "\ue901" - PAGE_DOWN = "\ue902" PAGE_UP = "\ue903" - CIRCLE_X = "\ue904" - CIRCLE_EXCLAMATION = "\ue905" - CIRCLE_CHECK = "\ue906" - FINGERPRINT = "\ue907" - PATH = "\ue908" - BITCOIN_LOGO = "\ue909" - BITCOIN_LOGO_2 = "\ue90a" + PAGE_DOWN = "\ue902" + PLUS = "\ue904" + CIRCLE_CHECK = "\ue907" + CIRCLE_EXCLAMATION = "\ue908" + CIRCLE_X = "\ue909" + FINGERPRINT = "\ue90a" + PATH = "\ue90b" + BITCOIN_LOGO_STRAIGHT = "\ue90c" + BITCOIN_LOGO_TILTED = "\ue90d" MIN_VALUE = LARGE_CHEVRON_LEFT - MAX_VALUE = BITCOIN_LOGO_2 + MAX_VALUE = BITCOIN_LOGO_TILTED @@ -419,7 +419,7 @@ class TextArea(BaseComponent): class Icon(BaseComponent): screen_x: int = 0 screen_y: int = 0 - icon_name: str = SeedSignerCustomIconConstants.BITCOIN_LOGO + icon_name: str = SeedSignerCustomIconConstants.BITCOIN_LOGO_TILTED icon_size: int = GUIConstants.ICON_FONT_SIZE icon_color: str = GUIConstants.BODY_FONT_COLOR @@ -438,11 +438,11 @@ class Icon(BaseComponent): def render(self): self.image_draw.text( - (self.screen_x, self.screen_y), + (self.screen_x, self.screen_y + self.height), text=self.icon_name, font=self.icon_font, fill=self.icon_color, - anchor="lt", # left, top anchor to avoid "ascender" gap space + anchor="ls", ) @@ -539,7 +539,8 @@ class IconTextLine(BaseComponent): if self.icon_name: icon_y = self.screen_y + int((self.height - self.icon.height)/2) self.icon.screen_y = icon_y - self.height = self.icon.height + + self.height = max(self.icon.height, self.height) if self.is_text_centered: if self.icon_name: @@ -762,8 +763,6 @@ class BtcAmount(BaseComponent): denomination = Settings.get_instance().get_value(SettingsConstants.SETTING__BTC_DENOMINATION) network = Settings.get_instance().get_value(SettingsConstants.SETTING__NETWORK) - print(self.total_sats) - btc_unit = "tBtc" sats_unit = "tSats" if network == SettingsConstants.MAINNET: @@ -789,7 +788,7 @@ class BtcAmount(BaseComponent): btc_icon = Icon( image_draw=draw, canvas=self.paste_image, - icon_name=SeedSignerCustomIconConstants.BITCOIN_LOGO, + icon_name=SeedSignerCustomIconConstants.BITCOIN_LOGO_TILTED, icon_color=btc_color, icon_size=self.icon_size, screen_x=0, @@ -877,7 +876,7 @@ class BtcAmount(BaseComponent): btc_icon = Icon( image_draw=draw, canvas=self.paste_image, - icon_name=SeedSignerCustomIconConstants.BITCOIN_LOGO, + icon_name=SeedSignerCustomIconConstants.BITCOIN_LOGO_TILTED, icon_color=btc_color, icon_size=self.icon_size, screen_x=0, @@ -1047,7 +1046,6 @@ class Button(BaseComponent): self.icon_selected = Icon(icon_name=self.icon_name, icon_size=self.icon_size, icon_color=self.selected_icon_color) if self.is_icon_inline: - # TODO: Only apply screen_* at render if self.is_text_centered: # Shift the text's centering if self.text: @@ -1077,6 +1075,7 @@ class Button(BaseComponent): self.right_icon_y = math.ceil((self.height - self.right_icon.height)/2) + def render(self): if self.is_selected: background_color = self.selected_color diff --git a/src/seedsigner/gui/renderer.py b/src/seedsigner/gui/renderer.py index e0e321e5..cd3d031c 100644 --- a/src/seedsigner/gui/renderer.py +++ b/src/seedsigner/gui/renderer.py @@ -18,17 +18,13 @@ class Renderer(ConfigurableSingleton): @classmethod - def configure_instance(cls, config={}): + def configure_instance(cls): from seedsigner.models.settings import Settings - super().configure_instance(config) # Instantiate the one and only Renderer instance renderer = cls.__new__(cls) cls._instance = renderer - # TODO: Use Settings values to wire up diff hardware params - settings = Settings.get_instance() - # Eventually we'll be able to plug in other display controllers renderer.disp = ST7789() renderer.canvas_width = renderer.disp.width diff --git a/src/seedsigner/gui/screens/psbt_screens.py b/src/seedsigner/gui/screens/psbt_screens.py index 9cb482d2..62b606ef 100644 --- a/src/seedsigner/gui/screens/psbt_screens.py +++ b/src/seedsigner/gui/screens/psbt_screens.py @@ -38,7 +38,6 @@ class PSBTOverviewScreen(ButtonListScreen): # icon_text_lines_y = self.components[-1].screen_y + self.components[-1].height icon_text_lines_y = self.top_nav.height + GUIConstants.COMPONENT_PADDING - if not self.destination_addresses: # This is a self-transfer spend_amount = self.change_amount @@ -52,7 +51,7 @@ class PSBTOverviewScreen(ButtonListScreen): # Prep the transaction flow chart self.chart_x = 0 - self.chart_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING + self.chart_y = self.components[-1].screen_y + self.components[-1].height + int(GUIConstants.COMPONENT_PADDING/2) chart_height = self.buttons[0].screen_y - self.chart_y - GUIConstants.COMPONENT_PADDING # We need to supersample the whole panel so that small/thin elements render @@ -70,7 +69,8 @@ class PSBTOverviewScreen(ButtonListScreen): font_size = GUIConstants.BODY_FONT_MIN_SIZE * ssf font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, font_size) - tw, chart_text_height = font.getsize("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890") # All possible chars for max range + # Measure from anchor "top" to include font pixels below baseline (e.g. the "g" in "change") + (left, top, right, chart_text_height) = font.getbbox(text="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890", anchor="lt") vertical_center = int(image.height/2) # Supersampling renders thin elements poorly if they land on an even line before scaling down if vertical_center % 2 == 1: diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index dda658cf..ba64f741 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -56,8 +56,6 @@ class BaseScreen(BaseComponent): return self._run() except Exception as e: - print(e) - print("------") repr(e) raise e finally: @@ -214,7 +212,42 @@ class BaseTopNavScreen(BaseScreen): def _run(self): - raise Exception("Must implement in a child class") + while True: + if not self.top_nav.show_back_button and not self.top_nav.show_power_button: + # There's no navigation away from this screen; nothing to do here + time.sleep(0.1) + continue + + user_input = self.hw_inputs.wait_for( + HardwareButtonsConstants.ALL_KEYS, + check_release=True, + release_keys=HardwareButtonsConstants.KEYS__ANYCLICK + ) + + with self.renderer.lock: + if not self.top_nav.is_selected and user_input in [ + HardwareButtonsConstants.KEY_LEFT, + HardwareButtonsConstants.KEY_UP + ]: + self.top_nav.is_selected = True + self.top_nav.render_buttons() + + elif self.top_nav.is_selected and user_input in [ + HardwareButtonsConstants.KEY_DOWN, + HardwareButtonsConstants.KEY_RIGHT + ]: + self.top_nav.is_selected = False + self.top_nav.render_buttons() + + elif self.top_nav.is_selected and user_input in HardwareButtonsConstants.KEYS__ANYCLICK: + return self.top_nav.selected_button + + else: + # Nothing to do with this input + continue + + # Write the screen updates + self.renderer.show_image() @@ -810,6 +843,7 @@ class DireWarningScreen(WarningScreen): status_color: str = GUIConstants.DIRE_WARNING_COLOR + @dataclass class ResetScreen(BaseTopNavScreen): def __post_init__(self): @@ -822,10 +856,6 @@ class ResetScreen(BaseTopNavScreen): screen_y=self.top_nav.height, height=self.canvas_height - self.top_nav.height, )) - - def _run(self): - while True: - pass @@ -841,7 +871,3 @@ class PowerOffScreen(BaseTopNavScreen): screen_y=self.top_nav.height, height=self.canvas_height - self.top_nav.height, )) - - def _run(self): - while True: - pass diff --git a/src/seedsigner/gui/screens/settings_screens.py b/src/seedsigner/gui/screens/settings_screens.py index d7340ad4..7bfc729c 100644 --- a/src/seedsigner/gui/screens/settings_screens.py +++ b/src/seedsigner/gui/screens/settings_screens.py @@ -5,7 +5,7 @@ from typing import List from seedsigner.gui.components import Button, CheckboxButton, CheckedSelectionButton, FontAwesomeIconConstants, GUIConstants, IconButton, TextArea from seedsigner.gui.screens.scan_screens import ScanScreen -from seedsigner.gui.screens.screen import ButtonListScreen +from seedsigner.gui.screens.screen import BaseTopNavScreen, ButtonListScreen from seedsigner.hardware.buttons import HardwareButtonsConstants from seedsigner.models.settings import SettingsConstants @@ -190,3 +190,24 @@ class IOTestScreen(ScanScreen): cur_selected_button.is_selected = True time.sleep(0.1) + + + +@dataclass +class DonateScreen(BaseTopNavScreen): + def __post_init__(self): + self.title = "Donate" + super().__post_init__() + + self.components.append(TextArea( + text="SeedSigner is 100% free & open source, funded solely by the Bitcoin community.\n\nDonate onchain or LN at:", + screen_y=self.top_nav.height + 3*GUIConstants.COMPONENT_PADDING, + )) + + self.components.append(TextArea( + text="seedsigner.com", + font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE + 8, + font_color=GUIConstants.ACCENT_COLOR, + supersampling_factor=1, + screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING + )) diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index 7578ea9d..b0c034b2 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -60,8 +60,6 @@ class DecodeQR: qr_type = DecodeQR.detect_segment_type(data, wordlist_language_code=self.wordlist_language_code) - print(qr_type) - if self.qr_type == None: self.qr_type = qr_type @@ -97,6 +95,10 @@ class DecodeQR: elif self.qr_type != qr_type: raise Exception('QR Fragment Unexpected Type Change') + + if not self.decoder: + # Did not find any recognizable format + return DecodeQRStatus.INVALID # Process the binary formats first if self.qr_type == QRType.SEED__COMPACTSEEDQR: diff --git a/src/seedsigner/models/psbt_parser.py b/src/seedsigner/models/psbt_parser.py index c3518743..af29a882 100644 --- a/src/seedsigner/models/psbt_parser.py +++ b/src/seedsigner/models/psbt_parser.py @@ -58,7 +58,6 @@ class PSBTParser(): def _set_root(self): self.root = bip32.HDKey.from_seed(self.seed.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]["xprv"]) - print(f"root: {self.root}") def parse(self): @@ -85,7 +84,6 @@ class PSBTParser(): def _parse_inputs(self): self.input_amount = 0 - print(f"psbt.inputs: {self.psbt.inputs}") self.num_inputs = len(self.psbt.inputs) for inp in self.psbt.inputs: if inp.witness_utxo: @@ -343,5 +341,5 @@ class PSBTParser(): i = change_data["output_index"] output = self.psbt.outputs[i] is_owner = descriptor.owns(output) - print(f"{self.psbt.tx.vout[i].script_pubkey.address()} | {output.value} | {is_owner}") + # print(f"{self.psbt.tx.vout[i].script_pubkey.address()} | {output.value} | {is_owner}") return is_owner diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index cf1f009a..4687e1fa 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -513,8 +513,6 @@ class SettingsDefinition: if __name__ == "__main__": import json - import os - print("Exporting SettingsDefinition to json") output_file = "settings_definition.json" with open(output_file, 'w') as json_file: diff --git a/src/seedsigner/resources/fonts/seedsigner-glyphs.otf b/src/seedsigner/resources/fonts/seedsigner-glyphs.otf index 3edea9f59c909e2ab0a38c20c7c3f24e43d5dbc3..63a0008cdca20b42e9f41981c9b8b46f5dc3c058 100644 GIT binary patch literal 11808 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 BKcD~r diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index ced51a9b..ec198129 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -109,7 +109,7 @@ class PSBTOverviewView(View): num_change_outputs = 0 num_self_transfer_outputs = 0 for change_output in change_data: - print(f"""{change_output["derivation_path"][0]}""") + # print(f"""{change_output["derivation_path"][0]}""") if change_output["derivation_path"][0].split("/")[-2] == "1": num_change_outputs += 1 else: @@ -277,8 +277,6 @@ class PSBTChangeDetailsView(View): # Single-sig verification is easy. We expect to find a single fingerprint # and derivation path. seed_fingerprint = self.controller.psbt_seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)) - print(f"seed fingerprint: {seed_fingerprint}") - print(change_data) if seed_fingerprint not in change_data.get("fingerprint"): # TODO: Something is wrong with this psbt(?). Reroute to warning? diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index da7b2bab..1179af00 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -256,7 +256,6 @@ class SeedReviewPassphraseView(View): def __init__(self): super().__init__() self.seed = self.controller.storage.get_pending_seed() - print(f"SeedReviewPassphraseView self.seed: {self.seed}") def run(self): @@ -348,7 +347,7 @@ class SeedOptionsView(View): REVIEW_PSBT = "Review PSBT" VERIFY_ADDRESS = "Verify Addr" EXPORT_XPUB = "Export Xpub" - BACKUP = ("Backup Seed", None, None, None, FontAwesomeIconConstants.CIRCLE_CHEVRON_RIGHT) + BACKUP = ("Backup Seed", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT) DISCARD = ("Discard Seed", None, None, "red") button_data = [] @@ -366,7 +365,7 @@ class SeedOptionsView(View): if self.controller.psbt: if PSBTParser.has_matching_input_fingerprint(self.controller.psbt, self.seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)): if self.controller.resume_main_flow and self.controller.resume_main_flow == Controller.FLOW__PSBT: - # Re-route us directly back to the start of the PSBT flow + # Re-route us directly back to the start of the PSBT flow self.controller.resume_main_flow = None self.controller.psbt_seed = self.seed return Destination(PSBTOverviewView, skip_current_view=True) @@ -1122,9 +1121,6 @@ class AddressVerificationStartView(View): self.controller.unverified_address["sig_type"] = sig_type self.controller.unverified_address["derivation_path"] = derivation_path - import json - print(json.dumps(self.controller.unverified_address, indent=4)) - return destination @@ -1332,8 +1328,6 @@ class SeedAddressVerificationView(View): # Successfully verified the addr; update the data self.controller.unverified_address["verified_index"] = self.verified_index.cur_count self.controller.unverified_address["verified_index_is_change"] = self.verified_index_is_change.cur_count == 1 - import json - print(json.dumps(self.controller.unverified_address, indent=4)) return Destination(AddressVerificationSuccessView, view_args=dict(seed_num=self.seed_num)) else: @@ -1379,14 +1373,12 @@ class SeedAddressVerificationView(View): (receive_address, change_address) = self.derive_single_sig(i) if self.address == receive_address: - print(f"Verified receive addr #{i}!") self.verified_index.set_value(i) self.verified_index_is_change.set_value(0) self.keep_running = False break elif self.address == change_address: - print(f"Verified change addr #{i}!") self.verified_index.set_value(i) self.verified_index_is_change.set_value(1) self.keep_running = False @@ -1490,8 +1482,6 @@ class MultisigWalletDescriptorView(View): policy = descriptor.brief_policy.split("multisig")[0].strip() - print(fingerprints) - RETURN = "Return to PSBT" VERIFY = "Verify Addr" OK = "OK" diff --git a/src/seedsigner/views/settings_views.py b/src/seedsigner/views/settings_views.py index e3c46b31..5a705e08 100644 --- a/src/seedsigner/views/settings_views.py +++ b/src/seedsigner/views/settings_views.py @@ -1,4 +1,4 @@ -from seedsigner.gui.components import FontAwesomeIconConstants +from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustomIconConstants from seedsigner.models.decode_qr import DecodeQR from .view import View, Destination, BackStackView, MainMenuView @@ -16,6 +16,9 @@ class SettingsMenuView(View): def run(self): + IO_TEST = "I/O test" + DONATE = "Donate" + settings_entries = SettingsDefinition.get_settings_entries( visibiilty=self.visibility ) @@ -32,16 +35,17 @@ class SettingsMenuView(View): title = "Settings" # Set up the next nested level of menuing - button_data.append(("Advanced", None, None, None, FontAwesomeIconConstants.CIRCLE_CHEVRON_RIGHT)) + button_data.append(("Advanced", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)) next = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__ADVANCED}) - button_data.append("I/O test") + button_data.append(IO_TEST) + button_data.append(DONATE) elif self.visibility == SettingsConstants.VISIBILITY__ADVANCED: title = "Advanced" # So far there are no real Developer options; disabling for now - # button_data.append(("Developer Options", None, None, None, FontAwesomeIconConstants.CIRCLE_CHEVRON_RIGHT)) + # button_data.append(("Developer Options", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)) # next = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__DEVELOPER}) next = None @@ -67,9 +71,12 @@ class SettingsMenuView(View): elif selected_menu_num == len(settings_entries): return next - elif selected_menu_num == len(settings_entries) + 1: + elif len(button_data) > selected_menu_num and button_data[selected_menu_num] == IO_TEST: return Destination(IOTestView) + elif len(button_data) > selected_menu_num and button_data[selected_menu_num] == DONATE: + return Destination(DonateView) + else: # TODO: Free-entry types (are there any?) will need their own SettingsEntryUpdateFreeEntryView(?). return Destination(SettingsEntryUpdateSelectionView, view_args={"attr_name": settings_entries[selected_menu_num].attr_name}) @@ -179,3 +186,11 @@ class IOTestView(View): ).display() return Destination(SettingsMenuView) + + + +class DonateView(View): + def run(self): + settings_screens.DonateScreen().display() + + return Destination(SettingsMenuView) diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index bcc8c2f8..8c6b2216 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -4,7 +4,6 @@ import time from PIL import Image from PIL.ImageOps import autocontrast -from seedsigner.gui.screens.screen import LargeButtonScreen from seedsigner.hardware.camera import Camera from seedsigner.gui.components import FontAwesomeIconConstants @@ -13,9 +12,9 @@ from seedsigner.gui.screens.tools_screens import ToolsDiceEntropyEntryScreen, To from seedsigner.helpers import mnemonic_generation from seedsigner.models.seed import Seed from seedsigner.models.settings_definition import SettingsConstants -from seedsigner.views.seed_views import SeedDiscardView, SeedFinalizeView, SeedMnemonicEntryView, SeedWordsView, SeedWordsWarningView +from seedsigner.views.seed_views import SeedDiscardView, SeedFinalizeView, SeedMnemonicEntryView, SeedWordsWarningView -from .view import NotYetImplementedView, View, Destination, BackStackView, MainMenuView +from .view import View, Destination, BackStackView @@ -207,9 +206,9 @@ class ToolsDiceEntropyEntryView(View): if ret == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - print(ret) + print(f"Dice rolls: {ret}") dice_seed_phrase = mnemonic_generation.generate_mnemonic_from_dice(ret) - print(dice_seed_phrase) + print(f"""Mnemonic: "{dice_seed_phrase}" """) # Add the mnemonic as an in-memory Seed seed = Seed(dice_seed_phrase, wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)) diff --git a/tests/test_controller.py b/tests/test_controller.py index ef3e7ea3..b6887369 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -3,6 +3,7 @@ import pytest from mock import MagicMock from seedsigner.controller import Controller from seedsigner.models.settings import Settings +from seedsigner.models.settings_definition import SettingsConstants @@ -11,10 +12,6 @@ def test_singleton_init_fails(): with pytest.raises(Exception): c = Controller() -def test_singleton_get_instance_without_configure_fails(): - """ Calling get_instance() without first calling configure_instance() should fail """ - with pytest.raises(Exception): - c = Controller.get_instance() def test_singleton_get_instance_preserves_state(): """ Changes to the Controller singleton should be preserved across calls to get_instance() """ @@ -25,38 +22,17 @@ def test_singleton_get_instance_preserves_state(): Settings._instance = None Controller._instance = None - settings = """ - [system] - debug = False - default_language = en - persistent_settings = False - - [display] - text_color = ORANGE - qr_background_color = FFFFFF - camera_rotation = 0 - - [wallet] - network = main - software = Prompt - qr_density = 2 - custom_derivation = m/0/0 - compact_seedqr_enabled = False - """ - config = configparser.ConfigParser() - config.read_string(settings) - # Initialize the instance and verify that it read the config settings - Controller.configure_instance(config, disable_hardware=True) + Controller.configure_instance(disable_hardware=True) controller = Controller.get_instance() - assert controller.color == "ORANGE" + assert controller.unverified_address is None # Change a value in the instance... - controller.color = "purple" + controller.unverified_address = "123abc" # ...get a new copy of the instance and confirm change controller = Controller.get_instance() - assert controller.color == "purple" + assert controller.unverified_address == "123abc" def test_missing_settings_get_defaults(): @@ -69,30 +45,9 @@ def test_missing_settings_get_defaults(): Settings._instance = None Controller._instance = None - # Intentionally omit `compact_seedqr_enabled` from settings: - settings = """ - [system] - debug = False - default_language = en - persistent_settings = False - - [display] - text_color = ORANGE - qr_background_color = FFFFFF - camera_rotation = 0 - - [wallet] - network = main - software = Prompt - qr_density = 2 - custom_derivation = m/0/0 - """ - config = configparser.ConfigParser() - config.read_string(settings) - # Controller should parse the settings fine, even though a field is missing - Controller.configure_instance(config, disable_hardware=True) + Controller.configure_instance(disable_hardware=True) # Controller should still have a default value controller = Controller.get_instance() - assert controller.settings.compact_seedqr_enabled is False + assert controller.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__DISABLED diff --git a/tests/test_decodepsbtqr.py b/tests/test_decodepsbtqr.py index 4891c882..ef9baa4a 100644 --- a/tests/test_decodepsbtqr.py +++ b/tests/test_decodepsbtqr.py @@ -3,6 +3,8 @@ from mock import MagicMock from seedsigner.models import Seed, DecodeQR, DecodeQRStatus, QRType, PSBTParser from embit import psbt, bip39 +from seedsigner.models.settings_definition import SettingsConstants + # this is an of this bug: https://github.com/Foundation-Devices/foundation-ur-py/issues/3 @@ -78,15 +80,15 @@ def test_base64_single_frame_singlsig(): assert str(tx) == base64_psbt - mnemonic = "height demise useless trap grow lion found off key clown transfer enroll" + mnemonic = "height demise useless trap grow lion found off key clown transfer enroll".split() pw = "" seed = Seed(mnemonic, passphrase=pw) - assert seed.mnemonic_str == mnemonic + assert seed.mnemonic_str == " ".join(mnemonic) assert seed.seed_bytes != None - pp = PSBTParser(tx,seed,"test") + pp = PSBTParser(p=tx, seed=seed, network=SettingsConstants.TESTNET) assert tx.inputs[0].witness_utxo.value == 25000 # input amount in psbt @@ -135,8 +137,8 @@ def test_base64_2_input_p2wsh(): assert str(tx) == base64_psbt - mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" - pp = PSBTParser(tx, Seed(mnemonic), "test") + mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".split() + pp = PSBTParser(p=tx, seed=Seed(mnemonic), network=SettingsConstants.TESTNET) assert tx.inputs[0].witness_utxo.value == 10000000 # input amount 1 in psbt @@ -162,8 +164,8 @@ def test_base64_1_input_p2sh_p2wsh(): assert str(tx) == base64_psbt - mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" - pp = PSBTParser(tx,Seed(mnemonic, wordlist=bip39.WORDLIST),"test") + mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".split() + pp = PSBTParser(p=tx, seed=Seed(mnemonic), network=SettingsConstants.TESTNET) assert tx.inputs[0].witness_utxo.value == 100000000 # input amount 1 in psbt @@ -175,62 +177,6 @@ def test_base64_1_input_p2sh_p2wsh(): assert len(pp.destination_addresses) == 1 -def test_ur_legacy(): - - qrcodes = [ - "UR:BYTES/2OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/3DJV07U94KMSQQQQQQQQZQ8A0CQSYQQQQQQQZQHA78FG5TANFV9Z2TPDWE57AUVCN2T06TDGCAJNYPAT9LVG5QFMU5QSQQQQQRLLLLLL3NZ3KK6TMJ2TTRH6SKU05V76GHEANAT2QSDCDPFG67YZQQPFFZKSZQQQQQQ0LLLLLUPYQRGRQQQQQQQQYGQZP4XCCZ3WFNWK" - , "UR:BYTES/2OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/3DJV07U94KMSQQQQQQQQZQ8A0CQSYQQQQQQQZQHA78FG5TANFV9Z2TPDWE57AUVCN2T06TDGCAJNYPAT9LVG5QFMU5QSQQQQQRLLLLLL3NZ3KK6TMJ2TTRH6SKU05V76GHEANAT2QSDCDPFG67YZQQPFFZKSZQQQQQQ0LLLLLUPYQRGRQQQQQQQQYGQZP4XCCZ3WFNWK" - , "UR:BYTES/3OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/0QXPCDECDVJ8PS0DPW59YDWSH262DUZV6VAT4E4GMPEQQQQQQQQQQ9SQZSRR3CY9AYW5V4Y0A5DMWD57JD7FXX9G95PYWVZYQGSX2Y0YENNVNLEHXVMTDS86QM4VNDHG9V0K3PNDMGAJLWLSTS6504SZYP80TGGDPFZR8AM7XZNM77XWKRHP0D0Y540FLPTA5C2HRUQ0" - , "UR:BYTES/3OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/0QXPCDECDVJ8PS0DPW59YDWSH262DUZV6VAT4E4GMPEQQQQQQQQQQ9SQZSRR3CY9AYW5V4Y0A5DMWD57JD7FXX9G95PYWVZYQGSX2Y0YENNVNLEHXVMTDS86QM4VNDHG9V0K3PNDMGAJLWLSTS6504SZYP80TGGDPFZR8AM7XZNM77XWKRHP0D0Y540FLPTA5C2HRUQ0" - , "UR:BYTES/3OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/0QXPCDECDVJ8PS0DPW59YDWSH262DUZV6VAT4E4GMPEQQQQQQQQQQ9SQZSRR3CY9AYW5V4Y0A5DMWD57JD7FXX9G95PYWVZYQGSX2Y0YENNVNLEHXVMTDS86QM4VNDHG9V0K3PNDMGAJLWLSTS6504SZYP80TGGDPFZR8AM7XZNM77XWKRHP0D0Y540FLPTA5C2HRUQ0" - , "UR:BYTES/4OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/3Y0PYQFPQW6EJAC7ZGZUYVSCVLPJUS8CVFT8389KFXX82VM3YQMT26FRVHLVUQJ8XPZQYGR9FH3H26HSK99ZH3MQ4D7EHCS7Y752EP37HGG2X6RU5V3PKD7PMSPZQF9L7DESNZNS0H4V56978HCMAEG9D99CNY5LYCV247F58T7M66QUQYSS907LVPMZLKYAWMDGRRKT" - , "UR:BYTES/4OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/3Y0PYQFPQW6EJAC7ZGZUYVSCVLPJUS8CVFT8389KFXX82VM3YQMT26FRVHLVUQJ8XPZQYGR9FH3H26HSK99ZH3MQ4D7EHCS7Y752EP37HGG2X6RU5V3PKD7PMSPZQF9L7DESNZNS0H4V56978HCMAEG9D99CNY5LYCV247F58T7M66QUQYSS907LVPMZLKYAWMDGRRKT" - , "UR:BYTES/5OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/GX8KAW6FYVRQ2HKD5T4WUJJ7K68T5262QQQQQQQPQY45QRGRQQQQQQQQYGQZP4XCCZ3WFNWK0QXPCDECDVJ8PS0DPW59YDWSH262DUZV6VAT4E4GQYZ5W53PQFMWVTS2CUAWJ43C87WR6ZZ705V5LU2AEKGEF8AM8FWP6TQFTD7AZGGZMKAVTUW8HDRTKMEET55FK6MX" - , "UR:BYTES/5OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/GX8KAW6FYVRQ2HKD5T4WUJJ7K68T5262QQQQQQQPQY45QRGRQQQQQQQQYGQZP4XCCZ3WFNWK0QXPCDECDVJ8PS0DPW59YDWSH262DUZV6VAT4E4GQYZ5W53PQFMWVTS2CUAWJ43C87WR6ZZ705V5LU2AEKGEF8AM8FWP6TQFTD7AZGGZMKAVTUW8HDRTKMEET55FK6MX" - , "UR:BYTES/5OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/GX8KAW6FYVRQ2HKD5T4WUJJ7K68T5262QQQQQQQPQY45QRGRQQQQQQQQYGQZP4XCCZ3WFNWK0QXPCDECDVJ8PS0DPW59YDWSH262DUZV6VAT4E4GQYZ5W53PQFMWVTS2CUAWJ43C87WR6ZZ705V5LU2AEKGEF8AM8FWP6TQFTD7AZGGZMKAVTUW8HDRTKMEET55FK6MX" - , "UR:BYTES/6OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/KTS8U3XQP95WYR8VQ9V2KWHCTS399T3ZQCP8DE3WPTRN462K8QLEC0GGTE73JNL3THXER9YLHVA9C8FVP9DHM5GUKUGYR2PSQQQGQQGQQZQQQQQQSQPQQQYQQQQQQQQRQQQQQGSXQTWM4303C7A5DWM089WJ3XMTV6EWQLJYCQYK3CSVASQ432E6LPWZY8XYUTHDXVQQ" - , "UR:BYTES/6OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/KTS8U3XQP95WYR8VQ9V2KWHCTS399T3ZQCP8DE3WPTRN462K8QLEC0GGTE73JNL3THXER9YLHVA9C8FVP9DHM5GUKUGYR2PSQQQGQQGQQZQQQQQQSQPQQQYQQQQQQQQRQQQQQGSXQTWM4303C7A5DWM089WJ3XMTV6EWQLJYCQYK3CSVASQ432E6LPWZY8XYUTHDXVQQ" - , "UR:BYTES/7OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/QZQQZQQQSQQQQQYQQGQQPQQQQQQQQQCQQQQQQQQPQ9R4YGGZDYQESZAAQHLY3L9L5VMSYRCQAXPWR6ES83NEC5XTMD4HPKVNGF4ZZQ4XNF7SKKG5KVH0UZPD3XXVNDWVP2CQAP0GLNRGQP2DHAD0U9HYLAF2UGSZQF5SRXQTH5ZLUJ8UH73NWQS0QR5C9C0TXQ7X08ZS" - , "UR:BYTES/7OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/QZQQZQQQSQQQQQYQQGQQPQQQQQQQQQCQQQQQQQQPQ9R4YGGZDYQESZAAQHLY3L9L5VMSYRCQAXPWR6ES83NEC5XTMD4HPKVNGF4ZZQ4XNF7SKKG5KVH0UZPD3XXVNDWVP2CQAP0GLNRGQP2DHAD0U9HYLAF2UGSZQF5SRXQTH5ZLUJ8UH73NWQS0QR5C9C0TXQ7X08ZS" - , "UR:BYTES/1OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/TYPUUURNVF607QGQ05PQQQQQQ9VXGQQDUMWH3LD9GH8XF4ZYJQNMQ80L6MC0KMV6JMVV22RDPZP0SQQQQQQQPLLLLLLSYS8ZQYQQQQQQQQTQQ9P6MG599TND0NJ54AZSE0ZHSHLY7Q4P94C79GQSQQQQQQQZYQPQ68Q0NLXPZYTUVURY6X4G8U7T5H4QVCHY3KKXAW0R" - , "UR:BYTES/1OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/TYPUUURNVF607QGQ05PQQQQQQ9VXGQQDUMWH3LD9GH8XF4ZYJQNMQ80L6MC0KMV6JMVV22RDPZP0SQQQQQQQPLLLLLLSYS8ZQYQQQQQQQQTQQ9P6MG599TND0NJ54AZSE0ZHSHLY7Q4P94C79GQSQQQQQQQZYQPQ68Q0NLXPZYTUVURY6X4G8U7T5H4QVCHY3KKXAW0R" - , "UR:BYTES/2OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/3DJV07U94KMSQQQQQQQQZQ8A0CQSYQQQQQQQZQHA78FG5TANFV9Z2TPDWE57AUVCN2T06TDGCAJNYPAT9LVG5QFMU5QSQQQQQRLLLLLL3NZ3KK6TMJ2TTRH6SKU05V76GHEANAT2QSDCDPFG67YZQQPFFZKSZQQQQQQ0LLLLLUPYQRGRQQQQQQQQYGQZP4XCCZ3WFNWK" - , "UR:BYTES/2OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/3DJV07U94KMSQQQQQQQQZQ8A0CQSYQQQQQQQZQHA78FG5TANFV9Z2TPDWE57AUVCN2T06TDGCAJNYPAT9LVG5QFMU5QSQQQQQRLLLLLL3NZ3KK6TMJ2TTRH6SKU05V76GHEANAT2QSDCDPFG67YZQQPFFZKSZQQQQQQ0LLLLLUPYQRGRQQQQQQQQYGQZP4XCCZ3WFNWK" - , "UR:BYTES/3OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/0QXPCDECDVJ8PS0DPW59YDWSH262DUZV6VAT4E4GMPEQQQQQQQQQQ9SQZSRR3CY9AYW5V4Y0A5DMWD57JD7FXX9G95PYWVZYQGSX2Y0YENNVNLEHXVMTDS86QM4VNDHG9V0K3PNDMGAJLWLSTS6504SZYP80TGGDPFZR8AM7XZNM77XWKRHP0D0Y540FLPTA5C2HRUQ0" - , "UR:BYTES/3OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/0QXPCDECDVJ8PS0DPW59YDWSH262DUZV6VAT4E4GMPEQQQQQQQQQQ9SQZSRR3CY9AYW5V4Y0A5DMWD57JD7FXX9G95PYWVZYQGSX2Y0YENNVNLEHXVMTDS86QM4VNDHG9V0K3PNDMGAJLWLSTS6504SZYP80TGGDPFZR8AM7XZNM77XWKRHP0D0Y540FLPTA5C2HRUQ0" - , "UR:BYTES/3OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/0QXPCDECDVJ8PS0DPW59YDWSH262DUZV6VAT4E4GMPEQQQQQQQQQQ9SQZSRR3CY9AYW5V4Y0A5DMWD57JD7FXX9G95PYWVZYQGSX2Y0YENNVNLEHXVMTDS86QM4VNDHG9V0K3PNDMGAJLWLSTS6504SZYP80TGGDPFZR8AM7XZNM77XWKRHP0D0Y540FLPTA5C2HRUQ0" - , "UR:BYTES/4OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/3Y0PYQFPQW6EJAC7ZGZUYVSCVLPJUS8CVFT8389KFXX82VM3YQMT26FRVHLVUQJ8XPZQYGR9FH3H26HSK99ZH3MQ4D7EHCS7Y752EP37HGG2X6RU5V3PKD7PMSPZQF9L7DESNZNS0H4V56978HCMAEG9D99CNY5LYCV247F58T7M66QUQYSS907LVPMZLKYAWMDGRRKT" - , "UR:BYTES/4OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/3Y0PYQFPQW6EJAC7ZGZUYVSCVLPJUS8CVFT8389KFXX82VM3YQMT26FRVHLVUQJ8XPZQYGR9FH3H26HSK99ZH3MQ4D7EHCS7Y752EP37HGG2X6RU5V3PKD7PMSPZQF9L7DESNZNS0H4V56978HCMAEG9D99CNY5LYCV247F58T7M66QUQYSS907LVPMZLKYAWMDGRRKT" - , "UR:BYTES/6OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/KTS8U3XQP95WYR8VQ9V2KWHCTS399T3ZQCP8DE3WPTRN462K8QLEC0GGTE73JNL3THXER9YLHVA9C8FVP9DHM5GUKUGYR2PSQQQGQQGQQZQQQQQQSQPQQQYQQQQQQQQRQQQQQGSXQTWM4303C7A5DWM089WJ3XMTV6EWQLJYCQYK3CSVASQ432E6LPWZY8XYUTHDXVQQ" - , "UR:BYTES/7OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/QZQQZQQQSQQQQQYQQGQQPQQQQQQQQQCQQQQQQQQPQ9R4YGGZDYQESZAAQHLY3L9L5VMSYRCQAXPWR6ES83NEC5XTMD4HPKVNGF4ZZQ4XNF7SKKG5KVH0UZPD3XXVNDWVP2CQAP0GLNRGQP2DHAD0U9HYLAF2UGSZQF5SRXQTH5ZLUJ8UH73NWQS0QR5C9C0TXQ7X08ZS" - , "UR:BYTES/7OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/QZQQZQQQSQQQQQYQQGQQPQQQQQQQQQCQQQQQQQQPQ9R4YGGZDYQESZAAQHLY3L9L5VMSYRCQAXPWR6ES83NEC5XTMD4HPKVNGF4ZZQ4XNF7SKKG5KVH0UZPD3XXVNDWVP2CQAP0GLNRGQP2DHAD0U9HYLAF2UGSZQF5SRXQTH5ZLUJ8UH73NWQS0QR5C9C0TXQ7X08ZS" - , "UR:BYTES/8OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/E0DKKUXEJDPX589HZPQ6SVQQQZQQZQQQSQQQQQYQQGQQPQQPQQQQQPQQQQQZYQSZ56D86Z6EZJEJALSG9KYCEJD4ES9TQR59AR7VDQQ9FKL44LSKUNL3E38ZAMFNQQQQSQQSQQYQQQQQPQQZQQQGQQGQQQQQGQQQQQQQ0P6SEH" - ] - - d = DecodeQR() - for i in qrcodes: - if d.add_data(i) == DecodeQRStatus.COMPLETE: - break - assert d.qr_type == QRType.PSBT__LEGACY_UR - - #complete should be true - assert d.is_complete - - tx = d.get_psbt() - - mnemonic = "zone zone zone zone zone abandon ability able abandon ability able abstract" - pp = PSBTParser(tx,Seed(mnemonic, wordlist=bip39.WORDLIST),"test") - - assert pp.input_amount == 200000 - - assert pp.fee_amount == 226 - - assert pp.spend_amount == 123456 - - assert pp.change_amount == (pp.input_amount - pp.spend_amount - pp.fee_amount) - - assert pp.change_amount == 76318 - def test_specter_multisig_animated_qr(): qrcodes = [ @@ -252,8 +198,8 @@ def test_specter_multisig_animated_qr(): tx = d.get_psbt() - mnemonic = "zone zone zone zone zone abandon ability able abandon ability able abstract" - pp = PSBTParser(tx,Seed(mnemonic, wordlist=bip39.WORDLIST),"test") + mnemonic = "zone zone zone zone zone abandon ability able abandon ability able abstract".split() + pp = PSBTParser(p=tx, seed=Seed(mnemonic), network=SettingsConstants.TESTNET) assert pp.input_amount == 1052818 @@ -290,8 +236,8 @@ def test_specter_multisig_animated_qr(): tx2 = d2.get_psbt() - mnemonic2 = "able bacon cable able bacon cable abandon abandon abandon abandon abandon access" - pp2 = PSBTParser(tx2,Seed(mnemonic2, wordlist=bip39.WORDLIST),"test") + mnemonic2 = "able bacon cable able bacon cable abandon abandon abandon abandon abandon access".split() + pp2 = PSBTParser(p=tx2, seed=Seed(mnemonic2), network=SettingsConstants.TESTNET) assert pp2.input_amount == 1052818 @@ -334,8 +280,8 @@ def test_short_4_letter_mnemonic_qr(): assert d.is_complete assert d.get_seed_phrase() == ["height", "demise", "useless", "trap", "grow", "lion", "found", "off", "key", "clown", "transfer", "enroll"] -def test_bitcoin_address(): - + +def test_bitcoin_address(): bad1 = "loremipsum" bad2 = "0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae" bad3 = "121802020768124106400009195602431595117715840445" @@ -361,49 +307,44 @@ def test_bitcoin_address(): d.add_data(legacy_address1) assert d.get_address() == legacy_address1 - assert d.get_address_type() == "P2PKH-main" + assert d.get_address_type() == (SettingsConstants.LEGACY_P2PKH, SettingsConstants.MAINNET) d = DecodeQR() d.add_data(legacy_address2) assert d.get_address() == legacy_address2 - assert d.get_address_type() == "P2PKH-main" + assert d.get_address_type() == (SettingsConstants.LEGACY_P2PKH, SettingsConstants.MAINNET) d = DecodeQR() d.add_data(main_bech32_address) assert d.get_address() == main_bech32_address - assert d.get_address_type() == "Bech32-main" + assert d.get_address_type() == (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.MAINNET) d = DecodeQR() d.add_data(test_bech32_address) assert d.get_address() == test_bech32_address - assert d.get_address_type() == "Bech32-test" + assert d.get_address_type() == (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.TESTNET) d = DecodeQR() d.add_data(main_nested_segwit_address) assert d.get_address() == main_nested_segwit_address - assert d.get_address_type() == "P2SH-main" + assert d.get_address_type() == (SettingsConstants.NESTED_SEGWIT, SettingsConstants.MAINNET) d = DecodeQR() d.add_data(test_nested_segwit_address) - + assert d.get_address() == test_nested_segwit_address - assert d.get_address_type() == "P2SH-test" - + assert d.get_address_type() == (SettingsConstants.NESTED_SEGWIT, SettingsConstants.TESTNET) + d = DecodeQR() d.add_data(main_bech32_address2) assert d.get_address() == "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" - assert d.get_address_type() == "Bech32-main" - - d = DecodeQR() - d.add_data(main_bech32_address3) - - assert d.get_address() == "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" - assert d.get_address_type() == "Bech32-main" + assert d.get_address_type() == (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.MAINNET) + def test_seed_qr(): diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py index 71a7a3b3..380a446d 100644 --- a/tests/test_encodepsbtqr.py +++ b/tests/test_encodepsbtqr.py @@ -1,7 +1,7 @@ import pytest from mock import MagicMock -from seedsigner.models import EncodeQR, QRType, EncodeQRDensity -from embit import psbt, bip39 +from seedsigner.models import EncodeQR, QRType +from embit import psbt from binascii import a2b_base64 from seedsigner.models.settings import SettingsConstants @@ -9,7 +9,6 @@ from seedsigner.models.settings import SettingsConstants def test_ur_qr_encode(): - base64_psbt = "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" tx = psbt.PSBT.parse(a2b_base64(base64_psbt)) @@ -23,8 +22,8 @@ def test_ur_qr_encode(): cnt += 1 -def test_specter_qr_encode(): +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)) @@ -60,43 +59,54 @@ def test_specter_qr_encode(): img = e.part_to_image(fragment) cnt += 1 -def test_seedsigner_qr(): + +def test_seedsigner_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = EncodeQR(seed_phrase=mnemonic.split(" "), qr_type=QRType.SEED__SEEDQR) + e = EncodeQR(seed_phrase=mnemonic.split(), qr_type=QRType.SEED__SEEDQR) print(e.next_part()) assert e.next_part() == "121802020768124106400009195602431595117715840445" -def test_xpub_qr(): + +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="test", derivation="m/48h/1h/0h/2h") + e = EncodeQR(seed_phrase=mnemonic.split(), passphrase="pass", qr_type=QRType.XPUB, network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h") assert e.next_part() == "[c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDNc5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBoMxfrVggXj5NJmaNEToWpax8GMMucozvAdqf1bW1JsZsfdBzsK3VUC5" -def test_specter_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="test", derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW) + 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) assert e.next_part() == "p1of4 [c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDN" assert e.next_part() == "p2of4 c5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBo" assert e.next_part() == "p3of4 MxfrVggXj5NJmaNEToWpax8GMMucozvAdqf1bW1J" assert e.next_part() == "p4of4 sZsfdBzsK3VUC5" -def test_ur_xpub_qr(): - + + +def test_ur_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__UR, network="test", derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__MEDIUM) + e = EncodeQR( + 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 + ) + # TODO: For Nick assert e.next_part() == "UR:CRYPTO-ACCOUNT/1-4/LPADAACSJKCYYNRDRTYAHDCAOEADCYSSMECPONAOLYTAADMETAADDLOXAXHDCLAOKSRLNLKPUEGYATHPMNRTAAOLAH" assert e.next_part() == "UR:CRYPTO-ACCOUNT/2-4/LPAOAACSJKCYYNRDRTYAHDCASNKKGHZMLUZORPVDGUOTECSTTKTOLPCWPTNTLKZTTIZTBEAAHDCXVDTPMYQDLAWNGL" assert e.next_part() == "UR:CRYPTO-ACCOUNT/3-4/LPAXAACSJKCYYNRDRTYAHDCARSTDSPZSBZSPGERLGDATUYNLPYBTGYIYYKBTWTAOSWKSVTSGCHBYDKYAVDSEPDJLAT" assert e.next_part() == "UR:CRYPTO-ACCOUNT/4-4/LPAAAACSJKCYYNRDRTYAHDCAAMTAADDYOEADLOCSDYYKADYKAEYKAOYKAOCYSSMECPONAYCYIOREKKJKAECADRRKIE" - diff --git a/tests/test_seed.py b/tests/test_seed.py index cc5eeaa3..45e597eb 100644 --- a/tests/test_seed.py +++ b/tests/test_seed.py @@ -6,10 +6,11 @@ from embit import bip39 from seedsigner.models.settings import SettingsConstants -spanish_wordlist = ["ábaco", "abdomen", "abeja", "abierto", "abogado", "abono", "aborto", "abrazo", "abrir", "abuelo", "abuso", "acabar", "academia", "acceso", "acción", "aceite", "acelga", "acento", "aceptar", "ácido", "aclarar", "acné", "acoger", "acoso", "activo", "acto", "actriz", "actuar", "acudir", "acuerdo", "acusar", "adicto", "admitir", "adoptar", "adorno", "aduana", "adulto", "aéreo", "afectar", "afición", "afinar", "afirmar", "ágil", "agitar", "agonía", "agosto", "agotar", "agregar", "agrio", "agua", "agudo", "águila", "aguja", "ahogo", "ahorro", "aire", "aislar", "ajedrez", "ajeno", "ajuste", "alacrán", "alambre", "alarma", "alba", "álbum", "alcalde", "aldea", "alegre", "alejar", "alerta", "aleta", "alfiler", "alga", "algodón", "aliado", "aliento", "alivio", "alma", "almeja", "almíbar", "altar", "alteza", "altivo", "alto", "altura", "alumno", "alzar", "amable", "amante", "amapola", "amargo", "amasar", "ámbar", "ámbito", "ameno", "amigo", "amistad", "amor", "amparo", "amplio", "ancho", "anciano", "ancla", "andar", "andén", "anemia", "ángulo", "anillo", "ánimo", "anís", "anotar", "antena", "antiguo", "antojo", "anual", "anular", "anuncio", "añadir", "añejo", "año", "apagar", "aparato", "apetito", "apio", "aplicar", "apodo", "aporte", "apoyo", "aprender", "aprobar", "apuesta", "apuro", "arado", "araña", "arar", "árbitro", "árbol", "arbusto", "archivo", "arco", "arder", "ardilla", "arduo", "área", "árido", "aries", "armonía", "arnés", "aroma", "arpa", "arpón", "arreglo", "arroz", "arruga", "arte", "artista", "asa", "asado", "asalto", "ascenso", "asegurar", "aseo", "asesor", "asiento", "asilo", "asistir", "asno", "asombro", "áspero", "astilla", "astro", "astuto", "asumir", "asunto", "atajo", "ataque", "atar", "atento", "ateo", "ático", "atleta", "átomo", "atraer", "atroz", "atún", "audaz", "audio", "auge", "aula", "aumento", "ausente", "autor", "aval", "avance", "avaro", "ave", "avellana", "avena", "avestruz", "avión", "aviso", "ayer", "ayuda", "ayuno", "azafrán", "azar", "azote", "azúcar", "azufre", "azul", "baba", "babor", "bache", "bahía", "baile", "bajar", "balanza", "balcón", "balde", "bambú", "banco", "banda", "baño", "barba", "barco", "barniz", "barro", "báscula", "bastón", "basura", "batalla", "batería", "batir", "batuta", "baúl", "bazar", "bebé", "bebida", "bello", "besar", "beso", "bestia", "bicho", "bien", "bingo", "blanco", "bloque", "blusa", "boa", "bobina", "bobo", "boca", "bocina", "boda", "bodega", "boina", "bola", "bolero", "bolsa", "bomba", "bondad", "bonito", "bono", "bonsái", "borde", "borrar", "bosque", "bote", "botín", "bóveda", "bozal", "bravo", "brazo", "brecha", "breve", "brillo", "brinco", "brisa", "broca", "broma", "bronce", "brote", "bruja", "brusco", "bruto", "buceo", "bucle", "bueno", "buey", "bufanda", "bufón", "búho", "buitre", "bulto", "burbuja", "burla", "burro", "buscar", "butaca", "buzón", "caballo", "cabeza", "cabina", "cabra", "cacao", "cadáver", "cadena", "caer", "café", "caída", "caimán", "caja", "cajón", "cal", "calamar", "calcio", "caldo", "calidad", "calle", "calma", "calor", "calvo", "cama", "cambio", "camello", "camino", "campo", "cáncer", "candil", "canela", "canguro", "canica", "canto", "caña", "cañón", "caoba", "caos", "capaz", "capitán", "capote", "captar", "capucha", "cara", "carbón", "cárcel", "careta", "carga", "cariño", "carne", "carpeta", "carro", "carta", "casa", "casco", "casero", "caspa", "castor", "catorce", "catre", "caudal", "causa", "cazo", "cebolla", "ceder", "cedro", "celda", "célebre", "celoso", "célula", "cemento", "ceniza", "centro", "cerca", "cerdo", "cereza", "cero", "cerrar", "certeza", "césped", "cetro", "chacal", "chaleco", "champú", "chancla", "chapa", "charla", "chico", "chiste", "chivo", "choque", "choza", "chuleta", "chupar", "ciclón", "ciego", "cielo", "cien", "cierto", "cifra", "cigarro", "cima", "cinco", "cine", "cinta", "ciprés", "circo", "ciruela", "cisne", "cita", "ciudad", "clamor", "clan", "claro", "clase", "clave", "cliente", "clima", "clínica", "cobre", "cocción", "cochino", "cocina", "coco", "código", "codo", "cofre", "coger", "cohete", "cojín", "cojo", "cola", "colcha", "colegio", "colgar", "colina", "collar", "colmo", "columna", "combate", "comer", "comida", "cómodo", "compra", "conde", "conejo", "conga", "conocer", "consejo", "contar", "copa", "copia", "corazón", "corbata", "corcho", "cordón", "corona", "correr", "coser", "cosmos", "costa", "cráneo", "cráter", "crear", "crecer", "creído", "crema", "cría", "crimen", "cripta", "crisis", "cromo", "crónica", "croqueta", "crudo", "cruz", "cuadro", "cuarto", "cuatro", "cubo", "cubrir", "cuchara", "cuello", "cuento", "cuerda", "cuesta", "cueva", "cuidar", "culebra", "culpa", "culto", "cumbre", "cumplir", "cuna", "cuneta", "cuota", "cupón", "cúpula", "curar", "curioso", "curso", "curva", "cutis", "dama", "danza", "dar", "dardo", "dátil", "deber", "débil", "década", "decir", "dedo", "defensa", "definir", "dejar", "delfín", "delgado", "delito", "demora", "denso", "dental", "deporte", "derecho", "derrota", "desayuno", "deseo", "desfile", "desnudo", "destino", "desvío", "detalle", "detener", "deuda", "día", "diablo", "diadema", "diamante", "diana", "diario", "dibujo", "dictar", "diente", "dieta", "diez", "difícil", "digno", "dilema", "diluir", "dinero", "directo", "dirigir", "disco", "diseño", "disfraz", "diva", "divino", "doble", "doce", "dolor", "domingo", "don", "donar", "dorado", "dormir", "dorso", "dos", "dosis", "dragón", "droga", "ducha", "duda", "duelo", "dueño", "dulce", "dúo", "duque", "durar", "dureza", "duro", "ébano", "ebrio", "echar", "eco", "ecuador", "edad", "edición", "edificio", "editor", "educar", "efecto", "eficaz", "eje", "ejemplo", "elefante", "elegir", "elemento", "elevar", "elipse", "élite", "elixir", "elogio", "eludir", "embudo", "emitir", "emoción", "empate", "empeño", "empleo", "empresa", "enano", "encargo", "enchufe", "encía", "enemigo", "enero", "enfado", "enfermo", "engaño", "enigma", "enlace", "enorme", "enredo", "ensayo", "enseñar", "entero", "entrar", "envase", "envío", "época", "equipo", "erizo", "escala", "escena", "escolar", "escribir", "escudo", "esencia", "esfera", "esfuerzo", "espada", "espejo", "espía", "esposa", "espuma", "esquí", "estar", "este", "estilo", "estufa", "etapa", "eterno", "ética", "etnia", "evadir", "evaluar", "evento", "evitar", "exacto", "examen", "exceso", "excusa", "exento", "exigir", "exilio", "existir", "éxito", "experto", "explicar", "exponer", "extremo", "fábrica", "fábula", "fachada", "fácil", "factor", "faena", "faja", "falda", "fallo", "falso", "faltar", "fama", "familia", "famoso", "faraón", "farmacia", "farol", "farsa", "fase", "fatiga", "fauna", "favor", "fax", "febrero", "fecha", "feliz", "feo", "feria", "feroz", "fértil", "fervor", "festín", "fiable", "fianza", "fiar", "fibra", "ficción", "ficha", "fideo", "fiebre", "fiel", "fiera", "fiesta", "figura", "fijar", "fijo", "fila", "filete", "filial", "filtro", "fin", "finca", "fingir", "finito", "firma", "flaco", "flauta", "flecha", "flor", "flota", "fluir", "flujo", "flúor", "fobia", "foca", "fogata", "fogón", "folio", "folleto", "fondo", "forma", "forro", "fortuna", "forzar", "fosa", "foto", "fracaso", "frágil", "franja", "frase", "fraude", "freír", "freno", "fresa", "frío", "frito", "fruta", "fuego", "fuente", "fuerza", "fuga", "fumar", "función", "funda", "furgón", "furia", "fusil", "fútbol", "futuro", "gacela", "gafas", "gaita", "gajo", "gala", "galería", "gallo", "gamba", "ganar", "gancho", "ganga", "ganso", "garaje", "garza", "gasolina", "gastar", "gato", "gavilán", "gemelo", "gemir", "gen", "género", "genio", "gente", "geranio", "gerente", "germen", "gesto", "gigante", "gimnasio", "girar", "giro", "glaciar", "globo", "gloria", "gol", "golfo", "goloso", "golpe", "goma", "gordo", "gorila", "gorra", "gota", "goteo", "gozar", "grada", "gráfico", "grano", "grasa", "gratis", "grave", "grieta", "grillo", "gripe", "gris", "grito", "grosor", "grúa", "grueso", "grumo", "grupo", "guante", "guapo", "guardia", "guerra", "guía", "guiño", "guion", "guiso", "guitarra", "gusano", "gustar", "haber", "hábil", "hablar", "hacer", "hacha", "hada", "hallar", "hamaca", "harina", "haz", "hazaña", "hebilla", "hebra", "hecho", "helado", "helio", "hembra", "herir", "hermano", "héroe", "hervir", "hielo", "hierro", "hígado", "higiene", "hijo", "himno", "historia", "hocico", "hogar", "hoguera", "hoja", "hombre", "hongo", "honor", "honra", "hora", "hormiga", "horno", "hostil", "hoyo", "hueco", "huelga", "huerta", "hueso", "huevo", "huida", "huir", "humano", "húmedo", "humilde", "humo", "hundir", "huracán", "hurto", "icono", "ideal", "idioma", "ídolo", "iglesia", "iglú", "igual", "ilegal", "ilusión", "imagen", "imán", "imitar", "impar", "imperio", "imponer", "impulso", "incapaz", "índice", "inerte", "infiel", "informe", "ingenio", "inicio", "inmenso", "inmune", "innato", "insecto", "instante", "interés", "íntimo", "intuir", "inútil", "invierno", "ira", "iris", "ironía", "isla", "islote", "jabalí", "jabón", "jamón", "jarabe", "jardín", "jarra", "jaula", "jazmín", "jefe", "jeringa", "jinete", "jornada", "joroba", "joven", "joya", "juerga", "jueves", "juez", "jugador", "jugo", "juguete", "juicio", "junco", "jungla", "junio", "juntar", "júpiter", "jurar", "justo", "juvenil", "juzgar", "kilo", "koala", "labio", "lacio", "lacra", "lado", "ladrón", "lagarto", "lágrima", "laguna", "laico", "lamer", "lámina", "lámpara", "lana", "lancha", "langosta", "lanza", "lápiz", "largo", "larva", "lástima", "lata", "látex", "latir", "laurel", "lavar", "lazo", "leal", "lección", "leche", "lector", "leer", "legión", "legumbre", "lejano", "lengua", "lento", "leña", "león", "leopardo", "lesión", "letal", "letra", "leve", "leyenda", "libertad", "libro", "licor", "líder", "lidiar", "lienzo", "liga", "ligero", "lima", "límite", "limón", "limpio", "lince", "lindo", "línea", "lingote", "lino", "linterna", "líquido", "liso", "lista", "litera", "litio", "litro", "llaga", "llama", "llanto", "llave", "llegar", "llenar", "llevar", "llorar", "llover", "lluvia", "lobo", "loción", "loco", "locura", "lógica", "logro", "lombriz", "lomo", "lonja", "lote", "lucha", "lucir", "lugar", "lujo", "luna", "lunes", "lupa", "lustro", "luto", "luz", "maceta", "macho", "madera", "madre", "maduro", "maestro", "mafia", "magia", "mago", "maíz", "maldad", "maleta", "malla", "malo", "mamá", "mambo", "mamut", "manco", "mando", "manejar", "manga", "maniquí", "manjar", "mano", "manso", "manta", "mañana", "mapa", "máquina", "mar", "marco", "marea", "marfil", "margen", "marido", "mármol", "marrón", "martes", "marzo", "masa", "máscara", "masivo", "matar", "materia", "matiz", "matriz", "máximo", "mayor", "mazorca", "mecha", "medalla", "medio", "médula", "mejilla", "mejor", "melena", "melón", "memoria", "menor", "mensaje", "mente", "menú", "mercado", "merengue", "mérito", "mes", "mesón", "meta", "meter", "método", "metro", "mezcla", "miedo", "miel", "miembro", "miga", "mil", "milagro", "militar", "millón", "mimo", "mina", "minero", "mínimo", "minuto", "miope", "mirar", "misa", "miseria", "misil", "mismo", "mitad", "mito", "mochila", "moción", "moda", "modelo", "moho", "mojar", "molde", "moler", "molino", "momento", "momia", "monarca", "moneda", "monja", "monto", "moño", "morada", "morder", "moreno", "morir", "morro", "morsa", "mortal", "mosca", "mostrar", "motivo", "mover", "móvil", "mozo", "mucho", "mudar", "mueble", "muela", "muerte", "muestra", "mugre", "mujer", "mula", "muleta", "multa", "mundo", "muñeca", "mural", "muro", "músculo", "museo", "musgo", "música", "muslo", "nácar", "nación", "nadar", "naipe", "naranja", "nariz", "narrar", "nasal", "natal", "nativo", "natural", "náusea", "naval", "nave", "navidad", "necio", "néctar", "negar", "negocio", "negro", "neón", "nervio", "neto", "neutro", "nevar", "nevera", "nicho", "nido", "niebla", "nieto", "niñez", "niño", "nítido", "nivel", "nobleza", "noche", "nómina", "noria", "norma", "norte", "nota", "noticia", "novato", "novela", "novio", "nube", "nuca", "núcleo", "nudillo", "nudo", "nuera", "nueve", "nuez", "nulo", "número", "nutria", "oasis", "obeso", "obispo", "objeto", "obra", "obrero", "observar", "obtener", "obvio", "oca", "ocaso", "océano", "ochenta", "ocho", "ocio", "ocre", "octavo", "octubre", "oculto", "ocupar", "ocurrir", "odiar", "odio", "odisea", "oeste", "ofensa", "oferta", "oficio", "ofrecer", "ogro", "oído", "oír", "ojo", "ola", "oleada", "olfato", "olivo", "olla", "olmo", "olor", "olvido", "ombligo", "onda", "onza", "opaco", "opción", "ópera", "opinar", "oponer", "optar", "óptica", "opuesto", "oración", "orador", "oral", "órbita", "orca", "orden", "oreja", "órgano", "orgía", "orgullo", "oriente", "origen", "orilla", "oro", "orquesta", "oruga", "osadía", "oscuro", "osezno", "oso", "ostra", "otoño", "otro", "oveja", "óvulo", "óxido", "oxígeno", "oyente", "ozono", "pacto", "padre", "paella", "página", "pago", "país", "pájaro", "palabra", "palco", "paleta", "pálido", "palma", "paloma", "palpar", "pan", "panal", "pánico", "pantera", "pañuelo", "papá", "papel", "papilla", "paquete", "parar", "parcela", "pared", "parir", "paro", "párpado", "parque", "párrafo", "parte", "pasar", "paseo", "pasión", "paso", "pasta", "pata", "patio", "patria", "pausa", "pauta", "pavo", "payaso", "peatón", "pecado", "pecera", "pecho", "pedal", "pedir", "pegar", "peine", "pelar", "peldaño", "pelea", "peligro", "pellejo", "pelo", "peluca", "pena", "pensar", "peñón", "peón", "peor", "pepino", "pequeño", "pera", "percha", "perder", "pereza", "perfil", "perico", "perla", "permiso", "perro", "persona", "pesa", "pesca", "pésimo", "pestaña", "pétalo", "petróleo", "pez", "pezuña", "picar", "pichón", "pie", "piedra", "pierna", "pieza", "pijama", "pilar", "piloto", "pimienta", "pino", "pintor", "pinza", "piña", "piojo", "pipa", "pirata", "pisar", "piscina", "piso", "pista", "pitón", "pizca", "placa", "plan", "plata", "playa", "plaza", "pleito", "pleno", "plomo", "pluma", "plural", "pobre", "poco", "poder", "podio", "poema", "poesía", "poeta", "polen", "policía", "pollo", "polvo", "pomada", "pomelo", "pomo", "pompa", "poner", "porción", "portal", "posada", "poseer", "posible", "poste", "potencia", "potro", "pozo", "prado", "precoz", "pregunta", "premio", "prensa", "preso", "previo", "primo", "príncipe", "prisión", "privar", "proa", "probar", "proceso", "producto", "proeza", "profesor", "programa", "prole", "promesa", "pronto", "propio", "próximo", "prueba", "público", "puchero", "pudor", "pueblo", "puerta", "puesto", "pulga", "pulir", "pulmón", "pulpo", "pulso", "puma", "punto", "puñal", "puño", "pupa", "pupila", "puré", "quedar", "queja", "quemar", "querer", "queso", "quieto", "química", "quince", "quitar", "rábano", "rabia", "rabo", "ración", "radical", "raíz", "rama", "rampa", "rancho", "rango", "rapaz", "rápido", "rapto", "rasgo", "raspa", "rato", "rayo", "raza", "razón", "reacción", "realidad", "rebaño", "rebote", "recaer", "receta", "rechazo", "recoger", "recreo", "recto", "recurso", "red", "redondo", "reducir", "reflejo", "reforma", "refrán", "refugio", "regalo", "regir", "regla", "regreso", "rehén", "reino", "reír", "reja", "relato", "relevo", "relieve", "relleno", "reloj", "remar", "remedio", "remo", "rencor", "rendir", "renta", "reparto", "repetir", "reposo", "reptil", "res", "rescate", "resina", "respeto", "resto", "resumen", "retiro", "retorno", "retrato", "reunir", "revés", "revista", "rey", "rezar", "rico", "riego", "rienda", "riesgo", "rifa", "rígido", "rigor", "rincón", "riñón", "río", "riqueza", "risa", "ritmo", "rito", "rizo", "roble", "roce", "rociar", "rodar", "rodeo", "rodilla", "roer", "rojizo", "rojo", "romero", "romper", "ron", "ronco", "ronda", "ropa", "ropero", "rosa", "rosca", "rostro", "rotar", "rubí", "rubor", "rudo", "rueda", "rugir", "ruido", "ruina", "ruleta", "rulo", "rumbo", "rumor", "ruptura", "ruta", "rutina", "sábado", "saber", "sabio", "sable", "sacar", "sagaz", "sagrado", "sala", "saldo", "salero", "salir", "salmón", "salón", "salsa", "salto", "salud", "salvar", "samba", "sanción", "sandía", "sanear", "sangre", "sanidad", "sano", "santo", "sapo", "saque", "sardina", "sartén", "sastre", "satán", "sauna", "saxofón", "sección", "seco", "secreto", "secta", "sed", "seguir", "seis", "sello", "selva", "semana", "semilla", "senda", "sensor", "señal", "señor", "separar", "sepia", "sequía", "ser", "serie", "sermón", "servir", "sesenta", "sesión", "seta", "setenta", "severo", "sexo", "sexto", "sidra", "siesta", "siete", "siglo", "signo", "sílaba", "silbar", "silencio", "silla", "símbolo", "simio", "sirena", "sistema", "sitio", "situar", "sobre", "socio", "sodio", "sol", "solapa", "soldado", "soledad", "sólido", "soltar", "solución", "sombra", "sondeo", "sonido", "sonoro", "sonrisa", "sopa", "soplar", "soporte", "sordo", "sorpresa", "sorteo", "sostén", "sótano", "suave", "subir", "suceso", "sudor", "suegra", "suelo", "sueño", "suerte", "sufrir", "sujeto", "sultán", "sumar", "superar", "suplir", "suponer", "supremo", "sur", "surco", "sureño", "surgir", "susto", "sutil", "tabaco", "tabique", "tabla", "tabú", "taco", "tacto", "tajo", "talar", "talco", "talento", "talla", "talón", "tamaño", "tambor", "tango", "tanque", "tapa", "tapete", "tapia", "tapón", "taquilla", "tarde", "tarea", "tarifa", "tarjeta", "tarot", "tarro", "tarta", "tatuaje", "tauro", "taza", "tazón", "teatro", "techo", "tecla", "técnica", "tejado", "tejer", "tejido", "tela", "teléfono", "tema", "temor", "templo", "tenaz", "tender", "tener", "tenis", "tenso", "teoría", "terapia", "terco", "término", "ternura", "terror", "tesis", "tesoro", "testigo", "tetera", "texto", "tez", "tibio", "tiburón", "tiempo", "tienda", "tierra", "tieso", "tigre", "tijera", "tilde", "timbre", "tímido", "timo", "tinta", "tío", "típico", "tipo", "tira", "tirón", "titán", "títere", "título", "tiza", "toalla", "tobillo", "tocar", "tocino", "todo", "toga", "toldo", "tomar", "tono", "tonto", "topar", "tope", "toque", "tórax", "torero", "tormenta", "torneo", "toro", "torpedo", "torre", "torso", "tortuga", "tos", "tosco", "toser", "tóxico", "trabajo", "tractor", "traer", "tráfico", "trago", "traje", "tramo", "trance", "trato", "trauma", "trazar", "trébol", "tregua", "treinta", "tren", "trepar", "tres", "tribu", "trigo", "tripa", "triste", "triunfo", "trofeo", "trompa", "tronco", "tropa", "trote", "trozo", "truco", "trueno", "trufa", "tubería", "tubo", "tuerto", "tumba", "tumor", "túnel", "túnica", "turbina", "turismo", "turno", "tutor", "ubicar", "úlcera", "umbral", "unidad", "unir", "universo", "uno", "untar", "uña", "urbano", "urbe", "urgente", "urna", "usar", "usuario", "útil", "utopía", "uva", "vaca", "vacío", "vacuna", "vagar", "vago", "vaina", "vajilla", "vale", "válido", "valle", "valor", "válvula", "vampiro", "vara", "variar", "varón", "vaso", "vecino", "vector", "vehículo", "veinte", "vejez", "vela", "velero", "veloz", "vena", "vencer", "venda", "veneno", "vengar", "venir", "venta", "venus", "ver", "verano", "verbo", "verde", "vereda", "verja", "verso", "verter", "vía", "viaje", "vibrar", "vicio", "víctima", "vida", "vídeo", "vidrio", "viejo", "viernes", "vigor", "vil", "villa", "vinagre", "vino", "viñedo", "violín", "viral", "virgo", "virtud", "visor", "víspera", "vista", "vitamina", "viudo", "vivaz", "vivero", "vivir", "vivo", "volcán", "volumen", "volver", "voraz", "votar", "voto", "voz", "vuelo", "vulgar", "yacer", "yate", "yegua", "yema", "yerno", "yeso", "yodo", "yoga", "yogur", "zafiro", "zanja", "zapato", "zarza", "zona", "zorro", "zumo", "zurdo"] +# TODO: Re-enable once multi-language bip-39 wordlists are supported +@pytest.mark.skip def test_seed(): - seed = Seed(mnemonic="obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash") + seed = Seed(mnemonic="obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash".split()) assert seed.seed_bytes == b'q\xb3\xd1i\x0c\x9b\x9b\xdf\xa7\xd9\xd97H\xa8,\xa7\xd9>\xeck\xc2\xf5ND?, \x88-\x07\x9aa\xc5\xee\xb7\xbf\xc4x\xd6\x07 X\xb6}?M\xaa\x05\xa6\xa7(>\xbf\x03\xb0\x9d\xef\xed":\xdf\x88w7' @@ -18,7 +19,7 @@ def test_seed(): assert seed.passphrase == "" # TODO: Not yet supported in new implementation - seed.set_wordlist_language_code(SettingsConstants.WORDLIST_LANGUAGE__SPANISH) + seed.set_wordlist_language_code("es") assert seed.mnemonic_str == "natural ayuda futuro nivel espejo abuelo vago bien repetir moreno relevo conga" @@ -29,7 +30,7 @@ def test_seed(): assert seed.mnemonic_str == "height demise useless trap grow lion found off key clown transfer enroll" # TODO: Not yet supported in new implementation - seed.set_wordlist_language_code(SettingsConstants.WORDLIST_LANGUAGE__SPANISH) + seed.set_wordlist_language_code("es") assert seed.mnemonic_str == "hebilla cría truco tigre gris llenar folio negocio laico casa tieso eludir" diff --git a/tests/test_seedqr.py b/tests/test_seedqr.py index b7668bee..7e2d6385 100644 --- a/tests/test_seedqr.py +++ b/tests/test_seedqr.py @@ -9,6 +9,7 @@ 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) @@ -16,7 +17,7 @@ def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type): print(seed_phrase) assert len(seed_phrase) == mnemonic_length - e = EncodeQR(seed_phrase=seed_phrase, qr_type=qr_type, wordlist=bip39.WORDLIST) + e = EncodeQR(seed_phrase=seed_phrase, qr_type=qr_type) data = e.next_part() print(data) @@ -28,7 +29,7 @@ def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type): border=3 ) - decoder = DecodeQR(wordlist_language_code=SettingsConstants.LANGUAGE__ENGLISH) + decoder = DecodeQR() status = decoder.add_image(image) assert status == DecodeQRStatus.COMPLETE From 16a01ea4bda320cf7d29d2e3a35c04598449cba9 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 2 Apr 2022 15:43:38 -0500 Subject: [PATCH 03/10] Minor cleanup --- src/seedsigner/models/settings_definition.py | 2 +- .../views/old/settings_tools_view.py | 29 ------------------- tests/test_seed.py | 28 ++++++++---------- 3 files changed, 13 insertions(+), 46 deletions(-) delete mode 100644 src/seedsigner/views/old/settings_tools_view.py diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index 4687e1fa..3e91ef1b 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -375,7 +375,7 @@ class SettingsDefinition: type=SettingsConstants.TYPE__SELECT_1, visibility=SettingsConstants.VISIBILITY__ADVANCED, selection_options=SettingsConstants.ALL_NETWORKS, - default_value=SettingsConstants.REGTEST), # DEBUGGING! + default_value=SettingsConstants.MAINNET), SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, attr_name=SettingsConstants.SETTING__QR_DENSITY, diff --git a/src/seedsigner/views/old/settings_tools_view.py b/src/seedsigner/views/old/settings_tools_view.py deleted file mode 100644 index 464dcdaf..00000000 --- a/src/seedsigner/views/old/settings_tools_view.py +++ /dev/null @@ -1,29 +0,0 @@ -# SeedSigner file class dependencies -from . import View -from seedsigner.helpers import B, QR -from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay -from seedsigner.models import EncodeQR - - - -class SettingsToolsView(View): - def __init__(self) -> None: - View.__init__(self) - - self.qr = QR() - self.donate_image = None - self.derivation = None - - - ### Donate Menu Item - def display_donate_info_screen(self): - self.renderer.draw_modal(["You can support", "SeedSigner by donating", "any amount of BTC", "Thank You!!!"], "", "(Press right for a QR code)") - return True - - - def display_donate_qr(self): - self.renderer.draw_modal(["Loading..."]) - self.donate_image = self.qr.qrimage("bc1qphlyv2dde290tqdlnk8uswztnshw3x9rjurexqqhksvu7vdevhtsuw4efe") - self.renderer.show_image(self.donate_image) - return True - diff --git a/tests/test_seed.py b/tests/test_seed.py index 45e597eb..cbfa8964 100644 --- a/tests/test_seed.py +++ b/tests/test_seed.py @@ -7,8 +7,6 @@ from seedsigner.models.settings import SettingsConstants -# TODO: Re-enable once multi-language bip-39 wordlists are supported -@pytest.mark.skip def test_seed(): seed = Seed(mnemonic="obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash".split()) @@ -19,29 +17,27 @@ def test_seed(): assert seed.passphrase == "" # TODO: Not yet supported in new implementation - seed.set_wordlist_language_code("es") + # seed.set_wordlist_language_code("es") - assert seed.mnemonic_str == "natural ayuda futuro nivel espejo abuelo vago bien repetir moreno relevo conga" + # assert seed.mnemonic_str == "natural ayuda futuro nivel espejo abuelo vago bien repetir moreno relevo conga" - seed.set_wordlist_language_code(SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + # seed.set_wordlist_language_code(SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) - seed.mnemonic_str = "height demise useless trap grow lion found off key clown transfer enroll" + # seed.mnemonic_str = "height demise useless trap grow lion found off key clown transfer enroll" - assert seed.mnemonic_str == "height demise useless trap grow lion found off key clown transfer enroll" + # assert seed.mnemonic_str == "height demise useless trap grow lion found off key clown transfer enroll" - # TODO: Not yet supported in new implementation - seed.set_wordlist_language_code("es") + # # TODO: Not yet supported in new implementation + # seed.set_wordlist_language_code("es") - assert seed.mnemonic_str == "hebilla cría truco tigre gris llenar folio negocio laico casa tieso eludir" - - seed.set_passphrase("test") - - assert seed.seed_bytes == b'\xdd\r\xcb\x0b V\xb4@\xee+\x01`\xabem\xc1B\xfd\x8fba0\xab;[\xab\xc9\xf9\xba[F\x0c5,\x7fd8\xebI\x90"\xb8\x86C\x821\x01\xdb\xbe\xf3\xbc\x1cBH"%\x18\xc2{\x04\x08a]\xa5' - - assert seed.passphrase == "test" + # assert seed.mnemonic_str == "hebilla cría truco tigre gris llenar folio negocio laico casa tieso eludir" + # seed.set_passphrase("test") + # assert seed.seed_bytes == b'\xdd\r\xcb\x0b V\xb4@\xee+\x01`\xabem\xc1B\xfd\x8fba0\xab;[\xab\xc9\xf9\xba[F\x0c5,\x7fd8\xebI\x90"\xb8\x86C\x821\x01\xdb\xbe\xf3\xbc\x1cBH"%\x18\xc2{\x04\x08a]\xa5' + # assert seed.passphrase == "test" + From 520cb61369bd445ee09a33279653e41c13d4f76f Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 3 Apr 2022 16:52:30 -0500 Subject: [PATCH 04/10] Improved I/O Test screen --- requirements.txt | 2 +- src/seedsigner/gui/components.py | 3 +- src/seedsigner/gui/renderer.py | 4 +- src/seedsigner/gui/screens/scan_screens.py | 68 +++++---- .../gui/screens/settings_screens.py | 141 ++++++++++++++---- src/seedsigner/views/settings_views.py | 7 +- 6 files changed, 156 insertions(+), 69 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7055026a..1fb2f7b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ embit==0.4.10 numpy==1.21.1 picamera==1.13 Pillow==8.2.0 --e git+https://github.com/kdmukai/pyzbar.git@c3c237821c6a20b17953efe59b90df0b514a1c03#egg=pyzbar +-e git+https://github.com/seedsigner/pyzbar.git@c3c237821c6a20b17953efe59b90df0b514a1c03#egg=pyzbar qrcode==7.3.1 RPi.GPIO==0.7.0 six==1.16.0 diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index 18bff593..ef1c6815 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -257,7 +257,7 @@ class TextArea(BaseComponent): screen_x: int = 0 screen_y: int = 0 min_text_x: int = None - background_color: str = "black" + background_color: str = GUIConstants.BACKGROUND_COLOR font_name: str = GUIConstants.BODY_FONT_NAME font_size: int = GUIConstants.BODY_FONT_SIZE font_color: str = GUIConstants.BODY_FONT_COLOR @@ -1168,6 +1168,7 @@ class IconButton(Button): icon_size: int = GUIConstants.ICON_INLINE_FONT_SIZE text: str = None is_icon_inline: bool = False + is_text_centered: bool = True diff --git a/src/seedsigner/gui/renderer.py b/src/seedsigner/gui/renderer.py index cd3d031c..1191eeec 100644 --- a/src/seedsigner/gui/renderer.py +++ b/src/seedsigner/gui/renderer.py @@ -11,8 +11,8 @@ class Renderer(ConfigurableSingleton): buttons = None canvas_width = 0 canvas_height = 0 - canvas: Image = None - draw: ImageDraw = None + canvas: Image.Image = None + draw: ImageDraw.ImageDraw = None disp = None lock = Lock() diff --git a/src/seedsigner/gui/screens/scan_screens.py b/src/seedsigner/gui/screens/scan_screens.py index 1677b49f..1b29af27 100644 --- a/src/seedsigner/gui/screens/scan_screens.py +++ b/src/seedsigner/gui/screens/scan_screens.py @@ -21,7 +21,8 @@ class ScanScreen(BaseScreen): instructions_text: str = "Scan a QR code" resolution: Tuple[int,int] = (480, 480) framerate: int = 12 - auto_deactivate_buttons: bool = False # Used by the I/O test screen + render_rect: Tuple[int,int,int,int] = None + def __post_init__(self): from seedsigner.hardware.camera import Camera @@ -36,25 +37,35 @@ class ScanScreen(BaseScreen): decoder=self.decoder, renderer=self.renderer, instructions_text=self.instructions_text, - components=self.components, - auto_deactivate_buttons=self.auto_deactivate_buttons, + render_rect=self.render_rect, )) class LivePreviewThread(BaseThread): - def __init__(self, camera: Camera, decoder: DecodeQR, renderer: renderer, instructions_text: str, components: List[BaseComponent], auto_deactivate_buttons: bool): + def __init__(self, camera: Camera, decoder: DecodeQR, renderer: renderer.Renderer, instructions_text: str, render_rect: Tuple[int,int,int,int]): self.camera = camera self.decoder = decoder self.renderer = renderer self.instructions_text = instructions_text - self.components = components - self.auto_deactivate_buttons = auto_deactivate_buttons + if render_rect: + self.render_rect = render_rect + else: + self.render_rect = (0, 0, self.renderer.canvas_width, self.renderer.canvas_height) + self.render_width = self.render_rect[2] - self.render_rect[0] + self.render_height = self.render_rect[3] - self.render_rect[1] + + print(f"render_width: {self.render_width}") + print(f"render_height: {self.render_height}") + super().__init__() def run(self): + from timeit import default_timer as timer + instructions_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE) while self.keep_running: + start = timer() frame = self.camera.read_video_stream(as_image=True) if frame is not None: scan_text = self.instructions_text @@ -62,40 +73,35 @@ class ScanScreen(BaseScreen): scan_text = str(self.decoder.get_percent_complete()) + "% Complete" with self.renderer.lock: - if frame.width > self.renderer.canvas_width or frame.height > self.renderer.canvas_height: + if frame.width > self.render_width or frame.height > self.render_height: frame = frame.resize( - (self.renderer.canvas_width, self.renderer.canvas_height) + (self.render_width, self.render_height) ) self.renderer.canvas.paste( frame, - (0, 0) + (self.render_rect[0], self.render_rect[1]) ) - self.renderer.draw.text( - xy=( - int(self.renderer.canvas_width/2), - self.renderer.canvas_height - GUIConstants.EDGE_PADDING - ), - text=scan_text, - fill=GUIConstants.BODY_FONT_COLOR, - font=instructions_font, - stroke_width=4, - stroke_fill=GUIConstants.BACKGROUND_COLOR, - anchor="ms" - ) - - # Need to re-render all onscreen UI components since we just overwrote - # the screen. - for component in self.components: - component.render() - if self.auto_deactivate_buttons and type(component) in [Button, IconButton]: - if component.is_selected: - # deactivate for the next round - component.is_selected = False + if scan_text: + self.renderer.draw.text( + xy=( + int(self.renderer.canvas_width/2), + self.renderer.canvas_height - GUIConstants.EDGE_PADDING + ), + text=scan_text, + fill=GUIConstants.BODY_FONT_COLOR, + font=instructions_font, + stroke_width=4, + stroke_fill=GUIConstants.BACKGROUND_COLOR, + anchor="ms" + ) self.renderer.show_image() - time.sleep(0.1) # turn this up or down to tune performance while decoding psbt + end = timer() + # print(f"{1.0/(end - start)} fps") # Time in seconds, e.g. 5.38091952400282 + + time.sleep(0.05) # turn this up or down to tune performance while decoding psbt if self.camera._video_stream is None: break diff --git a/src/seedsigner/gui/screens/settings_screens.py b/src/seedsigner/gui/screens/settings_screens.py index 7bfc729c..c09f1e27 100644 --- a/src/seedsigner/gui/screens/settings_screens.py +++ b/src/seedsigner/gui/screens/settings_screens.py @@ -1,12 +1,14 @@ import time from dataclasses import dataclass +from PIL.ImageOps import autocontrast from typing import List -from seedsigner.gui.components import Button, CheckboxButton, CheckedSelectionButton, FontAwesomeIconConstants, GUIConstants, IconButton, TextArea +from seedsigner.gui.components import Button, CheckboxButton, CheckedSelectionButton, FontAwesomeIconConstants, Fonts, GUIConstants, Icon, IconButton, IconTextLine, TextArea from seedsigner.gui.screens.scan_screens import ScanScreen -from seedsigner.gui.screens.screen import BaseTopNavScreen, ButtonListScreen +from seedsigner.gui.screens.screen import BaseScreen, BaseTopNavScreen, ButtonListScreen from seedsigner.hardware.buttons import HardwareButtonsConstants +from seedsigner.hardware.camera import Camera from seedsigner.models.settings import SettingsConstants @@ -48,14 +50,18 @@ class SettingsEntryUpdateSelectionScreen(ButtonListScreen): @dataclass -class IOTestScreen(ScanScreen): +class IOTestScreen(BaseTopNavScreen): def __post_init__(self): + self.title = "I/O Test" + self.show_back_button = False + self.resolution = (96, 96) + self.framerate = 10 + self.instructions_text = None super().__post_init__() # D-pad pictogram - input_button_width = GUIConstants.BUTTON_HEIGHT - input_button_height = input_button_width - + input_button_width = GUIConstants.BUTTON_HEIGHT + 2 + input_button_height = input_button_width + 2 dpad_center_x = GUIConstants.EDGE_PADDING + input_button_width + GUIConstants.COMPONENT_PADDING dpad_center_y = int((self.canvas_height - input_button_height)/2) @@ -73,7 +79,6 @@ class IOTestScreen(ScanScreen): self.joystick_up_button = IconButton( icon_name=FontAwesomeIconConstants.ANGLE_UP, icon_size=GUIConstants.ICON_INLINE_FONT_SIZE, - is_text_centered=True, width=input_button_width, height=input_button_height, screen_x=dpad_center_x, @@ -116,22 +121,30 @@ class IOTestScreen(ScanScreen): ) self.components.append(self.joystick_right_button) - key_button_width = int(1.75*GUIConstants.BUTTON_HEIGHT) - key_button_height = int(0.85*GUIConstants.BUTTON_HEIGHT) + # Hardware keys UI + font = Fonts.get_font(GUIConstants.BUTTON_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE) + (left, top, text_width, bottom) = font.getbbox(text="Clear", anchor="ls") + icon = Icon( + icon_name=FontAwesomeIconConstants.CAMERA, + icon_size=GUIConstants.ICON_INLINE_FONT_SIZE, + ) + key_button_width = text_width + 2*GUIConstants.COMPONENT_PADDING + GUIConstants.EDGE_PADDING + key_button_height = icon.height + int(1.5*GUIConstants.COMPONENT_PADDING) key2_y = int(self.canvas_height/2) - int(key_button_height/2) self.key2_button = Button( - text=" ", + text="Clear", # Initialize with text to set vertical centering width=key_button_width, height=key_button_height, screen_x=self.canvas_width - key_button_width + GUIConstants.EDGE_PADDING, screen_y=key2_y, outline_color=GUIConstants.ACCENT_COLOR, ) + self.key2_button.text = " " # but default state is empty self.components.append(self.key2_button) - self.key1_button = Button( - text=" ", + self.key1_button = IconButton( + icon_name=FontAwesomeIconConstants.CAMERA, width=key_button_width, height=key_button_height, screen_x=self.canvas_width - key_button_width + GUIConstants.EDGE_PADDING, @@ -153,10 +166,90 @@ class IOTestScreen(ScanScreen): def _run(self): cur_selected_button = self.key1_button + msg_height = GUIConstants.ICON_LARGE_BUTTON_SIZE + 2*GUIConstants.COMPONENT_PADDING + camera_message = TextArea( + text="Capturing image...", + font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE, + is_text_centered=True, + height=msg_height, + screen_y=int((self.canvas_height - msg_height)/ 2), + ) while True: input = self.hw_inputs.wait_for(keys=HardwareButtonsConstants.ALL_KEYS, check_release=False) - if input == HardwareButtonsConstants.KEY_PRESS: + if input == HardwareButtonsConstants.KEY1: + cur_selected_button = self.key1_button + + with self.renderer.lock: + cur_selected_button.is_selected = True + cur_selected_button.render() + camera_message.render() + # Render edges around message box + self.image_draw.rectangle( + ( + -1, int((self.canvas_height - msg_height)/ 2) - 1, + self.canvas_width + 1, int((self.canvas_height + msg_height)/ 2) + 1 + ), + outline=GUIConstants.ACCENT_COLOR, + width=1, + ) + self.renderer.show_image() + + # Snap a pic, render it as the background, re-render all onscreen elements + camera = Camera.get_instance() + try: + camera.start_single_frame_mode(resolution=(self.canvas_width, self.canvas_height)) + + # Reset the button state + with self.renderer.lock: + cur_selected_button.is_selected = False + cur_selected_button.render() + self.renderer.show_image() + + time.sleep(0.25) + background_frame = camera.capture_frame() + display_version = autocontrast( + background_frame, + cutoff=2 + ) + with self.renderer.lock: + self.canvas.paste(display_version, (0, self.top_nav.height)) + self.key2_button.text = "Clear" + for component in self.components: + component.render() + self.renderer.show_image() + finally: + camera.stop_single_frame_mode() + + continue + + elif input == HardwareButtonsConstants.KEY2: + cur_selected_button = self.key2_button + + # Clear the background + with self.renderer.lock: + cur_selected_button.is_selected = True + self._render() + self.renderer.show_image() + + # And then re-render Key2 in its initial state + self.key2_button.text = " " + cur_selected_button.is_selected = False + cur_selected_button.render() + self.renderer.show_image() + + continue + + elif input == HardwareButtonsConstants.KEY3: + # Exit + cur_selected_button = self.key3_button + cur_selected_button.is_selected = True + with self.renderer.lock: + cur_selected_button.render() + self.renderer.show_image() + return + + elif input == HardwareButtonsConstants.KEY_PRESS: cur_selected_button = self.joystick_click_button elif input == HardwareButtonsConstants.KEY_UP: @@ -171,23 +264,15 @@ class IOTestScreen(ScanScreen): elif input == HardwareButtonsConstants.KEY_RIGHT: cur_selected_button = self.joystick_right_button - elif input == HardwareButtonsConstants.KEY1: - cur_selected_button = self.key1_button - - elif input == HardwareButtonsConstants.KEY2: - cur_selected_button = self.key2_button - - elif input == HardwareButtonsConstants.KEY3: - # Exit - self.camera.stop_video_stream_mode() - cur_selected_button = self.key3_button + with self.renderer.lock: cur_selected_button.is_selected = True - with self.renderer.lock: - cur_selected_button.render() - self.renderer.show_image() - return + cur_selected_button.render() + self.renderer.show_image() - cur_selected_button.is_selected = True + with self.renderer.lock: + cur_selected_button.is_selected = False + cur_selected_button.render() + self.renderer.show_image() time.sleep(0.1) diff --git a/src/seedsigner/views/settings_views.py b/src/seedsigner/views/settings_views.py index 5a705e08..9aeb0dcb 100644 --- a/src/seedsigner/views/settings_views.py +++ b/src/seedsigner/views/settings_views.py @@ -178,12 +178,7 @@ class SettingsEntryUpdateSelectionView(View): ****************************************************************************""" class IOTestView(View): def run(self): - settings_screens.IOTestScreen( - instructions_text="Live camera feed", - resolution=(240,240), - framerate=24, - auto_deactivate_buttons=True, - ).display() + settings_screens.IOTestScreen().display() return Destination(SettingsMenuView) From 089e0e41e860db793afc65f00527dd963a257232 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 3 Apr 2022 17:22:26 -0500 Subject: [PATCH 05/10] PSBT Overview rendering fix * Cancel from LoadMultisigWalletDescriptorView correctly resumes PSBT flow. --- src/seedsigner/gui/screens/psbt_screens.py | 6 ++++-- src/seedsigner/views/seed_views.py | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/seedsigner/gui/screens/psbt_screens.py b/src/seedsigner/gui/screens/psbt_screens.py index 62b606ef..89bef6b1 100644 --- a/src/seedsigner/gui/screens/psbt_screens.py +++ b/src/seedsigner/gui/screens/psbt_screens.py @@ -69,8 +69,8 @@ class PSBTOverviewScreen(ButtonListScreen): font_size = GUIConstants.BODY_FONT_MIN_SIZE * ssf font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, font_size) - # Measure from anchor "top" to include font pixels below baseline (e.g. the "g" in "change") - (left, top, right, chart_text_height) = font.getbbox(text="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890", anchor="lt") + (left, top, right, bottom) = font.getbbox(text="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890[]", anchor="lt") + chart_text_height = bottom vertical_center = int(image.height/2) # Supersampling renders thin elements poorly if they land on an even line before scaling down if vertical_center % 2 == 1: @@ -197,6 +197,7 @@ class PSBTOverviewScreen(ButtonListScreen): text=input, font=font, fill=chart_font_color, + anchor="lt", ) # Render the association line to the conjunction point @@ -288,6 +289,7 @@ class PSBTOverviewScreen(ButtonListScreen): text=destination, font=font, fill=chart_font_color, + anchor="lt" ) # Render the association line from the conjunction point diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 1179af00..77edd540 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -1468,6 +1468,12 @@ class LoadMultisigWalletDescriptorView(View): if button_data[selected_menu_num] == SCAN: return Destination(ScanView) + + elif button_data[selected_menu_num] == CANCEL: + if self.controller.resume_main_flow == Controller.FLOW__PSBT: + return Destination(BackStackView) + else: + return Destination(MainMenuView) From 0ed1ca1174aac3a688e9e8062c399f2cdb5ae5ed Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 4 Apr 2022 08:21:17 -0500 Subject: [PATCH 06/10] Mnemonic backup verification flow --- src/seedsigner/gui/components.py | 2 +- src/seedsigner/gui/screens/seed_screens.py | 16 +++ src/seedsigner/views/seed_views.py | 159 +++++++++++++++++++-- 3 files changed, 167 insertions(+), 10 deletions(-) diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index ef1c6815..2457fef1 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -32,7 +32,7 @@ class GUIConstants: ICON_FONT_SIZE = 22 ICON_INLINE_FONT_SIZE = 24 ICON_LARGE_BUTTON_SIZE = 36 - ICON_PRIMARY_SCREEN_SIZE = 60 + ICON_PRIMARY_SCREEN_SIZE = 50 TOP_NAV_TITLE_FONT_NAME = "OpenSans-SemiBold" TOP_NAV_TITLE_FONT_SIZE = 20 diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index 60251979..b86cbf17 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -536,6 +536,22 @@ class SeedWordsScreen(WarningEdgesMixin, ButtonListScreen): +@dataclass +class SeedWordsBackupTestPromptScreen(ButtonListScreen): + def __post_init__(self): + self.title = "Verify Backup?" + self.show_back_button = False + self.is_bottom_list = True + super().__post_init__() + + self.components.append(TextArea( + text="Optionally verify that your mnemonic backup is correct.", + screen_y=self.top_nav.height, + is_text_centered=True, + )) + + + @dataclass class SeedExportXpubCustomDerivationScreen(BaseTopNavScreen): title: str = "Derivation Path" diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 77edd540..65d813d6 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -1,14 +1,19 @@ import embit +import random import time -from typing import List from binascii import hexlify +from embit import bip39 from embit.descriptor import Descriptor from embit.networks import NETWORKS -from seedsigner.controller import Controller -from seedsigner.models.decode_qr import DecodeQR +from typing import List +from seedsigner.controller import Controller from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustomIconConstants +from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, + WarningScreen, DireWarningScreen, seed_screens) +from seedsigner.gui.screens.screen import LargeIconStatusScreen, LoadingScreenThread, QRDisplayScreen +from seedsigner.models.decode_qr import DecodeQR from seedsigner.models.encode_qr import EncodeQR from seedsigner.models.psbt_parser import PSBTParser from seedsigner.models.qr_type import QRType @@ -16,9 +21,6 @@ from seedsigner.models.seed import InvalidSeedException, Seed from seedsigner.models.settings import SettingsConstants from seedsigner.models.settings_definition import SettingsDefinition from seedsigner.models.threads import BaseThread, ThreadsafeCounter -from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, - WarningScreen, DireWarningScreen, seed_screens) -from seedsigner.gui.screens.screen import LargeIconStatusScreen, LoadingScreenThread, QRDisplayScreen from seedsigner.views.psbt_views import PSBTChangeDetailsView from seedsigner.views.scan_views import ScanView @@ -808,13 +810,152 @@ class SeedWordsView(View): if button_data[selected_menu_num] == NEXT: if self.seed_num is None and self.page_index == self.num_pages - 1: - return Destination(SeedFinalizeView) + return Destination(SeedWordsBackupTestPromptView, view_args=dict(seed_num=self.seed_num)) else: - return Destination(SeedWordsView, view_args={"seed_num": self.seed_num, "page_index": self.page_index + 1}) + return Destination(SeedWordsView, view_args=dict(seed_num=self.seed_num, page_index=self.page_index + 1)) elif button_data[selected_menu_num] == DONE: # Must clear history to avoid BACK button returning to private info - return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num}, clear_history=True) + return Destination(SeedWordsBackupTestPromptView, view_args=dict(seed_num=self.seed_num)) + + + +"""**************************************************************************** + Seed Words Backup Test +****************************************************************************""" +class SeedWordsBackupTestPromptView(View): + def __init__(self, seed_num: int): + self.seed_num = seed_num + + + def run(self): + VERIFY = "Verify" + SKIP = "Skip" + button_data = [VERIFY, SKIP] + selected_menu_num = seed_screens.SeedWordsBackupTestPromptScreen( + button_data=button_data, + ).display() + + if button_data[selected_menu_num] == VERIFY: + return Destination(SeedWordsBackupTestView, view_args=dict(seed_num=self.seed_num)) + + elif button_data[selected_menu_num] == SKIP: + if self.seed_num is not None: + return Destination(SeedOptionsView, view_args=dict(seed_num=self.seed_num)) + else: + return Destination(SeedFinalizeView) + + + +class SeedWordsBackupTestView(View): + def __init__(self, seed_num: int, confirmed_list: List[bool] = None, cur_index: int = None): + super().__init__() + self.seed_num = seed_num + if self.seed_num is None: + self.seed = self.controller.storage.get_pending_seed() + else: + self.seed = self.controller.get_seed(self.seed_num) + + self.wordlist = self.seed.mnemonic_display_list + self.confirmed_list = confirmed_list + if not self.confirmed_list: + self.confirmed_list = [] + + self.cur_index = cur_index + + + def run(self): + if self.cur_index is None: + self.cur_index = int(random.random() * len(self.wordlist)) + while self.cur_index in self.confirmed_list: + self.cur_index = int(random.random() * len(self.wordlist)) + + real_word = self.wordlist[self.cur_index] + fake_word1 = bip39.WORDLIST[int(random.random() * 2048)] + fake_word2 = bip39.WORDLIST[int(random.random() * 2048)] + fake_word3 = bip39.WORDLIST[int(random.random() * 2048)] + + button_data = [real_word, fake_word1, fake_word2, fake_word3] + random.shuffle(button_data) + + selected_menu_num = ButtonListScreen( + title=f"Verify Word #{self.cur_index + 1}", + show_back_button=False, + button_data=button_data, + is_bottom_list=True, + is_button_text_centered=True, + ).display() + + if button_data[selected_menu_num] == real_word: + self.confirmed_list.append(self.cur_index) + if len(self.confirmed_list) == len(self.wordlist): + # Successfully confirmed the full mnemonic! + return Destination(SeedWordsBackupTestSuccessView, view_args=dict(seed_num=self.seed_num)) + else: + # Continue testing the remaining words + return Destination(SeedWordsBackupTestView, view_args=dict(seed_num=self.seed_num, confirmed_list=self.confirmed_list)) + + else: + # Picked the WRONG WORD! + return Destination( + SeedWordsBackupTestMistakeView, + view_args=dict( + seed_num=self.seed_num, + cur_index=self.cur_index, + wrong_word=button_data[selected_menu_num], + confirmed_list=self.confirmed_list, + ) + ) + + + +class SeedWordsBackupTestMistakeView(View): + def __init__(self, seed_num: int, cur_index: int, wrong_word: str, confirmed_list: List[bool] = None): + super().__init__() + self.seed_num = seed_num + self.cur_index = cur_index + self.wrong_word = wrong_word + self.confirmed_list = confirmed_list + + + def run(self): + REVIEW = "Review Seed Words" + RETRY = "Try Again" + button_data = [REVIEW, RETRY] + + selected_menu_num = DireWarningScreen( + title="Verification Error", + show_back_button=False, + status_headline=f"Wrong Word!", + text=f"Word #{self.cur_index + 1} is not \"{self.wrong_word}\"!", + button_data=button_data, + ).display() + + if button_data[selected_menu_num] == REVIEW: + return Destination(SeedWordsView, view_args=dict(seed_num=self.seed_num)) + + elif button_data[selected_menu_num] == RETRY: + return Destination(SeedWordsBackupTestView, view_args=dict(seed_num=self.seed_num, confirmed_list=self.confirmed_list, cur_index=self.cur_index)) + + + +class SeedWordsBackupTestSuccessView(View): + def __init__(self, seed_num: int): + self.seed_num = seed_num + + def run(self): + LargeIconStatusScreen( + title="Backup Verified", + show_back_button=False, + status_headline="Success!", + text="All mnemonic backup words were successfully verified!", + button_data=["OK"] + ).display() + + if self.seed_num is not None: + return Destination(SeedOptionsView, view_args=dict(seed_num=self.seed_num), clear_history=True) + else: + return Destination(SeedFinalizeView) From dc64147e5ec96e3cacb9263b19b99d47dcf90b6f Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 4 Apr 2022 08:57:59 -0500 Subject: [PATCH 07/10] Minor improvements, bugfix --- src/seedsigner/views/seed_views.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 65d813d6..c4dc93fe 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -856,7 +856,7 @@ class SeedWordsBackupTestView(View): else: self.seed = self.controller.get_seed(self.seed_num) - self.wordlist = self.seed.mnemonic_display_list + self.mnemonic_list = self.seed.mnemonic_display_list self.confirmed_list = confirmed_list if not self.confirmed_list: self.confirmed_list = [] @@ -866,14 +866,14 @@ class SeedWordsBackupTestView(View): def run(self): if self.cur_index is None: - self.cur_index = int(random.random() * len(self.wordlist)) + self.cur_index = int(random.random() * len(self.mnemonic_list)) while self.cur_index in self.confirmed_list: - self.cur_index = int(random.random() * len(self.wordlist)) + self.cur_index = int(random.random() * len(self.mnemonic_list)) - real_word = self.wordlist[self.cur_index] - fake_word1 = bip39.WORDLIST[int(random.random() * 2048)] - fake_word2 = bip39.WORDLIST[int(random.random() * 2048)] - fake_word3 = bip39.WORDLIST[int(random.random() * 2048)] + real_word = self.mnemonic_list[self.cur_index] + fake_word1 = bip39.WORDLIST[int(random.random() * 2047)] + fake_word2 = bip39.WORDLIST[int(random.random() * 2047)] + fake_word3 = bip39.WORDLIST[int(random.random() * 2047)] button_data = [real_word, fake_word1, fake_word2, fake_word3] random.shuffle(button_data) @@ -888,7 +888,7 @@ class SeedWordsBackupTestView(View): if button_data[selected_menu_num] == real_word: self.confirmed_list.append(self.cur_index) - if len(self.confirmed_list) == len(self.wordlist): + if len(self.confirmed_list) == len(self.mnemonic_list): # Successfully confirmed the full mnemonic! return Destination(SeedWordsBackupTestSuccessView, view_args=dict(seed_num=self.seed_num)) else: From 01812b4c404da9597df2cb1fb6714232c05c7713 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 4 Apr 2022 12:43:09 -0500 Subject: [PATCH 08/10] HRF logo on startup --- src/seedsigner/controller.py | 57 ++---------- src/seedsigner/gui/components.py | 2 +- src/seedsigner/models/settings_definition.py | 7 ++ .../resources/img/partners/hrf_logo.png | Bin 0 -> 7234 bytes src/seedsigner/views/screensaver.py | 84 ++++++++++++++---- 5 files changed, 78 insertions(+), 72 deletions(-) create mode 100644 src/seedsigner/resources/img/partners/hrf_logo.png diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 8c91e9d4..a03a160b 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -7,9 +7,8 @@ from PIL.Image import Image from typing import List from seedsigner.gui.renderer import Renderer -from seedsigner.gui.screens.screen import WarningScreen from seedsigner.hardware.buttons import HardwareButtons -from seedsigner.views.screensaver import ScreensaverView +from seedsigner.views.screensaver import ScreensaverScreen from seedsigner.views.view import Destination, NotYetImplementedView, UnhandledExceptionView from .models import Seed, SeedStorage, Settings, Singleton, PSBTParser @@ -81,7 +80,7 @@ class Controller(Singleton): resume_main_flow: str = None back_stack: BackStack = None - screensaver: ScreensaverView = None + screensaver: ScreensaverScreen = None @classmethod @@ -132,7 +131,7 @@ class Controller(Singleton): # Configure the Renderer Renderer.configure_instance() - controller.screensaver = ScreensaverView(controller.buttons) + controller.screensaver = ScreensaverScreen(controller.buttons) controller.back_stack = BackStack() @@ -180,9 +179,9 @@ class Controller(Singleton): def start(self) -> None: from .views import MainMenuView, BackStackView - from .views.screensaver import OpeningSplashView + from .views.screensaver import OpeningSplashScreen - opening_splash = OpeningSplashView() + opening_splash = OpeningSplashScreen() opening_splash.start() """ Class references can be stored as variables in python! @@ -315,49 +314,3 @@ class Controller(Singleton): exception_msg, ] return Destination(UnhandledExceptionView, view_args={"error": error}, clear_history=True) - -""" - - - - ### - ### Seed Tools Controller Naviation/Launcher - ### - - - ### Create a Seed w/ Dice Screen - - def show_create_seed_with_dice_tool(self) -> int: - seed = Seed(wordlist=self.settings.wordlist) - ret_val = True - - while True: - seed.mnemonic = self.seed_tools_view.display_generate_seed_from_dice() - if seed: - break - else: - return Path.SEED_TOOLS_SUB_MENU - - # display seed phrase (24 words) - while True: - ret_val = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, show_qr_option=True) - if ret_val == True: - break - else: - # no-op; can't back out of the seed phrase view - pass - - # Ask to save seed - if self.storage.slot_avaliable(): - r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Save Seed?") - if r == 1: #Yes - slot_num = self.menu_view.display_saved_seed_menu(self.storage,2,None) - if slot_num in (1,2,3): - self.storage.add_seed(seed, slot_num) - self.renderer.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Main Menu") - input = self.buttons.wait_for([B.KEY_RIGHT]) - - return Path.MAIN_MENU - - -""" \ No newline at end of file diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index 2457fef1..ebd451ea 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -169,7 +169,7 @@ def load_icon(icon_name: str, load_selected_variant: bool = False): -def load_image(image_name: str): +def load_image(image_name: str) -> Image.Image: image_url = os.path.join(pathlib.Path(__file__).parent.resolve(), "..", "resources", "img", image_name) image = Image.open(image_url).convert("RGB") return image diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index 3e91ef1b..f1f12af6 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -154,6 +154,7 @@ class SettingsConstants: SETTING__COMPACT_SEEDQR = "compact_seedqr" SETTING__PRIVACY_WARNINGS = "privacy_warnings" SETTING__DIRE_WARNINGS = "dire_warnings" + SETTING__PARTNER_LOGOS = "partner_logos" SETTING__DEBUG = "debug" @@ -447,6 +448,12 @@ class SettingsDefinition: visibility=SettingsConstants.VISIBILITY__ADVANCED, default_value=SettingsConstants.OPTION__ENABLED), + SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, + attr_name=SettingsConstants.SETTING__PARTNER_LOGOS, + display_name="Show partner logos", + visibility=SettingsConstants.VISIBILITY__ADVANCED, + default_value=SettingsConstants.OPTION__ENABLED), + # Developer options # TODO: No real Developer options needed yet. Disable for now. # SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM, diff --git a/src/seedsigner/resources/img/partners/hrf_logo.png b/src/seedsigner/resources/img/partners/hrf_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2d08a3bbbfc109ca65e5d63dc4aa811955f15486 GIT binary patch literal 7234 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 literal 0 HcmV?d00001 diff --git a/src/seedsigner/views/screensaver.py b/src/seedsigner/views/screensaver.py index bdb5bff5..29fb8da4 100644 --- a/src/seedsigner/views/screensaver.py +++ b/src/seedsigner/views/screensaver.py @@ -2,49 +2,95 @@ import os import random import time -from PIL import Image, ImageDraw - -from .view import View +from PIL import Image from seedsigner.gui.components import Fonts, GUIConstants, load_image +from seedsigner.gui.screens.screen import BaseScreen +from seedsigner.models.settings import Settings +from seedsigner.models.settings_definition import SettingsConstants -# TODO: Should be derived from View? -class LogoView: +# TODO: This early code is now outdated vis-a-vis Screen vs View distinctions +class LogoScreen(BaseScreen): def __init__(self): - from seedsigner.gui import Renderer - self.renderer = Renderer.get_instance() + super().__init__() self.logo = load_image("logo_black_240.png") + self.partners = [ + "hrf", + ] + + self.partner_logos: dict = {} + for partner in self.partners: + logo_url = os.path.join("partners", f"{partner}_logo.png") + self.partner_logos[partner] = load_image(logo_url) -class OpeningSplashView(LogoView): + def get_random_partner(self) -> str: + return self.partners[random.randrange(len(self.partners))] + + + +class OpeningSplashScreen(LogoScreen): def start(self): from seedsigner.controller import Controller controller = Controller.get_instance() + show_partner_logos = Settings.get_instance().get_value(SettingsConstants.SETTING__PARTNER_LOGOS) == SettingsConstants.OPTION__ENABLED + + if show_partner_logos: + logo_offset_y = -56 + else: + logo_offset_y = 0 + # Fade in alpha for i in range(250, -1, -25): self.logo.putalpha(255 - i) - background = Image.new("RGBA", self.logo.size, (0,0,0)) - self.renderer.disp.ShowImage(Image.alpha_composite(background, self.logo), 0, 0) + background = Image.new("RGBA", size=self.logo.size, color="black") + self.renderer.canvas.paste(Image.alpha_composite(background, self.logo), (0, logo_offset_y)) + self.renderer.show_image() - # Display version num and hold for a few seconds + # Display version num below SeedSigner logo font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.TOP_NAV_TITLE_FONT_SIZE) version = f"v{controller.VERSION}" - tw, th = font.getsize(version) - x = int((self.renderer.canvas_width - tw) / 2) - y = int(self.renderer.canvas_height / 2) + 40 + (left, top, version_tw, version_th) = font.getbbox(version, anchor="lt") - draw = ImageDraw.Draw(self.logo) - draw.text((x, y), version, fill=GUIConstants.ACCENT_COLOR, font=font) - self.renderer.show_image(self.logo) - time.sleep(3) + # The logo png is 240x240, but the actual logo is 70px tall, vertically centered + version_x = int(self.renderer.canvas_width/2) + version_y = int(self.canvas_height/2) + 35 + logo_offset_y + GUIConstants.COMPONENT_PADDING + self.renderer.draw.text(xy=(version_x, version_y), text=version, font=font, fill=GUIConstants.ACCENT_COLOR, anchor="mt") + self.renderer.show_image() + + if show_partner_logos: + # Hold on the version num for a moment + time.sleep(1) + + # Set up the partner logo + partner_logo: Image.Image = self.partner_logos[self.get_random_partner()] + font = Fonts.get_font(GUIConstants.TOP_NAV_TITLE_FONT_NAME, GUIConstants.BODY_FONT_SIZE) + sponsor_text = "With support from:" + (left, top, tw, th) = font.getbbox(sponsor_text, anchor="lt") + + x = int((self.renderer.canvas_width) / 2) + y = self.canvas_height - GUIConstants.COMPONENT_PADDING - partner_logo.height - int(GUIConstants.COMPONENT_PADDING/2) - th + self.renderer.draw.text(xy=(x, y), text=sponsor_text, font=font, fill="#ccc", anchor="mt") + self.renderer.canvas.paste( + partner_logo, + ( + int((self.renderer.canvas_width - partner_logo.width) / 2), + y + th + int(GUIConstants.COMPONENT_PADDING/2) + ) + ) + + self.renderer.show_image() + + time.sleep(2) -class ScreensaverView(LogoView): + +class ScreensaverScreen(LogoScreen): def __init__(self, buttons): super().__init__() From d9ecc472aa5d331456ff2f4e4d8682bcc4b47030 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 4 Apr 2022 12:54:51 -0500 Subject: [PATCH 09/10] Update controller.py --- src/seedsigner/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index a03a160b..2cf7bef8 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -48,7 +48,7 @@ class Controller(Singleton): rather than at the top in order avoid circular imports. """ - VERSION = "0.5.0" + VERSION = "0.5.0-rc1" # Declare class member vars with type hints to enable richer IDE support throughout # the code. From 795d9118a762bb04ddd1ad681f60b67f59821431 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 4 Apr 2022 13:52:00 -0500 Subject: [PATCH 10/10] Update seed_views.py --- src/seedsigner/views/seed_views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index c4dc93fe..bfac0043 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -392,7 +392,8 @@ class SeedOptionsView(View): ).display() if selected_menu_num == RET_CODE__BACK_BUTTON: - return Destination(BackStackView) + # Force BACK to always return to the Main Menu + return Destination(MainMenuView) if button_data[selected_menu_num] == REVIEW_PSBT: self.controller.psbt_seed = self.controller.get_seed(self.seed_num)