Merge branch '0.5.0' into patch-1

This commit is contained in:
Nick Klockenga
2022-04-04 22:22:02 -04:00
committed by GitHub
25 changed files with 866 additions and 464 deletions
+1 -1
View File
@@ -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
+6 -62
View File
@@ -7,9 +7,8 @@ from PIL.Image import Image
from typing import List
from seedsigner.gui.renderer import Renderer
from seedsigner.gui.screens.screen import WarningScreen
from seedsigner.hardware.buttons import HardwareButtons
from seedsigner.views.screensaver import ScreensaverView
from seedsigner.views.screensaver import ScreensaverScreen
from seedsigner.views.view import Destination, NotYetImplementedView, UnhandledExceptionView
from .models import Seed, SeedStorage, Settings, Singleton, PSBTParser
@@ -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 @@ class Controller(Singleton):
# Configure the Renderer
Renderer.configure_instance()
controller.screensaver = ScreensaverView(controller.buttons)
controller.screensaver = ScreensaverScreen(controller.buttons)
controller.back_stack = BackStack()
@@ -180,20 +179,11 @@ class Controller(Singleton):
def start(self) -> None:
from .views import MainMenuView, BackStackView
from .views.screensaver import OpeningSplashView
from .views.screensaver import OpeningSplashScreen
opening_splash = OpeningSplashView()
opening_splash = OpeningSplashScreen()
opening_splash.start()
# 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 @@ class Controller(Singleton):
exception_msg,
]
return Destination(UnhandledExceptionView, view_args={"error": error}, clear_history=True)
"""
###
### Seed Tools Controller Naviation/Launcher
###
### Create a Seed w/ Dice Screen
def show_create_seed_with_dice_tool(self) -> int:
seed = Seed(wordlist=self.settings.wordlist)
ret_val = True
while True:
seed.mnemonic = self.seed_tools_view.display_generate_seed_from_dice()
if seed:
break
else:
return Path.SEED_TOOLS_SUB_MENU
# display seed phrase (24 words)
while True:
ret_val = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, show_qr_option=True)
if ret_val == True:
break
else:
# no-op; can't back out of the seed phrase view
pass
# Ask to save seed
if self.storage.slot_avaliable():
r = self.renderer.display_generic_selection_menu(["Yes", "No"], "Save Seed?")
if r == 1: #Yes
slot_num = self.menu_view.display_saved_seed_menu(self.storage,2,None)
if slot_num in (1,2,3):
self.storage.add_seed(seed, slot_num)
self.renderer.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Main Menu")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
"""
+259 -18
View File
@@ -3,10 +3,13 @@ import os
import pathlib
from dataclasses import dataclass
from decimal import Decimal
from PIL import Image, ImageDraw, ImageFont, ImageFilter
from typing import List, Tuple
from seedsigner.models import Singleton
from seedsigner.models.settings import Settings
from seedsigner.models.settings_definition import SettingsConstants
# TODO: Remove all pixel hard coding
@@ -21,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 @@ class TextArea(BaseComponent):
class Icon(BaseComponent):
screen_x: int = 0
screen_y: int = 0
icon_name: str = SeedSignerCustomIconConstants.BITCOIN_LOGO
icon_name: str = SeedSignerCustomIconConstants.BITCOIN_LOGO_TILTED
icon_size: int = GUIConstants.ICON_FONT_SIZE
icon_color: str = GUIConstants.BODY_FONT_COLOR
@@ -432,11 +438,11 @@ class Icon(BaseComponent):
def render(self):
self.image_draw.text(
(self.screen_x, self.screen_y),
(self.screen_x, self.screen_y + self.height),
text=self.icon_name,
font=self.icon_font,
fill=self.icon_color,
anchor="lt", # left, top anchor to avoid "ascender" gap space
anchor="ls",
)
@@ -533,6 +539,8 @@ class IconTextLine(BaseComponent):
if self.icon_name:
icon_y = self.screen_y + int((self.height - self.icon.height)/2)
self.icon.screen_y = icon_y
self.height = max(self.icon.height, self.height)
if self.is_text_centered:
if self.icon_name:
@@ -546,6 +554,8 @@ class IconTextLine(BaseComponent):
# self.label_textarea.screen_x = self.screen_x + int((self.canvas_width - self.screen_x - max_textarea_width + (max_textarea_width - self.label_textarea.width))/2)
# self.value_textarea.screen_x = self.screen_x + int((self.canvas_width - self.screen_x - max_textarea_width + (max_textarea_width - self.value_textarea.width))/2)
self.width = self.canvas_width
def render(self):
if self.label_textarea:
@@ -729,6 +739,236 @@ class FormattedAddress(BaseComponent):
@dataclass
class BtcAmount(BaseComponent):
"""
Display btc value based on the SETTING__BTC_DENOMINATION Setting:
* btc: "B" icon + 8-decimal amount + "btc" (can truncate zero decimals to .0 or .09)
* sats: "B" icon + comma-separated amount + "sats"
* threshold: btc display at or above 0.01 btc; otherwise sats
* btcsatshybrd: "B" icon + 2-decimal amount + "|" + up to 6-digit, comma-separated sats + "sats"
"""
total_sats: int = None
icon_size: int = 34
font_size: int = 24
screen_x: int = 0
screen_y: int = None
def __post_init__(self):
super().__post_init__()
self.sub_components: List[BaseComponent] = []
self.paste_image: Image.Image = None
self.paste_coords = None
denomination = Settings.get_instance().get_value(SettingsConstants.SETTING__BTC_DENOMINATION)
network = Settings.get_instance().get_value(SettingsConstants.SETTING__NETWORK)
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 @@ class Button(BaseComponent):
self.icon_selected = Icon(icon_name=self.icon_name, icon_size=self.icon_size, icon_color=self.selected_icon_color)
if self.is_icon_inline:
# TODO: Only apply screen_* at render
if self.is_text_centered:
# Shift the text's centering
if self.text:
@@ -836,6 +1075,7 @@ class Button(BaseComponent):
self.right_icon_y = math.ceil((self.height - self.right_icon.height)/2)
def render(self):
if self.is_selected:
background_color = self.selected_color
@@ -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
+3 -7
View File
@@ -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
+16 -37
View File
@@ -7,7 +7,7 @@ from seedsigner.gui.renderer import Renderer
from seedsigner.models.threads import BaseThread
from .screen import ButtonListScreen, WarningScreen
from ..components import (Button, Icon, FontAwesomeIconConstants, IconTextLine, FormattedAddress, GUIConstants, Fonts, SeedSignerCustomIconConstants, TextArea,
from ..components import (BtcAmount, Button, Icon, FontAwesomeIconConstants, IconTextLine, FormattedAddress, GUIConstants, Fonts, SeedSignerCustomIconConstants, TextArea,
calc_bezier_curve, linear_interp)
@@ -38,30 +38,20 @@ class PSBTOverviewScreen(ButtonListScreen):
# icon_text_lines_y = self.components[-1].screen_y + self.components[-1].height
icon_text_lines_y = self.top_nav.height + GUIConstants.COMPONENT_PADDING
if not self.destination_addresses:
# This is a self-transfer
spend_amount = self.change_amount
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 @@ class PSBTOverviewScreen(ButtonListScreen):
font_size = GUIConstants.BODY_FONT_MIN_SIZE * ssf
font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, font_size)
tw, chart_text_height = font.getsize("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890") # All possible chars for max range
(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 @@ class PSBTOverviewScreen(ButtonListScreen):
text=input,
font=font,
fill=chart_font_color,
anchor="lt",
)
# Render the association line to the conjunction point
@@ -297,6 +289,7 @@ class PSBTOverviewScreen(ButtonListScreen):
text=destination,
font=font,
fill=chart_font_color,
anchor="lt"
)
# Render the association line from the conjunction point
@@ -592,20 +585,10 @@ class PSBTAddressDetailsScreen(ButtonListScreen):
center_img = Image.new("RGB", (self.canvas_width, center_img_height), GUIConstants.BACKGROUND_COLOR)
draw = ImageDraw.Draw(center_img)
if self.amount <= 1e7:
amount_display = f"{self.amount:,} sats"
else:
amount_display = f"{self.amount/1e8:,} btc"
icon_text_line = IconTextLine(
btc_amount = BtcAmount(
image_draw=draw,
canvas=center_img,
icon_name=SeedSignerCustomIconConstants.BITCOIN_LOGO,
icon_color=GUIConstants.ACCENT_COLOR,
icon_size=28,
is_text_centered=True,
value_text=f"{amount_display}",
font_size=22,
total_sats=self.amount,
screen_y=int(GUIConstants.COMPONENT_PADDING/2),
)
@@ -614,13 +597,13 @@ class PSBTAddressDetailsScreen(ButtonListScreen):
canvas=center_img,
width=self.canvas_width - 2*GUIConstants.EDGE_PADDING,
screen_x=GUIConstants.EDGE_PADDING,
screen_y=icon_text_line.height + GUIConstants.COMPONENT_PADDING,
screen_y=btc_amount.height + GUIConstants.COMPONENT_PADDING,
font_size=24,
address=self.address,
)
# Render each to the temp img we passed in
icon_text_line.render()
btc_amount.render()
formatted_address.render()
self.body_img = center_img.crop((
@@ -652,15 +635,11 @@ class PSBTChangeDetailsScreen(ButtonListScreen):
self.is_bottom_list = True
super().__post_init__()
self.components.append(IconTextLine(
icon_name=SeedSignerCustomIconConstants.BITCOIN_LOGO,
icon_color=GUIConstants.ACCENT_COLOR,
icon_size=28,
value_text=f"{self.amount} sats" if self.amount < 1e6 else f"{self.amount/1e8:0.8f} btc",
font_size=22,
is_text_centered=True,
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING
self.components.append(BtcAmount(
total_sats=self.amount,
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
))
self.components.append(FormattedAddress(
screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING,
address=self.address,
+37 -31
View File
@@ -21,7 +21,8 @@ class ScanScreen(BaseScreen):
instructions_text: str = "Scan a QR code"
resolution: Tuple[int,int] = (480, 480)
framerate: int = 12
auto_deactivate_buttons: bool = False # Used by the I/O test screen
render_rect: Tuple[int,int,int,int] = None
def __post_init__(self):
from seedsigner.hardware.camera import Camera
@@ -36,25 +37,35 @@ class ScanScreen(BaseScreen):
decoder=self.decoder,
renderer=self.renderer,
instructions_text=self.instructions_text,
components=self.components,
auto_deactivate_buttons=self.auto_deactivate_buttons,
render_rect=self.render_rect,
))
class LivePreviewThread(BaseThread):
def __init__(self, camera: Camera, decoder: DecodeQR, renderer: renderer, instructions_text: str, components: List[BaseComponent], auto_deactivate_buttons: bool):
def __init__(self, camera: Camera, decoder: DecodeQR, renderer: renderer.Renderer, instructions_text: str, render_rect: Tuple[int,int,int,int]):
self.camera = camera
self.decoder = decoder
self.renderer = renderer
self.instructions_text = instructions_text
self.components = components
self.auto_deactivate_buttons = auto_deactivate_buttons
if render_rect:
self.render_rect = render_rect
else:
self.render_rect = (0, 0, self.renderer.canvas_width, self.renderer.canvas_height)
self.render_width = self.render_rect[2] - self.render_rect[0]
self.render_height = self.render_rect[3] - self.render_rect[1]
print(f"render_width: {self.render_width}")
print(f"render_height: {self.render_height}")
super().__init__()
def run(self):
from timeit import default_timer as timer
instructions_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE)
while self.keep_running:
start = timer()
frame = self.camera.read_video_stream(as_image=True)
if frame is not None:
scan_text = self.instructions_text
@@ -62,40 +73,35 @@ class ScanScreen(BaseScreen):
scan_text = str(self.decoder.get_percent_complete()) + "% Complete"
with self.renderer.lock:
if frame.width > self.renderer.canvas_width or frame.height > self.renderer.canvas_height:
if frame.width > self.render_width or frame.height > self.render_height:
frame = frame.resize(
(self.renderer.canvas_width, self.renderer.canvas_height)
(self.render_width, self.render_height)
)
self.renderer.canvas.paste(
frame,
(0, 0)
(self.render_rect[0], self.render_rect[1])
)
self.renderer.draw.text(
xy=(
int(self.renderer.canvas_width/2),
self.renderer.canvas_height - GUIConstants.EDGE_PADDING
),
text=scan_text,
fill=GUIConstants.BODY_FONT_COLOR,
font=instructions_font,
stroke_width=4,
stroke_fill=GUIConstants.BACKGROUND_COLOR,
anchor="ms"
)
# Need to re-render all onscreen UI components since we just overwrote
# the screen.
for component in self.components:
component.render()
if self.auto_deactivate_buttons and type(component) in [Button, IconButton]:
if component.is_selected:
# deactivate for the next round
component.is_selected = False
if scan_text:
self.renderer.draw.text(
xy=(
int(self.renderer.canvas_width/2),
self.renderer.canvas_height - GUIConstants.EDGE_PADDING
),
text=scan_text,
fill=GUIConstants.BODY_FONT_COLOR,
font=instructions_font,
stroke_width=4,
stroke_fill=GUIConstants.BACKGROUND_COLOR,
anchor="ms"
)
self.renderer.show_image()
time.sleep(0.1) # turn this up or down to tune performance while decoding psbt
end = timer()
# print(f"{1.0/(end - start)} fps") # Time in seconds, e.g. 5.38091952400282
time.sleep(0.05) # turn this up or down to tune performance while decoding psbt
if self.camera._video_stream is None:
break
+38 -14
View File
@@ -7,7 +7,7 @@ from seedsigner.gui.renderer import Renderer
from seedsigner.models.threads import BaseThread
from seedsigner.models.encode_qr import EncodeQR
from seedsigner.models.settings import SettingsConstants
from seedsigner.models.settings import Settings, SettingsConstants
from ..components import (GUIConstants, BaseComponent, Button, Icon, LargeIconButton, SeedSignerCustomIconConstants, TopNav,
TextArea, load_image)
@@ -56,8 +56,6 @@ class BaseScreen(BaseComponent):
return self._run()
except Exception as e:
print(e)
print("------")
repr(e)
raise e
finally:
@@ -214,7 +212,42 @@ class BaseTopNavScreen(BaseScreen):
def _run(self):
raise Exception("Must implement in a child class")
while True:
if not self.top_nav.show_back_button and not self.top_nav.show_power_button:
# There's no navigation away from this screen; nothing to do here
time.sleep(0.1)
continue
user_input = self.hw_inputs.wait_for(
HardwareButtonsConstants.ALL_KEYS,
check_release=True,
release_keys=HardwareButtonsConstants.KEYS__ANYCLICK
)
with self.renderer.lock:
if not self.top_nav.is_selected and user_input in [
HardwareButtonsConstants.KEY_LEFT,
HardwareButtonsConstants.KEY_UP
]:
self.top_nav.is_selected = True
self.top_nav.render_buttons()
elif self.top_nav.is_selected and user_input in [
HardwareButtonsConstants.KEY_DOWN,
HardwareButtonsConstants.KEY_RIGHT
]:
self.top_nav.is_selected = False
self.top_nav.render_buttons()
elif self.top_nav.is_selected and user_input in HardwareButtonsConstants.KEYS__ANYCLICK:
return self.top_nav.selected_button
else:
# Nothing to do with this input
continue
# Write the screen updates
self.renderer.show_image()
@@ -390,8 +423,6 @@ class ButtonListScreen(BaseTopNavScreen):
release_keys=HardwareButtonsConstants.KEYS__ANYCLICK
)
print(user_input)
with self.renderer.lock:
if not self.top_nav.is_selected and (
user_input == HardwareButtonsConstants.KEY_LEFT or (
@@ -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 @@ class ResetScreen(BaseTopNavScreen):
screen_y=self.top_nav.height,
height=self.canvas_height - self.top_nav.height,
))
def _run(self):
while True:
pass
@@ -843,7 +871,3 @@ class PowerOffScreen(BaseTopNavScreen):
screen_y=self.top_nav.height,
height=self.canvas_height - self.top_nav.height,
))
def _run(self):
while True:
pass
@@ -536,6 +536,22 @@ class SeedWordsScreen(WarningEdgesMixin, ButtonListScreen):
@dataclass
class SeedWordsBackupTestPromptScreen(ButtonListScreen):
def __post_init__(self):
self.title = "Verify Backup?"
self.show_back_button = False
self.is_bottom_list = True
super().__post_init__()
self.components.append(TextArea(
text="Optionally verify that your mnemonic backup is correct.",
screen_y=self.top_nav.height,
is_text_centered=True,
))
@dataclass
class SeedExportXpubCustomDerivationScreen(BaseTopNavScreen):
title: str = "Derivation Path"
+134 -28
View File
@@ -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 @@ class SettingsEntryUpdateSelectionScreen(ButtonListScreen):
@dataclass
class IOTestScreen(ScanScreen):
class IOTestScreen(BaseTopNavScreen):
def __post_init__(self):
self.title = "I/O Test"
self.show_back_button = False
self.resolution = (96, 96)
self.framerate = 10
self.instructions_text = None
super().__post_init__()
# D-pad pictogram
input_button_width = GUIConstants.BUTTON_HEIGHT
input_button_height = input_button_width
input_button_width = GUIConstants.BUTTON_HEIGHT + 2
input_button_height = input_button_width + 2
dpad_center_x = GUIConstants.EDGE_PADDING + input_button_width + GUIConstants.COMPONENT_PADDING
dpad_center_y = int((self.canvas_height - input_button_height)/2)
@@ -73,7 +79,6 @@ class IOTestScreen(ScanScreen):
self.joystick_up_button = IconButton(
icon_name=FontAwesomeIconConstants.ANGLE_UP,
icon_size=GUIConstants.ICON_INLINE_FONT_SIZE,
is_text_centered=True,
width=input_button_width,
height=input_button_height,
screen_x=dpad_center_x,
@@ -116,22 +121,30 @@ class IOTestScreen(ScanScreen):
)
self.components.append(self.joystick_right_button)
key_button_width = int(1.75*GUIConstants.BUTTON_HEIGHT)
key_button_height = int(0.85*GUIConstants.BUTTON_HEIGHT)
# Hardware keys UI
font = Fonts.get_font(GUIConstants.BUTTON_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE)
(left, top, text_width, bottom) = font.getbbox(text="Clear", anchor="ls")
icon = Icon(
icon_name=FontAwesomeIconConstants.CAMERA,
icon_size=GUIConstants.ICON_INLINE_FONT_SIZE,
)
key_button_width = text_width + 2*GUIConstants.COMPONENT_PADDING + GUIConstants.EDGE_PADDING
key_button_height = icon.height + int(1.5*GUIConstants.COMPONENT_PADDING)
key2_y = int(self.canvas_height/2) - int(key_button_height/2)
self.key2_button = Button(
text=" ",
text="Clear", # Initialize with text to set vertical centering
width=key_button_width,
height=key_button_height,
screen_x=self.canvas_width - key_button_width + GUIConstants.EDGE_PADDING,
screen_y=key2_y,
outline_color=GUIConstants.ACCENT_COLOR,
)
self.key2_button.text = " " # but default state is empty
self.components.append(self.key2_button)
self.key1_button = Button(
text=" ",
self.key1_button = IconButton(
icon_name=FontAwesomeIconConstants.CAMERA,
width=key_button_width,
height=key_button_height,
screen_x=self.canvas_width - key_button_width + GUIConstants.EDGE_PADDING,
@@ -153,10 +166,90 @@ class IOTestScreen(ScanScreen):
def _run(self):
cur_selected_button = self.key1_button
msg_height = GUIConstants.ICON_LARGE_BUTTON_SIZE + 2*GUIConstants.COMPONENT_PADDING
camera_message = TextArea(
text="Capturing image...",
font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE,
is_text_centered=True,
height=msg_height,
screen_y=int((self.canvas_height - msg_height)/ 2),
)
while True:
input = self.hw_inputs.wait_for(keys=HardwareButtonsConstants.ALL_KEYS, check_release=False)
if input == HardwareButtonsConstants.KEY_PRESS:
if input == HardwareButtonsConstants.KEY1:
cur_selected_button = self.key1_button
with self.renderer.lock:
cur_selected_button.is_selected = True
cur_selected_button.render()
camera_message.render()
# Render edges around message box
self.image_draw.rectangle(
(
-1, int((self.canvas_height - msg_height)/ 2) - 1,
self.canvas_width + 1, int((self.canvas_height + msg_height)/ 2) + 1
),
outline=GUIConstants.ACCENT_COLOR,
width=1,
)
self.renderer.show_image()
# Snap a pic, render it as the background, re-render all onscreen elements
camera = Camera.get_instance()
try:
camera.start_single_frame_mode(resolution=(self.canvas_width, self.canvas_height))
# Reset the button state
with self.renderer.lock:
cur_selected_button.is_selected = False
cur_selected_button.render()
self.renderer.show_image()
time.sleep(0.25)
background_frame = camera.capture_frame()
display_version = autocontrast(
background_frame,
cutoff=2
)
with self.renderer.lock:
self.canvas.paste(display_version, (0, self.top_nav.height))
self.key2_button.text = "Clear"
for component in self.components:
component.render()
self.renderer.show_image()
finally:
camera.stop_single_frame_mode()
continue
elif input == HardwareButtonsConstants.KEY2:
cur_selected_button = self.key2_button
# Clear the background
with self.renderer.lock:
cur_selected_button.is_selected = True
self._render()
self.renderer.show_image()
# And then re-render Key2 in its initial state
self.key2_button.text = " "
cur_selected_button.is_selected = False
cur_selected_button.render()
self.renderer.show_image()
continue
elif input == HardwareButtonsConstants.KEY3:
# Exit
cur_selected_button = self.key3_button
cur_selected_button.is_selected = True
with self.renderer.lock:
cur_selected_button.render()
self.renderer.show_image()
return
elif input == HardwareButtonsConstants.KEY_PRESS:
cur_selected_button = self.joystick_click_button
elif input == HardwareButtonsConstants.KEY_UP:
@@ -171,22 +264,35 @@ class IOTestScreen(ScanScreen):
elif input == HardwareButtonsConstants.KEY_RIGHT:
cur_selected_button = self.joystick_right_button
elif input == HardwareButtonsConstants.KEY1:
cur_selected_button = self.key1_button
elif input == HardwareButtonsConstants.KEY2:
cur_selected_button = self.key2_button
elif input == HardwareButtonsConstants.KEY3:
# Exit
self.camera.stop_video_stream_mode()
cur_selected_button = self.key3_button
with self.renderer.lock:
cur_selected_button.is_selected = True
with self.renderer.lock:
cur_selected_button.render()
self.renderer.show_image()
return
cur_selected_button.render()
self.renderer.show_image()
cur_selected_button.is_selected = True
with self.renderer.lock:
cur_selected_button.is_selected = False
cur_selected_button.render()
self.renderer.show_image()
time.sleep(0.1)
@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
))
+4 -2
View File
@@ -60,8 +60,6 @@ class DecodeQR:
qr_type = DecodeQR.detect_segment_type(data, wordlist_language_code=self.wordlist_language_code)
print(qr_type)
if self.qr_type == None:
self.qr_type = qr_type
@@ -97,6 +95,10 @@ class DecodeQR:
elif self.qr_type != qr_type:
raise Exception('QR Fragment Unexpected Type Change')
if not self.decoder:
# Did not find any recognizable format
return DecodeQRStatus.INVALID
# Process the binary formats first
if self.qr_type == QRType.SEED__COMPACTSEEDQR:
+1 -3
View File
@@ -58,7 +58,6 @@ class PSBTParser():
def _set_root(self):
self.root = bip32.HDKey.from_seed(self.seed.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]["xprv"])
print(f"root: {self.root}")
def parse(self):
@@ -85,7 +84,6 @@ class PSBTParser():
def _parse_inputs(self):
self.input_amount = 0
print(f"psbt.inputs: {self.psbt.inputs}")
self.num_inputs = len(self.psbt.inputs)
for inp in self.psbt.inputs:
if inp.witness_utxo:
@@ -343,5 +341,5 @@ class PSBTParser():
i = change_data["output_index"]
output = self.psbt.outputs[i]
is_owner = descriptor.owns(output)
print(f"{self.psbt.tx.vout[i].script_pubkey.address()} | {output.value} | {is_owner}")
# print(f"{self.psbt.tx.vout[i].script_pubkey.address()} | {output.value} | {is_owner}")
return is_owner
+29 -4
View File
@@ -45,6 +45,17 @@ class SettingsConstants:
(LANGUAGE__ENGLISH, "English"),
]
BTC_DENOMINATION__BTC = "btc"
BTC_DENOMINATION__SATS = "sats"
BTC_DENOMINATION__THRESHOLD = "thr"
BTC_DENOMINATION__BTCSATSHYBRID = "hyb"
ALL_BTC_DENOMINATIONS = [
(BTC_DENOMINATION__BTC, "Btc-only"),
(BTC_DENOMINATION__SATS, "Sats-only"),
(BTC_DENOMINATION__THRESHOLD, "Threshold at 0.01"),
(BTC_DENOMINATION__BTCSATSHYBRID, "Btc | Sats hybrid"),
]
CAMERA_ROTATION__0 = 0
CAMERA_ROTATION__90 = 90
CAMERA_ROTATION__180 = 180
@@ -130,6 +141,7 @@ class SettingsConstants:
SETTING__WORDLIST_LANGUAGE = "wordlist_language"
SETTING__PERSISTENT_SETTINGS = "persistent_settings"
SETTING__COORDINATORS = "coordinators"
SETTING__BTC_DENOMINATION = "denomination"
SETTING__NETWORK = "network"
SETTING__QR_DENSITY = "qr_density"
@@ -142,6 +154,7 @@ class SettingsConstants:
SETTING__COMPACT_SEEDQR = "compact_seedqr"
SETTING__PRIVACY_WARNINGS = "privacy_warnings"
SETTING__DIRE_WARNINGS = "dire_warnings"
SETTING__PARTNER_LOGOS = "partner_logos"
SETTING__DEBUG = "debug"
@@ -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 @@ class SettingsDefinition:
if __name__ == "__main__":
import json
import os
print("Exporting SettingsDefinition to json")
output_file = "settings_definition.json"
with open(output_file, 'w') as json_file:
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

