mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
Merge branch '0.5.0' into patch-1
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
"""
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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__()
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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))
|
||||
|
||||
@@ -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
@@ -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():
|
||||
|
||||
+23
-14
File diff suppressed because one or more lines are too long
+13
-16
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user