mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-06 07:28:24 +00:00
Merge branch 'dev' into initial_multilanguage
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
* [Project Summary](#project-summary)
|
||||
* [Shopping List](#shopping-list)
|
||||
* [Software Installation](#software-installation)
|
||||
* [Verifying Your Software](#verifying-your-software)
|
||||
* [Verifying the Software](#verifying-the-software)
|
||||
* [Enclosure Designs](#enclosure-designs)
|
||||
* [SeedQR Printable Templates](#seedqr-printable-templates)
|
||||
* [Manual Installation Instructions](#manual-installation-instructions)
|
||||
@@ -76,11 +76,19 @@ Notes:
|
||||
---------------
|
||||
|
||||
# Software Installation
|
||||
|
||||
## Special Note on Minimizing Trust
|
||||
As is the nature of pre-packaged software downloads, downloading and using the prepared SeedSigner release images means implicitly placing trust in the individual preparing those images; in our project the release images are prepared and signed by the eponymous creator of the project, SeedSigner "the person". That individual is additionally the only person in possession of the PGP keys that are used to sign the release images.
|
||||
|
||||
However, one of the many advantages of the open source software model is that the need for this kind of trust can be negated by our users' ability to (1) review the project's source code and (2) assemble the operating image necessary to use the software themselves. From our project's inception, instructions to build a SeedSigner operating image (using precisely the same process that is used to create the prepared release images) have been made availabile. We have put a lot of thought and work into making these instructions easy to understand and follow, even for less technical users. These instructions can be found [here](docs/manual_installation.md).
|
||||
|
||||
## Downloading the Software
|
||||
|
||||
The quickest and easiest way to install the software is to download the most recent "seedsigner_X_X_X.zip" file in the [software releases](https://github.com/SeedSigner/seedsigner/releases) section of this repository.
|
||||
|
||||
After downloading the .zip file, extract the seedsigner .img file, and write it to a MicroSD card (at least 4GB in size or larger). Then install the MicroSD in the assembled hardware and off you go. If your goal is a more trustless installation, you can follow the [manual installation instructions](docs/manual_installation.md).
|
||||
After downloading the .zip file, extract the seedsigner .img file, and write it to a MicroSD card (at least 4GB in size or larger). Then install the MicroSD in the assembled hardware and off you go.
|
||||
|
||||
## Verifying Your Software
|
||||
## Verifying the Software
|
||||
You can verify the data integrity and authenticity of the latest release with as little as three commands. This process assumes that you know [how to navigate on a terminal](https://terminalcheatsheet.com/guides/navigate-terminal) and have navigated to the folder where you have these four relevant files present: (This will most likely be your Downloads folder.)
|
||||
|
||||
* seedsigner_pubkey.gpg (from the main folder of this repo)
|
||||
|
||||
@@ -480,3 +480,72 @@ b'\n\xcb\xba\x00\x8d\x9b\xa0\x05\xf5\x99k@\xa3G\\\xd9'
|
||||
</tr>
|
||||
</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
@@ -1,5 +1,5 @@
|
||||
Babel==2.10.1
|
||||
embit @ git+https://github.com/seedsigner/embit.git@03cba13e0add64b241972c67e2aa970cb2e3a5d2
|
||||
embit==0.6.1
|
||||
numpy==1.21.1
|
||||
picamera==1.13
|
||||
Pillow==9.1.0
|
||||
|
||||
@@ -70,6 +70,8 @@ class Controller(Singleton):
|
||||
|
||||
image_entropy_preview_frames: List[Image] = None
|
||||
image_entropy_final_image: Image = None
|
||||
|
||||
address_explorer_data: dict = None
|
||||
# TODO: end refactor section
|
||||
|
||||
# Destination placeholder for when we need to jump out to a side flow but intend to
|
||||
@@ -78,6 +80,7 @@ class Controller(Singleton):
|
||||
FLOW__PSBT = "psbt"
|
||||
FLOW__VERIFY_MULTISIG_ADDR = "multisig_addr"
|
||||
FLOW__VERIFY_SINGLESIG_ADDR = "singlesig_addr"
|
||||
FLOW__ADDRESS_EXPLORER = "address_explorer"
|
||||
resume_main_flow: str = None
|
||||
|
||||
back_stack: BackStack = None
|
||||
@@ -202,9 +205,9 @@ class Controller(Singleton):
|
||||
def run(self, some_arg, other_arg):
|
||||
print(other_arg)
|
||||
|
||||
class OtherView():
|
||||
class OtherView(View):
|
||||
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:
|
||||
|
||||
@@ -229,6 +232,7 @@ class Controller(Singleton):
|
||||
self.resume_main_flow = None
|
||||
self.multisig_wallet_descriptor = None
|
||||
self.unverified_address = None
|
||||
self.address_explorer_data = None
|
||||
|
||||
print(f"back_stack: {self.back_stack}")
|
||||
|
||||
|
||||
@@ -6,10 +6,9 @@ from PIL import Image, ImageDraw, ImageColor
|
||||
from typing import Any, List, Tuple
|
||||
from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
from seedsigner.hardware.buttons import HardwareButtons, HardwareButtonsConstants
|
||||
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
from seedsigner.hardware.buttons import HardwareButtonsConstants, HardwareButtons
|
||||
from seedsigner.models.threads import BaseThread
|
||||
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
|
||||
from seedsigner.models.encode_qr import EncodeQR
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
|
||||
@@ -682,41 +681,73 @@ class LargeButtonScreen(BaseTopNavScreen):
|
||||
class QRDisplayScreen(BaseScreen):
|
||||
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):
|
||||
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:
|
||||
ret = self._run_callback()
|
||||
if ret is not None:
|
||||
return ret
|
||||
|
||||
# convert the cur_brightness integer (31-255) into hex triplets
|
||||
hex_color = (hex(cur_brightness).split('x')[1]) * 3
|
||||
image = self.qr_encoder.next_part_image(240,240, border=2, background_color=hex_color)
|
||||
self.renderer.show_image(image)
|
||||
|
||||
# Target n held frames per second before rendering next QR image
|
||||
time.sleep(5/30.0)
|
||||
|
||||
if self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_DOWN):
|
||||
user_input = self.hw_inputs.wait_for(
|
||||
[
|
||||
HardwareButtonsConstants.KEY_UP,
|
||||
HardwareButtonsConstants.KEY_DOWN,
|
||||
HardwareButtonsConstants.KEY_LEFT,
|
||||
HardwareButtonsConstants.KEY_RIGHT,
|
||||
] + HardwareButtonsConstants.KEYS__ANYCLICK,
|
||||
check_release=True,
|
||||
release_keys=HardwareButtonsConstants.KEYS__ANYCLICK
|
||||
)
|
||||
if user_input == HardwareButtonsConstants.KEY_DOWN:
|
||||
# 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
|
||||
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
|
||||
|
||||
|
||||
settings.set_value(SettingsConstants.SETTING__QR_BRIGHTNESS, cur_brightness)
|
||||
|
||||
# TODO: handle left as BACK
|
||||
Settings.get_instance().set_value(SettingsConstants.SETTING__QR_BRIGHTNESS, self.qr_brightness.cur_count)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -434,7 +434,6 @@ class SeedFinalizeScreen(ButtonListScreen):
|
||||
@dataclass
|
||||
class SeedOptionsScreen(ButtonListScreen):
|
||||
# Customize defaults
|
||||
is_bottom_list: bool = True
|
||||
fingerprint: str = None
|
||||
has_passphrase: bool = False
|
||||
|
||||
@@ -443,6 +442,8 @@ class SeedOptionsScreen(ButtonListScreen):
|
||||
self.top_nav_icon_color = "blue"
|
||||
self.title = self.fingerprint
|
||||
self.is_button_text_centered = False
|
||||
self.is_bottom_list = True
|
||||
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
@@ -1415,6 +1416,9 @@ class SeedAddressVerificationScreen(ButtonListScreen):
|
||||
print(f"verified_index: {self.verified_index.cur_count}")
|
||||
if self.verified_index.cur_count is not None:
|
||||
print("Screen callback returning success!")
|
||||
self.threads[-1].stop()
|
||||
while self.threads[-1].is_alive():
|
||||
time.sleep(0.01)
|
||||
return 1
|
||||
|
||||
|
||||
@@ -1428,10 +1432,12 @@ class SeedAddressVerificationScreen(ButtonListScreen):
|
||||
|
||||
|
||||
def run(self):
|
||||
font = Fonts.get_font(GUIConstants.get_body_font_name(), GUIConstants.get_body_font_size())
|
||||
while self.keep_running:
|
||||
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)
|
||||
return
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
from dataclasses import dataclass
|
||||
from gettext import gettext as _
|
||||
from typing import Any
|
||||
from PIL.Image import Image
|
||||
|
||||
from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
|
||||
from seedsigner.hardware.camera import Camera
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, Fonts, 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, ButtonListScreen, KeyboardScreen
|
||||
from seedsigner.hardware.buttons import HardwareButtonsConstants
|
||||
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
|
||||
|
||||
|
||||
|
||||
@@ -370,3 +371,53 @@ class ToolsCalcFinalWordDoneScreen(ButtonListScreen):
|
||||
is_text_centered=True,
|
||||
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,
|
||||
))
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import List
|
||||
import RPi.GPIO as GPIO
|
||||
import time
|
||||
|
||||
from seedsigner.models.singleton import Singleton
|
||||
@@ -17,7 +19,6 @@ class HardwareButtons(Singleton):
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
import RPi.GPIO as GPIO
|
||||
# This is the only way to access the one and only instance
|
||||
if cls._instance is None:
|
||||
cls._instance = cls.__new__(cls)
|
||||
@@ -59,7 +60,6 @@ class HardwareButtons(Singleton):
|
||||
|
||||
def wait_for(self, keys=[], check_release=True, release_keys=[]) -> int:
|
||||
# TODO: Refactor to keep control in the Controller and not here
|
||||
import RPi.GPIO as GPIO
|
||||
from seedsigner.controller import Controller
|
||||
controller = Controller.get_instance()
|
||||
|
||||
@@ -134,7 +134,6 @@ class HardwareButtons(Singleton):
|
||||
|
||||
|
||||
def add_events(self, keys=[]):
|
||||
import RPi.GPIO as GPIO
|
||||
for key in keys:
|
||||
GPIO.add_event_detect(key, self.GPIO.RISING, callback=HardwareButtons.rising_callback)
|
||||
|
||||
@@ -156,16 +155,17 @@ class HardwareButtons(Singleton):
|
||||
HardwareButtonsConstants.release_lock = True
|
||||
return True
|
||||
|
||||
def check_for_low(self, key) -> bool:
|
||||
import RPi.GPIO as GPIO
|
||||
if self.GPIO.input(key) == self.GPIO.LOW:
|
||||
self.update_last_input_time()
|
||||
return True
|
||||
def check_for_low(self, key: int = None, keys: List[int] = None) -> bool:
|
||||
if key:
|
||||
keys = [key]
|
||||
for key in keys:
|
||||
if self.GPIO.input(key) == self.GPIO.LOW:
|
||||
self.update_last_input_time()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def has_any_input(self) -> bool:
|
||||
import RPi.GPIO as GPIO
|
||||
for key in HardwareButtonsConstants.ALL_KEYS:
|
||||
if self.GPIO.input(key) == GPIO.LOW:
|
||||
return True
|
||||
|
||||
@@ -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!")
|
||||
@@ -37,6 +37,7 @@ class EncodeQR:
|
||||
qr_type: str = None
|
||||
qr_density: str = SettingsConstants.DENSITY__MEDIUM
|
||||
wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
|
||||
bitcoin_address: str = None
|
||||
|
||||
def __post_init__(self):
|
||||
self.qr = QR()
|
||||
@@ -95,6 +96,9 @@ class EncodeQR:
|
||||
elif self.qr_type == QRType.SEED__COMPACTSEEDQR:
|
||||
self.encoder = CompactSeedQrEncoder(seed_phrase=self.seed_phrase,
|
||||
wordlist_language_code=self.wordlist_language_code)
|
||||
|
||||
elif self.qr_type == QRType.BITCOIN_ADDRESS:
|
||||
self.encoder = BitcoinAddressEncoder(address=self.bitcoin_address)
|
||||
|
||||
else:
|
||||
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):
|
||||
def __init__(self, seed_phrase, passphrase, derivation, network, wordlist_language_code):
|
||||
self.seed_phrase = seed_phrase
|
||||
|
||||
@@ -189,42 +189,6 @@ class PSBTParser():
|
||||
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
|
||||
def _get_policy(scope, scriptpubkey, xpubs):
|
||||
"""Parse scope and get policy"""
|
||||
|
||||
@@ -6,6 +6,7 @@ from embit.networks import NETWORKS
|
||||
from typing import List
|
||||
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
from seedsigner.helpers import embit_utils
|
||||
|
||||
|
||||
|
||||
@@ -111,12 +112,11 @@ class Seed:
|
||||
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')
|
||||
|
||||
|
||||
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"])
|
||||
xprv = root.derive(wallet_path)
|
||||
xpub = xprv.to_public()
|
||||
return xpub
|
||||
|
||||
return embit_utils.get_xpub(seed_bytes=self.seed_bytes, derivation_path=wallet_path, embit_network=SettingsConstants.map_network_to_embit(network))
|
||||
|
||||
|
||||
### override operators
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, Seed):
|
||||
|
||||
@@ -480,15 +480,9 @@ class PSBTFinalizeView(View):
|
||||
|
||||
if selected_menu_num == 0:
|
||||
# Sign PSBT
|
||||
loading_screen = LoadingScreenThread(text=_("Signing PSBT..."))
|
||||
loading_screen.start()
|
||||
|
||||
try:
|
||||
sig_cnt = PSBTParser.sig_count(psbt)
|
||||
psbt.sign_with(psbt_parser.root)
|
||||
trimmed_psbt = PSBTParser.trim(psbt)
|
||||
finally:
|
||||
loading_screen.stop()
|
||||
sig_cnt = PSBTParser.sig_count(psbt)
|
||||
psbt.sign_with(psbt_parser.root)
|
||||
trimmed_psbt = PSBTParser.trim(psbt)
|
||||
|
||||
if sig_cnt == PSBTParser.sig_count(trimmed_psbt):
|
||||
# Signing failed / didn't do anything
|
||||
|
||||
@@ -16,13 +16,13 @@ class ScanView(View):
|
||||
def run(self):
|
||||
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)
|
||||
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_seed:
|
||||
seed_mnemonic = self.decoder.get_seed_phrase()
|
||||
|
||||
+106
-100
@@ -11,6 +11,7 @@ from typing import List
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustomIconConstants
|
||||
from seedsigner.helpers import embit_utils
|
||||
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen,
|
||||
WarningScreen, DireWarningScreen, seed_screens)
|
||||
from seedsigner.gui.screens.screen import LargeIconStatusScreen, LoadingScreenThread, QRDisplayScreen
|
||||
@@ -19,7 +20,7 @@ from seedsigner.models.encode_qr import EncodeQR
|
||||
from seedsigner.models.psbt_parser import PSBTParser
|
||||
from seedsigner.models.qr_type import QRType
|
||||
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.threads import BaseThread, ThreadsafeCounter
|
||||
from seedsigner.views.main_menu_views import MainMenuView
|
||||
@@ -380,15 +381,22 @@ class SeedOptionsView(View):
|
||||
def run(self):
|
||||
from seedsigner.views.psbt_views import PSBTOverviewView
|
||||
|
||||
SCAN_PSBT = (_("Scan PSBT"), FontAwesomeIconConstants.QRCODE)
|
||||
REVIEW_PSBT = _("Review PSBT")
|
||||
VERIFY_ADDRESS = _("Verify Addr")
|
||||
EXPORT_XPUB = _("Export Xpub")
|
||||
BACKUP = (_("Backup Seed"), None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)
|
||||
DISCARD = (_("Discard Seed"), None, None, "red")
|
||||
SCAN_PSBT = ("Scan PSBT", FontAwesomeIconConstants.QRCODE)
|
||||
REVIEW_PSBT = "Review PSBT"
|
||||
VERIFY_ADDRESS = "Verify Addr"
|
||||
EXPORT_XPUB = "Export Xpub"
|
||||
EXPLORER = "Address Explorer"
|
||||
BACKUP = ("Backup Seed", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)
|
||||
DISCARD = ("Discard Seed", None, None, "red")
|
||||
|
||||
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.resume_main_flow == Controller.FLOW__VERIFY_SINGLESIG_ADDR:
|
||||
# Jump straight back into the single sig addr verification flow
|
||||
@@ -398,7 +406,7 @@ class SeedOptionsView(View):
|
||||
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:
|
||||
@@ -417,13 +425,14 @@ class SeedOptionsView(View):
|
||||
if self.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED:
|
||||
button_data.append(EXPORT_XPUB)
|
||||
|
||||
button_data.append(EXPLORER)
|
||||
button_data.append(BACKUP)
|
||||
button_data.append(DISCARD)
|
||||
|
||||
selected_menu_num = seed_screens.SeedOptionsScreen(
|
||||
button_data=button_data,
|
||||
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()
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
@@ -439,16 +448,20 @@ class SeedOptionsView(View):
|
||||
return Destination(ScanView)
|
||||
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
return Destination(SeedDiscardView, view_args={"seed_num": self.seed_num})
|
||||
return Destination(SeedDiscardView, view_args=dict(seed_num=self.seed_num))
|
||||
|
||||
|
||||
|
||||
@@ -522,23 +535,37 @@ class SeedExportXpubScriptTypeView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
from .tools_views import ToolsAddressExplorerAddressTypeView
|
||||
args = {"seed_num": self.seed_num, "sig_type": self.sig_type}
|
||||
if len(self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)) == 1:
|
||||
# Nothing to select; skip this screen
|
||||
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 = []
|
||||
for script_type in self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES):
|
||||
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(
|
||||
title=_("Export Xpub"),
|
||||
title=title,
|
||||
is_button_text_centered=False,
|
||||
button_data=button_data,
|
||||
is_bottom_list=True,
|
||||
).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)
|
||||
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)
|
||||
@@ -546,10 +573,11 @@ class SeedExportXpubScriptTypeView(View):
|
||||
if args["script_type"] == SettingsConstants.CUSTOM_DERIVATION:
|
||||
return Destination(SeedExportXpubCustomDerivationView, view_args=args)
|
||||
|
||||
return Destination(SeedExportXpubCoordinatorView, view_args=args)
|
||||
|
||||
elif selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
|
||||
del args["sig_type"]
|
||||
return Destination(ToolsAddressExplorerAddressTypeView, view_args=args)
|
||||
else:
|
||||
return Destination(SeedExportXpubCoordinatorView, view_args=args)
|
||||
|
||||
|
||||
|
||||
@@ -569,9 +597,13 @@ class SeedExportXpubCustomDerivationView(View):
|
||||
if ret == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
# ret should be the custom derivation path; store it in Settings
|
||||
# ret will be the custom derivation path
|
||||
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(
|
||||
SeedExportXpubCoordinatorView,
|
||||
view_args={
|
||||
@@ -681,29 +713,39 @@ class SeedExportXpubDetailsView(View):
|
||||
if self.script_type == SettingsConstants.CUSTOM_DERIVATION:
|
||||
derivation_path = self.custom_derivation
|
||||
else:
|
||||
derivation_path = PSBTParser.calc_derivation(
|
||||
derivation_path = embit_utils.get_standard_derivation_path(
|
||||
network=self.settings.get_value(SettingsConstants.SETTING__NETWORK),
|
||||
wallet_type=self.sig_type,
|
||||
script_type=self.script_type
|
||||
)
|
||||
|
||||
if self.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__ENABLED:
|
||||
embit_network = NETWORKS[SettingsConstants.map_network_to_embit(self.settings.get_value(SettingsConstants.SETTING__NETWORK))]
|
||||
version = embit.bip32.detect_version(
|
||||
derivation_path,
|
||||
default="xpub",
|
||||
network=embit_network
|
||||
)
|
||||
if self.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__DISABLED:
|
||||
# We're just skipping right past this screen
|
||||
selected_menu_num = 0
|
||||
|
||||
root = embit.bip32.HDKey.from_seed(
|
||||
self.seed.seed_bytes,
|
||||
version=embit_network["xprv"]
|
||||
)
|
||||
else:
|
||||
# The derivation calc takes a few moments. Run the loading screen while we wait.
|
||||
self.loading_screen = LoadingScreenThread(text="Generating xpub...")
|
||||
self.loading_screen.start()
|
||||
|
||||
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)
|
||||
try:
|
||||
embit_network = NETWORKS[SettingsConstants.map_network_to_embit(self.settings.get_value(SettingsConstants.SETTING__NETWORK))]
|
||||
version = embit.bip32.detect_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(
|
||||
fingerprint=fingerprint,
|
||||
@@ -712,17 +754,13 @@ class SeedExportXpubDetailsView(View):
|
||||
xpub=xpub_base58,
|
||||
).display()
|
||||
|
||||
else:
|
||||
selected_menu_num = 0
|
||||
|
||||
if selected_menu_num == 0:
|
||||
return Destination(
|
||||
SeedExportXpubQRDisplayView,
|
||||
{
|
||||
"seed_num": self.seed_num,
|
||||
"coordinator": self.coordinator,
|
||||
"derivation_path": derivation_path,
|
||||
}
|
||||
dict(seed_num=self.seed_num,
|
||||
coordinator=self.coordinator,
|
||||
derivation_path=derivation_path,
|
||||
)
|
||||
)
|
||||
|
||||
elif selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
@@ -1290,7 +1328,7 @@ class AddressVerificationStartView(View):
|
||||
# TODO: detect single sig vs multisig or have to prompt?
|
||||
destination = Destination(NotYetImplementedView)
|
||||
|
||||
derivation_path = PSBTParser.calc_derivation(
|
||||
derivation_path = embit_utils.get_standard_derivation_path(
|
||||
network=self.controller.unverified_address["network"],
|
||||
wallet_type=sig_type,
|
||||
script_type=self.controller.unverified_address["script_type"]
|
||||
@@ -1334,7 +1372,7 @@ class AddressVerificationSigTypeView(View):
|
||||
destination = Destination(LoadMultisigWalletDescriptorView)
|
||||
|
||||
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"],
|
||||
wallet_type=sig_type,
|
||||
script_type=self.controller.unverified_address["script_type"]
|
||||
@@ -1438,7 +1476,7 @@ class SeedAddressVerificationView(View):
|
||||
return Destination(NotYetImplementedView)
|
||||
|
||||
# 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
|
||||
# its current addr index number and the Screen to display its progress and
|
||||
@@ -1455,7 +1493,7 @@ class SeedAddressVerificationView(View):
|
||||
seed=self.seed,
|
||||
descriptor=self.controller.multisig_wallet_descriptor,
|
||||
script_type=self.script_type,
|
||||
network=embit_network,
|
||||
embit_network=embit_network,
|
||||
derivation_path=self.derivation_path,
|
||||
threadsafe_counter=self.threadsafe_counter,
|
||||
verified_index=self.verified_index,
|
||||
@@ -1521,13 +1559,15 @@ class SeedAddressVerificationView(View):
|
||||
# Halt the thread if the user gave up (will already be stopped if it verified the
|
||||
# target addr).
|
||||
self.addr_verification_thread.stop()
|
||||
while self.addr_verification_thread.is_alive():
|
||||
time.sleep(0.01)
|
||||
|
||||
return Destination(MainMenuView)
|
||||
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
@@ -1536,17 +1576,15 @@ class SeedAddressVerificationView(View):
|
||||
self.seed = seed
|
||||
self.descriptor = descriptor
|
||||
self.script_type = script_type
|
||||
self.network = network
|
||||
self.embit_network = embit_network
|
||||
self.derivation_path = derivation_path
|
||||
self.threadsafe_counter = threadsafe_counter
|
||||
self.verified_index = verified_index
|
||||
self.verified_index_is_change = verified_index_is_change
|
||||
|
||||
if self.seed:
|
||||
root = embit.bip32.HDKey.from_seed(self.seed.seed_bytes, version=network["xprv"])
|
||||
xprv = root.derive(self.derivation_path)
|
||||
self.xpub = xprv.to_public()
|
||||
|
||||
self.xpub = self.seed.get_xpub(wallet_path=self.derivation_path, network=Settings.get_instance().get_value(SettingsConstants.SETTING__NETWORK))
|
||||
|
||||
|
||||
def run(self):
|
||||
while self.keep_running:
|
||||
@@ -1556,9 +1594,12 @@ class SeedAddressVerificationView(View):
|
||||
i = self.threadsafe_counter.cur_count
|
||||
|
||||
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:
|
||||
(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:
|
||||
self.verified_index.set_value(i)
|
||||
@@ -1574,50 +1615,8 @@ class SeedAddressVerificationView(View):
|
||||
|
||||
# Increment our index counter
|
||||
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(_("Legacy P2PKH verification not 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):
|
||||
def __init__(self, seed_num: int):
|
||||
@@ -1681,6 +1680,7 @@ class MultisigWalletDescriptorView(View):
|
||||
|
||||
RETURN = _("Return to PSBT")
|
||||
VERIFY = _("Verify Addr")
|
||||
EXPLORER = _("Address Explorer")
|
||||
OK = _("OK")
|
||||
|
||||
button_data = [OK]
|
||||
@@ -1690,6 +1690,8 @@ class MultisigWalletDescriptorView(View):
|
||||
elif self.controller.resume_main_flow == Controller.FLOW__VERIFY_MULTISIG_ADDR and self.controller.unverified_address:
|
||||
VERIFY += f""" {self.controller.unverified_address["address"][:7]}"""
|
||||
button_data = [VERIFY]
|
||||
elif self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
|
||||
button_data = [EXPLORER]
|
||||
|
||||
selected_menu_num = seed_screens.MultisigWalletDescriptorScreen(
|
||||
policy=policy,
|
||||
@@ -1708,7 +1710,11 @@ class MultisigWalletDescriptorView(View):
|
||||
|
||||
elif button_data[selected_menu_num] == VERIFY:
|
||||
self.controller.resume_main_flow = None
|
||||
# TODO: Route properly when multisig brute-force addr verification is done
|
||||
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)
|
||||
|
||||
@@ -3,17 +3,22 @@ import os
|
||||
import time
|
||||
|
||||
from gettext import gettext as _
|
||||
from embit.descriptor import Descriptor
|
||||
from PIL import Image
|
||||
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.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.tools_screens import ToolsCalcFinalWordFinalizePromptScreen, ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsCalcFinalWordDoneScreen
|
||||
from seedsigner.helpers import mnemonic_generation
|
||||
from seedsigner.gui.screens.tools_screens import ToolsCalcFinalWordDoneScreen, ToolsCalcFinalWordFinalizePromptScreen, ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsAddressExplorerAddressTypeScreen
|
||||
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.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
|
||||
|
||||
@@ -24,7 +29,8 @@ class ToolsMenuView(View):
|
||||
IMAGE = (_(" New seed"), FontAwesomeIconConstants.CAMERA)
|
||||
DICE = (_("New seed"), FontAwesomeIconConstants.DICE)
|
||||
KEYBOARD = (_("Calc 12th/24th word"), FontAwesomeIconConstants.KEYBOARD)
|
||||
button_data = [IMAGE, DICE, KEYBOARD]
|
||||
EXPLORER = _("Address Explorer")
|
||||
button_data = [IMAGE, DICE, KEYBOARD, EXPLORER]
|
||||
screen = ButtonListScreen(
|
||||
title=_("Tools"),
|
||||
is_button_text_centered=False,
|
||||
@@ -44,6 +50,10 @@ class ToolsMenuView(View):
|
||||
elif button_data[selected_menu_num] == KEYBOARD:
|
||||
return Destination(ToolsCalcFinalWordNumWordsView)
|
||||
|
||||
elif button_data[selected_menu_num] == EXPLORER:
|
||||
return Destination(ToolsAddressExplorerSelectSourceView)
|
||||
|
||||
|
||||
|
||||
|
||||
"""****************************************************************************
|
||||
@@ -410,3 +420,256 @@ class ToolsCalcFinalWordDoneView(View):
|
||||
elif button_data[selected_menu_num] == DISCARD:
|
||||
return Destination(SeedDiscardView)
|
||||
|
||||
|
||||
|
||||
"""****************************************************************************
|
||||
Address Explorer Views
|
||||
****************************************************************************"""
|
||||
class ToolsAddressExplorerSelectSourceView(View):
|
||||
def run(self):
|
||||
SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE)
|
||||
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 = []
|
||||
|
||||
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)
|
||||
button_data.append(SCAN_DESCRIPTOR)
|
||||
button_data.append(TYPE_12WORD)
|
||||
button_data.append(TYPE_24WORD)
|
||||
|
||||
selected_menu_num = ButtonListScreen(
|
||||
title="Address Explorer",
|
||||
button_data=button_data,
|
||||
is_button_text_centered=False,
|
||||
is_bottom_list=True,
|
||||
).display()
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
# Most of the options require us to go through a side flow(s) before we can
|
||||
# continue to the address explorer. Set the Controller-level flow so that it
|
||||
# knows to re-route us once the side flow is complete.
|
||||
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:
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user