mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
Merge branch 'dev' into dev
This commit is contained in:
@@ -5,3 +5,4 @@ src/seedsigner.egg-info/
|
||||
.vscode
|
||||
src/seedsigner/models/settings_definition.json
|
||||
.idea
|
||||
*.mo
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 |
@@ -69,6 +69,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
|
||||
@@ -77,6 +79,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
|
||||
@@ -197,9 +200,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:
|
||||
|
||||
@@ -224,6 +227,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}")
|
||||
|
||||
@@ -2,6 +2,7 @@ from dataclasses import dataclass
|
||||
from lzma import is_check_supported
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
from typing import List
|
||||
import time
|
||||
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
from seedsigner.models.threads import BaseThread
|
||||
@@ -439,7 +440,7 @@ class PSBTOverviewScreen(ButtonListScreen):
|
||||
self.renderer.show_image()
|
||||
|
||||
# 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",
|
||||
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,
|
||||
))
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Any, List, Tuple
|
||||
from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
|
||||
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.settings import Settings, SettingsConstants
|
||||
|
||||
@@ -661,41 +661,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)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -436,7 +436,6 @@ class SeedFinalizeScreen(ButtonListScreen):
|
||||
@dataclass
|
||||
class SeedOptionsScreen(ButtonListScreen):
|
||||
# Customize defaults
|
||||
is_bottom_list: bool = True
|
||||
fingerprint: str = None
|
||||
has_passphrase: bool = False
|
||||
|
||||
@@ -445,6 +444,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__()
|
||||
|
||||
|
||||
@@ -1361,6 +1362,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
|
||||
|
||||
|
||||
@@ -1374,10 +1378,12 @@ class SeedAddressVerificationScreen(ButtonListScreen):
|
||||
|
||||
|
||||
def run(self):
|
||||
font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.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,11 +1,14 @@
|
||||
from dataclasses import dataclass
|
||||
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.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.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,
|
||||
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,4 @@
|
||||
from typing import List
|
||||
import RPi.GPIO as GPIO
|
||||
import time
|
||||
|
||||
@@ -157,10 +158,13 @@ class HardwareButtons(Singleton):
|
||||
HardwareButtonsConstants.release_lock = True
|
||||
return True
|
||||
|
||||
def check_for_low(self, key) -> bool:
|
||||
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
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -96,6 +97,9 @@ class EncodeQR:
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -106,11 +107,10 @@ 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):
|
||||
|
||||
@@ -484,44 +484,18 @@ class PSBTFinalizeView(View):
|
||||
|
||||
else:
|
||||
self.controller.psbt = trimmed_psbt
|
||||
|
||||
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)
|
||||
return Destination(PSBTSignedQRDisplayView)
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
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):
|
||||
def __init__(self, coordinator: str):
|
||||
super().__init__()
|
||||
self.coordinator = coordinator
|
||||
|
||||
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(
|
||||
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),
|
||||
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE),
|
||||
)
|
||||
|
||||
@@ -15,13 +15,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()
|
||||
|
||||
@@ -10,6 +10,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
|
||||
@@ -18,7 +19,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.psbt_views import PSBTChangeDetailsView
|
||||
@@ -348,11 +349,18 @@ class SeedOptionsView(View):
|
||||
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
|
||||
@@ -381,13 +389,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:
|
||||
@@ -403,16 +412,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))
|
||||
|
||||
|
||||
|
||||
@@ -487,23 +500,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)
|
||||
@@ -511,10 +538,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)
|
||||
|
||||
|
||||
|
||||
@@ -534,9 +562,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={
|
||||
@@ -646,29 +678,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,
|
||||
@@ -677,18 +719,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:
|
||||
@@ -1245,13 +1282,14 @@ class AddressVerificationStartView(View):
|
||||
destination = Destination(SeedSingleSigAddressVerificationSelectSeedView)
|
||||
|
||||
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:
|
||||
# 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"],
|
||||
wallet_type=sig_type,
|
||||
script_type=self.controller.unverified_address["script_type"]
|
||||
@@ -1295,7 +1333,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"]
|
||||
@@ -1399,7 +1437,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
|
||||
@@ -1416,7 +1454,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,
|
||||
@@ -1481,13 +1519,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
|
||||
"""
|
||||
@@ -1496,16 +1536,14 @@ 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):
|
||||
@@ -1516,9 +1554,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)
|
||||
@@ -1536,45 +1577,6 @@ class SeedAddressVerificationView(View):
|
||||
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):
|
||||
def __init__(self, seed_num: int):
|
||||
@@ -1638,6 +1640,7 @@ class MultisigWalletDescriptorView(View):
|
||||
|
||||
RETURN = "Return to PSBT"
|
||||
VERIFY = "Verify Addr"
|
||||
EXPLORER = "Address Explorer"
|
||||
OK = "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:
|
||||
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,
|
||||
@@ -1665,7 +1670,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)
|
||||
|
||||
@@ -2,17 +2,22 @@ import hashlib
|
||||
import os
|
||||
import time
|
||||
|
||||
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
|
||||
|
||||
@@ -23,7 +28,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,
|
||||
@@ -43,6 +49,10 @@ class ToolsMenuView(View):
|
||||
elif button_data[selected_menu_num] == KEYBOARD:
|
||||
return Destination(ToolsCalcFinalWordNumWordsView)
|
||||
|
||||
elif button_data[selected_menu_num] == EXPLORER:
|
||||
return Destination(ToolsAddressExplorerSelectSourceView)
|
||||
|
||||
|
||||
|
||||
|
||||
"""****************************************************************************
|
||||
@@ -409,3 +419,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