mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
Merge branch 'dev' into 194_keith_edits
This commit is contained in:
@@ -4,3 +4,5 @@ src/seedsigner.egg-info/
|
|||||||
.nova
|
.nova
|
||||||
.vscode
|
.vscode
|
||||||
src/seedsigner/models/settings_definition.json
|
src/seedsigner/models/settings_definition.json
|
||||||
|
.idea
|
||||||
|
*.mo
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* [Project Summary](#project-summary)
|
* [Project Summary](#project-summary)
|
||||||
* [Shopping List](#shopping-list)
|
* [Shopping List](#shopping-list)
|
||||||
* [Software Installation](#software-installation)
|
* [Software Installation](#software-installation)
|
||||||
* [Verifying Your Software](#verifying-your-software)
|
* [Verifying the Software](#verifying-the-software)
|
||||||
* [Enclosure Designs](#enclosure-designs)
|
* [Enclosure Designs](#enclosure-designs)
|
||||||
* [SeedQR Printable Templates](#seedqr-printable-templates)
|
* [SeedQR Printable Templates](#seedqr-printable-templates)
|
||||||
* [Manual Installation Instructions](#manual-installation-instructions)
|
* [Manual Installation Instructions](#manual-installation-instructions)
|
||||||
@@ -64,7 +64,7 @@ If you have specific questions about the project, our [Telegram Group](https://t
|
|||||||
|
|
||||||
To build a SeedSigner, you will need:
|
To build a SeedSigner, you will need:
|
||||||
|
|
||||||
* Raspberry Pi Zero (preferably version 1.3 with no WiFi/Bluetooth capability, but any Raspberry Pi 2/3/4 or Zero model will work)
|
* Raspberry Pi Zero (preferably version 1.3 with no WiFi/Bluetooth capability, but any Raspberry Pi 2/3/4 or Zero model will work, Raspberry Pi 1 devices will require a hardware modification to the Waveshare LCD Hat, as per the [instructions here](./docs/legacy_hardware.md))
|
||||||
* Waveshare 1.3" 240x240 pxl LCD (correct pixel count is important, more info at https://www.waveshare.com/wiki/1.3inch_LCD_HAT)
|
* Waveshare 1.3" 240x240 pxl LCD (correct pixel count is important, more info at https://www.waveshare.com/wiki/1.3inch_LCD_HAT)
|
||||||
* Pi Zero-compatible camera (tested to work with the Aokin / AuviPal 5MP 1080p with OV5647 Sensor)
|
* Pi Zero-compatible camera (tested to work with the Aokin / AuviPal 5MP 1080p with OV5647 Sensor)
|
||||||
|
|
||||||
@@ -76,11 +76,19 @@ Notes:
|
|||||||
---------------
|
---------------
|
||||||
|
|
||||||
# Software Installation
|
# 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.
|
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.)
|
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)
|
* seedsigner_pubkey.gpg (from the main folder of this repo)
|
||||||
@@ -142,6 +150,14 @@ The Orange Pill enclosure design offers a more finished look that includes butto
|
|||||||
|
|
||||||
The upper and lower portions of the enclosure can be printed using a standard FDM 3D printer, no supports necessary. The buttons and joystick nub should ideally be produced with a SLA/resin printer. An overview of the entire assembly process can be found [here](https://youtu.be/aIIc2DiZYcI). To access the design files and printable models, click [here](https://github.com/SeedSigner/seedsigner/tree/main/enclosures/orange_pill).
|
The upper and lower portions of the enclosure can be printed using a standard FDM 3D printer, no supports necessary. The buttons and joystick nub should ideally be produced with a SLA/resin printer. An overview of the entire assembly process can be found [here](https://youtu.be/aIIc2DiZYcI). To access the design files and printable models, click [here](https://github.com/SeedSigner/seedsigner/tree/main/enclosures/orange_pill).
|
||||||
|
|
||||||
|
### Community Designs
|
||||||
|
|
||||||
|
* [Lil Pill](https://cults3d.com/en/3d-model/gadget/lil-pill-seedsigner-case) by @_CyberNomad
|
||||||
|
* [OrangeSurf Case](https://github.com/orangesurf/orangesurf-seedsigner-case) by @OrangeSurfBTC
|
||||||
|
* [PS4 Seedsigner](https://www.thingiverse.com/thing:5363525) by @Silexperience
|
||||||
|
* [OpenPill Faceplate](https://www.printables.com/en/model/179924-seedsigner-open-pill-cover-plates-digital-cross-jo) by @Revetuzo
|
||||||
|
* [Waveshare CoverPlate](https://cults3d.com/en/3d-model/various/seedsigner-coverplate-for-waveshare-1-3-inch-lcd-hat-with-240x240-pixel-display) by @Adathome1
|
||||||
|
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
# SeedQR Printable Templates
|
# SeedQR Printable Templates
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
@@ -0,0 +1,22 @@
|
|||||||
|
# Legacy Hardware Modifications
|
||||||
|
|
||||||
|
Older Raspberry Pi devices have a smaller GPIO header, 26 pin as opposed to the 40 pin header found on more recent models. The Waveshare LCD hat uses some of the pins between 26 and 40 for the buttons, meaning that these will need to be reassigned (and remapped in software) when used with an older device.
|
||||||
|
|
||||||
|
## Hardware Changes
|
||||||
|
|
||||||
|
A suggested remapping can be found here:
|
||||||
|

|
||||||
|
|
||||||
|
This remapping can be done by soldering wires on to the Waveshare hat as below:
|
||||||
|

|
||||||
|
|
||||||
|
Alternatively, you could do this by connecting the LCD hat on with individual breadboard jumper wires.
|
||||||
|
|
||||||
|
Once you have re-mapped the pins, it is advised to do an IO Test to ensure that everything works.
|
||||||
|
|
||||||
|
**Warning: Some of the GPIO pins on contain 5 volt output. Raspberry Pi GPIO pins are NOT 5V tolerant, meaning that if you accidentally connect a 5V supply pin to a GPIO input pin, you risk permanent damage.**
|
||||||
|
|
||||||
|
## Software Changes
|
||||||
|
The Seedsigner software will automatically detect which hardware revision you are using and if the older hardware is detected, will remap the software to match the above modifications to the Waveshare hat.
|
||||||
|
|
||||||
|
If you are using a pre-built Seedsigner image that hasn't yet has this incorporated, you can simply take the file "buttons.py" (/src/seedsigner/hardware/ int his repository) and overwrite same file on your Seedsigner SD card. (The easiest way to do this is to copy it on to the /boot/ partitition of the SD card then copy it over via the command line while connected to your Pi via monitor+keyboard) O
|
||||||
@@ -480,3 +480,72 @@ b'\n\xcb\xba\x00\x8d\x9b\xa0\x05\xf5\x99k@\xa3G\\\xd9'
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Vectors 7-9: Additional Compact SeedQR problem characters
|
||||||
|
Explicitly check Compact SeedQRs whose byte stream contains `\n`, `\r`, or `\r\n`:
|
||||||
|
|
||||||
|
`\n`:
|
||||||
|
```bash
|
||||||
|
# 12-word seed:
|
||||||
|
dignity utility vacant shiver thought canoe feel multiply item youth actor coyote
|
||||||
|
|
||||||
|
# Standard SeedQR digit stream:
|
||||||
|
049619221923158517990268067811630950204300210397
|
||||||
|
|
||||||
|
# CompactSeedQR bitstream:
|
||||||
|
00111110000111100000101111000001111000110001111000001110010000110001010100110100100010110111011011011111111011000000101010011000
|
||||||
|
|
||||||
|
# CompactSeedQR bytestream:
|
||||||
|
b'>\x1e\x0b\xc1\xe3\x1e\x0eC\x154\x8bv\xdf\xec\n\x98'
|
||||||
|
```
|
||||||
|
|
||||||
|
<table align="center">
|
||||||
|
<tr>
|
||||||
|
<td align="center"><img src="img/vector7_compact_12word.png"><br/>CompactSeedQR</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
`\r`:
|
||||||
|
```bash
|
||||||
|
# 12-word seed:
|
||||||
|
corn voice scrap arrow original diamond trial property benefit choose junk lock
|
||||||
|
|
||||||
|
# Standard SeedQR digit stream:
|
||||||
|
038719631547010112530489185713790169032209701051
|
||||||
|
|
||||||
|
# CompactSeedQR bitstream:
|
||||||
|
00110000011111101010111100000101100001100101100111001010011110100111101000001101011000110001010100100101000010011110010101000001
|
||||||
|
|
||||||
|
# CompactSeedQR bytestream:
|
||||||
|
b'0~\xaf\x05\x86Y\xcazz\rc\x15%\t\xe5A'
|
||||||
|
```
|
||||||
|
|
||||||
|
<table align="center">
|
||||||
|
<tr>
|
||||||
|
<td align="center"><img src="img/vector8_compact_12word.png"><br/>CompactSeedQR</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
`\r\n`:
|
||||||
|
```bash
|
||||||
|
# 12-word seed:
|
||||||
|
vocal tray giggle tool duck letter category pattern train magnet excite swamp
|
||||||
|
|
||||||
|
# Standard SeedQR digit stream:
|
||||||
|
196218530783182905421028028912901848107106301753
|
||||||
|
|
||||||
|
# CompactSeedQR bitstream:
|
||||||
|
11110101010111001111010110000111111100100101010000111101000000010000100100001101000010101110011100010000101111010011101101101101
|
||||||
|
|
||||||
|
# CompactSeedQR bytestream:
|
||||||
|
b'\xf5\\\xf5\x87\xf2T=\x01\t\r\n\xe7\x10\xbd;m'
|
||||||
|
```
|
||||||
|
|
||||||
|
<table align="center">
|
||||||
|
<tr>
|
||||||
|
<td align="center"><img src="img/vector9_compact_12word.png"><br/>CompactSeedQR</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
embit==0.4.10
|
embit==0.5.0
|
||||||
numpy==1.21.1
|
numpy==1.21.1
|
||||||
picamera==1.13
|
picamera==1.13
|
||||||
Pillow==8.2.0
|
Pillow==8.2.0
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class Controller(Singleton):
|
|||||||
rather than at the top in order avoid circular imports.
|
rather than at the top in order avoid circular imports.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VERSION = "0.5.0"
|
VERSION = "0.5.1"
|
||||||
|
|
||||||
# Declare class member vars with type hints to enable richer IDE support throughout
|
# Declare class member vars with type hints to enable richer IDE support throughout
|
||||||
# the code.
|
# the code.
|
||||||
@@ -69,6 +69,8 @@ class Controller(Singleton):
|
|||||||
|
|
||||||
image_entropy_preview_frames: List[Image] = None
|
image_entropy_preview_frames: List[Image] = None
|
||||||
image_entropy_final_image: Image = None
|
image_entropy_final_image: Image = None
|
||||||
|
|
||||||
|
address_explorer_data: dict = None
|
||||||
# TODO: end refactor section
|
# TODO: end refactor section
|
||||||
|
|
||||||
# Destination placeholder for when we need to jump out to a side flow but intend to
|
# Destination placeholder for when we need to jump out to a side flow but intend to
|
||||||
@@ -77,6 +79,7 @@ class Controller(Singleton):
|
|||||||
FLOW__PSBT = "psbt"
|
FLOW__PSBT = "psbt"
|
||||||
FLOW__VERIFY_MULTISIG_ADDR = "multisig_addr"
|
FLOW__VERIFY_MULTISIG_ADDR = "multisig_addr"
|
||||||
FLOW__VERIFY_SINGLESIG_ADDR = "singlesig_addr"
|
FLOW__VERIFY_SINGLESIG_ADDR = "singlesig_addr"
|
||||||
|
FLOW__ADDRESS_EXPLORER = "address_explorer"
|
||||||
resume_main_flow: str = None
|
resume_main_flow: str = None
|
||||||
|
|
||||||
back_stack: BackStack = None
|
back_stack: BackStack = None
|
||||||
@@ -197,9 +200,9 @@ class Controller(Singleton):
|
|||||||
def run(self, some_arg, other_arg):
|
def run(self, some_arg, other_arg):
|
||||||
print(other_arg)
|
print(other_arg)
|
||||||
|
|
||||||
class OtherView():
|
class OtherView(View):
|
||||||
def run(self):
|
def run(self):
|
||||||
return (MyView, {"some_arg": 1, "other_arg": "hello"})
|
return (MyView, dict(some_arg=1, other_arg="hello"))
|
||||||
|
|
||||||
When `OtherView` is instantiated and run, we capture its return values:
|
When `OtherView` is instantiated and run, we capture its return values:
|
||||||
|
|
||||||
@@ -224,6 +227,7 @@ class Controller(Singleton):
|
|||||||
self.resume_main_flow = None
|
self.resume_main_flow = None
|
||||||
self.multisig_wallet_descriptor = None
|
self.multisig_wallet_descriptor = None
|
||||||
self.unverified_address = None
|
self.unverified_address = None
|
||||||
|
self.address_explorer_data = None
|
||||||
|
|
||||||
|
|
||||||
print(f"back_stack: {self.back_stack}")
|
print(f"back_stack: {self.back_stack}")
|
||||||
|
|||||||
@@ -289,7 +289,9 @@ class TextArea(BaseComponent):
|
|||||||
# fits in its bounding rect (plus accounting for edge padding) using its given
|
# fits in its bounding rect (plus accounting for edge padding) using its given
|
||||||
# font.
|
# font.
|
||||||
# Measure from left baseline ("ls")
|
# Measure from left baseline ("ls")
|
||||||
# getbbox() seems to ignore "\n" so won't affect height calcs
|
# TODO: getbbox() seems to ignore "\n" so isn't properly factored into height
|
||||||
|
# calcs and yields incorrect full_text_width. For now must specify self.height to
|
||||||
|
# render properly. Centering will be wrong.
|
||||||
(left, top, full_text_width, bottom) = self.font.getbbox(self.text, anchor="ls")
|
(left, top, full_text_width, bottom) = self.font.getbbox(self.text, anchor="ls")
|
||||||
self.text_font_height = -1 * top
|
self.text_font_height = -1 * top
|
||||||
self.bbox_height = self.text_font_height + bottom
|
self.bbox_height = self.text_font_height + bottom
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from dataclasses import dataclass
|
|||||||
from lzma import is_check_supported
|
from lzma import is_check_supported
|
||||||
from PIL import Image, ImageDraw, ImageFilter
|
from PIL import Image, ImageDraw, ImageFilter
|
||||||
from typing import List
|
from typing import List
|
||||||
|
import time
|
||||||
|
|
||||||
from seedsigner.gui.renderer import Renderer
|
from seedsigner.gui.renderer import Renderer
|
||||||
from seedsigner.models.threads import BaseThread
|
from seedsigner.models.threads import BaseThread
|
||||||
@@ -118,8 +119,12 @@ class PSBTOverviewScreen(ButtonListScreen):
|
|||||||
# max_destination_col_width += curve_width
|
# max_destination_col_width += curve_width
|
||||||
|
|
||||||
# Now let's maximize the actual destination col by adjusting our addr truncation
|
# Now let's maximize the actual destination col by adjusting our addr truncation
|
||||||
def calculate_destination_col_width(truncate_at: int):
|
def calculate_destination_col_width(truncate_at: int = 0):
|
||||||
def truncate_destination_addr(addr):
|
def truncate_destination_addr(addr):
|
||||||
|
# TODO: Properly handle the ellipsis truncation in different languages
|
||||||
|
if len(addr) <= truncate_at + len("..."):
|
||||||
|
# No point in truncating
|
||||||
|
return addr
|
||||||
return f"{addr[:truncate_at]}..."
|
return f"{addr[:truncate_at]}..."
|
||||||
|
|
||||||
destination_column = []
|
destination_column = []
|
||||||
@@ -129,7 +134,7 @@ class PSBTOverviewScreen(ButtonListScreen):
|
|||||||
destination_column.append(truncate_destination_addr(addr))
|
destination_column.append(truncate_destination_addr(addr))
|
||||||
|
|
||||||
for i in range(0, self.num_self_transfer_outputs):
|
for i in range(0, self.num_self_transfer_outputs):
|
||||||
destination_column.append(f"self-transfer")
|
destination_column.append(truncate_destination_addr("self-transfer"))
|
||||||
else:
|
else:
|
||||||
# destination_column.append(f"{len(self.destination_addresses)} recipients")
|
# destination_column.append(f"{len(self.destination_addresses)} recipients")
|
||||||
destination_column.append(f"recipient 1")
|
destination_column.append(f"recipient 1")
|
||||||
@@ -151,10 +156,10 @@ class PSBTOverviewScreen(ButtonListScreen):
|
|||||||
|
|
||||||
if len(self.destination_addresses) + self.num_self_transfer_outputs > 3:
|
if len(self.destination_addresses) + self.num_self_transfer_outputs > 3:
|
||||||
# We're not going to display any destination addrs so truncation doesn't matter
|
# We're not going to display any destination addrs so truncation doesn't matter
|
||||||
(destination_text_width, destination_column) = calculate_destination_col_width(truncate_at=0)
|
(destination_text_width, destination_column) = calculate_destination_col_width()
|
||||||
else:
|
else:
|
||||||
# Steadliy widen out the destination column until we run out of space
|
# Steadliy widen out the destination column until we run out of space
|
||||||
for i in range(6, 13):
|
for i in range(6, 14):
|
||||||
(new_width, new_col_text) = calculate_destination_col_width(truncate_at=i)
|
(new_width, new_col_text) = calculate_destination_col_width(truncate_at=i)
|
||||||
if new_width > max_destination_col_width:
|
if new_width > max_destination_col_width:
|
||||||
break
|
break
|
||||||
@@ -435,7 +440,7 @@ class PSBTOverviewScreen(ButtonListScreen):
|
|||||||
self.renderer.show_image()
|
self.renderer.show_image()
|
||||||
|
|
||||||
# No need to CPU limit when running in its own thread?
|
# No need to CPU limit when running in its own thread?
|
||||||
# time.sleep(0.02)
|
time.sleep(0.02)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -692,19 +697,3 @@ class PSBTFinalizeScreen(ButtonListScreen):
|
|||||||
text="Click to authorize this transaction",
|
text="Click to authorize this transaction",
|
||||||
screen_y=icon.screen_y + icon.height + GUIConstants.COMPONENT_PADDING
|
screen_y=icon.screen_y + icon.height + GUIConstants.COMPONENT_PADDING
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PSBTSelectCoordinatorScreen(ButtonListScreen):
|
|
||||||
def __post_init__(self):
|
|
||||||
# Customize defaults
|
|
||||||
self.title = "Signed PSBT"
|
|
||||||
self.is_bottom_list = True
|
|
||||||
super().__post_init__()
|
|
||||||
|
|
||||||
self.components.append(TextArea(
|
|
||||||
text="Export as a QR code for:",
|
|
||||||
is_text_centered=True,
|
|
||||||
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
|
|
||||||
))
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from typing import Any, List, Tuple
|
|||||||
from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
|
from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
|
||||||
from seedsigner.gui.renderer import Renderer
|
from seedsigner.gui.renderer import Renderer
|
||||||
|
|
||||||
from seedsigner.models.threads import BaseThread
|
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
|
||||||
from seedsigner.models.encode_qr import EncodeQR
|
from seedsigner.models.encode_qr import EncodeQR
|
||||||
from seedsigner.models.settings import Settings, SettingsConstants
|
from seedsigner.models.settings import Settings, SettingsConstants
|
||||||
|
|
||||||
@@ -661,41 +661,73 @@ class LargeButtonScreen(BaseTopNavScreen):
|
|||||||
class QRDisplayScreen(BaseScreen):
|
class QRDisplayScreen(BaseScreen):
|
||||||
qr_encoder: EncodeQR = None
|
qr_encoder: EncodeQR = None
|
||||||
|
|
||||||
|
class QRDisplayThread(BaseThread):
|
||||||
|
def __init__(self, qr_encoder: EncodeQR, qr_brightness: ThreadsafeCounter, renderer: Renderer):
|
||||||
|
super().__init__()
|
||||||
|
self.qr_encoder = qr_encoder
|
||||||
|
self.qr_brightness = qr_brightness
|
||||||
|
self.renderer = renderer
|
||||||
|
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
# Loop whether the QR is a single frame or animated; each loop might adjust
|
||||||
|
# brightness setting.
|
||||||
|
while self.keep_running:
|
||||||
|
# convert the self.qr_brightness integer (31-255) into hex triplets
|
||||||
|
hex_color = (hex(self.qr_brightness.cur_count).split('x')[1]) * 3
|
||||||
|
image = self.qr_encoder.next_part_image(240,240, border=2, background_color=hex_color)
|
||||||
|
with self.renderer.lock:
|
||||||
|
self.renderer.show_image(image)
|
||||||
|
|
||||||
|
# Target n held frames per second before rendering next QR image
|
||||||
|
time.sleep(5/30.0)
|
||||||
|
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
from seedsigner.models.settings import Settings
|
||||||
|
super().__post_init__()
|
||||||
|
|
||||||
|
# Shared coordination var so the display thread can detect success
|
||||||
|
settings = Settings.get_instance()
|
||||||
|
self.qr_brightness = ThreadsafeCounter(initial_value=settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS))
|
||||||
|
|
||||||
|
self.threads.append(QRDisplayScreen.QRDisplayThread(
|
||||||
|
qr_encoder=self.qr_encoder,
|
||||||
|
qr_brightness=self.qr_brightness,
|
||||||
|
renderer=self.renderer,
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
from seedsigner.models.settings import Settings
|
from seedsigner.models.settings import Settings
|
||||||
settings = Settings.get_instance()
|
|
||||||
cur_brightness = settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS)
|
|
||||||
|
|
||||||
# Loop whether the QR is a single frame or animated; each loop might adjust
|
|
||||||
# brightness setting.
|
|
||||||
while True:
|
while True:
|
||||||
ret = self._run_callback()
|
user_input = self.hw_inputs.wait_for(
|
||||||
if ret is not None:
|
[
|
||||||
return ret
|
HardwareButtonsConstants.KEY_UP,
|
||||||
|
HardwareButtonsConstants.KEY_DOWN,
|
||||||
# convert the cur_brightness integer (31-255) into hex triplets
|
HardwareButtonsConstants.KEY_LEFT,
|
||||||
hex_color = (hex(cur_brightness).split('x')[1]) * 3
|
HardwareButtonsConstants.KEY_RIGHT,
|
||||||
image = self.qr_encoder.next_part_image(240,240, border=2, background_color=hex_color)
|
] + HardwareButtonsConstants.KEYS__ANYCLICK,
|
||||||
self.renderer.show_image(image)
|
check_release=True,
|
||||||
|
release_keys=HardwareButtonsConstants.KEYS__ANYCLICK
|
||||||
# Target n held frames per second before rendering next QR image
|
)
|
||||||
time.sleep(5/30.0)
|
if user_input == HardwareButtonsConstants.KEY_DOWN:
|
||||||
|
|
||||||
if self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_DOWN):
|
|
||||||
# Reduce QR code background brightness
|
# Reduce QR code background brightness
|
||||||
cur_brightness = max(31, cur_brightness - 31)
|
self.qr_brightness.set_value(max(31, self.qr_brightness.cur_count - 31))
|
||||||
|
|
||||||
elif self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_UP):
|
elif user_input == HardwareButtonsConstants.KEY_UP:
|
||||||
# Incrase QR code background brightness
|
# Incrase QR code background brightness
|
||||||
cur_brightness = min(cur_brightness + 31, 255)
|
self.qr_brightness.set_value(min(self.qr_brightness.cur_count + 31, 255))
|
||||||
|
|
||||||
elif self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_RIGHT):
|
else:
|
||||||
|
# Any other input exits the screen
|
||||||
|
self.threads[-1].stop()
|
||||||
|
while self.threads[-1].is_alive():
|
||||||
|
time.sleep(0.01)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
Settings.get_instance().set_value(SettingsConstants.SETTING__QR_BRIGHTNESS, self.qr_brightness.cur_count)
|
||||||
settings.set_value(SettingsConstants.SETTING__QR_BRIGHTNESS, cur_brightness)
|
|
||||||
|
|
||||||
# TODO: handle left as BACK
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -436,7 +436,6 @@ class SeedFinalizeScreen(ButtonListScreen):
|
|||||||
@dataclass
|
@dataclass
|
||||||
class SeedOptionsScreen(ButtonListScreen):
|
class SeedOptionsScreen(ButtonListScreen):
|
||||||
# Customize defaults
|
# Customize defaults
|
||||||
is_bottom_list: bool = True
|
|
||||||
fingerprint: str = None
|
fingerprint: str = None
|
||||||
has_passphrase: bool = False
|
has_passphrase: bool = False
|
||||||
|
|
||||||
@@ -445,6 +444,8 @@ class SeedOptionsScreen(ButtonListScreen):
|
|||||||
self.top_nav_icon_color = "blue"
|
self.top_nav_icon_color = "blue"
|
||||||
self.title = self.fingerprint
|
self.title = self.fingerprint
|
||||||
self.is_button_text_centered = False
|
self.is_button_text_centered = False
|
||||||
|
self.is_bottom_list = True
|
||||||
|
|
||||||
super().__post_init__()
|
super().__post_init__()
|
||||||
|
|
||||||
|
|
||||||
@@ -1017,6 +1018,7 @@ class SeedReviewPassphraseScreen(ButtonListScreen):
|
|||||||
font_size=font_size,
|
font_size=font_size,
|
||||||
is_text_centered=True,
|
is_text_centered=True,
|
||||||
screen_y=screen_y,
|
screen_y=screen_y,
|
||||||
|
allow_text_overflow=True
|
||||||
))
|
))
|
||||||
screen_y += char_height + 2
|
screen_y += char_height + 2
|
||||||
|
|
||||||
@@ -1361,6 +1363,9 @@ class SeedAddressVerificationScreen(ButtonListScreen):
|
|||||||
print(f"verified_index: {self.verified_index.cur_count}")
|
print(f"verified_index: {self.verified_index.cur_count}")
|
||||||
if self.verified_index.cur_count is not None:
|
if self.verified_index.cur_count is not None:
|
||||||
print("Screen callback returning success!")
|
print("Screen callback returning success!")
|
||||||
|
self.threads[-1].stop()
|
||||||
|
while self.threads[-1].is_alive():
|
||||||
|
time.sleep(0.01)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
@@ -1374,10 +1379,12 @@ class SeedAddressVerificationScreen(ButtonListScreen):
|
|||||||
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BODY_FONT_SIZE)
|
|
||||||
while self.keep_running:
|
while self.keep_running:
|
||||||
if self.verified_index.cur_count is not None:
|
if self.verified_index.cur_count is not None:
|
||||||
# Have to trigger a hw_input event to break the Screen out of the wait_for loop
|
# This thread will detect the success state while its parent Screen
|
||||||
|
# holds in its `wait_for`. Have to trigger a hw_input event to break
|
||||||
|
# the Screen._run out of the `wait_for` state. The Screen will then
|
||||||
|
# call its `_run_callback` and detect the success state and exit.
|
||||||
HardwareButtons.get_instance().trigger_override(force_release=True)
|
HardwareButtons.get_instance().trigger_override(force_release=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
from PIL.Image import Image
|
from PIL.Image import Image
|
||||||
from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
|
from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
|
||||||
from seedsigner.hardware.camera import Camera
|
from seedsigner.hardware.camera import Camera
|
||||||
from seedsigner.gui.components import FontAwesomeIconConstants, Fonts, GUIConstants, IconTextLine, SeedSignerCustomIconConstants, TextArea
|
from seedsigner.gui.components import FontAwesomeIconConstants, Fonts, FormattedAddress, GUIConstants, IconTextLine, SeedSignerCustomIconConstants, TextArea
|
||||||
|
|
||||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, KeyboardScreen
|
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, KeyboardScreen
|
||||||
from seedsigner.hardware.buttons import HardwareButtonsConstants
|
from seedsigner.hardware.buttons import HardwareButtonsConstants
|
||||||
|
from seedsigner.helpers.qr import QR
|
||||||
|
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -151,7 +154,195 @@ class ToolsDiceEntropyEntryScreen(KeyboardScreen):
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ToolsCalcFinalWordShowFinalWordScreen(ButtonListScreen):
|
class ToolsCalcFinalWordFinalizePromptScreen(ButtonListScreen):
|
||||||
|
mnemonic_length: int = None
|
||||||
|
num_entropy_bits: int = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.title = "Build Final Word"
|
||||||
|
self.is_bottom_list = True
|
||||||
|
self.is_button_text_centered = True
|
||||||
|
super().__post_init__()
|
||||||
|
|
||||||
|
self.components.append(TextArea(
|
||||||
|
text=f"The {self.mnemonic_length}th word is built from {self.num_entropy_bits} more entropy bits plus auto-calculated checksum.",
|
||||||
|
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ToolsCoinFlipEntryScreen(KeyboardScreen):
|
||||||
|
def __post_init__(self):
|
||||||
|
# Override values set by the parent class
|
||||||
|
self.title = f"Coin Flip 1/{self.return_after_n_chars}"
|
||||||
|
|
||||||
|
# Specify the keys in the keyboard
|
||||||
|
self.rows = 1
|
||||||
|
self.cols = 4
|
||||||
|
self.key_height = GUIConstants.TOP_NAV_TITLE_FONT_SIZE + 2 + 2*GUIConstants.EDGE_PADDING
|
||||||
|
self.keys_charset = "10"
|
||||||
|
|
||||||
|
# Now initialize the parent class
|
||||||
|
super().__post_init__()
|
||||||
|
|
||||||
|
self.components.append(TextArea(
|
||||||
|
text="Heads = 1",
|
||||||
|
screen_y = self.keyboard.rect[3] + 4*GUIConstants.COMPONENT_PADDING,
|
||||||
|
))
|
||||||
|
self.components.append(TextArea(
|
||||||
|
text="Tails = 0",
|
||||||
|
screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING,
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def update_title(self) -> bool:
|
||||||
|
self.title = f"Coin Flip {self.cursor_position + 1}/{self.return_after_n_chars}"
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ToolsCalcFinalWordScreen(ButtonListScreen):
|
||||||
|
selected_final_word: str = None
|
||||||
|
selected_final_bits: str = None
|
||||||
|
checksum_bits: str = None
|
||||||
|
actual_final_word: str = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.is_bottom_list = True
|
||||||
|
super().__post_init__()
|
||||||
|
|
||||||
|
# First what's the total bit display width and where do the checksum bits start?
|
||||||
|
bit_font_size = GUIConstants.BUTTON_FONT_SIZE + 2
|
||||||
|
font = Fonts.get_font(GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, bit_font_size)
|
||||||
|
(left, top, bit_display_width, bit_font_height) = font.getbbox("0" * 11, anchor="lt")
|
||||||
|
(left, top, checksum_x, bottom) = font.getbbox("0" * (11 - len(self.checksum_bits)), anchor="lt")
|
||||||
|
bit_display_x = int((self.canvas_width - bit_display_width)/2)
|
||||||
|
checksum_x += bit_display_x
|
||||||
|
|
||||||
|
# Display the user's additional entropy input
|
||||||
|
if self.selected_final_word:
|
||||||
|
selection_text = self.selected_final_word
|
||||||
|
keeper_selected_bits = self.selected_final_bits[:11 - len(self.checksum_bits)]
|
||||||
|
|
||||||
|
# The word's least significant bits will be rendered differently to convey
|
||||||
|
# the fact that they're being discarded.
|
||||||
|
discard_selected_bits = self.selected_final_bits[-1*len(self.checksum_bits):]
|
||||||
|
else:
|
||||||
|
# User entered coin flips or all zeros
|
||||||
|
selection_text = self.selected_final_bits
|
||||||
|
keeper_selected_bits = self.selected_final_bits
|
||||||
|
|
||||||
|
# We'll append spacer chars to preserve the vertical alignment (most
|
||||||
|
# significant n bits always rendered in same column)
|
||||||
|
discard_selected_bits = "_" * (len(self.checksum_bits))
|
||||||
|
|
||||||
|
self.components.append(TextArea(
|
||||||
|
text=f"""Your input: \"{selection_text}\"""",
|
||||||
|
screen_y=self.top_nav.height,
|
||||||
|
))
|
||||||
|
|
||||||
|
# ...and that entropy's associated 11 bits
|
||||||
|
screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING
|
||||||
|
self.components.append(TextArea(
|
||||||
|
text=keeper_selected_bits,
|
||||||
|
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
|
||||||
|
font_size=bit_font_size,
|
||||||
|
edge_padding=0,
|
||||||
|
screen_x=bit_display_x,
|
||||||
|
screen_y=screen_y,
|
||||||
|
height=bit_font_height,
|
||||||
|
is_text_centered=False,
|
||||||
|
))
|
||||||
|
|
||||||
|
# Render the least significant bits that will be replaced by the checksum in a
|
||||||
|
# de-emphasized font color.
|
||||||
|
self.components.append(TextArea(
|
||||||
|
text=discard_selected_bits,
|
||||||
|
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
|
||||||
|
font_color=GUIConstants.LABEL_FONT_COLOR,
|
||||||
|
font_size=bit_font_size,
|
||||||
|
edge_padding=0,
|
||||||
|
screen_x=checksum_x,
|
||||||
|
screen_y=screen_y,
|
||||||
|
height=bit_font_height,
|
||||||
|
is_text_centered=False,
|
||||||
|
))
|
||||||
|
|
||||||
|
# Show the checksum..
|
||||||
|
self.components.append(TextArea(
|
||||||
|
text="Checksum",
|
||||||
|
edge_padding=0,
|
||||||
|
screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING,
|
||||||
|
))
|
||||||
|
|
||||||
|
# ...and its actual bits. Prepend spacers to keep vertical alignment
|
||||||
|
checksum_spacer = "_" * (11 - len(self.checksum_bits))
|
||||||
|
|
||||||
|
screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING
|
||||||
|
|
||||||
|
# This time we de-emphasize the prepended spacers that are irrelevant
|
||||||
|
self.components.append(TextArea(
|
||||||
|
text=checksum_spacer,
|
||||||
|
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
|
||||||
|
font_color=GUIConstants.LABEL_FONT_COLOR,
|
||||||
|
font_size=bit_font_size,
|
||||||
|
edge_padding=0,
|
||||||
|
screen_x=bit_display_x,
|
||||||
|
screen_y=screen_y,
|
||||||
|
height=bit_font_height,
|
||||||
|
is_text_centered=False,
|
||||||
|
))
|
||||||
|
|
||||||
|
# And especially highlight (orange!) the actual checksum bits
|
||||||
|
self.components.append(TextArea(
|
||||||
|
text=self.checksum_bits,
|
||||||
|
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
|
||||||
|
font_size=bit_font_size,
|
||||||
|
font_color=GUIConstants.ACCENT_COLOR,
|
||||||
|
edge_padding=0,
|
||||||
|
screen_x=checksum_x,
|
||||||
|
screen_y=screen_y,
|
||||||
|
is_text_centered=False,
|
||||||
|
))
|
||||||
|
|
||||||
|
# And now the *actual* final word after merging the bit data
|
||||||
|
self.components.append(TextArea(
|
||||||
|
text=f"""Final Word: \"{self.actual_final_word}\"""",
|
||||||
|
screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING,
|
||||||
|
))
|
||||||
|
|
||||||
|
# Once again show the bits that came from the user's entropy...
|
||||||
|
num_checksum_bits = len(self.checksum_bits)
|
||||||
|
user_component = self.selected_final_bits[:11 - num_checksum_bits]
|
||||||
|
screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING
|
||||||
|
self.components.append(TextArea(
|
||||||
|
text=user_component,
|
||||||
|
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
|
||||||
|
font_size=bit_font_size,
|
||||||
|
edge_padding=0,
|
||||||
|
screen_x=bit_display_x,
|
||||||
|
screen_y=screen_y,
|
||||||
|
is_text_centered=False,
|
||||||
|
))
|
||||||
|
|
||||||
|
# ...and append the checksum's bits, still highlighted in orange
|
||||||
|
self.components.append(TextArea(
|
||||||
|
text=self.checksum_bits,
|
||||||
|
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
|
||||||
|
font_color=GUIConstants.ACCENT_COLOR,
|
||||||
|
font_size=bit_font_size,
|
||||||
|
edge_padding=0,
|
||||||
|
screen_x=checksum_x,
|
||||||
|
screen_y=screen_y,
|
||||||
|
is_text_centered=False,
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ToolsCalcFinalWordDoneScreen(ButtonListScreen):
|
||||||
final_word: str = None
|
final_word: str = None
|
||||||
mnemonic_word_length: int = 12
|
mnemonic_word_length: int = 12
|
||||||
fingerprint: str = None
|
fingerprint: str = None
|
||||||
@@ -178,3 +369,53 @@ class ToolsCalcFinalWordShowFinalWordScreen(ButtonListScreen):
|
|||||||
is_text_centered=True,
|
is_text_centered=True,
|
||||||
screen_y=self.components[-1].screen_y + self.components[-1].height + 3*GUIConstants.COMPONENT_PADDING,
|
screen_y=self.components[-1].screen_y + self.components[-1].height + 3*GUIConstants.COMPONENT_PADDING,
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen):
|
||||||
|
fingerprint: str = None
|
||||||
|
wallet_descriptor_display_name: Any = None
|
||||||
|
script_type: str = None
|
||||||
|
custom_derivation_path: str = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.title = "Address Explorer"
|
||||||
|
self.is_bottom_list = True
|
||||||
|
super().__post_init__()
|
||||||
|
|
||||||
|
if self.fingerprint:
|
||||||
|
self.components.append(IconTextLine(
|
||||||
|
icon_name=SeedSignerCustomIconConstants.FINGERPRINT,
|
||||||
|
icon_color="blue",
|
||||||
|
label_text="Fingerprint",
|
||||||
|
value_text=self.fingerprint,
|
||||||
|
screen_x=GUIConstants.EDGE_PADDING,
|
||||||
|
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
|
||||||
|
))
|
||||||
|
|
||||||
|
if self.script_type != SettingsConstants.CUSTOM_DERIVATION:
|
||||||
|
self.components.append(IconTextLine(
|
||||||
|
icon_name=SeedSignerCustomIconConstants.PATH,
|
||||||
|
label_text="Derivation",
|
||||||
|
value_text=SettingsDefinition.get_settings_entry(attr_name=SettingsConstants.SETTING__SCRIPT_TYPES).get_selection_option_display_name_by_value(value=self.script_type),
|
||||||
|
screen_x=GUIConstants.EDGE_PADDING,
|
||||||
|
screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING,
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
self.components.append(IconTextLine(
|
||||||
|
icon_name=SeedSignerCustomIconConstants.PATH,
|
||||||
|
label_text="Derivation",
|
||||||
|
value_text=self.custom_derivation_path,
|
||||||
|
screen_x=GUIConstants.EDGE_PADDING,
|
||||||
|
screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING,
|
||||||
|
))
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.components.append(IconTextLine(
|
||||||
|
label_text="Wallet descriptor",
|
||||||
|
value_text=self.wallet_descriptor_display_name,
|
||||||
|
is_text_centered=False,
|
||||||
|
screen_x=GUIConstants.EDGE_PADDING,
|
||||||
|
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
|
||||||
|
))
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ class ST7789(object):
|
|||||||
self.height = 240
|
self.height = 240
|
||||||
|
|
||||||
#Initialize DC RST pin
|
#Initialize DC RST pin
|
||||||
self._dc = 25
|
self._dc = 22
|
||||||
self._rst = 27
|
self._rst = 13
|
||||||
self._bl = 24
|
self._bl = 18
|
||||||
|
|
||||||
GPIO.setmode(GPIO.BCM)
|
GPIO.setmode(GPIO.BOARD)
|
||||||
GPIO.setwarnings(False)
|
GPIO.setwarnings(False)
|
||||||
GPIO.setup(self._dc,GPIO.OUT)
|
GPIO.setup(self._dc,GPIO.OUT)
|
||||||
GPIO.setup(self._rst,GPIO.OUT)
|
GPIO.setup(self._rst,GPIO.OUT)
|
||||||
|
|||||||
@@ -1,20 +1,33 @@
|
|||||||
|
from typing import List
|
||||||
import RPi.GPIO as GPIO
|
import RPi.GPIO as GPIO
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from seedsigner.models.singleton import Singleton
|
from seedsigner.models.singleton import Singleton
|
||||||
|
|
||||||
|
|
||||||
class HardwareButtons(Singleton):
|
class HardwareButtons(Singleton):
|
||||||
KEY_UP_PIN = 6
|
if GPIO.RPI_INFO['P1_REVISION'] == 3: #This indicates that we have revision 3 GPIO
|
||||||
KEY_DOWN_PIN = 19
|
print("Detected 40pin GPIO (Rasbperry Pi 2 and above)")
|
||||||
KEY_LEFT_PIN = 5
|
KEY_UP_PIN = 31
|
||||||
KEY_RIGHT_PIN = 26
|
KEY_DOWN_PIN = 35
|
||||||
KEY_PRESS_PIN = 13
|
KEY_LEFT_PIN = 29
|
||||||
|
KEY_RIGHT_PIN = 37
|
||||||
|
KEY_PRESS_PIN = 33
|
||||||
|
|
||||||
KEY1_PIN = 21
|
KEY1_PIN = 40
|
||||||
KEY2_PIN = 20
|
KEY2_PIN = 38
|
||||||
KEY3_PIN = 16
|
KEY3_PIN = 36
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("Assuming 26 Pin GPIO (Raspberry P1 1)")
|
||||||
|
KEY_UP_PIN = 5
|
||||||
|
KEY_DOWN_PIN = 11
|
||||||
|
KEY_LEFT_PIN = 3
|
||||||
|
KEY_RIGHT_PIN = 15
|
||||||
|
KEY_PRESS_PIN = 7
|
||||||
|
|
||||||
|
KEY1_PIN = 16
|
||||||
|
KEY2_PIN = 12
|
||||||
|
KEY3_PIN = 8
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_instance(cls):
|
def get_instance(cls):
|
||||||
@@ -23,7 +36,7 @@ class HardwareButtons(Singleton):
|
|||||||
cls._instance = cls.__new__(cls)
|
cls._instance = cls.__new__(cls)
|
||||||
|
|
||||||
#init GPIO
|
#init GPIO
|
||||||
GPIO.setmode(GPIO.BCM)
|
GPIO.setmode(GPIO.BOARD)
|
||||||
GPIO.setup(HardwareButtons.KEY_UP_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
|
GPIO.setup(HardwareButtons.KEY_UP_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
|
||||||
GPIO.setup(HardwareButtons.KEY_DOWN_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
|
GPIO.setup(HardwareButtons.KEY_DOWN_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
|
||||||
GPIO.setup(HardwareButtons.KEY_LEFT_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
|
GPIO.setup(HardwareButtons.KEY_LEFT_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
|
||||||
@@ -145,10 +158,13 @@ class HardwareButtons(Singleton):
|
|||||||
HardwareButtonsConstants.release_lock = True
|
HardwareButtonsConstants.release_lock = True
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def check_for_low(self, key) -> bool:
|
def check_for_low(self, key: int = None, keys: List[int] = None) -> bool:
|
||||||
if self.GPIO.input(key) == self.GPIO.LOW:
|
if key:
|
||||||
self.update_last_input_time()
|
keys = [key]
|
||||||
return True
|
for key in keys:
|
||||||
|
if self.GPIO.input(key) == self.GPIO.LOW:
|
||||||
|
self.update_last_input_time()
|
||||||
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -158,21 +174,31 @@ class HardwareButtons(Singleton):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# class used as short hand for static button/channel lookup values
|
# class used as short hand for static button/channel lookup values
|
||||||
# TODO: Implement `release_lock` functionality as a global somewhere. Mixes up design
|
# TODO: Implement `release_lock` functionality as a global somewhere. Mixes up design
|
||||||
# patterns to have a static constants class plus a settable global value.
|
# patterns to have a static constants class plus a settable global value.
|
||||||
class HardwareButtonsConstants:
|
class HardwareButtonsConstants:
|
||||||
KEY_UP = 6
|
if GPIO.RPI_INFO['P1_REVISION'] == 3: #This indicates that we have revision 3 GPIO
|
||||||
KEY_DOWN = 19
|
KEY_UP = 31
|
||||||
KEY_LEFT = 5
|
KEY_DOWN = 35
|
||||||
KEY_RIGHT = 26
|
KEY_LEFT = 29
|
||||||
KEY_PRESS = 13
|
KEY_RIGHT = 37
|
||||||
KEY1 = 21
|
KEY_PRESS = 33
|
||||||
KEY2 = 20
|
|
||||||
KEY3 = 16
|
KEY1 = 40
|
||||||
|
KEY2 = 38
|
||||||
|
KEY3 = 36
|
||||||
|
else:
|
||||||
|
KEY_UP = 5
|
||||||
|
KEY_DOWN = 11
|
||||||
|
KEY_LEFT = 3
|
||||||
|
KEY_RIGHT = 15
|
||||||
|
KEY_PRESS = 7
|
||||||
|
|
||||||
|
KEY1 = 16
|
||||||
|
KEY2 = 12
|
||||||
|
KEY3 = 8
|
||||||
|
|
||||||
OVERRIDE = 1000
|
OVERRIDE = 1000
|
||||||
|
|
||||||
ALL_KEYS = [
|
ALL_KEYS = [
|
||||||
|
|||||||
@@ -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!")
|
||||||
@@ -8,23 +8,25 @@ from typing import List
|
|||||||
from seedsigner.models.seed import Seed
|
from seedsigner.models.seed import Seed
|
||||||
|
|
||||||
|
|
||||||
def calculate_checksum(partial_mnemonic: list, wordlist_language_code: str) -> List[str]:
|
def calculate_checksum(mnemonic: list, wordlist_language_code: str) -> List[str]:
|
||||||
""" Provide 11- or 23-word mnemonic, returns complete mnemonic w/checksum as a list """
|
"""
|
||||||
if len(partial_mnemonic) not in [11, 23]:
|
Provide 12- or 24-word mnemonic, returns complete mnemonic w/checksum as a list.
|
||||||
raise Exception("Pass in a 11- or 23-word mnemonic")
|
|
||||||
|
|
||||||
|
If 11- or 23-words are provided, append word `0000` to end of list as temp final
|
||||||
|
word.
|
||||||
|
"""
|
||||||
|
if len(mnemonic) in [11, 23]:
|
||||||
|
mnemonic.append(Seed.get_wordlist(wordlist_language_code)[0])
|
||||||
|
|
||||||
|
if len(mnemonic) not in [12, 24]:
|
||||||
|
raise Exception("Pass in a 12- or 24-word mnemonic")
|
||||||
|
|
||||||
# Work on a copy of the input list
|
# Work on a copy of the input list
|
||||||
mnemonic_copy = partial_mnemonic.copy()
|
mnemonic_copy = mnemonic.copy()
|
||||||
|
|
||||||
# 12-word seeds contribute 7 bits of entropy to the final word; 24-word seeds
|
|
||||||
# contribute 3 bits. But we don't have any partial entropy bits to use to help us
|
|
||||||
# create the final word. So just default to filling those missing values with zeroes
|
|
||||||
# ("abandon" is word 0000, so effectively inserts zeroes).
|
|
||||||
mnemonic_copy.append("abandon")
|
|
||||||
|
|
||||||
# Convert the resulting mnemonic to bytes, but we `ignore_checksum` validation
|
# Convert the resulting mnemonic to bytes, but we `ignore_checksum` validation
|
||||||
# because we have to assume it's incorrect since we just hard-coded it above; we'll
|
# because we assume it's incorrect since we either let the user select their own
|
||||||
# fix that next.
|
# final word OR we injected the 0000 word from the wordlist.
|
||||||
mnemonic_bytes = bip39.mnemonic_to_bytes(unicodedata.normalize("NFKD", " ".join(mnemonic_copy)), ignore_checksum=True, wordlist=Seed.get_wordlist(wordlist_language_code))
|
mnemonic_bytes = bip39.mnemonic_to_bytes(unicodedata.normalize("NFKD", " ".join(mnemonic_copy)), ignore_checksum=True, wordlist=Seed.get_wordlist(wordlist_language_code))
|
||||||
|
|
||||||
# This function will convert the bytes back into a mnemonic, but it will also
|
# This function will convert the bytes back into a mnemonic, but it will also
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class EncodeQR:
|
|||||||
qr_type: str = None
|
qr_type: str = None
|
||||||
qr_density: str = SettingsConstants.DENSITY__MEDIUM
|
qr_density: str = SettingsConstants.DENSITY__MEDIUM
|
||||||
wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
|
wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
|
||||||
|
bitcoin_address: str = None
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
self.qr = QR()
|
self.qr = QR()
|
||||||
@@ -95,6 +96,9 @@ class EncodeQR:
|
|||||||
elif self.qr_type == QRType.SEED__COMPACTSEEDQR:
|
elif self.qr_type == QRType.SEED__COMPACTSEEDQR:
|
||||||
self.encoder = CompactSeedQrEncoder(seed_phrase=self.seed_phrase,
|
self.encoder = CompactSeedQrEncoder(seed_phrase=self.seed_phrase,
|
||||||
wordlist_language_code=self.wordlist_language_code)
|
wordlist_language_code=self.wordlist_language_code)
|
||||||
|
|
||||||
|
elif self.qr_type == QRType.BITCOIN_ADDRESS:
|
||||||
|
self.encoder = BitcoinAddressEncoder(address=self.bitcoin_address)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise Exception('QR Type not supported')
|
raise Exception('QR Type not supported')
|
||||||
@@ -318,6 +322,25 @@ class CompactSeedQrEncoder(SeedQrEncoder):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class BitcoinAddressEncoder(BaseQrEncoder):
|
||||||
|
def __init__(self, address: str):
|
||||||
|
super().__init__()
|
||||||
|
self.address = address
|
||||||
|
|
||||||
|
|
||||||
|
def seq_len(self):
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def next_part(self):
|
||||||
|
return self.address
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_complete(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class XpubQrEncoder(BaseQrEncoder):
|
class XpubQrEncoder(BaseQrEncoder):
|
||||||
def __init__(self, seed_phrase, passphrase, derivation, network, wordlist_language_code):
|
def __init__(self, seed_phrase, passphrase, derivation, network, wordlist_language_code):
|
||||||
self.seed_phrase = seed_phrase
|
self.seed_phrase = seed_phrase
|
||||||
|
|||||||
@@ -189,42 +189,6 @@ class PSBTParser():
|
|||||||
return cnt
|
return cnt
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def calc_derivation(network, wallet_type, script_type):
|
|
||||||
# TODO: Move this to Seed?
|
|
||||||
if network == SettingsConstants.MAINNET:
|
|
||||||
network_path = "0'"
|
|
||||||
elif network == SettingsConstants.TESTNET:
|
|
||||||
network_path = "1'"
|
|
||||||
elif network == SettingsConstants.REGTEST:
|
|
||||||
# TODO: Is this right?
|
|
||||||
network_path = "1'"
|
|
||||||
else:
|
|
||||||
raise Exception("Unexpected network")
|
|
||||||
|
|
||||||
if wallet_type == SettingsConstants.SINGLE_SIG:
|
|
||||||
if script_type == SettingsConstants.NATIVE_SEGWIT:
|
|
||||||
return f"m/84'/{network_path}/0'"
|
|
||||||
elif script_type == SettingsConstants.NESTED_SEGWIT:
|
|
||||||
return f"m/49'/{network_path}/0'"
|
|
||||||
elif script_type == SettingsConstants.TAPROOT:
|
|
||||||
return f"m/86'/{network_path}/0'"
|
|
||||||
else:
|
|
||||||
raise Exception("Unexpected script type")
|
|
||||||
|
|
||||||
elif wallet_type == SettingsConstants.MULTISIG:
|
|
||||||
if script_type == SettingsConstants.NATIVE_SEGWIT:
|
|
||||||
return f"m/48'/{network_path}/0'/2'"
|
|
||||||
elif script_type == SettingsConstants.NESTED_SEGWIT:
|
|
||||||
return f"m/48'/{network_path}/0'/1'"
|
|
||||||
elif script_type == SettingsConstants.TAPROOT:
|
|
||||||
raise Exception("Taproot multisig/musig not yet supported")
|
|
||||||
else:
|
|
||||||
raise Exception("Unexpected script type")
|
|
||||||
else:
|
|
||||||
raise Exception("Unexpected wallet type") # checks that all inputs are from the same wallet
|
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_policy(scope, scriptpubkey, xpubs):
|
def _get_policy(scope, scriptpubkey, xpubs):
|
||||||
"""Parse scope and get policy"""
|
"""Parse scope and get policy"""
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from embit.networks import NETWORKS
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from seedsigner.models.settings import SettingsConstants
|
from seedsigner.models.settings import SettingsConstants
|
||||||
|
from seedsigner.helpers import embit_utils
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
@@ -108,11 +109,9 @@ class Seed:
|
|||||||
root = bip32.HDKey.from_seed(self.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(network)]["xprv"])
|
root = bip32.HDKey.from_seed(self.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(network)]["xprv"])
|
||||||
return hexlify(root.child(0).fingerprint).decode('utf-8')
|
return hexlify(root.child(0).fingerprint).decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
def get_xpub(self, wallet_path: str = '/', network: str = SettingsConstants.MAINNET):
|
def get_xpub(self, wallet_path: str = '/', network: str = SettingsConstants.MAINNET):
|
||||||
root = bip32.HDKey.from_seed(self.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(network)]["xprv"])
|
return embit_utils.get_xpub(seed_bytes=self.seed_bytes, derivation_path=wallet_path, embit_network=SettingsConstants.map_network_to_embit(network))
|
||||||
xprv = root.derive(wallet_path)
|
|
||||||
xpub = xprv.to_public()
|
|
||||||
return xpub
|
|
||||||
|
|
||||||
|
|
||||||
# Derives a BIP85 mnemonic (seed word) from the master seed words using embit functions
|
# Derives a BIP85 mnemonic (seed word) from the master seed words using embit functions
|
||||||
@@ -141,8 +140,9 @@ class Seed:
|
|||||||
|
|
||||||
# Return the derived BIP85 child mnemonic using the truncated derived entropy
|
# Return the derived BIP85 child mnemonic using the truncated derived entropy
|
||||||
return bip39.mnemonic_from_bytes(entropy[:width])
|
return bip39.mnemonic_from_bytes(entropy[:width])
|
||||||
|
|
||||||
|
|
||||||
### override operators
|
### override operators
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if isinstance(other, Seed):
|
if isinstance(other, Seed):
|
||||||
return self.seed_bytes == other.seed_bytes
|
return self.seed_bytes == other.seed_bytes
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ class PSBTSelectSeedView(View):
|
|||||||
seeds = self.controller.storage.seeds
|
seeds = self.controller.storage.seeds
|
||||||
|
|
||||||
SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE)
|
SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE)
|
||||||
ENTER_WORDS = "Enter 12/24 words"
|
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||||
|
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||||
button_data = []
|
button_data = []
|
||||||
for seed in seeds:
|
for seed in seeds:
|
||||||
button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
|
button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
|
||||||
@@ -45,7 +46,8 @@ class PSBTSelectSeedView(View):
|
|||||||
pass
|
pass
|
||||||
button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue"))
|
button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue"))
|
||||||
button_data.append(SCAN_SEED)
|
button_data.append(SCAN_SEED)
|
||||||
button_data.append(ENTER_WORDS)
|
button_data.append(TYPE_12WORD)
|
||||||
|
button_data.append(TYPE_24WORD)
|
||||||
|
|
||||||
selected_menu_num = ButtonListScreen(
|
selected_menu_num = ButtonListScreen(
|
||||||
title="Select Signer",
|
title="Select Signer",
|
||||||
@@ -68,8 +70,12 @@ class PSBTSelectSeedView(View):
|
|||||||
from seedsigner.views.scan_views import ScanView
|
from seedsigner.views.scan_views import ScanView
|
||||||
return Destination(ScanView)
|
return Destination(ScanView)
|
||||||
|
|
||||||
elif button_data[selected_menu_num] == ENTER_WORDS:
|
elif button_data[selected_menu_num] in [TYPE_12WORD, TYPE_24WORD]:
|
||||||
from seedsigner.views.seed_views import SeedMnemonicEntryView
|
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)
|
return Destination(SeedMnemonicEntryView)
|
||||||
|
|
||||||
|
|
||||||
@@ -78,18 +84,22 @@ class PSBTOverviewView(View):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
# The PSBTParser takes a while to read the PSBT. Run the loading screen while we
|
self.loading_screen = None
|
||||||
# wait.
|
|
||||||
self.loading_screen = LoadingScreenThread(text="Parsing PSBT...")
|
|
||||||
self.loading_screen.start()
|
|
||||||
|
|
||||||
if not self.controller.psbt_parser or self.controller.psbt_parser.seed != self.controller.psbt_seed:
|
if not self.controller.psbt_parser or self.controller.psbt_parser.seed != self.controller.psbt_seed:
|
||||||
# Must run the PSBTParser or re-parse
|
# The PSBTParser takes a while to read the PSBT. Run the loading screen while
|
||||||
self.controller.psbt_parser = PSBTParser(
|
# we wait.
|
||||||
self.controller.psbt,
|
self.loading_screen = LoadingScreenThread(text="Parsing PSBT...")
|
||||||
seed=self.controller.psbt_seed,
|
self.loading_screen.start()
|
||||||
network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)
|
try:
|
||||||
)
|
self.controller.psbt_parser = PSBTParser(
|
||||||
|
self.controller.psbt,
|
||||||
|
seed=self.controller.psbt_seed,
|
||||||
|
network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.loading_screen.stop()
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
@@ -126,7 +136,8 @@ class PSBTOverviewView(View):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Everything is set. Stop the loading screen
|
# Everything is set. Stop the loading screen
|
||||||
self.loading_screen.stop()
|
if self.loading_screen:
|
||||||
|
self.loading_screen.stop()
|
||||||
|
|
||||||
# Run the overview screen
|
# Run the overview screen
|
||||||
selected_menu_num = screen.display()
|
selected_menu_num = screen.display()
|
||||||
@@ -145,6 +156,8 @@ class PSBTOverviewView(View):
|
|||||||
else:
|
else:
|
||||||
return Destination(PSBTMathView)
|
return Destination(PSBTMathView)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PSBTUnsupportedScriptTypeWarningView(View):
|
class PSBTUnsupportedScriptTypeWarningView(View):
|
||||||
def run(self):
|
def run(self):
|
||||||
selected_menu_num = WarningScreen(
|
selected_menu_num = WarningScreen(
|
||||||
@@ -459,15 +472,10 @@ class PSBTFinalizeView(View):
|
|||||||
|
|
||||||
if selected_menu_num == 0:
|
if selected_menu_num == 0:
|
||||||
# Sign PSBT
|
# Sign PSBT
|
||||||
loading_screen = LoadingScreenThread(text="Signing PSBT...")
|
|
||||||
loading_screen.start()
|
|
||||||
|
|
||||||
sig_cnt = PSBTParser.sig_count(psbt)
|
sig_cnt = PSBTParser.sig_count(psbt)
|
||||||
psbt.sign_with(psbt_parser.root)
|
psbt.sign_with(psbt_parser.root)
|
||||||
trimmed_psbt = PSBTParser.trim(psbt)
|
trimmed_psbt = PSBTParser.trim(psbt)
|
||||||
|
|
||||||
loading_screen.stop()
|
|
||||||
|
|
||||||
if sig_cnt == PSBTParser.sig_count(trimmed_psbt):
|
if sig_cnt == PSBTParser.sig_count(trimmed_psbt):
|
||||||
# Signing failed / didn't do anything
|
# Signing failed / didn't do anything
|
||||||
# TODO: Reserved for Nick. Are there different failure scenarios that we can detect?
|
# TODO: Reserved for Nick. Are there different failure scenarios that we can detect?
|
||||||
@@ -476,44 +484,18 @@ class PSBTFinalizeView(View):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
self.controller.psbt = trimmed_psbt
|
self.controller.psbt = trimmed_psbt
|
||||||
|
return Destination(PSBTSignedQRDisplayView)
|
||||||
if len(self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)) == 1:
|
|
||||||
return Destination(PSBTSignedQRDisplayView, view_args={"coordinator": self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[0]})
|
|
||||||
else:
|
|
||||||
return Destination(PSBTSelectCoordinatorView)
|
|
||||||
|
|
||||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||||
return Destination(BackStackView)
|
return Destination(BackStackView)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PSBTSelectCoordinatorView(View):
|
|
||||||
def run(self):
|
|
||||||
button_data = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)
|
|
||||||
selected_menu_num = psbt_screens.PSBTSelectCoordinatorScreen(
|
|
||||||
button_data=button_data
|
|
||||||
).display()
|
|
||||||
|
|
||||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
|
||||||
return Destination(BackStackView)
|
|
||||||
|
|
||||||
return Destination(PSBTSignedQRDisplayView, view_args={"coordinator": button_data[selected_menu_num]})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PSBTSignedQRDisplayView(View):
|
class PSBTSignedQRDisplayView(View):
|
||||||
def __init__(self, coordinator: str):
|
|
||||||
super().__init__()
|
|
||||||
self.coordinator = coordinator
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
qr_psbt_type = QRType.PSBT__UR2
|
|
||||||
if self.coordinator == SettingsConstants.COORDINATOR__SPECTER_DESKTOP:
|
|
||||||
qr_psbt_type = QRType.PSBT__SPECTER
|
|
||||||
|
|
||||||
qr_encoder = EncodeQR(
|
qr_encoder = EncodeQR(
|
||||||
psbt=self.controller.psbt,
|
psbt=self.controller.psbt,
|
||||||
qr_type=qr_psbt_type,
|
qr_type=QRType.PSBT__UR2, # All coordinators (as of 2022-08) use this format
|
||||||
qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY),
|
qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY),
|
||||||
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE),
|
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ class ScanView(View):
|
|||||||
def run(self):
|
def run(self):
|
||||||
from seedsigner.gui.screens.scan_screens import ScanScreen
|
from seedsigner.gui.screens.scan_screens import ScanScreen
|
||||||
|
|
||||||
# Run the live preview and QR code capture process
|
|
||||||
# TODO: Does this belong in its own BaseThread?
|
|
||||||
wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
|
wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
|
||||||
self.decoder = DecodeQR(wordlist_language_code=wordlist_language_code)
|
self.decoder = DecodeQR(wordlist_language_code=wordlist_language_code)
|
||||||
screen = ScanScreen(decoder=self.decoder)
|
|
||||||
screen.display()
|
|
||||||
|
|
||||||
|
# Start the live preview and background QR reading
|
||||||
|
ScanScreen(decoder=self.decoder).display()
|
||||||
|
|
||||||
|
# Handle the results
|
||||||
if self.decoder.is_complete:
|
if self.decoder.is_complete:
|
||||||
if self.decoder.is_seed:
|
if self.decoder.is_seed:
|
||||||
seed_mnemonic = self.decoder.get_seed_phrase()
|
seed_mnemonic = self.decoder.get_seed_phrase()
|
||||||
|
|||||||
+138
-132
@@ -10,6 +10,7 @@ from typing import List
|
|||||||
|
|
||||||
from seedsigner.controller import Controller
|
from seedsigner.controller import Controller
|
||||||
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustomIconConstants
|
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustomIconConstants
|
||||||
|
from seedsigner.helpers import embit_utils
|
||||||
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen,
|
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen,
|
||||||
WarningScreen, DireWarningScreen, seed_screens)
|
WarningScreen, DireWarningScreen, seed_screens)
|
||||||
from seedsigner.gui.screens.screen import LargeIconStatusScreen, LoadingScreenThread, QRDisplayScreen
|
from seedsigner.gui.screens.screen import LargeIconStatusScreen, LoadingScreenThread, QRDisplayScreen
|
||||||
@@ -18,7 +19,7 @@ from seedsigner.models.encode_qr import EncodeQR
|
|||||||
from seedsigner.models.psbt_parser import PSBTParser
|
from seedsigner.models.psbt_parser import PSBTParser
|
||||||
from seedsigner.models.qr_type import QRType
|
from seedsigner.models.qr_type import QRType
|
||||||
from seedsigner.models.seed import InvalidSeedException, Seed
|
from seedsigner.models.seed import InvalidSeedException, Seed
|
||||||
from seedsigner.models.settings import SettingsConstants
|
from seedsigner.models.settings import Settings, SettingsConstants
|
||||||
from seedsigner.models.settings_definition import SettingsDefinition
|
from seedsigner.models.settings_definition import SettingsDefinition
|
||||||
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
|
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
|
||||||
from seedsigner.views.psbt_views import PSBTChangeDetailsView
|
from seedsigner.views.psbt_views import PSBTChangeDetailsView
|
||||||
@@ -73,13 +74,13 @@ class SeedsMenuView(View):
|
|||||||
class LoadSeedView(View):
|
class LoadSeedView(View):
|
||||||
def run(self):
|
def run(self):
|
||||||
SEED_QR = (" Scan a SeedQR", FontAwesomeIconConstants.QRCODE)
|
SEED_QR = (" Scan a SeedQR", FontAwesomeIconConstants.QRCODE)
|
||||||
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
|
|
||||||
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
|
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||||
|
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||||
CREATE = (" Create a seed", FontAwesomeIconConstants.PLUS)
|
CREATE = (" Create a seed", FontAwesomeIconConstants.PLUS)
|
||||||
button_data=[
|
button_data=[
|
||||||
SEED_QR,
|
SEED_QR,
|
||||||
TYPE_24WORD,
|
|
||||||
TYPE_12WORD,
|
TYPE_12WORD,
|
||||||
|
TYPE_24WORD,
|
||||||
CREATE,
|
CREATE,
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -96,14 +97,14 @@ class LoadSeedView(View):
|
|||||||
from .scan_views import ScanView
|
from .scan_views import ScanView
|
||||||
return Destination(ScanView)
|
return Destination(ScanView)
|
||||||
|
|
||||||
elif button_data[selected_menu_num] == TYPE_24WORD:
|
|
||||||
self.controller.storage.init_pending_mnemonic(num_words=24)
|
|
||||||
return Destination(SeedMnemonicEntryView)
|
|
||||||
|
|
||||||
elif button_data[selected_menu_num] == TYPE_12WORD:
|
elif button_data[selected_menu_num] == TYPE_12WORD:
|
||||||
self.controller.storage.init_pending_mnemonic(num_words=12)
|
self.controller.storage.init_pending_mnemonic(num_words=12)
|
||||||
return Destination(SeedMnemonicEntryView)
|
return Destination(SeedMnemonicEntryView)
|
||||||
|
|
||||||
|
elif button_data[selected_menu_num] == TYPE_24WORD:
|
||||||
|
self.controller.storage.init_pending_mnemonic(num_words=24)
|
||||||
|
return Destination(SeedMnemonicEntryView)
|
||||||
|
|
||||||
elif button_data[selected_menu_num] == CREATE:
|
elif button_data[selected_menu_num] == CREATE:
|
||||||
from .tools_views import ToolsMenuView
|
from .tools_views import ToolsMenuView
|
||||||
return Destination(ToolsMenuView)
|
return Destination(ToolsMenuView)
|
||||||
@@ -127,13 +128,14 @@ class SeedMnemonicEntryView(View):
|
|||||||
|
|
||||||
if ret == RET_CODE__BACK_BUTTON:
|
if ret == RET_CODE__BACK_BUTTON:
|
||||||
if self.cur_word_index > 0:
|
if self.cur_word_index > 0:
|
||||||
return Destination(
|
return Destination(BackStackView)
|
||||||
SeedMnemonicEntryView,
|
# return Destination(
|
||||||
view_args={
|
# SeedMnemonicEntryView,
|
||||||
"cur_word_index": self.cur_word_index - 1,
|
# view_args={
|
||||||
"is_calc_final_word": self.is_calc_final_word
|
# "cur_word_index": self.cur_word_index - 1,
|
||||||
}
|
# "is_calc_final_word": self.is_calc_final_word
|
||||||
)
|
# }
|
||||||
|
# )
|
||||||
else:
|
else:
|
||||||
self.controller.storage.discard_pending_mnemonic()
|
self.controller.storage.discard_pending_mnemonic()
|
||||||
return Destination(MainMenuView)
|
return Destination(MainMenuView)
|
||||||
@@ -142,17 +144,15 @@ class SeedMnemonicEntryView(View):
|
|||||||
self.controller.storage.update_pending_mnemonic(ret, self.cur_word_index)
|
self.controller.storage.update_pending_mnemonic(ret, self.cur_word_index)
|
||||||
|
|
||||||
if self.is_calc_final_word and self.cur_word_index == self.controller.storage.pending_mnemonic_length - 2:
|
if self.is_calc_final_word and self.cur_word_index == self.controller.storage.pending_mnemonic_length - 2:
|
||||||
# Time to calculate the last word
|
# Time to calculate the last word. User must decide how they want to specify
|
||||||
# TODO: Option to add missing entropy for the last word:
|
# the last bits of entropy for the final word.
|
||||||
# * 3 bits for a 24-word seed
|
from seedsigner.views.tools_views import ToolsCalcFinalWordFinalizePromptView
|
||||||
# * 7 bits for a 12-word seed
|
return Destination(ToolsCalcFinalWordFinalizePromptView)
|
||||||
from seedsigner.helpers import mnemonic_generation
|
|
||||||
|
if self.is_calc_final_word and self.cur_word_index == self.controller.storage.pending_mnemonic_length - 1:
|
||||||
|
# Time to calculate the last word. User must either select a final word to
|
||||||
|
# contribute entropy to the checksum word OR we assume 0 ("abandon").
|
||||||
from seedsigner.views.tools_views import ToolsCalcFinalWordShowFinalWordView
|
from seedsigner.views.tools_views import ToolsCalcFinalWordShowFinalWordView
|
||||||
full_mnemonic = mnemonic_generation.calculate_checksum(
|
|
||||||
self.controller.storage.pending_mnemonic[:-1], # Must omit the last word's empty value
|
|
||||||
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
|
|
||||||
)
|
|
||||||
self.controller.storage.update_pending_mnemonic(full_mnemonic[-1], self.cur_word_index+1)
|
|
||||||
return Destination(ToolsCalcFinalWordShowFinalWordView)
|
return Destination(ToolsCalcFinalWordShowFinalWordView)
|
||||||
|
|
||||||
if self.cur_word_index < self.controller.storage.pending_mnemonic_length - 1:
|
if self.cur_word_index < self.controller.storage.pending_mnemonic_length - 1:
|
||||||
@@ -349,12 +349,19 @@ class SeedOptionsView(View):
|
|||||||
REVIEW_PSBT = "Review PSBT"
|
REVIEW_PSBT = "Review PSBT"
|
||||||
VERIFY_ADDRESS = "Verify Addr"
|
VERIFY_ADDRESS = "Verify Addr"
|
||||||
EXPORT_XPUB = "Export Xpub"
|
EXPORT_XPUB = "Export Xpub"
|
||||||
|
EXPLORER = "Address Explorer"
|
||||||
BACKUP = ("Backup Seed", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)
|
BACKUP = ("Backup Seed", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)
|
||||||
BIP85_CHILD_SEED = "BIP-85 Child Seed"
|
BIP85_CHILD_SEED = "BIP-85 Child Seed"
|
||||||
DISCARD = ("Discard Seed", None, None, "red")
|
DISCARD = ("Discard Seed", None, None, "red")
|
||||||
|
|
||||||
button_data = []
|
button_data = []
|
||||||
|
|
||||||
|
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
|
||||||
|
# Jump straight back into the address explorer script type selection flow
|
||||||
|
# But do ont cancel the `resume_main_flow` as we'll still need that after
|
||||||
|
# derivation path is specified.
|
||||||
|
return Destination(SeedExportXpubScriptTypeView, view_args=dict(seed_num=self.seed_num, sig_type=SettingsConstants.SINGLE_SIG), skip_current_view=True)
|
||||||
|
|
||||||
if self.controller.unverified_address:
|
if self.controller.unverified_address:
|
||||||
if self.controller.resume_main_flow == Controller.FLOW__VERIFY_SINGLESIG_ADDR:
|
if self.controller.resume_main_flow == Controller.FLOW__VERIFY_SINGLESIG_ADDR:
|
||||||
# Jump straight back into the single sig addr verification flow
|
# Jump straight back into the single sig addr verification flow
|
||||||
@@ -364,7 +371,7 @@ class SeedOptionsView(View):
|
|||||||
addr = self.controller.unverified_address["address"][:7]
|
addr = self.controller.unverified_address["address"][:7]
|
||||||
VERIFY_ADDRESS += f" {addr}"
|
VERIFY_ADDRESS += f" {addr}"
|
||||||
button_data.append(VERIFY_ADDRESS)
|
button_data.append(VERIFY_ADDRESS)
|
||||||
|
|
||||||
if self.controller.psbt:
|
if self.controller.psbt:
|
||||||
if PSBTParser.has_matching_input_fingerprint(self.controller.psbt, self.seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)):
|
if PSBTParser.has_matching_input_fingerprint(self.controller.psbt, self.seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)):
|
||||||
if self.controller.resume_main_flow and self.controller.resume_main_flow == Controller.FLOW__PSBT:
|
if self.controller.resume_main_flow and self.controller.resume_main_flow == Controller.FLOW__PSBT:
|
||||||
@@ -383,6 +390,7 @@ class SeedOptionsView(View):
|
|||||||
if self.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED:
|
if self.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED:
|
||||||
button_data.append(EXPORT_XPUB)
|
button_data.append(EXPORT_XPUB)
|
||||||
|
|
||||||
|
button_data.append(EXPLORER)
|
||||||
button_data.append(BACKUP)
|
button_data.append(BACKUP)
|
||||||
|
|
||||||
if self.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__ENABLED:
|
if self.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__ENABLED:
|
||||||
@@ -393,7 +401,7 @@ class SeedOptionsView(View):
|
|||||||
selected_menu_num = seed_screens.SeedOptionsScreen(
|
selected_menu_num = seed_screens.SeedOptionsScreen(
|
||||||
button_data=button_data,
|
button_data=button_data,
|
||||||
fingerprint=self.seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)),
|
fingerprint=self.seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)),
|
||||||
has_passphrase=self.seed.passphrase is not None
|
has_passphrase=self.seed.passphrase is not None,
|
||||||
).display()
|
).display()
|
||||||
|
|
||||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||||
@@ -409,19 +417,23 @@ class SeedOptionsView(View):
|
|||||||
return Destination(ScanView)
|
return Destination(ScanView)
|
||||||
|
|
||||||
elif button_data[selected_menu_num] == VERIFY_ADDRESS:
|
elif button_data[selected_menu_num] == VERIFY_ADDRESS:
|
||||||
return Destination(SeedAddressVerificationView, view_args={"seed_num": self.seed_num})
|
return Destination(SeedAddressVerificationView, view_args=dict(seed_num=self.seed_num))
|
||||||
|
|
||||||
elif button_data[selected_menu_num] == EXPORT_XPUB:
|
elif button_data[selected_menu_num] == EXPORT_XPUB:
|
||||||
return Destination(SeedExportXpubSigTypeView, view_args={"seed_num": self.seed_num})
|
return Destination(SeedExportXpubSigTypeView, view_args=dict(seed_num=self.seed_num))
|
||||||
|
|
||||||
|
elif button_data[selected_menu_num] == EXPLORER:
|
||||||
|
self.controller.resume_main_flow = Controller.FLOW__ADDRESS_EXPLORER
|
||||||
|
return Destination(SeedExportXpubScriptTypeView, view_args=dict(seed_num=self.seed_num, sig_type=SettingsConstants.SINGLE_SIG))
|
||||||
|
|
||||||
elif button_data[selected_menu_num] == BACKUP:
|
elif button_data[selected_menu_num] == BACKUP:
|
||||||
return Destination(SeedBackupView, view_args={"seed_num": self.seed_num})
|
return Destination(SeedBackupView, view_args=dict(seed_num=self.seed_num))
|
||||||
|
|
||||||
elif button_data[selected_menu_num] == BIP85_CHILD_SEED:
|
elif button_data[selected_menu_num] == BIP85_CHILD_SEED:
|
||||||
return Destination(SeedBIP85ApplicationModeView, view_args={"seed_num": self.seed_num})
|
return Destination(SeedBIP85ApplicationModeView, view_args={"seed_num": self.seed_num})
|
||||||
|
|
||||||
elif button_data[selected_menu_num] == DISCARD:
|
elif button_data[selected_menu_num] == DISCARD:
|
||||||
return Destination(SeedDiscardView, view_args={"seed_num": self.seed_num})
|
return Destination(SeedDiscardView, view_args=dict(seed_num=self.seed_num))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -496,23 +508,37 @@ class SeedExportXpubScriptTypeView(View):
|
|||||||
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
from .tools_views import ToolsAddressExplorerAddressTypeView
|
||||||
args = {"seed_num": self.seed_num, "sig_type": self.sig_type}
|
args = {"seed_num": self.seed_num, "sig_type": self.sig_type}
|
||||||
if len(self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)) == 1:
|
if len(self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)) == 1:
|
||||||
# Nothing to select; skip this screen
|
# Nothing to select; skip this screen
|
||||||
args["script_type"] = self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)[0]
|
args["script_type"] = self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)[0]
|
||||||
return Destination(SeedExportXpubCoordinatorView, view_args=args, skip_current_view=True)
|
|
||||||
|
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
|
||||||
|
del args["sig_type"]
|
||||||
|
return Destination(ToolsAddressExplorerAddressTypeView, view_args=args, skip_current_view=True)
|
||||||
|
else:
|
||||||
|
return Destination(SeedExportXpubCoordinatorView, view_args=args, skip_current_view=True)
|
||||||
|
|
||||||
button_data = []
|
button_data = []
|
||||||
for script_type in self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES):
|
for script_type in self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES):
|
||||||
button_data.append(script_type)
|
button_data.append(script_type)
|
||||||
|
|
||||||
|
title = "Export Xpub"
|
||||||
|
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
|
||||||
|
title = "Address Explorer"
|
||||||
|
|
||||||
selected_menu_num = ButtonListScreen(
|
selected_menu_num = ButtonListScreen(
|
||||||
title="Export Xpub",
|
title=title,
|
||||||
is_button_text_centered=False,
|
is_button_text_centered=False,
|
||||||
button_data=button_data,
|
button_data=button_data,
|
||||||
|
is_bottom_list=True,
|
||||||
).display()
|
).display()
|
||||||
|
|
||||||
if selected_menu_num < len(button_data):
|
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||||
|
return Destination(BackStackView)
|
||||||
|
|
||||||
|
else:
|
||||||
script_types_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES)
|
script_types_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES)
|
||||||
selected_display_name = button_data[selected_menu_num]
|
selected_display_name = button_data[selected_menu_num]
|
||||||
args["script_type"] = script_types_settings_entry.get_selection_option_value_by_display_name(selected_display_name)
|
args["script_type"] = script_types_settings_entry.get_selection_option_value_by_display_name(selected_display_name)
|
||||||
@@ -520,10 +546,11 @@ class SeedExportXpubScriptTypeView(View):
|
|||||||
if args["script_type"] == SettingsConstants.CUSTOM_DERIVATION:
|
if args["script_type"] == SettingsConstants.CUSTOM_DERIVATION:
|
||||||
return Destination(SeedExportXpubCustomDerivationView, view_args=args)
|
return Destination(SeedExportXpubCustomDerivationView, view_args=args)
|
||||||
|
|
||||||
return Destination(SeedExportXpubCoordinatorView, view_args=args)
|
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
|
||||||
|
del args["sig_type"]
|
||||||
elif selected_menu_num == RET_CODE__BACK_BUTTON:
|
return Destination(ToolsAddressExplorerAddressTypeView, view_args=args)
|
||||||
return Destination(BackStackView)
|
else:
|
||||||
|
return Destination(SeedExportXpubCoordinatorView, view_args=args)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -543,9 +570,13 @@ class SeedExportXpubCustomDerivationView(View):
|
|||||||
if ret == RET_CODE__BACK_BUTTON:
|
if ret == RET_CODE__BACK_BUTTON:
|
||||||
return Destination(BackStackView)
|
return Destination(BackStackView)
|
||||||
|
|
||||||
# ret should be the custom derivation path; store it in Settings
|
# ret will be the custom derivation path
|
||||||
custom_derivation = ret
|
custom_derivation = ret
|
||||||
|
|
||||||
|
if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
|
||||||
|
from .tools_views import ToolsAddressExplorerAddressTypeView
|
||||||
|
return Destination(ToolsAddressExplorerAddressTypeView, view_args=dict(seed_num=self.seed_num, script_type=self.script_type, custom_derivation=custom_derivation))
|
||||||
|
|
||||||
return Destination(
|
return Destination(
|
||||||
SeedExportXpubCoordinatorView,
|
SeedExportXpubCoordinatorView,
|
||||||
view_args={
|
view_args={
|
||||||
@@ -652,61 +683,57 @@ class SeedExportXpubDetailsView(View):
|
|||||||
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
# The calc_derivation takes a few moments. Run the loading screen while we wait.
|
|
||||||
self.loading_screen = LoadingScreenThread(text="Generating xpub...")
|
|
||||||
self.loading_screen.start()
|
|
||||||
|
|
||||||
if self.script_type == SettingsConstants.CUSTOM_DERIVATION:
|
if self.script_type == SettingsConstants.CUSTOM_DERIVATION:
|
||||||
derivation_path = self.custom_derivation
|
derivation_path = self.custom_derivation
|
||||||
else:
|
else:
|
||||||
derivation_path = PSBTParser.calc_derivation(
|
derivation_path = embit_utils.get_standard_derivation_path(
|
||||||
network=self.settings.get_value(SettingsConstants.SETTING__NETWORK),
|
network=self.settings.get_value(SettingsConstants.SETTING__NETWORK),
|
||||||
wallet_type=self.sig_type,
|
wallet_type=self.sig_type,
|
||||||
script_type=self.script_type
|
script_type=self.script_type
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__ENABLED:
|
if self.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__DISABLED:
|
||||||
embit_network = NETWORKS[SettingsConstants.map_network_to_embit(self.settings.get_value(SettingsConstants.SETTING__NETWORK))]
|
# We're just skipping right past this screen
|
||||||
version = embit.bip32.detect_version(
|
selected_menu_num = 0
|
||||||
derivation_path,
|
|
||||||
default="xpub",
|
|
||||||
network=embit_network
|
|
||||||
)
|
|
||||||
|
|
||||||
root = embit.bip32.HDKey.from_seed(
|
else:
|
||||||
self.seed.seed_bytes,
|
# The derivation calc takes a few moments. Run the loading screen while we wait.
|
||||||
version=embit_network["xprv"]
|
self.loading_screen = LoadingScreenThread(text="Generating xpub...")
|
||||||
)
|
self.loading_screen.start()
|
||||||
|
|
||||||
fingerprint = hexlify(root.child(0).fingerprint).decode('utf-8')
|
try:
|
||||||
xprv = root.derive(derivation_path)
|
embit_network = NETWORKS[SettingsConstants.map_network_to_embit(self.settings.get_value(SettingsConstants.SETTING__NETWORK))]
|
||||||
xpub = xprv.to_public()
|
version = embit.bip32.detect_version(
|
||||||
xpub_base58 = xpub.to_string(version=version)
|
derivation_path,
|
||||||
|
default="xpub",
|
||||||
|
network=embit_network
|
||||||
|
)
|
||||||
|
root = embit.bip32.HDKey.from_seed(
|
||||||
|
self.seed.seed_bytes,
|
||||||
|
version=embit_network["xprv"]
|
||||||
|
)
|
||||||
|
fingerprint = hexlify(root.child(0).fingerprint).decode('utf-8')
|
||||||
|
xprv = root.derive(derivation_path)
|
||||||
|
xpub = xprv.to_public()
|
||||||
|
xpub_base58 = xpub.to_string(version=version)
|
||||||
|
|
||||||
screen = seed_screens.SeedExportXpubDetailsScreen(
|
finally:
|
||||||
|
self.loading_screen.stop()
|
||||||
|
|
||||||
|
selected_menu_num = seed_screens.SeedExportXpubDetailsScreen(
|
||||||
fingerprint=fingerprint,
|
fingerprint=fingerprint,
|
||||||
has_passphrase=self.seed.passphrase is not None,
|
has_passphrase=self.seed.passphrase is not None,
|
||||||
derivation_path=derivation_path,
|
derivation_path=derivation_path,
|
||||||
xpub=xpub_base58,
|
xpub=xpub_base58,
|
||||||
)
|
).display()
|
||||||
|
|
||||||
self.loading_screen.stop()
|
|
||||||
|
|
||||||
selected_menu_num = screen.display()
|
|
||||||
else:
|
|
||||||
self.loading_screen.stop()
|
|
||||||
selected_menu_num = 0
|
|
||||||
|
|
||||||
if selected_menu_num == 0:
|
if selected_menu_num == 0:
|
||||||
return Destination(
|
return Destination(
|
||||||
SeedExportXpubQRDisplayView,
|
SeedExportXpubQRDisplayView,
|
||||||
{
|
dict(seed_num=self.seed_num,
|
||||||
"seed_num": self.seed_num,
|
coordinator=self.coordinator,
|
||||||
"sig_type": self.sig_type,
|
derivation_path=derivation_path,
|
||||||
"script_type": self.script_type,
|
)
|
||||||
"coordinator": self.coordinator,
|
|
||||||
"derivation_path": derivation_path,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
elif selected_menu_num == RET_CODE__BACK_BUTTON:
|
elif selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||||
@@ -715,7 +742,7 @@ class SeedExportXpubDetailsView(View):
|
|||||||
|
|
||||||
|
|
||||||
class SeedExportXpubQRDisplayView(View):
|
class SeedExportXpubQRDisplayView(View):
|
||||||
def __init__(self, seed_num: int, sig_type: str, script_type: str, coordinator: str, derivation_path: str):
|
def __init__(self, seed_num: int, coordinator: str, derivation_path: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.seed = self.controller.get_seed(seed_num)
|
self.seed = self.controller.get_seed(seed_num)
|
||||||
|
|
||||||
@@ -1382,13 +1409,14 @@ class AddressVerificationStartView(View):
|
|||||||
destination = Destination(SeedSingleSigAddressVerificationSelectSeedView)
|
destination = Destination(SeedSingleSigAddressVerificationSelectSeedView)
|
||||||
|
|
||||||
elif self.controller.unverified_address["script_type"] == SettingsConstants.TAPROOT:
|
elif self.controller.unverified_address["script_type"] == SettingsConstants.TAPROOT:
|
||||||
destination = Destination(NotYetImplementedView)
|
# TODO: add Taproot support
|
||||||
|
return Destination(NotYetImplementedView)
|
||||||
|
|
||||||
elif self.controller.unverified_address["script_type"] == SettingsConstants.LEGACY_P2PKH:
|
elif self.controller.unverified_address["script_type"] == SettingsConstants.LEGACY_P2PKH:
|
||||||
# TODO: detect single sig vs multisig or have to prompt?
|
# TODO: detect single sig vs multisig or have to prompt?
|
||||||
destination = Destination(NotYetImplementedView)
|
return Destination(NotYetImplementedView)
|
||||||
|
|
||||||
derivation_path = PSBTParser.calc_derivation(
|
derivation_path = embit_utils.get_standard_derivation_path(
|
||||||
network=self.controller.unverified_address["network"],
|
network=self.controller.unverified_address["network"],
|
||||||
wallet_type=sig_type,
|
wallet_type=sig_type,
|
||||||
script_type=self.controller.unverified_address["script_type"]
|
script_type=self.controller.unverified_address["script_type"]
|
||||||
@@ -1432,7 +1460,7 @@ class AddressVerificationSigTypeView(View):
|
|||||||
destination = Destination(LoadMultisigWalletDescriptorView)
|
destination = Destination(LoadMultisigWalletDescriptorView)
|
||||||
|
|
||||||
self.controller.unverified_address["sig_type"] = sig_type
|
self.controller.unverified_address["sig_type"] = sig_type
|
||||||
derivation_path = PSBTParser.calc_derivation(
|
derivation_path = embit_utils.get_standard_derivation_path(
|
||||||
network=self.controller.unverified_address["network"],
|
network=self.controller.unverified_address["network"],
|
||||||
wallet_type=sig_type,
|
wallet_type=sig_type,
|
||||||
script_type=self.controller.unverified_address["script_type"]
|
script_type=self.controller.unverified_address["script_type"]
|
||||||
@@ -1448,7 +1476,8 @@ class SeedSingleSigAddressVerificationSelectSeedView(View):
|
|||||||
seeds = self.controller.storage.seeds
|
seeds = self.controller.storage.seeds
|
||||||
|
|
||||||
SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE)
|
SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE)
|
||||||
ENTER_WORDS = "Enter 12/24 words"
|
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||||
|
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||||
button_data = []
|
button_data = []
|
||||||
|
|
||||||
text = "Load the seed to verify"
|
text = "Load the seed to verify"
|
||||||
@@ -1464,7 +1493,8 @@ class SeedSingleSigAddressVerificationSelectSeedView(View):
|
|||||||
text = "Select seed to verify"
|
text = "Select seed to verify"
|
||||||
|
|
||||||
button_data.append(SCAN_SEED)
|
button_data.append(SCAN_SEED)
|
||||||
button_data.append(ENTER_WORDS)
|
button_data.append(TYPE_12WORD)
|
||||||
|
button_data.append(TYPE_24WORD)
|
||||||
|
|
||||||
selected_menu_num = seed_screens.SeedSingleSigAddressVerificationSelectSeedScreen(
|
selected_menu_num = seed_screens.SeedSingleSigAddressVerificationSelectSeedScreen(
|
||||||
title="Verify Address",
|
title="Verify Address",
|
||||||
@@ -1491,7 +1521,12 @@ class SeedSingleSigAddressVerificationSelectSeedView(View):
|
|||||||
from seedsigner.views.scan_views import ScanView
|
from seedsigner.views.scan_views import ScanView
|
||||||
return Destination(ScanView)
|
return Destination(ScanView)
|
||||||
|
|
||||||
elif button_data[selected_menu_num] == ENTER_WORDS:
|
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)
|
return Destination(SeedMnemonicEntryView)
|
||||||
|
|
||||||
|
|
||||||
@@ -1529,7 +1564,7 @@ class SeedAddressVerificationView(View):
|
|||||||
return Destination(NotYetImplementedView)
|
return Destination(NotYetImplementedView)
|
||||||
|
|
||||||
# TODO: This should be in `Seed` or `PSBT` utility class
|
# TODO: This should be in `Seed` or `PSBT` utility class
|
||||||
embit_network = NETWORKS[SettingsConstants.map_network_to_embit(self.network)]
|
embit_network = SettingsConstants.map_network_to_embit(self.network)
|
||||||
|
|
||||||
# The ThreadsafeCounter will be shared by the brute-force thread to keep track of
|
# The ThreadsafeCounter will be shared by the brute-force thread to keep track of
|
||||||
# its current addr index number and the Screen to display its progress and
|
# its current addr index number and the Screen to display its progress and
|
||||||
@@ -1546,7 +1581,7 @@ class SeedAddressVerificationView(View):
|
|||||||
seed=self.seed,
|
seed=self.seed,
|
||||||
descriptor=self.controller.multisig_wallet_descriptor,
|
descriptor=self.controller.multisig_wallet_descriptor,
|
||||||
script_type=self.script_type,
|
script_type=self.script_type,
|
||||||
network=embit_network,
|
embit_network=embit_network,
|
||||||
derivation_path=self.derivation_path,
|
derivation_path=self.derivation_path,
|
||||||
threadsafe_counter=self.threadsafe_counter,
|
threadsafe_counter=self.threadsafe_counter,
|
||||||
verified_index=self.verified_index,
|
verified_index=self.verified_index,
|
||||||
@@ -1611,13 +1646,15 @@ class SeedAddressVerificationView(View):
|
|||||||
# Halt the thread if the user gave up (will already be stopped if it verified the
|
# Halt the thread if the user gave up (will already be stopped if it verified the
|
||||||
# target addr).
|
# target addr).
|
||||||
self.addr_verification_thread.stop()
|
self.addr_verification_thread.stop()
|
||||||
|
while self.addr_verification_thread.is_alive():
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
return Destination(MainMenuView)
|
return Destination(MainMenuView)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BruteForceAddressVerificationThread(BaseThread):
|
class BruteForceAddressVerificationThread(BaseThread):
|
||||||
def __init__(self, address: str, seed: Seed, descriptor: Descriptor, script_type: str, network: str, derivation_path: str, threadsafe_counter: ThreadsafeCounter, verified_index: ThreadsafeCounter, verified_index_is_change: ThreadsafeCounter):
|
def __init__(self, address: str, seed: Seed, descriptor: Descriptor, script_type: str, embit_network: str, derivation_path: str, threadsafe_counter: ThreadsafeCounter, verified_index: ThreadsafeCounter, verified_index_is_change: ThreadsafeCounter):
|
||||||
"""
|
"""
|
||||||
Either seed or descriptor will be None
|
Either seed or descriptor will be None
|
||||||
"""
|
"""
|
||||||
@@ -1626,17 +1663,15 @@ class SeedAddressVerificationView(View):
|
|||||||
self.seed = seed
|
self.seed = seed
|
||||||
self.descriptor = descriptor
|
self.descriptor = descriptor
|
||||||
self.script_type = script_type
|
self.script_type = script_type
|
||||||
self.network = network
|
self.embit_network = embit_network
|
||||||
self.derivation_path = derivation_path
|
self.derivation_path = derivation_path
|
||||||
self.threadsafe_counter = threadsafe_counter
|
self.threadsafe_counter = threadsafe_counter
|
||||||
self.verified_index = verified_index
|
self.verified_index = verified_index
|
||||||
self.verified_index_is_change = verified_index_is_change
|
self.verified_index_is_change = verified_index_is_change
|
||||||
|
|
||||||
if self.seed:
|
if self.seed:
|
||||||
root = embit.bip32.HDKey.from_seed(self.seed.seed_bytes, version=network["xprv"])
|
self.xpub = self.seed.get_xpub(wallet_path=self.derivation_path, network=Settings.get_instance().get_value(SettingsConstants.SETTING__NETWORK))
|
||||||
xprv = root.derive(self.derivation_path)
|
|
||||||
self.xpub = xprv.to_public()
|
|
||||||
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while self.keep_running:
|
while self.keep_running:
|
||||||
@@ -1646,9 +1681,12 @@ class SeedAddressVerificationView(View):
|
|||||||
i = self.threadsafe_counter.cur_count
|
i = self.threadsafe_counter.cur_count
|
||||||
|
|
||||||
if self.descriptor:
|
if self.descriptor:
|
||||||
(receive_address, change_address) = self.derive_multisig(i)
|
receive_address = embit_utils.get_multisig_address(descriptor=self.descriptor, index=i, is_change=False, embit_network=self.embit_network)
|
||||||
|
change_address = embit_utils.get_multisig_address(descriptor=self.descriptor, index=i, is_change=True, embit_network=self.embit_network)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
(receive_address, change_address) = self.derive_single_sig(i)
|
receive_address = embit_utils.get_single_sig_address(xpub=self.xpub, script_type=self.script_type, index=i, is_change=False, embit_network=self.embit_network)
|
||||||
|
change_address = embit_utils.get_single_sig_address(xpub=self.xpub, script_type=self.script_type, index=i, is_change=True, embit_network=self.embit_network)
|
||||||
|
|
||||||
if self.address == receive_address:
|
if self.address == receive_address:
|
||||||
self.verified_index.set_value(i)
|
self.verified_index.set_value(i)
|
||||||
@@ -1664,47 +1702,8 @@ class SeedAddressVerificationView(View):
|
|||||||
|
|
||||||
# Increment our index counter
|
# Increment our index counter
|
||||||
self.threadsafe_counter.increment()
|
self.threadsafe_counter.increment()
|
||||||
|
|
||||||
|
|
||||||
def derive_single_sig(self, index):
|
|
||||||
r_pubkey = self.xpub.derive([0,index]).key
|
|
||||||
c_pubkey = self.xpub.derive([1,index]).key
|
|
||||||
|
|
||||||
receive_address = ""
|
|
||||||
change_address = ""
|
|
||||||
|
|
||||||
if self.script_type == SettingsConstants.NATIVE_SEGWIT:
|
|
||||||
receive_address = embit.script.p2wpkh(r_pubkey).address(network=self.network)
|
|
||||||
change_address = embit.script.p2wpkh(c_pubkey).address(network=self.network)
|
|
||||||
elif self.script_type == SettingsConstants.NESTED_SEGWIT:
|
|
||||||
receive_address = embit.script.p2sh(embit.script.p2wpkh(r_pubkey)).address(network=self.network)
|
|
||||||
change_address = embit.script.p2sh(embit.script.p2wpkh(c_pubkey)).address(network=self.network)
|
|
||||||
elif self.script_type == SettingsConstants.LEGACY_P2PKH:
|
|
||||||
receive_address = embit.script.p2pkh(r_pubkey).address(network=self.network)
|
|
||||||
change_address = embit.script.p2pkh(c_pubkey).address(network=self.network)
|
|
||||||
elif self.script_type == SettingsConstants.TAPROOT:
|
|
||||||
# TODO: Not yet implemented!
|
|
||||||
raise Exception("Taproot verification not yet implemented!")
|
|
||||||
|
|
||||||
return (receive_address, change_address)
|
|
||||||
|
|
||||||
|
|
||||||
def derive_multisig(self, index):
|
|
||||||
if self.script_type in [SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT]:
|
|
||||||
receive_address = self.descriptor.derive(index, branch_index=0).script_pubkey().address(network=self.network)
|
|
||||||
change_address = self.descriptor.derive(index, branch_index=1).script_pubkey().address(network=self.network)
|
|
||||||
|
|
||||||
elif self.script_type == SettingsConstants.LEGACY_P2PKH:
|
|
||||||
# TODO: Not yet implemented!
|
|
||||||
raise Exception("Taproot verification not yet implemented!")
|
|
||||||
|
|
||||||
elif self.script_type == SettingsConstants.TAPROOT:
|
|
||||||
# TODO: Not yet implemented!
|
|
||||||
raise Exception("Taproot verification not yet implemented!")
|
|
||||||
|
|
||||||
return (receive_address, change_address)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AddressVerificationSuccessView(View):
|
class AddressVerificationSuccessView(View):
|
||||||
def __init__(self, seed_num: int):
|
def __init__(self, seed_num: int):
|
||||||
@@ -1768,6 +1767,7 @@ class MultisigWalletDescriptorView(View):
|
|||||||
|
|
||||||
RETURN = "Return to PSBT"
|
RETURN = "Return to PSBT"
|
||||||
VERIFY = "Verify Addr"
|
VERIFY = "Verify Addr"
|
||||||
|
EXPLORER = "Address Explorer"
|
||||||
OK = "OK"
|
OK = "OK"
|
||||||
|
|
||||||
button_data = [OK]
|
button_data = [OK]
|
||||||
@@ -1777,6 +1777,8 @@ class MultisigWalletDescriptorView(View):
|
|||||||
elif self.controller.resume_main_flow == Controller.FLOW__VERIFY_MULTISIG_ADDR and self.controller.unverified_address:
|
elif self.controller.resume_main_flow == Controller.FLOW__VERIFY_MULTISIG_ADDR and self.controller.unverified_address:
|
||||||
VERIFY += f""" {self.controller.unverified_address["address"][:7]}"""
|
VERIFY += f""" {self.controller.unverified_address["address"][:7]}"""
|
||||||
button_data = [VERIFY]
|
button_data = [VERIFY]
|
||||||
|
elif self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER:
|
||||||
|
button_data = [EXPLORER]
|
||||||
|
|
||||||
selected_menu_num = seed_screens.MultisigWalletDescriptorScreen(
|
selected_menu_num = seed_screens.MultisigWalletDescriptorScreen(
|
||||||
policy=policy,
|
policy=policy,
|
||||||
@@ -1795,7 +1797,11 @@ class MultisigWalletDescriptorView(View):
|
|||||||
|
|
||||||
elif button_data[selected_menu_num] == VERIFY:
|
elif button_data[selected_menu_num] == VERIFY:
|
||||||
self.controller.resume_main_flow = None
|
self.controller.resume_main_flow = None
|
||||||
# TODO: Route properly when multisig brute-force addr verification is done
|
|
||||||
return Destination(SeedAddressVerificationView)
|
return Destination(SeedAddressVerificationView)
|
||||||
|
|
||||||
|
elif button_data[selected_menu_num] == EXPLORER:
|
||||||
|
from seedsigner.views.tools_views import ToolsAddressExplorerAddressTypeView
|
||||||
|
self.controller.resume_main_flow = None
|
||||||
|
return Destination(ToolsAddressExplorerAddressTypeView)
|
||||||
|
|
||||||
return Destination(MainMenuView)
|
return Destination(MainMenuView)
|
||||||
|
|||||||
@@ -2,17 +2,22 @@ import hashlib
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from embit.descriptor import Descriptor
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from PIL.ImageOps import autocontrast
|
from PIL.ImageOps import autocontrast
|
||||||
|
from seedsigner.controller import Controller
|
||||||
|
from seedsigner.gui.screens.screen import LoadingScreenThread, QRDisplayScreen
|
||||||
|
|
||||||
from seedsigner.hardware.camera import Camera
|
from seedsigner.hardware.camera import Camera
|
||||||
from seedsigner.gui.components import FontAwesomeIconConstants
|
from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants, SeedSignerCustomIconConstants
|
||||||
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen)
|
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen)
|
||||||
from seedsigner.gui.screens.tools_screens import ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsCalcFinalWordShowFinalWordScreen
|
from seedsigner.gui.screens.tools_screens import ToolsCalcFinalWordDoneScreen, ToolsCalcFinalWordFinalizePromptScreen, ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsAddressExplorerAddressTypeScreen
|
||||||
from seedsigner.helpers import mnemonic_generation
|
from seedsigner.helpers import embit_utils, mnemonic_generation
|
||||||
|
from seedsigner.models.encode_qr import EncodeQR
|
||||||
|
from seedsigner.models.qr_type import QRType
|
||||||
from seedsigner.models.seed import Seed
|
from seedsigner.models.seed import Seed
|
||||||
from seedsigner.models.settings_definition import SettingsConstants
|
from seedsigner.models.settings_definition import SettingsConstants
|
||||||
from seedsigner.views.seed_views import SeedDiscardView, SeedFinalizeView, SeedMnemonicEntryView, SeedWordsWarningView
|
from seedsigner.views.seed_views import SeedDiscardView, SeedFinalizeView, SeedMnemonicEntryView, SeedWordsWarningView, SeedExportXpubScriptTypeView
|
||||||
|
|
||||||
from .view import View, Destination, BackStackView
|
from .view import View, Destination, BackStackView
|
||||||
|
|
||||||
@@ -23,7 +28,8 @@ class ToolsMenuView(View):
|
|||||||
IMAGE = (" New seed", FontAwesomeIconConstants.CAMERA)
|
IMAGE = (" New seed", FontAwesomeIconConstants.CAMERA)
|
||||||
DICE = ("New seed", FontAwesomeIconConstants.DICE)
|
DICE = ("New seed", FontAwesomeIconConstants.DICE)
|
||||||
KEYBOARD = ("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD)
|
KEYBOARD = ("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD)
|
||||||
button_data = [IMAGE, DICE, KEYBOARD]
|
EXPLORER = "Address Explorer"
|
||||||
|
button_data = [IMAGE, DICE, KEYBOARD, EXPLORER]
|
||||||
screen = ButtonListScreen(
|
screen = ButtonListScreen(
|
||||||
title="Tools",
|
title="Tools",
|
||||||
is_button_text_centered=False,
|
is_button_text_centered=False,
|
||||||
@@ -43,6 +49,10 @@ class ToolsMenuView(View):
|
|||||||
elif button_data[selected_menu_num] == KEYBOARD:
|
elif button_data[selected_menu_num] == KEYBOARD:
|
||||||
return Destination(ToolsCalcFinalWordNumWordsView)
|
return Destination(ToolsCalcFinalWordNumWordsView)
|
||||||
|
|
||||||
|
elif button_data[selected_menu_num] == EXPLORER:
|
||||||
|
return Destination(ToolsAddressExplorerSelectSourceView)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"""****************************************************************************
|
"""****************************************************************************
|
||||||
@@ -240,15 +250,148 @@ class ToolsCalcFinalWordNumWordsView(View):
|
|||||||
|
|
||||||
elif button_data[selected_menu_num] == TWELVE:
|
elif button_data[selected_menu_num] == TWELVE:
|
||||||
self.controller.storage.init_pending_mnemonic(12)
|
self.controller.storage.init_pending_mnemonic(12)
|
||||||
return Destination(SeedMnemonicEntryView, view_args={"is_calc_final_word": True})
|
|
||||||
|
# return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
|
||||||
|
return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
|
||||||
|
|
||||||
elif button_data[selected_menu_num] == TWENTY_FOUR:
|
elif button_data[selected_menu_num] == TWENTY_FOUR:
|
||||||
self.controller.storage.init_pending_mnemonic(24)
|
self.controller.storage.init_pending_mnemonic(24)
|
||||||
return Destination(SeedMnemonicEntryView, view_args={"is_calc_final_word": True})
|
|
||||||
|
# return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
|
||||||
|
return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ToolsCalcFinalWordFinalizePromptView(View):
|
||||||
|
def run(self):
|
||||||
|
mnemonic = self.controller.storage.pending_mnemonic
|
||||||
|
mnemonic_length = len(mnemonic)
|
||||||
|
if mnemonic_length == 12:
|
||||||
|
num_entropy_bits = 7
|
||||||
|
else:
|
||||||
|
num_entropy_bits = 3
|
||||||
|
|
||||||
|
COIN_FLIPS = "Coin flip entropy"
|
||||||
|
SELECT_WORD = f"Word selection entropy"
|
||||||
|
ZEROS = "Finalize with zeros"
|
||||||
|
|
||||||
|
button_data = [COIN_FLIPS, SELECT_WORD, ZEROS]
|
||||||
|
selected_menu_num = ToolsCalcFinalWordFinalizePromptScreen(
|
||||||
|
mnemonic_length=mnemonic_length,
|
||||||
|
num_entropy_bits=num_entropy_bits,
|
||||||
|
button_data=button_data,
|
||||||
|
).display()
|
||||||
|
|
||||||
|
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||||
|
return Destination(BackStackView)
|
||||||
|
|
||||||
|
elif button_data[selected_menu_num] == COIN_FLIPS:
|
||||||
|
return Destination(ToolsCalcFinalWordCoinFlipsView)
|
||||||
|
|
||||||
|
elif button_data[selected_menu_num] == SELECT_WORD:
|
||||||
|
# Clear the final word slot, just in case we're returning via BACK button
|
||||||
|
self.controller.storage.update_pending_mnemonic(None, mnemonic_length - 1)
|
||||||
|
return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True, cur_word_index=mnemonic_length - 1))
|
||||||
|
|
||||||
|
elif button_data[selected_menu_num] == ZEROS:
|
||||||
|
# User skipped the option to select a final word to provide last bits of
|
||||||
|
# entropy. We'll insert all zeros and piggy-back on the coin flip attr
|
||||||
|
wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
|
||||||
|
self.controller.storage.update_pending_mnemonic(Seed.get_wordlist(wordlist_language_code)[0], mnemonic_length - 1)
|
||||||
|
return Destination(ToolsCalcFinalWordShowFinalWordView, view_args=dict(coin_flips="0" * num_entropy_bits))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ToolsCalcFinalWordCoinFlipsView(View):
|
||||||
|
def run(self):
|
||||||
|
mnemonic = self.controller.storage.pending_mnemonic
|
||||||
|
mnemonic_length = len(mnemonic)
|
||||||
|
|
||||||
|
if mnemonic_length == 12:
|
||||||
|
total_flips = 7
|
||||||
|
else:
|
||||||
|
total_flips = 3
|
||||||
|
|
||||||
|
ret_val = ToolsCoinFlipEntryScreen(
|
||||||
|
return_after_n_chars=total_flips,
|
||||||
|
).display()
|
||||||
|
|
||||||
|
if ret_val == RET_CODE__BACK_BUTTON:
|
||||||
|
return Destination(BackStackView)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(ret_val)
|
||||||
|
binary_string = ret_val + "0" * (11 - total_flips)
|
||||||
|
wordlist_index = int(binary_string, 2)
|
||||||
|
wordlist = Seed.get_wordlist(self.controller.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE))
|
||||||
|
word = wordlist[wordlist_index]
|
||||||
|
self.controller.storage.update_pending_mnemonic(word, mnemonic_length - 1)
|
||||||
|
|
||||||
|
return Destination(ToolsCalcFinalWordShowFinalWordView, view_args=dict(coin_flips=ret_val))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ToolsCalcFinalWordShowFinalWordView(View):
|
class ToolsCalcFinalWordShowFinalWordView(View):
|
||||||
|
def __init__(self, coin_flips=None):
|
||||||
|
super().__init__()
|
||||||
|
self.coin_flips = coin_flips
|
||||||
|
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
# Construct the actual final word. The user's selected_final_word
|
||||||
|
# contributes:
|
||||||
|
# * 3 bits to a 24-word seed (plus 8-bit checksum)
|
||||||
|
# * 7 bits to a 12-word seed (plus 4-bit checksum)
|
||||||
|
from seedsigner.helpers import mnemonic_generation
|
||||||
|
|
||||||
|
mnemonic = self.controller.storage.pending_mnemonic
|
||||||
|
mnemonic_length = len(mnemonic)
|
||||||
|
wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
|
||||||
|
wordlist = Seed.get_wordlist(wordlist_language_code)
|
||||||
|
|
||||||
|
final_mnemonic = mnemonic_generation.calculate_checksum(
|
||||||
|
mnemonic=self.controller.storage.pending_mnemonic,
|
||||||
|
wordlist_language_code=wordlist_language_code,
|
||||||
|
)
|
||||||
|
self.controller.storage.update_pending_mnemonic(final_mnemonic[-1], mnemonic_length - 1)
|
||||||
|
|
||||||
|
# Prep the user's selected word (if there was one) and the actual final word for
|
||||||
|
# the display.
|
||||||
|
if self.coin_flips:
|
||||||
|
selected_final_word = None
|
||||||
|
selected_final_bits = self.coin_flips
|
||||||
|
else:
|
||||||
|
# Convert the user's final word selection into its binary index equivalent
|
||||||
|
selected_final_word = mnemonic[-1]
|
||||||
|
selected_final_bits = format(wordlist.index(selected_final_word), '011b')
|
||||||
|
|
||||||
|
# And grab the actual final word's checksum bits
|
||||||
|
actual_final_word = self.controller.storage.pending_mnemonic[-1]
|
||||||
|
if mnemonic_length == 12:
|
||||||
|
checksum_bits = format(wordlist.index(actual_final_word), '011b')[-4:]
|
||||||
|
else:
|
||||||
|
checksum_bits = format(wordlist.index(actual_final_word), '011b')[-8:]
|
||||||
|
|
||||||
|
NEXT = "Next"
|
||||||
|
button_data = [NEXT]
|
||||||
|
selected_menu_num = ToolsCalcFinalWordScreen(
|
||||||
|
title="Final Word Calc",
|
||||||
|
button_data=button_data,
|
||||||
|
selected_final_word=selected_final_word,
|
||||||
|
selected_final_bits=selected_final_bits,
|
||||||
|
checksum_bits=checksum_bits,
|
||||||
|
actual_final_word=actual_final_word,
|
||||||
|
).display()
|
||||||
|
|
||||||
|
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||||
|
return Destination(BackStackView)
|
||||||
|
|
||||||
|
elif button_data[selected_menu_num] == NEXT:
|
||||||
|
return Destination(ToolsCalcFinalWordDoneView)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ToolsCalcFinalWordDoneView(View):
|
||||||
def run(self):
|
def run(self):
|
||||||
mnemonic = self.controller.storage.pending_mnemonic
|
mnemonic = self.controller.storage.pending_mnemonic
|
||||||
mnemonic_word_length = len(mnemonic)
|
mnemonic_word_length = len(mnemonic)
|
||||||
@@ -258,7 +401,7 @@ class ToolsCalcFinalWordShowFinalWordView(View):
|
|||||||
DISCARD = ("Discard", None, None, "red")
|
DISCARD = ("Discard", None, None, "red")
|
||||||
button_data = [LOAD, DISCARD]
|
button_data = [LOAD, DISCARD]
|
||||||
|
|
||||||
selected_menu_num = ToolsCalcFinalWordShowFinalWordScreen(
|
selected_menu_num = ToolsCalcFinalWordDoneScreen(
|
||||||
final_word=final_word,
|
final_word=final_word,
|
||||||
mnemonic_word_length=mnemonic_word_length,
|
mnemonic_word_length=mnemonic_word_length,
|
||||||
fingerprint=self.controller.storage.get_pending_mnemonic_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)),
|
fingerprint=self.controller.storage.get_pending_mnemonic_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)),
|
||||||
@@ -276,3 +419,256 @@ class ToolsCalcFinalWordShowFinalWordView(View):
|
|||||||
elif button_data[selected_menu_num] == DISCARD:
|
elif button_data[selected_menu_num] == DISCARD:
|
||||||
return Destination(SeedDiscardView)
|
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)
|
||||||
|
|||||||
@@ -43,6 +43,26 @@ def test_calculate_checksum():
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculate_checksum_with_default_final_word():
|
||||||
|
""" 11-word and 23-word mnemonics use word `0000` as a temp final word to complete
|
||||||
|
the mnemonic.
|
||||||
|
"""
|
||||||
|
partial_mnemonic = "crawl focus rescue cable view pledge rather dinner cousin unfair day"
|
||||||
|
mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
||||||
|
|
||||||
|
partial_mnemonic += " abandon"
|
||||||
|
mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
||||||
|
assert mnemonic1 == mnemonic2
|
||||||
|
|
||||||
|
partial_mnemonic = "bubble father debate ankle injury fence mesh evolve section wet coyote violin pyramid flower rent arrow round clutch myth safe base skin mobile"
|
||||||
|
mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
||||||
|
|
||||||
|
partial_mnemonic += " abandon"
|
||||||
|
mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
||||||
|
assert mnemonic1 == mnemonic2
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_verify_against_coldcard_sample():
|
def test_verify_against_coldcard_sample():
|
||||||
""" https://coldcard.com/docs/verifying-dice-roll-math """
|
""" https://coldcard.com/docs/verifying-dice-roll-math """
|
||||||
dice_rolls = "123456"
|
dice_rolls = "123456"
|
||||||
|
|||||||
Reference in New Issue
Block a user