@@ -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
+1 -3
View File
@@ -109,7 +109,7 @@ class PSBTOverviewView(View):
num_change_outputs = 0
num_self_transfer_outputs = 0
for change_output in change_data:
print(f"""{change_output["derivation_path"][0]}""")
# print(f"""{change_output["derivation_path"][0]}""")
if change_output["derivation_path"][0].split("/")[-2] == "1":
num_change_outputs += 1
else:
@@ -277,8 +277,6 @@ class PSBTChangeDetailsView(View):
# Single-sig verification is easy. We expect to find a single fingerprint
# and derivation path.
seed_fingerprint = self.controller.psbt_seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
print(f"seed fingerprint: {seed_fingerprint}")
print(change_data)
if seed_fingerprint not in change_data.get("fingerprint"):
# TODO: Something is wrong with this psbt(?). Reroute to warning?
+65 -19
View File
@@ -2,49 +2,95 @@ import os
import random
import time
from PIL import Image, ImageDraw
from .view import View
from PIL import Image
from seedsigner.gui.components import Fonts, GUIConstants, load_image
from seedsigner.gui.screens.screen import BaseScreen
from seedsigner.models.settings import Settings
from seedsigner.models.settings_definition import SettingsConstants
# TODO: Should be derived from View?
class LogoView:
# TODO: This early code is now outdated vis-a-vis Screen vs View distinctions
class LogoScreen(BaseScreen):
def __init__(self):
from seedsigner.gui import Renderer
self.renderer = Renderer.get_instance()
super().__init__()
self.logo = load_image("logo_black_240.png")
self.partners = [
"hrf",
]
self.partner_logos: dict = {}
for partner in self.partners:
logo_url = os.path.join("partners", f"{partner}_logo.png")
self.partner_logos[partner] = load_image(logo_url)
class OpeningSplashView(LogoView):
def get_random_partner(self) -> str:
return self.partners[random.randrange(len(self.partners))]
class OpeningSplashScreen(LogoScreen):
def start(self):
from seedsigner.controller import Controller
controller = Controller.get_instance()
show_partner_logos = Settings.get_instance().get_value(SettingsConstants.SETTING__PARTNER_LOGOS) == SettingsConstants.OPTION__ENABLED
if show_partner_logos:
logo_offset_y = -56
else:
logo_offset_y = 0
# Fade in alpha
for i in range(250, -1, -25):
self.logo.putalpha(255 - i)
background = Image.new("RGBA", self.logo.size, (0,0,0))
self.renderer.disp.ShowImage(Image.alpha_composite(background, self.logo), 0, 0)
background = Image.new("RGBA", size=self.logo.size, color="black")
self.renderer.canvas.paste(Image.alpha_composite(background, self.logo), (0, logo_offset_y))
self.renderer.show_image()
# Display version num and hold for a few seconds
# Display version num below SeedSigner logo
font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.TOP_NAV_TITLE_FONT_SIZE)
version = f"v{controller.VERSION}"
tw, th = font.getsize(version)
x = int((self.renderer.canvas_width - tw) / 2)
y = int(self.renderer.canvas_height / 2) + 40
(left, top, version_tw, version_th) = font.getbbox(version, anchor="lt")
draw = ImageDraw.Draw(self.logo)
draw.text((x, y), version, fill=GUIConstants.ACCENT_COLOR, font=font)
self.renderer.show_image(self.logo)
time.sleep(3)
# The logo png is 240x240, but the actual logo is 70px tall, vertically centered
version_x = int(self.renderer.canvas_width/2)
version_y = int(self.canvas_height/2) + 35 + logo_offset_y + GUIConstants.COMPONENT_PADDING
self.renderer.draw.text(xy=(version_x, version_y), text=version, font=font, fill=GUIConstants.ACCENT_COLOR, anchor="mt")
self.renderer.show_image()
if show_partner_logos:
# Hold on the version num for a moment
time.sleep(1)
# Set up the partner logo
partner_logo: Image.Image = self.partner_logos[self.get_random_partner()]
font = Fonts.get_font(GUIConstants.TOP_NAV_TITLE_FONT_NAME, GUIConstants.BODY_FONT_SIZE)
sponsor_text = "With support from:"
(left, top, tw, th) = font.getbbox(sponsor_text, anchor="lt")
x = int((self.renderer.canvas_width) / 2)
y = self.canvas_height - GUIConstants.COMPONENT_PADDING - partner_logo.height - int(GUIConstants.COMPONENT_PADDING/2) - th
self.renderer.draw.text(xy=(x, y), text=sponsor_text, font=font, fill="#ccc", anchor="mt")
self.renderer.canvas.paste(
partner_logo,
(
int((self.renderer.canvas_width - partner_logo.width) / 2),
y + th + int(GUIConstants.COMPONENT_PADDING/2)
)
)
self.renderer.show_image()
time.sleep(2)
class ScreensaverView(LogoView):
class ScreensaverScreen(LogoScreen):
def __init__(self, buttons):
super().__init__()
+160 -22
View File
@@ -1,14 +1,19 @@
import embit
import random
import time
from typing import List
from binascii import hexlify
from embit import bip39
from embit.descriptor import Descriptor
from embit.networks import NETWORKS
from seedsigner.controller import Controller
from seedsigner.models.decode_qr import DecodeQR
from typing import List
from seedsigner.controller import Controller
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustomIconConstants
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen,
WarningScreen, DireWarningScreen, seed_screens)
from seedsigner.gui.screens.screen import LargeIconStatusScreen, LoadingScreenThread, QRDisplayScreen
from seedsigner.models.decode_qr import DecodeQR
from seedsigner.models.encode_qr import EncodeQR
from seedsigner.models.psbt_parser import PSBTParser
from seedsigner.models.qr_type import QRType
@@ -16,9 +21,6 @@ from seedsigner.models.seed import InvalidSeedException, Seed
from seedsigner.models.settings import SettingsConstants
from seedsigner.models.settings_definition import SettingsDefinition
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen,
WarningScreen, DireWarningScreen, seed_screens)
from seedsigner.gui.screens.screen import LargeIconStatusScreen, LoadingScreenThread, QRDisplayScreen
from seedsigner.views.psbt_views import PSBTChangeDetailsView
from seedsigner.views.scan_views import ScanView
@@ -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 @@ class SeedOptionsView(View):
REVIEW_PSBT = "Review PSBT"
VERIFY_ADDRESS = "Verify Addr"
EXPORT_XPUB = "Export Xpub"
BACKUP = ("Backup Seed", None, None, None, FontAwesomeIconConstants.CIRCLE_CHEVRON_RIGHT)
BACKUP = ("Backup Seed", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)
DISCARD = ("Discard Seed", None, None, "red")
button_data = []
@@ -366,7 +367,7 @@ class SeedOptionsView(View):
if self.controller.psbt:
if PSBTParser.has_matching_input_fingerprint(self.controller.psbt, self.seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)):
if self.controller.resume_main_flow and self.controller.resume_main_flow == Controller.FLOW__PSBT:
# Re-route us directly back to the start of the PSBT flow
# Re-route us directly back to the start of the PSBT flow
self.controller.resume_main_flow = None
self.controller.psbt_seed = self.seed
return Destination(PSBTOverviewView, skip_current_view=True)
@@ -391,7 +392,8 @@ class SeedOptionsView(View):
).display()
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
# Force BACK to always return to the Main Menu
return Destination(MainMenuView)
if button_data[selected_menu_num] == REVIEW_PSBT:
self.controller.psbt_seed = self.controller.get_seed(self.seed_num)
@@ -809,13 +811,152 @@ class SeedWordsView(View):
if button_data[selected_menu_num] == NEXT:
if self.seed_num is None and self.page_index == self.num_pages - 1:
return Destination(SeedFinalizeView)
return Destination(SeedWordsBackupTestPromptView, view_args=dict(seed_num=self.seed_num))
else:
return Destination(SeedWordsView, view_args={"seed_num": self.seed_num, "page_index": self.page_index + 1})
return Destination(SeedWordsView, view_args=dict(seed_num=self.seed_num, page_index=self.page_index + 1))
elif button_data[selected_menu_num] == DONE:
# Must clear history to avoid BACK button returning to private info
return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num}, clear_history=True)
return Destination(SeedWordsBackupTestPromptView, view_args=dict(seed_num=self.seed_num))
"""****************************************************************************
Seed Words Backup Test
****************************************************************************"""
class SeedWordsBackupTestPromptView(View):
def __init__(self, seed_num: int):
self.seed_num = seed_num
def run(self):
VERIFY = "Verify"
SKIP = "Skip"
button_data = [VERIFY, SKIP]
selected_menu_num = seed_screens.SeedWordsBackupTestPromptScreen(
button_data=button_data,
).display()
if button_data[selected_menu_num] == VERIFY:
return Destination(SeedWordsBackupTestView, view_args=dict(seed_num=self.seed_num))
elif button_data[selected_menu_num] == SKIP:
if self.seed_num is not None:
return Destination(SeedOptionsView, view_args=dict(seed_num=self.seed_num))
else:
return Destination(SeedFinalizeView)
class SeedWordsBackupTestView(View):
def __init__(self, seed_num: int, confirmed_list: List[bool] = None, cur_index: int = None):
super().__init__()
self.seed_num = seed_num
if self.seed_num is None:
self.seed = self.controller.storage.get_pending_seed()
else:
self.seed = self.controller.get_seed(self.seed_num)
self.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 @@ class AddressVerificationStartView(View):
self.controller.unverified_address["sig_type"] = sig_type
self.controller.unverified_address["derivation_path"] = derivation_path
import json
print(json.dumps(self.controller.unverified_address, indent=4))
return destination
@@ -1332,8 +1470,6 @@ class SeedAddressVerificationView(View):
# Successfully verified the addr; update the data
self.controller.unverified_address["verified_index"] = self.verified_index.cur_count
self.controller.unverified_address["verified_index_is_change"] = self.verified_index_is_change.cur_count == 1
import json
print(json.dumps(self.controller.unverified_address, indent=4))
return Destination(AddressVerificationSuccessView, view_args=dict(seed_num=self.seed_num))
else:
@@ -1379,14 +1515,12 @@ class SeedAddressVerificationView(View):
(receive_address, change_address) = self.derive_single_sig(i)
if self.address == receive_address:
print(f"Verified receive addr #{i}!")
self.verified_index.set_value(i)
self.verified_index_is_change.set_value(0)
self.keep_running = False
break
elif self.address == change_address:
print(f"Verified change addr #{i}!")
self.verified_index.set_value(i)
self.verified_index_is_change.set_value(1)
self.keep_running = False
@@ -1476,6 +1610,12 @@ class LoadMultisigWalletDescriptorView(View):
if button_data[selected_menu_num] == SCAN:
return Destination(ScanView)
elif button_data[selected_menu_num] == CANCEL:
if self.controller.resume_main_flow == Controller.FLOW__PSBT:
return Destination(BackStackView)
else:
return Destination(MainMenuView)
@@ -1490,8 +1630,6 @@ class MultisigWalletDescriptorView(View):
policy = descriptor.brief_policy.split("multisig")[0].strip()
print(fingerprints)
RETURN = "Return to PSBT"
VERIFY = "Verify Addr"
OK = "OK"
+21 -11
View File
@@ -1,4 +1,4 @@
from seedsigner.gui.components import FontAwesomeIconConstants
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustomIconConstants
from seedsigner.models.decode_qr import DecodeQR
from .view import View, Destination, BackStackView, MainMenuView
@@ -16,6 +16,9 @@ class SettingsMenuView(View):
def run(self):
IO_TEST = "I/O test"
DONATE = "Donate"
settings_entries = SettingsDefinition.get_settings_entries(
visibiilty=self.visibility
)
@@ -32,16 +35,17 @@ class SettingsMenuView(View):
title = "Settings"
# Set up the next nested level of menuing
button_data.append(("Advanced", None, None, None, FontAwesomeIconConstants.CIRCLE_CHEVRON_RIGHT))
button_data.append(("Advanced", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT))
next = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__ADVANCED})
button_data.append("I/O test")
button_data.append(IO_TEST)
button_data.append(DONATE)
elif self.visibility == SettingsConstants.VISIBILITY__ADVANCED:
title = "Advanced"
# So far there are no real Developer options; disabling for now
# button_data.append(("Developer Options", None, None, None, FontAwesomeIconConstants.CIRCLE_CHEVRON_RIGHT))
# button_data.append(("Developer Options", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT))
# next = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__DEVELOPER})
next = None
@@ -67,9 +71,12 @@ class SettingsMenuView(View):
elif selected_menu_num == len(settings_entries):
return next
elif selected_menu_num == len(settings_entries) + 1:
elif len(button_data) > selected_menu_num and button_data[selected_menu_num] == IO_TEST:
return Destination(IOTestView)
elif len(button_data) > selected_menu_num and button_data[selected_menu_num] == DONATE:
return Destination(DonateView)
else:
# TODO: Free-entry types (are there any?) will need their own SettingsEntryUpdateFreeEntryView(?).
return Destination(SettingsEntryUpdateSelectionView, view_args={"attr_name": settings_entries[selected_menu_num].attr_name})
@@ -171,11 +178,14 @@ class SettingsEntryUpdateSelectionView(View):
****************************************************************************"""
class IOTestView(View):
def run(self):
settings_screens.IOTestScreen(
instructions_text="Live camera feed",
resolution=(240,240),
framerate=24,
auto_deactivate_buttons=True,
).display()
settings_screens.IOTestScreen().display()
return Destination(SettingsMenuView)
class DonateView(View):
def run(self):
settings_screens.DonateScreen().display()
return Destination(SettingsMenuView)
+4 -5
View File
@@ -4,7 +4,6 @@ import time
from PIL import Image
from PIL.ImageOps import autocontrast
from seedsigner.gui.screens.screen import LargeButtonScreen
from seedsigner.hardware.camera import Camera
from seedsigner.gui.components import FontAwesomeIconConstants
@@ -13,9 +12,9 @@ from seedsigner.gui.screens.tools_screens import ToolsDiceEntropyEntryScreen, To
from seedsigner.helpers import mnemonic_generation
from seedsigner.models.seed import Seed
from seedsigner.models.settings_definition import SettingsConstants
from seedsigner.views.seed_views import SeedDiscardView, SeedFinalizeView, SeedMnemonicEntryView, SeedWordsView, SeedWordsWarningView
from seedsigner.views.seed_views import SeedDiscardView, SeedFinalizeView, SeedMnemonicEntryView, SeedWordsWarningView
from .view import NotYetImplementedView, View, Destination, BackStackView, MainMenuView
from .view import View, Destination, BackStackView
@@ -207,9 +206,9 @@ class ToolsDiceEntropyEntryView(View):
if ret == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
print(ret)
print(f"Dice rolls: {ret}")
dice_seed_phrase = mnemonic_generation.generate_mnemonic_from_dice(ret)
print(dice_seed_phrase)
print(f"""Mnemonic: "{dice_seed_phrase}" """)
# Add the mnemonic as an in-memory Seed
seed = Seed(dice_seed_phrase, wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE))
+7 -52
View File
@@ -3,6 +3,7 @@ import pytest
from mock import MagicMock
from seedsigner.controller import Controller
from seedsigner.models.settings import Settings
from seedsigner.models.settings_definition import SettingsConstants
@@ -11,10 +12,6 @@ def test_singleton_init_fails():
with pytest.raises(Exception):
c = Controller()
def test_singleton_get_instance_without_configure_fails():
""" Calling get_instance() without first calling configure_instance() should fail """
with pytest.raises(Exception):
c = Controller.get_instance()
def test_singleton_get_instance_preserves_state():
""" Changes to the Controller singleton should be preserved across calls to get_instance() """
@@ -25,38 +22,17 @@ def test_singleton_get_instance_preserves_state():
Settings._instance = None
Controller._instance = None
settings = """
[system]
debug = False
default_language = en
persistent_settings = False
[display]
text_color = ORANGE
qr_background_color = FFFFFF
camera_rotation = 0
[wallet]
network = main
software = Prompt
qr_density = 2
custom_derivation = m/0/0
compact_seedqr_enabled = False
"""
config = configparser.ConfigParser()
config.read_string(settings)
# Initialize the instance and verify that it read the config settings
Controller.configure_instance(config, disable_hardware=True)
Controller.configure_instance(disable_hardware=True)
controller = Controller.get_instance()
assert controller.color == "ORANGE"
assert controller.unverified_address is None
# Change a value in the instance...
controller.color = "purple"
controller.unverified_address = "123abc"
# ...get a new copy of the instance and confirm change
controller = Controller.get_instance()
assert controller.color == "purple"
assert controller.unverified_address == "123abc"
def test_missing_settings_get_defaults():
@@ -69,30 +45,9 @@ def test_missing_settings_get_defaults():
Settings._instance = None
Controller._instance = None
# Intentionally omit `compact_seedqr_enabled` from settings:
settings = """
[system]
debug = False
default_language = en
persistent_settings = False
[display]
text_color = ORANGE
qr_background_color = FFFFFF
camera_rotation = 0
[wallet]
network = main
software = Prompt
qr_density = 2
custom_derivation = m/0/0
"""
config = configparser.ConfigParser()
config.read_string(settings)
# Controller should parse the settings fine, even though a field is missing
Controller.configure_instance(config, disable_hardware=True)
Controller.configure_instance(disable_hardware=True)
# Controller should still have a default value
controller = Controller.get_instance()
assert controller.settings.compact_seedqr_enabled is False
assert controller.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__DISABLED
+25 -84
View File
@@ -3,6 +3,8 @@ from mock import MagicMock
from seedsigner.models import Seed, DecodeQR, DecodeQRStatus, QRType, PSBTParser
from embit import psbt, bip39
from seedsigner.models.settings_definition import SettingsConstants
# this is an of this bug: https://github.com/Foundation-Devices/foundation-ur-py/issues/3
@@ -78,15 +80,15 @@ def test_base64_single_frame_singlsig():
assert str(tx) == base64_psbt
mnemonic = "height demise useless trap grow lion found off key clown transfer enroll"
mnemonic = "height demise useless trap grow lion found off key clown transfer enroll".split()
pw = ""
seed = Seed(mnemonic, passphrase=pw)
assert seed.mnemonic_str == mnemonic
assert seed.mnemonic_str == " ".join(mnemonic)
assert seed.seed_bytes != None
pp = PSBTParser(tx,seed,"test")
pp = PSBTParser(p=tx, seed=seed, network=SettingsConstants.TESTNET)
assert tx.inputs[0].witness_utxo.value == 25000 # input amount in psbt
@@ -135,8 +137,8 @@ def test_base64_2_input_p2wsh():
assert str(tx) == base64_psbt
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
pp = PSBTParser(tx, Seed(mnemonic), "test")
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".split()
pp = PSBTParser(p=tx, seed=Seed(mnemonic), network=SettingsConstants.TESTNET)
assert tx.inputs[0].witness_utxo.value == 10000000 # input amount 1 in psbt
@@ -162,8 +164,8 @@ def test_base64_1_input_p2sh_p2wsh():
assert str(tx) == base64_psbt
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
pp = PSBTParser(tx,Seed(mnemonic, wordlist=bip39.WORDLIST),"test")
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".split()
pp = PSBTParser(p=tx, seed=Seed(mnemonic), network=SettingsConstants.TESTNET)
assert tx.inputs[0].witness_utxo.value == 100000000 # input amount 1 in psbt
@@ -175,62 +177,6 @@ def test_base64_1_input_p2sh_p2wsh():
assert len(pp.destination_addresses) == 1
def test_ur_legacy():
qrcodes = [
"UR:BYTES/2OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/3DJV07U94KMSQQQQQQQQZQ8A0CQSYQQQQQQQZQHA78FG5TANFV9Z2TPDWE57AUVCN2T06TDGCAJNYPAT9LVG5QFMU5QSQQQQQRLLLLLL3NZ3KK6TMJ2TTRH6SKU05V76GHEANAT2QSDCDPFG67YZQQPFFZKSZQQQQQQ0LLLLLUPYQRGRQQQQQQQQYGQZP4XCCZ3WFNWK"
, "UR:BYTES/2OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/3DJV07U94KMSQQQQQQQQZQ8A0CQSYQQQQQQQZQHA78FG5TANFV9Z2TPDWE57AUVCN2T06TDGCAJNYPAT9LVG5QFMU5QSQQQQQRLLLLLL3NZ3KK6TMJ2TTRH6SKU05V76GHEANAT2QSDCDPFG67YZQQPFFZKSZQQQQQQ0LLLLLUPYQRGRQQQQQQQQYGQZP4XCCZ3WFNWK"
, "UR:BYTES/3OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/0QXPCDECDVJ8PS0DPW59YDWSH262DUZV6VAT4E4GMPEQQQQQQQQQQ9SQZSRR3CY9AYW5V4Y0A5DMWD57JD7FXX9G95PYWVZYQGSX2Y0YENNVNLEHXVMTDS86QM4VNDHG9V0K3PNDMGAJLWLSTS6504SZYP80TGGDPFZR8AM7XZNM77XWKRHP0D0Y540FLPTA5C2HRUQ0"
, "UR:BYTES/3OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/0QXPCDECDVJ8PS0DPW59YDWSH262DUZV6VAT4E4GMPEQQQQQQQQQQ9SQZSRR3CY9AYW5V4Y0A5DMWD57JD7FXX9G95PYWVZYQGSX2Y0YENNVNLEHXVMTDS86QM4VNDHG9V0K3PNDMGAJLWLSTS6504SZYP80TGGDPFZR8AM7XZNM77XWKRHP0D0Y540FLPTA5C2HRUQ0"
, "UR:BYTES/3OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/0QXPCDECDVJ8PS0DPW59YDWSH262DUZV6VAT4E4GMPEQQQQQQQQQQ9SQZSRR3CY9AYW5V4Y0A5DMWD57JD7FXX9G95PYWVZYQGSX2Y0YENNVNLEHXVMTDS86QM4VNDHG9V0K3PNDMGAJLWLSTS6504SZYP80TGGDPFZR8AM7XZNM77XWKRHP0D0Y540FLPTA5C2HRUQ0"
, "UR:BYTES/4OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/3Y0PYQFPQW6EJAC7ZGZUYVSCVLPJUS8CVFT8389KFXX82VM3YQMT26FRVHLVUQJ8XPZQYGR9FH3H26HSK99ZH3MQ4D7EHCS7Y752EP37HGG2X6RU5V3PKD7PMSPZQF9L7DESNZNS0H4V56978HCMAEG9D99CNY5LYCV247F58T7M66QUQYSS907LVPMZLKYAWMDGRRKT"
, "UR:BYTES/4OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/3Y0PYQFPQW6EJAC7ZGZUYVSCVLPJUS8CVFT8389KFXX82VM3YQMT26FRVHLVUQJ8XPZQYGR9FH3H26HSK99ZH3MQ4D7EHCS7Y752EP37HGG2X6RU5V3PKD7PMSPZQF9L7DESNZNS0H4V56978HCMAEG9D99CNY5LYCV247F58T7M66QUQYSS907LVPMZLKYAWMDGRRKT"
, "UR:BYTES/5OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/GX8KAW6FYVRQ2HKD5T4WUJJ7K68T5262QQQQQQQPQY45QRGRQQQQQQQQYGQZP4XCCZ3WFNWK0QXPCDECDVJ8PS0DPW59YDWSH262DUZV6VAT4E4GQYZ5W53PQFMWVTS2CUAWJ43C87WR6ZZ705V5LU2AEKGEF8AM8FWP6TQFTD7AZGGZMKAVTUW8HDRTKMEET55FK6MX"
, "UR:BYTES/5OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/GX8KAW6FYVRQ2HKD5T4WUJJ7K68T5262QQQQQQQPQY45QRGRQQQQQQQQYGQZP4XCCZ3WFNWK0QXPCDECDVJ8PS0DPW59YDWSH262DUZV6VAT4E4GQYZ5W53PQFMWVTS2CUAWJ43C87WR6ZZ705V5LU2AEKGEF8AM8FWP6TQFTD7AZGGZMKAVTUW8HDRTKMEET55FK6MX"
, "UR:BYTES/5OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/GX8KAW6FYVRQ2HKD5T4WUJJ7K68T5262QQQQQQQPQY45QRGRQQQQQQQQYGQZP4XCCZ3WFNWK0QXPCDECDVJ8PS0DPW59YDWSH262DUZV6VAT4E4GQYZ5W53PQFMWVTS2CUAWJ43C87WR6ZZ705V5LU2AEKGEF8AM8FWP6TQFTD7AZGGZMKAVTUW8HDRTKMEET55FK6MX"
, "UR:BYTES/6OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/KTS8U3XQP95WYR8VQ9V2KWHCTS399T3ZQCP8DE3WPTRN462K8QLEC0GGTE73JNL3THXER9YLHVA9C8FVP9DHM5GUKUGYR2PSQQQGQQGQQZQQQQQQSQPQQQYQQQQQQQQRQQQQQGSXQTWM4303C7A5DWM089WJ3XMTV6EWQLJYCQYK3CSVASQ432E6LPWZY8XYUTHDXVQQ"
, "UR:BYTES/6OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/KTS8U3XQP95WYR8VQ9V2KWHCTS399T3ZQCP8DE3WPTRN462K8QLEC0GGTE73JNL3THXER9YLHVA9C8FVP9DHM5GUKUGYR2PSQQQGQQGQQZQQQQQQSQPQQQYQQQQQQQQRQQQQQGSXQTWM4303C7A5DWM089WJ3XMTV6EWQLJYCQYK3CSVASQ432E6LPWZY8XYUTHDXVQQ"
, "UR:BYTES/7OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/QZQQZQQQSQQQQQYQQGQQPQQQQQQQQQCQQQQQQQQPQ9R4YGGZDYQESZAAQHLY3L9L5VMSYRCQAXPWR6ES83NEC5XTMD4HPKVNGF4ZZQ4XNF7SKKG5KVH0UZPD3XXVNDWVP2CQAP0GLNRGQP2DHAD0U9HYLAF2UGSZQF5SRXQTH5ZLUJ8UH73NWQS0QR5C9C0TXQ7X08ZS"
, "UR:BYTES/7OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/QZQQZQQQSQQQQQYQQGQQPQQQQQQQQQCQQQQQQQQPQ9R4YGGZDYQESZAAQHLY3L9L5VMSYRCQAXPWR6ES83NEC5XTMD4HPKVNGF4ZZQ4XNF7SKKG5KVH0UZPD3XXVNDWVP2CQAP0GLNRGQP2DHAD0U9HYLAF2UGSZQF5SRXQTH5ZLUJ8UH73NWQS0QR5C9C0TXQ7X08ZS"
, "UR:BYTES/1OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/TYPUUURNVF607QGQ05PQQQQQQ9VXGQQDUMWH3LD9GH8XF4ZYJQNMQ80L6MC0KMV6JMVV22RDPZP0SQQQQQQQPLLLLLLSYS8ZQYQQQQQQQQTQQ9P6MG599TND0NJ54AZSE0ZHSHLY7Q4P94C79GQSQQQQQQQZYQPQ68Q0NLXPZYTUVURY6X4G8U7T5H4QVCHY3KKXAW0R"
, "UR:BYTES/1OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/TYPUUURNVF607QGQ05PQQQQQQ9VXGQQDUMWH3LD9GH8XF4ZYJQNMQ80L6MC0KMV6JMVV22RDPZP0SQQQQQQQPLLLLLLSYS8ZQYQQQQQQQQTQQ9P6MG599TND0NJ54AZSE0ZHSHLY7Q4P94C79GQSQQQQQQQZYQPQ68Q0NLXPZYTUVURY6X4G8U7T5H4QVCHY3KKXAW0R"
, "UR:BYTES/2OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/3DJV07U94KMSQQQQQQQQZQ8A0CQSYQQQQQQQZQHA78FG5TANFV9Z2TPDWE57AUVCN2T06TDGCAJNYPAT9LVG5QFMU5QSQQQQQRLLLLLL3NZ3KK6TMJ2TTRH6SKU05V76GHEANAT2QSDCDPFG67YZQQPFFZKSZQQQQQQ0LLLLLUPYQRGRQQQQQQQQYGQZP4XCCZ3WFNWK"
, "UR:BYTES/2OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/3DJV07U94KMSQQQQQQQQZQ8A0CQSYQQQQQQQZQHA78FG5TANFV9Z2TPDWE57AUVCN2T06TDGCAJNYPAT9LVG5QFMU5QSQQQQQRLLLLLL3NZ3KK6TMJ2TTRH6SKU05V76GHEANAT2QSDCDPFG67YZQQPFFZKSZQQQQQQ0LLLLLUPYQRGRQQQQQQQQYGQZP4XCCZ3WFNWK"
, "UR:BYTES/3OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/0QXPCDECDVJ8PS0DPW59YDWSH262DUZV6VAT4E4GMPEQQQQQQQQQQ9SQZSRR3CY9AYW5V4Y0A5DMWD57JD7FXX9G95PYWVZYQGSX2Y0YENNVNLEHXVMTDS86QM4VNDHG9V0K3PNDMGAJLWLSTS6504SZYP80TGGDPFZR8AM7XZNM77XWKRHP0D0Y540FLPTA5C2HRUQ0"
, "UR:BYTES/3OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/0QXPCDECDVJ8PS0DPW59YDWSH262DUZV6VAT4E4GMPEQQQQQQQQQQ9SQZSRR3CY9AYW5V4Y0A5DMWD57JD7FXX9G95PYWVZYQGSX2Y0YENNVNLEHXVMTDS86QM4VNDHG9V0K3PNDMGAJLWLSTS6504SZYP80TGGDPFZR8AM7XZNM77XWKRHP0D0Y540FLPTA5C2HRUQ0"
, "UR:BYTES/3OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/0QXPCDECDVJ8PS0DPW59YDWSH262DUZV6VAT4E4GMPEQQQQQQQQQQ9SQZSRR3CY9AYW5V4Y0A5DMWD57JD7FXX9G95PYWVZYQGSX2Y0YENNVNLEHXVMTDS86QM4VNDHG9V0K3PNDMGAJLWLSTS6504SZYP80TGGDPFZR8AM7XZNM77XWKRHP0D0Y540FLPTA5C2HRUQ0"
, "UR:BYTES/4OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/3Y0PYQFPQW6EJAC7ZGZUYVSCVLPJUS8CVFT8389KFXX82VM3YQMT26FRVHLVUQJ8XPZQYGR9FH3H26HSK99ZH3MQ4D7EHCS7Y752EP37HGG2X6RU5V3PKD7PMSPZQF9L7DESNZNS0H4V56978HCMAEG9D99CNY5LYCV247F58T7M66QUQYSS907LVPMZLKYAWMDGRRKT"
, "UR:BYTES/4OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/3Y0PYQFPQW6EJAC7ZGZUYVSCVLPJUS8CVFT8389KFXX82VM3YQMT26FRVHLVUQJ8XPZQYGR9FH3H26HSK99ZH3MQ4D7EHCS7Y752EP37HGG2X6RU5V3PKD7PMSPZQF9L7DESNZNS0H4V56978HCMAEG9D99CNY5LYCV247F58T7M66QUQYSS907LVPMZLKYAWMDGRRKT"
, "UR:BYTES/6OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/KTS8U3XQP95WYR8VQ9V2KWHCTS399T3ZQCP8DE3WPTRN462K8QLEC0GGTE73JNL3THXER9YLHVA9C8FVP9DHM5GUKUGYR2PSQQQGQQGQQZQQQQQQSQPQQQYQQQQQQQQRQQQQQGSXQTWM4303C7A5DWM089WJ3XMTV6EWQLJYCQYK3CSVASQ432E6LPWZY8XYUTHDXVQQ"
, "UR:BYTES/7OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/QZQQZQQQSQQQQQYQQGQQPQQQQQQQQQCQQQQQQQQPQ9R4YGGZDYQESZAAQHLY3L9L5VMSYRCQAXPWR6ES83NEC5XTMD4HPKVNGF4ZZQ4XNF7SKKG5KVH0UZPD3XXVNDWVP2CQAP0GLNRGQP2DHAD0U9HYLAF2UGSZQF5SRXQTH5ZLUJ8UH73NWQS0QR5C9C0TXQ7X08ZS"
, "UR:BYTES/7OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/QZQQZQQQSQQQQQYQQGQQPQQQQQQQQQCQQQQQQQQPQ9R4YGGZDYQESZAAQHLY3L9L5VMSYRCQAXPWR6ES83NEC5XTMD4HPKVNGF4ZZQ4XNF7SKKG5KVH0UZPD3XXVNDWVP2CQAP0GLNRGQP2DHAD0U9HYLAF2UGSZQF5SRXQTH5ZLUJ8UH73NWQS0QR5C9C0TXQ7X08ZS"
, "UR:BYTES/8OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/E0DKKUXEJDPX589HZPQ6SVQQQZQQZQQQSQQQQQYQQGQQPQQPQQQQQPQQQQQZYQSZ56D86Z6EZJEJALSG9KYCEJD4ES9TQR59AR7VDQQ9FKL44LSKUNL3E38ZAMFNQQQQSQQSQQYQQQQQPQQZQQQGQQGQQQQQGQQQQQQQ0P6SEH"
]
d = DecodeQR()
for i in qrcodes:
if d.add_data(i) == DecodeQRStatus.COMPLETE:
break
assert d.qr_type == QRType.PSBT__LEGACY_UR
#complete should be true
assert d.is_complete
tx = d.get_psbt()
mnemonic = "zone zone zone zone zone abandon ability able abandon ability able abstract"
pp = PSBTParser(tx,Seed(mnemonic, wordlist=bip39.WORDLIST),"test")
assert pp.input_amount == 200000
assert pp.fee_amount == 226
assert pp.spend_amount == 123456
assert pp.change_amount == (pp.input_amount - pp.spend_amount - pp.fee_amount)
assert pp.change_amount == 76318
def test_specter_multisig_animated_qr():
qrcodes = [
@@ -252,8 +198,8 @@ def test_specter_multisig_animated_qr():
tx = d.get_psbt()
mnemonic = "zone zone zone zone zone abandon ability able abandon ability able abstract"
pp = PSBTParser(tx,Seed(mnemonic, wordlist=bip39.WORDLIST),"test")
mnemonic = "zone zone zone zone zone abandon ability able abandon ability able abstract".split()
pp = PSBTParser(p=tx, seed=Seed(mnemonic), network=SettingsConstants.TESTNET)
assert pp.input_amount == 1052818
@@ -290,8 +236,8 @@ def test_specter_multisig_animated_qr():
tx2 = d2.get_psbt()
mnemonic2 = "able bacon cable able bacon cable abandon abandon abandon abandon abandon access"
pp2 = PSBTParser(tx2,Seed(mnemonic2, wordlist=bip39.WORDLIST),"test")
mnemonic2 = "able bacon cable able bacon cable abandon abandon abandon abandon abandon access".split()
pp2 = PSBTParser(p=tx2, seed=Seed(mnemonic2), network=SettingsConstants.TESTNET)
assert pp2.input_amount == 1052818
@@ -334,8 +280,8 @@ def test_short_4_letter_mnemonic_qr():
assert d.is_complete
assert d.get_seed_phrase() == ["height", "demise", "useless", "trap", "grow", "lion", "found", "off", "key", "clown", "transfer", "enroll"]
def test_bitcoin_address():
def test_bitcoin_address():
bad1 = "loremipsum"
bad2 = "0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae"
bad3 = "121802020768124106400009195602431595117715840445"
@@ -361,49 +307,44 @@ def test_bitcoin_address():
d.add_data(legacy_address1)
assert d.get_address() == legacy_address1
assert d.get_address_type() == "P2PKH-main"
assert d.get_address_type() == (SettingsConstants.LEGACY_P2PKH, SettingsConstants.MAINNET)
d = DecodeQR()
d.add_data(legacy_address2)
assert d.get_address() == legacy_address2
assert d.get_address_type() == "P2PKH-main"
assert d.get_address_type() == (SettingsConstants.LEGACY_P2PKH, SettingsConstants.MAINNET)
d = DecodeQR()
d.add_data(main_bech32_address)
assert d.get_address() == main_bech32_address
assert d.get_address_type() == "Bech32-main"
assert d.get_address_type() == (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.MAINNET)
d = DecodeQR()
d.add_data(test_bech32_address)
assert d.get_address() == test_bech32_address
assert d.get_address_type() == "Bech32-test"
assert d.get_address_type() == (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.TESTNET)
d = DecodeQR()
d.add_data(main_nested_segwit_address)
assert d.get_address() == main_nested_segwit_address
assert d.get_address_type() == "P2SH-main"
assert d.get_address_type() == (SettingsConstants.NESTED_SEGWIT, SettingsConstants.MAINNET)
d = DecodeQR()
d.add_data(test_nested_segwit_address)
assert d.get_address() == test_nested_segwit_address
assert d.get_address_type() == "P2SH-test"
assert d.get_address_type() == (SettingsConstants.NESTED_SEGWIT, SettingsConstants.TESTNET)
d = DecodeQR()
d.add_data(main_bech32_address2)
assert d.get_address() == "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
assert d.get_address_type() == "Bech32-main"
d = DecodeQR()
d.add_data(main_bech32_address3)
assert d.get_address() == "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
assert d.get_address_type() == "Bech32-main"
assert d.get_address_type() == (SettingsConstants.NATIVE_SEGWIT, SettingsConstants.MAINNET)
def test_seed_qr():
File diff suppressed because one or more lines are too long
+13 -16
View File
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -9,6 +9,7 @@ from seedsigner.models.qr_type import QRType
from seedsigner.models.settings import SettingsConstants
def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type):
""" Helper method to re-run multiple variations of the same encode/decode test """
print(entropy)
@@ -16,7 +17,7 @@ def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type):
print(seed_phrase)
assert len(seed_phrase) == mnemonic_length
e = EncodeQR(seed_phrase=seed_phrase, qr_type=qr_type, wordlist=bip39.WORDLIST)
e = EncodeQR(seed_phrase=seed_phrase, qr_type=qr_type)
data = e.next_part()
print(data)
@@ -28,7 +29,7 @@ def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type):
border=3
)
decoder = DecodeQR(wordlist_language_code=SettingsConstants.LANGUAGE__ENGLISH)
decoder = DecodeQR()
status = decoder.add_image(image)
assert status == DecodeQRStatus.COMPLETE