Skip to content

Commit

Permalink
Merge branch 'dev' into nostr_delegation
Browse files Browse the repository at this point in the history
  • Loading branch information
kdmukai committed Mar 17, 2023
2 parents df7b4a6 + 2570dfe commit 85df49e
Show file tree
Hide file tree
Showing 28 changed files with 319 additions and 64 deletions.
40 changes: 40 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
[run]
branch = True

[report]
skip_empty = True
skip_covered = True

# Omit; need a different approach to test modules with hardware dependencies
omit =
*/__init__.py
*/tests/*
*/pyzbar/*
*/gui/*

# Regexes for lines to exclude from consideration
exclude_lines =
# Have to re-enable the standard pragma
pragma: no cover

# Don't complain about missing debug-only code:
def __repr__
def __str__
if self\.debug

# Don't complain if tests don't hit defensive assertion code:
raise AssertionError
raise NotImplementedError

# Don't complain if non-runnable code isn't run:
if 0:
if __name__ == .__main__.:

# Don't complain about abstract methods, they aren't run:
@(abc\.)?abstractmethod


[html]
directory = coverage_html_report
skip_empty = True
skip_covered = False
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ src/seedsigner.egg-info/
src/seedsigner/models/settings_definition.json
.idea
*.mo
.coverage
195 changes: 165 additions & 30 deletions README.md

Large diffs are not rendered by default.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added enclosures/open_pill_mini/Main_Chassis.stl
Binary file not shown.
Binary file not shown.
Binary file added enclosures/open_pill_mini_w_coverplate/Lid.stl
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setuptools.setup(
name="seedsigner",
version="0.5.2",
version="0.6.0",
author="SeedSigner",
author_email="[email protected]",
description="Build an offline, airgapped Bitcoin signing device for less than $50!",
Expand Down
2 changes: 1 addition & 1 deletion src/seedsigner/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class Controller(Singleton):
rather than at the top in order avoid circular imports.
"""

VERSION = "0.5.2"
VERSION = "0.6.0"

# Declare class member vars with type hints to enable richer IDE support throughout
# the code.
Expand Down
2 changes: 1 addition & 1 deletion src/seedsigner/gui/screens/scan_screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
@dataclass
class ScanScreen(BaseScreen):
decoder: DecodeQR = None
instructions_text: str = "Scan a QR code"
instructions_text: str = "< back | Scan a QR code"
resolution: Tuple[int,int] = (480, 480)
framerate: int = 12
render_rect: Tuple[int,int,int,int] = None
Expand Down
7 changes: 5 additions & 2 deletions src/seedsigner/models/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,11 @@ def get_multiselect_value_display_names(self, attr_name: str) -> List[str]:
raise Exception(f"Unsupported SettingsEntry.type: {settings_entry.type}")

display_names = []
for value in self._data[attr_name]:
display_names.append(settings_entry.get_selection_option_display_name_by_value(value))
# Iterate through the selection_options list in order to preserve intended sort
# order when adding which options are selected.
for value, display_name in settings_entry.selection_options:
if value in self._data[attr_name]:
display_names.append(display_name)
return display_names


Expand Down
15 changes: 11 additions & 4 deletions src/seedsigner/models/settings_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ class SettingsConstants:
COORDINATOR__NUNCHUK = "nun"
COORDINATOR__SPARROW = "spa"
COORDINATOR__SPECTER_DESKTOP = "spd"
COORDINATOR__KEEPER = "kpr"
ALL_COORDINATORS = [
(COORDINATOR__BLUE_WALLET, "BlueWallet"),
(COORDINATOR__NUNCHUK, "Nunchuk"),
(COORDINATOR__SPARROW, "Sparrow"),
(COORDINATOR__SPECTER_DESKTOP, "Specter Desktop"),
(COORDINATOR__KEEPER, "Keeper"),
]

LANGUAGE__ENGLISH = "en"
Expand All @@ -53,10 +55,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
Expand Down Expand Up @@ -364,7 +366,12 @@ class SettingsDefinition:
display_name="Coordinator software",
type=SettingsConstants.TYPE__MULTISELECT,
selection_options=SettingsConstants.ALL_COORDINATORS,
default_value=SettingsConstants.ALL_COORDINATORS),
default_value=[
SettingsConstants.COORDINATOR__BLUE_WALLET,
SettingsConstants.COORDINATOR__NUNCHUK,
SettingsConstants.COORDINATOR__SPARROW,
SettingsConstants.COORDINATOR__SPECTER_DESKTOP,
]),

SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
attr_name=SettingsConstants.SETTING__BTC_DENOMINATION,
Expand Down
12 changes: 5 additions & 7 deletions src/seedsigner/views/psbt_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,17 @@ def run(self):
if not PSBTParser.has_matching_input_fingerprint(psbt=self.controller.psbt, seed=seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)):
# Doesn't look like this seed can sign the current PSBT
button_str += " (?)"

if seed.passphrase is not None:
# TODO: Include lock icon on right side of button
pass

button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue"))

button_data.append(SCAN_SEED)
button_data.append(TYPE_12WORD)
button_data.append(TYPE_24WORD)

if self.controller.psbt_seed:
if PSBTParser.has_matching_input_fingerprint(psbt=self.controller.psbt, seed=self.controller.psbt_seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)):
# skip the seed prompt if a seed was previous selected and has matching input fingerprint
return Destination(PSBTOverviewView)
if PSBTParser.has_matching_input_fingerprint(psbt=self.controller.psbt, seed=self.controller.psbt_seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)):
# skip the seed prompt if a seed was previous selected and has matching input fingerprint
return Destination(PSBTOverviewView)

selected_menu_num = ButtonListScreen(
title="Select Signer",
Expand Down
8 changes: 6 additions & 2 deletions src/seedsigner/views/scan_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def run(self):
psbt = self.decoder.get_psbt()
self.controller.psbt = psbt
self.controller.psbt_parser = None
return Destination(PSBTSelectSeedView)
return Destination(PSBTSelectSeedView, skip_current_view=True)

elif self.decoder.is_settings:
from seedsigner.models.settings import Settings
Expand Down Expand Up @@ -84,7 +84,7 @@ def run(self):
return Destination(NotYetImplementedView)

self.controller.multisig_wallet_descriptor = descriptor
return Destination(MultisigWalletDescriptorView)
return Destination(MultisigWalletDescriptorView, skip_current_view=True)

elif self.decoder.is_address:
from seedsigner.views.seed_views import AddressVerificationStartView
Expand All @@ -93,6 +93,7 @@ def run(self):

return Destination(
AddressVerificationStartView,
skip_current_view=True,
view_args={
"address": address,
"script_type": script_type,
Expand Down Expand Up @@ -123,6 +124,9 @@ def run(self):
else:
return Destination(NotYetImplementedView)

elif self.decoder.is_invalid:
raise Exception("QRCode not recognized or not yet supported.")

return Destination(MainMenuView)


Expand Down
28 changes: 18 additions & 10 deletions src/seedsigner/views/seed_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ def __init__(self):
self.seeds = []
for seed in self.controller.storage.seeds:
self.seeds.append({
"fingerprint": seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)),
"has_passphrase": seed.passphrase is not None
"fingerprint": seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
})


Expand Down Expand Up @@ -375,6 +374,14 @@ def run(self):
addr = self.controller.unverified_address["address"][:7]
VERIFY_ADDRESS += f" {addr}"
button_data.append(VERIFY_ADDRESS)

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
self.controller.resume_main_flow = None
self.controller.psbt_seed = self.seed
return Destination(PSBTOverviewView, skip_current_view=True)

button_data.append(SCAN_PSBT)

Expand Down Expand Up @@ -530,6 +537,9 @@ def run(self):
).display()

if selected_menu_num == RET_CODE__BACK_BUTTON:
# If previous view is SeedOptionsView then that should be where resume_main_flow started (otherwise it would have been skipped).
if len(self.controller.back_stack) >= 2 and self.controller.back_stack[-2].View_cls == SeedOptionsView:
self.controller.resume_main_flow = None
return Destination(BackStackView)

else:
Expand Down Expand Up @@ -747,6 +757,9 @@ def __init__(self, seed_num: int, coordinator: str, derivation_path: str):
elif coordinator == SettingsConstants.COORDINATOR__BLUE_WALLET:
qr_type = QRType.XPUB

elif coordinator == SettingsConstants.COORDINATOR__KEEPER:
qr_type = QRType.XPUB

elif coordinator == SettingsConstants.COORDINATOR__NUNCHUK:
qr_type = QRType.XPUB__UR

Expand Down Expand Up @@ -1392,14 +1405,14 @@ def run(self):
sig_type = SettingsConstants.MULTISIG
if self.controller.multisig_wallet_descriptor:
# Can jump straight to the brute-force verification View
destination = Destination(SeedAddressVerificationView)
destination = Destination(SeedAddressVerificationView, skip_current_view=True)
else:
self.controller.resume_main_flow = Controller.FLOW__VERIFY_MULTISIG_ADDR
destination = Destination(LoadMultisigWalletDescriptorView)
destination = Destination(LoadMultisigWalletDescriptorView, skip_current_view=True)

else:
sig_type = SettingsConstants.SINGLE_SIG
destination = Destination(SeedSingleSigAddressVerificationSelectSeedView)
destination = Destination(SeedSingleSigAddressVerificationSelectSeedView, skip_current_view=True)

elif self.controller.unverified_address["script_type"] == SettingsConstants.TAPROOT:
# TODO: add Taproot support
Expand Down Expand Up @@ -1477,12 +1490,7 @@ def run(self):

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"))

text = "Select seed to verify"

button_data.append(SCAN_SEED)
Expand Down
4 changes: 0 additions & 4 deletions src/seedsigner/views/tools_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,10 +433,6 @@ def run(self):
seeds = self.controller.storage.seeds
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"))

button_data.append(SCAN_SEED)
Expand Down
16 changes: 16 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,19 @@ Run a specific test:
```
pytest tests/test_this_file.py::test_this_specific_test
```

### Test Coverage
Run tests and generate test coverage
```
coverage run -m pytest
```

Show the resulting test coverage details:
```
coverage report
```

Generate the html overview:
```
coverage html
```
3 changes: 2 additions & 1 deletion tests/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
coverage==7.2.1
mock==4.0.3
pytest==6.2.4
mock==4.0.3
2 changes: 1 addition & 1 deletion tests/test_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def test_missing_settings_get_defaults(reset_controller):
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__COORDINATORS) == [i for i,j in SettingsConstants.ALL_COORDINATORS if i!="kpr"]
assert controller.settings.get_value(SettingsConstants.SETTING__BTC_DENOMINATION) == SettingsConstants.BTC_DENOMINATION__THRESHOLD

# Advanced Settings defaults
Expand Down
46 changes: 46 additions & 0 deletions tests/test_mnemonic_generation.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pytest
import random

from embit import bip39
Expand Down Expand Up @@ -42,6 +43,35 @@ def test_calculate_checksum():
assert bip39.mnemonic_is_valid(" ".join(mnemonic))


def test_calculate_checksum_invalid_mnemonics():
"""
Should raise an Exception on a mnemonic that is invalid due to length or using invalid words.
"""
with pytest.raises(Exception) as e:
# Mnemonic is too short: 10 words instead of 11
partial_mnemonic = "abandon " * 9 + "about"
mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
assert "12- or 24-word" in str(e)

with pytest.raises(Exception) as e:
# Valid mnemonic but unsupported length
mnemonic = "devote myth base logic dust horse nut collect buddy element eyebrow visit empty dress jungle"
mnemonic_generation.calculate_checksum(mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
assert "12- or 24-word" in str(e)

with pytest.raises(Exception) as e:
# Mnemonic is too short: 22 words instead of 23
partial_mnemonic = "abandon " * 21 + "about"
mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
assert "12- or 24-word" in str(e)

with pytest.raises(ValueError) as e:
# Invalid BIP-39 word
partial_mnemonic = "foobar " * 11 + "about"
mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
assert "not in the dictionary" in str(e)



def test_calculate_checksum_with_default_final_word():
""" 11-word and 23-word mnemonics use word `0000` as a temp final word to complete
Expand All @@ -62,6 +92,22 @@ def test_calculate_checksum_with_default_final_word():
assert mnemonic1 == mnemonic2


def test_generate_mnemonic_from_bytes():
"""
Should generate a valid BIP-39 mnemonic from entropy bytes
"""
# From iancoleman.io
entropy = "3350f6ac9eeb07d2c6209932808aa7f6"
expected_mnemonic = "crew marble private differ race truly blush basket crater affair prepare unique".split()
mnemonic = mnemonic_generation.generate_mnemonic_from_bytes(bytes.fromhex(entropy))
assert mnemonic == expected_mnemonic

entropy = "5bf41629fce815c3570955e8f45422abd7e2234141bd4d7ec63b741043b98cad"
expected_mnemonic = "fossil pass media what life ticket found click trophy pencil anger fish lawsuit balance agree dash estate wage mom trial aerobic system crawl review".split()
mnemonic = mnemonic_generation.generate_mnemonic_from_bytes(bytes.fromhex(entropy))
assert mnemonic == expected_mnemonic



def test_verify_against_coldcard_sample():
""" https://coldcard.com/docs/verifying-dice-roll-math """
Expand Down

0 comments on commit 85df49e

Please sign in to comment.