mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
merging main upstream repo into fork
This commit is contained in:
+53
-27
@@ -1,12 +1,30 @@
|
|||||||
name: Build
|
name: Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
pull_request:
|
||||||
|
# Build on changes to this workflow files in PRs to test proposed changes
|
||||||
|
paths:
|
||||||
|
- '.github/workflows/build.yml'
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
os-ref:
|
||||||
|
description: The seedsigner-os ref (tag/branch/sha1) to use
|
||||||
|
default: main
|
||||||
|
required: true
|
||||||
|
|
||||||
|
# Increment this number as part of a PR to trigger an image build for the PR
|
||||||
|
# trigger = 0
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: build
|
name: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
# Prevent resource consuming cron triggered runs in forks
|
||||||
|
if: (!github.event.repository.fork || github.event_name == 'workflow_dispatch')
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -16,25 +34,41 @@ jobs:
|
|||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
repository: "seedsigner/seedsigner-os"
|
repository: "seedsigner/seedsigner-os"
|
||||||
|
# use the os-ref input parameter in case of workflow_dispatch or default to main in case of cron triggers
|
||||||
|
ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.os-ref || 'main' }}
|
||||||
submodules: true
|
submodules: true
|
||||||
path: "seedsigner-os"
|
path: "seedsigner-os"
|
||||||
|
# get full history + tags for "git describe"
|
||||||
- name: get seedsigner-os latest commit hash
|
fetch-depth: 0
|
||||||
id: get-seedsigner-os-hash
|
|
||||||
run: |
|
|
||||||
cd seedsigner-os
|
|
||||||
echo "builder_hash=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: checkout source
|
- name: checkout source
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
|
# ref defaults to repo default-branch=dev (cron) or SHA of event (workflow_dispatch)
|
||||||
path: "seedsigner-os/opt/rootfs-overlay/opt"
|
path: "seedsigner-os/opt/rootfs-overlay/opt"
|
||||||
|
# get full history + tags for "git describe"
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: get seedsigner latest commit hash
|
- name: Get and set meta data
|
||||||
id: get-seedsigner-hash
|
|
||||||
run: |
|
run: |
|
||||||
git init
|
# The builder_hash (seedsigner-os hash) for the cache action step key
|
||||||
echo "source_hash=$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_ENV
|
echo "builder_hash=$(git -C seedsigner-os rev-parse --short HEAD)"| tee -a $GITHUB_ENV
|
||||||
|
|
||||||
|
# Derive tag based versions, like 0.7.0-40-g0424967 (=$tag-$number-of-commits-since-tag-$short-sha1),
|
||||||
|
# or just e.g. 0.7.0, if we are exactly on a 0.7.0 tagged commit.
|
||||||
|
# --always to fall back to commit sha, if no tag present like in partial forks of the repo
|
||||||
|
os_version="$(git -C seedsigner-os describe --tags --always)"
|
||||||
|
source_version="$(git -C seedsigner-os/opt/rootfs-overlay/opt describe --tags --always)"
|
||||||
|
|
||||||
|
# Combine seedsigner and seedsigner-os version into one version string and squash the versions, if
|
||||||
|
# they are identical: So os_version=0.7.0 + source_version=0.7.0 combine to just only "0.7.0",
|
||||||
|
# whereas os_version=0.6.0-61-g9fafebe + source_version=0.7.0-40-g0424967 combine to "os0.6.0-61-g9fafebe_sw0.7.0-40-g0424967"
|
||||||
|
if [ "${os_version}" = "${source_version}" ]; then
|
||||||
|
# seedsigner + seedsigner_os have the same tag
|
||||||
|
echo "img_version=${source_version}"| tee -a $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "img_version=os${os_version}_sw${source_version}"| tee -a $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
- name: delete unnecessary files
|
- name: delete unnecessary files
|
||||||
run: |
|
run: |
|
||||||
@@ -44,11 +78,9 @@ jobs:
|
|||||||
ls -la src
|
ls -la src
|
||||||
|
|
||||||
- name: restore build cache
|
- name: restore build cache
|
||||||
id: build-cache-restore
|
uses: actions/cache@v3
|
||||||
uses: actions/cache/restore@v3
|
# Caching reduces the build time to ~50% (currently: ~30 mins instead of ~1 hour,
|
||||||
# Caching seedsigner-os/buildroot_dl is optional.
|
# while consuming ~850 MB storage space).
|
||||||
# Caching it can save a small amount of build time,
|
|
||||||
# but it will occupy a larger amount of storage space.
|
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.buildroot-ccache/
|
~/.buildroot-ccache/
|
||||||
@@ -62,16 +94,6 @@ jobs:
|
|||||||
cd seedsigner-os/opt
|
cd seedsigner-os/opt
|
||||||
./build.sh --${{ matrix.target }} --skip-repo --no-clean
|
./build.sh --${{ matrix.target }} --skip-repo --no-clean
|
||||||
|
|
||||||
- name: save build cache
|
|
||||||
id: build-cache-save
|
|
||||||
if: steps.build-cache-restore.outputs.cache-hit != 'true'
|
|
||||||
uses: actions/cache/save@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.buildroot-ccache/
|
|
||||||
seedsigner-os/buildroot_dl
|
|
||||||
key: build-cache-${{ matrix.target }}-${{ env.builder_hash }}
|
|
||||||
|
|
||||||
- name: list image (before rename)
|
- name: list image (before rename)
|
||||||
run: |
|
run: |
|
||||||
ls -la seedsigner-os/images
|
ls -la seedsigner-os/images
|
||||||
@@ -79,7 +101,7 @@ jobs:
|
|||||||
- name: rename image
|
- name: rename image
|
||||||
run: |
|
run: |
|
||||||
cd seedsigner-os/images
|
cd seedsigner-os/images
|
||||||
mv seedsigner_os*.img seedsigner_os.${{ env.source_hash }}.${{ matrix.target }}.img
|
mv seedsigner_os*.img seedsigner_os.${{ env.img_version }}.${{ matrix.target }}.img
|
||||||
|
|
||||||
- name: print sha256sum
|
- name: print sha256sum
|
||||||
run: |
|
run: |
|
||||||
@@ -96,6 +118,8 @@ jobs:
|
|||||||
name: seedsigner_os_images
|
name: seedsigner_os_images
|
||||||
path: "seedsigner-os/images/*.img"
|
path: "seedsigner-os/images/*.img"
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
# maximum 90 days retention
|
||||||
|
retention-days: 90
|
||||||
|
|
||||||
sha256sum:
|
sha256sum:
|
||||||
name: calculate sha256sum
|
name: calculate sha256sum
|
||||||
@@ -128,4 +152,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: seedsigner_os_images
|
name: seedsigner_os_images
|
||||||
path: "images/*.sha256"
|
path: "images/*.sha256"
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
# maximum 90 days retention
|
||||||
|
retention-days: 90
|
||||||
|
|||||||
@@ -10,13 +10,17 @@
|
|||||||
* [Verifying the Software](#verifying-the-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)
|
* [Build from Source](#build-from-source)
|
||||||
|
* [Developer Local Build Instructions](#developer-local-build-instructions)
|
||||||
|
|
||||||
|
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
# Project Summary
|
# Project Summary
|
||||||
|
|
||||||
|
[](https://github.com/SeedSigner/seedsigner/actions/workflows/tests.yml)
|
||||||
|
[](https://github.com/SeedSigner/seedsigner/actions/workflows/build.yml)
|
||||||
|
|
||||||
The goal of SeedSigner is to lower the cost and complexity of Bitcoin multi-signature wallet use. To accomplish this goal, SeedSigner offers anyone the opportunity to build a verifiably air-gapped, stateless Bitcoin signing device using inexpensive, publicly available hardware components (usually < $50). SeedSigner helps users save with Bitcoin by assisting with trustless private key generation and multisignature (aka "multisig") wallet setup, and helps users transact with Bitcoin via a secure, air-gapped QR-exchange signing model.
|
The goal of SeedSigner is to lower the cost and complexity of Bitcoin multi-signature wallet use. To accomplish this goal, SeedSigner offers anyone the opportunity to build a verifiably air-gapped, stateless Bitcoin signing device using inexpensive, publicly available hardware components (usually < $50). SeedSigner helps users save with Bitcoin by assisting with trustless private key generation and multisignature (aka "multisig") wallet setup, and helps users transact with Bitcoin via a secure, air-gapped QR-exchange signing model.
|
||||||
|
|
||||||
Additional information about the project can be found at [SeedSigner.com](https://seedsigner.com).
|
Additional information about the project can be found at [SeedSigner.com](https://seedsigner.com).
|
||||||
@@ -81,9 +85,11 @@ Notes:
|
|||||||
# Software Installation
|
# Software Installation
|
||||||
|
|
||||||
## A Special Note On Minimizing Trust
|
## A 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.
|
As is the nature of pre-packaged software downloads, downloading and using the prepared SeedSigner release images means implicitly placing trust in the people preparing those images; in our project the released 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 available. 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).
|
Starting with v0.7.0, the images distributed via GitHub are reproducible. This means you and others can verify the released images are byte-for-byte the same when built from source. You can contribute to this project by building from source and sharing the hash of the final images.
|
||||||
|
|
||||||
|
Instructions to build a SeedSigner OS image (using precisely the same process that is used to create the prepared release images) have been made available. 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](https://github.com/SeedSigner/seedsigner-os/blob/main/docs/building.md).
|
||||||
|
|
||||||
## Downloading the Software
|
## Downloading the Software
|
||||||
|
|
||||||
@@ -118,6 +124,8 @@ Once the files have all finished downloading, follow the steps below to verify t
|
|||||||
You can quickly verify that the software you just downloaded is both authentic and unaltered, by following these instructions.
|
You can quickly verify that the software you just downloaded is both authentic and unaltered, by following these instructions.
|
||||||
We assume you are running the commands from a computer where both [GPG](https://gnupg.org/download/index.html) and [shasum](https://command-not-found.com/shasum) are already installed, and that you also know [how to navigate on a terminal](https://terminalcheatsheet.com/guides/navigate-terminal).
|
We assume you are running the commands from a computer where both [GPG](https://gnupg.org/download/index.html) and [shasum](https://command-not-found.com/shasum) are already installed, and that you also know [how to navigate on a terminal](https://terminalcheatsheet.com/guides/navigate-terminal).
|
||||||
|
|
||||||
|
> You must run the following verification before opening or mounting the .img file.
|
||||||
|
> Some operating systems modify the file on mount causing verification to fail.
|
||||||
|
|
||||||
### Step 1. Verify that the signature (.sig) file is genuine:
|
### Step 1. Verify that the signature (.sig) file is genuine:
|
||||||
|
|
||||||
@@ -325,5 +333,8 @@ Letter templates(8.5in * 11in):
|
|||||||
* [29x29 - stores 24-word seeds ONLY as plaintext SeedQR format ONLY](docs/seed_qr/printable_templates/29x29_letter_trading_card_2sided.pdf)
|
* [29x29 - stores 24-word seeds ONLY as plaintext SeedQR format ONLY](docs/seed_qr/printable_templates/29x29_letter_trading_card_2sided.pdf)
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
# Manual Installation Instructions
|
# Build from Source
|
||||||
see the docs: [Manual Installation Instructions](docs/manual_installation.md)
|
See the [SeedSigner OS repo](https://github.com/SeedSigner/seedsigner-os/) for instructions.
|
||||||
|
|
||||||
|
# Developer Local Build Instructions
|
||||||
|
Raspberry Pi OS is commonly used for development. See the [Raspberry Pi OS Build Instructions](docs/raspberry_pi_os_build_instructions.md)
|
||||||
|
|||||||
@@ -205,3 +205,70 @@ We double-checked in two different web tools implementing different methods for
|
|||||||
|
|
||||||
So congratulations if the fingerprints, zpubs and addresses all match up in your example so you can be much more confident that nothing is wrong with your generated seed.
|
So congratulations if the fingerprints, zpubs and addresses all match up in your example so you can be much more confident that nothing is wrong with your generated seed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Command Line Tool
|
||||||
|
_(for more advanced/python-savvy users)_
|
||||||
|
|
||||||
|
Run the exact same SeedSigner mnemonic generation code from the command line to quickly test and externally verify the results.
|
||||||
|
|
||||||
|
Create a python virtualenv (out of the scope of this doc) and install dependencies:
|
||||||
|
```bash
|
||||||
|
pip3 install embit
|
||||||
|
|
||||||
|
# Install the main project code to make it importable
|
||||||
|
pip3 install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Then run the utility script with `-h` to view the usage instructions:
|
||||||
|
```bash
|
||||||
|
cd tools
|
||||||
|
python3 mnemonic.py -h
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Verify SeedSigner's dice rolls and coin flip entropy-to-mnemonic conversion via this tool.
|
||||||
|
|
||||||
|
Compare its results against iancoleman.io/bip39 and bitcoiner.guide/seed
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
# 50 dice rolls / 12-word mnemonic
|
||||||
|
python3 mnemonic.py dice 5624433434...
|
||||||
|
|
||||||
|
# 99 dice rolls / 24-word mnemonic
|
||||||
|
python3 mnemonic.py dice 6151463561...
|
||||||
|
|
||||||
|
# 50 dice rolls, entered as 0-5 / 12-word mnemonic
|
||||||
|
python3 mnemonic.py --zero-indexed-dice dice 5135535514...
|
||||||
|
|
||||||
|
# 128 coin flips / 12-word mnemonic
|
||||||
|
python3 mnemonic.py coins 1111100111...
|
||||||
|
|
||||||
|
# 256 coin flips / 24-word mnemonic
|
||||||
|
python mnemonic.py coins 0010111010...
|
||||||
|
|
||||||
|
# GENERATE 50 random dice rolls / 12-word mnemonic
|
||||||
|
python3 mnemonic.py dice rand12
|
||||||
|
|
||||||
|
# GENERATE 99 random dice rolls / 24-word mnemonic
|
||||||
|
python3 mnemonic.py dice rand24
|
||||||
|
|
||||||
|
# GENERATE 99 random dice rolls, entered as 0-5 / 24-word mnemonic
|
||||||
|
python3 mnemonic.py --zero-indexed-dice dice rand24
|
||||||
|
|
||||||
|
# GENERATE 128 random coin flips / 12-word mnemonic
|
||||||
|
python3 mnemonic.py coins rand12
|
||||||
|
|
||||||
|
# GENERATE 256 random coin flips / 24-word mnemonic
|
||||||
|
python3 mnemonic.py coins rand24
|
||||||
|
```
|
||||||
|
|
||||||
|
### How to get the same results in iancoleman.io
|
||||||
|
Always specify your expected length in the "Mnemonic Length" droplist (defaults to "Use Raw Entropy (3 words per 32 bits)").
|
||||||
|
|
||||||
|
Dice Rolls: Do NOT use the "Dice [1-6]" option; select "Base 10 [0-9]" or "Hex [0-9A-F]"
|
||||||
|
|
||||||
|
Zero-indexed dice rolls: Select "Base 6 [0-5]", "Base 10 [0-9]", or "Hex [0-9A-F]"
|
||||||
|
|
||||||
|
Coin Flips: Select "Binary [0-1]", "Base 6 [0-5]", "Base 10 [0-9]", or "Hex [0-9A-F]"
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
# Manual Installation Instructions
|
# Raspberry Pi OS Local Dev Build Instructions
|
||||||
|
|
||||||
|
Since v0.6.0, official releases use our custom [SeedSigner OS](https://github.com/SeedSigner/seedsigner-os/) However, project contributors looking to do rapid development cycles typically use the older Raspberry Pi OS that we had previously built on prior to v0.6.0. If you're here to set up your SeedSigner for local development, continue reading.
|
||||||
|
|
||||||
Begin by acquiring the latest 32-bit, Buster-based Raspberry Pi Lite operating system. This guide was tested using the version dated 2023-05-03; which can be found here:
|
Begin by acquiring the latest 32-bit, Buster-based Raspberry Pi Lite operating system. This guide was tested using the version dated 2023-05-03; which can be found here:
|
||||||
|
|
||||||
@@ -109,7 +111,7 @@ sudo apt install python3-apt -y
|
|||||||
Copy this entire box and run it as one command (~15 minutes on a Pi Zero 1.3):
|
Copy this entire box and run it as one command (~15 minutes on a Pi Zero 1.3):
|
||||||
```bash
|
```bash
|
||||||
sudo apt update && sudo apt install -y wiringpi python3-pip \
|
sudo apt update && sudo apt install -y wiringpi python3-pip \
|
||||||
python3-numpy python-pil libjpeg-dev zlib1g-dev libopenjp2-7 \
|
python-pil libjpeg-dev zlib1g-dev libopenjp2-7 \
|
||||||
git python3-opencv python3-picamera libatlas-base-dev qrencode
|
git python3-opencv python3-picamera libatlas-base-dev qrencode
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
picamera==1.13
|
picamera==1.13
|
||||||
|
# numpy = transitive picamera dependency
|
||||||
|
numpy==1.25.2
|
||||||
RPi.GPIO==0.7.0
|
RPi.GPIO==0.7.0
|
||||||
spidev==3.5
|
spidev==3.5
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
embit==0.7.0
|
embit==0.7.0
|
||||||
numpy==1.25.2
|
|
||||||
Pillow==9.4.0
|
Pillow==9.4.0
|
||||||
pyzbar @ git+https://github.com/seedsigner/pyzbar.git@c3c237821c6a20b17953efe59b90df0b514a1c03
|
pyzbar @ git+https://github.com/seedsigner/pyzbar.git@c3c237821c6a20b17953efe59b90df0b514a1c03
|
||||||
qrcode==7.3.1
|
qrcode==7.3.1
|
||||||
six==1.16.0
|
|
||||||
urtypes @ git+https://github.com/selfcustody/urtypes.git@7fb280eab3b3563dfc57d2733b0bf5cbc0a96a6a
|
urtypes @ git+https://github.com/selfcustody/urtypes.git@7fb280eab3b3563dfc57d2733b0bf5cbc0a96a6a
|
||||||
|
|||||||
@@ -398,6 +398,15 @@ class Controller(Singleton):
|
|||||||
# Start the screensaver, but it will block until it can acquire the Renderer.lock.
|
# Start the screensaver, but it will block until it can acquire the Renderer.lock.
|
||||||
self.screensaver.start()
|
self.screensaver.start()
|
||||||
print("Controller: Screensaver started")
|
print("Controller: Screensaver started")
|
||||||
|
|
||||||
|
|
||||||
|
def reset_screensaver_timeout(self):
|
||||||
|
"""
|
||||||
|
Reset the screensaver's timeout starting point to right now (i.e. make it think
|
||||||
|
that zero time has elapsed since the last user interaction).
|
||||||
|
"""
|
||||||
|
from seedsigner.hardware.buttons import HardwareButtons
|
||||||
|
HardwareButtons.get_instance().update_last_input_time()
|
||||||
|
|
||||||
|
|
||||||
def activate_toast(self, toast_manager_thread: BaseToastOverlayManagerThread):
|
def activate_toast(self, toast_manager_thread: BaseToastOverlayManagerThread):
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ class GUIConstants:
|
|||||||
BITCOIN_ORANGE = "#FF9416"
|
BITCOIN_ORANGE = "#FF9416"
|
||||||
TESTNET_COLOR = "#00F100"
|
TESTNET_COLOR = "#00F100"
|
||||||
REGTEST_COLOR = "#00CAF1"
|
REGTEST_COLOR = "#00CAF1"
|
||||||
|
GREEN_INDICATOR_COLOR = "#00FF00"
|
||||||
|
INACTIVE_COLOR = "#414141"
|
||||||
|
|
||||||
ICON_FONT_NAME__FONT_AWESOME = "Font_Awesome_6_Free-Solid-900"
|
ICON_FONT_NAME__FONT_AWESOME = "Font_Awesome_6_Free-Solid-900"
|
||||||
ICON_FONT_NAME__SEEDSIGNER = "seedsigner-icons"
|
ICON_FONT_NAME__SEEDSIGNER = "seedsigner-icons"
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ from seedsigner.gui import renderer
|
|||||||
from seedsigner.hardware.buttons import HardwareButtonsConstants
|
from seedsigner.hardware.buttons import HardwareButtonsConstants
|
||||||
from seedsigner.hardware.camera import Camera
|
from seedsigner.hardware.camera import Camera
|
||||||
from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus
|
from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus
|
||||||
from seedsigner.models.threads import BaseThread
|
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
|
||||||
|
|
||||||
from .screen import BaseScreen, ButtonListScreen
|
from .screen import BaseScreen
|
||||||
from ..components import GUIConstants, Fonts, TextArea
|
from ..components import GUIConstants, Fonts, SeedSignerIconConstants
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -47,6 +47,9 @@ class ScanScreen(BaseScreen):
|
|||||||
framerate: int = 6 # TODO: alternate optimization for Pi Zero 2W?
|
framerate: int = 6 # TODO: alternate optimization for Pi Zero 2W?
|
||||||
render_rect: tuple[int,int,int,int] = None
|
render_rect: tuple[int,int,int,int] = None
|
||||||
|
|
||||||
|
FRAME__ADDED_PART = 1
|
||||||
|
FRAME__REPEATED_PART = 2
|
||||||
|
FRAME__MISS = 3
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
from seedsigner.hardware.camera import Camera
|
from seedsigner.hardware.camera import Camera
|
||||||
@@ -58,17 +61,22 @@ class ScanScreen(BaseScreen):
|
|||||||
self.camera = Camera.get_instance()
|
self.camera = Camera.get_instance()
|
||||||
self.camera.start_video_stream_mode(resolution=self.resolution, framerate=self.framerate, format="rgb")
|
self.camera.start_video_stream_mode(resolution=self.resolution, framerate=self.framerate, format="rgb")
|
||||||
|
|
||||||
|
self.frames_decode_status = ThreadsafeCounter()
|
||||||
|
self.frames_decoded_counter = ThreadsafeCounter()
|
||||||
|
|
||||||
self.threads.append(ScanScreen.LivePreviewThread(
|
self.threads.append(ScanScreen.LivePreviewThread(
|
||||||
camera=self.camera,
|
camera=self.camera,
|
||||||
decoder=self.decoder,
|
decoder=self.decoder,
|
||||||
renderer=self.renderer,
|
renderer=self.renderer,
|
||||||
instructions_text=self.instructions_text,
|
instructions_text=self.instructions_text,
|
||||||
render_rect=self.render_rect,
|
render_rect=self.render_rect,
|
||||||
|
frame_decode_status=self.frames_decode_status,
|
||||||
|
frames_decoded_counter=self.frames_decoded_counter,
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
class LivePreviewThread(BaseThread):
|
class LivePreviewThread(BaseThread):
|
||||||
def __init__(self, camera: Camera, decoder: DecodeQR, renderer: renderer.Renderer, instructions_text: str, render_rect: tuple[int,int,int,int]):
|
def __init__(self, camera: Camera, decoder: DecodeQR, renderer: renderer.Renderer, instructions_text: str, render_rect: tuple[int,int,int,int], frame_decode_status: ThreadsafeCounter, frames_decoded_counter: ThreadsafeCounter):
|
||||||
self.camera = camera
|
self.camera = camera
|
||||||
self.decoder = decoder
|
self.decoder = decoder
|
||||||
self.renderer = renderer
|
self.renderer = renderer
|
||||||
@@ -77,6 +85,9 @@ class ScanScreen(BaseScreen):
|
|||||||
self.render_rect = render_rect
|
self.render_rect = render_rect
|
||||||
else:
|
else:
|
||||||
self.render_rect = (0, 0, self.renderer.canvas_width, self.renderer.canvas_height)
|
self.render_rect = (0, 0, self.renderer.canvas_width, self.renderer.canvas_height)
|
||||||
|
self.frame_decode_status = frame_decode_status
|
||||||
|
self.frames_decoded_counter = frames_decoded_counter
|
||||||
|
self.last_frame_decoded_count = self.frames_decoded_counter.cur_count
|
||||||
self.render_width = self.render_rect[2] - self.render_rect[0]
|
self.render_width = self.render_rect[2] - self.render_rect[0]
|
||||||
self.render_height = self.render_rect[3] - self.render_rect[1]
|
self.render_height = self.render_rect[3] - self.render_rect[1]
|
||||||
self.decoder_fps = "0.0"
|
self.decoder_fps = "0.0"
|
||||||
@@ -85,12 +96,15 @@ class ScanScreen(BaseScreen):
|
|||||||
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
from timeit import default_timer as timer
|
|
||||||
|
|
||||||
instructions_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE)
|
instructions_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE)
|
||||||
|
|
||||||
|
# pre-calculate how big the animated QR percent display can be
|
||||||
|
left, _, right, _ = instructions_font.getbbox("100%")
|
||||||
|
progress_text_width = right - left
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
num_frames = 0
|
num_frames = 0
|
||||||
|
debug = False
|
||||||
show_framerate = False # enable for debugging / testing
|
show_framerate = False # enable for debugging / testing
|
||||||
while self.keep_running:
|
while self.keep_running:
|
||||||
frame = self.camera.read_video_stream(as_image=True)
|
frame = self.camera.read_video_stream(as_image=True)
|
||||||
@@ -98,16 +112,22 @@ class ScanScreen(BaseScreen):
|
|||||||
num_frames += 1
|
num_frames += 1
|
||||||
cur_time = time.time()
|
cur_time = time.time()
|
||||||
cur_fps = num_frames / (cur_time - start_time)
|
cur_fps = num_frames / (cur_time - start_time)
|
||||||
if self.decoder and self.decoder.get_percent_complete() > 0 and self.decoder.is_psbt:
|
|
||||||
scan_text = str(self.decoder.get_percent_complete()) + "% Complete"
|
scan_text = None
|
||||||
if show_framerate:
|
progress_percentage = self.decoder.get_percent_complete()
|
||||||
scan_text += f" {cur_fps:0.2f} | {self.decoder_fps}"
|
if progress_percentage == 0:
|
||||||
else:
|
# We've just started scanning, no results yet
|
||||||
if show_framerate:
|
if show_framerate:
|
||||||
scan_text = f"{cur_fps:0.2f} | {self.decoder_fps}"
|
scan_text = f"{cur_fps:0.2f} | {self.decoder_fps}"
|
||||||
else:
|
else:
|
||||||
scan_text = self.instructions_text
|
scan_text = self.instructions_text
|
||||||
|
|
||||||
|
elif debug:
|
||||||
|
# Special debugging output for animated QRs
|
||||||
|
scan_text = f"{self.decoder.get_percent_complete()}% | {self.decoder.get_percent_complete(weight_mixed_frames=True)}% (new)"
|
||||||
|
if show_framerate:
|
||||||
|
scan_text += f" {cur_fps:0.2f} | {self.decoder_fps}"
|
||||||
|
|
||||||
with self.renderer.lock:
|
with self.renderer.lock:
|
||||||
if frame.width > self.render_width or frame.height > self.render_height:
|
if frame.width > self.render_width or frame.height > self.render_height:
|
||||||
frame = frame.resize(
|
frame = frame.resize(
|
||||||
@@ -115,14 +135,13 @@ class ScanScreen(BaseScreen):
|
|||||||
resample=Image.NEAREST # Use nearest neighbor for max speed
|
resample=Image.NEAREST # Use nearest neighbor for max speed
|
||||||
)
|
)
|
||||||
|
|
||||||
draw = ImageDraw.Draw(frame)
|
|
||||||
|
|
||||||
if scan_text:
|
if scan_text:
|
||||||
# Note: shadowed text (adding a 'stroke' outline) can
|
# Note: shadowed text (adding a 'stroke' outline) can
|
||||||
# significantly slow down the rendering.
|
# significantly slow down the rendering.
|
||||||
# Temp solution: render a slight 1px shadow behind the text
|
# Temp solution: render a slight 1px shadow behind the text
|
||||||
# TODO: Replace the instructions_text with a disappearing
|
# TODO: Replace the instructions_text with a disappearing
|
||||||
# toast/popup (see: QR Brightness UI)?
|
# toast/popup (see: QR Brightness UI)?
|
||||||
|
draw = ImageDraw.Draw(frame)
|
||||||
draw.text(xy=(
|
draw.text(xy=(
|
||||||
int(self.renderer.canvas_width/2 + 2),
|
int(self.renderer.canvas_width/2 + 2),
|
||||||
self.renderer.canvas_height - GUIConstants.EDGE_PADDING + 2
|
self.renderer.canvas_height - GUIConstants.EDGE_PADDING + 2
|
||||||
@@ -142,8 +161,82 @@ class ScanScreen(BaseScreen):
|
|||||||
font=instructions_font,
|
font=instructions_font,
|
||||||
anchor="ms")
|
anchor="ms")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Render the progress bar
|
||||||
|
rectangle = Image.new('RGBA', (self.renderer.canvas_width - 2*GUIConstants.EDGE_PADDING, GUIConstants.BUTTON_HEIGHT), (0, 0, 0, 0))
|
||||||
|
draw = ImageDraw.Draw(rectangle)
|
||||||
|
|
||||||
|
# Start with a background rounded rectangle, same dims as the buttons
|
||||||
|
overlay_color = (0, 0, 0, 191) # opacity ranges from 0-255
|
||||||
|
draw.rounded_rectangle(
|
||||||
|
(
|
||||||
|
(0, 0),
|
||||||
|
(rectangle.width, rectangle.height)
|
||||||
|
),
|
||||||
|
fill=overlay_color,
|
||||||
|
radius=8,
|
||||||
|
outline=overlay_color,
|
||||||
|
width=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
progress_bar_thickness = 4
|
||||||
|
progress_bar_width = rectangle.width - 2*GUIConstants.EDGE_PADDING - progress_text_width - int(GUIConstants.EDGE_PADDING/2)
|
||||||
|
progress_bar_xy = (
|
||||||
|
(GUIConstants.EDGE_PADDING, int((rectangle.height - progress_bar_thickness) / 2)),
|
||||||
|
(GUIConstants.EDGE_PADDING + progress_bar_width, int(rectangle.height + progress_bar_thickness) / 2)
|
||||||
|
)
|
||||||
|
draw.rounded_rectangle(
|
||||||
|
progress_bar_xy,
|
||||||
|
fill=GUIConstants.INACTIVE_COLOR,
|
||||||
|
radius=8
|
||||||
|
)
|
||||||
|
|
||||||
|
progress_percentage = self.decoder.get_percent_complete(weight_mixed_frames=True)
|
||||||
|
draw.rounded_rectangle(
|
||||||
|
(
|
||||||
|
progress_bar_xy[0],
|
||||||
|
(GUIConstants.EDGE_PADDING + int(progress_percentage * progress_bar_width / 100.0), progress_bar_xy[1][1])
|
||||||
|
),
|
||||||
|
fill=GUIConstants.GREEN_INDICATOR_COLOR,
|
||||||
|
radius=8
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
draw.text(
|
||||||
|
xy=(rectangle.width - GUIConstants.EDGE_PADDING, int(rectangle.height / 2)),
|
||||||
|
text=f"{progress_percentage}%",
|
||||||
|
# text=f"100%",
|
||||||
|
fill=GUIConstants.BODY_FONT_COLOR,
|
||||||
|
font=instructions_font,
|
||||||
|
anchor="rm", # right-justified, middle
|
||||||
|
)
|
||||||
|
|
||||||
|
frame.paste(rectangle, (GUIConstants.EDGE_PADDING, self.renderer.canvas_height - GUIConstants.EDGE_PADDING - rectangle.height), rectangle)
|
||||||
|
|
||||||
|
# Render the dot to indicate successful QR frame read
|
||||||
|
indicator_size = 10
|
||||||
|
self.last_frame_decoded_count = self.frames_decoded_counter.cur_count
|
||||||
|
status_color_map = {
|
||||||
|
ScanScreen.FRAME__ADDED_PART: GUIConstants.SUCCESS_COLOR,
|
||||||
|
ScanScreen.FRAME__REPEATED_PART: GUIConstants.INACTIVE_COLOR,
|
||||||
|
ScanScreen.FRAME__MISS: None,
|
||||||
|
}
|
||||||
|
status_color = status_color_map.get(self.frame_decode_status.cur_count)
|
||||||
|
if status_color:
|
||||||
|
# Good! Most recent frame successfully decoded.
|
||||||
|
# Draw the onscreen indicator dot
|
||||||
|
draw = ImageDraw.Draw(frame)
|
||||||
|
draw.ellipse(
|
||||||
|
(
|
||||||
|
(self.renderer.canvas_width - GUIConstants.EDGE_PADDING - indicator_size, self.renderer.canvas_height - GUIConstants.EDGE_PADDING - GUIConstants.BUTTON_HEIGHT - GUIConstants.COMPONENT_PADDING - indicator_size),
|
||||||
|
(self.renderer.canvas_width - GUIConstants.EDGE_PADDING, self.renderer.canvas_height - GUIConstants.EDGE_PADDING - GUIConstants.BUTTON_HEIGHT - GUIConstants.COMPONENT_PADDING)
|
||||||
|
),
|
||||||
|
fill=status_color,
|
||||||
|
outline="black",
|
||||||
|
width=1,
|
||||||
|
)
|
||||||
|
|
||||||
self.renderer.show_image(frame, show_direct=True)
|
self.renderer.show_image(frame, show_direct=True)
|
||||||
# print(f" {cur_fps:0.2f} | {self.decoder_fps}")
|
|
||||||
|
|
||||||
if self.camera._video_stream is None:
|
if self.camera._video_stream is None:
|
||||||
break
|
break
|
||||||
@@ -169,6 +262,21 @@ class ScanScreen(BaseScreen):
|
|||||||
if status in (DecodeQRStatus.COMPLETE, DecodeQRStatus.INVALID):
|
if status in (DecodeQRStatus.COMPLETE, DecodeQRStatus.INVALID):
|
||||||
self.camera.stop_video_stream_mode()
|
self.camera.stop_video_stream_mode()
|
||||||
break
|
break
|
||||||
|
|
||||||
|
self.frames_decoded_counter.increment()
|
||||||
|
# Notify the live preview thread how our most recent decode went
|
||||||
|
if status == DecodeQRStatus.FALSE:
|
||||||
|
# Did not find anything to decode in the current frame
|
||||||
|
self.frames_decode_status.set_value(self.FRAME__MISS)
|
||||||
|
|
||||||
|
else:
|
||||||
|
if status == DecodeQRStatus.PART_COMPLETE:
|
||||||
|
# We received a valid frame that added new data
|
||||||
|
self.frames_decode_status.set_value(self.FRAME__ADDED_PART)
|
||||||
|
|
||||||
|
elif status == DecodeQRStatus.PART_EXISTING:
|
||||||
|
# We received a valid frame, but we've already seen in
|
||||||
|
self.frames_decode_status.set_value(self.FRAME__REPEATED_PART)
|
||||||
|
|
||||||
if self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_RIGHT) or self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_LEFT):
|
if self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_RIGHT) or self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_LEFT):
|
||||||
self.camera.stop_video_stream_mode()
|
self.camera.stop_video_stream_mode()
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from seedsigner.gui.components import (GUIConstants,
|
|||||||
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.hardware.buttons import HardwareButtonsConstants, HardwareButtons
|
from seedsigner.hardware.buttons import HardwareButtonsConstants, HardwareButtons
|
||||||
|
from seedsigner.models.encode_qr import BaseQrEncoder
|
||||||
from seedsigner.models.settings import SettingsConstants
|
from seedsigner.models.settings import SettingsConstants
|
||||||
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
|
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
|
||||||
|
|
||||||
@@ -657,10 +658,10 @@ class LargeButtonScreen(BaseTopNavScreen):
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class QRDisplayScreen(BaseScreen):
|
class QRDisplayScreen(BaseScreen):
|
||||||
qr_encoder: 'EncodeQR' = None
|
qr_encoder: BaseQrEncoder = None
|
||||||
|
|
||||||
class QRDisplayThread(BaseThread):
|
class QRDisplayThread(BaseThread):
|
||||||
def __init__(self, qr_encoder: 'EncodeQR', qr_brightness: ThreadsafeCounter, renderer: Renderer,
|
def __init__(self, qr_encoder: BaseQrEncoder, qr_brightness: ThreadsafeCounter, renderer: Renderer,
|
||||||
tips_start_time: ThreadsafeCounter):
|
tips_start_time: ThreadsafeCounter):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.qr_encoder = qr_encoder
|
self.qr_encoder = qr_encoder
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class Camera(Singleton):
|
|||||||
return frame
|
return frame
|
||||||
else:
|
else:
|
||||||
if frame is not None:
|
if frame is not None:
|
||||||
return Image.fromarray(frame.astype('uint8'), 'RGB').rotate(90 + self._camera_rotation)
|
return Image.fromarray(frame.astype('uint8'), 'RGB').convert('RGBA').rotate(90 + self._camera_rotation)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,40 @@ import hashlib
|
|||||||
import unicodedata
|
import unicodedata
|
||||||
|
|
||||||
from embit import bip39
|
from embit import bip39
|
||||||
from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes
|
from seedsigner.models.settings_definition import SettingsConstants
|
||||||
from typing import List
|
from seedsigner.models.seed import Seed
|
||||||
|
|
||||||
|
"""
|
||||||
|
This is SeedSigner's internal mnemonic generation utility.
|
||||||
|
|
||||||
|
It can also be run as an independently-executable CLI to facilitate external
|
||||||
|
verification of SeedSigner's results for a given input entropy.
|
||||||
|
|
||||||
|
see: docs/dice_verification.md (the "Command Line Tool" section).
|
||||||
|
"""
|
||||||
|
|
||||||
|
DICE__NUM_ROLLS__12WORD = 50
|
||||||
|
DICE__NUM_ROLLS__24WORD = 99
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_checksum(mnemonic: list, wordlist_language_code: str) -> List[str]:
|
def calculate_checksum(mnemonic: list | str, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Provide 12- or 24-word mnemonic, returns complete mnemonic w/checksum as a list.
|
Provide 12- or 24-word mnemonic, returns complete mnemonic w/checksum as a list.
|
||||||
|
|
||||||
|
Mnemonic may be a list of words or a string of words separated by spaces or commas.
|
||||||
|
|
||||||
If 11- or 23-words are provided, append word `0000` to end of list as temp final
|
If 11- or 23-words are provided, append word `0000` to end of list as temp final
|
||||||
word.
|
word.
|
||||||
"""
|
"""
|
||||||
from seedsigner.models.seed import Seed
|
if type(mnemonic) == str:
|
||||||
|
import re
|
||||||
|
# split on commas or spaces
|
||||||
|
mnemonic = re.findall(r'[^,\s]+', mnemonic)
|
||||||
|
|
||||||
if len(mnemonic) in [11, 23]:
|
if len(mnemonic) in [11, 23]:
|
||||||
mnemonic.append(Seed.get_wordlist(wordlist_language_code)[0])
|
temp_final_word = Seed.get_wordlist(wordlist_language_code)[0]
|
||||||
|
mnemonic.append(temp_final_word)
|
||||||
|
|
||||||
if len(mnemonic) not in [12, 24]:
|
if len(mnemonic) not in [12, 24]:
|
||||||
raise Exception("Pass in a 12- or 24-word mnemonic")
|
raise Exception("Pass in a 12- or 24-word mnemonic")
|
||||||
@@ -37,28 +56,68 @@ def calculate_checksum(mnemonic: list, wordlist_language_code: str) -> List[str]
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def generate_mnemonic_from_bytes(entropy_bytes) -> List[str]:
|
def generate_mnemonic_from_bytes(entropy_bytes, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]:
|
||||||
return bip39.mnemonic_from_bytes(entropy_bytes).split()
|
return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=Seed.get_wordlist(wordlist_language_code)).split()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def generate_mnemonic_from_dice(roll_data: str) -> List[str]:
|
def generate_mnemonic_from_dice(roll_data: str, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]:
|
||||||
|
"""
|
||||||
|
Takes a string of 50 or 99 dice rolls and returns a 12- or 24-word mnemonic.
|
||||||
|
|
||||||
|
Uses the iancoleman.io/bip39 and bitcoiner.guide/seed "Base 10" or "Hex" mode approach:
|
||||||
|
* dice rolls are treated as string data.
|
||||||
|
* hashed via SHA256.
|
||||||
|
|
||||||
|
Important note: This method is NOT compatible with iancoleman's "Dice" mode.
|
||||||
|
"""
|
||||||
entropy_bytes = hashlib.sha256(roll_data.encode()).digest()
|
entropy_bytes = hashlib.sha256(roll_data.encode()).digest()
|
||||||
|
|
||||||
if len(roll_data) == 50:
|
if len(roll_data) == DICE__NUM_ROLLS__12WORD:
|
||||||
# 12-word mnemonic; only use 128bits / 16 bytes
|
# 12-word mnemonic; only use 128bits / 16 bytes
|
||||||
entropy_bytes = entropy_bytes[:16]
|
entropy_bytes = entropy_bytes[:16]
|
||||||
|
|
||||||
# Return as a list
|
# Return as a list
|
||||||
return bip39.mnemonic_from_bytes(entropy_bytes).split()
|
return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=Seed.get_wordlist(wordlist_language_code)).split()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def generate_mnemonic_from_coin_flips(coin_flips: str, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]:
|
||||||
|
"""
|
||||||
|
Takes a string of 128 or 256 0s and 1s and returns a 12- or 24-word mnemonic.
|
||||||
|
|
||||||
|
Uses the iancoleman.io/bip39 and bitcoiner.guide/seed "Binary" mode approach:
|
||||||
|
* binary digit stream is treated as string data.
|
||||||
|
* hashed via SHA256.
|
||||||
|
"""
|
||||||
|
entropy_bytes = hashlib.sha256(coin_flips.encode()).digest()
|
||||||
|
|
||||||
|
if len(coin_flips) == 128:
|
||||||
|
# 12-word mnemonic; only use 128bits / 16 bytes
|
||||||
|
entropy_bytes = entropy_bytes[:16]
|
||||||
|
|
||||||
|
# Return as a list
|
||||||
|
return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=Seed.get_wordlist(wordlist_language_code)).split()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_partial_final_word(coin_flips: str, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> str:
|
||||||
|
""" Look up the partial final word for the given coin flips.
|
||||||
|
7 coin flips: 0101010 + **** where the final 4 bits will be replaced with the checksum
|
||||||
|
3 coin flips: 010 + ******** where the final 8 bits will be replaced with the checksum
|
||||||
|
"""
|
||||||
|
binary_string = coin_flips + "0" * (11 - len(coin_flips))
|
||||||
|
wordlist_index = int(binary_string, 2)
|
||||||
|
|
||||||
|
return Seed.get_wordlist(wordlist_language_code)[wordlist_index]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Note: This currently isn't being used since we're now chaining hashed bytes for the
|
# Note: This currently isn't being used since we're now chaining hashed bytes for the
|
||||||
# image-based entropy and aren't just ingesting a single image.
|
# image-based entropy and aren't just ingesting a single image.
|
||||||
def generate_mnemonic_from_image(image) -> List[str]:
|
def generate_mnemonic_from_image(image, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]:
|
||||||
import hashlib
|
import hashlib
|
||||||
hash = hashlib.sha256(image.tobytes())
|
hash = hashlib.sha256(image.tobytes())
|
||||||
|
|
||||||
# Return as a list
|
# Return as a list
|
||||||
return bip39.mnemonic_from_bytes(hash.digest()).split()
|
return bip39.mnemonic_from_bytes(hash.digest(), wordlist=Seed.get_wordlist(wordlist_language_code)).split()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# Copyright © 2020 Foundation Devices, Inc.
|
# Copyright © 2020 Foundation Devices, Inc.
|
||||||
# Licensed under the "BSD-2-Clause Plus Patent License"
|
# Licensed under the "BSD-2-Clause Plus Patent License"
|
||||||
#
|
#
|
||||||
|
import time
|
||||||
from .fountain_utils import choose_fragments, contains, is_strict_subset, set_difference
|
from .fountain_utils import choose_fragments, contains, is_strict_subset, set_difference
|
||||||
from .utils import join_lists, join_bytes, crc32_int, xor_with, take_first
|
from .utils import join_lists, join_bytes, crc32_int, xor_with, take_first
|
||||||
|
|
||||||
@@ -71,13 +71,53 @@ class FountainDecoder:
|
|||||||
def result_error(self):
|
def result_error(self):
|
||||||
return self.result
|
return self.result
|
||||||
|
|
||||||
def estimated_percent_complete(self):
|
|
||||||
|
def estimated_percent_complete(self, weight_mixed_frames: bool = False):
|
||||||
|
"""
|
||||||
|
Weighted mixed frame method:
|
||||||
|
* counts completed frames
|
||||||
|
* counts each additional frame that is currently XORed in a mixed frame; its
|
||||||
|
score is weighted by the number of frames mixed together (1/num frames mixed).
|
||||||
|
"""
|
||||||
if self.is_complete():
|
if self.is_complete():
|
||||||
return 1
|
return 1
|
||||||
if self.expected_part_indexes == None:
|
if self.expected_part_indexes == None:
|
||||||
return 0
|
return 0
|
||||||
estimated_input_parts = self.expected_part_count() * 1.75
|
|
||||||
return min(0.99, self.processed_parts_count / estimated_input_parts)
|
if not weight_mixed_frames:
|
||||||
|
# Original estimation method
|
||||||
|
estimated_input_parts = self.expected_part_count() * 1.75
|
||||||
|
return min(0.99, self.processed_parts_count / estimated_input_parts)
|
||||||
|
else:
|
||||||
|
parts = self.expected_part_count() if self.expected_part_indexes != None else 'None'
|
||||||
|
mixed = []
|
||||||
|
mixed_index_scoring = {}
|
||||||
|
mixed_set = set()
|
||||||
|
for indexes, p in self.mixed_parts.items():
|
||||||
|
if not indexes:
|
||||||
|
continue
|
||||||
|
mixed.append(self.indexes_to_string(indexes))
|
||||||
|
mixed_set.update(indexes)
|
||||||
|
score = 1.0 / float(len(indexes))
|
||||||
|
for index in indexes:
|
||||||
|
if index not in mixed_index_scoring:
|
||||||
|
mixed_index_scoring[index] = 0.0
|
||||||
|
|
||||||
|
# sum up partial scores
|
||||||
|
mixed_index_scoring[index] += score
|
||||||
|
|
||||||
|
mixed_score = 0.0
|
||||||
|
for index, score in mixed_index_scoring.items():
|
||||||
|
# set a ceiling; don't let an index in a mixed/XOR frame
|
||||||
|
# achieve equal weight as a fully decoded frame. Also if
|
||||||
|
# the ceiling is too high, can potentially see your
|
||||||
|
# reported progress percentage DECREASE during a decode.
|
||||||
|
mixed_score += min(score, 0.75)
|
||||||
|
|
||||||
|
num_complete = len(self.received_part_indexes)
|
||||||
|
weighted_estimate = (num_complete + mixed_score) / float(parts)
|
||||||
|
return weighted_estimate
|
||||||
|
|
||||||
|
|
||||||
def receive_part(self, encoder_part):
|
def receive_part(self, encoder_part):
|
||||||
# Don't process the part if we're already done
|
# Don't process the part if we're already done
|
||||||
@@ -93,6 +133,9 @@ class FountainDecoder:
|
|||||||
self.last_part_indexes = p.indexes
|
self.last_part_indexes = p.indexes
|
||||||
self.enqueue(p)
|
self.enqueue(p)
|
||||||
|
|
||||||
|
num_complete = len(self.received_part_indexes)
|
||||||
|
num_mixed_frames = len(self.mixed_parts)
|
||||||
|
|
||||||
# Process the queue until we're done or the queue is empty
|
# Process the queue until we're done or the queue is empty
|
||||||
while not self.is_complete() and len(self.queued_parts) != 0:
|
while not self.is_complete() and len(self.queued_parts) != 0:
|
||||||
self.process_queue_item()
|
self.process_queue_item()
|
||||||
@@ -101,6 +144,12 @@ class FountainDecoder:
|
|||||||
self.processed_parts_count += 1
|
self.processed_parts_count += 1
|
||||||
|
|
||||||
# self.print_part_end()
|
# self.print_part_end()
|
||||||
|
# self.print_state()
|
||||||
|
|
||||||
|
if num_complete == len(self.received_part_indexes) and num_mixed_frames == len(self.mixed_parts):
|
||||||
|
# This part didn't add any new info
|
||||||
|
# print("No new data")
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -114,6 +163,7 @@ class FountainDecoder:
|
|||||||
self.queued_parts.append(p)
|
self.queued_parts.append(p)
|
||||||
|
|
||||||
def process_queue_item(self):
|
def process_queue_item(self):
|
||||||
|
start = time.time()
|
||||||
part = self.queued_parts.pop(0)
|
part = self.queued_parts.pop(0)
|
||||||
# self.print_part(part)
|
# self.print_part(part)
|
||||||
|
|
||||||
@@ -121,6 +171,8 @@ class FountainDecoder:
|
|||||||
self.process_simple_part(part)
|
self.process_simple_part(part)
|
||||||
else:
|
else:
|
||||||
self.process_mixed_part(part)
|
self.process_mixed_part(part)
|
||||||
|
|
||||||
|
# print(f"Queue processing: {int((time.time() - start)*1000.0)}ms")
|
||||||
# self.print_state()
|
# self.print_state()
|
||||||
|
|
||||||
def reduce_mixed_by(self, p):
|
def reduce_mixed_by(self, p):
|
||||||
@@ -141,6 +193,7 @@ class FountainDecoder:
|
|||||||
new_mixed[reduced_part.indexes] = reduced_part
|
new_mixed[reduced_part.indexes] = reduced_part
|
||||||
|
|
||||||
self.mixed_parts = new_mixed
|
self.mixed_parts = new_mixed
|
||||||
|
# print(self.mixed_parts.keys())
|
||||||
|
|
||||||
def reduce_part_by_part(self, a, b):
|
def reduce_part_by_part(self, a, b):
|
||||||
# If the fragments mixed into `b` are a strict (proper) subset of those in `a`...
|
# If the fragments mixed into `b` are a strict (proper) subset of those in `a`...
|
||||||
@@ -266,13 +319,23 @@ class FountainDecoder:
|
|||||||
print("processed: {}, expected: {}, received: {}, percent: {}%".format(self.processed_parts_count, expected, len(self.received_part_indexes), percent))
|
print("processed: {}, expected: {}, received: {}, percent: {}%".format(self.processed_parts_count, expected, len(self.received_part_indexes), percent))
|
||||||
|
|
||||||
def print_state(self):
|
def print_state(self):
|
||||||
parts = self.expected_part_count() if self.expected_part_indexes != None else 'None'
|
guesstimate = self.estimated_percent_complete(weight_mixed_frames=True)
|
||||||
received = self.indexes_to_string(self.received_part_indexes)
|
original_metric = self.estimated_percent_complete()
|
||||||
mixed = []
|
mixed = []
|
||||||
for indexes, p in self.mixed_parts.items():
|
mixed_set = set()
|
||||||
mixed.append(self.indexes_to_string(indexes))
|
try:
|
||||||
|
for indexes, p in self.mixed_parts.items():
|
||||||
mixed_s = "[{}]".format(', '.join(mixed))
|
if not indexes or len(indexes) == 0:
|
||||||
queued = len(self.queued_parts)
|
continue
|
||||||
res = self.result_description()
|
mixed.append(self.indexes_to_string(indexes))
|
||||||
print('parts: {}, received: {}, mixed: {}, queued: {}, result: {}'.format(parts, received, mixed_s, queued, res))
|
mixed_set.update(indexes)
|
||||||
|
|
||||||
|
num_complete = len(self.received_part_indexes)
|
||||||
|
|
||||||
|
mixed_s = "[{}]".format(', '.join(mixed))
|
||||||
|
queued = len(self.queued_parts)
|
||||||
|
print(f"{original_metric*100.0:5.1f}% | {guesstimate*100.0:5.1f}% | done: {num_complete:2d}, mixed: {len(mixed_set):2d}, queued: {queued}, frames: {self.processed_parts_count:2d} | {mixed_s}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ class FountainEncoder:
|
|||||||
self.fragment_len = FountainEncoder.find_nominal_fragment_length(self.message_len, min_fragment_len, max_fragment_len)
|
self.fragment_len = FountainEncoder.find_nominal_fragment_length(self.message_len, min_fragment_len, max_fragment_len)
|
||||||
self.fragments = FountainEncoder.partition_message(message, self.fragment_len)
|
self.fragments = FountainEncoder.partition_message(message, self.fragment_len)
|
||||||
self.seq_num = first_seq_num
|
self.seq_num = first_seq_num
|
||||||
|
self.current_part: Part = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_nominal_fragment_length(message_len, min_fragment_len, max_fragment_len):
|
def find_nominal_fragment_length(message_len, min_fragment_len, max_fragment_len):
|
||||||
@@ -143,7 +144,17 @@ class FountainEncoder:
|
|||||||
indexes = choose_fragments(self.seq_num, self.seq_len(), self.checksum)
|
indexes = choose_fragments(self.seq_num, self.seq_len(), self.checksum)
|
||||||
mixed = self.mix(indexes)
|
mixed = self.mix(indexes)
|
||||||
data = bytes(mixed)
|
data = bytes(mixed)
|
||||||
return Part(self.seq_num, self.seq_len(), self.message_len, self.checksum, data)
|
self.current_part = Part(self.seq_num, self.seq_len(), self.message_len, self.checksum, data)
|
||||||
|
return self.current_part
|
||||||
|
|
||||||
|
|
||||||
|
def restart(self):
|
||||||
|
"""
|
||||||
|
Restart from the beginning; each cycle's first n frames are full data frames
|
||||||
|
(not XOR composites).
|
||||||
|
"""
|
||||||
|
self.seq_num = 0
|
||||||
|
|
||||||
|
|
||||||
def mix(self, indexes):
|
def mix(self, indexes):
|
||||||
result = [0] * self.fragment_len
|
result = [0] * self.fragment_len
|
||||||
|
|||||||
@@ -153,8 +153,8 @@ class URDecoder:
|
|||||||
def processed_parts_count(self):
|
def processed_parts_count(self):
|
||||||
return self.fountain_decoder.processed_parts_count
|
return self.fountain_decoder.processed_parts_count
|
||||||
|
|
||||||
def estimated_percent_complete(self):
|
def estimated_percent_complete(self, weight_mixed_frames: bool = False):
|
||||||
return self.fountain_decoder.estimated_percent_complete()
|
return self.fountain_decoder.estimated_percent_complete(weight_mixed_frames=weight_mixed_frames)
|
||||||
|
|
||||||
def is_success(self):
|
def is_success(self):
|
||||||
result = self.result
|
result = self.result
|
||||||
|
|||||||
@@ -34,12 +34,26 @@ class UREncoder:
|
|||||||
def is_single_part(self):
|
def is_single_part(self):
|
||||||
return self.fountain_encoder.is_single_part()
|
return self.fountain_encoder.is_single_part()
|
||||||
|
|
||||||
def next_part(self):
|
def next_part(self) -> str:
|
||||||
part = self.fountain_encoder.next_part()
|
|
||||||
if self.is_single_part():
|
if self.is_single_part():
|
||||||
return UREncoder.encode(self.ur)
|
return UREncoder.encode(self.ur)
|
||||||
else:
|
else:
|
||||||
|
part = self.fountain_encoder.next_part()
|
||||||
return UREncoder.encode_part(self.ur.type, part)
|
return UREncoder.encode_part(self.ur.type, part)
|
||||||
|
|
||||||
|
def current_part(self) -> str:
|
||||||
|
if self.is_single_part():
|
||||||
|
return UREncoder.encode(self.ur)
|
||||||
|
else:
|
||||||
|
part = self.fountain_encoder.current_part
|
||||||
|
if not part:
|
||||||
|
part = self.fountain_encoder.next_part()
|
||||||
|
return UREncoder.encode_part(self.ur.type, part)
|
||||||
|
|
||||||
|
|
||||||
|
def restart(self):
|
||||||
|
self.fountain_encoder.restart()
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def encode_part(type, part):
|
def encode_part(type, part):
|
||||||
|
|||||||
@@ -120,11 +120,14 @@ class DecodeQR:
|
|||||||
qr_str = data
|
qr_str = data
|
||||||
|
|
||||||
if self.qr_type in [QRType.PSBT__UR2, QRType.OUTPUT__UR, QRType.ACCOUNT__UR, QRType.BYTES__UR]:
|
if self.qr_type in [QRType.PSBT__UR2, QRType.OUTPUT__UR, QRType.ACCOUNT__UR, QRType.BYTES__UR]:
|
||||||
self.decoder.receive_part(qr_str)
|
added_part = self.decoder.receive_part(qr_str)
|
||||||
if self.decoder.is_complete():
|
if self.decoder.is_complete():
|
||||||
self.complete = True
|
self.complete = True
|
||||||
return DecodeQRStatus.COMPLETE
|
return DecodeQRStatus.COMPLETE
|
||||||
return DecodeQRStatus.PART_COMPLETE # segment added to ur2 decoder
|
if added_part:
|
||||||
|
return DecodeQRStatus.PART_COMPLETE
|
||||||
|
else:
|
||||||
|
return DecodeQRStatus.PART_EXISTING
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# All other formats use the same method signature
|
# All other formats use the same method signature
|
||||||
@@ -219,12 +222,12 @@ class DecodeQR:
|
|||||||
return self.decoder.get_wallet_descriptor()
|
return self.decoder.get_wallet_descriptor()
|
||||||
|
|
||||||
|
|
||||||
def get_percent_complete(self) -> int:
|
def get_percent_complete(self, weight_mixed_frames: bool = False) -> int:
|
||||||
if not self.decoder:
|
if not self.decoder:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
if self.qr_type in [QRType.PSBT__UR2, QRType.OUTPUT__UR, QRType.ACCOUNT__UR, QRType.BYTES__UR]:
|
if self.qr_type in [QRType.PSBT__UR2, QRType.OUTPUT__UR, QRType.ACCOUNT__UR, QRType.BYTES__UR]:
|
||||||
return int(self.decoder.estimated_percent_complete() * 100)
|
return int(self.decoder.estimated_percent_complete(weight_mixed_frames=weight_mixed_frames) * 100)
|
||||||
|
|
||||||
elif self.qr_type in [QRType.PSBT__SPECTER]:
|
elif self.qr_type in [QRType.PSBT__SPECTER]:
|
||||||
if self.decoder.total_segments == None:
|
if self.decoder.total_segments == None:
|
||||||
@@ -261,6 +264,7 @@ class DecodeQR:
|
|||||||
QRType.PSBT__BASE43,
|
QRType.PSBT__BASE43,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_seed(self):
|
def is_seed(self):
|
||||||
return self.qr_type in [
|
return self.qr_type in [
|
||||||
@@ -305,7 +309,7 @@ class DecodeQR:
|
|||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def extract_qr_data(image, is_binary:bool = False) -> str:
|
def extract_qr_data(image, is_binary:bool = False) -> str | None:
|
||||||
if image is None:
|
if image is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -337,10 +341,10 @@ class DecodeQR:
|
|||||||
# PSBT
|
# PSBT
|
||||||
if re.search("^UR:CRYPTO-PSBT/", s, re.IGNORECASE):
|
if re.search("^UR:CRYPTO-PSBT/", s, re.IGNORECASE):
|
||||||
return QRType.PSBT__UR2
|
return QRType.PSBT__UR2
|
||||||
|
|
||||||
elif re.search("^UR:CRYPTO-OUTPUT/", s, re.IGNORECASE):
|
elif re.search("^UR:CRYPTO-OUTPUT/", s, re.IGNORECASE):
|
||||||
return QRType.OUTPUT__UR
|
return QRType.OUTPUT__UR
|
||||||
|
|
||||||
elif re.search("^UR:CRYPTO-ACCOUNT/", s, re.IGNORECASE):
|
elif re.search("^UR:CRYPTO-ACCOUNT/", s, re.IGNORECASE):
|
||||||
return QRType.ACCOUNT__UR
|
return QRType.ACCOUNT__UR
|
||||||
|
|
||||||
@@ -362,10 +366,10 @@ class DecodeQR:
|
|||||||
elif re.search(r'^\{\"label\".*\"descriptor\"\:.*', desc_str, re.IGNORECASE):
|
elif re.search(r'^\{\"label\".*\"descriptor\"\:.*', desc_str, re.IGNORECASE):
|
||||||
# if json starting with label and contains descriptor, assume specter wallet json
|
# if json starting with label and contains descriptor, assume specter wallet json
|
||||||
return QRType.WALLET__SPECTER
|
return QRType.WALLET__SPECTER
|
||||||
|
|
||||||
elif "multisig setup file" in s.lower():
|
elif "multisig setup file" in s.lower():
|
||||||
return QRType.WALLET__CONFIGFILE
|
return QRType.WALLET__CONFIGFILE
|
||||||
|
|
||||||
elif "sortedmulti" in s:
|
elif "sortedmulti" in s:
|
||||||
return QRType.WALLET__GENERIC
|
return QRType.WALLET__GENERIC
|
||||||
|
|
||||||
@@ -392,7 +396,7 @@ class DecodeQR:
|
|||||||
_4LETTER_WORDLIST = [word[:4].strip() for word in wordlist]
|
_4LETTER_WORDLIST = [word[:4].strip() for word in wordlist]
|
||||||
except:
|
except:
|
||||||
_4LETTER_WORDLIST = []
|
_4LETTER_WORDLIST = []
|
||||||
|
|
||||||
if all(x in wordlist for x in s.strip().split(" ")):
|
if all(x in wordlist for x in s.strip().split(" ")):
|
||||||
# checks if all words in list are in bip39 word list
|
# checks if all words in list are in bip39 word list
|
||||||
return QRType.SEED__MNEMONIC
|
return QRType.SEED__MNEMONIC
|
||||||
@@ -408,7 +412,7 @@ class DecodeQR:
|
|||||||
# Probably this isn't meant to be string data; check if it's valid byte data
|
# Probably this isn't meant to be string data; check if it's valid byte data
|
||||||
# below.
|
# below.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Is it byte data?
|
# Is it byte data?
|
||||||
# 32 bytes for 24-word CompactSeedQR; 16 bytes for 12-word CompactSeedQR
|
# 32 bytes for 24-word CompactSeedQR; 16 bytes for 12-word CompactSeedQR
|
||||||
if len(s) == 32 or len(s) == 16:
|
if len(s) == 32 or len(s) == 16:
|
||||||
|
|||||||
+217
-358
@@ -2,7 +2,7 @@ import math
|
|||||||
|
|
||||||
from embit import bip32
|
from embit import bip32
|
||||||
from embit.networks import NETWORKS
|
from embit.networks import NETWORKS
|
||||||
from binascii import b2a_base64, hexlify
|
from binascii import hexlify
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List
|
from typing import List
|
||||||
from embit import bip32
|
from embit import bip32
|
||||||
@@ -11,8 +11,6 @@ from embit.psbt import PSBT
|
|||||||
from seedsigner.helpers.ur2.ur_encoder import UREncoder
|
from seedsigner.helpers.ur2.ur_encoder import UREncoder
|
||||||
from seedsigner.helpers.ur2.ur import UR
|
from seedsigner.helpers.ur2.ur import UR
|
||||||
from seedsigner.helpers.qr import QR
|
from seedsigner.helpers.qr import QR
|
||||||
from seedsigner.helpers import embit_utils
|
|
||||||
from seedsigner.models.qr_type import QRType
|
|
||||||
from seedsigner.models.seed import Seed
|
from seedsigner.models.seed import Seed
|
||||||
from seedsigner.models.settings import SettingsConstants
|
from seedsigner.models.settings import SettingsConstants
|
||||||
|
|
||||||
@@ -21,149 +19,62 @@ from urtypes.crypto import Account, HDKey, Output, Keypath, PathComponent, SCRIP
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class EncodeQR:
|
class BaseQrEncoder:
|
||||||
"""
|
|
||||||
Encode psbt for displaying as qr image
|
|
||||||
"""
|
|
||||||
# TODO: Refactor so that this is a base class with implementation classes for each
|
|
||||||
# QR type. No reason exterior code can't directly instantiate the encoder it needs.
|
|
||||||
|
|
||||||
# Dataclass input vars on __init__()
|
|
||||||
psbt: PSBT = None
|
|
||||||
seed: Seed = None
|
|
||||||
derivation: str = None
|
|
||||||
network: str = SettingsConstants.MAINNET
|
|
||||||
qr_type: str = None
|
|
||||||
qr_density: str = SettingsConstants.DENSITY__MEDIUM
|
qr_density: str = SettingsConstants.DENSITY__MEDIUM
|
||||||
wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
|
|
||||||
bitcoin_address: str = None
|
|
||||||
signed_message: str = None
|
|
||||||
sig_type : str = SettingsConstants.SINGLE_SIG
|
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
self.qr = QR()
|
self.qr = QR()
|
||||||
|
|
||||||
if not self.qr_type:
|
|
||||||
raise Exception('qr_type is required')
|
|
||||||
|
|
||||||
if self.qr_density == None:
|
|
||||||
self.qr_density = SettingsConstants.DENSITY__MEDIUM
|
|
||||||
|
|
||||||
self.encoder: BaseQrEncoder = None
|
|
||||||
|
|
||||||
# PSBT formats
|
|
||||||
if self.qr_type == QRType.PSBT__SPECTER:
|
|
||||||
self.encoder = SpecterPsbtQrEncoder(psbt=self.psbt, qr_density=self.qr_density)
|
|
||||||
|
|
||||||
elif self.qr_type == QRType.PSBT__UR2:
|
|
||||||
self.encoder = UrPsbtQrEncoder(psbt=self.psbt, qr_density=self.qr_density)
|
|
||||||
|
|
||||||
# XPUB formats
|
|
||||||
elif self.qr_type == QRType.XPUB:
|
|
||||||
self.encoder = XpubQrEncoder(
|
|
||||||
seed=self.seed,
|
|
||||||
derivation=self.derivation,
|
|
||||||
network=self.network,
|
|
||||||
wordlist_language_code=self.wordlist_language_code,
|
|
||||||
sig_type=self.sig_type
|
|
||||||
)
|
|
||||||
|
|
||||||
elif self.qr_type == QRType.XPUB__UR:
|
|
||||||
self.encoder = UrXpubQrEncoder(
|
|
||||||
qr_density=self.qr_density,
|
|
||||||
seed=self.seed,
|
|
||||||
derivation=self.derivation,
|
|
||||||
network=self.network,
|
|
||||||
wordlist_language_code=self.wordlist_language_code,
|
|
||||||
sig_type=self.sig_type
|
|
||||||
)
|
|
||||||
|
|
||||||
elif self.qr_type == QRType.XPUB__SPECTER:
|
|
||||||
self.encoder = SpecterXPubQrEncoder(
|
|
||||||
qr_density=self.qr_density,
|
|
||||||
seed=self.seed,
|
|
||||||
derivation=self.derivation,
|
|
||||||
network=self.network,
|
|
||||||
wordlist_language_code=self.wordlist_language_code,
|
|
||||||
sig_type=self.sig_type
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# SeedQR formats
|
|
||||||
elif self.qr_type == QRType.SEED__SEEDQR:
|
|
||||||
self.encoder = SeedQrEncoder(seed_phrase=self.seed.mnemonic_list,
|
|
||||||
wordlist_language_code=self.wordlist_language_code)
|
|
||||||
|
|
||||||
elif self.qr_type == QRType.SEED__COMPACTSEEDQR:
|
|
||||||
self.encoder = CompactSeedQrEncoder(seed_phrase=self.seed.mnemonic_list,
|
|
||||||
wordlist_language_code=self.wordlist_language_code)
|
|
||||||
|
|
||||||
# Misc formats
|
|
||||||
elif self.qr_type == QRType.BITCOIN_ADDRESS:
|
|
||||||
self.encoder = BitcoinAddressEncoder(address=self.bitcoin_address)
|
|
||||||
|
|
||||||
elif self.qr_type == QRType.SIGN_MESSAGE:
|
|
||||||
self.encoder = SignedMessageEncoder(signed_message=self.signed_message)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise Exception('QR Type not supported')
|
|
||||||
|
|
||||||
|
|
||||||
def total_parts(self) -> int:
|
|
||||||
return self.encoder.seq_len()
|
|
||||||
|
|
||||||
|
|
||||||
def next_part(self):
|
|
||||||
return self.encoder.next_part()
|
|
||||||
|
|
||||||
|
|
||||||
def part_to_image(self, part, width=240, height=240, border=3):
|
|
||||||
return self.qr.qrimage_io(part, width, height, border)
|
|
||||||
|
|
||||||
|
|
||||||
def next_part_image(self, width=240, height=240, border=3, background_color="bdbdbd"):
|
|
||||||
part = self.next_part()
|
|
||||||
if self.qr_type == QRType.SEED__SEEDQR:
|
|
||||||
return self.qr.qrimage(part, width, height, border)
|
|
||||||
else:
|
|
||||||
return self.qr.qrimage_io(part, width, height, border, background_color=background_color)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: Make these properties?
|
|
||||||
def is_complete(self):
|
|
||||||
return self.encoder.is_complete
|
|
||||||
|
|
||||||
|
|
||||||
def get_qr_density(self):
|
|
||||||
return self.qr_density
|
|
||||||
|
|
||||||
|
|
||||||
def get_qr_type(self):
|
|
||||||
return self.qr_type
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BaseQrEncoder:
|
|
||||||
def seq_len(self):
|
|
||||||
raise Exception("Not implemented in child class")
|
|
||||||
|
|
||||||
def next_part(self) -> str:
|
|
||||||
raise Exception("Not implemented in child class")
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_complete(self):
|
def is_complete(self):
|
||||||
raise Exception("Not implemented in child class")
|
raise Exception("Not implemented in child class")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def qr_max_fragment_size(self):
|
||||||
|
raise Exception("Not implemented in child class")
|
||||||
|
|
||||||
|
def seq_len(self):
|
||||||
|
raise Exception("Not implemented in child class")
|
||||||
|
|
||||||
|
def next_part(self) -> str:
|
||||||
|
raise Exception("Not implemented in child class")
|
||||||
|
|
||||||
|
def cur_part(self) -> str:
|
||||||
|
raise Exception("Not implemented in child class")
|
||||||
|
|
||||||
|
def restart(self):
|
||||||
|
# only used by animated QR encoders
|
||||||
|
pass
|
||||||
|
|
||||||
def _create_parts(self):
|
def _create_parts(self):
|
||||||
raise Exception("Not implemented in child class")
|
raise Exception("Not implemented in child class")
|
||||||
|
|
||||||
|
|
||||||
|
def part_to_image(self, part, width, height, border: int = 3, background_color: str = "ffffff"):
|
||||||
|
return self.qr.qrimage_io(part, width, height, border, background_color=background_color)
|
||||||
|
|
||||||
|
|
||||||
|
def next_part_image(self, width=240, height=240, border=3, background_color="bdbdbd"):
|
||||||
|
part = self.next_part()
|
||||||
|
return self.part_to_image(part, width, height, border, background_color=background_color)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""**************************************************************************************
|
||||||
|
STATIC QR encoders
|
||||||
|
**************************************************************************************"""
|
||||||
|
@dataclass
|
||||||
class BaseStaticQrEncoder(BaseQrEncoder):
|
class BaseStaticQrEncoder(BaseQrEncoder):
|
||||||
def seq_len(self):
|
def seq_len(self):
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
def cur_part(self) -> str:
|
||||||
|
""" static QRs only have a single part, which `next_part` always returns """
|
||||||
|
return self.next_part()
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -172,86 +83,125 @@ class BaseStaticQrEncoder(BaseQrEncoder):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BasePsbtQrEncoder(BaseQrEncoder):
|
@dataclass
|
||||||
def __init__(self, psbt: PSBT):
|
class SeedQrEncoder(BaseStaticQrEncoder):
|
||||||
self.psbt = psbt
|
seed : Seed = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.wordlist = Seed.get_wordlist(self.seed.wordlist_language_code)
|
||||||
|
super().__post_init__()
|
||||||
|
|
||||||
|
self.data = ""
|
||||||
|
# Output as Numeric data format
|
||||||
|
for word in self.seed.mnemonic_list:
|
||||||
|
index = self.wordlist.index(word)
|
||||||
|
self.data += str("%04d" % index)
|
||||||
|
|
||||||
|
|
||||||
|
def next_part(self):
|
||||||
|
return self.data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class UrPsbtQrEncoder(BasePsbtQrEncoder):
|
@dataclass
|
||||||
def __init__(self, psbt, qr_density):
|
class CompactSeedQrEncoder(SeedQrEncoder):
|
||||||
super().__init__(psbt)
|
def next_part(self):
|
||||||
self.qr_max_fragment_size = 20
|
# Output as binary data format
|
||||||
|
binary_str = ""
|
||||||
|
for word in self.seed.mnemonic_list:
|
||||||
|
index = self.wordlist.index(word)
|
||||||
|
|
||||||
|
# Convert index to binary, strip out '0b' prefix; zero-pad to 11 bits
|
||||||
|
binary_str += bin(index).split('b')[1].zfill(11)
|
||||||
|
|
||||||
|
# We can exclude the checksum bits at the end
|
||||||
|
if len(self.seed.mnemonic_list) == 24:
|
||||||
|
# 8 checksum bits in a 24-word seed
|
||||||
|
binary_str = binary_str[:-8]
|
||||||
|
|
||||||
|
elif len(self.seed.mnemonic_list) == 12:
|
||||||
|
# 4 checksum bits in a 12-word seed
|
||||||
|
binary_str = binary_str[:-4]
|
||||||
|
|
||||||
|
# Now convert to bytes, 8 bits at a time
|
||||||
|
as_bytes = bytearray()
|
||||||
|
for i in range(0, math.ceil(len(binary_str) / 8)):
|
||||||
|
# int conversion reads byte data as a string prefixed with '0b'
|
||||||
|
as_bytes.append(int('0b' + binary_str[i*8:(i+1)*8], 2))
|
||||||
|
|
||||||
qr_ur_bytes = UR("crypto-psbt", UR_PSBT(self.psbt.serialize()).to_cbor())
|
# Must return data as `bytes` for `qrcode` to properly recognize it as byte data
|
||||||
|
return bytes(as_bytes)
|
||||||
if qr_density == SettingsConstants.DENSITY__LOW:
|
|
||||||
self.qr_max_fragment_size = 10
|
|
||||||
elif qr_density == SettingsConstants.DENSITY__MEDIUM:
|
|
||||||
self.qr_max_fragment_size = 30
|
|
||||||
elif qr_density == SettingsConstants.DENSITY__HIGH:
|
|
||||||
self.qr_max_fragment_size = 120
|
|
||||||
|
|
||||||
self.ur2_encode = UREncoder(ur=qr_ur_bytes, max_fragment_len=self.qr_max_fragment_size)
|
|
||||||
|
|
||||||
|
|
||||||
def seq_len(self):
|
|
||||||
return self.ur2_encode.fountain_encoder.seq_len()
|
@dataclass
|
||||||
|
class GenericStaticQrEncoder(BaseStaticQrEncoder):
|
||||||
|
data: str = None
|
||||||
|
|
||||||
|
def next_part(self):
|
||||||
|
return self.data
|
||||||
|
|
||||||
|
|
||||||
def next_part(self) -> str:
|
|
||||||
return self.ur2_encode.next_part().upper()
|
@dataclass
|
||||||
|
class BaseXpubQrEncoder(BaseQrEncoder):
|
||||||
|
"""
|
||||||
|
Base Xpub QrEncoder for static and animated formats
|
||||||
|
"""
|
||||||
|
seed: Seed = None
|
||||||
|
derivation: str = None
|
||||||
|
network: str = SettingsConstants.MAINNET
|
||||||
|
wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
|
||||||
|
|
||||||
|
def prep_xpub(self):
|
||||||
|
self.wordlist = Seed.get_wordlist()
|
||||||
|
|
||||||
|
if self.wordlist == None:
|
||||||
|
raise Exception('Wordlist Required')
|
||||||
|
|
||||||
|
version = bip32.detect_version(self.derivation, default="xpub", network=NETWORKS[SettingsConstants.map_network_to_embit(self.network)])
|
||||||
|
self.root = bip32.HDKey.from_seed(self.seed.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]["xprv"])
|
||||||
|
self.fingerprint = self.root.child(0).fingerprint
|
||||||
|
self.xprv = self.root.derive(self.derivation)
|
||||||
|
self.xpub = self.xprv.to_public()
|
||||||
|
self.xpub_base58 = self.xpub.to_string(version=version)
|
||||||
|
|
||||||
|
self.xpubstring = "[{}{}]{}".format(
|
||||||
|
hexlify(self.fingerprint).decode('utf-8'),
|
||||||
|
self.derivation[1:],
|
||||||
|
self.xpub_base58
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class StaticXpubQrEncoder(BaseXpubQrEncoder, BaseStaticQrEncoder):
|
||||||
|
def __post_init__(self):
|
||||||
|
super().__post_init__()
|
||||||
|
self.prep_xpub()
|
||||||
|
|
||||||
|
|
||||||
|
def next_part(self):
|
||||||
|
self.prep_xpub()
|
||||||
|
return self.xpubstring
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""**************************************************************************************
|
||||||
|
Simple animated QR encoders
|
||||||
|
**************************************************************************************"""
|
||||||
|
@dataclass
|
||||||
|
class BaseSimpleAnimatedQREncoder(BaseQrEncoder):
|
||||||
|
def __post_init__(self):
|
||||||
|
super().__post_init__()
|
||||||
|
self.parts = []
|
||||||
|
self.part_num_sent = 0
|
||||||
|
self.sent_complete = False
|
||||||
|
self._create_parts()
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_complete(self):
|
def is_complete(self):
|
||||||
return self.ur2_encode.is_complete()
|
return self.sent_complete
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SpecterPsbtQrEncoder(BasePsbtQrEncoder):
|
|
||||||
def __init__(self, psbt, qr_density):
|
|
||||||
super().__init__(psbt)
|
|
||||||
self.qr_max_fragement_size = 65
|
|
||||||
self.parts = []
|
|
||||||
self.part_num_sent = 0
|
|
||||||
self.sent_complete = False
|
|
||||||
|
|
||||||
if qr_density == SettingsConstants.DENSITY__LOW:
|
|
||||||
self.qr_max_fragement_size = 40
|
|
||||||
elif qr_density == SettingsConstants.DENSITY__MEDIUM:
|
|
||||||
self.qr_max_fragement_size = 65
|
|
||||||
elif qr_density == SettingsConstants.DENSITY__HIGH:
|
|
||||||
self.qr_max_fragement_size = 90
|
|
||||||
|
|
||||||
self._create_parts()
|
|
||||||
|
|
||||||
|
|
||||||
def _create_parts(self):
|
|
||||||
base64_psbt = b2a_base64(self.psbt.serialize())
|
|
||||||
|
|
||||||
if base64_psbt[-1:] == b"\n":
|
|
||||||
base64_psbt = base64_psbt[:-1]
|
|
||||||
|
|
||||||
base64_psbt = base64_psbt.decode('utf-8')
|
|
||||||
|
|
||||||
start = 0
|
|
||||||
stop = self.qr_max_fragement_size
|
|
||||||
qr_cnt = ((len(base64_psbt)-1) // self.qr_max_fragement_size) + 1
|
|
||||||
|
|
||||||
if qr_cnt == 1:
|
|
||||||
self.parts.append(base64_psbt[start:stop])
|
|
||||||
|
|
||||||
cnt = 0
|
|
||||||
while cnt < qr_cnt and qr_cnt != 1:
|
|
||||||
part = "p" + str(cnt+1) + "of" + str(qr_cnt) + " " + base64_psbt[start:stop]
|
|
||||||
self.parts.append(part)
|
|
||||||
|
|
||||||
start = start + self.qr_max_fragement_size
|
|
||||||
stop = stop + self.qr_max_fragement_size
|
|
||||||
if stop > len(base64_psbt):
|
|
||||||
stop = len(base64_psbt)
|
|
||||||
cnt += 1
|
|
||||||
|
|
||||||
|
|
||||||
def seq_len(self):
|
def seq_len(self):
|
||||||
@@ -275,154 +225,34 @@ class SpecterPsbtQrEncoder(BasePsbtQrEncoder):
|
|||||||
return part
|
return part
|
||||||
|
|
||||||
|
|
||||||
@property
|
def cur_part(self) -> str:
|
||||||
def is_complete(self):
|
if self.part_num_sent == 0:
|
||||||
return self.sent_complete
|
# Rewind all the way back to the end
|
||||||
|
self.part_num_sent = len(self.parts) - 1
|
||||||
|
else:
|
||||||
|
self.part_num_sent -= 1
|
||||||
|
return self.next_part()
|
||||||
|
|
||||||
|
|
||||||
|
def restart(self) -> str:
|
||||||
class SeedQrEncoder(BaseStaticQrEncoder):
|
|
||||||
def __init__(self, seed_phrase: List[str], wordlist_language_code: str):
|
|
||||||
super().__init__()
|
|
||||||
self.seed_phrase = seed_phrase
|
|
||||||
self.wordlist = Seed.get_wordlist(wordlist_language_code)
|
|
||||||
|
|
||||||
if self.wordlist == None:
|
|
||||||
raise Exception('Wordlist Required')
|
|
||||||
|
|
||||||
|
|
||||||
def next_part(self):
|
|
||||||
data = ""
|
|
||||||
# Output as Numeric data format
|
|
||||||
for word in self.seed_phrase:
|
|
||||||
index = self.wordlist.index(word)
|
|
||||||
data += str("%04d" % index)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CompactSeedQrEncoder(SeedQrEncoder):
|
|
||||||
def next_part(self):
|
|
||||||
# Output as binary data format
|
|
||||||
binary_str = ""
|
|
||||||
for word in self.seed_phrase:
|
|
||||||
index = self.wordlist.index(word)
|
|
||||||
|
|
||||||
# Convert index to binary, strip out '0b' prefix; zero-pad to 11 bits
|
|
||||||
binary_str += bin(index).split('b')[1].zfill(11)
|
|
||||||
|
|
||||||
# We can exclude the checksum bits at the end
|
|
||||||
if len(self.seed_phrase) == 24:
|
|
||||||
# 8 checksum bits in a 24-word seed
|
|
||||||
binary_str = binary_str[:-8]
|
|
||||||
|
|
||||||
elif len(self.seed_phrase) == 12:
|
|
||||||
# 4 checksum bits in a 12-word seed
|
|
||||||
binary_str = binary_str[:-4]
|
|
||||||
|
|
||||||
# Now convert to bytes, 8 bits at a time
|
|
||||||
as_bytes = bytearray()
|
|
||||||
for i in range(0, math.ceil(len(binary_str) / 8)):
|
|
||||||
# int conversion reads byte data as a string prefixed with '0b'
|
|
||||||
as_bytes.append(int('0b' + binary_str[i*8:(i+1)*8], 2))
|
|
||||||
|
|
||||||
# Must return data as `bytes` for `qrcode` to properly recognize it as byte data
|
|
||||||
return bytes(as_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BitcoinAddressEncoder(BaseStaticQrEncoder):
|
|
||||||
def __init__(self, address: str):
|
|
||||||
super().__init__()
|
|
||||||
self.address = address
|
|
||||||
|
|
||||||
|
|
||||||
def next_part(self):
|
|
||||||
return self.address
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SignedMessageEncoder(BaseStaticQrEncoder):
|
|
||||||
"""
|
|
||||||
Assumes that a signed message will fit in a single-frame QR
|
|
||||||
"""
|
|
||||||
def __init__(self, signed_message: str):
|
|
||||||
super().__init__()
|
|
||||||
self.signed_message = signed_message
|
|
||||||
|
|
||||||
|
|
||||||
def next_part(self):
|
|
||||||
return self.signed_message
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class XpubQrEncoder(BaseQrEncoder):
|
|
||||||
def __init__(self, seed, derivation, network, wordlist_language_code, sig_type : str = SettingsConstants.SINGLE_SIG):
|
|
||||||
self.seed = seed
|
|
||||||
self.derivation = derivation
|
|
||||||
self.network = network
|
|
||||||
self.wordlist = Seed.get_wordlist(wordlist_language_code)
|
|
||||||
self.parts = []
|
|
||||||
self.part_num_sent = 0
|
self.part_num_sent = 0
|
||||||
self.sent_complete = False
|
|
||||||
|
|
||||||
if self.wordlist == None:
|
|
||||||
raise Exception('Wordlist Required')
|
|
||||||
|
|
||||||
version = self.seed.detect_version(self.derivation, network, sig_type)
|
|
||||||
self.root = bip32.HDKey.from_seed(self.seed.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]["xprv"])
|
|
||||||
self.fingerprint = self.root.child(0).fingerprint
|
|
||||||
self.xprv = self.root.derive(self.derivation)
|
|
||||||
self.xpub = self.xprv.to_public()
|
|
||||||
self.xpub_base58 = self.xpub.to_string(version=version)
|
|
||||||
|
|
||||||
self.xpubstring = "[{}{}]{}".format(
|
|
||||||
hexlify(self.fingerprint).decode('utf-8'),
|
|
||||||
self.derivation[1:],
|
|
||||||
self.xpub_base58
|
|
||||||
)
|
|
||||||
|
|
||||||
self._create_parts()
|
|
||||||
|
|
||||||
|
|
||||||
def _create_parts(self):
|
|
||||||
self.parts = []
|
|
||||||
self.parts.append(self.xpubstring)
|
|
||||||
|
|
||||||
|
|
||||||
def next_part(self) -> str:
|
|
||||||
if len(self.parts) > 0:
|
|
||||||
self.sent_complete = True
|
|
||||||
return self.parts[0]
|
|
||||||
|
|
||||||
|
|
||||||
def seq_len(self):
|
|
||||||
return len(self.parts)
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SpecterXPubQrEncoder(BaseSimpleAnimatedQREncoder, BaseXpubQrEncoder):
|
||||||
@property
|
@property
|
||||||
def is_complete(self):
|
def qr_max_fragment_size(self):
|
||||||
return self.sent_complete
|
density_mapping = {
|
||||||
|
SettingsConstants.DENSITY__LOW: 40,
|
||||||
|
SettingsConstants.DENSITY__MEDIUM: 65,
|
||||||
|
SettingsConstants.DENSITY__HIGH: 90,
|
||||||
class SpecterXPubQrEncoder(XpubQrEncoder):
|
}
|
||||||
def __init__(self, qr_density, **kwargs):
|
return density_mapping.get(self.qr_density, 65)
|
||||||
# Must set up qr_max_fragment_size before calling super().__init__()
|
|
||||||
self.qr_max_fragment_size = 65
|
|
||||||
if qr_density == SettingsConstants.DENSITY__LOW:
|
|
||||||
self.qr_max_fragment_size = 40
|
|
||||||
elif qr_density == SettingsConstants.DENSITY__MEDIUM:
|
|
||||||
self.qr_max_fragment_size = 65
|
|
||||||
elif qr_density == SettingsConstants.DENSITY__HIGH:
|
|
||||||
self.qr_max_fragment_size = 90
|
|
||||||
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def _create_parts(self):
|
def _create_parts(self):
|
||||||
self.parts = []
|
self.prep_xpub()
|
||||||
|
|
||||||
start = 0
|
start = 0
|
||||||
stop = self.qr_max_fragment_size
|
stop = self.qr_max_fragment_size
|
||||||
qr_cnt = ((len(self.xpubstring)-1) // self.qr_max_fragment_size) + 1
|
qr_cnt = ((len(self.xpubstring)-1) // self.qr_max_fragment_size) + 1
|
||||||
@@ -442,34 +272,60 @@ class SpecterXPubQrEncoder(XpubQrEncoder):
|
|||||||
cnt += 1
|
cnt += 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""**************************************************************************************
|
||||||
|
Fountain encoded animated QR encoders
|
||||||
|
**************************************************************************************"""
|
||||||
|
@dataclass
|
||||||
|
class BaseFountainQrEncoder(BaseQrEncoder):
|
||||||
|
def __post_init__(self):
|
||||||
|
super().__post_init__()
|
||||||
|
|
||||||
|
self.ur2_encode: UREncoder = None
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_complete(self):
|
||||||
|
return self.ur2_encode.is_complete()
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def qr_max_fragment_size(self):
|
||||||
|
density_mapping = {
|
||||||
|
SettingsConstants.DENSITY__LOW: 10,
|
||||||
|
SettingsConstants.DENSITY__MEDIUM: 30,
|
||||||
|
SettingsConstants.DENSITY__HIGH: 120,
|
||||||
|
}
|
||||||
|
return density_mapping.get(self.qr_density, 30)
|
||||||
|
|
||||||
|
|
||||||
|
def _create_parts(self):
|
||||||
|
""" parts are dynamically generated by the fountain encoder """
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def seq_len(self):
|
||||||
|
return self.ur2_encode.fountain_encoder.seq_len()
|
||||||
|
|
||||||
|
|
||||||
def next_part(self) -> str:
|
def next_part(self) -> str:
|
||||||
# if part num sent is gt number of parts, start at 0
|
return self.ur2_encode.next_part().upper()
|
||||||
if self.part_num_sent > (len(self.parts) - 1):
|
|
||||||
self.part_num_sent = 0
|
|
||||||
|
|
||||||
part = self.parts[self.part_num_sent]
|
|
||||||
|
|
||||||
# when parts sent eq num of parts in list
|
def cur_part(self) -> str:
|
||||||
if self.part_num_sent == (len(self.parts) - 1):
|
return self.ur2_encode.current_part().upper()
|
||||||
self.sent_complete = True
|
|
||||||
|
|
||||||
# increment to next part
|
def restart(self):
|
||||||
self.part_num_sent += 1
|
self.ur2_encode.fountain_encoder.restart()
|
||||||
|
|
||||||
return part
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class UrXpubQrEncoder(XpubQrEncoder):
|
@dataclass
|
||||||
def __init__(self, qr_density, **kwargs):
|
class UrXpubQrEncoder(BaseFountainQrEncoder, BaseXpubQrEncoder):
|
||||||
super().__init__(**kwargs)
|
def __post_init__(self):
|
||||||
|
super().__post_init__()
|
||||||
if qr_density == SettingsConstants.DENSITY__LOW:
|
self.prep_xpub()
|
||||||
self.qr_max_fragment_size = 10
|
|
||||||
elif qr_density == SettingsConstants.DENSITY__MEDIUM:
|
|
||||||
self.qr_max_fragment_size = 30
|
|
||||||
elif qr_density == SettingsConstants.DENSITY__HIGH:
|
|
||||||
self.qr_max_fragment_size = 120
|
|
||||||
|
|
||||||
def derivation_to_keypath(path: str) -> list:
|
def derivation_to_keypath(path: str) -> list:
|
||||||
arr = path.split("/")
|
arr = path.split("/")
|
||||||
@@ -527,9 +383,12 @@ class UrXpubQrEncoder(XpubQrEncoder):
|
|||||||
self.ur2_encode = UREncoder(ur=qr_ur_bytes, max_fragment_len=self.qr_max_fragment_size)
|
self.ur2_encode = UREncoder(ur=qr_ur_bytes, max_fragment_len=self.qr_max_fragment_size)
|
||||||
|
|
||||||
|
|
||||||
def seq_len(self):
|
|
||||||
return self.ur2_encode.fountain_encoder.seq_len()
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UrPsbtQrEncoder(BaseFountainQrEncoder):
|
||||||
|
psbt: PSBT = None
|
||||||
|
|
||||||
def next_part(self) -> str:
|
def __post_init__(self):
|
||||||
return self.ur2_encode.next_part().upper()
|
super().__post_init__()
|
||||||
|
qr_ur_bytes = UR("crypto-psbt", UR_PSBT(self.psbt.serialize()).to_cbor())
|
||||||
|
self.ur2_encode = UREncoder(ur=qr_ur_bytes, max_fragment_len=self.qr_max_fragment_size)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class Seed:
|
|||||||
mnemonic: List[str] = None,
|
mnemonic: List[str] = None,
|
||||||
passphrase: str = "",
|
passphrase: str = "",
|
||||||
wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> None:
|
wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> None:
|
||||||
self.wordlist_language_code = wordlist_language_code
|
self._wordlist_language_code = wordlist_language_code
|
||||||
|
|
||||||
if not mnemonic:
|
if not mnemonic:
|
||||||
raise Exception("Must initialize a Seed with a mnemonic List[str]")
|
raise Exception("Must initialize a Seed with a mnemonic List[str]")
|
||||||
@@ -58,7 +58,10 @@ class Seed:
|
|||||||
@property
|
@property
|
||||||
def mnemonic_list(self) -> List[str]:
|
def mnemonic_list(self) -> List[str]:
|
||||||
return self._mnemonic
|
return self._mnemonic
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wordlist_language_code(self) -> str:
|
||||||
|
return self._wordlist_language_code
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mnemonic_display_str(self) -> str:
|
def mnemonic_display_str(self) -> str:
|
||||||
@@ -95,7 +98,7 @@ class Seed:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def wordlist(self) -> List[str]:
|
def wordlist(self) -> List[str]:
|
||||||
return Seed.get_wordlist(self.wordlist_language_code)
|
return Seed.get_wordlist(self._wordlist_language_code)
|
||||||
|
|
||||||
|
|
||||||
def set_wordlist_language_code(self, language_code: str):
|
def set_wordlist_language_code(self, language_code: str):
|
||||||
|
|||||||
@@ -430,7 +430,7 @@ class SettingsDefinition:
|
|||||||
type=SettingsConstants.TYPE__MULTISELECT,
|
type=SettingsConstants.TYPE__MULTISELECT,
|
||||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||||
selection_options=SettingsConstants.ALL_SCRIPT_TYPES,
|
selection_options=SettingsConstants.ALL_SCRIPT_TYPES,
|
||||||
default_value=[SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT]),
|
default_value=[SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT, SettingsConstants.TAPROOT]),
|
||||||
|
|
||||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||||
attr_name=SettingsConstants.SETTING__XPUB_DETAILS,
|
attr_name=SettingsConstants.SETTING__XPUB_DETAILS,
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ from embit.networks import NETWORKS
|
|||||||
from seedsigner.controller import Controller
|
from seedsigner.controller import Controller
|
||||||
|
|
||||||
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants
|
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants
|
||||||
from seedsigner.models.encode_qr import EncodeQR
|
from seedsigner.models.encode_qr import UrPsbtQrEncoder
|
||||||
from seedsigner.models.psbt_parser import PSBTParser
|
from seedsigner.models.psbt_parser import PSBTParser
|
||||||
from seedsigner.models.qr_type import QRType
|
|
||||||
from seedsigner.models.settings import SettingsConstants
|
from seedsigner.models.settings import SettingsConstants
|
||||||
from seedsigner.gui.screens.psbt_screens import PSBTOverviewScreen, PSBTMathScreen, PSBTAddressDetailsScreen, PSBTChangeDetailsScreen, PSBTFinalizeScreen
|
from seedsigner.gui.screens.psbt_screens import PSBTOverviewScreen, PSBTMathScreen, PSBTAddressDetailsScreen, PSBTChangeDetailsScreen, PSBTFinalizeScreen
|
||||||
from seedsigner.gui.screens.screen import (RET_CODE__BACK_BUTTON, ButtonListScreen, WarningScreen, DireWarningScreen, QRDisplayScreen)
|
from seedsigner.gui.screens.screen import (RET_CODE__BACK_BUTTON, ButtonListScreen, WarningScreen, DireWarningScreen, QRDisplayScreen)
|
||||||
@@ -300,6 +299,7 @@ class PSBTAddressDetailsView(View):
|
|||||||
|
|
||||||
class PSBTChangeDetailsView(View):
|
class PSBTChangeDetailsView(View):
|
||||||
NEXT = "Next"
|
NEXT = "Next"
|
||||||
|
SKIP_VERIFICATION = "Skip Verificiation"
|
||||||
VERIFY_MULTISIG = "Verify Multisig Change"
|
VERIFY_MULTISIG = "Verify Multisig Change"
|
||||||
|
|
||||||
|
|
||||||
@@ -360,7 +360,7 @@ class PSBTChangeDetailsView(View):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
# Have the Screen offer to load in the multisig descriptor.
|
# Have the Screen offer to load in the multisig descriptor.
|
||||||
button_data = [self.VERIFY_MULTISIG, self.NEXT]
|
button_data = [self.VERIFY_MULTISIG, self.SKIP_VERIFICATION]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Single sig
|
# Single sig
|
||||||
@@ -428,7 +428,8 @@ class PSBTChangeDetailsView(View):
|
|||||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||||
return Destination(BackStackView)
|
return Destination(BackStackView)
|
||||||
|
|
||||||
elif button_data[selected_menu_num] == self.NEXT:
|
elif button_data[selected_menu_num] == self.NEXT or button_data[selected_menu_num] == self.SKIP_VERIFICATION:
|
||||||
|
|
||||||
if self.change_address_num < psbt_parser.num_change_outputs - 1:
|
if self.change_address_num < psbt_parser.num_change_outputs - 1:
|
||||||
return Destination(PSBTChangeDetailsView, view_args={"change_address_num": self.change_address_num + 1})
|
return Destination(PSBTChangeDetailsView, view_args={"change_address_num": self.change_address_num + 1})
|
||||||
else:
|
else:
|
||||||
@@ -513,11 +514,9 @@ class PSBTFinalizeView(View):
|
|||||||
|
|
||||||
class PSBTSignedQRDisplayView(View):
|
class PSBTSignedQRDisplayView(View):
|
||||||
def run(self):
|
def run(self):
|
||||||
qr_encoder = EncodeQR(
|
qr_encoder = UrPsbtQrEncoder(
|
||||||
psbt=self.controller.psbt,
|
psbt=self.controller.psbt,
|
||||||
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),
|
|
||||||
)
|
)
|
||||||
self.run_screen(QRDisplayScreen, qr_encoder=qr_encoder)
|
self.run_screen(QRDisplayScreen, qr_encoder=qr_encoder)
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ class ScanView(View):
|
|||||||
decoder=self.decoder
|
decoder=self.decoder
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# A long scan might have exceeded the screensaver timeout; ensure screensaver
|
||||||
|
# doesn't immediately engage when we leave here.
|
||||||
|
self.controller.reset_screensaver_timeout()
|
||||||
|
|
||||||
# Handle the results
|
# Handle the results
|
||||||
if self.decoder.is_complete:
|
if self.decoder.is_complete:
|
||||||
if not self.is_valid_qr_type:
|
if not self.is_valid_qr_type:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen,
|
|||||||
from seedsigner.gui.screens.screen import LargeIconStatusScreen, QRDisplayScreen
|
from seedsigner.gui.screens.screen import LargeIconStatusScreen, QRDisplayScreen
|
||||||
from seedsigner.helpers import embit_utils
|
from seedsigner.helpers import embit_utils
|
||||||
from seedsigner.models.decode_qr import DecodeQR
|
from seedsigner.models.decode_qr import DecodeQR
|
||||||
from seedsigner.models.encode_qr import EncodeQR
|
from seedsigner.models.encode_qr import CompactSeedQrEncoder, GenericStaticQrEncoder, SeedQrEncoder, SpecterXPubQrEncoder, StaticXpubQrEncoder, UrXpubQrEncoder
|
||||||
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
|
||||||
@@ -754,20 +754,25 @@ class SeedExportXpubCoordinatorView(View):
|
|||||||
args["coordinator"] = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[0]
|
args["coordinator"] = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[0]
|
||||||
return Destination(SeedExportXpubWarningView, view_args=args, skip_current_view=True)
|
return Destination(SeedExportXpubWarningView, view_args=args, skip_current_view=True)
|
||||||
|
|
||||||
|
button_data = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)
|
||||||
|
|
||||||
selected_menu_num = self.run_screen(
|
selected_menu_num = self.run_screen(
|
||||||
ButtonListScreen,
|
ButtonListScreen,
|
||||||
title="Export Xpub",
|
title="Export Xpub",
|
||||||
is_button_text_centered=False,
|
is_button_text_centered=False,
|
||||||
button_data=self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS),
|
button_data=button_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
if selected_menu_num < len(self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)):
|
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||||
args["coordinator"] = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[selected_menu_num]
|
|
||||||
return Destination(SeedExportXpubWarningView, view_args=args)
|
|
||||||
|
|
||||||
elif selected_menu_num == RET_CODE__BACK_BUTTON:
|
|
||||||
return Destination(BackStackView)
|
return Destination(BackStackView)
|
||||||
|
|
||||||
|
coordinators_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__COORDINATORS)
|
||||||
|
selected_display_name = button_data[selected_menu_num]
|
||||||
|
args["coordinator"] = coordinators_settings_entry.get_selection_option_value_by_display_name(selected_display_name)
|
||||||
|
|
||||||
|
return Destination(SeedExportXpubWarningView, view_args=args)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SeedExportXpubWarningView(View):
|
class SeedExportXpubWarningView(View):
|
||||||
@@ -898,35 +903,24 @@ class SeedExportXpubQRDisplayView(View):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.seed = self.controller.get_seed(seed_num)
|
self.seed = self.controller.get_seed(seed_num)
|
||||||
|
|
||||||
qr_density = self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY)
|
encoder_args = dict(
|
||||||
if coordinator == SettingsConstants.COORDINATOR__SPECTER_DESKTOP:
|
|
||||||
qr_type = QRType.XPUB__SPECTER
|
|
||||||
|
|
||||||
elif coordinator == SettingsConstants.COORDINATOR__BLUE_WALLET:
|
|
||||||
qr_type = QRType.XPUB
|
|
||||||
|
|
||||||
elif coordinator == SettingsConstants.COORDINATOR__KEEPER:
|
|
||||||
qr_type = QRType.XPUB
|
|
||||||
|
|
||||||
elif coordinator == SettingsConstants.COORDINATOR__NUNCHUK:
|
|
||||||
qr_type = QRType.XPUB__UR
|
|
||||||
|
|
||||||
# As of 2022-03-02 Nunchuk doesn't seem to support animated QRs for Xpub import
|
|
||||||
qr_density = SettingsConstants.DENSITY__HIGH
|
|
||||||
|
|
||||||
else:
|
|
||||||
qr_type = QRType.XPUB__UR
|
|
||||||
|
|
||||||
self.qr_encoder = EncodeQR(
|
|
||||||
seed=self.seed,
|
seed=self.seed,
|
||||||
derivation=derivation_path,
|
derivation=derivation_path,
|
||||||
network=self.settings.get_value(SettingsConstants.SETTING__NETWORK),
|
network=self.settings.get_value(SettingsConstants.SETTING__NETWORK),
|
||||||
qr_type=qr_type,
|
qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY)
|
||||||
qr_density=qr_density,
|
|
||||||
wordlist_language_code=self.seed.wordlist_language_code,
|
|
||||||
sig_type=sig_type
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if coordinator == SettingsConstants.COORDINATOR__SPECTER_DESKTOP:
|
||||||
|
self.qr_encoder = SpecterXPubQrEncoder(**encoder_args)
|
||||||
|
|
||||||
|
elif coordinator in [SettingsConstants.COORDINATOR__BLUE_WALLET,
|
||||||
|
SettingsConstants.COORDINATOR__KEEPER,
|
||||||
|
SettingsConstants.COORDINATOR__NUNCHUK]:
|
||||||
|
self.qr_encoder = StaticXpubQrEncoder(**encoder_args)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.qr_encoder = UrXpubQrEncoder(**encoder_args)
|
||||||
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.run_screen(
|
self.run_screen(
|
||||||
@@ -1426,11 +1420,12 @@ class SeedTranscribeSeedQRWholeQRView(View):
|
|||||||
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
e = EncodeQR(
|
encoder_args = dict(seed=self.seed)
|
||||||
seed=self.seed,
|
if self.seedqr_format == QRType.SEED__SEEDQR:
|
||||||
qr_type=self.seedqr_format,
|
e = SeedQrEncoder(**encoder_args)
|
||||||
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
|
elif self.seedqr_format == QRType.SEED__COMPACTSEEDQR:
|
||||||
)
|
e = CompactSeedQrEncoder(**encoder_args)
|
||||||
|
|
||||||
data = e.next_part()
|
data = e.next_part()
|
||||||
|
|
||||||
ret = seed_screens.SeedTranscribeSeedQRWholeQRScreen(
|
ret = seed_screens.SeedTranscribeSeedQRWholeQRScreen(
|
||||||
@@ -1461,11 +1456,12 @@ class SeedTranscribeSeedQRZoomedInView(View):
|
|||||||
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
e = EncodeQR(
|
encoder_args = dict(seed=self.seed)
|
||||||
seed=self.seed,
|
if self.seedqr_format == QRType.SEED__SEEDQR:
|
||||||
qr_type=self.seedqr_format,
|
e = SeedQrEncoder(**encoder_args)
|
||||||
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
|
elif self.seedqr_format == QRType.SEED__COMPACTSEEDQR:
|
||||||
)
|
e = CompactSeedQrEncoder(**encoder_args)
|
||||||
|
|
||||||
data = e.next_part()
|
data = e.next_part()
|
||||||
|
|
||||||
if len(self.seed.mnemonic_list) == 24:
|
if len(self.seed.mnemonic_list) == 24:
|
||||||
@@ -2108,7 +2104,7 @@ class SeedSignMessageSignedMessageQRView(View):
|
|||||||
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
qr_encoder = EncodeQR(qr_type=QRType.SIGN_MESSAGE, signed_message=self.signed_message)
|
qr_encoder = GenericStaticQrEncoder(data=self.signed_message)
|
||||||
|
|
||||||
self.run_screen(
|
self.run_screen(
|
||||||
QRDisplayScreen,
|
QRDisplayScreen,
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ from seedsigner.gui.screens.tools_screens import (ToolsCalcFinalWordDoneScreen,
|
|||||||
ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen,
|
ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen,
|
||||||
ToolsImageEntropyLivePreviewScreen, ToolsAddressExplorerAddressTypeScreen)
|
ToolsImageEntropyLivePreviewScreen, ToolsAddressExplorerAddressTypeScreen)
|
||||||
from seedsigner.helpers import embit_utils, mnemonic_generation
|
from seedsigner.helpers import embit_utils, mnemonic_generation
|
||||||
from seedsigner.models.encode_qr import EncodeQR
|
from seedsigner.models.encode_qr import GenericStaticQrEncoder
|
||||||
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, SeedOptionsView, SeedWordsWarningView, SeedExportXpubScriptTypeView
|
from seedsigner.views.seed_views import SeedDiscardView, SeedFinalizeView, SeedMnemonicEntryView, SeedOptionsView, SeedWordsWarningView, SeedExportXpubScriptTypeView
|
||||||
@@ -188,8 +187,8 @@ class ToolsImageEntropyMnemonicLengthView(View):
|
|||||||
****************************************************************************"""
|
****************************************************************************"""
|
||||||
class ToolsDiceEntropyMnemonicLengthView(View):
|
class ToolsDiceEntropyMnemonicLengthView(View):
|
||||||
def run(self):
|
def run(self):
|
||||||
TWELVE = "12 words (50 rolls)"
|
TWELVE = f"12 words ({mnemonic_generation.DICE__NUM_ROLLS__12WORD} rolls)"
|
||||||
TWENTY_FOUR = "24 words (99 rolls)"
|
TWENTY_FOUR = f"24 words ({mnemonic_generation.DICE__NUM_ROLLS__24WORD} rolls)"
|
||||||
|
|
||||||
button_data = [TWELVE, TWENTY_FOUR]
|
button_data = [TWELVE, TWENTY_FOUR]
|
||||||
selected_menu_num = ButtonListScreen(
|
selected_menu_num = ButtonListScreen(
|
||||||
@@ -203,10 +202,10 @@ class ToolsDiceEntropyMnemonicLengthView(View):
|
|||||||
return Destination(BackStackView)
|
return Destination(BackStackView)
|
||||||
|
|
||||||
elif button_data[selected_menu_num] == TWELVE:
|
elif button_data[selected_menu_num] == TWELVE:
|
||||||
return Destination(ToolsDiceEntropyEntryView, view_args=dict(total_rolls=50))
|
return Destination(ToolsDiceEntropyEntryView, view_args=dict(total_rolls=mnemonic_generation.DICE__NUM_ROLLS__12WORD))
|
||||||
|
|
||||||
elif button_data[selected_menu_num] == TWENTY_FOUR:
|
elif button_data[selected_menu_num] == TWENTY_FOUR:
|
||||||
return Destination(ToolsDiceEntropyEntryView, view_args=dict(total_rolls=99))
|
return Destination(ToolsDiceEntropyEntryView, view_args=dict(total_rolls=mnemonic_generation.DICE__NUM_ROLLS__24WORD))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -713,7 +712,7 @@ class ToolsAddressExplorerAddressView(View):
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
from seedsigner.gui.screens.screen import QRDisplayScreen
|
from seedsigner.gui.screens.screen import QRDisplayScreen
|
||||||
qr_encoder = EncodeQR(qr_type=QRType.BITCOIN_ADDRESS, bitcoin_address=self.address)
|
qr_encoder = GenericStaticQrEncoder(data=self.address)
|
||||||
self.run_screen(
|
self.run_screen(
|
||||||
QRDisplayScreen,
|
QRDisplayScreen,
|
||||||
qr_encoder=qr_encoder,
|
qr_encoder=qr_encoder,
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from mock import MagicMock, Mock, patch
|
from unittest.mock import MagicMock, Mock, patch
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
# Prevent importing modules w/Raspi hardware dependencies.
|
# Prevent importing modules w/Raspi hardware dependencies.
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
coverage==7.2.1
|
coverage==7.3.1
|
||||||
mock==4.0.3
|
pytest==7.4.2
|
||||||
pytest==6.2.4
|
|
||||||
pytest-cov==4.1.0
|
pytest-cov==4.1.0
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import embit
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from mock import Mock, patch, MagicMock
|
from unittest.mock import Mock, patch, MagicMock
|
||||||
from seedsigner.helpers import embit_utils
|
from seedsigner.helpers import embit_utils
|
||||||
|
|
||||||
from seedsigner.models.settings import Settings
|
from seedsigner.models.settings import Settings
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
from seedsigner.models.seed import Seed
|
from seedsigner.models.seed import Seed
|
||||||
from embit import bip39
|
from embit import bip39
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ class TestController(BaseTest):
|
|||||||
assert controller.settings.get_value(SettingsConstants.SETTING__QR_DENSITY) == SettingsConstants.DENSITY__MEDIUM
|
assert controller.settings.get_value(SettingsConstants.SETTING__QR_DENSITY) == SettingsConstants.DENSITY__MEDIUM
|
||||||
assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED
|
assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED
|
||||||
assert controller.settings.get_value(SettingsConstants.SETTING__SIG_TYPES) == [i for i,j in SettingsConstants.ALL_SIG_TYPES]
|
assert controller.settings.get_value(SettingsConstants.SETTING__SIG_TYPES) == [i for i,j in SettingsConstants.ALL_SIG_TYPES]
|
||||||
assert controller.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES) == [SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT]
|
assert controller.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES) == [SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT, SettingsConstants.TAPROOT]
|
||||||
assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__ENABLED
|
assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__ENABLED
|
||||||
assert controller.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__ENABLED
|
assert controller.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__ENABLED
|
||||||
assert controller.settings.get_value(SettingsConstants.SETTING__CAMERA_ROTATION) == SettingsConstants.CAMERA_ROTATION__180
|
assert controller.settings.get_value(SettingsConstants.SETTING__CAMERA_ROTATION) == SettingsConstants.CAMERA_ROTATION__180
|
||||||
|
|||||||
+22
-52
File diff suppressed because one or more lines are too long
@@ -1,6 +1,5 @@
|
|||||||
from base import FlowTest, FlowStep
|
from base import FlowTest, FlowStep
|
||||||
|
|
||||||
from seedsigner.controller import Controller
|
|
||||||
from seedsigner.views.view import MainMenuView
|
from seedsigner.views.view import MainMenuView
|
||||||
from seedsigner.views import scan_views, seed_views, psbt_views
|
from seedsigner.views import scan_views, seed_views, psbt_views
|
||||||
from seedsigner.models.settings import SettingsConstants
|
from seedsigner.models.settings import SettingsConstants
|
||||||
@@ -74,7 +73,7 @@ class TestPSBTFlows(FlowTest):
|
|||||||
FlowStep(psbt_views.PSBTOverviewView),
|
FlowStep(psbt_views.PSBTOverviewView),
|
||||||
FlowStep(psbt_views.PSBTMathView),
|
FlowStep(psbt_views.PSBTMathView),
|
||||||
FlowStep(psbt_views.PSBTAddressDetailsView, button_data_selection=0),
|
FlowStep(psbt_views.PSBTAddressDetailsView, button_data_selection=0),
|
||||||
FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT),
|
FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.SKIP_VERIFICATION),
|
||||||
FlowStep(psbt_views.PSBTFinalizeView, button_data_selection=psbt_views.PSBTFinalizeView.APPROVE_PSBT),
|
FlowStep(psbt_views.PSBTFinalizeView, button_data_selection=psbt_views.PSBTFinalizeView.APPROVE_PSBT),
|
||||||
FlowStep(psbt_views.PSBTSigningErrorView, button_data_selection=psbt_views.PSBTSigningErrorView.SELECT_DIFF_SEED),
|
FlowStep(psbt_views.PSBTSigningErrorView, button_data_selection=psbt_views.PSBTSigningErrorView.SELECT_DIFF_SEED),
|
||||||
FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.SCAN_SEED),
|
FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.SCAN_SEED),
|
||||||
@@ -86,7 +85,7 @@ class TestPSBTFlows(FlowTest):
|
|||||||
FlowStep(psbt_views.PSBTOverviewView),
|
FlowStep(psbt_views.PSBTOverviewView),
|
||||||
FlowStep(psbt_views.PSBTMathView),
|
FlowStep(psbt_views.PSBTMathView),
|
||||||
FlowStep(psbt_views.PSBTAddressDetailsView, button_data_selection=0),
|
FlowStep(psbt_views.PSBTAddressDetailsView, button_data_selection=0),
|
||||||
FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT),
|
FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.SKIP_VERIFICATION),
|
||||||
FlowStep(psbt_views.PSBTFinalizeView, button_data_selection=psbt_views.PSBTFinalizeView.APPROVE_PSBT),
|
FlowStep(psbt_views.PSBTFinalizeView, button_data_selection=psbt_views.PSBTFinalizeView.APPROVE_PSBT),
|
||||||
FlowStep(psbt_views.PSBTSignedQRDisplayView),
|
FlowStep(psbt_views.PSBTSignedQRDisplayView),
|
||||||
FlowStep(MainMenuView),
|
FlowStep(MainMenuView),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from mock import PropertyMock, patch
|
from unittest.mock import PropertyMock, patch
|
||||||
|
|
||||||
# Must import test base before the Controller
|
# Must import test base before the Controller
|
||||||
from base import FlowTest, FlowStep
|
from base import FlowTest, FlowStep
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
# Must import test base before the Controller
|
# Must import test base before the Controller
|
||||||
from base import FlowTest, FlowStep
|
from base import FlowTest, FlowStep
|
||||||
|
|||||||
@@ -12,16 +12,17 @@ def test_dice_rolls():
|
|||||||
dice_rolls = ""
|
dice_rolls = ""
|
||||||
for i in range(0, 99):
|
for i in range(0, 99):
|
||||||
# Do not need truly rigorous random for this test
|
# Do not need truly rigorous random for this test
|
||||||
dice_rolls += str(random.randint(0, 5))
|
dice_rolls += str(random.randint(1, 6))
|
||||||
|
|
||||||
mnemonic = mnemonic_generation.generate_mnemonic_from_dice(dice_rolls)
|
mnemonic = mnemonic_generation.generate_mnemonic_from_dice(dice_rolls)
|
||||||
|
|
||||||
assert len(mnemonic) == 24
|
assert len(mnemonic) == 24
|
||||||
assert bip39.mnemonic_is_valid(" ".join(mnemonic))
|
assert bip39.mnemonic_is_valid(" ".join(mnemonic))
|
||||||
|
|
||||||
dice_rolls = ""
|
dice_rolls = ""
|
||||||
for i in range(0, 50):
|
for i in range(0, mnemonic_generation.DICE__NUM_ROLLS__12WORD):
|
||||||
# Do not need truly rigorous random for this test
|
# Do not need truly rigorous random for this test
|
||||||
dice_rolls += str(random.randint(0, 5))
|
dice_rolls += str(random.randint(1, 6))
|
||||||
|
|
||||||
mnemonic = mnemonic_generation.generate_mnemonic_from_dice(dice_rolls)
|
mnemonic = mnemonic_generation.generate_mnemonic_from_dice(dice_rolls)
|
||||||
assert len(mnemonic) == 12
|
assert len(mnemonic) == 12
|
||||||
@@ -29,18 +30,40 @@ def test_dice_rolls():
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_calculate_checksum():
|
def test_calculate_checksum_input_type():
|
||||||
""" Given an 11-word or 23-word mnemonic, the calculated checksum should yield a
|
"""
|
||||||
|
Given an 11-word or 23-word mnemonic, the calculated checksum should yield a
|
||||||
valid complete mnemonic.
|
valid complete mnemonic.
|
||||||
|
|
||||||
|
calculate_checksum should accept the mnemonic as:
|
||||||
|
* a list of strings
|
||||||
|
* string: "A B C", "A, B, C", "A,B,C"
|
||||||
"""
|
"""
|
||||||
# Test mnemonics from https://iancoleman.io/bip39/
|
# Test mnemonics from https://iancoleman.io/bip39/
|
||||||
|
def _try_all_input_formats(partial_mnemonic: str):
|
||||||
|
# List of strings
|
||||||
|
mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "))
|
||||||
|
assert bip39.mnemonic_is_valid(" ".join(mnemonic))
|
||||||
|
|
||||||
|
# Comma-separated string
|
||||||
|
mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.replace(" ", ","))
|
||||||
|
assert bip39.mnemonic_is_valid(" ".join(mnemonic))
|
||||||
|
|
||||||
|
# Comma-separated string w/space
|
||||||
|
mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.replace(" ", ", "))
|
||||||
|
assert bip39.mnemonic_is_valid(" ".join(mnemonic))
|
||||||
|
|
||||||
|
# Space-separated string
|
||||||
|
mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic)
|
||||||
|
assert bip39.mnemonic_is_valid(" ".join(mnemonic))
|
||||||
|
|
||||||
partial_mnemonic = "crawl focus rescue cable view pledge rather dinner cousin unfair day"
|
partial_mnemonic = "crawl focus rescue cable view pledge rather dinner cousin unfair day"
|
||||||
mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
_try_all_input_formats(partial_mnemonic)
|
||||||
assert bip39.mnemonic_is_valid(" ".join(mnemonic))
|
|
||||||
|
|
||||||
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"
|
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"
|
||||||
mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
_try_all_input_formats(partial_mnemonic)
|
||||||
assert bip39.mnemonic_is_valid(" ".join(mnemonic))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_calculate_checksum_invalid_mnemonics():
|
def test_calculate_checksum_invalid_mnemonics():
|
||||||
@@ -50,25 +73,25 @@ def test_calculate_checksum_invalid_mnemonics():
|
|||||||
with pytest.raises(Exception) as e:
|
with pytest.raises(Exception) as e:
|
||||||
# Mnemonic is too short: 10 words instead of 11
|
# Mnemonic is too short: 10 words instead of 11
|
||||||
partial_mnemonic = "abandon " * 9 + "about"
|
partial_mnemonic = "abandon " * 9 + "about"
|
||||||
mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
mnemonic_generation.calculate_checksum(partial_mnemonic)
|
||||||
assert "12- or 24-word" in str(e)
|
assert "12- or 24-word" in str(e)
|
||||||
|
|
||||||
with pytest.raises(Exception) as e:
|
with pytest.raises(Exception) as e:
|
||||||
# Valid mnemonic but unsupported length
|
# Valid mnemonic but unsupported length
|
||||||
mnemonic = "devote myth base logic dust horse nut collect buddy element eyebrow visit empty dress jungle"
|
mnemonic = "devote myth base logic dust horse nut collect buddy element eyebrow visit empty dress jungle"
|
||||||
mnemonic_generation.calculate_checksum(mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
mnemonic_generation.calculate_checksum(mnemonic)
|
||||||
assert "12- or 24-word" in str(e)
|
assert "12- or 24-word" in str(e)
|
||||||
|
|
||||||
with pytest.raises(Exception) as e:
|
with pytest.raises(Exception) as e:
|
||||||
# Mnemonic is too short: 22 words instead of 23
|
# Mnemonic is too short: 22 words instead of 23
|
||||||
partial_mnemonic = "abandon " * 21 + "about"
|
partial_mnemonic = "abandon " * 21 + "about"
|
||||||
mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
mnemonic_generation.calculate_checksum(partial_mnemonic)
|
||||||
assert "12- or 24-word" in str(e)
|
assert "12- or 24-word" in str(e)
|
||||||
|
|
||||||
with pytest.raises(ValueError) as e:
|
with pytest.raises(ValueError) as e:
|
||||||
# Invalid BIP-39 word
|
# Invalid BIP-39 word
|
||||||
partial_mnemonic = "foobar " * 11 + "about"
|
partial_mnemonic = "foobar " * 11 + "about"
|
||||||
mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
mnemonic_generation.calculate_checksum(partial_mnemonic)
|
||||||
assert "not in the dictionary" in str(e)
|
assert "not in the dictionary" in str(e)
|
||||||
|
|
||||||
|
|
||||||
@@ -78,17 +101,17 @@ def test_calculate_checksum_with_default_final_word():
|
|||||||
the mnemonic.
|
the mnemonic.
|
||||||
"""
|
"""
|
||||||
partial_mnemonic = "crawl focus rescue cable view pledge rather dinner cousin unfair day"
|
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)
|
mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic)
|
||||||
|
|
||||||
partial_mnemonic += " abandon"
|
partial_mnemonic += " abandon"
|
||||||
mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic)
|
||||||
assert mnemonic1 == mnemonic2
|
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"
|
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)
|
mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic)
|
||||||
|
|
||||||
partial_mnemonic += " abandon"
|
partial_mnemonic += " abandon"
|
||||||
mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic)
|
||||||
assert mnemonic1 == mnemonic2
|
assert mnemonic1 == mnemonic2
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+9
-10
@@ -1,10 +1,8 @@
|
|||||||
import os
|
import os
|
||||||
import pyzbar
|
|
||||||
from embit import bip39
|
from embit import bip39
|
||||||
from seedsigner.helpers.qr import QR
|
from seedsigner.helpers.qr import QR
|
||||||
from seedsigner.helpers.ur2.bytewords import decode
|
|
||||||
from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus
|
from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus
|
||||||
from seedsigner.models.encode_qr import EncodeQR
|
from seedsigner.models.encode_qr import SeedQrEncoder, CompactSeedQrEncoder
|
||||||
from seedsigner.models.qr_type import QRType
|
from seedsigner.models.qr_type import QRType
|
||||||
from seedsigner.models.seed import Seed
|
from seedsigner.models.seed import Seed
|
||||||
from seedsigner.models.settings import SettingsConstants
|
from seedsigner.models.settings import SettingsConstants
|
||||||
@@ -13,12 +11,14 @@ from seedsigner.models.settings import SettingsConstants
|
|||||||
|
|
||||||
def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type):
|
def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type):
|
||||||
""" Helper method to re-run multiple variations of the same encode/decode test """
|
""" Helper method to re-run multiple variations of the same encode/decode test """
|
||||||
print(entropy)
|
mnemonic = bip39.mnemonic_from_bytes(entropy).split()
|
||||||
seed_phrase = bip39.mnemonic_from_bytes(entropy).split()
|
assert len(mnemonic) == mnemonic_length
|
||||||
print(seed_phrase)
|
|
||||||
assert len(seed_phrase) == mnemonic_length
|
if qr_type == QRType.SEED__SEEDQR:
|
||||||
|
e = SeedQrEncoder(seed=Seed(mnemonic))
|
||||||
|
elif qr_type == QRType.SEED__COMPACTSEEDQR:
|
||||||
|
e = CompactSeedQrEncoder(seed=Seed(mnemonic))
|
||||||
|
|
||||||
e = EncodeQR(seed=Seed(seed_phrase), qr_type=qr_type)
|
|
||||||
data = e.next_part()
|
data = e.next_part()
|
||||||
print(data)
|
print(data)
|
||||||
|
|
||||||
@@ -35,8 +35,7 @@ def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type):
|
|||||||
assert status == DecodeQRStatus.COMPLETE
|
assert status == DecodeQRStatus.COMPLETE
|
||||||
|
|
||||||
decoded_seed_phrase = decoder.get_seed_phrase()
|
decoded_seed_phrase = decoder.get_seed_phrase()
|
||||||
print(decoded_seed_phrase)
|
assert mnemonic == decoded_seed_phrase
|
||||||
assert seed_phrase == decoded_seed_phrase
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import argparse
|
||||||
|
import random
|
||||||
|
from seedsigner.helpers import mnemonic_generation
|
||||||
|
from embit.wordlists.bip39 import WORDLIST as WORDLIST__ENGLISH
|
||||||
|
|
||||||
|
"""
|
||||||
|
see: docs/dice_verification.md (the "Command Line Tool" section) for full instructions.
|
||||||
|
|
||||||
|
tldr:
|
||||||
|
pip3 install embit
|
||||||
|
pip3 install -e .
|
||||||
|
cd tools
|
||||||
|
python3 mnemonic.py -h
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
usage = f"""
|
||||||
|
Verify SeedSigner's dice rolls and coin flip entropy-to-mnemonic conversion via this tool.
|
||||||
|
|
||||||
|
Compare its results against iancoleman.io/bip39 and bitcoiner.guide/seed
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
# {mnemonic_generation.DICE__NUM_ROLLS__12WORD} dice rolls / 12-word mnemonic
|
||||||
|
python3 mnemonic.py dice 5624433434...
|
||||||
|
|
||||||
|
# {mnemonic_generation.DICE__NUM_ROLLS__24WORD} dice rolls / 24-word mnemonic
|
||||||
|
python3 mnemonic.py dice 6151463561...
|
||||||
|
|
||||||
|
# {mnemonic_generation.DICE__NUM_ROLLS__12WORD} dice rolls, entered as 0-5 / 12-word mnemonic
|
||||||
|
python3 mnemonic.py --zero-indexed-dice dice 5135535514...
|
||||||
|
|
||||||
|
# 128 coin flips / 12-word mnemonic
|
||||||
|
python3 mnemonic.py coins 1111100111...
|
||||||
|
|
||||||
|
# 256 coin flips / 24-word mnemonic
|
||||||
|
python mnemonic.py coins 0010111010...
|
||||||
|
|
||||||
|
# GENERATE {mnemonic_generation.DICE__NUM_ROLLS__12WORD} random dice rolls / 12-word mnemonic
|
||||||
|
python3 mnemonic.py dice rand12
|
||||||
|
|
||||||
|
# GENERATE {mnemonic_generation.DICE__NUM_ROLLS__24WORD} random dice rolls / 24-word mnemonic
|
||||||
|
python3 mnemonic.py dice rand24
|
||||||
|
|
||||||
|
# GENERATE {mnemonic_generation.DICE__NUM_ROLLS__24WORD} random dice rolls, entered as 0-5 / 24-word mnemonic
|
||||||
|
python3 mnemonic.py --zero-indexed-dice dice rand24
|
||||||
|
|
||||||
|
# GENERATE 128 random coin flips / 12-word mnemonic
|
||||||
|
python3 mnemonic.py coins rand12
|
||||||
|
|
||||||
|
# GENERATE 256 random coin flips / 24-word mnemonic
|
||||||
|
python3 mnemonic.py coins rand24
|
||||||
|
"""
|
||||||
|
RAND_12 = "rand12"
|
||||||
|
RAND_24 = "rand24"
|
||||||
|
parser = argparse.ArgumentParser(description=f'SeedSigner entropy-to-mnemonic tool\n\n{usage}', formatter_class=argparse.RawTextHelpFormatter)
|
||||||
|
|
||||||
|
# Required positional arguments
|
||||||
|
parser.add_argument('method', type=str, choices=['dice', 'coins', 'final_word'], help="Input entropy method")
|
||||||
|
parser.add_argument('entropy', type=str, help=f"""Entropy data. Enter "{ RAND_12 }" or "{ RAND_24 }" to create a random (not-secure) example seed.""")
|
||||||
|
|
||||||
|
# Optional arguments
|
||||||
|
parser.add_argument('-z', '--zero-indexed-dice',
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
dest="zero_indexed_dice",
|
||||||
|
help="Enables dice entry as [0-5] instead of default [1-6]")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
method = args.method
|
||||||
|
entropy = args.entropy
|
||||||
|
zero_indexed_dice = args.zero_indexed_dice
|
||||||
|
|
||||||
|
is_rand_seed = 'rand' in entropy
|
||||||
|
if is_rand_seed:
|
||||||
|
# Generate random data as our entropy
|
||||||
|
if entropy not in [RAND_12, RAND_24]:
|
||||||
|
print(f"""Invalid random entropy value: Must be either "{RAND_12}" or "{RAND_24}".""")
|
||||||
|
exit(1)
|
||||||
|
mnemonic_length = 12 if entropy == RAND_12 else 24
|
||||||
|
|
||||||
|
if method == 'dice':
|
||||||
|
if zero_indexed_dice:
|
||||||
|
entropy = ''.join([str(random.randint(0, 5)) for i in range(mnemonic_generation.DICE__NUM_ROLLS__12WORD if mnemonic_length == 12 else mnemonic_generation.DICE__NUM_ROLLS__24WORD)])
|
||||||
|
else:
|
||||||
|
entropy = ''.join([str(random.randint(1, 6)) for i in range(mnemonic_generation.DICE__NUM_ROLLS__12WORD if mnemonic_length == 12 else mnemonic_generation.DICE__NUM_ROLLS__24WORD)])
|
||||||
|
|
||||||
|
elif method == 'coins':
|
||||||
|
entropy = ''.join([str(random.randint(0, 1)) for i in range(128 if mnemonic_length == 12 else 256)])
|
||||||
|
|
||||||
|
elif method == 'final_word':
|
||||||
|
random_dice_rolls = ''.join([str(random.randint(0, 1)) for i in range(128 if mnemonic_length == 12 else 256)])
|
||||||
|
entropy = " ".join(mnemonic_generation.generate_mnemonic_from_coin_flips(random_dice_rolls)[:-1])
|
||||||
|
print(len(entropy.split()), entropy)
|
||||||
|
|
||||||
|
if method == 'dice':
|
||||||
|
if not zero_indexed_dice and ('0' in entropy or '6' not in entropy):
|
||||||
|
print("Dice entry must be 1-6 unless --zero-indexed-dice is specified")
|
||||||
|
exit(1)
|
||||||
|
if len(entropy) not in [mnemonic_generation.DICE__NUM_ROLLS__12WORD, mnemonic_generation.DICE__NUM_ROLLS__24WORD]:
|
||||||
|
print(f"Dice entropy must be {mnemonic_generation.DICE__NUM_ROLLS__12WORD} or {mnemonic_generation.DICE__NUM_ROLLS__24WORD} rolls")
|
||||||
|
exit(1)
|
||||||
|
mnemonic = mnemonic_generation.generate_mnemonic_from_dice(entropy)
|
||||||
|
|
||||||
|
elif method == 'coins':
|
||||||
|
if len(entropy) not in [128, 256]:
|
||||||
|
print("Coin flip entropy must be 128 or 256 flips")
|
||||||
|
exit(1)
|
||||||
|
mnemonic = mnemonic_generation.generate_mnemonic_from_coin_flips(entropy)
|
||||||
|
|
||||||
|
elif method == 'final_word':
|
||||||
|
num_input_words = len(entropy.split())
|
||||||
|
if num_input_words not in [11, 12, 23, 24]:
|
||||||
|
print(f"Final word entropy must be 11, 12, 23, or 24 words ({num_input_words} provided)")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
if num_input_words in [11, 23]:
|
||||||
|
# We need to fill the last bits of entropy
|
||||||
|
if num_input_words == 11:
|
||||||
|
# 7 final bits of entropy in the 12th word (7 + 4-bit checksum)
|
||||||
|
num_final_entropy_bits = 7
|
||||||
|
else:
|
||||||
|
# 3 final bits of entropy in the 24th word (3 + 8-bit checksum)
|
||||||
|
num_final_entropy_bits = 3
|
||||||
|
|
||||||
|
final_entropy_method = None
|
||||||
|
while final_entropy_method not in ['1', '2', '3']:
|
||||||
|
final_entropy_method = input(f"""
|
||||||
|
How would you like to fill the final {num_final_entropy_bits} bits of entropy?
|
||||||
|
|
||||||
|
1.) {num_final_entropy_bits} coin flips
|
||||||
|
2.) Select an additional word from the wordlist
|
||||||
|
3.) Fill with zeros
|
||||||
|
|
||||||
|
Type 1, 2, or 3: """)
|
||||||
|
|
||||||
|
if final_entropy_method == '1':
|
||||||
|
coin_flips = input(f""" Enter {num_final_entropy_bits} coin flips as 0 or 1 (e.g. { "".join(str(random.randint(0,1)) for f in range(0, num_final_entropy_bits)) }): """)
|
||||||
|
if len(coin_flips) != num_final_entropy_bits:
|
||||||
|
print(f"Invalid number of coin flips: needed {num_final_entropy_bits}, got {len(coin_flips)}")
|
||||||
|
final_word = mnemonic_generation.get_partial_final_word(coin_flips)
|
||||||
|
entropy += f" {final_word}"
|
||||||
|
|
||||||
|
elif final_entropy_method == '2':
|
||||||
|
final_word = input(f""" Enter the final word: """)
|
||||||
|
if final_word not in WORDLIST__ENGLISH:
|
||||||
|
print(f"Invalid word: {final_word}")
|
||||||
|
exit(1)
|
||||||
|
entropy += f" {final_word}"
|
||||||
|
|
||||||
|
elif final_entropy_method == '3':
|
||||||
|
# Nothing to do; just pass the 11 or 23 words straight to calculate_checksum
|
||||||
|
pass
|
||||||
|
|
||||||
|
mnemonic = mnemonic_generation.calculate_checksum(entropy)
|
||||||
|
|
||||||
|
print("\n")
|
||||||
|
if is_rand_seed:
|
||||||
|
print("\t***** This is a demo seed. Do not use it to store funds!!! *****")
|
||||||
|
|
||||||
|
print(f"""\t{" ".join(mnemonic)}\n""")
|
||||||
|
|
||||||
|
if is_rand_seed:
|
||||||
|
print(f"\tEntropy: {entropy}\n")
|
||||||
|
|
||||||
|
if method == "dice":
|
||||||
|
print(f"""\tVerify at iancoleman.io/bip39 or bitcoiner.guide/seed using "Base 10" or "Hex" mode.\n""")
|
||||||
|
elif method == "coins":
|
||||||
|
print(f"""\tVerify at iancoleman.io/bip39 or bitcoiner.guide/seed using "Binary" mode.\n""")
|
||||||
Reference in New Issue
Block a user