Merge branch 'dev' into 0.5.1-ram

This commit is contained in:
Nick Klockenga
2022-11-29 22:05:40 -05:00
26 changed files with 872 additions and 296 deletions
+2
View File
@@ -4,3 +4,5 @@ src/seedsigner.egg-info/
.nova .nova
.vscode .vscode
src/seedsigner/models/settings_definition.json src/seedsigner/models/settings_definition.json
.idea
*.mo
+2 -2
View File
@@ -7,7 +7,7 @@
* [Project Summary](#project-summary) * [Project Summary](#project-summary)
* [Shopping List](#shopping-list) * [Shopping List](#shopping-list)
* [Software Installation](#software-installation) * [Software Installation](#software-installation)
* [Verifying Your Software](#verifying-your-software) * [Verifying the Software](#verifying-the-software)
* [Enclosure Designs](#enclosure-designs) * [Enclosure Designs](#enclosure-designs)
* [SeedQR Printable Templates](#seedqr-printable-templates) * [SeedQR Printable Templates](#seedqr-printable-templates)
* [Manual Installation Instructions](#manual-installation-instructions) * [Manual Installation Instructions](#manual-installation-instructions)
@@ -64,7 +64,7 @@ If you have specific questions about the project, our [Telegram Group](https://t
To build a SeedSigner, you will need: To build a SeedSigner, you will need:
* Raspberry Pi Zero (preferably version 1.3 with no WiFi/Bluetooth capability, but any Raspberry Pi 2/3/4 or Zero model will work) * Raspberry Pi Zero (preferably version 1.3 with no WiFi/Bluetooth capability, but any Raspberry Pi 2/3/4 or Zero model will work, Raspberry Pi 1 devices will require a hardware modification to the Waveshare LCD Hat, as per the [instructions here](./docs/legacy_hardware.md))
* Waveshare 1.3" 240x240 pxl LCD (correct pixel count is important, more info at https://www.waveshare.com/wiki/1.3inch_LCD_HAT) * Waveshare 1.3" 240x240 pxl LCD (correct pixel count is important, more info at https://www.waveshare.com/wiki/1.3inch_LCD_HAT)
* Pi Zero-compatible camera (tested to work with the Aokin / AuviPal 5MP 1080p with OV5647 Sensor) * Pi Zero-compatible camera (tested to work with the Aokin / AuviPal 5MP 1080p with OV5647 Sensor)
Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

+22
View File
@@ -0,0 +1,22 @@
# Legacy Hardware Modifications
Older Raspberry Pi devices have a smaller GPIO header, 26 pin as opposed to the 40 pin header found on more recent models. The Waveshare LCD hat uses some of the pins between 26 and 40 for the buttons, meaning that these will need to be reassigned (and remapped in software) when used with an older device.
## Hardware Changes
A suggested remapping can be found here:
![Remapped Pins Shematic](./img/legacy_hardware_remapped_pins.jpg)
This remapping can be done by soldering wires on to the Waveshare hat as below:
![Remapped Pins Photo](./img/legacy_hardware_remapped_pins_photo.jpg)
Alternatively, you could do this by connecting the LCD hat on with individual breadboard jumper wires.
Once you have re-mapped the pins, it is advised to do an IO Test to ensure that everything works.
**Warning: Some of the GPIO pins on contain 5 volt output. Raspberry Pi GPIO pins are NOT 5V tolerant, meaning that if you accidentally connect a 5V supply pin to a GPIO input pin, you risk permanent damage.**
## Software Changes
The Seedsigner software will automatically detect which hardware revision you are using and if the older hardware is detected, will remap the software to match the above modifications to the Waveshare hat.
If you are using a pre-built Seedsigner image that hasn't yet has this incorporated, you can simply take the file "buttons.py" (/src/seedsigner/hardware/ int his repository) and overwrite same file on your Seedsigner SD card. (The easiest way to do this is to copy it on to the /boot/ partitition of the SD card then copy it over via the command line while connected to your Pi via monitor+keyboard) O
+69
View File
@@ -480,3 +480,72 @@ b'\n\xcb\xba\x00\x8d\x9b\xa0\x05\xf5\x99k@\xa3G\\\xd9'
</tr> </tr>
</table> </table>
---
## Test Vectors 7-9: Additional Compact SeedQR problem characters
Explicitly check Compact SeedQRs whose byte stream contains `\n`, `\r`, or `\r\n`:
`\n`:
```bash
# 12-word seed:
dignity utility vacant shiver thought canoe feel multiply item youth actor coyote
# Standard SeedQR digit stream:
049619221923158517990268067811630950204300210397
# CompactSeedQR bitstream:
00111110000111100000101111000001111000110001111000001110010000110001010100110100100010110111011011011111111011000000101010011000
# CompactSeedQR bytestream:
b'>\x1e\x0b\xc1\xe3\x1e\x0eC\x154\x8bv\xdf\xec\n\x98'
```
<table align="center">
<tr>
<td align="center"><img src="img/vector7_compact_12word.png"><br/>CompactSeedQR</td>
</tr>
</table>
`\r`:
```bash
# 12-word seed:
corn voice scrap arrow original diamond trial property benefit choose junk lock
# Standard SeedQR digit stream:
038719631547010112530489185713790169032209701051
# CompactSeedQR bitstream:
00110000011111101010111100000101100001100101100111001010011110100111101000001101011000110001010100100101000010011110010101000001
# CompactSeedQR bytestream:
b'0~\xaf\x05\x86Y\xcazz\rc\x15%\t\xe5A'
```
<table align="center">
<tr>
<td align="center"><img src="img/vector8_compact_12word.png"><br/>CompactSeedQR</td>
</tr>
</table>
`\r\n`:
```bash
# 12-word seed:
vocal tray giggle tool duck letter category pattern train magnet excite swamp
# Standard SeedQR digit stream:
196218530783182905421028028912901848107106301753
# CompactSeedQR bitstream:
11110101010111001111010110000111111100100101010000111101000000010000100100001101000010101110011100010000101111010011101101101101
# CompactSeedQR bytestream:
b'\xf5\\\xf5\x87\xf2T=\x01\t\r\n\xe7\x10\xbd;m'
```
<table align="center">
<tr>
<td align="center"><img src="img/vector9_compact_12word.png"><br/>CompactSeedQR</td>
</tr>
</table>
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

+1 -1
View File
@@ -1,4 +1,4 @@
embit==0.5.0 embit==0.6.1
numpy==1.21.1 numpy==1.21.1
picamera==1.13 picamera==1.13
Pillow==8.2.0 Pillow==8.2.0
+6 -2
View File
@@ -71,6 +71,8 @@ class Controller(Singleton):
image_entropy_preview_frames: List[Image] = None image_entropy_preview_frames: List[Image] = None
image_entropy_final_image: Image = None image_entropy_final_image: Image = None
address_explorer_data: dict = None
# TODO: end refactor section # TODO: end refactor section
# Destination placeholder for when we need to jump out to a side flow but intend to # Destination placeholder for when we need to jump out to a side flow but intend to
@@ -79,6 +81,7 @@ class Controller(Singleton):
FLOW__PSBT = "psbt" FLOW__PSBT = "psbt"
FLOW__VERIFY_MULTISIG_ADDR = "multisig_addr" FLOW__VERIFY_MULTISIG_ADDR = "multisig_addr"
FLOW__VERIFY_SINGLESIG_ADDR = "singlesig_addr" FLOW__VERIFY_SINGLESIG_ADDR = "singlesig_addr"
FLOW__ADDRESS_EXPLORER = "address_explorer"
resume_main_flow: str = None resume_main_flow: str = None
back_stack: BackStack = None back_stack: BackStack = None
@@ -202,9 +205,9 @@ class Controller(Singleton):
def run(self, some_arg, other_arg): def run(self, some_arg, other_arg):
print(other_arg) print(other_arg)
class OtherView(): class OtherView(View):
def run(self): def run(self):
return (MyView, {"some_arg": 1, "other_arg": "hello"}) return (MyView, dict(some_arg=1, other_arg="hello"))
When `OtherView` is instantiated and run, we capture its return values: When `OtherView` is instantiated and run, we capture its return values:
@@ -229,6 +232,7 @@ class Controller(Singleton):
self.resume_main_flow = None self.resume_main_flow = None
self.multisig_wallet_descriptor = None self.multisig_wallet_descriptor = None
self.unverified_address = None self.unverified_address = None
self.address_explorer_data = None
print(f"back_stack: {self.back_stack}") print(f"back_stack: {self.back_stack}")
+11 -3
View File
@@ -39,6 +39,12 @@ class Keyboard:
"font": COMPACT_KEY_FONT, "font": COMPACT_KEY_FONT,
"size": 1, "size": 1,
} }
KEY_SPACE_2 = {
"code": "SPACE",
"letter": "space",
"font": COMPACT_KEY_FONT,
"size": 2,
}
KEY_SPACE_3 = { KEY_SPACE_3 = {
"code": "SPACE", "code": "SPACE",
"letter": "space", "letter": "space",
@@ -117,6 +123,7 @@ class Keyboard:
rect_color = self.keyboard.deactivated_background_color rect_color = self.keyboard.deactivated_background_color
font_color = "#333" # Show the letter but render as gray font_color = "#333" # Show the letter but render as gray
outline_color = self.keyboard.deactivated_background_color outline_color = self.keyboard.deactivated_background_color
if self.is_selected: if self.is_selected:
# Inactive, selected just gets highlighted outline # Inactive, selected just gets highlighted outline
outline_color = self.keyboard.highlight_color outline_color = self.keyboard.highlight_color
@@ -125,11 +132,11 @@ class Keyboard:
font_color = "black" font_color = "black"
else: else:
if self.is_additional_key: if self.is_additional_key:
# rect_color = "#111" rect_color = "#000"
rect_color = self.keyboard.background_color font_color = "#999"
else: else:
rect_color = self.keyboard.background_color rect_color = self.keyboard.background_color
font_color = "#e8e8e8" font_color = "#e8e8e8"
self.keyboard.draw.rounded_rectangle( self.keyboard.draw.rounded_rectangle(
( (
@@ -186,6 +193,7 @@ class Keyboard:
self.auto_wrap = auto_wrap self.auto_wrap = auto_wrap
self.background_color = GUIConstants.BUTTON_BACKGROUND_COLOR self.background_color = GUIConstants.BUTTON_BACKGROUND_COLOR
self.deactivated_background_color = GUIConstants.BACKGROUND_COLOR self.deactivated_background_color = GUIConstants.BACKGROUND_COLOR
self.additional_key_deactivated_background_color = GUIConstants.BACKGROUND_COLOR
self.highlight_color = highlight_color self.highlight_color = highlight_color
# Does the specified layout work? # Does the specified layout work?
+2 -17
View File
@@ -2,6 +2,7 @@ from dataclasses import dataclass
from lzma import is_check_supported from lzma import is_check_supported
from PIL import Image, ImageDraw, ImageFilter from PIL import Image, ImageDraw, ImageFilter
from typing import List from typing import List
import time
from seedsigner.gui.renderer import Renderer from seedsigner.gui.renderer import Renderer
from seedsigner.models.threads import BaseThread from seedsigner.models.threads import BaseThread
@@ -439,7 +440,7 @@ class PSBTOverviewScreen(ButtonListScreen):
self.renderer.show_image() self.renderer.show_image()
# No need to CPU limit when running in its own thread? # No need to CPU limit when running in its own thread?
# time.sleep(0.02) time.sleep(0.02)
@@ -696,19 +697,3 @@ class PSBTFinalizeScreen(ButtonListScreen):
text="Click to authorize this transaction", text="Click to authorize this transaction",
screen_y=icon.screen_y + icon.height + GUIConstants.COMPONENT_PADDING screen_y=icon.screen_y + icon.height + GUIConstants.COMPONENT_PADDING
)) ))
@dataclass
class PSBTSelectCoordinatorScreen(ButtonListScreen):
def __post_init__(self):
# Customize defaults
self.title = "Signed PSBT"
self.is_bottom_list = True
super().__post_init__()
self.components.append(TextArea(
text="Export as a QR code for:",
is_text_centered=True,
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
))
+58 -26
View File
@@ -6,7 +6,7 @@ from typing import Any, List, Tuple
from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
from seedsigner.gui.renderer import Renderer from seedsigner.gui.renderer import Renderer
from seedsigner.models.threads import BaseThread from seedsigner.models.threads import BaseThread, ThreadsafeCounter
from seedsigner.models.encode_qr import EncodeQR from seedsigner.models.encode_qr import EncodeQR
from seedsigner.models.settings import Settings, SettingsConstants from seedsigner.models.settings import Settings, SettingsConstants
@@ -660,41 +660,73 @@ class LargeButtonScreen(BaseTopNavScreen):
class QRDisplayScreen(BaseScreen): class QRDisplayScreen(BaseScreen):
qr_encoder: EncodeQR = None qr_encoder: EncodeQR = None
class QRDisplayThread(BaseThread):
def __init__(self, qr_encoder: EncodeQR, qr_brightness: ThreadsafeCounter, renderer: Renderer):
super().__init__()
self.qr_encoder = qr_encoder
self.qr_brightness = qr_brightness
self.renderer = renderer
def run(self):
# Loop whether the QR is a single frame or animated; each loop might adjust
# brightness setting.
while self.keep_running:
# convert the self.qr_brightness integer (31-255) into hex triplets
hex_color = (hex(self.qr_brightness.cur_count).split('x')[1]) * 3
image = self.qr_encoder.next_part_image(240,240, border=2, background_color=hex_color)
with self.renderer.lock:
self.renderer.show_image(image)
# Target n held frames per second before rendering next QR image
time.sleep(5/30.0)
def __post_init__(self):
from seedsigner.models.settings import Settings
super().__post_init__()
# Shared coordination var so the display thread can detect success
settings = Settings.get_instance()
self.qr_brightness = ThreadsafeCounter(initial_value=settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS))
self.threads.append(QRDisplayScreen.QRDisplayThread(
qr_encoder=self.qr_encoder,
qr_brightness=self.qr_brightness,
renderer=self.renderer,
))
def _run(self): def _run(self):
from seedsigner.models.settings import Settings from seedsigner.models.settings import Settings
settings = Settings.get_instance()
cur_brightness = settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS)
# Loop whether the QR is a single frame or animated; each loop might adjust
# brightness setting.
while True: while True:
ret = self._run_callback() user_input = self.hw_inputs.wait_for(
if ret is not None: [
return ret HardwareButtonsConstants.KEY_UP,
HardwareButtonsConstants.KEY_DOWN,
# convert the cur_brightness integer (31-255) into hex triplets HardwareButtonsConstants.KEY_LEFT,
hex_color = (hex(cur_brightness).split('x')[1]) * 3 HardwareButtonsConstants.KEY_RIGHT,
image = self.qr_encoder.next_part_image(240,240, border=2, background_color=hex_color) ] + HardwareButtonsConstants.KEYS__ANYCLICK,
self.renderer.show_image(image) check_release=True,
release_keys=HardwareButtonsConstants.KEYS__ANYCLICK
# Target n held frames per second before rendering next QR image )
time.sleep(5/30.0) if user_input == HardwareButtonsConstants.KEY_DOWN:
if self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_DOWN):
# Reduce QR code background brightness # Reduce QR code background brightness
cur_brightness = max(31, cur_brightness - 31) self.qr_brightness.set_value(max(31, self.qr_brightness.cur_count - 31))
elif self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_UP): elif user_input == HardwareButtonsConstants.KEY_UP:
# Incrase QR code background brightness # Incrase QR code background brightness
cur_brightness = min(cur_brightness + 31, 255) self.qr_brightness.set_value(min(self.qr_brightness.cur_count + 31, 255))
elif self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_RIGHT): else:
# Any other input exits the screen
self.threads[-1].stop()
while self.threads[-1].is_alive():
time.sleep(0.01)
break break
Settings.get_instance().set_value(SettingsConstants.SETTING__QR_BRIGHTNESS, self.qr_brightness.cur_count)
settings.set_value(SettingsConstants.SETTING__QR_BRIGHTNESS, cur_brightness)
# TODO: handle left as BACK
+58 -15
View File
@@ -436,7 +436,6 @@ class SeedFinalizeScreen(ButtonListScreen):
@dataclass @dataclass
class SeedOptionsScreen(ButtonListScreen): class SeedOptionsScreen(ButtonListScreen):
# Customize defaults # Customize defaults
is_bottom_list: bool = True
fingerprint: str = None fingerprint: str = None
has_passphrase: bool = False has_passphrase: bool = False
@@ -445,6 +444,8 @@ class SeedOptionsScreen(ButtonListScreen):
self.top_nav_icon_color = "blue" self.top_nav_icon_color = "blue"
self.title = self.fingerprint self.title = self.fingerprint
self.is_button_text_centered = False self.is_button_text_centered = False
self.is_bottom_list = True
super().__post_init__() super().__post_init__()
@@ -621,7 +622,8 @@ class SeedAddPassphraseScreen(BaseTopNavScreen):
KEYBOARD__LOWERCASE_BUTTON_TEXT = "abc" KEYBOARD__LOWERCASE_BUTTON_TEXT = "abc"
KEYBOARD__UPPERCASE_BUTTON_TEXT = "ABC" KEYBOARD__UPPERCASE_BUTTON_TEXT = "ABC"
KEYBOARD__DIGITS_BUTTON_TEXT = "123" KEYBOARD__DIGITS_BUTTON_TEXT = "123"
KEYBOARD__SYMBOLS_BUTTON_TEXT = "!@#" KEYBOARD__SYMBOLS_1_BUTTON_TEXT = "!@#"
KEYBOARD__SYMBOLS_2_BUTTON_TEXT = "*[]"
def __post_init__(self): def __post_init__(self):
@@ -630,7 +632,14 @@ class SeedAddPassphraseScreen(BaseTopNavScreen):
keys_lower = "abcdefghijklmnopqrstuvwxyz" keys_lower = "abcdefghijklmnopqrstuvwxyz"
keys_upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" keys_upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
keys_number = "0123456789" keys_number = "0123456789"
keys_symbol = "!\"#$%&'()*+,=./;:<>?@[]|-_`~"
# Present the most common/puncutation-related symbols & the most human-friendly
# symbols first (limited to 18 chars).
keys_symbol_1 = """!@#$%&();:,.-+='"?"""
# Isolate the more math-oriented or just uncommon symbols
keys_symbol_2 = """^*[]{}_\\|<>/`~"""
# Set up the keyboard params # Set up the keyboard params
self.right_panel_buttons_width = 56 self.right_panel_buttons_width = 56
@@ -701,11 +710,11 @@ class SeedAddPassphraseScreen(BaseTopNavScreen):
render_now=False render_now=False
) )
self.keyboard_symbols = Keyboard( self.keyboard_symbols_1 = Keyboard(
draw=self.renderer.draw, draw=self.renderer.draw,
charset=keys_symbol, charset=keys_symbol_1,
rows=4, rows=4,
cols=max_cols, cols=6,
rect=( rect=(
GUIConstants.COMPONENT_PADDING, GUIConstants.COMPONENT_PADDING,
keyboard_start_y, keyboard_start_y,
@@ -713,7 +722,28 @@ class SeedAddPassphraseScreen(BaseTopNavScreen):
self.canvas_height - GUIConstants.EDGE_PADDING self.canvas_height - GUIConstants.EDGE_PADDING
), ),
additional_keys=[ additional_keys=[
Keyboard.KEY_SPACE_4, Keyboard.KEY_SPACE_2,
Keyboard.KEY_CURSOR_LEFT,
Keyboard.KEY_CURSOR_RIGHT,
Keyboard.KEY_BACKSPACE
],
auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT],
render_now=False
)
self.keyboard_symbols_2 = Keyboard(
draw=self.renderer.draw,
charset=keys_symbol_2,
rows=4,
cols=6,
rect=(
GUIConstants.COMPONENT_PADDING,
keyboard_start_y,
self.canvas_width - GUIConstants.COMPONENT_PADDING - self.right_panel_buttons_width,
self.canvas_height - GUIConstants.EDGE_PADDING
),
additional_keys=[
Keyboard.KEY_SPACE_2,
Keyboard.KEY_CURSOR_LEFT, Keyboard.KEY_CURSOR_LEFT,
Keyboard.KEY_CURSOR_RIGHT, Keyboard.KEY_CURSOR_RIGHT,
Keyboard.KEY_BACKSPACE Keyboard.KEY_BACKSPACE
@@ -823,8 +853,10 @@ class SeedAddPassphraseScreen(BaseTopNavScreen):
# Return to the same button2 keyboard, if applicable # Return to the same button2 keyboard, if applicable
if cur_keyboard == self.keyboard_digits: if cur_keyboard == self.keyboard_digits:
cur_button2_text = self.KEYBOARD__DIGITS_BUTTON_TEXT cur_button2_text = self.KEYBOARD__DIGITS_BUTTON_TEXT
elif cur_keyboard == self.keyboard_symbols: elif cur_keyboard == self.keyboard_symbols_1:
cur_button2_text = self.KEYBOARD__SYMBOLS_BUTTON_TEXT cur_button2_text = self.KEYBOARD__SYMBOLS_1_BUTTON_TEXT
elif cur_keyboard == self.keyboard_symbols_2:
cur_button2_text = self.KEYBOARD__SYMBOLS_2_BUTTON_TEXT
if cur_button1_text == self.KEYBOARD__LOWERCASE_BUTTON_TEXT: if cur_button1_text == self.KEYBOARD__LOWERCASE_BUTTON_TEXT:
self.keyboard_abc.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"]) self.keyboard_abc.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"])
@@ -861,10 +893,15 @@ class SeedAddPassphraseScreen(BaseTopNavScreen):
self.keyboard_digits.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"]) self.keyboard_digits.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"])
cur_keyboard = self.keyboard_digits cur_keyboard = self.keyboard_digits
cur_keyboard.render_keys() cur_keyboard.render_keys()
cur_button2_text = self.KEYBOARD__SYMBOLS_BUTTON_TEXT cur_button2_text = self.KEYBOARD__SYMBOLS_1_BUTTON_TEXT
else: elif cur_button2_text == self.KEYBOARD__SYMBOLS_1_BUTTON_TEXT:
self.keyboard_symbols.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"]) self.keyboard_symbols_1.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"])
cur_keyboard = self.keyboard_symbols cur_keyboard = self.keyboard_symbols_1
cur_keyboard.render_keys()
cur_button2_text = self.KEYBOARD__SYMBOLS_2_BUTTON_TEXT
elif cur_button2_text == self.KEYBOARD__SYMBOLS_2_BUTTON_TEXT:
self.keyboard_symbols_2.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"])
cur_keyboard = self.keyboard_symbols_2
cur_keyboard.render_keys() cur_keyboard.render_keys()
cur_button2_text = self.KEYBOARD__DIGITS_BUTTON_TEXT cur_button2_text = self.KEYBOARD__DIGITS_BUTTON_TEXT
cur_keyboard.render_keys() cur_keyboard.render_keys()
@@ -1017,6 +1054,7 @@ class SeedReviewPassphraseScreen(ButtonListScreen):
font_size=font_size, font_size=font_size,
is_text_centered=True, is_text_centered=True,
screen_y=screen_y, screen_y=screen_y,
allow_text_overflow=True
)) ))
screen_y += char_height + 2 screen_y += char_height + 2
@@ -1361,6 +1399,9 @@ class SeedAddressVerificationScreen(ButtonListScreen):
print(f"verified_index: {self.verified_index.cur_count}") print(f"verified_index: {self.verified_index.cur_count}")
if self.verified_index.cur_count is not None: if self.verified_index.cur_count is not None:
print("Screen callback returning success!") print("Screen callback returning success!")
self.threads[-1].stop()
while self.threads[-1].is_alive():
time.sleep(0.01)
return 1 return 1
@@ -1374,10 +1415,12 @@ class SeedAddressVerificationScreen(ButtonListScreen):
def run(self): def run(self):
font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BODY_FONT_SIZE)
while self.keep_running: while self.keep_running:
if self.verified_index.cur_count is not None: if self.verified_index.cur_count is not None:
# Have to trigger a hw_input event to break the Screen out of the wait_for loop # This thread will detect the success state while its parent Screen
# holds in its `wait_for`. Have to trigger a hw_input event to break
# the Screen._run out of the `wait_for` state. The Screen will then
# call its `_run_callback` and detect the success state and exit.
HardwareButtons.get_instance().trigger_override(force_release=True) HardwareButtons.get_instance().trigger_override(force_release=True)
return return
+54 -1
View File
@@ -1,11 +1,14 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any
from PIL.Image import Image from PIL.Image import Image
from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
from seedsigner.hardware.camera import Camera from seedsigner.hardware.camera import Camera
from seedsigner.gui.components import FontAwesomeIconConstants, Fonts, GUIConstants, IconTextLine, SeedSignerCustomIconConstants, TextArea from seedsigner.gui.components import FontAwesomeIconConstants, Fonts, FormattedAddress, GUIConstants, IconTextLine, SeedSignerCustomIconConstants, TextArea
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, KeyboardScreen from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, KeyboardScreen
from seedsigner.hardware.buttons import HardwareButtonsConstants from seedsigner.hardware.buttons import HardwareButtonsConstants
from seedsigner.helpers.qr import QR
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
@@ -366,3 +369,53 @@ class ToolsCalcFinalWordDoneScreen(ButtonListScreen):
is_text_centered=True, is_text_centered=True,
screen_y=self.components[-1].screen_y + self.components[-1].height + 3*GUIConstants.COMPONENT_PADDING, screen_y=self.components[-1].screen_y + self.components[-1].height + 3*GUIConstants.COMPONENT_PADDING,
)) ))
@dataclass
class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen):
fingerprint: str = None
wallet_descriptor_display_name: Any = None
script_type: str = None
custom_derivation_path: str = None
def __post_init__(self):
self.title = "Address Explorer"
self.is_bottom_list = True
super().__post_init__()
if self.fingerprint:
self.components.append(IconTextLine(
icon_name=SeedSignerCustomIconConstants.FINGERPRINT,
icon_color="blue",
label_text="Fingerprint",
value_text=self.fingerprint,
screen_x=GUIConstants.EDGE_PADDING,
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
))
if self.script_type != SettingsConstants.CUSTOM_DERIVATION:
self.components.append(IconTextLine(
icon_name=SeedSignerCustomIconConstants.PATH,
label_text="Derivation",
value_text=SettingsDefinition.get_settings_entry(attr_name=SettingsConstants.SETTING__SCRIPT_TYPES).get_selection_option_display_name_by_value(value=self.script_type),
screen_x=GUIConstants.EDGE_PADDING,
screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING,
))
else:
self.components.append(IconTextLine(
icon_name=SeedSignerCustomIconConstants.PATH,
label_text="Derivation",
value_text=self.custom_derivation_path,
screen_x=GUIConstants.EDGE_PADDING,
screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING,
))
else:
self.components.append(IconTextLine(
label_text="Wallet descriptor",
value_text=self.wallet_descriptor_display_name,
is_text_centered=False,
screen_x=GUIConstants.EDGE_PADDING,
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
))
+4 -4
View File
@@ -13,11 +13,11 @@ class ST7789(object):
self.height = 240 self.height = 240
#Initialize DC RST pin #Initialize DC RST pin
self._dc = 25 self._dc = 22
self._rst = 27 self._rst = 13
self._bl = 24 self._bl = 18
GPIO.setmode(GPIO.BCM) GPIO.setmode(GPIO.BOARD)
GPIO.setwarnings(False) GPIO.setwarnings(False)
GPIO.setup(self._dc,GPIO.OUT) GPIO.setup(self._dc,GPIO.OUT)
GPIO.setup(self._rst,GPIO.OUT) GPIO.setup(self._rst,GPIO.OUT)
+51 -25
View File
@@ -1,20 +1,33 @@
from typing import List
import RPi.GPIO as GPIO import RPi.GPIO as GPIO
import time import time
from seedsigner.models.singleton import Singleton from seedsigner.models.singleton import Singleton
class HardwareButtons(Singleton): class HardwareButtons(Singleton):
KEY_UP_PIN = 6 if GPIO.RPI_INFO['P1_REVISION'] == 3: #This indicates that we have revision 3 GPIO
KEY_DOWN_PIN = 19 print("Detected 40pin GPIO (Rasbperry Pi 2 and above)")
KEY_LEFT_PIN = 5 KEY_UP_PIN = 31
KEY_RIGHT_PIN = 26 KEY_DOWN_PIN = 35
KEY_PRESS_PIN = 13 KEY_LEFT_PIN = 29
KEY_RIGHT_PIN = 37
KEY_PRESS_PIN = 33
KEY1_PIN = 21 KEY1_PIN = 40
KEY2_PIN = 20 KEY2_PIN = 38
KEY3_PIN = 16 KEY3_PIN = 36
else:
print("Assuming 26 Pin GPIO (Raspberry P1 1)")
KEY_UP_PIN = 5
KEY_DOWN_PIN = 11
KEY_LEFT_PIN = 3
KEY_RIGHT_PIN = 15
KEY_PRESS_PIN = 7
KEY1_PIN = 16
KEY2_PIN = 12
KEY3_PIN = 8
@classmethod @classmethod
def get_instance(cls): def get_instance(cls):
@@ -23,7 +36,7 @@ class HardwareButtons(Singleton):
cls._instance = cls.__new__(cls) cls._instance = cls.__new__(cls)
#init GPIO #init GPIO
GPIO.setmode(GPIO.BCM) GPIO.setmode(GPIO.BOARD)
GPIO.setup(HardwareButtons.KEY_UP_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up GPIO.setup(HardwareButtons.KEY_UP_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
GPIO.setup(HardwareButtons.KEY_DOWN_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up GPIO.setup(HardwareButtons.KEY_DOWN_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
GPIO.setup(HardwareButtons.KEY_LEFT_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up GPIO.setup(HardwareButtons.KEY_LEFT_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
@@ -145,10 +158,13 @@ class HardwareButtons(Singleton):
HardwareButtonsConstants.release_lock = True HardwareButtonsConstants.release_lock = True
return True return True
def check_for_low(self, key) -> bool: def check_for_low(self, key: int = None, keys: List[int] = None) -> bool:
if self.GPIO.input(key) == self.GPIO.LOW: if key:
self.update_last_input_time() keys = [key]
return True for key in keys:
if self.GPIO.input(key) == self.GPIO.LOW:
self.update_last_input_time()
return True
else: else:
return False return False
@@ -158,21 +174,31 @@ class HardwareButtons(Singleton):
return True return True
return False return False
# class used as short hand for static button/channel lookup values # class used as short hand for static button/channel lookup values
# TODO: Implement `release_lock` functionality as a global somewhere. Mixes up design # TODO: Implement `release_lock` functionality as a global somewhere. Mixes up design
# patterns to have a static constants class plus a settable global value. # patterns to have a static constants class plus a settable global value.
class HardwareButtonsConstants: class HardwareButtonsConstants:
KEY_UP = 6 if GPIO.RPI_INFO['P1_REVISION'] == 3: #This indicates that we have revision 3 GPIO
KEY_DOWN = 19 KEY_UP = 31
KEY_LEFT = 5 KEY_DOWN = 35
KEY_RIGHT = 26 KEY_LEFT = 29
KEY_PRESS = 13 KEY_RIGHT = 37
KEY1 = 21 KEY_PRESS = 33
KEY2 = 20
KEY3 = 16 KEY1 = 40
KEY2 = 38
KEY3 = 36
else:
KEY_UP = 5
KEY_DOWN = 11
KEY_LEFT = 3
KEY_RIGHT = 15
KEY_PRESS = 7
KEY1 = 16
KEY2 = 12
KEY3 = 8
OVERRIDE = 1000 OVERRIDE = 1000
ALL_KEYS = [ ALL_KEYS = [
+95
View File
@@ -0,0 +1,95 @@
import embit
from embit import bip32
from embit.bip32 import HDKey
from embit.descriptor import Descriptor
from embit.networks import NETWORKS
from embit.networks import NETWORKS
from seedsigner.models.settings_definition import SettingsConstants
"""
Collection of generic embit-powered util methods.
"""
# TODO: PR these directly into `embit`? Or replace with new/existing methods already in `embit`?
def get_standard_derivation_path(network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG, script_type: str = SettingsConstants.NATIVE_SEGWIT) -> str:
if network == SettingsConstants.MAINNET:
network_path = "0'"
elif network == SettingsConstants.TESTNET:
network_path = "1'"
elif network == SettingsConstants.REGTEST:
network_path = "1'"
else:
raise Exception("Unexpected network")
if wallet_type == SettingsConstants.SINGLE_SIG:
if script_type == SettingsConstants.NATIVE_SEGWIT:
return f"m/84'/{network_path}/0'"
elif script_type == SettingsConstants.NESTED_SEGWIT:
return f"m/49'/{network_path}/0'"
elif script_type == SettingsConstants.TAPROOT:
return f"m/86'/{network_path}/0'"
else:
raise Exception("Unexpected script type")
elif wallet_type == SettingsConstants.MULTISIG:
if script_type == SettingsConstants.NATIVE_SEGWIT:
return f"m/48'/{network_path}/0'/2'"
elif script_type == SettingsConstants.NESTED_SEGWIT:
return f"m/48'/{network_path}/0'/1'"
elif script_type == SettingsConstants.TAPROOT:
raise Exception("Taproot multisig/musig not yet supported")
else:
raise Exception("Unexpected script type")
else:
raise Exception("Unexpected wallet type") # checks that all inputs are from the same wallet
def get_xpub(seed_bytes, derivation_path: str, embit_network: str = "main") -> HDKey:
root = bip32.HDKey.from_seed(seed_bytes, version=NETWORKS[embit_network]["xprv"])
xprv = root.derive(derivation_path)
xpub = xprv.to_public()
return xpub
def get_single_sig_address(xpub: HDKey, script_type: str = SettingsConstants.NATIVE_SEGWIT, index: int = 0, is_change: bool = False, embit_network: str = "main") -> str:
if is_change:
pubkey = xpub.derive([1,index]).key
else:
pubkey = xpub.derive([0,index]).key
if script_type == SettingsConstants.NATIVE_SEGWIT:
return embit.script.p2wpkh(pubkey).address(network=NETWORKS[embit_network])
elif script_type == SettingsConstants.NESTED_SEGWIT:
return embit.script.p2sh(embit.script.p2wpkh(pubkey)).address(network=NETWORKS[embit_network])
elif script_type == SettingsConstants.LEGACY_P2PKH:
return embit.script.p2pkh(pubkey).address(network=NETWORKS[embit_network])
elif script_type == SettingsConstants.TAPROOT:
return embit.script.p2tr(pubkey).address(network=NETWORKS[embit_network])
def get_multisig_address(descriptor: Descriptor, index: int = 0, is_change: bool = False, embit_network: str = "main"):
if is_change:
branch_index = 1
else:
branch_index = 0
if descriptor.is_segwit:
# Could be native segwit or nested segwit (descriptor.is_wrapped)
return descriptor.derive(index, branch_index=branch_index).script_pubkey().address(network=NETWORKS[embit_network])
elif descriptor.is_legacy:
# TODO: Not yet implemented!
raise Exception("Legacy P2PKH verification not yet implemented!")
elif descriptor.is_taproot:
# TODO: Not yet implemented!
raise Exception("Taproot verification not yet implemented!")
+25
View File
@@ -37,6 +37,7 @@ class EncodeQR:
qr_type: str = None qr_type: str = None
qr_density: str = SettingsConstants.DENSITY__MEDIUM qr_density: str = SettingsConstants.DENSITY__MEDIUM
wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
bitcoin_address: str = None
def __post_init__(self): def __post_init__(self):
self.qr = QR() self.qr = QR()
@@ -95,6 +96,9 @@ class EncodeQR:
elif self.qr_type == QRType.SEED__COMPACTSEEDQR: elif self.qr_type == QRType.SEED__COMPACTSEEDQR:
self.encoder = CompactSeedQrEncoder(seed_phrase=self.seed_phrase, self.encoder = CompactSeedQrEncoder(seed_phrase=self.seed_phrase,
wordlist_language_code=self.wordlist_language_code) wordlist_language_code=self.wordlist_language_code)
elif self.qr_type == QRType.BITCOIN_ADDRESS:
self.encoder = BitcoinAddressEncoder(address=self.bitcoin_address)
else: else:
raise Exception('QR Type not supported') raise Exception('QR Type not supported')
@@ -318,6 +322,25 @@ class CompactSeedQrEncoder(SeedQrEncoder):
class BitcoinAddressEncoder(BaseQrEncoder):
def __init__(self, address: str):
super().__init__()
self.address = address
def seq_len(self):
return 1
def next_part(self):
return self.address
@property
def is_complete(self):
return True
class XpubQrEncoder(BaseQrEncoder): class XpubQrEncoder(BaseQrEncoder):
def __init__(self, seed_phrase, passphrase, derivation, network, wordlist_language_code): def __init__(self, seed_phrase, passphrase, derivation, network, wordlist_language_code):
self.seed_phrase = seed_phrase self.seed_phrase = seed_phrase
@@ -475,6 +498,8 @@ class UrXpubQrEncoder(XpubQrEncoder):
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey)) ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey))
elif origin.components[3].index == 1: # Nested Multisig elif origin.components[3].index == 1: # Nested Multisig
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey)) ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey))
elif origin.components[0].index == 86: # P2TR
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[409]],self.ur_hdkey))
# If empty, add all script types # If empty, add all script types
if len(ur_outputs) == 0: if len(ur_outputs) == 0:
+48 -44
View File
@@ -139,18 +139,40 @@ class PSBTParser():
if sc.data == self.psbt.tx.vout[i].script_pubkey.data: if sc.data == self.psbt.tx.vout[i].script_pubkey.data:
is_change = True is_change = True
elif "p2tr" in self.policy["type"]:
print("TAPROOT output!")
my_pubkey = None
# should have one or zero derivations for single-key addresses
if len(out.taproot_bip32_derivations.values()) > 0:
# TODO: Support keys in taptree leaves
leaf_hashes, derivation = list(out.taproot_bip32_derivations.values())[0]
der = derivation.derivation
my_pubkey = self.root.derive(der)
sc = script.p2tr(my_pubkey)
if sc.data == self.psbt.tx.vout[i].script_pubkey.data:
is_change = True
if sc.data == self.psbt.tx.vout[i].script_pubkey.data: if sc.data == self.psbt.tx.vout[i].script_pubkey.data:
is_change = True is_change = True
if is_change: if is_change:
addr = self.psbt.tx.vout[i].script_pubkey.address(NETWORKS[SettingsConstants.map_network_to_embit(self.network)]) addr = self.psbt.tx.vout[i].script_pubkey.address(NETWORKS[SettingsConstants.map_network_to_embit(self.network)])
fingerprints = None fingerprints = []
derivation_paths = None derivation_paths = []
# extract info from non-taproot outputs
if len(self.psbt.outputs[i].bip32_derivations) > 0: if len(self.psbt.outputs[i].bip32_derivations) > 0:
fingerprints = []
derivation_paths = []
for d, derivation_path in self.psbt.outputs[i].bip32_derivations.items(): for d, derivation_path in self.psbt.outputs[i].bip32_derivations.items():
fingerprints.append(hexlify(derivation_path.fingerprint).decode()) fingerprints.append(hexlify(derivation_path.fingerprint).decode())
derivation_paths.append(bip32.path_to_str(derivation_path.derivation)) derivation_paths.append(bip32.path_to_str(derivation_path.derivation))
# extract info from taproot outputs
if len(self.psbt.outputs[i].taproot_bip32_derivations) > 0:
for d, (leaf_hashes, derivation) in self.psbt.outputs[i].taproot_bip32_derivations.items():
fingerprints.append(hexlify(derivation.fingerprint).decode())
derivation_paths.append(bip32.path_to_str(derivation.derivation))
self.change_data.append({ self.change_data.append({
"output_index": i, "output_index": i,
"address": addr, "address": addr,
@@ -159,6 +181,7 @@ class PSBTParser():
"derivation_path": derivation_paths, "derivation_path": derivation_paths,
}) })
self.change_amount += self.psbt.tx.vout[i].value self.change_amount += self.psbt.tx.vout[i].value
else: else:
addr = self.psbt.tx.vout[i].script_pubkey.address(NETWORKS[SettingsConstants.map_network_to_embit(self.network)]) addr = self.psbt.tx.vout[i].script_pubkey.address(NETWORKS[SettingsConstants.map_network_to_embit(self.network)])
self.destination_addresses.append(addr) self.destination_addresses.append(addr)
@@ -172,10 +195,13 @@ class PSBTParser():
@staticmethod @staticmethod
def trim(tx): def trim(tx):
trimmed_psbt = psbt.PSBT(tx.tx) trimmed_psbt = psbt.PSBT(tx.tx)
sigsEnd = 0
for i, inp in enumerate(tx.inputs): for i, inp in enumerate(tx.inputs):
sigsEnd += len(list(inp.partial_sigs.keys())) if inp.final_scriptwitness:
trimmed_psbt.inputs[i].partial_sigs = inp.partial_sigs # Taproot sign; leave the input as-is
# TODO: See BIP-371 about fields that can be trimmed
trimmed_psbt.inputs[i] = inp
else:
trimmed_psbt.inputs[i].partial_sigs = inp.partial_sigs
return trimmed_psbt return trimmed_psbt
@@ -184,47 +210,15 @@ class PSBTParser():
def sig_count(tx): def sig_count(tx):
cnt = 0 cnt = 0
for i, inp in enumerate(tx.inputs): for i, inp in enumerate(tx.inputs):
cnt += len(list(inp.partial_sigs.keys())) if inp.final_scriptwitness is not None:
# Taproot sign
cnt += 1
else:
cnt += len(list(inp.partial_sigs.keys()))
return cnt return cnt
@staticmethod
def calc_derivation(network, wallet_type, script_type):
# TODO: Move this to Seed?
if network == SettingsConstants.MAINNET:
network_path = "0'"
elif network == SettingsConstants.TESTNET:
network_path = "1'"
elif network == SettingsConstants.REGTEST:
# TODO: Is this right?
network_path = "1'"
else:
raise Exception("Unexpected network")
if wallet_type == SettingsConstants.SINGLE_SIG:
if script_type == SettingsConstants.NATIVE_SEGWIT:
return f"m/84'/{network_path}/0'"
elif script_type == SettingsConstants.NESTED_SEGWIT:
return f"m/49'/{network_path}/0'"
elif script_type == SettingsConstants.TAPROOT:
return f"m/86'/{network_path}/0'"
else:
raise Exception("Unexpected script type")
elif wallet_type == SettingsConstants.MULTISIG:
if script_type == SettingsConstants.NATIVE_SEGWIT:
return f"m/48'/{network_path}/0'/2'"
elif script_type == SettingsConstants.NESTED_SEGWIT:
return f"m/48'/{network_path}/0'/1'"
elif script_type == SettingsConstants.TAPROOT:
raise Exception("Taproot multisig/musig not yet supported")
else:
raise Exception("Unexpected script type")
else:
raise Exception("Unexpected wallet type") # checks that all inputs are from the same wallet
@staticmethod @staticmethod
def _get_policy(scope, scriptpubkey, xpubs): def _get_policy(scope, scriptpubkey, xpubs):
"""Parse scope and get policy""" """Parse scope and get policy"""
@@ -319,6 +313,12 @@ class PSBTParser():
for input in psbt.inputs: for input in psbt.inputs:
for pub, derivation_path in input.bip32_derivations.items(): for pub, derivation_path in input.bip32_derivations.items():
fingerprints.add(hexlify(derivation_path.fingerprint).decode()) fingerprints.add(hexlify(derivation_path.fingerprint).decode())
for pub, (leaf_hashes, derivation_path) in input.taproot_bip32_derivations.items():
# TODO: Support spends from leaves; depends on support in embit
if len(leaf_hashes) > 0:
raise Exception("Signing keyspends from within a taptree not yet implemented")
fingerprints.add(hexlify(derivation_path.fingerprint).decode())
return list(fingerprints) return list(fingerprints)
@@ -333,6 +333,10 @@ class PSBTParser():
for pub, derivation_path in input.bip32_derivations.items(): for pub, derivation_path in input.bip32_derivations.items():
if seed_fingerprint == hexlify(derivation_path.fingerprint).decode(): if seed_fingerprint == hexlify(derivation_path.fingerprint).decode():
return True return True
for pub, (leaf_hashes, derivation_path) in input.taproot_bip32_derivations.items():
if seed_fingerprint == hexlify(derivation_path.fingerprint).decode():
return True
return False return False
+5 -5
View File
@@ -6,6 +6,7 @@ from embit.networks import NETWORKS
from typing import List from typing import List
from seedsigner.models.settings import SettingsConstants from seedsigner.models.settings import SettingsConstants
from seedsigner.helpers import embit_utils
@@ -106,12 +107,11 @@ class Seed:
root = bip32.HDKey.from_seed(self.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(network)]["xprv"]) root = bip32.HDKey.from_seed(self.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(network)]["xprv"])
return hexlify(root.child(0).fingerprint).decode('utf-8') return hexlify(root.child(0).fingerprint).decode('utf-8')
def get_xpub(self, wallet_path: str = '/', network: str = SettingsConstants.MAINNET): def get_xpub(self, wallet_path: str = '/', network: str = SettingsConstants.MAINNET):
root = bip32.HDKey.from_seed(self.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(network)]["xprv"]) return embit_utils.get_xpub(seed_bytes=self.seed_bytes, derivation_path=wallet_path, embit_network=SettingsConstants.map_network_to_embit(network))
xprv = root.derive(wallet_path)
xpub = xprv.to_public()
return xpub
### override operators ### override operators
def __eq__(self, other): def __eq__(self, other):
if isinstance(other, Seed): if isinstance(other, Seed):
+2 -28
View File
@@ -484,44 +484,18 @@ class PSBTFinalizeView(View):
else: else:
self.controller.psbt = trimmed_psbt self.controller.psbt = trimmed_psbt
return Destination(PSBTSignedQRDisplayView)
if len(self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)) == 1:
return Destination(PSBTSignedQRDisplayView, view_args={"coordinator": self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[0]})
else:
return Destination(PSBTSelectCoordinatorView)
if selected_menu_num == RET_CODE__BACK_BUTTON: if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView) return Destination(BackStackView)
class PSBTSelectCoordinatorView(View):
def run(self):
button_data = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)
selected_menu_num = psbt_screens.PSBTSelectCoordinatorScreen(
button_data=button_data
).display()
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
return Destination(PSBTSignedQRDisplayView, view_args={"coordinator": button_data[selected_menu_num]})
class PSBTSignedQRDisplayView(View): class PSBTSignedQRDisplayView(View):
def __init__(self, coordinator: str):
super().__init__()
self.coordinator = coordinator
def run(self): def run(self):
qr_psbt_type = QRType.PSBT__UR2
if self.coordinator == SettingsConstants.COORDINATOR__SPECTER_DESKTOP:
qr_psbt_type = QRType.PSBT__SPECTER
qr_encoder = EncodeQR( qr_encoder = EncodeQR(
psbt=self.controller.psbt, psbt=self.controller.psbt,
qr_type=qr_psbt_type, qr_type=QRType.PSBT__UR2, # All coordinators (as of 2022-08) use this format
qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY), qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY),
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE), wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE),
) )
+4 -4
View File
@@ -15,13 +15,13 @@ class ScanView(View):
def run(self): def run(self):
from seedsigner.gui.screens.scan_screens import ScanScreen from seedsigner.gui.screens.scan_screens import ScanScreen
# Run the live preview and QR code capture process
# TODO: Does this belong in its own BaseThread?
wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
self.decoder = DecodeQR(wordlist_language_code=wordlist_language_code) self.decoder = DecodeQR(wordlist_language_code=wordlist_language_code)
screen = ScanScreen(decoder=self.decoder)
screen.display()
# Start the live preview and background QR reading
ScanScreen(decoder=self.decoder).display()
# Handle the results
if self.decoder.is_complete: if self.decoder.is_complete:
if self.decoder.is_seed: if self.decoder.is_seed:
seed_mnemonic = self.decoder.get_seed_phrase() seed_mnemonic = self.decoder.get_seed_phrase()
+103 -94
View File
@@ -10,6 +10,7 @@ from typing import List
from seedsigner.controller import Controller from seedsigner.controller import Controller
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustomIconConstants from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustomIconConstants
from seedsigner.helpers import embit_utils
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen,
WarningScreen, DireWarningScreen, seed_screens) WarningScreen, DireWarningScreen, seed_screens)
from seedsigner.gui.screens.screen import LargeIconStatusScreen, LoadingScreenThread, QRDisplayScreen from seedsigner.gui.screens.screen import LargeIconStatusScreen, LoadingScreenThread, QRDisplayScreen
@@ -18,7 +19,7 @@ from seedsigner.models.encode_qr import EncodeQR
from seedsigner.models.psbt_parser import PSBTParser from seedsigner.models.psbt_parser import PSBTParser
from seedsigner.models.qr_type import QRType from seedsigner.models.qr_type import QRType
from seedsigner.models.seed import InvalidSeedException, Seed from seedsigner.models.seed import InvalidSeedException, Seed
from seedsigner.models.settings import SettingsConstants from seedsigner.models.settings import Settings, SettingsConstants
from seedsigner.models.settings_definition import SettingsDefinition from seedsigner.models.settings_definition import SettingsDefinition
from seedsigner.models.threads import BaseThread, ThreadsafeCounter from seedsigner.models.threads import BaseThread, ThreadsafeCounter
from seedsigner.views.psbt_views import PSBTChangeDetailsView from seedsigner.views.psbt_views import PSBTChangeDetailsView
@@ -348,11 +349,18 @@ class SeedOptionsView(View):
REVIEW_PSBT = "Review PSBT" REVIEW_PSBT = "Review PSBT"
VERIFY_ADDRESS = "Verify Addr" VERIFY_ADDRESS = "Verify Addr"
EXPORT_XPUB = "Export Xpub" EXPORT_XPUB = "Export Xpub"
EXPLORER = "Address Explorer"
BACKUP = ("Backup Seed", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT) BACKUP = ("Backup Seed", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)
DISCARD = ("Discard Seed", None, None, "red") DISCARD = ("Discard Seed", None, None, "red")
button_data = [] button_data = []
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
# Jump straight back into the address explorer script type selection flow
# But do ont cancel the `resume_main_flow` as we'll still need that after
# derivation path is specified.
return Destination(SeedExportXpubScriptTypeView, view_args=dict(seed_num=self.seed_num, sig_type=SettingsConstants.SINGLE_SIG), skip_current_view=True)
if self.controller.unverified_address: if self.controller.unverified_address:
if self.controller.resume_main_flow == Controller.FLOW__VERIFY_SINGLESIG_ADDR: if self.controller.resume_main_flow == Controller.FLOW__VERIFY_SINGLESIG_ADDR:
# Jump straight back into the single sig addr verification flow # Jump straight back into the single sig addr verification flow
@@ -362,7 +370,7 @@ class SeedOptionsView(View):
addr = self.controller.unverified_address["address"][:7] addr = self.controller.unverified_address["address"][:7]
VERIFY_ADDRESS += f" {addr}" VERIFY_ADDRESS += f" {addr}"
button_data.append(VERIFY_ADDRESS) button_data.append(VERIFY_ADDRESS)
if self.controller.psbt: if self.controller.psbt:
if PSBTParser.has_matching_input_fingerprint(self.controller.psbt, self.seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)): 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: if self.controller.resume_main_flow and self.controller.resume_main_flow == Controller.FLOW__PSBT:
@@ -381,13 +389,14 @@ class SeedOptionsView(View):
if self.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED: if self.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED:
button_data.append(EXPORT_XPUB) button_data.append(EXPORT_XPUB)
button_data.append(EXPLORER)
button_data.append(BACKUP) button_data.append(BACKUP)
button_data.append(DISCARD) button_data.append(DISCARD)
selected_menu_num = seed_screens.SeedOptionsScreen( selected_menu_num = seed_screens.SeedOptionsScreen(
button_data=button_data, button_data=button_data,
fingerprint=self.seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)), fingerprint=self.seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)),
has_passphrase=self.seed.passphrase is not None has_passphrase=self.seed.passphrase is not None,
).display() ).display()
if selected_menu_num == RET_CODE__BACK_BUTTON: if selected_menu_num == RET_CODE__BACK_BUTTON:
@@ -403,16 +412,20 @@ class SeedOptionsView(View):
return Destination(ScanView) return Destination(ScanView)
elif button_data[selected_menu_num] == VERIFY_ADDRESS: elif button_data[selected_menu_num] == VERIFY_ADDRESS:
return Destination(SeedAddressVerificationView, view_args={"seed_num": self.seed_num}) return Destination(SeedAddressVerificationView, view_args=dict(seed_num=self.seed_num))
elif button_data[selected_menu_num] == EXPORT_XPUB: elif button_data[selected_menu_num] == EXPORT_XPUB:
return Destination(SeedExportXpubSigTypeView, view_args={"seed_num": self.seed_num}) return Destination(SeedExportXpubSigTypeView, view_args=dict(seed_num=self.seed_num))
elif button_data[selected_menu_num] == EXPLORER:
self.controller.resume_main_flow = Controller.FLOW__ADDRESS_EXPLORER
return Destination(SeedExportXpubScriptTypeView, view_args=dict(seed_num=self.seed_num, sig_type=SettingsConstants.SINGLE_SIG))
elif button_data[selected_menu_num] == BACKUP: elif button_data[selected_menu_num] == BACKUP:
return Destination(SeedBackupView, view_args={"seed_num": self.seed_num}) return Destination(SeedBackupView, view_args=dict(seed_num=self.seed_num))
elif button_data[selected_menu_num] == DISCARD: elif button_data[selected_menu_num] == DISCARD:
return Destination(SeedDiscardView, view_args={"seed_num": self.seed_num}) return Destination(SeedDiscardView, view_args=dict(seed_num=self.seed_num))
@@ -487,23 +500,37 @@ class SeedExportXpubScriptTypeView(View):
def run(self): def run(self):
from .tools_views import ToolsAddressExplorerAddressTypeView
args = {"seed_num": self.seed_num, "sig_type": self.sig_type} args = {"seed_num": self.seed_num, "sig_type": self.sig_type}
if len(self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)) == 1: if len(self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)) == 1:
# Nothing to select; skip this screen # Nothing to select; skip this screen
args["script_type"] = self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)[0] args["script_type"] = self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)[0]
return Destination(SeedExportXpubCoordinatorView, view_args=args, skip_current_view=True)
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
del args["sig_type"]
return Destination(ToolsAddressExplorerAddressTypeView, view_args=args, skip_current_view=True)
else:
return Destination(SeedExportXpubCoordinatorView, view_args=args, skip_current_view=True)
button_data = [] button_data = []
for script_type in self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES): for script_type in self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES):
button_data.append(script_type) button_data.append(script_type)
title = "Export Xpub"
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
title = "Address Explorer"
selected_menu_num = ButtonListScreen( selected_menu_num = ButtonListScreen(
title="Export Xpub", title=title,
is_button_text_centered=False, is_button_text_centered=False,
button_data=button_data, button_data=button_data,
is_bottom_list=True,
).display() ).display()
if selected_menu_num < len(button_data): if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
else:
script_types_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES) script_types_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES)
selected_display_name = button_data[selected_menu_num] selected_display_name = button_data[selected_menu_num]
args["script_type"] = script_types_settings_entry.get_selection_option_value_by_display_name(selected_display_name) args["script_type"] = script_types_settings_entry.get_selection_option_value_by_display_name(selected_display_name)
@@ -511,10 +538,11 @@ class SeedExportXpubScriptTypeView(View):
if args["script_type"] == SettingsConstants.CUSTOM_DERIVATION: if args["script_type"] == SettingsConstants.CUSTOM_DERIVATION:
return Destination(SeedExportXpubCustomDerivationView, view_args=args) return Destination(SeedExportXpubCustomDerivationView, view_args=args)
return Destination(SeedExportXpubCoordinatorView, view_args=args) if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
del args["sig_type"]
elif selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(ToolsAddressExplorerAddressTypeView, view_args=args)
return Destination(BackStackView) else:
return Destination(SeedExportXpubCoordinatorView, view_args=args)
@@ -534,9 +562,13 @@ class SeedExportXpubCustomDerivationView(View):
if ret == RET_CODE__BACK_BUTTON: if ret == RET_CODE__BACK_BUTTON:
return Destination(BackStackView) return Destination(BackStackView)
# ret should be the custom derivation path; store it in Settings # ret will be the custom derivation path
custom_derivation = ret custom_derivation = ret
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
from .tools_views import ToolsAddressExplorerAddressTypeView
return Destination(ToolsAddressExplorerAddressTypeView, view_args=dict(seed_num=self.seed_num, script_type=self.script_type, custom_derivation=custom_derivation))
return Destination( return Destination(
SeedExportXpubCoordinatorView, SeedExportXpubCoordinatorView,
view_args={ view_args={
@@ -646,29 +678,39 @@ class SeedExportXpubDetailsView(View):
if self.script_type == SettingsConstants.CUSTOM_DERIVATION: if self.script_type == SettingsConstants.CUSTOM_DERIVATION:
derivation_path = self.custom_derivation derivation_path = self.custom_derivation
else: else:
derivation_path = PSBTParser.calc_derivation( derivation_path = embit_utils.get_standard_derivation_path(
network=self.settings.get_value(SettingsConstants.SETTING__NETWORK), network=self.settings.get_value(SettingsConstants.SETTING__NETWORK),
wallet_type=self.sig_type, wallet_type=self.sig_type,
script_type=self.script_type script_type=self.script_type
) )
if self.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__ENABLED: if self.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__DISABLED:
embit_network = NETWORKS[SettingsConstants.map_network_to_embit(self.settings.get_value(SettingsConstants.SETTING__NETWORK))] # We're just skipping right past this screen
version = embit.bip32.detect_version( selected_menu_num = 0
derivation_path,
default="xpub",
network=embit_network
)
root = embit.bip32.HDKey.from_seed( else:
self.seed.seed_bytes, # The derivation calc takes a few moments. Run the loading screen while we wait.
version=embit_network["xprv"] self.loading_screen = LoadingScreenThread(text="Generating xpub...")
) self.loading_screen.start()
fingerprint = hexlify(root.child(0).fingerprint).decode('utf-8') try:
xprv = root.derive(derivation_path) embit_network = NETWORKS[SettingsConstants.map_network_to_embit(self.settings.get_value(SettingsConstants.SETTING__NETWORK))]
xpub = xprv.to_public() version = embit.bip32.detect_version(
xpub_base58 = xpub.to_string(version=version) derivation_path,
default="xpub",
network=embit_network
)
root = embit.bip32.HDKey.from_seed(
self.seed.seed_bytes,
version=embit_network["xprv"]
)
fingerprint = hexlify(root.child(0).fingerprint).decode('utf-8')
xprv = root.derive(derivation_path)
xpub = xprv.to_public()
xpub_base58 = xpub.to_string(version=version)
finally:
self.loading_screen.stop()
selected_menu_num = seed_screens.SeedExportXpubDetailsScreen( selected_menu_num = seed_screens.SeedExportXpubDetailsScreen(
fingerprint=fingerprint, fingerprint=fingerprint,
@@ -677,18 +719,13 @@ class SeedExportXpubDetailsView(View):
xpub=xpub_base58, xpub=xpub_base58,
).display() ).display()
else:
selected_menu_num = 0
if selected_menu_num == 0: if selected_menu_num == 0:
return Destination( return Destination(
SeedExportXpubQRDisplayView, SeedExportXpubQRDisplayView,
{ dict(seed_num=self.seed_num,
"seed_num": self.seed_num, coordinator=self.coordinator,
"coordinator": self.coordinator, derivation_path=derivation_path,
"derivation_path": derivation_path, )
}
) )
elif selected_menu_num == RET_CODE__BACK_BUTTON: elif selected_menu_num == RET_CODE__BACK_BUTTON:
@@ -1245,13 +1282,14 @@ class AddressVerificationStartView(View):
destination = Destination(SeedSingleSigAddressVerificationSelectSeedView) destination = Destination(SeedSingleSigAddressVerificationSelectSeedView)
elif self.controller.unverified_address["script_type"] == SettingsConstants.TAPROOT: elif self.controller.unverified_address["script_type"] == SettingsConstants.TAPROOT:
destination = Destination(NotYetImplementedView) # TODO: add Taproot support
return Destination(NotYetImplementedView)
elif self.controller.unverified_address["script_type"] == SettingsConstants.LEGACY_P2PKH: elif self.controller.unverified_address["script_type"] == SettingsConstants.LEGACY_P2PKH:
# TODO: detect single sig vs multisig or have to prompt? # TODO: detect single sig vs multisig or have to prompt?
destination = Destination(NotYetImplementedView) return Destination(NotYetImplementedView)
derivation_path = PSBTParser.calc_derivation( derivation_path = embit_utils.get_standard_derivation_path(
network=self.controller.unverified_address["network"], network=self.controller.unverified_address["network"],
wallet_type=sig_type, wallet_type=sig_type,
script_type=self.controller.unverified_address["script_type"] script_type=self.controller.unverified_address["script_type"]
@@ -1295,7 +1333,7 @@ class AddressVerificationSigTypeView(View):
destination = Destination(LoadMultisigWalletDescriptorView) destination = Destination(LoadMultisigWalletDescriptorView)
self.controller.unverified_address["sig_type"] = sig_type self.controller.unverified_address["sig_type"] = sig_type
derivation_path = PSBTParser.calc_derivation( derivation_path = embit_utils.get_standard_derivation_path(
network=self.controller.unverified_address["network"], network=self.controller.unverified_address["network"],
wallet_type=sig_type, wallet_type=sig_type,
script_type=self.controller.unverified_address["script_type"] script_type=self.controller.unverified_address["script_type"]
@@ -1399,7 +1437,7 @@ class SeedAddressVerificationView(View):
return Destination(NotYetImplementedView) return Destination(NotYetImplementedView)
# TODO: This should be in `Seed` or `PSBT` utility class # TODO: This should be in `Seed` or `PSBT` utility class
embit_network = NETWORKS[SettingsConstants.map_network_to_embit(self.network)] embit_network = SettingsConstants.map_network_to_embit(self.network)
# The ThreadsafeCounter will be shared by the brute-force thread to keep track of # The ThreadsafeCounter will be shared by the brute-force thread to keep track of
# its current addr index number and the Screen to display its progress and # its current addr index number and the Screen to display its progress and
@@ -1416,7 +1454,7 @@ class SeedAddressVerificationView(View):
seed=self.seed, seed=self.seed,
descriptor=self.controller.multisig_wallet_descriptor, descriptor=self.controller.multisig_wallet_descriptor,
script_type=self.script_type, script_type=self.script_type,
network=embit_network, embit_network=embit_network,
derivation_path=self.derivation_path, derivation_path=self.derivation_path,
threadsafe_counter=self.threadsafe_counter, threadsafe_counter=self.threadsafe_counter,
verified_index=self.verified_index, verified_index=self.verified_index,
@@ -1481,13 +1519,15 @@ class SeedAddressVerificationView(View):
# Halt the thread if the user gave up (will already be stopped if it verified the # Halt the thread if the user gave up (will already be stopped if it verified the
# target addr). # target addr).
self.addr_verification_thread.stop() self.addr_verification_thread.stop()
while self.addr_verification_thread.is_alive():
time.sleep(0.01)
return Destination(MainMenuView) return Destination(MainMenuView)
class BruteForceAddressVerificationThread(BaseThread): class BruteForceAddressVerificationThread(BaseThread):
def __init__(self, address: str, seed: Seed, descriptor: Descriptor, script_type: str, network: str, derivation_path: str, threadsafe_counter: ThreadsafeCounter, verified_index: ThreadsafeCounter, verified_index_is_change: ThreadsafeCounter): def __init__(self, address: str, seed: Seed, descriptor: Descriptor, script_type: str, embit_network: str, derivation_path: str, threadsafe_counter: ThreadsafeCounter, verified_index: ThreadsafeCounter, verified_index_is_change: ThreadsafeCounter):
""" """
Either seed or descriptor will be None Either seed or descriptor will be None
""" """
@@ -1496,17 +1536,15 @@ class SeedAddressVerificationView(View):
self.seed = seed self.seed = seed
self.descriptor = descriptor self.descriptor = descriptor
self.script_type = script_type self.script_type = script_type
self.network = network self.embit_network = embit_network
self.derivation_path = derivation_path self.derivation_path = derivation_path
self.threadsafe_counter = threadsafe_counter self.threadsafe_counter = threadsafe_counter
self.verified_index = verified_index self.verified_index = verified_index
self.verified_index_is_change = verified_index_is_change self.verified_index_is_change = verified_index_is_change
if self.seed: if self.seed:
root = embit.bip32.HDKey.from_seed(self.seed.seed_bytes, version=network["xprv"]) self.xpub = self.seed.get_xpub(wallet_path=self.derivation_path, network=Settings.get_instance().get_value(SettingsConstants.SETTING__NETWORK))
xprv = root.derive(self.derivation_path)
self.xpub = xprv.to_public()
def run(self): def run(self):
while self.keep_running: while self.keep_running:
@@ -1516,9 +1554,12 @@ class SeedAddressVerificationView(View):
i = self.threadsafe_counter.cur_count i = self.threadsafe_counter.cur_count
if self.descriptor: if self.descriptor:
(receive_address, change_address) = self.derive_multisig(i) receive_address = embit_utils.get_multisig_address(descriptor=self.descriptor, index=i, is_change=False, embit_network=self.embit_network)
change_address = embit_utils.get_multisig_address(descriptor=self.descriptor, index=i, is_change=True, embit_network=self.embit_network)
else: else:
(receive_address, change_address) = self.derive_single_sig(i) receive_address = embit_utils.get_single_sig_address(xpub=self.xpub, script_type=self.script_type, index=i, is_change=False, embit_network=self.embit_network)
change_address = embit_utils.get_single_sig_address(xpub=self.xpub, script_type=self.script_type, index=i, is_change=True, embit_network=self.embit_network)
if self.address == receive_address: if self.address == receive_address:
self.verified_index.set_value(i) self.verified_index.set_value(i)
@@ -1534,47 +1575,8 @@ class SeedAddressVerificationView(View):
# Increment our index counter # Increment our index counter
self.threadsafe_counter.increment() self.threadsafe_counter.increment()
def derive_single_sig(self, index):
r_pubkey = self.xpub.derive([0,index]).key
c_pubkey = self.xpub.derive([1,index]).key
receive_address = ""
change_address = ""
if self.script_type == SettingsConstants.NATIVE_SEGWIT:
receive_address = embit.script.p2wpkh(r_pubkey).address(network=self.network)
change_address = embit.script.p2wpkh(c_pubkey).address(network=self.network)
elif self.script_type == SettingsConstants.NESTED_SEGWIT:
receive_address = embit.script.p2sh(embit.script.p2wpkh(r_pubkey)).address(network=self.network)
change_address = embit.script.p2sh(embit.script.p2wpkh(c_pubkey)).address(network=self.network)
elif self.script_type == SettingsConstants.LEGACY_P2PKH:
receive_address = embit.script.p2pkh(r_pubkey).address(network=self.network)
change_address = embit.script.p2pkh(c_pubkey).address(network=self.network)
elif self.script_type == SettingsConstants.TAPROOT:
# TODO: Not yet implemented!
raise Exception("Taproot verification not yet implemented!")
return (receive_address, change_address)
def derive_multisig(self, index):
if self.script_type in [SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT]:
receive_address = self.descriptor.derive(index, branch_index=0).script_pubkey().address(network=self.network)
change_address = self.descriptor.derive(index, branch_index=1).script_pubkey().address(network=self.network)
elif self.script_type == SettingsConstants.LEGACY_P2PKH:
# TODO: Not yet implemented!
raise Exception("Taproot verification not yet implemented!")
elif self.script_type == SettingsConstants.TAPROOT:
# TODO: Not yet implemented!
raise Exception("Taproot verification not yet implemented!")
return (receive_address, change_address)
class AddressVerificationSuccessView(View): class AddressVerificationSuccessView(View):
def __init__(self, seed_num: int): def __init__(self, seed_num: int):
@@ -1638,6 +1640,7 @@ class MultisigWalletDescriptorView(View):
RETURN = "Return to PSBT" RETURN = "Return to PSBT"
VERIFY = "Verify Addr" VERIFY = "Verify Addr"
EXPLORER = "Address Explorer"
OK = "OK" OK = "OK"
button_data = [OK] button_data = [OK]
@@ -1647,6 +1650,8 @@ class MultisigWalletDescriptorView(View):
elif self.controller.resume_main_flow == Controller.FLOW__VERIFY_MULTISIG_ADDR and self.controller.unverified_address: elif self.controller.resume_main_flow == Controller.FLOW__VERIFY_MULTISIG_ADDR and self.controller.unverified_address:
VERIFY += f""" {self.controller.unverified_address["address"][:7]}""" VERIFY += f""" {self.controller.unverified_address["address"][:7]}"""
button_data = [VERIFY] button_data = [VERIFY]
elif self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
button_data = [EXPLORER]
selected_menu_num = seed_screens.MultisigWalletDescriptorScreen( selected_menu_num = seed_screens.MultisigWalletDescriptorScreen(
policy=policy, policy=policy,
@@ -1665,7 +1670,11 @@ class MultisigWalletDescriptorView(View):
elif button_data[selected_menu_num] == VERIFY: elif button_data[selected_menu_num] == VERIFY:
self.controller.resume_main_flow = None self.controller.resume_main_flow = None
# TODO: Route properly when multisig brute-force addr verification is done
return Destination(SeedAddressVerificationView) return Destination(SeedAddressVerificationView)
elif button_data[selected_menu_num] == EXPLORER:
from seedsigner.views.tools_views import ToolsAddressExplorerAddressTypeView
self.controller.resume_main_flow = None
return Destination(ToolsAddressExplorerAddressTypeView)
return Destination(MainMenuView) return Destination(MainMenuView)
+250 -25
View File
@@ -3,17 +3,22 @@ import os
import time import time
from embit.descriptor import Descriptor
from PIL import Image from PIL import Image
from PIL.ImageOps import autocontrast from PIL.ImageOps import autocontrast
from seedsigner.controller import Controller
from seedsigner.gui.screens.screen import LoadingScreenThread, QRDisplayScreen
from seedsigner.hardware.camera import Camera from seedsigner.hardware.camera import Camera
from seedsigner.gui.components import FontAwesomeIconConstants from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants, SeedSignerCustomIconConstants
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen) from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen)
from seedsigner.gui.screens.tools_screens import ToolsCalcFinalWordFinalizePromptScreen, ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsCalcFinalWordDoneScreen from seedsigner.gui.screens.tools_screens import ToolsCalcFinalWordDoneScreen, ToolsCalcFinalWordFinalizePromptScreen, ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsAddressExplorerAddressTypeScreen
from seedsigner.helpers import mnemonic_generation from seedsigner.helpers import embit_utils, mnemonic_generation
from seedsigner.models.encode_qr import EncodeQR
from seedsigner.models.qr_type import QRType
from seedsigner.models.seed import Seed from seedsigner.models.seed import Seed
from seedsigner.models.settings_definition import SettingsConstants from seedsigner.models.settings_definition import SettingsConstants
from seedsigner.views.seed_views import SeedDiscardView, SeedFinalizeView, SeedMnemonicEntryView, SeedWordsWarningView from seedsigner.views.seed_views import SeedDiscardView, SeedFinalizeView, SeedMnemonicEntryView, SeedWordsWarningView, SeedExportXpubScriptTypeView
from .view import View, Destination, BackStackView from .view import View, Destination, BackStackView
@@ -24,7 +29,8 @@ class ToolsMenuView(View):
IMAGE = (" New seed", FontAwesomeIconConstants.CAMERA) IMAGE = (" New seed", FontAwesomeIconConstants.CAMERA)
DICE = ("New seed", FontAwesomeIconConstants.DICE) DICE = ("New seed", FontAwesomeIconConstants.DICE)
KEYBOARD = ("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD) KEYBOARD = ("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD)
button_data = [IMAGE, DICE, KEYBOARD] EXPLORER = "Address Explorer"
button_data = [IMAGE, DICE, KEYBOARD, EXPLORER]
screen = ButtonListScreen( screen = ButtonListScreen(
title="Tools", title="Tools",
is_button_text_centered=False, is_button_text_centered=False,
@@ -44,8 +50,8 @@ class ToolsMenuView(View):
elif button_data[selected_menu_num] == KEYBOARD: elif button_data[selected_menu_num] == KEYBOARD:
return Destination(ToolsCalcFinalWordNumWordsView) return Destination(ToolsCalcFinalWordNumWordsView)
elif button_data[selected_menu_num] == MICROSD: elif button_data[selected_menu_num] == EXPLORER:
return Destination(ToolsMicroSDStatusView) return Destination(ToolsAddressExplorerSelectSourceView)
"""**************************************************************************** """****************************************************************************
@@ -414,34 +420,253 @@ class ToolsCalcFinalWordDoneView(View):
"""**************************************************************************** """****************************************************************************
Export MicroSD Address Explorer Views
****************************************************************************""" ****************************************************************************"""
class ToolsMicroSDStatusView(View): class ToolsAddressExplorerSelectSourceView(View):
def run(self): def run(self):
from subprocess import call SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE)
from os.path import exists SCAN_DESCRIPTOR = ("Scan wallet descriptor", FontAwesomeIconConstants.QRCODE)
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
button_data = []
file_exists = exists("/mnt/microsd") 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"))
if file_exists: button_data.append(SCAN_SEED)
MICROSD = "Umount MicroSD" button_data.append(SCAN_DESCRIPTOR)
else: button_data.append(TYPE_12WORD)
MICROSD = "Slot empty" button_data.append(TYPE_24WORD)
button_data = [MICROSD]
selected_menu_num = ButtonListScreen( selected_menu_num = ButtonListScreen(
title="MicroSD status", title="Address Explorer",
is_bottom_list=True,
is_button_text_centered=True,
button_data=button_data, button_data=button_data,
is_button_text_centered=False,
is_bottom_list=True,
).display() ).display()
if selected_menu_num == RET_CODE__BACK_BUTTON: if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView) return Destination(BackStackView)
elif button_data[selected_menu_num] == MICROSD: # Most of the options require us to go through a side flow(s) before we can
if file_exists: # continue to the address explorer. Set the Controller-level flow so that it
call("/etc/mdev/./mdev.sh remove", shell=True) # knows to re-route us once the side flow is complete.
return Destination(BackStackView) self.controller.resume_main_flow = Controller.FLOW__ADDRESS_EXPLORER
if len(seeds) > 0 and selected_menu_num < len(seeds):
# User selected one of the n seeds
return Destination(
SeedExportXpubScriptTypeView,
view_args=dict(
seed_num=selected_menu_num,
sig_type=SettingsConstants.SINGLE_SIG,
)
)
elif button_data[selected_menu_num] in [SCAN_SEED, SCAN_DESCRIPTOR]:
from seedsigner.views.scan_views import ScanView
return Destination(ScanView)
elif button_data[selected_menu_num] in [TYPE_12WORD, TYPE_24WORD]:
from seedsigner.views.seed_views import SeedMnemonicEntryView
if button_data[selected_menu_num] == TYPE_12WORD:
self.controller.storage.init_pending_mnemonic(num_words=12)
else: else:
return Destination(BackStackView) self.controller.storage.init_pending_mnemonic(num_words=24)
return Destination(SeedMnemonicEntryView)
class ToolsAddressExplorerAddressTypeView(View):
def __init__(self, seed_num: int = None, script_type: str = None, custom_derivation: str = None):
"""
If the explorer source is a seed, `seed_num` and `script_type` must be
specified. `custom_derivation` can be specified as needed.
If the source is a multisig or single sig wallet descriptor, `seed_num`,
`script_type`, and `custom_derivation` should be `None`.
"""
super().__init__()
self.seed_num = seed_num
self.script_type = script_type
self.custom_derivation = custom_derivation
network = self.settings.get_value(SettingsConstants.SETTING__NETWORK)
# Store everything in the Controller's `address_explorer_data` so we don't have
# to keep passing vals around from View to View and recalculating.
data = dict(
seed_num=seed_num,
network=self.settings.get_value(SettingsConstants.SETTING__NETWORK),
embit_network=SettingsConstants.map_network_to_embit(network),
script_type=script_type,
)
if self.seed_num is not None:
self.seed = self.controller.storage.seeds[seed_num]
data["seed_num"] = self.seed
if self.script_type == SettingsConstants.CUSTOM_DERIVATION:
derivation_path = self.custom_derivation
else:
derivation_path = embit_utils.get_standard_derivation_path(
network=self.settings.get_value(SettingsConstants.SETTING__NETWORK),
wallet_type=SettingsConstants.SINGLE_SIG,
script_type=self.script_type,
)
data["derivation_path"] = derivation_path
data["xpub"] = self.seed.get_xpub(derivation_path, network=network)
else:
data["wallet_descriptor"] = self.controller.multisig_wallet_descriptor
self.controller.address_explorer_data = data
def run(self):
data = self.controller.address_explorer_data
RECEIVE = "Receive Addresses"
CHANGE = "Change Addresses"
button_data = [RECEIVE, CHANGE]
wallet_descriptor_display_name = None
if "wallet_descriptor" in data:
wallet_descriptor_display_name = data["wallet_descriptor"].brief_policy.replace(" (sorted)", "")
script_type = data["script_type"] if "script_type" in data else None
selected_menu_num = ToolsAddressExplorerAddressTypeScreen(
button_data=button_data,
fingerprint=self.seed.get_fingerprint() if self.seed_num is not None else None,
wallet_descriptor_display_name=wallet_descriptor_display_name,
script_type=script_type,
custom_derivation_path=self.custom_derivation,
).display()
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
elif button_data[selected_menu_num] in [RECEIVE, CHANGE]:
return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=button_data[selected_menu_num] == CHANGE))
class ToolsAddressExplorerAddressListView(View):
def __init__(self, is_change: bool = False, start_index: int = 0, selected_button_index: int = 0):
super().__init__()
self.is_change = is_change
self.start_index = start_index
self.selected_button_index = selected_button_index
def run(self):
self.loading_screen = None
try:
addresses = []
button_data = []
data = self.controller.address_explorer_data
addrs_per_screen = 10
addr_storage_key = "receive_addrs"
if self.is_change:
addr_storage_key = "change_addrs"
if addr_storage_key in data and len(data[addr_storage_key]) >= self.start_index + addrs_per_screen:
# We already calculated this range addresses; just retrieve them
addresses = data[addr_storage_key][self.start_index:self.start_index + addrs_per_screen]
else:
self.loading_screen = LoadingScreenThread(text="Calculating addrs...")
self.loading_screen.start()
if addr_storage_key not in data:
data[addr_storage_key] = []
if "xpub" in data:
# Single sig explore from seed
if "script_type" in data and data["script_type"] != SettingsConstants.CUSTOM_DERIVATION:
# Standard derivation path
for i in range(self.start_index, self.start_index + addrs_per_screen):
address = embit_utils.get_single_sig_address(xpub=data["xpub"], script_type=data["script_type"], index=i, is_change=self.is_change, embit_network=data["embit_network"])
addresses.append(address)
data[addr_storage_key].append(address)
else:
# TODO: Custom derivation path
raise Exception("Custom Derivation address explorer not yet implemented")
elif "wallet_descriptor" in data:
descriptor: Descriptor = data["wallet_descriptor"]
if descriptor.is_basic_multisig:
for i in range(self.start_index, self.start_index + addrs_per_screen):
address = embit_utils.get_multisig_address(descriptor=descriptor, index=i, is_change=self.is_change, embit_network=data["embit_network"])
addresses.append(address)
data[addr_storage_key].append(address)
else:
raise Exception("Single sig descriptors not yet supported")
for i, address in enumerate(addresses):
cur_index = i + self.start_index
if cur_index < 10:
end_digits = -6
elif cur_index < 100:
end_digits = -5
else:
end_digits = -4
button_data.append(f"{cur_index}:{address[:8]}...{address[end_digits:]}")
button_data.append(("Next {}".format(addrs_per_screen), None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT))
screen = ButtonListScreen(
title="{} Addrs".format("Receive" if not self.is_change else "Change"),
button_data=button_data,
button_font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
button_font_size=GUIConstants.BUTTON_FONT_SIZE + 4,
is_button_text_centered=False,
is_bottom_list=True,
selected_button=self.selected_button_index,
)
finally:
# Everything is set. Stop the loading screen
if self.loading_screen:
self.loading_screen.stop()
selected_menu_num = screen.display()
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
if selected_menu_num == len(addresses):
# User clicked NEXT
return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=self.is_change, start_index=self.start_index + addrs_per_screen))
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)
class ToolsAddressExplorerAddressView(View):
def __init__(self, index: int, address: str, is_change: bool, start_index: int):
super().__init__()
self.index = index
self.address = address
self.is_change = is_change
self.start_index = start_index
data = self.controller.address_explorer_data
def run(self):
qr_encoder = EncodeQR(qr_type=QRType.BITCOIN_ADDRESS, bitcoin_address=self.address)
QRDisplayScreen(
qr_encoder=qr_encoder,
).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)