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/controller.py b/src/seedsigner/controller.py index 5431f62c..2cf7bef8 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -7,9 +7,8 @@ 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 @@ -49,7 +48,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-rc1" # Declare class member vars with type hints to enable richer IDE support throughout # the code. @@ -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 @@ def configure_instance(cls, disable_hardware=False): # Configure the Renderer Renderer.configure_instance() - controller.screensaver = ScreensaverView(controller.buttons) + controller.screensaver = ScreensaverScreen(controller.buttons) controller.back_stack = BackStack() @@ -180,20 +179,11 @@ def clear_back_stack(self): 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() - # 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` @@ -324,49 +314,3 @@ def handle_exception(self, e) -> Destination: 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 d27640b9..ebd451ea 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -3,10 +3,13 @@ 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,13 +24,15 @@ 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" ICON_FONT_SIZE = 22 ICON_INLINE_FONT_SIZE = 24 ICON_LARGE_BUTTON_SIZE = 36 - ICON_PRIMARY_SCREEN_SIZE = 44 + ICON_PRIMARY_SCREEN_SIZE = 50 TOP_NAV_TITLE_FONT_NAME = "OpenSans-SemiBold" TOP_NAV_TITLE_FONT_SIZE = 20 @@ -103,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 @@ -163,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 @@ -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 @@ -251,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 @@ -413,7 +419,7 @@ def render(self): 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 @@ -432,11 +438,11 @@ def __post_init__(self): 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", ) @@ -533,6 +539,8 @@ def __post_init__(self): if self.icon_name: icon_y = self.screen_y + int((self.height - self.icon.height)/2) self.icon.screen_y = icon_y + + self.height = max(self.icon.height, self.height) if self.is_text_centered: if self.icon_name: @@ -546,6 +554,8 @@ def __post_init__(self): # 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 +739,236 @@ def render(self): +@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) + + 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_TILTED, + 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_TILTED, + 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 @@ -806,7 +1046,6 @@ def __post_init__(self): 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: @@ -836,6 +1075,7 @@ def __post_init__(self): 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 @@ -928,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 e0e321e5..1191eeec 100644 --- a/src/seedsigner/gui/renderer.py +++ b/src/seedsigner/gui/renderer.py @@ -11,24 +11,20 @@ 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() @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 a51145e5..89bef6b1 100644 --- a/src/seedsigner/gui/screens/psbt_screens.py +++ b/src/seedsigner/gui/screens/psbt_screens.py @@ -7,7 +7,7 @@ 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) @@ -38,30 +38,20 @@ def __post_init__(self): # 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 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, )) # 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 @@ -79,7 +69,8 @@ def __post_init__(self): 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 + (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: @@ -206,6 +197,7 @@ def truncate_destination_addr(addr): text=input, font=font, fill=chart_font_color, + anchor="lt", ) # Render the association line to the conjunction point @@ -297,6 +289,7 @@ def truncate_destination_addr(addr): text=destination, font=font, fill=chart_font_color, + anchor="lt" ) # Render the association line from the conjunction point @@ -592,20 +585,10 @@ def __post_init__(self): 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 +597,13 @@ def __post_init__(self): 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 +635,11 @@ def __post_init__(self): 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/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 @@ def __post_init__(self): 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 @@ def run(self): 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/screen.py b/src/seedsigner/gui/screens/screen.py index f173f2cc..ba64f741 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -7,7 +7,7 @@ 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) @@ -56,8 +56,6 @@ def display(self) -> Any: return self._run() except Exception as e: - print(e) - print("------") repr(e) raise e finally: @@ -214,7 +212,42 @@ def __post_init__(self): 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() @@ -390,8 +423,6 @@ def _run(self): 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 ( @@ -812,6 +843,7 @@ class DireWarningScreen(WarningScreen): status_color: str = GUIConstants.DIRE_WARNING_COLOR + @dataclass class ResetScreen(BaseTopNavScreen): def __post_init__(self): @@ -824,10 +856,6 @@ def __post_init__(self): screen_y=self.top_nav.height, height=self.canvas_height - self.top_nav.height, )) - - def _run(self): - while True: - pass @@ -843,7 +871,3 @@ def __post_init__(self): 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/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 @@ def __post_init__(self): +@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/gui/screens/settings_screens.py b/src/seedsigner/gui/screens/settings_screens.py index d7340ad4..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 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 @@ def __post_init__(self): @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 @@ def __post_init__(self): 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 @@ def __post_init__(self): ) 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 @@ def __post_init__(self): 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,22 +264,35 @@ def _run(self): 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) + + + +@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 @@ def add_data(self, data): 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 @@ def add_data(self, data): 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 @@ def num_destinations(self): 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 @@ def parse(self): 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 @@ def verify_multisig_output(self, descriptor: Descriptor, change_num: int) -> boo 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 30b526a6..f1f12af6 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 @@ def map_network_to_embit(cls, network) -> str: 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" @@ -142,6 +154,7 @@ def map_network_to_embit(cls, network) -> str: SETTING__COMPACT_SEEDQR = "compact_seedqr" SETTING__PRIVACY_WARNINGS = "privacy_warnings" SETTING__DIRE_WARNINGS = "dire_warnings" + SETTING__PARTNER_LOGOS = "partner_logos" SETTING__DEBUG = "debug" @@ -334,7 +347,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 +361,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, @@ -355,7 +376,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, @@ -427,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, @@ -493,8 +520,6 @@ def to_dict(cls) -> dict: 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 3edea9f5..63a0008c 100644 Binary files a/src/seedsigner/resources/fonts/seedsigner-glyphs.otf and b/src/seedsigner/resources/fonts/seedsigner-glyphs.otf differ 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 00000000..2d08a3bb Binary files /dev/null and b/src/seedsigner/resources/img/partners/hrf_logo.png differ 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/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 @@ def run(self): 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 @@ def run(self): # 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/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 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) + + def get_random_partner(self) -> str: + return self.partners[random.randrange(len(self.partners))] -class OpeningSplashView(LogoView): + + +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") + + # 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) - 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) -class ScreensaverView(LogoView): +class ScreensaverScreen(LogoScreen): def __init__(self, buttons): super().__init__() diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index da7b2bab..bfac0043 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.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 @@ -256,7 +258,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 +349,7 @@ def run(self): 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 +367,7 @@ def run(self): 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) @@ -391,7 +392,8 @@ def run(self): ).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) @@ -809,13 +811,152 @@ def run(self): 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.mnemonic_list = 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.mnemonic_list)) + while self.cur_index in self.confirmed_list: + self.cur_index = int(random.random() * len(self.mnemonic_list)) + + 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) + + 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.mnemonic_list): + # 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) @@ -1122,9 +1263,6 @@ def run(self): 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 +1470,6 @@ def run(self): # 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 +1515,12 @@ def run(self): (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 @@ -1476,6 +1610,12 @@ def run(self): 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) @@ -1490,8 +1630,6 @@ def run(self): 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..9aeb0dcb 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 @@ def __init__(self, visibility: str = SettingsConstants.VISIBILITY__GENERAL, sele def run(self): + IO_TEST = "I/O test" + DONATE = "Donate" + settings_entries = SettingsDefinition.get_settings_entries( visibiilty=self.visibility ) @@ -32,16 +35,17 @@ def run(self): 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 @@ def run(self): 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}) @@ -171,11 +178,14 @@ def run(self): ****************************************************************************""" 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) + + + +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 @@ 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.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 @@ def run(self): 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 @@ 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 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 @@ 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 = "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" 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..cbfa8964 100644 --- a/tests/test_seed.py +++ b/tests/test_seed.py @@ -6,10 +6,9 @@ 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"] 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,29 +17,27 @@ 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" + # 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(SettingsConstants.WORDLIST_LANGUAGE__SPANISH) - - assert seed.mnemonic_str == "hebilla cría truco tigre gris llenar folio negocio laico casa tieso eludir" - - seed.set_passphrase("test") + # # TODO: Not yet supported in new implementation + # seed.set_wordlist_language_code("es") - 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" + 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.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