Merge pull request #316 from SeedSigner/dev

Merge recent dev branch PRs into 0.5.2 release branch
This commit is contained in:
Nick Klockenga
2023-02-01 07:57:01 -05:00
committed by GitHub
9 changed files with 109 additions and 60 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ Derivation paths for standard script types for mainnet:
- Script Type: P2WSH in P2SH
- Public Key Encoding: 0x0295b43f - Ypub
Custom derivation paths are also optional when generating an xpub from SeedSigner. The Public Key Encodings is detected based on the derivation path configured. Embit bitcoin library does the detection and is documented [here](https://github.com/diybitcoinhardware/embit/blob/master/docs/api/bip32.md#detect_version). For a video explination of these standards see a presentation by Stepan of Embit/Specter on this topic: https://youtube.com/watch?v=JCaC5DG2HTM
Custom derivation paths are also optional when generating an xpub from SeedSigner. The Public Key Encodings are detected based on the derivation path configured. The `embit` bitcoin library does this detection and is documented [here](https://github.com/diybitcoinhardware/embit/blob/master/docs/api/bip32.md#detect_version). For a video explanation of these standards see a presentation by Stepan of `embit` on this topic: https://youtube.com/watch?v=JCaC5DG2HTM
Changing the network settings from main to test in SeedSigner will change the public key encoding and derivation path following [slip-0132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) standards.
+20 -5
View File
@@ -265,6 +265,9 @@ class ButtonListScreen(BaseTopNavScreen):
Button_cls = Button
checked_buttons: List[int] = None
# Enables returning w/buttons rendered at the same place
scroll_y_initial_offset: int = None
def __post_init__(self):
super().__post_init__()
@@ -315,6 +318,7 @@ class ButtonListScreen(BaseTopNavScreen):
right_icon_name=right_icon_name,
screen_x=GUIConstants.EDGE_PADDING,
screen_y=button_list_y + i * (button_height + GUIConstants.LIST_ITEM_PADDING),
scroll_y=self.scroll_y_initial_offset if self.scroll_y_initial_offset is not None else 0,
width=self.canvas_width - (2 * GUIConstants.EDGE_PADDING),
height=button_height,
is_text_centered=self.is_button_text_centered,
@@ -330,7 +334,7 @@ class ButtonListScreen(BaseTopNavScreen):
if self.has_scroll_arrows:
self.arrow_half_width = 10
self.cur_scroll_y = 0
self.cur_scroll_y = self.scroll_y_initial_offset if self.scroll_y_initial_offset is not None else 0
self.up_arrow_img = Image.new("RGBA", size=(2 * self.arrow_half_width, 8), color="black")
self.up_arrow_img_y = self.top_nav.height - 12
arrow_draw = ImageDraw.Draw(self.up_arrow_img)
@@ -346,10 +350,6 @@ class ButtonListScreen(BaseTopNavScreen):
cur_selected_button = self.buttons[self.selected_button]
cur_selected_button.is_selected = True
if self.has_scroll_arrows:
frame_scroll = self.buttons[0].screen_y - cur_selected_button.screen_y
for button in self.buttons:
button.scroll_y -= frame_scroll
def _render(self):
@@ -906,6 +906,21 @@ class PowerOffScreen(BaseTopNavScreen):
@dataclass
class PowerOffNotRequiredScreen(BaseTopNavScreen):
def __post_init__(self):
self.title = "Just Unplug It"
self.show_back_button = True
super().__post_init__()
self.components.append(TextArea(
text="It is safe to disconnect power at any time.",
screen_y=self.top_nav.height,
height=self.canvas_height - self.top_nav.height,
))
@dataclass
class KeyboardScreen(BaseTopNavScreen):
"""
+1 -1
View File
@@ -616,7 +616,7 @@ class SeedExportXpubDetailsScreen(WarningEdgesMixin, ButtonListScreen):
@dataclass
class SeedAddPassphraseScreen(BaseTopNavScreen):
title: str = "Add Passphrase"
title: str = "BIP-39 Passphrase"
passphrase: str = ""
KEYBOARD__LOWERCASE_BUTTON_TEXT = "abc"
+3 -3
View File
@@ -53,10 +53,10 @@ class SettingsConstants:
BTC_DENOMINATION__THRESHOLD = "thr"
BTC_DENOMINATION__BTCSATSHYBRID = "hyb"
ALL_BTC_DENOMINATIONS = [
(BTC_DENOMINATION__BTC, "Btc-only"),
(BTC_DENOMINATION__SATS, "Sats-only"),
(BTC_DENOMINATION__BTC, "BTC"),
(BTC_DENOMINATION__SATS, "sats"),
(BTC_DENOMINATION__THRESHOLD, "Threshold at 0.01"),
(BTC_DENOMINATION__BTCSATSHYBRID, "Btc | Sats hybrid"),
(BTC_DENOMINATION__BTCSATSHYBRID, "BTC | sats hybrid"),
]
CAMERA_ROTATION__0 = 0
+1 -1
View File
@@ -211,7 +211,7 @@ class SeedFinalizeView(View):
def run(self):
FINALIZE = "Done"
PASSPHRASE = ("Add Passphrase", FontAwesomeIconConstants.LOCK)
PASSPHRASE = "BIP-39 Passphrase"
button_data = []
button_data.append(FINALIZE)
+16 -7
View File
@@ -9,11 +9,14 @@ from seedsigner.models.settings import SettingsConstants, SettingsDefinition
class SettingsMenuView(View):
def __init__(self, visibility: str = SettingsConstants.VISIBILITY__GENERAL, selected_attr: str = None):
def __init__(self, visibility: str = SettingsConstants.VISIBILITY__GENERAL, selected_attr: str = None, initial_scroll: int = 0):
super().__init__()
self.visibility = visibility
self.selected_attr = selected_attr
# Used to preserve the rendering position in the list
self.initial_scroll = initial_scroll
def run(self):
IO_TEST = "I/O test"
@@ -53,12 +56,17 @@ class SettingsMenuView(View):
title = "Dev Options"
next = None
selected_menu_num = ButtonListScreen(
screen = ButtonListScreen(
title=title,
is_button_text_centered=False,
button_data=button_data,
selected_button=selected_button,
).display()
scroll_y_initial_offset=self.initial_scroll,
)
selected_menu_num = screen.display()
# Preserve our scroll position in this Screen so we can return
initial_scroll = screen.buttons[0].scroll_y
if selected_menu_num == RET_CODE__BACK_BUTTON:
if self.visibility == SettingsConstants.VISIBILITY__GENERAL:
@@ -78,8 +86,7 @@ class SettingsMenuView(View):
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})
return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=settings_entries[selected_menu_num].attr_name, parent_initial_scroll=initial_scroll))
@@ -88,10 +95,11 @@ class SettingsEntryUpdateSelectionView(View):
Handles changes to all selection-type settings (Multiselect, SELECT_1,
Enabled/Disabled, etc).
"""
def __init__(self, attr_name: str):
def __init__(self, attr_name: str, parent_initial_scroll: int = 0):
super().__init__()
self.settings_entry = SettingsDefinition.get_settings_entry(attr_name)
self.selected_button = None
self.parent_initial_scroll = parent_initial_scroll
def run(self):
@@ -129,7 +137,8 @@ class SettingsEntryUpdateSelectionView(View):
SettingsMenuView,
view_args={
"visibility": self.settings_entry.visibility,
"selected_attr": self.settings_entry.attr_name
"selected_attr": self.settings_entry.attr_name,
"initial_scroll": self.parent_initial_scroll,
}
)
+10 -5
View File
@@ -559,11 +559,12 @@ class ToolsAddressExplorerAddressTypeView(View):
class ToolsAddressExplorerAddressListView(View):
def __init__(self, is_change: bool = False, start_index: int = 0, selected_button_index: int = 0):
def __init__(self, is_change: bool = False, start_index: int = 0, selected_button_index: int = 0, initial_scroll: int = 0):
super().__init__()
self.is_change = is_change
self.start_index = start_index
self.selected_button_index = selected_button_index
self.initial_scroll = initial_scroll
def run(self):
@@ -632,6 +633,7 @@ class ToolsAddressExplorerAddressListView(View):
is_button_text_centered=False,
is_bottom_list=True,
selected_button=self.selected_button_index,
scroll_y_initial_offset=self.initial_scroll,
)
finally:
# Everything is set. Stop the loading screen
@@ -647,20 +649,23 @@ class ToolsAddressExplorerAddressListView(View):
# User clicked NEXT
return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=self.is_change, start_index=self.start_index + addrs_per_screen))
# Preserve the list's current scroll so we can return to the same spot
initial_scroll = screen.buttons[0].scroll_y
index = selected_menu_num + self.start_index
return Destination(ToolsAddressExplorerAddressView, view_args=dict(index=index, address=addresses[selected_menu_num], is_change=self.is_change, start_index=self.start_index), skip_current_view=True)
return Destination(ToolsAddressExplorerAddressView, view_args=dict(index=index, address=addresses[selected_menu_num], is_change=self.is_change, start_index=self.start_index, parent_initial_scroll=initial_scroll), skip_current_view=True)
class ToolsAddressExplorerAddressView(View):
def __init__(self, index: int, address: str, is_change: bool, start_index: int):
def __init__(self, index: int, address: str, is_change: bool, start_index: int, parent_initial_scroll: int = 0):
super().__init__()
self.index = index
self.address = address
self.is_change = is_change
self.start_index = start_index
self.parent_initial_scroll = parent_initial_scroll
data = self.controller.address_explorer_data
def run(self):
qr_encoder = EncodeQR(qr_type=QRType.BITCOIN_ADDRESS, bitcoin_address=self.address)
@@ -669,4 +674,4 @@ class ToolsAddressExplorerAddressView(View):
).display()
# Exiting/Cancelling the QR display screen always returns to the list
return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=self.is_change, start_index=self.start_index, selected_button_index=self.index - self.start_index), skip_current_view=True)
return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=self.is_change, start_index=self.start_index, selected_button_index=self.index - self.start_index, initial_scroll=self.parent_initial_scroll), skip_current_view=True)
+5 -7
View File
@@ -3,7 +3,7 @@ from typing import List
from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants
from seedsigner.gui.screens import RET_CODE__POWER_BUTTON
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, DireWarningScreen, LargeButtonScreen, PowerOffScreen, ResetScreen, WarningScreen
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, DireWarningScreen, LargeButtonScreen, PowerOffScreen, PowerOffNotRequiredScreen, ResetScreen, WarningScreen
from seedsigner.models.threads import BaseThread
from seedsigner.models import Settings
@@ -199,6 +199,10 @@ class RestartView(View):
class PowerOffView(View):
def run(self):
if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
PowerOffNotRequiredScreen().display()
return Destination(BackStackView)
else:
thread = PowerOffView.PowerOffThread()
thread.start()
PowerOffScreen().display()
@@ -210,12 +214,6 @@ class PowerOffView(View):
from subprocess import call
while self.keep_running:
time.sleep(5)
if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
# disable microsd detection before shutdown to prevent display of toast notification during shutdown
from seedsigner.controller import Controller
Controller.get_instance().microsd.stop()
call("poweroff", shell=True)
else:
call("sudo shutdown --poweroff now", shell=True)
+46 -24
View File
@@ -1,29 +1,37 @@
import configparser
import pytest
from mock import MagicMock
from seedsigner.hardware.microsd import MicroSD
from seedsigner.controller import Controller
from seedsigner.models.settings import Settings
from seedsigner.models.settings_definition import SettingsConstants
def test_singleton_init_fails():
@pytest.fixture()
def reset_controller():
"""fixture to setup, then yield to run test, then tear down"""
# setup
Controller.configure_instance(disable_hardware=True)
# yield to run a single test
yield
# tear down
MicroSD._instance = None
Controller._instance = None
def test_singleton_init_fails(reset_controller):
""" The Controller should not allow any code to instantiate it via Controller() """
with pytest.raises(Exception):
c = Controller()
def test_singleton_get_instance_preserves_state():
def test_singleton_get_instance_preserves_state(reset_controller):
""" Changes to the Controller singleton should be preserved across calls to get_instance() """
# Must reset Singleton instances; pytest cannot properly isolate Singletons for us
# automatically.
# TODO: Cleaner solution here would be nice.
Settings._instance = None
Controller._instance = None
# Initialize the instance and verify that it read the config settings
Controller.configure_instance(disable_hardware=True)
controller = Controller.get_instance()
assert controller.unverified_address is None
@@ -35,19 +43,33 @@ def test_singleton_get_instance_preserves_state():
assert controller.unverified_address == "123abc"
def test_missing_settings_get_defaults():
""" Should gracefully handle any missing fields from `settings.ini` """
# TODO: This is not complete; currently only handles missing compact_seedqr_enabled.
def test_missing_settings_get_defaults(reset_controller):
""" Should gracefully handle all missing fields from `settings.json` """
# Must reset Singleton instances; pytest cannot properly isolate Singletons for us
# automatically.
# TODO: Cleaner solution here would be nice.
Settings._instance = None
Controller._instance = None
# Controller should parse the settings fine, even though a field is missing
Controller.configure_instance(disable_hardware=True)
# Controller should still have a default value
controller = Controller.get_instance()
assert controller.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__DISABLED
# Settings defaults
assert controller.settings.get_value(SettingsConstants.SETTING__LANGUAGE) == SettingsConstants.LANGUAGE__ENGLISH
assert controller.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) == SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
assert controller.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED
assert controller.settings.get_value(SettingsConstants.SETTING__COORDINATORS) == [i for i,j in SettingsConstants.ALL_COORDINATORS]
assert controller.settings.get_value(SettingsConstants.SETTING__BTC_DENOMINATION) == SettingsConstants.BTC_DENOMINATION__THRESHOLD
# Advanced Settings defaults
assert controller.settings.get_value(SettingsConstants.SETTING__NETWORK) == SettingsConstants.MAINNET
assert controller.settings.get_value(SettingsConstants.SETTING__QR_DENSITY) == SettingsConstants.DENSITY__MEDIUM
assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__SIG_TYPES) == [i for i,j in SettingsConstants.ALL_SIG_TYPES]
assert controller.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES) == [SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT]
assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__CAMERA_ROTATION) == SettingsConstants.CAMERA_ROTATION__0
assert controller.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__DISABLED
assert controller.settings.get_value(SettingsConstants.SETTING__PRIVACY_WARNINGS) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__ENABLED
assert controller.settings.get_value(SettingsConstants.SETTING__PARTNER_LOGOS) == SettingsConstants.OPTION__ENABLED
# Hidden Settings defaults
assert controller.settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS) == 189