cleanup, light refactor, new test

This commit is contained in:
kdmukai
2023-08-08 18:55:19 -05:00
parent 5d5521cfc7
commit d0e623be28
6 changed files with 229 additions and 47 deletions
+21 -8
View File
@@ -313,7 +313,6 @@ class TextArea(BaseComponent):
)
# Calculate the actual font height from the "baseline" anchor ("_s")
height_calc_start = time()
font = Fonts.get_font(self.font_name, self.font_size)
# Note: from the baseline anchor, `top` is a negative number while `bottom`
@@ -325,8 +324,6 @@ class TextArea(BaseComponent):
# Initialize the text rendering relative to the baseline
self.text_y = self.text_height_above_baseline
print(f"{time() - height_calc_start}s to calc self.text_font_height")
# Other components, like IconTextLine will need to know how wide the actual
# rendered text will be, separate from the TextArea's defined overall `width`.
self.text_width = max(line["text_width"] for line in self.text_lines)
@@ -1485,15 +1482,31 @@ def reflow_text_for_width(text: str,
def break_lines_into_pages(lines: list[str],
def reflow_text_into_pages(text: str,
width: int,
height: int,
font_name=GUIConstants.BODY_FONT_NAME,
font_size=GUIConstants.BODY_FONT_SIZE,
line_spacer: int = GUIConstants.BODY_LINE_SPACING) -> list[str]:
line_spacer: int = GUIConstants.BODY_LINE_SPACING,
allow_text_overflow: bool=False) -> list[str]:
"""
After splitting a long text into width-limited individual text lines, now calculate
how many lines will fit on a "page" and group accordingly.
Invokes `reflow_text_for_width` above to convert long text into width-limited
individual text lines and then calculates how many lines will fit on a "page" and
groups the output accordingly.
Returns a list of strings where each string is a page's worth of line-breaked text.
"""
reflowed_lines_dicts = reflow_text_for_width(text=text,
width=width,
font_name=font_name,
font_size=font_size,
allow_text_overflow=allow_text_overflow)
lines = []
for line_dict in reflowed_lines_dicts:
lines.append(line_dict["text"])
print(f"""{line_dict["text_width"]:3}: {line_dict["text"]}""")
font = Fonts.get_font(font_name=font_name, size=font_size)
# Measure the font's height above baseline via the "baseline" anchor ("_s")
(left, top, right, bottom) = font.getbbox("Agjpqy", anchor="ls")
@@ -1503,7 +1516,7 @@ def break_lines_into_pages(lines: list[str],
# I'm sure there's a smarter way to do this...
lines_per_page = 0
for i in range(1, height):
if height > font_height_above_baseline * i + line_spacer * (i-1) + bottom:
if height > font_height_above_baseline * i + line_spacer * (i-1) + font_height_below_baseline:
lines_per_page = i
else:
break
+3 -15
View File
@@ -11,7 +11,7 @@ from seedsigner.models.threads import BaseThread, ThreadsafeCounter
from .screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, KeyboardScreen, WarningEdgesMixin
from ..components import (Button, FontAwesomeIconConstants, Fonts, FormattedAddress, IconButton,
IconTextLine, SeedSignerIconConstants, TextArea, GUIConstants, reflow_text_for_width, break_lines_into_pages)
IconTextLine, SeedSignerIconConstants, TextArea, GUIConstants, reflow_text_into_pages)
from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
from seedsigner.hardware.buttons import HardwareButtons, HardwareButtonsConstants
@@ -1512,22 +1512,10 @@ class SeedSignMessageConfirmMessageScreen(ButtonListScreen):
self.sign_message_data = Controller.get_instance().sign_message_data
if "paged_message" not in self.sign_message_data:
reflowed_lines_dicts = reflow_text_for_width(
paged = reflow_text_into_pages(
text=self.sign_message_data["message"],
width=renderer.canvas_width - 2*GUIConstants.EDGE_PADDING,
font_name=GUIConstants.BODY_FONT_NAME,
font_size=GUIConstants.BODY_FONT_SIZE,
allow_text_overflow=True,
)
lines = []
for line_dict in reflowed_lines_dicts:
lines.append(line_dict["text"])
print(f"""{line_dict["text_width"]:3}: {line_dict["text"]}""")
paged = break_lines_into_pages(
lines=lines,
height=message_height
height=message_height,
)
self.sign_message_data["paged_message"] = paged
+15 -4
View File
@@ -113,6 +113,13 @@ def get_embit_network_name(settings_name):
def parse_derivation_path(derivation_path: str) -> dict:
"""
Parses a derivation path into its related SettingsConstants equivalents.
Primarily only supports single sig derivation paths.
May return None for fields it cannot parse.
"""
# Support either m/44'/... or m/44h/... style
derivation_path = derivation_path.replace("'", "h")
@@ -136,14 +143,18 @@ def parse_derivation_path(derivation_path: str) -> dict:
details = dict()
details["script_type"] = lookups["script_types"].get(sections[1])
if not details["script_type"]:
details["script_type"] = SettingsConstants.CUSTOM_DERIVATION
details["network"] = lookups["networks"].get(sections[2])
details["is_change"] = sections[-2] == "1"
details["index"] = int(sections[-1])
if details["script_type"] and details["network"] and sections[3] == "0h":
details["clean_match"] = True
else:
details["clean_match"] = False
details["clean_match"] = True
for k, v in details.items():
if v is None:
# At least one field couldn't be parsed
details["clean_match"] = False
break
return details
+22 -19
View File
@@ -76,6 +76,12 @@ class SeedSelectSeedView(View):
* `flow`: indicates which user flow is in progress during seed selection (e.g.
verify single sig addr or sign message).
"""
SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE)
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
button_data = []
def __init__(self, flow: str = Controller.FLOW__VERIFY_SINGLESIG_ADDR):
super().__init__()
self.flow = flow
@@ -84,11 +90,6 @@ class SeedSelectSeedView(View):
def run(self):
seeds = self.controller.storage.seeds
SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE)
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
button_data = []
if self.flow == Controller.FLOW__VERIFY_SINGLESIG_ADDR:
title = "Verify Address"
if not seeds:
@@ -106,24 +107,26 @@ class SeedSelectSeedView(View):
else:
raise Exception(f"Unsupported `flow` specified: {self.flow}")
self.button_data = []
for seed in seeds:
button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
if seed.passphrase is not None:
# TODO: Include lock icon on right side of button
pass
button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue"))
self.button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue"))
self.button_data.append(self.SCAN_SEED)
self.button_data.append(self.TYPE_12WORD)
self.button_data.append(self.TYPE_24WORD)
button_data.append(SCAN_SEED)
button_data.append(TYPE_12WORD)
button_data.append(TYPE_24WORD)
selected_menu_num = seed_screens.SeedSelectSeedScreen(
selected_menu_num = self.run_screen(
seed_screens.SeedSelectSeedScreen,
title=title,
text=text,
is_button_text_centered=False,
button_data=button_data
).display()
button_data=self.button_data
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
@@ -140,13 +143,13 @@ class SeedSelectSeedView(View):
self.controller.resume_main_flow = self.flow
if button_data[selected_menu_num] == SCAN_SEED:
if self.button_data[selected_menu_num] == self.SCAN_SEED:
from seedsigner.views.scan_views import ScanView
return Destination(ScanView)
elif button_data[selected_menu_num] in [TYPE_12WORD, TYPE_24WORD]:
elif self.button_data[selected_menu_num] in [self.TYPE_12WORD, self.TYPE_24WORD]:
from seedsigner.views.seed_views import SeedMnemonicEntryView
if button_data[selected_menu_num] == TYPE_12WORD:
if self.button_data[selected_menu_num] == self.TYPE_12WORD:
self.controller.storage.init_pending_mnemonic(num_words=12)
else:
self.controller.storage.init_pending_mnemonic(num_words=24)
@@ -1995,7 +1998,7 @@ class SeedSignMessageStartView(View):
class SeedSignMessageConfirmMessageView(View):
def __init__(self, page_num: int = 0):
super().__init__()
self.page_num = page_num
self.page_num = page_num # Note: zero-indexed numbering!
self.data = self.controller.sign_message_data
self.seed_num = self.data.get("seed_num")
@@ -2012,7 +2015,7 @@ class SeedSignMessageConfirmMessageView(View):
)
if selected_menu_num == RET_CODE__BACK_BUTTON:
if self.page_num == 1:
if self.page_num == 0:
# We're exiting this flow entirely
self.controller.resume_main_flow = None
self.controller.sign_message_data = None
@@ -2021,7 +2024,7 @@ class SeedSignMessageConfirmMessageView(View):
# User clicked "Next"
if self.page_num == len(self.controller.sign_message_data["paged_message"]) - 1:
# We've reached the end of the paged message
return Destination(SeedSignMessageConfirmAddressView, skip_current_view=True)
return Destination(SeedSignMessageConfirmAddressView)
else:
return Destination(SeedSignMessageConfirmMessageView, view_args=dict(page_num=self.page_num + 1))
+65
View File
@@ -325,3 +325,68 @@ def test_get_multisig_address():
print(f' {func.__name__}(descriptor=Descriptor.from_string("{descriptor}"), index={args[1]}, is_change={args[2]}, embit_network="{args[3]}") raises Exception"')
func(descriptor=descriptor, index=args[1], is_change=args[2], embit_network=args[3])
def test_parse_derivation_path():
# Shouldn't care if input uses "'" or "h"
derivation_path = "m/84'/0'/0'/0/0"
result = embit_utils.parse_derivation_path(derivation_path)
assert(result["script_type"] == SC.NATIVE_SEGWIT)
assert(result["network"] == SC.MAINNET)
result = embit_utils.parse_derivation_path(derivation_path.replace("'", "h"))
assert(result["script_type"] == SC.NATIVE_SEGWIT)
assert(result["network"] == SC.MAINNET)
# Now exhaustively test supported permutations
vectors_args = {
(SC.MAINNET, SC.NATIVE_SEGWIT, False): "m/84'/0'/0'/0/5",
(SC.TESTNET, SC.NATIVE_SEGWIT, False): "m/84'/1'/0'/0/5",
(SC.REGTEST, SC.NATIVE_SEGWIT, False): "m/84'/1'/0'/0/5",
(SC.MAINNET, SC.NATIVE_SEGWIT, True): "m/84'/0'/0'/1/5",
(SC.TESTNET, SC.NATIVE_SEGWIT, True): "m/84'/1'/0'/1/5",
(SC.REGTEST, SC.NATIVE_SEGWIT, True): "m/84'/1'/0'/1/5",
(SC.MAINNET, SC.NESTED_SEGWIT, False): "m/49'/0'/0'/0/5",
(SC.TESTNET, SC.NESTED_SEGWIT, False): "m/49'/1'/0'/0/5",
(SC.REGTEST, SC.NESTED_SEGWIT, False): "m/49'/1'/0'/0/5",
(SC.MAINNET, SC.NESTED_SEGWIT, True): "m/49'/0'/0'/1/5",
(SC.TESTNET, SC.NESTED_SEGWIT, True): "m/49'/1'/0'/1/5",
(SC.REGTEST, SC.NESTED_SEGWIT, True): "m/49'/1'/0'/1/5",
(SC.MAINNET, SC.TAPROOT, False): "m/86'/0'/0'/0/5",
(SC.TESTNET, SC.TAPROOT, False): "m/86'/1'/0'/0/5",
(SC.REGTEST, SC.TAPROOT, False): "m/86'/1'/0'/0/5",
(SC.MAINNET, SC.TAPROOT, True): "m/86'/0'/0'/1/5",
(SC.TESTNET, SC.TAPROOT, True): "m/86'/1'/0'/1/5",
(SC.REGTEST, SC.TAPROOT, True): "m/86'/1'/0'/1/5",
# Try a typical custom derivation path (Unchained Capital)
(SC.MAINNET, SC.CUSTOM_DERIVATION, False): "m/45'/0'/0'/0/5",
(SC.TESTNET, SC.CUSTOM_DERIVATION, False): "m/45'/1'/0'/0/5",
(SC.REGTEST, SC.CUSTOM_DERIVATION, False): "m/45'/1'/0'/0/5",
(SC.MAINNET, SC.CUSTOM_DERIVATION, True): "m/45'/0'/0'/1/5",
(SC.TESTNET, SC.CUSTOM_DERIVATION, True): "m/45'/1'/0'/1/5",
(SC.REGTEST, SC.CUSTOM_DERIVATION, True): "m/45'/1'/0'/1/5",
# CRAZY custom derivation path
(None, SC.CUSTOM_DERIVATION, False): "m/879345978543'/908327034508534983495'/9085098430894380959043'/0/5",
}
for expected_result, derivation_path in vectors_args.items():
actual_result = embit_utils.parse_derivation_path(derivation_path)
if expected_result[0] == SC.MAINNET:
assert(actual_result["network"] == expected_result[0])
assert(actual_result["clean_match"] is True)
elif expected_result[0] is None:
assert(actual_result["network"] is None)
assert(actual_result["clean_match"] is False)
else:
# Testnet and regtest are returned as a list since the parser can't tell which is intended
assert(expected_result[0] in actual_result["network"])
assert(actual_result["clean_match"] is True)
assert(actual_result["script_type"] == expected_result[1])
assert(actual_result["is_change"] == expected_result[2])
assert(actual_result["index"] == int(derivation_path.split("/")[-1]))
+103 -1
View File
@@ -3,9 +3,10 @@ from base import BaseTest, FlowTest, FlowStep
from base import FlowTestRunScreenNotExecutedException, FlowTestInvalidButtonDataSelectionException
import pytest
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
from seedsigner.models.settings import SettingsConstants
from seedsigner.models.seed import Seed
from seedsigner.views.view import MainMenuView
from seedsigner.views.view import MainMenuView, View
from seedsigner.views import seed_views, scan_views
@@ -288,3 +289,104 @@ class TestSeedFlows(FlowTest):
FlowStep(seed_views.LoadSeedView),
]
)
def test_sign_message_flow(self):
"""
"""
# Ensure message signing is enabled
self.settings.set_value(SettingsConstants.SETTING__MESSAGE_SIGNING, SettingsConstants.OPTION__ENABLED)
def load_seed_into_decoder(view: scan_views.ScanView):
view.decoder.add_data("0000" * 11 + "0003")
derivation_path = "m/84h/0h/0h/0/0"
message = "I attest that I control this bitcoin address blah blah blah"
multipage_message = """Chancellor on brink of second bailout for banks
Billions may be needed as lending squeeze tightens
Alistair Darling has been forced to consider a second bailout for banks as the lending drought worsens.
The Chancellor will decide within weeks whether to pump billions more into the economy as evidence mounts that the £37 billion part-nationalisation last year has failed to keep credit flowing. Options include cash injections, offering banks cheaper state guarantees to raise money privately or buying up “toxic assets”, The Times has learnt."""
def load_standard_message_into_decoder(view: View):
view.decoder.add_data(f"signmessage {derivation_path} ascii:{message}")
def load_multipage_message_into_decoder(view: View):
view.decoder.add_data(f"signmessage {derivation_path} ascii:{multipage_message}")
def inject_mesage_as_paged_message(view: View):
# Because the Screen won't actually run, we have to do the Screen's work here
from seedsigner.gui.components import reflow_text_into_pages, GUIConstants
paged = reflow_text_into_pages(
text=self.controller.sign_message_data["message"],
width=240 - 2*GUIConstants.EDGE_PADDING,
height=240 - GUIConstants.TOP_NAV_HEIGHT - 3*GUIConstants.EDGE_PADDING - GUIConstants.BUTTON_HEIGHT,
)
self.controller.sign_message_data["paged_message"] = paged
# Scenario 1: Load the mesage first, then the seed
self.run_sequence([
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
FlowStep(scan_views.ScanView, before_run=load_standard_message_into_decoder), # simulate read message QR; ret val is ignored
FlowStep(seed_views.SeedSignMessageStartView, is_redirect=True),
FlowStep(seed_views.SeedSelectSeedView, button_data_selection=seed_views.SeedSelectSeedView.SCAN_SEED),
FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder), # simulate read SeedQR; ret val is ignored
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
FlowStep(seed_views.SeedSignMessageConfirmMessageView, before_run=inject_mesage_as_paged_message, screen_return_value=0),
FlowStep(seed_views.SeedSignMessageConfirmAddressView, screen_return_value=0),
FlowStep(seed_views.SeedSignMessageSignedMessageQRView, screen_return_value=0),
FlowStep(MainMenuView),
])
# Scenario 2: Scan the seed first, then select Sign Message
self.controller.discard_seed(0)
self.run_sequence([
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder), # simulate read SeedQR; ret val is ignored
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.SIGN_MESSAGE),
FlowStep(scan_views.ScanView, before_run=load_standard_message_into_decoder), # simulate read message QR; ret val is ignored
FlowStep(seed_views.SeedSignMessageStartView, is_redirect=True),
FlowStep(seed_views.SeedSignMessageConfirmMessageView, before_run=inject_mesage_as_paged_message, screen_return_value=0),
FlowStep(seed_views.SeedSignMessageConfirmAddressView, screen_return_value=0),
FlowStep(seed_views.SeedSignMessageSignedMessageQRView, screen_return_value=0),
FlowStep(MainMenuView),
])
# Scenario 3: Load a long, multipage message
self.run_sequence([
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
FlowStep(scan_views.ScanView, before_run=load_multipage_message_into_decoder), # simulate read message QR; ret val is ignored
FlowStep(seed_views.SeedSignMessageStartView, is_redirect=True),
FlowStep(seed_views.SeedSelectSeedView, button_data_selection=seed_views.SeedSelectSeedView.SCAN_SEED),
FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder), # simulate read SeedQR; ret val is ignored
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
FlowStep(seed_views.SeedSignMessageConfirmMessageView, before_run=inject_mesage_as_paged_message, screen_return_value=0), # page 1/5
FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=0), # page 2/5
FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=0), # page 3/5
FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=0), # page 4/5
FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=0), # page 5/5
# Arrive at the address confirmation, then go backwards to re-review the paged message
FlowStep(seed_views.SeedSignMessageConfirmAddressView, screen_return_value=RET_CODE__BACK_BUTTON), # then back to page 5/5
FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=RET_CODE__BACK_BUTTON), # back to page 4/5
FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=RET_CODE__BACK_BUTTON), # back to page 3/5
FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=RET_CODE__BACK_BUTTON), # back to page 2/5
FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=RET_CODE__BACK_BUTTON), # back to page 1/5
# Now proceed forward again to the end
FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=0), # page 1/5
FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=0), # page 2/5
FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=0), # page 3/5
FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=0), # page 4/5
FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=0), # page 5/5
FlowStep(seed_views.SeedSignMessageConfirmAddressView, screen_return_value=0),
FlowStep(seed_views.SeedSignMessageSignedMessageQRView, screen_return_value=0),
FlowStep(MainMenuView),
])