├── .github ├── pull_request_template.md └── workflows │ ├── build.yml │ ├── telegram.yml │ └── tests.yml ├── .gitignore ├── .gitmodules ├── LICENSE.md ├── README.md ├── docker-compose.yml ├── docker ├── Dockerfile └── setup.sh ├── docs ├── code_structure.md ├── debug_crash.md ├── developer_tips.md ├── dice_verification.md ├── electrum.md ├── feature_roadmap.md ├── img │ ├── Mini_Pill_Main_Photo.jpg │ ├── Open_Pill_Mini_Models.JPG │ ├── Open_Pill_Mini_w_Faceplate.JPG │ ├── Open_Pill_Models.JPG │ ├── Open_Pill_Star.JPG │ ├── Open_Pill_w_Comfort_Joystick.png │ ├── OrangePillMini_Thumb.jpg │ ├── Orange_Pill.JPG │ ├── Orange_Pill_Models.JPG │ ├── Rugged_Pill_Thumb.jpg │ ├── Simple_Pill_Models.JPG │ ├── dice_entr.png │ ├── dice_pic1.png │ ├── dice_pic2.png │ ├── dice_pic3.png │ ├── dice_type.png │ ├── dicedoc │ │ ├── coleman_addresses.png │ │ ├── coleman_change_addresses_1.png │ │ ├── coleman_change_addresses_2.png │ │ ├── coleman_entropy.png │ │ ├── coleman_verify.png │ │ ├── coleman_zpub.png │ │ ├── seedtool_1.png │ │ ├── seedtool_2.png │ │ ├── seedtool_3.png │ │ ├── seedtool_addresses.png │ │ ├── seedtool_change_addresses_1.png │ │ ├── seedtool_change_addresses_2.png │ │ ├── seedtool_fingerprint.png │ │ ├── seedtool_zpub.png │ │ ├── sesi_dice_1.png │ │ ├── sesi_dice_2.png │ │ ├── sesi_export_xpub_1.png │ │ ├── sesi_export_xpub_2.png │ │ ├── sesi_finger_print.png │ │ ├── sesi_seed_1.png │ │ ├── sesi_seed_2.png │ │ ├── sesi_tools_dice_seed.png │ │ ├── sparrow_wallet_1.png │ │ ├── sparrow_wallet_2.png │ │ └── sparrow_zpub.png │ ├── legacy_hardware_remapped_pins.jpg │ ├── legacy_hardware_remapped_pins_photo.jpg │ ├── logo.svg │ ├── smartcard_pn532_headerconnection.jpg │ ├── smartcard_usb_readers.png │ ├── usb_relay_01.png │ ├── usb_relay_linux_01.png │ ├── usb_relay_linux_02.png │ ├── usb_relay_mac_01.png │ ├── usb_relay_mac_02.png │ ├── usb_relay_mac_03.png │ └── usb_relay_mac_04.png ├── legacy_hardware.md ├── qr_formats.md ├── raspberry_pi_os_build_instructions.md ├── recovery.md ├── seed_qr │ ├── README.md │ ├── img │ │ ├── compact_12word.png │ │ ├── compact_24word.png │ │ ├── handmade_qr.jpg │ │ ├── phone_screenshot_compact.jpg │ │ ├── phone_screenshot_standard.jpg │ │ ├── qrcode_capacity.png │ │ ├── seedqr_plate.jpg │ │ ├── standard_12word.png │ │ ├── standard_24word.png │ │ ├── standard_24word_with_logo.png │ │ ├── vector1_compact_24word.png │ │ ├── vector1_standard_24word.png │ │ ├── vector2_compact_24word.png │ │ ├── vector2_standard_24word.png │ │ ├── vector3_compact_24word.png │ │ ├── vector3_standard_24word.png │ │ ├── vector4_compact_12word.png │ │ ├── vector4_standard_12word.png │ │ ├── vector5_compact_12word.png │ │ ├── vector5_standard_12word.png │ │ ├── vector6_compact_12word.png │ │ ├── vector6_standard_12word.png │ │ ├── vector7_compact_12word.png │ │ ├── vector8_compact_12word.png │ │ ├── vector9_compact_12word.png │ │ └── zxing_screenshot.png │ └── printable_templates │ │ ├── 21x21_A4_trading_card_2sided.pdf │ │ ├── 21x21_letter_trading_card_2sided.pdf │ │ ├── 25x25_A4_trading_card_2sided.pdf │ │ ├── 25x25_letter_trading_card_2sided.pdf │ │ ├── 29x29_A4_trading_card_2sided.pdf │ │ ├── 29x29_letter_trading_card_2sided.pdf │ │ ├── dots_21x21.pdf │ │ ├── dots_25x25.pdf │ │ ├── dots_29x29.pdf │ │ ├── grid_21x21.pdf │ │ ├── grid_25x25.pdf │ │ ├── grid_29x29.pdf │ │ ├── grid_wfingerprint_21x21.pdf │ │ ├── grid_wfingerprint_21x21_4-up.pdf │ │ ├── grid_wfingerprint_25x25.pdf │ │ ├── grid_wfingerprint_29x29.pdf │ │ ├── mnemonic_12words_wfingerprint.pdf │ │ ├── mnemonic_12words_wfingerprint_4up.pdf │ │ ├── mnemonic_24word_wfingerprint.pdf │ │ ├── mnemonic_24word_wfingerprint_4-up.pdf │ │ ├── trading_card_21x21_w12words.pdf │ │ ├── trading_card_25x25_w24words.pdf │ │ ├── trading_card_29x29_w24words.pdf │ │ └── xpub_wfingerprint.pdf ├── smartcard_support_installation.md └── usb_relay.md ├── enclosures ├── look_screws_pill │ ├── Bottom.stl │ ├── ButtonsConnected.stl │ ├── README.md │ ├── Top.stl │ └── pressPad.stl ├── open_pill │ ├── README.md │ ├── design_file │ │ └── open_pill.f3d │ └── print_file │ │ └── open_pill.stl ├── open_pill_mini │ ├── Main_Chassis.stl │ └── README.md ├── open_pill_mini_w_coverplate │ ├── Buttons.stl │ ├── Lid.stl │ ├── Main_Chassis.stl │ ├── README.md │ ├── Thumbstick_PLA.stl │ └── Thumbstick_TPU.stl ├── orange_pill │ ├── README.md │ ├── design_files │ │ ├── OP_Lower.f3d │ │ ├── OP_Upper.f3d │ │ ├── button.f3d │ │ └── joystiick.f3d │ └── print_files │ │ ├── OP_Lower.stl │ │ ├── OP_Upper.stl │ │ ├── button.stl │ │ └── joystick.stl ├── orange_pill_mini │ ├── OrangePillMini_Bottom.3mf │ ├── OrangePillMini_Buttons.stl │ ├── OrangePillMini_Top.stl │ ├── README.md │ ├── ThumbStick_small.stl │ └── Trackball.stl ├── pushcase │ ├── 3mf │ │ ├── Bottom - Push Case - SeedSigner.3mf │ │ ├── Buttons - Push Case - SeedSigner.3mf │ │ ├── Middle - Push Case - SeedSigner.3mf │ │ ├── Soldering Support - Push Case - SeedSigner.3mf │ │ └── Top - Push Case - SeedSigner.3mf │ ├── README.md │ ├── images │ │ ├── back1.png │ │ ├── back2.png │ │ ├── camera1.png │ │ ├── camera2.png │ │ ├── camera3.png │ │ ├── camera4.png │ │ ├── camera5.png │ │ ├── components.png │ │ ├── front1.png │ │ ├── front2.png │ │ ├── front3.png │ │ ├── front4.png │ │ ├── pushcase.png │ │ ├── solder1.png │ │ ├── solder2.png │ │ ├── solder3.png │ │ ├── stack1.png │ │ ├── stack2.png │ │ ├── stack3.png │ │ └── stack4.png │ └── stl │ │ ├── Bottom - Push Case - SeedSigner.stl │ │ ├── Button - Push Case - SeedSigner.stl │ │ ├── Middle - Push Case - SeedSigner.stl │ │ ├── Soldering Support - Push Case - SeedSigner.stl │ │ ├── Thumbstick - Push Case - SeedSigner.stl │ │ └── Top - Push Case - SeedSigner.stl ├── rugged_pill │ ├── Multimaterial │ │ ├── RuggedPIll_Bottom_MMU.3mf │ │ └── RuggedPill_Top_MMU.3mf │ ├── README.md │ ├── RuggedPillBottom_4SeedHammer.stl │ ├── RuggedPill_Bottom.stl │ ├── RuggedPill_Buttons.stl │ ├── RuggedPill_Thumbstick.stl │ ├── RuggedPill_Top.stl │ └── RuggedPill_Trackball.stl └── simple_pill │ ├── README.md │ ├── protective_case │ ├── sp_protective_case_lower.stl │ └── sp_protective_case_upper.stl │ ├── sp_button.stl │ ├── sp_dpad.stl │ ├── sp_lower.stl │ └── sp_upper.stl ├── l10n ├── README.md ├── messages.pot └── requirements-l10n.txt ├── pyproject.toml ├── requirements-raspi.txt ├── requirements.txt ├── seedsigner_pubkey.gpg ├── setup.cfg ├── setup.py ├── src ├── main.py └── seedsigner │ ├── __init__.py │ ├── controller.py │ ├── gui │ ├── __init__.py │ ├── components.py │ ├── keyboard.py │ ├── renderer.py │ ├── screens │ │ ├── __init__.py │ │ ├── psbt_screens.py │ │ ├── scan_screens.py │ │ ├── screen.py │ │ ├── seed_screens.py │ │ ├── settings_screens.py │ │ └── tools_screens.py │ └── toast.py │ ├── hardware │ ├── ST7789.py │ ├── __init__.py │ ├── buttons.py │ ├── camera.py │ ├── microsd.py │ └── pivideostream.py │ ├── helpers │ ├── __init__.py │ ├── embit_utils.py │ ├── l10n.py │ ├── mnemonic_generation.py │ ├── qr.py │ ├── seedkeeper_utils.py │ └── ur2 │ │ ├── LICENSE │ │ ├── __init__.py │ │ ├── bytewords.py │ │ ├── cbor_lite.py │ │ ├── constants.py │ │ ├── crc32.py │ │ ├── fountain_decoder.py │ │ ├── fountain_encoder.py │ │ ├── fountain_utils.py │ │ ├── random_sampler.py │ │ ├── ur.py │ │ ├── ur_decoder.py │ │ ├── ur_encoder.py │ │ ├── utils.py │ │ └── xoshiro256.py │ ├── models │ ├── __init__.py │ ├── decode_qr.py │ ├── encode_qr.py │ ├── encryptedqr.py │ ├── encryption.py │ ├── psbt_parser.py │ ├── qr_type.py │ ├── seed.py │ ├── seed_storage.py │ ├── settings.py │ ├── settings_definition.py │ ├── singleton.py │ └── threads.py │ ├── resources │ ├── fonts │ │ ├── Font_Awesome_6_Free-Solid-900.otf │ │ ├── Inconsolata-Regular.ttf │ │ ├── Inconsolata-SemiBold.ttf │ │ ├── OpenSans-Regular.ttf │ │ ├── OpenSans-SemiBold.ttf │ │ ├── PlemolJPConsole-Regular-S.ttf │ │ ├── PlemolJPConsole-SemiBold-S.ttf │ │ ├── RobotoCondensed-Bold.ttf │ │ ├── RobotoCondensed-Regular.ttf │ │ └── seedsigner-icons.otf │ ├── icons │ │ ├── arrow-down.png │ │ ├── arrow-down_selected.png │ │ ├── arrow-up.png │ │ ├── arrow-up_selected.png │ │ ├── back.png │ │ ├── back_selected.png │ │ ├── btc_logo.png │ │ ├── btc_logo_30x30.png │ │ ├── btc_logo_bw.png │ │ ├── dire_warning.png │ │ └── warning.png │ └── img │ │ ├── btc_logo_60x60.png │ │ ├── logo_black_240.png │ │ └── partners │ │ └── hrf_logo.png │ └── views │ ├── __init__.py │ ├── psbt_views.py │ ├── scan_views.py │ ├── screensaver.py │ ├── seed_views.py │ ├── settings_views.py │ ├── tools_views.py │ └── view.py ├── tests ├── README.md ├── base.py ├── requirements.txt ├── run_full_coverage.sh ├── screenshot_generator │ ├── README.md │ ├── __init__.py │ ├── conftest.py │ ├── generator.py │ ├── template.md │ └── utils.py ├── test_bip85.py ├── test_controller.py ├── test_decodepsbtqr.py ├── test_embit_utils.py ├── test_encodepsbtqr.py ├── test_flows.py ├── test_flows_l10n.py ├── test_flows_psbt.py ├── test_flows_seed.py ├── test_flows_settings.py ├── test_flows_tools.py ├── test_flows_view.py ├── test_l10n.py ├── test_main.py ├── test_mnemonic_generation.py ├── test_psbt_parser.py ├── test_seed.py ├── test_seedqr.py ├── test_settings.py └── test_settingsqr_decoder.py └── tools ├── javacard-build.xml.manual ├── javacard-build.xml.seedsigneros ├── mnemonic.py └── seed_phrase_to_qr.py /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | _Describe the change simply. Provide a reason for the change._ 4 | 5 | _Include screenshots of any new or modified screens (or at least explain why they were omitted)_ 6 | 7 | This pull request is categorized as a: 8 | 9 | - [ ] New feature 10 | - [ ] Bug fix 11 | - [ ] Code refactor 12 | - [ ] Documentation 13 | - [ ] Other 14 | 15 | ## Checklist 16 | 17 | - [ ] I’ve run `pytest` and made sure all unit tests pass before sumbitting the PR 18 | 19 | If you modified or added functionality/workflow, did you add new unit tests? 20 | 21 | - [ ] No, I’m a fool 22 | - [ ] Yes 23 | - [ ] N/A 24 | 25 | I have tested this PR on the following platforms/os: 26 | 27 | - [ ] Raspberry Pi OS [Manual Build](https://github.com/SeedSigner/seedsigner/blob/dev/docs/manual_installation.md) 28 | - [ ] [SeedSigner OS](https://github.com/SeedSigner/seedsigner-os) on a Pi0/Pi0W board 29 | - [ ] Other 30 | 31 | 32 | Note: Keep your changes limited in scope; if you uncover other issues or improvements along the way, ideally submit those as a separate PR. The more complicated the PR the harder to review, test, and merge. 33 | -------------------------------------------------------------------------------- /.github/workflows/telegram.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Notify on Telegram 2 | on: 3 | pull_request_target: 4 | branches: 5 | - dev 6 | types: 7 | - closed 8 | 9 | jobs: 10 | if_merged: 11 | if: github.event.pull_request.merged == true 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Notify on Telegram 15 | uses: EverythingSuckz/github-telegram-notify@main 16 | with: 17 | bot_token: '${{ secrets.BOT_TOKEN }}' 18 | chat_id: '${{ secrets.CHAT_ID }}' 19 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | - main 8 | pull_request: 9 | 10 | concurrency: 11 | # Concurrency group that uses the workflow name and PR number if available 12 | # or commit SHA as a fallback. If a new build is triggered under that 13 | # concurrency group while a previous build is running it will be canceled. 14 | # Repeated pushes to a PR will cancel all previous builds, while multiple 15 | # merges to main will not cancel. 16 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | test: 21 | runs-on: ubuntu-latest 22 | strategy: 23 | matrix: 24 | # 3.10: currently used by Seedsigner 25 | # 3.12: latest stable Python as upper test bound 26 | python-version: ["3.10", "3.12"] 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | with: 31 | # Needs to also pull the seedsigner-translations repo 32 | submodules: recursive 33 | - name: Set up Python ${{ matrix.python-version }} 34 | uses: actions/setup-python@v5 35 | with: 36 | python-version: ${{ matrix.python-version }} 37 | - name: Install dependencies 38 | run: | 39 | sudo apt-get install libzbar0 40 | python -m pip install --upgrade pip 41 | pip install -r requirements.txt -r tests/requirements.txt 42 | pip install . 43 | - name: Test with pytest 44 | run: | 45 | mkdir artifacts 46 | python -m pytest \ 47 | --color=yes \ 48 | --cov=seedsigner \ 49 | --cov-append \ 50 | --cov-branch \ 51 | --durations 5 \ 52 | -vv 53 | - name: Generate screenshots 54 | run: | 55 | python -m pytest tests/screenshot_generator/generator.py \ 56 | --color=yes \ 57 | --cov=seedsigner \ 58 | --cov-append \ 59 | --cov-branch \ 60 | --cov-report html:./artifacts/cov_html \ 61 | -vv 62 | cp -r ./seedsigner-screenshots ./artifacts/ 63 | - name: Coverage report 64 | run: coverage report 65 | - name: Archive CI Artifacts 66 | uses: actions/upload-artifact@v4 67 | with: 68 | name: ci-artifacts-${{ matrix.python-version }} 69 | path: artifacts/** 70 | retention-days: 10 71 | # Upload also when tests fail. The workflow result (red/green) will 72 | # be not effected by this. 73 | if: always() 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __pycache__/ 3 | src/seedsigner.egg-info/ 4 | .nova 5 | .vscode 6 | src/seedsigner/models/settings_definition.json 7 | .idea 8 | .coverage* 9 | 10 | *.po 11 | *.mo -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/seedsigner/resources/seedsigner-translations"] 2 | path = src/seedsigner/resources/seedsigner-translations 3 | url = https://github.com/SeedSigner/seedsigner-translations.git 4 | branch = dev 5 | [submodule "seedsigner-screenshots"] 6 | path = seedsigner-screenshots 7 | url = https://github.com/SeedSigner/seedsigner-screenshots.git 8 | branch = dev 9 | update = none 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 SeedSigner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | seedsigner-dev: 5 | build: 6 | context: . 7 | dockerfile: docker/Dockerfile 8 | command: sh -c 'bash -c "docker/setup.sh"' 9 | volumes: 10 | - ../seedsigner:/seedsigner -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-bullseye 2 | 3 | # install zbar dependencyy 4 | RUN apt-get -qq update 5 | RUN apt-get -y -qq install zbar-tools 6 | 7 | # temp copy requirements files to local repo to do pip3 install 8 | COPY ../requirements.txt /requirements.txt 9 | COPY ../tests/requirements.txt /tests-requirements.txt 10 | 11 | WORKDIR / 12 | RUN pip3 install -r requirements.txt 13 | RUN pip3 install -r tests-requirements.txt 14 | 15 | # clean up copied files 16 | RUN rm /requirements.txt 17 | RUN rm /tests-requirements.txt 18 | 19 | # set working dir 20 | WORKDIR /seedsigner -------------------------------------------------------------------------------- /docker/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pip3 install -e . 4 | tail -f /dev/null -------------------------------------------------------------------------------- /docs/code_structure.md: -------------------------------------------------------------------------------- 1 | # Code Structure 2 | 3 | SeedSigner roughly follows a Model-View-Controller approach. Like in a typical web app (e.g. Flask) the `View`s can be called as needed like individual web urls. After completing display and interaction with the user, the `View` then decides where to route the user next, analogous to a web app returning a `response.redirect(url)`. 4 | 5 | The `Controller` then ends up being quite stripped down. For example, there's no need for a web app's `urls.py` since there are no mappings from url to `View` to maintain since we're not actually using a url/http routing approach. 6 | 7 | `View`s have to handle user interaction so there are `while True` loops that cycle between waiting for user input, gathering data, and then updating the UI components accordingly. You wouldn't find this kind of cycle in a web app because this sort of interactive user input is handled in the browser at the html/css/js level. 8 | 9 | 10 | 11 | * `Model`s: Store the persistent settings, the in-memory seeds, current wallet information, etc. 12 | * `Controller`: Manages the state of the world and controls access to global resources. 13 | * `View`s: Implementation of each screen. Prepares relevant data for display. Must also instantiate the display objects that will actually render the UI. 14 | * `gui.screens`: Re-usable formatted UI renderers. 15 | * `gui.components`: Basic individual UI elements that are used by the `templates` such as the top nav, buttons, button lists, text displays. 16 | 17 | In an typical webserver context the `View` would send data to an html template (e.g. Jinja) which would then dynamically populate the page with html elements like ``, ``, ``, etc. This is analgous to our `gui.screens` constructing a UI renderer by piecing together various `gui.components` as needed. 18 | 19 | 20 | 21 | `Controller` is a global singleton that any `View` can access and update as needed. 22 | -------------------------------------------------------------------------------- /docs/debug_crash.md: -------------------------------------------------------------------------------- 1 | ## Debugging a Crash for advanced (technical) users 2 | 3 | These instructions are intended to help users of SeedSigner provide crash exception and traceback logs to developers to aid in troubleshooting and resolving bugs. 4 | 5 | ### Testnet vs Mainnet 6 | 7 | Whenever possible recreate a crash in testnet. This will help avoid accidently revealing private information about yourself, your bitcoin transactions, or lose any funds. 8 | 9 | ### Network connected SeedSigner 10 | 11 | If you are using SeedSigner for development and testing, then I recommend network access via ssh to view crash logs. Follow [these](https://github.com/SeedSigner/seedsigner/blob/main/docs/usb_relay.md) instructions to setup a USB relay for internet access. You can also connect your SeedSigner to Wifi if you have a rasp pi zero w/ wifi. 12 | 13 | ### Airgapped debugging setup 14 | 15 | If you are using SeedSigner for mainnet transactions, then do not connect your device to a network or the internet. Instead connect your SeedSigner to a HDMI display (without internet) and a USB keyboard. This will require an HDMI adapter and micro USB to USB A adapter. Plug in the HDMI display and keyboard before powering on SeedSigner. The password for the SeedSigner pi user is `raspberry`. 16 | 17 | ### Debugging steps 18 | 19 | At this point you should be signed into the pi user either on a HDMI display (via command line) or a ssh connection. 20 | 21 | Follow these steps to setup a debug session. 22 | 23 | `cd seedsigner/src` 24 | 25 | `nano settings.ini` 26 | 27 | in nano editor change `debug = False` to `debug = True` (case sensitive). Save and exit settings.ini. 28 | 29 | stop the seedsigner systemd process by running 30 | 31 | `sudo systemctl stop seedsigner.service` 32 | 33 | now start the python app manually by running 34 | 35 | `python3 main.py` 36 | 37 | SeedSigner should now be up and running. Keep it connected to the display and keyboard. Recreate the steps to cause the crash. The traceback log and exception will be displayed on the HDMI display. -------------------------------------------------------------------------------- /docs/developer_tips.md: -------------------------------------------------------------------------------- 1 | # Developer Tips 2 | 3 | ### Quickly generate a new seed to test with 4 | Generate a new 12- or 24-word seed via [https://iancoleman.io/bip39/](https://iancoleman.io/bip39/). 5 | 6 | Access a `python3` environment that has the `embit` library installed (e.g. your own local machine, ssh into the SeedSigner, etc) 7 | 8 | Start a python REPL session by just typing: `python3` 9 | 10 | Paste in the following but insert your newly generated mnemonic: 11 | ``` 12 | from embit import bip39 13 | seed_phrase = "smoke chimney announce candy glory tongue refuse fatigue cricket once consider beef treat urge wing deny gym robot tobacco adult problem priority wheat diagram" 14 | data = "" 15 | for word in seed_phrase.split(" "): 16 | index = bip39.WORDLIST.index(word) 17 | data += "%04d" % index 18 | 19 | print(data) 20 | ``` 21 | 22 | For the seed in the snippet, you should see: 23 | ``` 24 | 163803200074026607961827144306700411123603780160185419152013046908321497181700301371136719990487 25 | ``` 26 | 27 | Take the output and paste it into a [QR code generator](https://www.the-qrcode-generator.com/). 28 | 29 | Start up SeedSigner's UI to import a seed from a QR code. Scan the new QR code and you're good to go! 30 | 31 | 32 | # Advanced developer notes 33 | 34 | ## Backup an SD card 35 | 36 | You can back up and restore any size SD card but the process is a little inefficient so the bigger the source SD card, the bigger the backup will be and the longer it'll take to create (and even longer to image back to a new SD card), even if most of the card is blank. 37 | 38 | You can restore a backup image to an SD card of the same or larger size. So it's strongly recommended to do repetitive development work on a smaller card that's easier to backup and restore. Once the image is stabilized, then write it to a bigger card, if necessary (that being said, there's really no reason to use a large SD card for SeedSigner. An 8GB SD card is more than big enough). 39 | 40 | Insert the SD card into a Mac/Linux machine and create a compressed img file. 41 | 42 | First verify the name of your SD card: 43 | 44 | ``` 45 | # Mac: 46 | diskutil list 47 | 48 | # Linux: 49 | sudo fdisk -l 50 | ``` 51 | 52 | It will most likely be `/dev/disk1` on most systems. 53 | 54 | Now we use `dd` to clone and `gzip` to compress it. Note that we reference the SD card by adding an `r` in front of the disk name. This speeds up the cloning considerably. 55 | 56 | ``` 57 | sudo dd if=/dev/rdisk1 conv=sparse bs=4m | gzip -9 > seedsigner.img.gz 58 | ``` 59 | 60 | The process should take about 15 minutes and will typically generate a roughly 1.1GB image. 61 | 62 | To restore from your backup image, just use the Raspberry Pi Imager. Remember that you can only write it to an SD card of equal or greater size than the original SD card. 63 | -------------------------------------------------------------------------------- /docs/electrum.md: -------------------------------------------------------------------------------- 1 | # SeedSigner Electrum seed phrase support 2 | 3 | SeedSigner supports loading of [Electrum's Segwit seed phrases](https://electrum.readthedocs.io/en/latest/seedphrase.html#electrum-seed-version-system). This is considered an Advanced feature that is disabled by default. 4 | 5 | To load an Electrum Segwit seed phrase, first enable Electrum seed support in Settings -> Advanced -> Electrum seed support. After this option is enabled, the user will now be able to enter an Electrum seed phrase by selecting "Enter Electrum seed" in the Load Seed screen. 6 | 7 | Some SeedSigner functionality is deliberately disabled when using an Electrum mnemonic: 8 | 9 | - BIP-85 child seeds 10 | - Not applicable for Electrum seed types 11 | - SeedQR backups 12 | - Since Electrum seeds are not supported by other SeedQR implementations, it would be dangerous to use SeedQR as a backup tool for Electrum seeds and is thus disabled 13 | - Custom derivations 14 | - Hard coded derivation path and script types in SeedSigner to match Electrum wallet software. These are m/0h for single sig and m/1h for multisig 15 | - User-chosen custom derivations are thus not supported for Electrum seeds 16 | -------------------------------------------------------------------------------- /docs/feature_roadmap.md: -------------------------------------------------------------------------------- 1 | # Feature Roadmap 2 | 3 | Current focus: v0.5.0 preview releases. 4 | 5 | *Note: It may or may not make sense to do minor bugfix preview releases along the way (e.g. 1.0 -> 1.1).* 6 | 7 | 8 | ## v0.5.0 Pre-Release 1.x 9 | * Scan SeedQR / CompactSeedQR 10 | * Add/Edit passphrase 11 | * View seed words w/configurable warnings 12 | * Export xpub w/configurable warnings and flow determined by Settings 13 | * Scan PSBT 14 | * Full PSBT review screens 15 | * "Full Spend" (no change) warning 16 | * Fully verify PSBT change addrs 17 | * Send signed PSBT via QR 18 | * QR display dimming/brightness UP/DOWN 19 | * Subset of configurable Settings; persistent Settings storage 20 | * SettingsQR integration proof-of-concept 21 | 22 | Screens will be functional but not necessarily in their final presentation state (icons, text, positioning, etc). 23 | 24 | 25 | ## v0.5.0 Pre-Release 2.x 26 | * Existing screen refinement (visual presentation, text, etc) 27 | * Create new seed via image entropy 28 | * Manual mnemonic seed word entry 29 | * 12th/24th word calc 30 | * SeedQR/CompactSeedQR manual transcription UI w/configurable UI style (dots vs grid) 31 | * Single sig address scan and verification 32 | * SettingsQR standalone UI refinement 33 | * Fix broken tests 34 | * All GUI Components support scrollable Screens 35 | 36 | 37 | ## v0.5.0 Pre-Release 3.x 38 | * Settings: I/O Test 39 | * Create new seed via dice rolls 40 | * Custom derivation paths in xpub export flow 41 | * QR display dimming/brightness, framerate, density(?) controls in transparent overlay 42 | * HRF partner logo on startup 43 | * Improve test suite coverage 44 | * Further existing screen refinement 45 | * "Final" bugfixes 46 | 47 | 48 | ## Initial v0.5.0 Release 49 | All of the above! 50 | 51 | 52 | ## Beyond v0.5.0 53 | These features will not be included in the initial v0.5.0 release and will have varying degrees of priority for subsequent releases (or possibly not at all). 54 | 55 | * Multisig wallet descriptor QR scan(?) and addr verification(?) 56 | * Sign taproot txs 57 | * Multi-language support (Transifex free for open source projects) 58 | * Multisig: sign PSBT with multiple keys at once. 59 | * Custom OS, possibly with swappable SD card PSBT and multisig wallet descriptor storage 60 | * Decoy game mode at launch (Snake, Tetris, Sudoku...?) 61 | * BIP-39 wordlists in additional languages 62 | * Address message signing 63 | * UI color scheme customization 64 | * Specify missing entropy for 12th/24th word calc 65 | 66 | 67 | # v0.6 and Beyond...? 68 | * Alternate hardware profile / touchscreen 69 | * PGP signer 70 | * Liquid? 71 | -------------------------------------------------------------------------------- /docs/img/Mini_Pill_Main_Photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/Mini_Pill_Main_Photo.jpg -------------------------------------------------------------------------------- /docs/img/Open_Pill_Mini_Models.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/Open_Pill_Mini_Models.JPG -------------------------------------------------------------------------------- /docs/img/Open_Pill_Mini_w_Faceplate.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/Open_Pill_Mini_w_Faceplate.JPG -------------------------------------------------------------------------------- /docs/img/Open_Pill_Models.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/Open_Pill_Models.JPG -------------------------------------------------------------------------------- /docs/img/Open_Pill_Star.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/Open_Pill_Star.JPG -------------------------------------------------------------------------------- /docs/img/Open_Pill_w_Comfort_Joystick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/Open_Pill_w_Comfort_Joystick.png -------------------------------------------------------------------------------- /docs/img/OrangePillMini_Thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/OrangePillMini_Thumb.jpg -------------------------------------------------------------------------------- /docs/img/Orange_Pill.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/Orange_Pill.JPG -------------------------------------------------------------------------------- /docs/img/Orange_Pill_Models.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/Orange_Pill_Models.JPG -------------------------------------------------------------------------------- /docs/img/Rugged_Pill_Thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/Rugged_Pill_Thumb.jpg -------------------------------------------------------------------------------- /docs/img/Simple_Pill_Models.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/Simple_Pill_Models.JPG -------------------------------------------------------------------------------- /docs/img/dice_entr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dice_entr.png -------------------------------------------------------------------------------- /docs/img/dice_pic1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dice_pic1.png -------------------------------------------------------------------------------- /docs/img/dice_pic2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dice_pic2.png -------------------------------------------------------------------------------- /docs/img/dice_pic3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dice_pic3.png -------------------------------------------------------------------------------- /docs/img/dice_type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dice_type.png -------------------------------------------------------------------------------- /docs/img/dicedoc/coleman_addresses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/coleman_addresses.png -------------------------------------------------------------------------------- /docs/img/dicedoc/coleman_change_addresses_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/coleman_change_addresses_1.png -------------------------------------------------------------------------------- /docs/img/dicedoc/coleman_change_addresses_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/coleman_change_addresses_2.png -------------------------------------------------------------------------------- /docs/img/dicedoc/coleman_entropy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/coleman_entropy.png -------------------------------------------------------------------------------- /docs/img/dicedoc/coleman_verify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/coleman_verify.png -------------------------------------------------------------------------------- /docs/img/dicedoc/coleman_zpub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/coleman_zpub.png -------------------------------------------------------------------------------- /docs/img/dicedoc/seedtool_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/seedtool_1.png -------------------------------------------------------------------------------- /docs/img/dicedoc/seedtool_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/seedtool_2.png -------------------------------------------------------------------------------- /docs/img/dicedoc/seedtool_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/seedtool_3.png -------------------------------------------------------------------------------- /docs/img/dicedoc/seedtool_addresses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/seedtool_addresses.png -------------------------------------------------------------------------------- /docs/img/dicedoc/seedtool_change_addresses_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/seedtool_change_addresses_1.png -------------------------------------------------------------------------------- /docs/img/dicedoc/seedtool_change_addresses_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/seedtool_change_addresses_2.png -------------------------------------------------------------------------------- /docs/img/dicedoc/seedtool_fingerprint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/seedtool_fingerprint.png -------------------------------------------------------------------------------- /docs/img/dicedoc/seedtool_zpub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/seedtool_zpub.png -------------------------------------------------------------------------------- /docs/img/dicedoc/sesi_dice_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/sesi_dice_1.png -------------------------------------------------------------------------------- /docs/img/dicedoc/sesi_dice_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/sesi_dice_2.png -------------------------------------------------------------------------------- /docs/img/dicedoc/sesi_export_xpub_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/sesi_export_xpub_1.png -------------------------------------------------------------------------------- /docs/img/dicedoc/sesi_export_xpub_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/sesi_export_xpub_2.png -------------------------------------------------------------------------------- /docs/img/dicedoc/sesi_finger_print.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/sesi_finger_print.png -------------------------------------------------------------------------------- /docs/img/dicedoc/sesi_seed_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/sesi_seed_1.png -------------------------------------------------------------------------------- /docs/img/dicedoc/sesi_seed_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/sesi_seed_2.png -------------------------------------------------------------------------------- /docs/img/dicedoc/sesi_tools_dice_seed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/sesi_tools_dice_seed.png -------------------------------------------------------------------------------- /docs/img/dicedoc/sparrow_wallet_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/sparrow_wallet_1.png -------------------------------------------------------------------------------- /docs/img/dicedoc/sparrow_wallet_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/sparrow_wallet_2.png -------------------------------------------------------------------------------- /docs/img/dicedoc/sparrow_zpub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/dicedoc/sparrow_zpub.png -------------------------------------------------------------------------------- /docs/img/legacy_hardware_remapped_pins.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/legacy_hardware_remapped_pins.jpg -------------------------------------------------------------------------------- /docs/img/legacy_hardware_remapped_pins_photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/legacy_hardware_remapped_pins_photo.jpg -------------------------------------------------------------------------------- /docs/img/smartcard_pn532_headerconnection.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/smartcard_pn532_headerconnection.jpg -------------------------------------------------------------------------------- /docs/img/smartcard_usb_readers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/smartcard_usb_readers.png -------------------------------------------------------------------------------- /docs/img/usb_relay_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/usb_relay_01.png -------------------------------------------------------------------------------- /docs/img/usb_relay_linux_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/usb_relay_linux_01.png -------------------------------------------------------------------------------- /docs/img/usb_relay_linux_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/usb_relay_linux_02.png -------------------------------------------------------------------------------- /docs/img/usb_relay_mac_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/usb_relay_mac_01.png -------------------------------------------------------------------------------- /docs/img/usb_relay_mac_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/usb_relay_mac_02.png -------------------------------------------------------------------------------- /docs/img/usb_relay_mac_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/usb_relay_mac_03.png -------------------------------------------------------------------------------- /docs/img/usb_relay_mac_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/img/usb_relay_mac_04.png -------------------------------------------------------------------------------- /docs/legacy_hardware.md: -------------------------------------------------------------------------------- 1 | # Legacy Hardware Modifications 2 | 3 | Older Raspberry Pi devices have a smaller GPIO header, 26 pin as opposed to the 40 pin header found on more recent models. The Waveshare LCD hat uses some of the pins between 26 and 40 for the buttons, meaning that these will need to be reassigned (and remapped in software) when used with an older device. 4 | 5 | ## Hardware Changes 6 | 7 | A suggested remapping can be found here: 8 |  9 | 10 | This remapping can be done by soldering wires on to the Waveshare hat as below: 11 |  12 | 13 | Alternatively, you could do this by connecting the LCD hat on with individual breadboard jumper wires. 14 | 15 | Once you have re-mapped the pins, it is advised to do an IO Test to ensure that everything works. 16 | 17 | **Warning: Some of the GPIO pins on contain 5 volt output. Raspberry Pi GPIO pins are NOT 5V tolerant, meaning that if you accidentally connect a 5V supply pin to a GPIO input pin, you risk permanent damage.** 18 | 19 | ## Software Changes 20 | The Seedsigner software will automatically detect which hardware revision you are using and if the older hardware is detected, will remap the software to match the above modifications to the Waveshare hat. 21 | 22 | If you are using a pre-built Seedsigner image that hasn't yet has this incorporated, you can simply take the file "buttons.py" (/src/seedsigner/hardware/ int his repository) and overwrite same file on your Seedsigner SD card. (The easiest way to do this is to copy it on to the /boot/ partitition of the SD card then copy it over via the command line while connected to your Pi via monitor+keyboard) O -------------------------------------------------------------------------------- /docs/qr_formats.md: -------------------------------------------------------------------------------- 1 | # QR Formats 2 | 3 | ### Scanning QR Codes 4 | 5 | SeedSigner supports scanning the following QR formats 6 | 7 | Animated QR Formats: 8 | - PSBT 9 | - Blockchain Commons [UR2 PSBT](https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-006-urtypes.md) 10 | - Specter Desktop base64 segments animated 11 | - [Legacy UR](https://github.com/BlockchainCommons/Research/blob/d4d72417a1ff18f9422371b2f71bf2652adce41c/papers/bcr-2020-005-ur.md) Format that should no longer be used 12 | 13 | Static QR Formats: 14 | - PSBT 15 | - Base64 (if the PSBT byte size is too large, SS may have trouble scanning) 16 | - Seed 17 | - [SeedSigner SeedQR](seed_qr/README.md) format 18 | - A 48 or 96 length string of numbers representing a bip39 wordlist (all wordlist languages supported). The numeric sequence is a concatenation of four-digit, zero-padded segments. Each four-digit segment represents a bip39 word expressed by a zero-indexed position in the wordlist. For example "0000" is abandon in the english bip39 wordlist. 19 | - [SeedSigner CompactSeedQR](seed_qr/README.md) format 20 | - The 128- or 256-bit entropy encoded as a binary QR. 21 | - English Bip39 Mnemonic words separated by a space. Currently only supports 12 and 24 word seeds. 22 | - English Bip39 Mnemonic with only first 4 letters seperated by a space. Currently only supports 12 and 24 word seeds. 23 | 24 | ### Displaying QR Codes 25 | 26 | SeedSigner supports displaying QR's in the following formats 27 | 28 | Animated QR Formats: 29 | - PSBT 30 | - Blockchain Commons [UR2 PSBT](https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-006-urtypes.md) 31 | - Specter Desktop base64 segments animated 32 | 33 | Static QR Formats: 34 | - Seed 35 | - SeedSigner Seed QR format 36 | - A 48 or 96 length string of numbers representing a bip39 wordlist (all wordlist languages supported). The numeric sequence is a concatination of four-digit zero padded segments. Each four-digit segment represents a bip39 word expressed by a zero-indexed position in the wordlist. For example "0000" is abandon in the english bip39 wordlist. 37 | -------------------------------------------------------------------------------- /docs/recovery.md: -------------------------------------------------------------------------------- 1 | # SeedSigner Recovery Information 2 | 3 | SeedSigner creates extended public keys for the coordinator following [slip-0132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) to encode the extended public key. SeedSigner supports native and nested segwit for single sig and multisig. 4 | 5 | Derivation paths for standard script types for mainnet: 6 | 7 | - Single Sig 8 | - Native Segwit 9 | - Derivation Path: m/84'/0'/0' 10 | - Script Type: P2WPKH 11 | - Public Key Encoding: 0x04b24746 - zpub 12 | - Nested Segwit 13 | - Derivation Path: m/49'/0'/0' 14 | - Script Type: P2WPKH in P2SH 15 | - Public Key Encoding: 0x049d7cb2 - ypub 16 | - Taproot 17 | - Derivation Path: m/86'/0'/0' 18 | - Script Type: P2TR 19 | - Public Key Encoding: 0x0488b21e - xpub 20 | - Multisig 21 | - Native Segwit 22 | - Derivation Path: m/48'/0'/0'/2' 23 | - Script Type: P2WSH 24 | - Public Key Encoding: 0x02aa7ed3 - Zpub 25 | - Nested Segwit 26 | - Derivation Path: m/48'/0'/0'/1' 27 | - Script Type: P2WSH in P2SH 28 | - Public Key Encoding: 0x0295b43f - Ypub 29 | 30 | Custom derivation paths are also optional when generating an xpub from SeedSigner. The Public Key Encodings are detected based on the derivation path configured. The `embit` bitcoin library does this detection and is documented [here](https://github.com/diybitcoinhardware/embit/blob/master/docs/api/bip32.md#detect_version). For a video explanation of these standards see a presentation by Stepan of `embit` on this topic: https://youtube.com/watch?v=JCaC5DG2HTM 31 | 32 | Changing the network settings from main to test in SeedSigner will change the public key encoding and derivation path following [slip-0132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) standards. 33 | 34 | Related Standards: 35 | - [bip-0032](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) 36 | - [bip-0044](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) 37 | - [bip-0048](https://github.com/bitcoin/bips/blob/master/bip-0048.mediawiki) 38 | - [bip-0049](https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki) 39 | - [bip-0084](https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki) 40 | - [bip-0086](https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki) 41 | -------------------------------------------------------------------------------- /docs/seed_qr/img/compact_12word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/compact_12word.png -------------------------------------------------------------------------------- /docs/seed_qr/img/compact_24word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/compact_24word.png -------------------------------------------------------------------------------- /docs/seed_qr/img/handmade_qr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/handmade_qr.jpg -------------------------------------------------------------------------------- /docs/seed_qr/img/phone_screenshot_compact.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/phone_screenshot_compact.jpg -------------------------------------------------------------------------------- /docs/seed_qr/img/phone_screenshot_standard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/phone_screenshot_standard.jpg -------------------------------------------------------------------------------- /docs/seed_qr/img/qrcode_capacity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/qrcode_capacity.png -------------------------------------------------------------------------------- /docs/seed_qr/img/seedqr_plate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/seedqr_plate.jpg -------------------------------------------------------------------------------- /docs/seed_qr/img/standard_12word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/standard_12word.png -------------------------------------------------------------------------------- /docs/seed_qr/img/standard_24word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/standard_24word.png -------------------------------------------------------------------------------- /docs/seed_qr/img/standard_24word_with_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/standard_24word_with_logo.png -------------------------------------------------------------------------------- /docs/seed_qr/img/vector1_compact_24word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/vector1_compact_24word.png -------------------------------------------------------------------------------- /docs/seed_qr/img/vector1_standard_24word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/vector1_standard_24word.png -------------------------------------------------------------------------------- /docs/seed_qr/img/vector2_compact_24word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/vector2_compact_24word.png -------------------------------------------------------------------------------- /docs/seed_qr/img/vector2_standard_24word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/vector2_standard_24word.png -------------------------------------------------------------------------------- /docs/seed_qr/img/vector3_compact_24word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/vector3_compact_24word.png -------------------------------------------------------------------------------- /docs/seed_qr/img/vector3_standard_24word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/vector3_standard_24word.png -------------------------------------------------------------------------------- /docs/seed_qr/img/vector4_compact_12word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/vector4_compact_12word.png -------------------------------------------------------------------------------- /docs/seed_qr/img/vector4_standard_12word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/vector4_standard_12word.png -------------------------------------------------------------------------------- /docs/seed_qr/img/vector5_compact_12word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/vector5_compact_12word.png -------------------------------------------------------------------------------- /docs/seed_qr/img/vector5_standard_12word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/vector5_standard_12word.png -------------------------------------------------------------------------------- /docs/seed_qr/img/vector6_compact_12word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/vector6_compact_12word.png -------------------------------------------------------------------------------- /docs/seed_qr/img/vector6_standard_12word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/vector6_standard_12word.png -------------------------------------------------------------------------------- /docs/seed_qr/img/vector7_compact_12word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/vector7_compact_12word.png -------------------------------------------------------------------------------- /docs/seed_qr/img/vector8_compact_12word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/vector8_compact_12word.png -------------------------------------------------------------------------------- /docs/seed_qr/img/vector9_compact_12word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/vector9_compact_12word.png -------------------------------------------------------------------------------- /docs/seed_qr/img/zxing_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/img/zxing_screenshot.png -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/21x21_A4_trading_card_2sided.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/21x21_A4_trading_card_2sided.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/21x21_letter_trading_card_2sided.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/21x21_letter_trading_card_2sided.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/25x25_A4_trading_card_2sided.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/25x25_A4_trading_card_2sided.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/25x25_letter_trading_card_2sided.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/25x25_letter_trading_card_2sided.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/29x29_A4_trading_card_2sided.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/29x29_A4_trading_card_2sided.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/29x29_letter_trading_card_2sided.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/29x29_letter_trading_card_2sided.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/dots_21x21.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/dots_21x21.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/dots_25x25.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/dots_25x25.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/dots_29x29.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/dots_29x29.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/grid_21x21.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/grid_21x21.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/grid_25x25.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/grid_25x25.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/grid_29x29.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/grid_29x29.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/grid_wfingerprint_21x21.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/grid_wfingerprint_21x21.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/grid_wfingerprint_21x21_4-up.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/grid_wfingerprint_21x21_4-up.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/grid_wfingerprint_25x25.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/grid_wfingerprint_25x25.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/grid_wfingerprint_29x29.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/grid_wfingerprint_29x29.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/mnemonic_12words_wfingerprint.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/mnemonic_12words_wfingerprint.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/mnemonic_12words_wfingerprint_4up.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/mnemonic_12words_wfingerprint_4up.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/mnemonic_24word_wfingerprint.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/mnemonic_24word_wfingerprint.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/mnemonic_24word_wfingerprint_4-up.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/mnemonic_24word_wfingerprint_4-up.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/trading_card_21x21_w12words.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/trading_card_21x21_w12words.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/trading_card_25x25_w24words.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/trading_card_25x25_w24words.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/trading_card_29x29_w24words.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/trading_card_29x29_w24words.pdf -------------------------------------------------------------------------------- /docs/seed_qr/printable_templates/xpub_wfingerprint.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/docs/seed_qr/printable_templates/xpub_wfingerprint.pdf -------------------------------------------------------------------------------- /enclosures/look_screws_pill/Bottom.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/look_screws_pill/Bottom.stl -------------------------------------------------------------------------------- /enclosures/look_screws_pill/ButtonsConnected.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/look_screws_pill/ButtonsConnected.stl -------------------------------------------------------------------------------- /enclosures/look_screws_pill/README.md: -------------------------------------------------------------------------------- 1 | Motivation for this design: Faceplate screws could look cool. Judge for yourself! 2 | 3 | It's work that's built on top of giants (@gobrrrme and @blackcoffee). 4 | 5 | The main chassis has the properties of a battle-tested enclosure called SimplePill by @blackcoffee and connected buttons are a slightly reworked design done by @gobrrrme. 6 | 7 | What distinguishes this design are the visible screws on top of the enclosure and a presspad. 8 | 9 | This design requires: 10 | * 4x 10mm M2.5 risers 11 | * 8x 10mm M2.5 screws 12 | 13 | The buttons and presspad HAVE to be printed out of TPU filament. The Top and Bottom are best from PLA but PETG will work just as well. The presspad has a slightly smaller cavity for the stubby nub of the waveshare hat. DO NOT PANIC! It's done on purpose to create friction for the pad to hold on the nub. 14 | 15 |  16 | -------------------------------------------------------------------------------- /enclosures/look_screws_pill/Top.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/look_screws_pill/Top.stl -------------------------------------------------------------------------------- /enclosures/look_screws_pill/pressPad.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/look_screws_pill/pressPad.stl -------------------------------------------------------------------------------- /enclosures/open_pill/README.md: -------------------------------------------------------------------------------- 1 | ## The Open Pill Enclosure 2 | 3 | The "Open Pill" was the second officially released SeedSigner enclosure. Given some of the challenges associated with the "Orange Pill", the Open Pill was designed for quick, inexpensive, and simple deployment. No secondary hardware components are required for assembly and the design consists of a single printed part that can be produced with even basic 3D printers. This simpler design was intended to put the focus back on the project's software, which was improving by leaps and bounds when the enclosure was released. 4 | 5 | 6 | 7 | ### Characterisics: 8 | - Supported Camera? Legacy RPi Camera (w/ gold Pi Zero Cable) 9 | - Supported HAT? Waveshare 240x240 pixel LCD display + controls 10 | - Recommended printing process? FDM 11 | - Recommended printing materials? PLA 12 | - Secondary Hardware Required? No 13 | - Data-enabled USB Port accessible? Yes 14 | - Mini-HDMI port accessible? Yes 15 | - Removeable Memory Card? Yes 16 | - Comfortable controls? No (see note below) 17 | 18 | ### Assembly Demonstration: 19 | https://www.youtube.com/watch?v=gXPFJygZobE 20 | 21 | 22 | ### Comfortable Controls Optional Upgrade: 23 | The bare joystick on an open pill or open pill mini design can be uncomfortable for some thumbs. Twitter user @Vulcan21com developed a DIY solution to add comfort to the exposed joystick using a M2.5 knurled nut standardized under DIN 466, widely available at many hardware stores. The joystick itself has no threading, but it is possible to screw the M2.5 nut onto the joystick with a bit of precision and patience. While screwing it on for the first time it will cut a light thread into the plastic. Be gentle as to not transfer too much torque into the joystick. The result will look like this: 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /enclosures/open_pill/design_file/open_pill.f3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/open_pill/design_file/open_pill.f3d -------------------------------------------------------------------------------- /enclosures/open_pill/print_file/open_pill.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/open_pill/print_file/open_pill.stl -------------------------------------------------------------------------------- /enclosures/open_pill_mini/Main_Chassis.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/open_pill_mini/Main_Chassis.stl -------------------------------------------------------------------------------- /enclosures/open_pill_mini/README.md: -------------------------------------------------------------------------------- 1 | ## The Open Pill Mini Enclosure 2 | 3 | The "Open Pill Mini" is simply a smaller version of the original "Open Pill" that is designed to incorporate a more compact camera specifically designed for the Raspberry Pi Zero -- this enclosure is just about as small as a SeedSigner can conceivably get. It is pictured below, alongside the original Open Pill enclosure. 4 | 5 | 6 | 7 | ### Characterisics: 8 | - Supported Camera? "ZeroCam" designed specifically for RPi Zero 9 | - Supported HAT? Waveshare 240x240 pixel LCD display + controls 10 | - Recommended printing process? FDM 11 | - Recommended printing materials? PLA 12 | - Secondary Hardware Required? No 13 | - Data-enabled USB Port accessible? Yes 14 | - Mini-HDMI port accessible? Yes 15 | - Removeable Memory Card? Yes 16 | - Comfortable controls? No (see note below) 17 | 18 | ### Assembly Demonstration: 19 | (assembly is very similar to the original Open Pill assembly, depicted below) 20 | https://www.youtube.com/watch?v=aIIc2DiZYcI 21 | 22 | ### Comfortable Controls Optional Upgrade: 23 | The bare joystick on an open pill or open pill mini design can be uncomfortable for some thumbs. Twitter user @Vulcan21com developed a DIY solution to add comfort to the exposed joystick using a M2.5 knurled nut standardized under DIN 466, widely available at many hardware stores. The joystick itself has no threading, but it is possible to screw the M2.5 nut onto the joystick with a bit of precision and patience. While screwing it on for the first time it will cut a light thread into the plastic. Be gentle as to not transfer too much torque into the joystick. The result will look like this: 24 | 25 | 26 | (image displayed is Open Pill, not Open Pill Mini) 27 | -------------------------------------------------------------------------------- /enclosures/open_pill_mini_w_coverplate/Buttons.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/open_pill_mini_w_coverplate/Buttons.stl -------------------------------------------------------------------------------- /enclosures/open_pill_mini_w_coverplate/Lid.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/open_pill_mini_w_coverplate/Lid.stl -------------------------------------------------------------------------------- /enclosures/open_pill_mini_w_coverplate/Main_Chassis.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/open_pill_mini_w_coverplate/Main_Chassis.stl -------------------------------------------------------------------------------- /enclosures/open_pill_mini_w_coverplate/README.md: -------------------------------------------------------------------------------- 1 | ## The Open Pill Mini w/ Coverplate Enclosure 2 | 3 | The "Open Pill Mini w/ Coverplate" was designed as an attempt to incorporate the best of the "Orange Pill" and the "Open Pill" enclosures into a single design. It is fully enclosed for improved aesthetics and protection of electronic components, yet is compact and can be manufactured with a simple FDM printer, also not requiring any additional hardware components. A first for this enclosure was preventing access to the SeedSigner's data-enabled USB port as an additional security assurance for users. 4 | 5 | 6 | 7 | ### Characterisics: 8 | - Supported Camera? "ZeroCam" designed specifically for RPi Zero 9 | - Supported HAT? Waveshare 240x240 pixel LCD display + controls 10 | - Recommended printing process? FDM for enclosure & controls 11 | - Recommended printing materials? PLA for enclosure, TPU for controls 12 | - Secondary Hardware Required? No 13 | - Data-enabled USB Port accessible? No 14 | - Mini-HDMI port accessible? No 15 | - Removeable Memory Card? Yes 16 | - Comfortable controls? Yes 17 | 18 | ### Assembly Demonstration: 19 | https://www.youtube.com/watch?v=6-5cDneXoWs 20 | -------------------------------------------------------------------------------- /enclosures/open_pill_mini_w_coverplate/Thumbstick_PLA.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/open_pill_mini_w_coverplate/Thumbstick_PLA.stl -------------------------------------------------------------------------------- /enclosures/open_pill_mini_w_coverplate/Thumbstick_TPU.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/open_pill_mini_w_coverplate/Thumbstick_TPU.stl -------------------------------------------------------------------------------- /enclosures/orange_pill/README.md: -------------------------------------------------------------------------------- 1 | ## The Orange Pill Enclosure 2 | 3 | The "Orange Pill" was the first SeedSigner enclosure. It's eye-catching design is the enclosure most commonly associated with the SeedSigner project for many. While aesthetically appealing and fun to showcase to others, the thumbstick topper's sub-optimal design, the need for secondary hardware components, and other drawbacks make this enclosure a less desirable choice for every day SeedSigner use. 4 | 5 | 6 | 7 | ### Characterisics: 8 | - Supported Camera? Legacy RPi Camera (w/ gold Pi Zero Cable) 9 | - Supported HAT? Waveshare 240x240 pixel LCD display + controls 10 | - Recommended printing process? FDM for enclosure, SLA for controls 11 | - Recommended printing materials? PLA for enclosure, resin for controls 12 | - Secondary Hardware Required? Yes 13 | - Data-enabled USB Port accessible? Yes 14 | - Mini-HDMI port accessible? Yes 15 | - Removeable Memory Card? Requires partial disassembly 16 | - Comfortable controls? Yes (but thumbstick can be clumsy) 17 | 18 | ### Secondary Hardware Required: 19 | - Four (4): 10mm M2.5 F-F spacers 20 | - Four (4): 6mm M2.5 screws 21 | - Four (4): 12mm M2.5 screws 22 | 23 | ### Assembly Demonstration: 24 | https://www.youtube.com/watch?v=aIIc2DiZYcI 25 | -------------------------------------------------------------------------------- /enclosures/orange_pill/design_files/OP_Lower.f3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/orange_pill/design_files/OP_Lower.f3d -------------------------------------------------------------------------------- /enclosures/orange_pill/design_files/OP_Upper.f3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/orange_pill/design_files/OP_Upper.f3d -------------------------------------------------------------------------------- /enclosures/orange_pill/design_files/button.f3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/orange_pill/design_files/button.f3d -------------------------------------------------------------------------------- /enclosures/orange_pill/design_files/joystiick.f3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/orange_pill/design_files/joystiick.f3d -------------------------------------------------------------------------------- /enclosures/orange_pill/print_files/OP_Lower.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/orange_pill/print_files/OP_Lower.stl -------------------------------------------------------------------------------- /enclosures/orange_pill/print_files/OP_Upper.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/orange_pill/print_files/OP_Upper.stl -------------------------------------------------------------------------------- /enclosures/orange_pill/print_files/button.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/orange_pill/print_files/button.stl -------------------------------------------------------------------------------- /enclosures/orange_pill/print_files/joystick.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/orange_pill/print_files/joystick.stl -------------------------------------------------------------------------------- /enclosures/orange_pill_mini/OrangePillMini_Bottom.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/orange_pill_mini/OrangePillMini_Bottom.3mf -------------------------------------------------------------------------------- /enclosures/orange_pill_mini/OrangePillMini_Buttons.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/orange_pill_mini/OrangePillMini_Buttons.stl -------------------------------------------------------------------------------- /enclosures/orange_pill_mini/OrangePillMini_Top.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/orange_pill_mini/OrangePillMini_Top.stl -------------------------------------------------------------------------------- /enclosures/orange_pill_mini/README.md: -------------------------------------------------------------------------------- 1 | ## The Orange Pill Mini Enclosure 2 | 3 | 4 | 5 | - The Orange Pill Mini is a smaller, hommage to the original Orange Pill enclosure. 6 | - Hardware free build, no screws or spacers needed. 7 | - This enclosure requires use of the so called "zerocam", which allows for a more compact form factor. 8 | - The enclosure offers a very comfortable handling experience. 9 | - Top and bottom part snap fit together. 10 | - SD-card removable 11 | - Data port is closed. 12 | 13 | 14 | ### Printing tips 15 | - OrangePillMini_Bottom.3mf is ready for multicolor printing but can also be printed in a single color. 16 | - Files are preoriented for optimal printing, no supports required. 17 | - Controls - buttons, thumbstick and trackball work best if printed from TPU. 18 | - The included controls can be printed from PLA, but TPU is recommended. 19 | - Recommended shore hardness is 95A, but any TPU will do. 20 | -------------------------------------------------------------------------------- /enclosures/orange_pill_mini/ThumbStick_small.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/orange_pill_mini/ThumbStick_small.stl -------------------------------------------------------------------------------- /enclosures/orange_pill_mini/Trackball.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/orange_pill_mini/Trackball.stl -------------------------------------------------------------------------------- /enclosures/pushcase/3mf/Bottom - Push Case - SeedSigner.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/3mf/Bottom - Push Case - SeedSigner.3mf -------------------------------------------------------------------------------- /enclosures/pushcase/3mf/Buttons - Push Case - SeedSigner.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/3mf/Buttons - Push Case - SeedSigner.3mf -------------------------------------------------------------------------------- /enclosures/pushcase/3mf/Middle - Push Case - SeedSigner.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/3mf/Middle - Push Case - SeedSigner.3mf -------------------------------------------------------------------------------- /enclosures/pushcase/3mf/Soldering Support - Push Case - SeedSigner.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/3mf/Soldering Support - Push Case - SeedSigner.3mf -------------------------------------------------------------------------------- /enclosures/pushcase/3mf/Top - Push Case - SeedSigner.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/3mf/Top - Push Case - SeedSigner.3mf -------------------------------------------------------------------------------- /enclosures/pushcase/README.md: -------------------------------------------------------------------------------- 1 | # The Push Case 2 | 3 | 4 | 5 | Designer: @kayth21 6 | 7 | ## Motivation 8 | 9 | The motivation for this design was to get a thin, screwless and good looking enclosure with minimal secondary hardware requirements. 10 | 11 | ## Characterisics 12 | 13 | - No screws needed 14 | - Soldering required 15 | - Mini-HDMI port not accessible 16 | - Removeable memory card accessible 17 | - Data-enabled USB port not accessible 18 | 19 | ## Hardware restrictions 20 | 21 | - Raspberry Pi Zero without pre-installed GPIO pins required. 22 | - Raspberry Pi Camera in smaller "Zero" style version required. 23 | 24 | ## Secondary hardware requirements 25 | 26 | - 40x pins (6mm / 0.04in) 27 | - Soldering iron and solder 28 | 29 | ## 3D prints and screwless design 30 | 31 | Please note that the prints must be accurate in order to fit tightly without screws and for the buttons to perform smoothly. The prints are well tested with a Prusa MK3s (.3mf files attached), Prusament PLA and hardware components from early 2024. With this setup, the enclosure holds strongly together, but can still be opened again with a little effort (although this is not recommended, as it may not hold together as tightly afterwards). Other printers or newer or older hardware may produce results that do not fit and in worst case damage the hardware. 32 | 33 | ## Known issues 34 | 35 | - Bambu Studio Slicer: There is a strange problem when slicing the buttons. A few layers are missing. As a workaround you can use another slicer, e.g. the Prusa Slicer. 36 | 37 | ## Assembly 38 | 39 | ### 1. Get all the parts 40 | 41 | 42 | 43 | - Raspberry Pi Zero v.1.3 44 | - Waveshare LCD Display (240x240) 45 | - Camera for Raspberry Pi Zero 46 | - 40 pins (6mm / 0.04in, cut them to size if you can only find larger ones) 47 | - Soldering iron and solder (not on picture) 48 | - Soldering support (3D print) 49 | - Top, middle, back and buttons (3D prints) 50 | - Tweezers (if available - not on picture) 51 | 52 | ### 2. Place the display and the Raspberry Pi on the soldering support 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | - Remove the screen protection of the LCD display. 62 | - Place the display face down on the soldering support. 63 | - Place the middle support on the display. 64 | - Place the Raspberry Pi face down on the middle support. 65 | 66 | ### 3. Solder the pins 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | - Insert the 40 pins with the help of tweezers and push them all the way down. 75 | - Solder the pins (please pay attention not to melt the orange support elements!). 76 | - Optional: Pull out the Raspberry Pi to check that the pins are holding properly after soldering. 77 | 78 | ### 4. Attach camera 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | - Carefully pull out the camera cable mount (ideally with the support of a tool). 87 | - Plug in the camera cable (it doesn't go very deep). 88 | - Close the camera cable mount to fix the cable. 89 | 90 | ### 5. Stick on the back part 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | - Remove the camera protection. 99 | - Bend the camera cable into position (do not use the adhesive tape!). 100 | - Place the back part on the signer, but do not push it in completely yet. 101 | - Align the camera precisely with the help of a tool. 102 | - Push the back part in completely (the camera should fit perfectly and have no more play). 103 | 104 | ### 6. Stick on the front part 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | - Remove the soldering support. 114 | - Insert the 3 buttons into the front. 115 | - Press the front completely into the signer (hold the device upside down so that the buttons do not fall out). 116 | - Plug in the thumbstick. 117 | 118 | ### 7. Flash a MicroSD card 119 | 120 | - Please follow the documentation on [SeedSigner.com](https://seedsigner.com). 121 | -------------------------------------------------------------------------------- /enclosures/pushcase/images/back1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/images/back1.png -------------------------------------------------------------------------------- /enclosures/pushcase/images/back2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/images/back2.png -------------------------------------------------------------------------------- /enclosures/pushcase/images/camera1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/images/camera1.png -------------------------------------------------------------------------------- /enclosures/pushcase/images/camera2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/images/camera2.png -------------------------------------------------------------------------------- /enclosures/pushcase/images/camera3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/images/camera3.png -------------------------------------------------------------------------------- /enclosures/pushcase/images/camera4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/images/camera4.png -------------------------------------------------------------------------------- /enclosures/pushcase/images/camera5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/images/camera5.png -------------------------------------------------------------------------------- /enclosures/pushcase/images/components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/images/components.png -------------------------------------------------------------------------------- /enclosures/pushcase/images/front1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/images/front1.png -------------------------------------------------------------------------------- /enclosures/pushcase/images/front2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/images/front2.png -------------------------------------------------------------------------------- /enclosures/pushcase/images/front3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/images/front3.png -------------------------------------------------------------------------------- /enclosures/pushcase/images/front4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/images/front4.png -------------------------------------------------------------------------------- /enclosures/pushcase/images/pushcase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/images/pushcase.png -------------------------------------------------------------------------------- /enclosures/pushcase/images/solder1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/images/solder1.png -------------------------------------------------------------------------------- /enclosures/pushcase/images/solder2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/images/solder2.png -------------------------------------------------------------------------------- /enclosures/pushcase/images/solder3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/images/solder3.png -------------------------------------------------------------------------------- /enclosures/pushcase/images/stack1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/images/stack1.png -------------------------------------------------------------------------------- /enclosures/pushcase/images/stack2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/images/stack2.png -------------------------------------------------------------------------------- /enclosures/pushcase/images/stack3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/images/stack3.png -------------------------------------------------------------------------------- /enclosures/pushcase/images/stack4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/images/stack4.png -------------------------------------------------------------------------------- /enclosures/pushcase/stl/Bottom - Push Case - SeedSigner.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/stl/Bottom - Push Case - SeedSigner.stl -------------------------------------------------------------------------------- /enclosures/pushcase/stl/Button - Push Case - SeedSigner.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/stl/Button - Push Case - SeedSigner.stl -------------------------------------------------------------------------------- /enclosures/pushcase/stl/Middle - Push Case - SeedSigner.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/stl/Middle - Push Case - SeedSigner.stl -------------------------------------------------------------------------------- /enclosures/pushcase/stl/Soldering Support - Push Case - SeedSigner.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/stl/Soldering Support - Push Case - SeedSigner.stl -------------------------------------------------------------------------------- /enclosures/pushcase/stl/Thumbstick - Push Case - SeedSigner.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/stl/Thumbstick - Push Case - SeedSigner.stl -------------------------------------------------------------------------------- /enclosures/pushcase/stl/Top - Push Case - SeedSigner.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/pushcase/stl/Top - Push Case - SeedSigner.stl -------------------------------------------------------------------------------- /enclosures/rugged_pill/Multimaterial/RuggedPIll_Bottom_MMU.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/rugged_pill/Multimaterial/RuggedPIll_Bottom_MMU.3mf -------------------------------------------------------------------------------- /enclosures/rugged_pill/Multimaterial/RuggedPill_Top_MMU.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/rugged_pill/Multimaterial/RuggedPill_Top_MMU.3mf -------------------------------------------------------------------------------- /enclosures/rugged_pill/README.md: -------------------------------------------------------------------------------- 1 | ## The Rugged Pill Enclosure 2 | 3 | 4 | 5 | - The Rugged Pill is an enclosure designed for building a SeedSigner without the need for screws or spacers. 6 | - This enclosure requires use of the so called "zerocam", which allows for a more compact form factor. 7 | - The enclosure offers a very comfortable handling experience. 8 | - The included controls can be printed from PLA, but TPU is recommended. 9 | - Recommended shore hardness is 95A, but any TPU will do. 10 | - Separate files are available for single and multicolor printing. 11 | - Top and bottom part snap fit together. 12 | - SD-card removable 13 | - Data port is closed. (Use the separate file labeled 4_SeedHammer) 14 | 15 | 16 | ### File overwiev 17 | 18 | #### Single color: 19 | - RuggedPill_Top.stl[^1] 20 | - RuggedPill_Bottom.stl[^1] 21 | - RuggedPillBottom_4SeedHammer.stl[^1] 22 | 23 | - RuggedPill_Buttons.stl[^2] 24 | - RuggedPill_Thumbstick.stl[^2] 25 | - Rugged_Pill_Trackball.stl[^2] 26 | 27 | #### Multimaterial: 28 | - RuggedPill_Top_MMU.3mf 29 | - RuggedPill_Bottom_MMU.3mf 30 | 31 | You only need one set of buttons and a Thumbstick or Trackball, print both since they use minmal material. Test which one you like best and use that. 32 | 33 | [^1]: (Recommended Materials: PLA, PETG) 34 | [^2]: (Recommended Materials: TPU) 35 | -------------------------------------------------------------------------------- /enclosures/rugged_pill/RuggedPillBottom_4SeedHammer.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/rugged_pill/RuggedPillBottom_4SeedHammer.stl -------------------------------------------------------------------------------- /enclosures/rugged_pill/RuggedPill_Bottom.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/rugged_pill/RuggedPill_Bottom.stl -------------------------------------------------------------------------------- /enclosures/rugged_pill/RuggedPill_Buttons.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/rugged_pill/RuggedPill_Buttons.stl -------------------------------------------------------------------------------- /enclosures/rugged_pill/RuggedPill_Thumbstick.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/rugged_pill/RuggedPill_Thumbstick.stl -------------------------------------------------------------------------------- /enclosures/rugged_pill/RuggedPill_Top.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/rugged_pill/RuggedPill_Top.stl -------------------------------------------------------------------------------- /enclosures/rugged_pill/RuggedPill_Trackball.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/rugged_pill/RuggedPill_Trackball.stl -------------------------------------------------------------------------------- /enclosures/simple_pill/README.md: -------------------------------------------------------------------------------- 1 | ## The Simple Pill Enclosure 2 | 3 | The "Simple Pill" is a community-contributed design by @blackcoffeebtc. The most significant innovation of this design was the adaptation of a DPAD-stype thumbstick topper for a SeedSigner enclosure, offering a much more comfortable and responsive control experience. The Simple Pill was also the first enclosure to allow for easy access to the device's memory card. 4 | 5 | 6 | 7 | ### Characterisics: 8 | - Supported Camera? Legacy RPi Camera (w/ gold Pi Zero Cable) 9 | - Supported HAT? Waveshare 240x240 pixel LCD display + controls 10 | - Recommended printing process? FDM for enclosure, SLA for controls 11 | - Recommended printing materials? PLA for enclosure, resin for controls 12 | - Secondary Hardware Required? Yes 13 | - Data-enabled USB Port accessible? Yes 14 | - Mini-HDMI port accessible? Yes 15 | - Removeable Memory Card? Yes 16 | - Comfortable controls? Yes 17 | 18 | ### Secondary Hardware Required: 19 | - Four (4): 10mm M2.5 F-F spacers 20 | - Four (4): 6mm M2.5 screws 21 | - Four (4): 12mm M2.5 screws 22 | 23 | ### Assembly Demonstration: 24 | (assembly is similar to that of the original Orange Pill) 25 | https://www.youtube.com/watch?v=aIIc2DiZYcI 26 | -------------------------------------------------------------------------------- /enclosures/simple_pill/protective_case/sp_protective_case_lower.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/simple_pill/protective_case/sp_protective_case_lower.stl -------------------------------------------------------------------------------- /enclosures/simple_pill/protective_case/sp_protective_case_upper.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/simple_pill/protective_case/sp_protective_case_upper.stl -------------------------------------------------------------------------------- /enclosures/simple_pill/sp_button.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/enclosures/simple_pill/sp_button.stl -------------------------------------------------------------------------------- /l10n/requirements-l10n.txt: -------------------------------------------------------------------------------- 1 | Babel==2.16.0 2 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "setuptools.build_meta" 3 | requires = ["setuptools", "wheel"] 4 | 5 | [project] 6 | authors = [{name = "SeedSigner", email = "author@example.com"}] 7 | classifiers = [ 8 | "Programming Language :: Python :: 3", 9 | "License :: OSI Approved :: MIT License", 10 | "Operating System :: OS Independent" 11 | ] 12 | description = "Build an offline, airgapped Bitcoin signing device for less than $50!" 13 | name = "seedsigner" 14 | readme = "README.md" 15 | requires-python = ">=3.10" 16 | version = "0.8.5" 17 | 18 | [project.urls] 19 | "Bug Tracker" = "https://github.com/SeedSigner/seedsigner/issues" 20 | Homepage = "https://seedsigner.com/" 21 | Repository = "https://github.com/SeedSigner/seedsigner" 22 | 23 | [tool.coverage.html] 24 | directory = "coverage_html_report" 25 | skip_covered = false 26 | skip_empty = true 27 | 28 | [tool.coverage.report] 29 | # Regexes for lines to exclude from consideration 30 | exclude_lines = [ 31 | # Have to re-enable the standard pragma 32 | "pragma: no cover", 33 | # Don't complain about missing debug-only code: 34 | "def __repr__", 35 | "def __str__", 36 | "if self\\.debug", 37 | # Don't complain if tests don't hit defensive assertion code: 38 | "raise AssertionError", 39 | "raise NotImplementedError", 40 | # Don't complain if non-runnable code isn't run: 41 | "if 0:", 42 | "if __name__ == .__main__.:", 43 | # Don't complain about abstract methods, they aren't run: 44 | "@(abc\\.)?abstractmethod" 45 | ] 46 | # Omit; need a different approach to test modules with hardware dependencies 47 | omit = [ 48 | "*/__init__.py", 49 | "*/tests/*", 50 | ] 51 | skip_covered = false 52 | skip_empty = true 53 | 54 | [tool.coverage.run] 55 | source = ["src"] 56 | branch = true 57 | 58 | [tool.pytest.ini_options] 59 | testpaths = ["tests"] 60 | log_level = "DEBUG" 61 | 62 | [tool.setuptools] 63 | include-package-data = true 64 | 65 | [tool.setuptools.package-data] 66 | "seedsigner.resources" = ["**"] 67 | 68 | [tool.setuptools.packages.find] 69 | where = ["src"] 70 | -------------------------------------------------------------------------------- /requirements-raspi.txt: -------------------------------------------------------------------------------- 1 | picamera==1.13 2 | # numpy = transitive picamera dependency 3 | numpy==1.25.2 4 | RPi.GPIO==0.7.0 5 | spidev==3.5 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | embit==0.8.0 2 | Pillow==10.0.1 3 | pyzbar @ git+https://github.com/seedsigner/pyzbar.git@c3c237821c6a20b17953efe59b90df0b514a1c03 4 | qrcode==7.3.1 5 | urtypes @ git+https://github.com/selfcustody/urtypes.git@7fb280eab3b3563dfc57d2733b0bf5cbc0a96a6a 6 | pycryptodomex @ git+https://github.com/Legrandin/pycryptodome.git@0de7b4d80438aeba63e8010fa7da0f1d6d98effb 7 | -------------------------------------------------------------------------------- /seedsigner_pubkey.gpg: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQGNBGCQlVIBDACdFq+XXr2IsvTHj4/uYiaFidkX1OpYZHaYyRpWr7qrTPfIiCzU 4 | qzPfdtPuzAIkaZ34MQNebEybQFq9y1DKx7npnVL+0Hl5X7tIxLgngz/pA+HVZ0ig 5 | IEA7iX2gutocj/ilCWC+23Nqt+n2ViAFFW7IZhBzY582303vsFTtoLtEDY0OP6B9 6 | HxFQbls4PrcTl+OORULEipfPB9ta6kfT7xu9jtnfKB9ZuPB+YTP+IzPmmOLTKV8I 7 | 88vuHHagVwZp1eZWFBvYJFFAic5IZhwUiReYhHfuTsBV7o/kn/A/ubwM30FvjoBL 8 | y4o2XUUF7eufmXrkRjkYgoFSkZhG38O+WtF/bOJZO8BQQworIlzQuVC1ZlH7Yq6r 9 | 7W5VBKiyWkhSZuBdwQMC0zJ7uWTuT6hU/aF+aca2unK6NrP0oI6REc+N3gFusYtE 10 | 1Ak3pJtab8kurHb3peFtabeWpsicmbnf1tCD6IMqUT+iyETZuQk0JzMb98ClDz5/ 11 | xXbmRr9+VfzRTckAEQEAAbQtc2VlZHNpZ25lciA8YnRjLmhhcmR3YXJlLnNvbHV0 12 | aW9uc0BnbWFpbC5jb20+iQHOBBMBCgA4FiEERnObdLVq2I8UsIgux+9wkAcmARkF 13 | AmCQlVICGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQx+9wkAcmARl9igv/ 14 | WgGIxc1I4L9ACLotU3hUhdFyGSxdtwvHHDn9nkOiTefLqBN9BtqiCTKuebHj7Qy5 15 | rQm27ZkRDIH0zGUxepMWEy8+n2bjF8BBu03CZvPBuLeRXj4avGROcMbNkTxnfeSD 16 | Gbw3/DmD67CoMSwlhzlXHC7Vazx2cFSyWsRi6y5jQt5whfGkf74MAhSRGM4iwAq2 17 | 1i+660eqJDNG6juSG7IVSL7gvxvsnb7ahLzwEw5R4D70J6RCYnhwIoldvAUb5cgU 18 | qlo0QAI6znlQYvV8g4A7uNFNiMTyFFY0fCNwX3IPiJQ/uQlSlrMydoPnXChdSvpZ 19 | yc4DScnuKLPOpHjDrf3mk5e3oILhDbwrqP7gmje9ySqh034VmtoPYYYkN5cxfPnH 20 | ZgxH7QaTpxI2KqH750NKKdWHJlcrwI09V0kWeJv9SsDx7ASlRoABhTUHhDXM1q3D 21 | NJwbYNNCqtlvtsUWRYptwZoOBjoOSLLHaHH2H+oT8JFszyTENVZ4JyoSEkcpd2lM 22 | uQGNBGCQlVIBDADRjXqoG6D16zegPZOk7KR1Zqdulyr1PaeAuvX6+8S+3wN1EWFx 23 | 4X6RoHOs0+ctL4CThL/6g/q7KHERvO6FJuWf7tmqp28zZcycToHnKstuRAnAmKBt 24 | 4Z4fMAnyz0FT60x51+M0O65tHWkpO/+VXY1WdjHHv99V5WT4b60VfB3UANYnjZAb 25 | oJahmMp6xv/PulJ7/2AvC+hGIe+zUR9FtGmjfVd+mARfWi0OE/z9kCjPyzL4lyjI 26 | gvvJiMG9dxkoVjGXfUKi9kXOXmazi/wBzZoHjpx8BXnR0p/LUXrE/sNv8hGnnnJI 27 | CtivwjhOeF2oiCFHSvFMNBCacVNoLdKpj5xEzs+xYNuRhDB4JJg81xmCKxXOHmBf 28 | x8NE5b4WTvIGhXrcS+sWK3/DCPOUtzb/Yi8tAlmljsUAWdJoJyHosTzGywSzryOw 29 | l2OliM+fwAC20TlcJBy9GlnOEl8Gllrz+3aYnVtzEJ+dHUt6amMZ5w7G91lU+KZD 30 | Ff5RDot2kJr7VScAEQEAAYkBtgQYAQoAIBYhBEZzm3S1atiPFLCILsfvcJAHJgEZ 31 | BQJgkJVSAhsMAAoJEMfvcJAHJgEZb/4L/35KljNNS1rP6m9D64uzh9zjmMAtiYhr 32 | cl8+GabNk/EwB37DZdG+YveULRXCEX8btdVQf9kZMStZ5dXx2i3hDMzpKOkKkacO 33 | qcWvfpmOvz9HDH4zijo0cGwmwt3eoWFadWgetOOSebTo/80+eeBLunPmL+clsX8i 34 | IgPMszWV6o/h6Gk37pGVT63TW24fhbeo+0M0ZiZQ24xMCyZcCyNX4+nCcii2aKuI 35 | r8YD6FOpokv/fsqpMt2dmRCsPNm34eB4ELSvpN/f+l1EqkW3VEouIapeVPWtC9Hb 36 | +nGv6LfYR8fBQ/Qzu+SvZUxv8FkudzP6PQFkbLL7DUwlmHleSsqGtFhp6dCyYdwB 37 | BbSa8Hz0cLd7GYo3Y8jNL/0BJAWdX971BI+7z30niZDYponV5tSz58XQc8umHM93 38 | Nh8iaYVptNSwDRTt//FBA+DwABYVNL4t4q2IHpsIKmH3cQuitSPH4oG0LzWUSutk 39 | lmOueMWQgP/hSqowukz7nv+wzY0RSweGQg== 40 | =y6CL 41 | -----END PGP PUBLIC KEY BLOCK----- 42 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [extract_messages] 2 | keywords = _ _mft mark_for_translation ButtonOption 3 | add_comments = TRANSLATOR_NOTE: 4 | strip_comments = yes 5 | add_location = file 6 | input_dirs = src 7 | output_file = l10n/messages.pot 8 | 9 | 10 | [compile_catalog] 11 | use_fuzzy = yes 12 | directory = src/seedsigner/resources/seedsigner-translations/l10n 13 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Empty setup.py necessary for the python-babel integration (e.g. python setup.py extract_messages). 3 | 4 | See the configuration in setup.cfg. 5 | """ 6 | import setuptools 7 | 8 | setuptools.setup() -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import logging 5 | import sys 6 | 7 | from seedsigner.controller import Controller 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | DEFAULT_MODULE_LOG_LEVELS = { 12 | "PIL": logging.WARNING, 13 | # "seedsigner.gui.toast": logging.DEBUG, # example of more specific submodule logging config 14 | } 15 | 16 | 17 | def main(sys_argv=None): 18 | parser = argparse.ArgumentParser() 19 | parser.add_argument( 20 | "-l", 21 | "--loglevel", 22 | choices=list((logging._nameToLevel.keys())), 23 | default="INFO", 24 | type=str, 25 | help=( 26 | "Set the log level (default: %(default)s), WARNING: changing the log level " 27 | "to something more verbose than %(default)s may result in unwanted data " 28 | "being written to stderr" 29 | ), 30 | ) 31 | 32 | args = parser.parse_args(sys_argv) 33 | 34 | root_logger = logging.getLogger() 35 | root_logger.setLevel(logging.getLevelName(args.loglevel)) 36 | console_handler = logging.StreamHandler(sys.stderr) 37 | console_handler.setFormatter( 38 | logging.Formatter("%(asctime)s %(levelname)8s [%(name)s %(funcName)s (%(lineno)d)]: %(message)s") 39 | ) 40 | root_logger.addHandler(console_handler) 41 | 42 | # Set log levels for specific modules 43 | for module, level in DEFAULT_MODULE_LOG_LEVELS.items(): 44 | logging.getLogger(module).setLevel(level) 45 | 46 | logger.info(f"Starting SeedSigner with: {args.__dict__}") 47 | 48 | # Get the one and only Controller instance and start our main loop 49 | Controller.get_instance().start() 50 | 51 | 52 | if __name__ == "__main__": 53 | main(sys.argv[1:]) 54 | -------------------------------------------------------------------------------- /src/seedsigner/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/__init__.py -------------------------------------------------------------------------------- /src/seedsigner/gui/__init__.py: -------------------------------------------------------------------------------- 1 | from .renderer import Renderer -------------------------------------------------------------------------------- /src/seedsigner/gui/renderer.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw 2 | from threading import Lock 3 | 4 | from seedsigner.hardware.ST7789 import ST7789 5 | from seedsigner.models.singleton import ConfigurableSingleton 6 | 7 | 8 | 9 | class Renderer(ConfigurableSingleton): 10 | buttons = None 11 | canvas_width = 0 12 | canvas_height = 0 13 | canvas: Image.Image = None 14 | draw: ImageDraw.ImageDraw = None 15 | disp = None 16 | lock = Lock() 17 | 18 | 19 | @classmethod 20 | def configure_instance(cls): 21 | # Instantiate the one and only Renderer instance 22 | renderer = cls.__new__(cls) 23 | cls._instance = renderer 24 | 25 | # Eventually we'll be able to plug in other display controllers 26 | renderer.disp = ST7789() 27 | renderer.canvas_width = renderer.disp.width 28 | renderer.canvas_height = renderer.disp.height 29 | 30 | renderer.canvas = Image.new('RGB', (renderer.canvas_width, renderer.canvas_height)) 31 | renderer.draw = ImageDraw.Draw(renderer.canvas) 32 | 33 | 34 | def show_image(self, image=None, alpha_overlay=None, show_direct=False): 35 | if show_direct: 36 | # Use the incoming image as the canvas and immediately render 37 | self.disp.ShowImage(image, 0, 0) 38 | return 39 | 40 | if alpha_overlay: 41 | if image == None: 42 | image = self.canvas 43 | image = Image.alpha_composite(image, alpha_overlay) 44 | 45 | if image: 46 | # Always write to the current canvas, rather than trying to replace it 47 | self.canvas.paste(image) 48 | 49 | self.disp.ShowImage(self.canvas, 0, 0) 50 | 51 | 52 | def show_image_pan(self, image, start_x, start_y, end_x, end_y, rate, alpha_overlay=None): 53 | cur_x = start_x 54 | cur_y = start_y 55 | rate_x = rate 56 | rate_y = rate 57 | if end_x - start_x < 0: 58 | rate_x = rate_x * -1 59 | if end_y - start_y < 0: 60 | rate_y = rate_y * -1 61 | 62 | while (cur_x != end_x or cur_y != end_y) and (rate_x != 0 or rate_y != 0): 63 | cur_x += rate_x 64 | if (rate_x > 0 and cur_x > end_x) or (rate_x < 0 and cur_x < end_x): 65 | # We've moved too far; back up and undo that last move. 66 | cur_x -= rate_x 67 | rate_x = 0 68 | 69 | cur_y += rate_y 70 | if (rate_y > 0 and cur_y > end_y) or (rate_y < 0 and cur_y < end_y): 71 | # We've moved too far; back up and undo that last move. 72 | cur_y -= rate_y 73 | rate_y = 0 74 | 75 | crop = image.crop((cur_x, cur_y, cur_x + self.canvas_width, cur_y + self.canvas_height)) 76 | 77 | if alpha_overlay: 78 | crop = Image.alpha_composite(crop, alpha_overlay) 79 | 80 | # Always keep a copy of the current display in the canvas 81 | self.canvas.paste(crop) 82 | 83 | self.disp.ShowImage(crop, 0, 0) 84 | 85 | 86 | 87 | def display_blank_screen(self): 88 | self.draw.rectangle((0, 0, self.canvas_width, self.canvas_height), outline=0, fill=0) 89 | self.show_image() 90 | -------------------------------------------------------------------------------- /src/seedsigner/gui/screens/__init__.py: -------------------------------------------------------------------------------- 1 | from .screen import * -------------------------------------------------------------------------------- /src/seedsigner/hardware/ST7789.py: -------------------------------------------------------------------------------- 1 | import spidev 2 | import RPi.GPIO as GPIO 3 | import time 4 | import array 5 | 6 | 7 | 8 | class ST7789(object): 9 | """class for ST7789 240*240 1.3inch OLED displays.""" 10 | 11 | def __init__(self): 12 | self.width = 240 13 | self.height = 240 14 | 15 | #Initialize DC RST pin 16 | self._dc = 22 17 | self._rst = 13 18 | self._bl = 18 19 | 20 | GPIO.setmode(GPIO.BOARD) 21 | GPIO.setwarnings(False) 22 | GPIO.setup(self._dc,GPIO.OUT) 23 | GPIO.setup(self._rst,GPIO.OUT) 24 | GPIO.setup(self._bl,GPIO.OUT) 25 | GPIO.output(self._bl, GPIO.HIGH) 26 | 27 | #Initialize SPI 28 | self._spi = spidev.SpiDev(0, 0) 29 | self._spi.max_speed_hz = 40000000 30 | 31 | self.init() 32 | 33 | 34 | """ Write register address and data """ 35 | def command(self, cmd): 36 | GPIO.output(self._dc, GPIO.LOW) 37 | self._spi.writebytes([cmd]) 38 | 39 | def data(self, val): 40 | GPIO.output(self._dc, GPIO.HIGH) 41 | self._spi.writebytes([val]) 42 | 43 | def init(self): 44 | """Initialize dispaly""" 45 | self.reset() 46 | 47 | self.command(0x36) 48 | self.data(0x70) #self.data(0x00) 49 | 50 | self.command(0x3A) 51 | self.data(0x05) 52 | 53 | self.command(0xB2) 54 | self.data(0x0C) 55 | self.data(0x0C) 56 | self.data(0x00) 57 | self.data(0x33) 58 | self.data(0x33) 59 | 60 | self.command(0xB7) 61 | self.data(0x35) 62 | 63 | self.command(0xBB) 64 | self.data(0x19) 65 | 66 | self.command(0xC0) 67 | self.data(0x2C) 68 | 69 | self.command(0xC2) 70 | self.data(0x01) 71 | 72 | self.command(0xC3) 73 | self.data(0x12) 74 | 75 | self.command(0xC4) 76 | self.data(0x20) 77 | 78 | self.command(0xC6) 79 | self.data(0x0F) 80 | 81 | self.command(0xD0) 82 | self.data(0xA4) 83 | self.data(0xA1) 84 | 85 | self.command(0xE0) 86 | self.data(0xD0) 87 | self.data(0x04) 88 | self.data(0x0D) 89 | self.data(0x11) 90 | self.data(0x13) 91 | self.data(0x2B) 92 | self.data(0x3F) 93 | self.data(0x54) 94 | self.data(0x4C) 95 | self.data(0x18) 96 | self.data(0x0D) 97 | self.data(0x0B) 98 | self.data(0x1F) 99 | self.data(0x23) 100 | 101 | self.command(0xE1) 102 | self.data(0xD0) 103 | self.data(0x04) 104 | self.data(0x0C) 105 | self.data(0x11) 106 | self.data(0x13) 107 | self.data(0x2C) 108 | self.data(0x3F) 109 | self.data(0x44) 110 | self.data(0x51) 111 | self.data(0x2F) 112 | self.data(0x1F) 113 | self.data(0x1F) 114 | self.data(0x20) 115 | self.data(0x23) 116 | 117 | self.command(0x21) 118 | 119 | self.command(0x11) 120 | 121 | self.command(0x29) 122 | 123 | def reset(self): 124 | """Reset the display""" 125 | GPIO.output(self._rst,GPIO.HIGH) 126 | time.sleep(0.01) 127 | GPIO.output(self._rst,GPIO.LOW) 128 | time.sleep(0.01) 129 | GPIO.output(self._rst,GPIO.HIGH) 130 | time.sleep(0.01) 131 | 132 | def SetWindows(self, Xstart, Ystart, Xend, Yend): 133 | #set the X coordinates 134 | self.command(0x2A) 135 | self.data(0x00) #Set the horizontal starting point to the high octet 136 | self.data(Xstart & 0xff) #Set the horizontal starting point to the low octet 137 | self.data(0x00) #Set the horizontal end to the high octet 138 | self.data((Xend - 1) & 0xff) #Set the horizontal end to the low octet 139 | 140 | #set the Y coordinates 141 | self.command(0x2B) 142 | self.data(0x00) 143 | self.data((Ystart & 0xff)) 144 | self.data(0x00) 145 | self.data((Yend - 1) & 0xff ) 146 | 147 | self.command(0x2C) 148 | 149 | def ShowImage(self,Image,Xstart,Ystart): 150 | """Set buffer to value of Python Imaging Library image.""" 151 | """Write display buffer to physical display""" 152 | imwidth, imheight = Image.size 153 | if imwidth != self.width or imheight != self.height: 154 | raise ValueError('Image must be same dimensions as display \ 155 | ({0}x{1}).' .format(self.width, self.height)) 156 | # convert 24-bit RGB-8:8:8 to gBRG-3:5:5:3; then per-pixel byteswap to 16-bit RGB-5:6:5 157 | arr = array.array("H", Image.convert("BGR;16").tobytes()) 158 | arr.byteswap() 159 | pix = arr.tobytes() 160 | self.SetWindows ( 0, 0, self.width, self.height) 161 | GPIO.output(self._dc,GPIO.HIGH) 162 | self._spi.writebytes2(pix) 163 | 164 | def clear(self): 165 | """Clear contents of image buffer""" 166 | _buffer = [0xff]*(self.width * self.height * 2) 167 | self.SetWindows ( 0, 0, self.width, self.height) 168 | GPIO.output(self._dc,GPIO.HIGH) 169 | self._spi.writebytes2(_buffer) 170 | -------------------------------------------------------------------------------- /src/seedsigner/hardware/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/hardware/__init__.py -------------------------------------------------------------------------------- /src/seedsigner/hardware/camera.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | from PIL import Image 4 | from seedsigner.hardware.pivideostream import PiVideoStream 5 | from seedsigner.models.settings import Settings, SettingsConstants 6 | from seedsigner.models.singleton import Singleton 7 | 8 | 9 | 10 | class Camera(Singleton): 11 | _video_stream = None 12 | _picamera = None 13 | _camera_rotation = None 14 | 15 | @classmethod 16 | def get_instance(cls): 17 | # This is the only way to access the one and only Controller 18 | if cls._instance is None: 19 | cls._instance = cls.__new__(cls) 20 | cls._instance._camera_rotation = int(Settings.get_instance().get_value(SettingsConstants.SETTING__CAMERA_ROTATION)) 21 | return cls._instance 22 | 23 | 24 | def start_video_stream_mode(self, resolution=(512, 384), framerate=12, format="bgr"): 25 | from seedsigner.hardware.pivideostream import PiVideoStream 26 | if self._video_stream is not None: 27 | self.stop_video_stream_mode() 28 | 29 | self._video_stream = PiVideoStream(resolution=resolution,framerate=framerate, format=format) 30 | self._video_stream.start() 31 | 32 | 33 | def read_video_stream(self, as_image=False): 34 | if not self._video_stream: 35 | raise Exception("Must call start_video_stream first.") 36 | frame = self._video_stream.read() 37 | if not as_image: 38 | return frame 39 | else: 40 | if frame is not None: 41 | return Image.fromarray(frame.astype('uint8'), 'RGB').convert('RGBA').rotate(90 + self._camera_rotation) 42 | return None 43 | 44 | 45 | def stop_video_stream_mode(self): 46 | if self._video_stream is not None: 47 | self._video_stream.stop() 48 | self._video_stream = None 49 | 50 | 51 | def start_single_frame_mode(self, resolution=(720, 480)): 52 | from picamera import PiCamera 53 | if self._video_stream is not None: 54 | self.stop_video_stream_mode() 55 | if self._picamera is not None: 56 | self._picamera.close() 57 | 58 | self._picamera = PiCamera(resolution=resolution, framerate=24) 59 | self._picamera.start_preview() 60 | 61 | 62 | def capture_frame(self): 63 | if self._picamera is None: 64 | raise Exception("Must call start_single_frame_mode first.") 65 | 66 | # Set auto-exposure values 67 | self._picamera.shutter_speed = self._picamera.exposure_speed 68 | self._picamera.exposure_mode = 'off' 69 | g = self._picamera.awb_gains 70 | self._picamera.awb_mode = 'off' 71 | self._picamera.awb_gains = g 72 | 73 | stream = io.BytesIO() 74 | self._picamera.capture(stream, format='jpeg') 75 | 76 | # "Rewind" the stream to the beginning so we can read its content 77 | stream.seek(0) 78 | return Image.open(stream).rotate(90 + self._camera_rotation) 79 | 80 | 81 | def stop_single_frame_mode(self): 82 | if self._picamera is not None: 83 | self._picamera.close() 84 | self._picamera = None 85 | 86 | -------------------------------------------------------------------------------- /src/seedsigner/hardware/microsd.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import time 4 | 5 | from seedsigner.models.singleton import Singleton 6 | from seedsigner.models.threads import BaseThread 7 | from seedsigner.models.settings import Settings 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class MicroSD(Singleton, BaseThread): 13 | MOUNT_POINT = "/mnt/microsd" 14 | FIFO_PATH = "/tmp/mdev_fifo" 15 | FIFO_MODE = 0o600 16 | ACTION__INSERTED = "add" 17 | ACTION__REMOVED = "remove" 18 | 19 | 20 | @classmethod 21 | def get_instance(cls): 22 | # This is the only way to access the one and only instance 23 | if cls._instance is None: 24 | # Instantiate the one and only instance 25 | microsd = cls.__new__(cls) 26 | cls._instance = microsd 27 | 28 | # explicitly call BaseThread __init__ since multiple class inheritance 29 | BaseThread.__init__(microsd) 30 | 31 | return cls._instance 32 | 33 | 34 | @property 35 | def is_inserted(self): 36 | if Settings.HOSTNAME == Settings.SEEDSIGNER_OS: 37 | return os.path.exists(MicroSD.MOUNT_POINT) 38 | else: 39 | # Always True for Raspi OS 40 | return True 41 | 42 | 43 | def start_detection(self): 44 | self.start() 45 | 46 | 47 | def run(self): 48 | from seedsigner.controller import Controller 49 | from seedsigner.gui.toast import SDCardStateChangeToastManagerThread 50 | action = "" 51 | 52 | # explicitly only microsd add/remove detection in seedsigner-os 53 | if Settings.HOSTNAME == Settings.SEEDSIGNER_OS: 54 | 55 | # at start-up, get current status and inform Settings 56 | Settings.handle_microsd_state_change( 57 | action=MicroSD.ACTION__INSERTED if self.is_inserted else MicroSD.ACTION__REMOVED 58 | ) 59 | 60 | if os.path.exists(self.FIFO_PATH): 61 | os.remove(self.FIFO_PATH) 62 | 63 | os.mkfifo(self.FIFO_PATH, self.FIFO_MODE) 64 | 65 | while self.keep_running: 66 | with open(self.FIFO_PATH) as fifo: 67 | action = fifo.read() 68 | logger.info(f"fifo message: {action}") 69 | 70 | Settings.handle_microsd_state_change(action=action) 71 | Controller.get_instance().activate_toast(SDCardStateChangeToastManagerThread(action=action)) 72 | 73 | time.sleep(0.1) 74 | -------------------------------------------------------------------------------- /src/seedsigner/hardware/pivideostream.py: -------------------------------------------------------------------------------- 1 | # import the necessary packages 2 | import logging 3 | from picamera.array import PiRGBArray 4 | from picamera import PiCamera 5 | from threading import Thread 6 | import time 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | # Modified from: https://github.com/jrosebr1/imutils 12 | class PiVideoStream: 13 | def __init__(self, resolution=(320, 240), framerate=32, format="bgr", **kwargs): 14 | # initialize the camera 15 | self.camera = PiCamera(resolution=resolution, framerate=framerate, **kwargs) 16 | 17 | # initialize the stream 18 | self.rawCapture = PiRGBArray(self.camera, size=resolution) 19 | self.stream = self.camera.capture_continuous(self.rawCapture, 20 | format=format, use_video_port=True) 21 | 22 | # initialize the frame and the variable used to indicate 23 | # if the thread should be stopped 24 | self.frame = None 25 | self.should_stop = False 26 | self.is_stopped = True 27 | 28 | def start(self): 29 | # start the thread to read frames from the video stream 30 | t = Thread(target=self.update, args=()) 31 | t.daemon = True 32 | t.start() 33 | self.is_stopped = False 34 | return self 35 | 36 | def update(self): 37 | # keep looping infinitely until the thread is stopped 38 | for f in self.stream: 39 | # grab the frame from the stream and clear the stream in 40 | # preparation for the next frame 41 | self.frame = f.array 42 | self.rawCapture.truncate(0) 43 | 44 | # if the thread indicator variable is set, stop the thread 45 | # and resource camera resources 46 | if self.should_stop: 47 | logger.info("PiVideoStream: closing everything") 48 | self.stream.close() 49 | self.rawCapture.close() 50 | self.camera.close() 51 | self.should_stop = False 52 | self.is_stopped = True 53 | return 54 | 55 | def read(self): 56 | # return the frame most recently read 57 | return self.frame 58 | 59 | def stop(self): 60 | # indicate that the thread should be stopped 61 | self.should_stop = True 62 | 63 | # Block in this thread until stopped 64 | while not self.is_stopped: 65 | pass 66 | -------------------------------------------------------------------------------- /src/seedsigner/helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/helpers/__init__.py -------------------------------------------------------------------------------- /src/seedsigner/helpers/l10n.py: -------------------------------------------------------------------------------- 1 | def mark_for_translation(message: str) -> str: 2 | # Wraps the target string literal for translation but does NOT return the translated string. 3 | return message -------------------------------------------------------------------------------- /src/seedsigner/helpers/mnemonic_generation.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import unicodedata 3 | 4 | from embit import bip39 5 | from seedsigner.models.settings_definition import SettingsConstants 6 | from seedsigner.models.seed import Seed 7 | 8 | """ 9 | This is SeedSigner's internal mnemonic generation utility. 10 | 11 | It can also be run as an independently-executable CLI to facilitate external 12 | verification of SeedSigner's results for a given input entropy. 13 | 14 | see: docs/dice_verification.md (the "Command Line Tool" section). 15 | """ 16 | 17 | DICE__NUM_ROLLS__12WORD = 50 18 | DICE__NUM_ROLLS__24WORD = 99 19 | 20 | 21 | 22 | def calculate_checksum(mnemonic: list | str, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]: 23 | """ 24 | Provide 12- or 24-word mnemonic, returns complete mnemonic w/checksum as a list. 25 | 26 | Mnemonic may be a list of words or a string of words separated by spaces or commas. 27 | 28 | If 11- or 23-words are provided, append word `0000` to end of list as temp final 29 | word. 30 | """ 31 | if type(mnemonic) == str: 32 | import re 33 | # split on commas or spaces 34 | mnemonic = re.findall(r'[^,\s]+', mnemonic) 35 | 36 | if len(mnemonic) in [11, 23]: 37 | temp_final_word = Seed.get_wordlist(wordlist_language_code)[0] 38 | mnemonic.append(temp_final_word) 39 | 40 | if len(mnemonic) not in [12, 24]: 41 | raise Exception("Pass in a 12- or 24-word mnemonic") 42 | 43 | # Work on a copy of the input list 44 | mnemonic_copy = mnemonic.copy() 45 | 46 | # Convert the resulting mnemonic to bytes, but we `ignore_checksum` validation 47 | # because we assume it's incorrect since we either let the user select their own 48 | # final word OR we injected the 0000 word from the wordlist. 49 | mnemonic_bytes = bip39.mnemonic_to_bytes(unicodedata.normalize("NFKD", " ".join(mnemonic_copy)), ignore_checksum=True, wordlist=Seed.get_wordlist(wordlist_language_code)) 50 | 51 | # This function will convert the bytes back into a mnemonic, but it will also 52 | # calculate the proper checksum bits while doing so. For a 12-word seed it will just 53 | # overwrite the last 4 bits from the above result with the checksum; for a 24-word 54 | # seed it'll overwrite the last 8 bits. 55 | return bip39.mnemonic_from_bytes(mnemonic_bytes).split() 56 | 57 | 58 | 59 | def generate_mnemonic_from_bytes(entropy_bytes, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]: 60 | return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=Seed.get_wordlist(wordlist_language_code)).split() 61 | 62 | 63 | 64 | def generate_mnemonic_from_dice(roll_data: str, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]: 65 | """ 66 | Takes a string of 50 or 99 dice rolls and returns a 12- or 24-word mnemonic. 67 | 68 | Uses the iancoleman.io/bip39 and bitcoiner.guide/seed "Base 10" or "Hex" mode approach: 69 | * dice rolls are treated as string data. 70 | * hashed via SHA256. 71 | 72 | Important note: This method is NOT compatible with iancoleman's "Dice" mode. 73 | """ 74 | entropy_bytes = hashlib.sha256(roll_data.encode()).digest() 75 | 76 | if len(roll_data) == DICE__NUM_ROLLS__12WORD: 77 | # 12-word mnemonic; only use 128bits / 16 bytes 78 | entropy_bytes = entropy_bytes[:16] 79 | 80 | # Return as a list 81 | return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=Seed.get_wordlist(wordlist_language_code)).split() 82 | 83 | 84 | 85 | def generate_mnemonic_from_coin_flips(coin_flips: str, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]: 86 | """ 87 | Takes a string of 128 or 256 0s and 1s and returns a 12- or 24-word mnemonic. 88 | 89 | Uses the iancoleman.io/bip39 and bitcoiner.guide/seed "Binary" mode approach: 90 | * binary digit stream is treated as string data. 91 | * hashed via SHA256. 92 | """ 93 | entropy_bytes = hashlib.sha256(coin_flips.encode()).digest() 94 | 95 | if len(coin_flips) == 128: 96 | # 12-word mnemonic; only use 128bits / 16 bytes 97 | entropy_bytes = entropy_bytes[:16] 98 | 99 | # Return as a list 100 | return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=Seed.get_wordlist(wordlist_language_code)).split() 101 | 102 | 103 | 104 | def get_partial_final_word(coin_flips: str, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> str: 105 | """ Look up the partial final word for the given coin flips. 106 | 7 coin flips: 0101010 + **** where the final 4 bits will be replaced with the checksum 107 | 3 coin flips: 010 + ******** where the final 8 bits will be replaced with the checksum 108 | """ 109 | binary_string = coin_flips + "0" * (11 - len(coin_flips)) 110 | wordlist_index = int(binary_string, 2) 111 | 112 | return Seed.get_wordlist(wordlist_language_code)[wordlist_index] 113 | 114 | 115 | 116 | # Note: This currently isn't being used since we're now chaining hashed bytes for the 117 | # image-based entropy and aren't just ingesting a single image. 118 | def generate_mnemonic_from_image(image, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]: 119 | import hashlib 120 | hash = hashlib.sha256(image.tobytes()) 121 | 122 | # Return as a list 123 | return bip39.mnemonic_from_bytes(hash.digest(), wordlist=Seed.get_wordlist(wordlist_language_code)).split() 124 | -------------------------------------------------------------------------------- /src/seedsigner/helpers/ur2/LICENSE: -------------------------------------------------------------------------------- 1 | Unless otherwise noted (either in /README.md or in the file's header comments) the contents of this repository are released under the following license: 2 | 3 | BSD-2-Clause Plus Patent License 4 | 5 | SPDX-License-Identifier: [BSD-2-Clause-Patent](https://spdx.org/licenses/BSD-2-Clause-Patent.html) 6 | 7 | Copyright © 2020 Foundation Devices, Inc. 8 | 9 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 10 | 11 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 12 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 13 | Subject to the terms and conditions of this license, each copyright holder and contributor hereby grants to those receiving rights under this license a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except for failure to satisfy the conditions of this license) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer this software, where such license applies only to those patent claims, already acquired or hereafter acquired, licensable by such copyright holder or contributor that are necessarily infringed by: 14 | 15 | (a) their Contribution(s) (the licensed copyrights of copyright holders and non-copyrightable additions of contributors, in source or binary form) alone; or 16 | (b) combination of their Contribution(s) with the work of authorship to which such Contribution(s) was added by such copyright holder or contributor, if, at the time the Contribution is added, such addition causes such combination to be necessarily infringed. The patent license shall not apply to any other combinations which include the Contribution. 17 | Except as expressly stated above, no rights or licenses from any copyright holder or contributor is granted under this license, whether expressly, by implication, estoppel or otherwise. 18 | 19 | DISCLAIMER 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /src/seedsigner/helpers/ur2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/helpers/ur2/__init__.py -------------------------------------------------------------------------------- /src/seedsigner/helpers/ur2/bytewords.py: -------------------------------------------------------------------------------- 1 | # 2 | # bytewords.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | from .utils import crc32_bytes, partition 9 | 10 | BYTEWORDS = 'ableacidalsoapexaquaarchatomauntawayaxisbackbaldbarnbeltbetabiasbluebodybragbrewbulbbuzzcalmcashcatschefcityclawcodecolacookcostcruxcurlcuspcyandarkdatadaysdelidicedietdoordowndrawdropdrumdulldutyeacheasyechoedgeepicevenexamexiteyesfactfairfernfigsfilmfishfizzflapflewfluxfoxyfreefrogfuelfundgalagamegeargemsgiftgirlglowgoodgraygrimgurugushgyrohalfhanghardhawkheathelphighhillholyhopehornhutsicedideaidleinchinkyintoirisironitemjadejazzjoinjoltjowljudojugsjumpjunkjurykeepkenokeptkeyskickkilnkingkitekiwiknoblamblavalazyleaflegsliarlimplionlistlogoloudloveluaulucklungmainmanymathmazememomenumeowmildmintmissmonknailnavyneednewsnextnoonnotenumbobeyoboeomitonyxopenovalowlspaidpartpeckplaypluspoempoolposepuffpumapurrquadquizraceramprealredorichroadrockroofrubyruinrunsrustsafesagascarsetssilkskewslotsoapsolosongstubsurfswantacotasktaxitenttiedtimetinytoiltombtoystriptunatwinuglyundouniturgeuservastveryvetovialvibeviewvisavoidvowswallwandwarmwaspwavewaxywebswhatwhenwhizwolfworkyankyawnyellyogayurtzapszerozestzinczonezoom' 11 | WORD_ARRAY = None 12 | 13 | def decode_word(word, word_len): 14 | global WORD_ARRAY 15 | global BYTEWORDS 16 | 17 | if len(word) != word_len: 18 | raise ValueError('Invalid Bytewords.') 19 | 20 | dim = 26 21 | 22 | # Since the first and last letters of each Byteword are unique, 23 | # we can use them as indexes into a two-dimensional lookup table. 24 | # This table is generated lazily. 25 | if WORD_ARRAY == None: 26 | WORD_ARRAY = [-1] * (dim * dim) # create empty array 27 | 28 | for i in range(256): 29 | byteword_offset = i * 4 30 | x = ord(BYTEWORDS[byteword_offset]) - ord('a') 31 | y = ord(BYTEWORDS[byteword_offset + 3]) - ord('a') 32 | array_offset = y * dim + x 33 | WORD_ARRAY[array_offset] = i 34 | 35 | # If the coordinates generated by the first and last letters are out of bounds, 36 | # or the lookup table contains -1 at the coordinates, then the word is not valid. 37 | x = ord(word[0].lower()) - ord('a') 38 | y = ord((word[3 if len(word) == 4 else 1]).lower()) - ord('a') 39 | if not (0 <= x and x < dim and 0 <= y and y < dim): 40 | raise ValueError('Invalid Bytewords.') 41 | 42 | offset = y * dim + x 43 | value = WORD_ARRAY[offset] 44 | if value == -1: 45 | raise ValueError('Invalid Bytewords.') 46 | 47 | # If we're decoding a full four-letter word, verify that the two middle letters are correct. 48 | if len(word) == 4: 49 | byteword_offset = value * 4 50 | c1 = word[1].lower() 51 | c2 = word[2].lower() 52 | if c1 != BYTEWORDS[byteword_offset + 1] or c2 != BYTEWORDS[byteword_offset + 2]: 53 | raise ValueError('Invalid Bytewords.') 54 | 55 | # Successful decode. 56 | return value 57 | 58 | def get_word(index): 59 | byteword_offset = index * 4 60 | return BYTEWORDS[byteword_offset:byteword_offset + 4] 61 | 62 | def get_minimal_word(index): 63 | byteword_offset = index * 4 64 | return BYTEWORDS[byteword_offset] + BYTEWORDS[byteword_offset + 3] 65 | 66 | def encode(buf, separator): 67 | words = [] 68 | for i in range(len(buf)): 69 | byte = buf[i] 70 | words.append(get_word(byte)) 71 | 72 | return separator.join(words) 73 | 74 | def add_crc(buf): 75 | crc_buf = crc32_bytes(buf) 76 | return buf + crc_buf 77 | 78 | def encode_with_separator(buf, separator): 79 | crc_buf = add_crc(buf) 80 | return encode(crc_buf, separator) 81 | 82 | def encode_minimal(buf): 83 | result = '' 84 | 85 | crc_buf = add_crc(buf) 86 | for i in range(len(crc_buf)): 87 | byte = crc_buf[i] 88 | result += get_minimal_word(byte) 89 | 90 | return result 91 | 92 | def decode(s, separator, word_len): 93 | buf = bytearray() 94 | 95 | if word_len == 4: 96 | words = s.split(separator) 97 | else: 98 | words = partition(s, 2) 99 | 100 | for word in words: 101 | buf.append(decode_word(word, word_len)) 102 | 103 | if len(buf) < 5: 104 | raise ValueError('Invalid Bytewords.') 105 | 106 | # Validate checksum 107 | body = buf[0:-4] 108 | body_checksum = buf[-4:] 109 | checksum = crc32_bytes(body) 110 | # if checksum != body_checksum: 111 | # raise ValueError('Invalid Bytewords.') 112 | 113 | return body 114 | 115 | Bytewords_Style_standard = 1 116 | Bytewords_Style_uri = 2 117 | Bytewords_Style_minimal = 3 118 | 119 | class Bytewords: 120 | @staticmethod 121 | def encode(style, bytes): 122 | if style == Bytewords_Style_standard: 123 | return encode_with_separator(bytes, ' ') 124 | elif style == Bytewords_Style_uri: 125 | return encode_with_separator(bytes, '-') 126 | elif style == Bytewords_Style_minimal: 127 | return encode_minimal(bytes) 128 | else: 129 | assert False 130 | 131 | @staticmethod 132 | def decode(style, str): 133 | if style == Bytewords_Style_standard: 134 | return decode(str, ' ', 4) 135 | elif style == Bytewords_Style_uri: 136 | return decode(str, '-', 4) 137 | elif style == Bytewords_Style_minimal: 138 | return decode(str, 0, 2) 139 | else: 140 | assert False 141 | -------------------------------------------------------------------------------- /src/seedsigner/helpers/ur2/constants.py: -------------------------------------------------------------------------------- 1 | # 2 | # constants.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | MAX_UINT32 = 0xffffffff 9 | MAX_UINT64 = 0xffffffffffffffff 10 | -------------------------------------------------------------------------------- /src/seedsigner/helpers/ur2/crc32.py: -------------------------------------------------------------------------------- 1 | # 2 | # crc32.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | from .constants import MAX_UINT32 9 | 10 | def bit_length(n): 11 | return len(bin(abs(n))) - 2 12 | 13 | TABLE = None 14 | 15 | def crc32(buf): 16 | # Lazily instantiate CRC table 17 | global TABLE 18 | if TABLE == None: 19 | TABLE = [None] * (256 * 4) 20 | 21 | for i in range(256): 22 | c = i 23 | for j in range(8): 24 | c = (c >> 1) if (c % 2 == 0) else (0xEDB88320 ^ (c >> 1)) 25 | 26 | TABLE[i] = c 27 | 28 | crc = MAX_UINT32 & ~0 29 | for byte in buf: 30 | crc = (crc >> 8) ^ TABLE[(crc ^ byte) & 0xFF] 31 | 32 | return MAX_UINT32 & ~crc 33 | 34 | def crc32n(buf): 35 | n = crc32(buf) 36 | return n.to_bytes((bit_length(n) + 7) // 8, 'big') 37 | -------------------------------------------------------------------------------- /src/seedsigner/helpers/ur2/fountain_encoder.py: -------------------------------------------------------------------------------- 1 | # 2 | # fountain_encoder.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | import math 9 | from .cbor_lite import CBORDecoder, CBOREncoder 10 | from .fountain_utils import choose_fragments 11 | from .utils import split, crc32_int, xor_into, data_to_hex 12 | from .constants import MAX_UINT32, MAX_UINT64 13 | 14 | class InvalidHeader(Exception): 15 | pass 16 | 17 | class Part: 18 | 19 | def __init__(self, seq_num, seq_len, message_len, checksum, data): 20 | self.seq_num = seq_num 21 | self.seq_len = seq_len 22 | self.message_len = message_len 23 | self.checksum = checksum 24 | self.data = data 25 | 26 | @staticmethod 27 | def from_cbor(cbor_buf): 28 | try: 29 | decoder = CBORDecoder(cbor_buf) 30 | (array_size, _) = decoder.decodeArraySize() 31 | if array_size != 5: 32 | raise InvalidHeader() 33 | 34 | (seq_num, _) = decoder.decodeUnsigned() 35 | if seq_num > MAX_UINT64: # TODO: Do something better with this check 36 | raise InvalidHeader() 37 | 38 | (seq_len, _) = decoder.decodeUnsigned() 39 | if seq_len > MAX_UINT64: 40 | raise InvalidHeader() 41 | 42 | (message_len, _) = decoder.decodeUnsigned() 43 | if message_len > MAX_UINT64: 44 | raise InvalidHeader() 45 | 46 | (checksum, _) = decoder.decodeUnsigned() 47 | if checksum > MAX_UINT64: 48 | raise InvalidHeader() 49 | 50 | (data, _) = decoder.decodeBytes() 51 | 52 | return Part(seq_num, seq_len, message_len, checksum, data) 53 | except Exception as err: 54 | raise InvalidHeader() 55 | 56 | def cbor(self): 57 | encoder = CBOREncoder() 58 | encoder.encodeArraySize(5) 59 | encoder.encodeInteger(self.seq_num) 60 | encoder.encodeInteger(self.seq_len) 61 | encoder.encodeInteger(self.message_len) 62 | encoder.encodeInteger(self.checksum) 63 | encoder.encodeBytes(self.data) 64 | return encoder.get_bytes() 65 | 66 | def seq_num(self): 67 | return self.seq_num 68 | 69 | def seq_len(self): 70 | return self.seq_len 71 | 72 | def message_len(self): 73 | return self.message_len 74 | 75 | def checksum(self): 76 | return self.checksum 77 | 78 | def data(self): 79 | return self.data 80 | 81 | def description(self): 82 | return "seqNum:{}, seqLen:{}, messageLen:{}, checksum:{}, data:{}".format( 83 | self.seq_num, self.seq_len, self.message_len, self.checksum, data_to_hex(self.data)) 84 | 85 | class FountainEncoder: 86 | def __init__(self, message, max_fragment_len, first_seq_num = 0, min_fragment_len = 10): 87 | assert len(message) <= MAX_UINT32 88 | self.message_len = len(message) 89 | self.checksum = crc32_int(message) 90 | self.fragment_len = FountainEncoder.find_nominal_fragment_length(self.message_len, min_fragment_len, max_fragment_len) 91 | self.fragments = FountainEncoder.partition_message(message, self.fragment_len) 92 | self.seq_num = first_seq_num 93 | self.current_part: Part = None 94 | 95 | @staticmethod 96 | def find_nominal_fragment_length(message_len, min_fragment_len, max_fragment_len): 97 | assert message_len > 0 98 | assert min_fragment_len > 0 99 | assert max_fragment_len >= min_fragment_len 100 | max_fragment_count = message_len // min_fragment_len 101 | fragment_len = None 102 | 103 | for fragment_count in range(1, max_fragment_count + 1): 104 | fragment_len = math.ceil(message_len / fragment_count) 105 | if fragment_len <= max_fragment_len: 106 | break 107 | 108 | assert fragment_len != None 109 | return fragment_len 110 | 111 | 112 | @staticmethod 113 | def partition_message(message, fragment_len): 114 | remaining = message 115 | fragments = [] 116 | while len(remaining) != 0: 117 | (fragment, remaining) = split(remaining, fragment_len) 118 | padding = fragment_len - len(fragment) 119 | while padding > 0: 120 | fragment.append(0) 121 | padding -= 1 122 | fragments.append(fragment) 123 | 124 | return fragments 125 | 126 | def last_part_indexes(self): 127 | return self.last_part_indexes 128 | 129 | def seq_len(self): 130 | return len(self.fragments) 131 | 132 | # This becomes `true` when the minimum number of parts 133 | # to relay the complete message have been generated 134 | def is_complete(self): 135 | return self.seq_num >= self.seq_len() 136 | 137 | # True if only a single part will be generated. 138 | def is_single_part(self): 139 | return self.seq_len() == 1 140 | 141 | def next_part(self): 142 | self.seq_num += 1 143 | self.seq_num = self.seq_num % MAX_UINT32 # wrap at period 2^32 144 | indexes = choose_fragments(self.seq_num, self.seq_len(), self.checksum) 145 | mixed = self.mix(indexes) 146 | data = bytes(mixed) 147 | self.current_part = Part(self.seq_num, self.seq_len(), self.message_len, self.checksum, data) 148 | return self.current_part 149 | 150 | 151 | def restart(self): 152 | """ 153 | Restart from the beginning; each cycle's first n frames are full data frames 154 | (not XOR composites). 155 | """ 156 | self.seq_num = 0 157 | 158 | 159 | def mix(self, indexes): 160 | result = [0] * self.fragment_len 161 | for index in indexes: 162 | xor_into(result, self.fragments[index]) 163 | return result 164 | -------------------------------------------------------------------------------- /src/seedsigner/helpers/ur2/fountain_utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # fountain_utils.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | from .random_sampler import RandomSampler 9 | from .utils import int_to_bytes 10 | from .xoshiro256 import Xoshiro256 11 | 12 | # Fisher-Yates shuffle 13 | def shuffled(items, rng): 14 | remaining = items 15 | result = [] 16 | while len(remaining) > 0: 17 | index = rng.next_int(0, len(remaining) - 1) 18 | item = remaining.pop(index) 19 | result.append(item) 20 | 21 | return result 22 | 23 | def choose_degree(seq_len, rng): 24 | degree_probabilities = [] 25 | for i in range(1, seq_len + 1): 26 | degree_probabilities.append(1.0 / i) 27 | 28 | degree_chooser = RandomSampler(degree_probabilities) 29 | return degree_chooser.next(lambda: rng.next_double()) + 1 30 | 31 | def choose_fragments(seq_num, seq_len, checksum): 32 | # The first `seq_len` parts are the "pure" fragments, not mixed with any 33 | # others. This means that if you only generate the first `seq_len` parts, 34 | # then you have all the parts you need to decode the message. 35 | if seq_num <= seq_len: 36 | return set([seq_num - 1]) 37 | else: 38 | seed = int_to_bytes(seq_num) + int_to_bytes(checksum) 39 | rng = Xoshiro256.from_bytes(seed) 40 | degree = choose_degree(seq_len, rng) 41 | indexes = [] 42 | 43 | for i in range(seq_len): 44 | indexes.append(i) 45 | shuffled_indexes = shuffled(indexes, rng) 46 | return set(shuffled_indexes[0:degree]) 47 | 48 | def contains(set_or_list, el): 49 | return el in set_or_list 50 | 51 | def is_strict_subset(a, b): 52 | return a.issubset(b) 53 | 54 | def set_difference(a, b): 55 | return a.difference(b) -------------------------------------------------------------------------------- /src/seedsigner/helpers/ur2/random_sampler.py: -------------------------------------------------------------------------------- 1 | # 2 | # random_sampler.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | class RandomSampler: 9 | 10 | def __init__(self, probs): 11 | for p in probs: 12 | assert p > 0 13 | 14 | # Normalize given probabilities 15 | total = sum(probs) 16 | assert total > 0 17 | 18 | n = len(probs) 19 | 20 | P = [] 21 | for p in probs: 22 | P.append((p * float(n)) / total) 23 | 24 | S = [] 25 | L = [] 26 | 27 | # Set separate index lists for small and large probabilities: 28 | for i in reversed(range(0, n)): 29 | # at variance from Schwarz, we reverse the index order 30 | if P[i] < 1: 31 | S.append(i) 32 | else: 33 | L.append(i) 34 | 35 | # Work through index lists 36 | _probs = [0] * n 37 | _aliases = [0] * n 38 | 39 | while len(S) > 0 and len(L) > 0: 40 | a = S.pop() # Schwarz's l 41 | g = L.pop() # Schwarz's g 42 | _probs[a] = P[a] 43 | _aliases[a] = g 44 | P[g] += P[a] - 1 45 | if P[g] < 1: 46 | S.append(g) 47 | else: 48 | L.append(g) 49 | 50 | while len(L) > 0: 51 | _probs[L.pop()] = 1 52 | 53 | while len(S) > 0: 54 | # can only happen through numeric instability 55 | _probs[S.pop()] = 1 56 | 57 | self.probs = _probs 58 | self.aliases = _aliases 59 | 60 | def next(self, rng_func): 61 | r1 = rng_func() 62 | r2 = rng_func() 63 | n = len(self.probs) 64 | i = int(float(n) * r1) 65 | return i if r2 < self.probs[i] else self.aliases[i] 66 | -------------------------------------------------------------------------------- /src/seedsigner/helpers/ur2/ur.py: -------------------------------------------------------------------------------- 1 | # 2 | # ur.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | from .utils import is_ur_type 9 | 10 | class InvalidType(Exception): 11 | pass 12 | 13 | class UR: 14 | 15 | def __init__(self, type, cbor): 16 | if not is_ur_type(type): 17 | raise InvalidType() 18 | 19 | self.type = type 20 | self.cbor = cbor 21 | 22 | def __eq__(self, obj): 23 | if obj == None: 24 | return False 25 | return self.type == obj.type and self.cbor == obj.cbor 26 | -------------------------------------------------------------------------------- /src/seedsigner/helpers/ur2/ur_decoder.py: -------------------------------------------------------------------------------- 1 | # 2 | # ur_decoder.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | from .ur import UR 9 | from .fountain_encoder import FountainEncoder, Part as FountainEncoderPart 10 | from .fountain_decoder import FountainDecoder 11 | from .bytewords import * 12 | from .utils import drop_first, is_ur_type 13 | 14 | class InvalidScheme(Exception): 15 | pass 16 | 17 | class InvalidType(Exception): 18 | pass 19 | 20 | class InvalidPathLength(Exception): 21 | pass 22 | 23 | class InvalidSequenceComponent(Exception): 24 | pass 25 | 26 | class InvalidFragment(Exception): 27 | pass 28 | 29 | class URDecoder: 30 | def __init__(self): 31 | self.fountain_decoder = FountainDecoder() 32 | self.expected_type = None 33 | self.result = None 34 | 35 | @staticmethod 36 | def decode(str): 37 | (type, components) = URDecoder.parse(str) 38 | if len(components) == 0: 39 | raise InvalidPathLength() 40 | 41 | body = components[0] 42 | return URDecoder.decode_by_type(type, body) 43 | 44 | @staticmethod 45 | def decode_by_type(type, body): 46 | cbor = Bytewords.decode(Bytewords_Style_minimal, body) 47 | return UR(type, cbor) 48 | 49 | @staticmethod 50 | def parse(str): 51 | # Don't consider case 52 | lowered = str.lower() 53 | 54 | # Validate URI scheme 55 | if not lowered.startswith('ur:'): 56 | raise InvalidScheme() 57 | 58 | path = drop_first(lowered, 3) 59 | 60 | # Split the remainder into path components 61 | components = path.split('/') 62 | 63 | # Make sure there are at least two path components 64 | if len(components) < 2: 65 | raise InvalidPathLength() 66 | 67 | # Validate the type 68 | type = components[0] 69 | if not is_ur_type(type): 70 | raise InvalidType() 71 | 72 | comps = components[1:] # Don't include the ur type 73 | return (type, comps) 74 | 75 | @staticmethod 76 | def parse_sequence_component(str): 77 | try: 78 | comps = str.split('-') 79 | if len(comps) != 2: 80 | raise InvalidSequenceComponent() 81 | seq_num = int(comps[0]) 82 | seq_len = int(comps[1]) 83 | if seq_num < 1 or seq_len < 1: 84 | raise InvalidSequenceComponent() 85 | return (seq_num, seq_len) 86 | except: 87 | raise InvalidSequenceComponent() 88 | 89 | def validate_part(self, type): 90 | if self.expected_type == None: 91 | if not is_ur_type(type): 92 | return False 93 | self.expected_type = type 94 | return True 95 | else: 96 | return type == self.expected_type 97 | 98 | def receive_part(self, str): 99 | try: 100 | # Don't process the part if we're already done 101 | if self.result != None: 102 | return False 103 | 104 | # Don't continue if this part doesn't validate 105 | (type, components) = URDecoder.parse(str) 106 | if not self.validate_part(type): 107 | return False 108 | 109 | # If this is a single-part UR then we're done 110 | if len(components) == 1: 111 | body = components[0] 112 | self.result = self.decode_by_type(type, body) 113 | return True 114 | 115 | # Multi-part URs must have two path components: seq/fragment 116 | if len(components) != 2: 117 | raise InvalidPathLength() 118 | seq = components[0] 119 | fragment = components[1] 120 | 121 | # Parse the sequence component and the fragment, and make sure they agree. 122 | (seq_num, seq_len) = URDecoder.parse_sequence_component(seq) 123 | cbor = Bytewords.decode(Bytewords_Style_minimal, fragment) 124 | part = FountainEncoderPart.from_cbor(cbor) 125 | if seq_num != part.seq_num or seq_len != part.seq_len: 126 | return False 127 | 128 | # Process the part 129 | if not self.fountain_decoder.receive_part(part): 130 | return False 131 | 132 | if self.fountain_decoder.is_success(): 133 | self.result = UR(type, self.fountain_decoder.result_message()) 134 | elif self.fountain_decoder.is_failure(): 135 | self.result = self.fountain_decoder.result_error() 136 | 137 | return True 138 | except Exception as err: 139 | return False 140 | 141 | def expected_type(self): 142 | return self.expected_type 143 | 144 | def expected_part_count(self): 145 | return self.fountain_decoder.expected_part_count() 146 | 147 | def received_part_indexes(self): 148 | return self.fountain_decoder.received_part_indexes 149 | 150 | def last_part_indexes(self): 151 | return self.fountain_decoder.last_part_indexes 152 | 153 | def processed_parts_count(self): 154 | return self.fountain_decoder.processed_parts_count 155 | 156 | def estimated_percent_complete(self, weight_mixed_frames: bool = False): 157 | return self.fountain_decoder.estimated_percent_complete(weight_mixed_frames=weight_mixed_frames) 158 | 159 | def is_success(self): 160 | result = self.result 161 | return result if not isinstance(result, Exception) else False 162 | 163 | def is_failure(self): 164 | result = self.result 165 | return result if isinstance(result, Exception) else False 166 | 167 | def is_complete(self): 168 | return self.result != None 169 | 170 | def result_message(self): 171 | return self.result 172 | 173 | def result_error(self): 174 | return self.result 175 | 176 | -------------------------------------------------------------------------------- /src/seedsigner/helpers/ur2/ur_encoder.py: -------------------------------------------------------------------------------- 1 | # 2 | # ur_encoder.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | from .fountain_encoder import FountainEncoder 9 | from .bytewords import * 10 | 11 | class UREncoder: 12 | # Start encoding a (possibly) multi-part UR. 13 | def __init__(self, ur, max_fragment_len, first_seq_num = 0, min_fragment_len = 10): 14 | self.ur = ur 15 | self.fountain_encoder = FountainEncoder(ur.cbor, max_fragment_len, first_seq_num, min_fragment_len) 16 | 17 | # Encode a single-part UR. 18 | @staticmethod 19 | def encode(ur): 20 | body = Bytewords.encode(Bytewords_Style_minimal, ur.cbor) 21 | return UREncoder.encode_ur([ur.type, body]) 22 | 23 | def last_part_indexes(self): 24 | return self.fountain_encoder.last_part_indexes() 25 | 26 | # `True` if the minimal number of parts to transmit the message have been 27 | # generated. Parts generated when this is `true` will be fountain codes 28 | # containing various mixes of the part data. 29 | def is_complete(self): 30 | return self.fountain_encoder.is_complete() 31 | 32 | # `True` if this UR can be contained in a single part. If `True`, repeated 33 | # calls to `next_part()` will all return the same single-part UR. 34 | def is_single_part(self): 35 | return self.fountain_encoder.is_single_part() 36 | 37 | def next_part(self) -> str: 38 | if self.is_single_part(): 39 | return UREncoder.encode(self.ur) 40 | else: 41 | part = self.fountain_encoder.next_part() 42 | return UREncoder.encode_part(self.ur.type, part) 43 | 44 | def current_part(self) -> str: 45 | if self.is_single_part(): 46 | return UREncoder.encode(self.ur) 47 | else: 48 | part = self.fountain_encoder.current_part 49 | if not part: 50 | part = self.fountain_encoder.next_part() 51 | return UREncoder.encode_part(self.ur.type, part) 52 | 53 | 54 | def restart(self): 55 | self.fountain_encoder.restart() 56 | 57 | 58 | @staticmethod 59 | def encode_part(type, part): 60 | seq = '{}-{}'.format(part.seq_num, part.seq_len) 61 | body = Bytewords.encode(Bytewords_Style_minimal, part.cbor()) 62 | result = UREncoder.encode_ur([type, seq, body]) 63 | return result 64 | 65 | @staticmethod 66 | def encode_uri(scheme, path_components): 67 | path = '/'.join(path_components) 68 | return ':'.join([scheme, path]) 69 | 70 | @staticmethod 71 | def encode_ur(path_components): 72 | return UREncoder.encode_uri('ur', path_components) 73 | -------------------------------------------------------------------------------- /src/seedsigner/helpers/ur2/utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # utils.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | from .crc32 import crc32, crc32n 9 | 10 | def crc32_bytes(buf): 11 | checksum = crc32n(buf) 12 | return checksum 13 | 14 | def crc32_int(buf): 15 | return crc32(buf) 16 | 17 | def data_to_hex(buf): 18 | return ''.join('{:02x}'.format(x) for x in buf) 19 | 20 | def int_to_bytes(n): 21 | # return n.to_bytes((n.bit_length() + 7) // 8, 'big') 22 | return n.to_bytes(4, 'big') 23 | 24 | def bytes_to_int(buf): 25 | return int.from_bytes(buf, 'big') 26 | 27 | def string_to_bytes(s): 28 | return bytes(s, 'utf8') 29 | 30 | def is_ur_type(ch): 31 | if 'a' <= ch and ch <= 'z': 32 | return True 33 | if '0' <= ch and ch <= '9': 34 | return True 35 | if ch == '-': 36 | return True 37 | return False 38 | 39 | def partition(s, n): 40 | return [s[i:i+n] for i in range(0, len(s), n)] 41 | 42 | # Split the given sequence into two parts returned in a tuple 43 | # The first entry in the tuple has the first `count` values. 44 | # The second entry in the tuple has the remaining values. 45 | def split(buf, count): 46 | return (buf[0:count], buf[count:]) 47 | 48 | def join_lists(lists): 49 | # return [y for x in lists for y in x] 50 | return sum(lists, []) 51 | 52 | def join_bytes(list_of_ba): 53 | out = bytearray() 54 | for ba in list_of_ba: 55 | out.extend(ba) 56 | return out 57 | 58 | def xor_into(target, source): 59 | count = len(target) 60 | assert count == len(source) # Must be the same length 61 | for i in range(count): 62 | target[i] ^= source[i] 63 | 64 | def xor_with(a, b): 65 | target = a 66 | xor_into(target, b) 67 | return target 68 | 69 | def take_first(s, count): 70 | return s[0:count] 71 | 72 | def drop_first(s, count): 73 | return s[count:] 74 | -------------------------------------------------------------------------------- /src/seedsigner/helpers/ur2/xoshiro256.py: -------------------------------------------------------------------------------- 1 | # 2 | # xoshiro256.py 3 | # 4 | # Copyright © 2020 Foundation Devices, Inc. 5 | # Licensed under the "BSD-2-Clause Plus Patent License" 6 | # 7 | 8 | import sys 9 | try: 10 | import uhashlib as hashlib 11 | except: 12 | try: 13 | import hashlib 14 | except: 15 | sys.exit("ERROR: No hashlib or uhashlib implementation found (required for sha256)") 16 | 17 | from .utils import string_to_bytes, int_to_bytes 18 | from .constants import MAX_UINT64 19 | 20 | # Original Info: 21 | # Written in 2018 by David Blackman and Sebastiano Vigna (vigna@acm.org) 22 | 23 | # To the extent possible under law, the author has dedicated all copyright 24 | # and related and neighboring rights to this software to the public domain 25 | # worldwide. This software is distributed without any warranty. 26 | 27 | # See . 28 | 29 | # This is xoshiro256** 1.0, one of our all-purpose, rock-solid 30 | # generators. It has excellent (sub-ns) speed, a state (256 bits) that is 31 | # large enough for any parallel application, and it passes all tests we 32 | # are aware of. 33 | 34 | # For generating just floating-point numbers, xoshiro256+ is even faster. 35 | 36 | # The state must be seeded so that it is not everywhere zero. If you have 37 | # a 64-bit seed, we suggest to seed a splitmix64 generator and use its 38 | # output to fill s. 39 | 40 | def rotl(x, k): 41 | return ((x << k) | (x >> (64 - k))) & MAX_UINT64 42 | 43 | JUMP = [ 0x180ec6d33cfd0aba, 0xd5a61266f0c9392c, 0xa9582618e03fc9aa, 0x39abdc4529b1661c ] 44 | LONG_JUMP = [ 0x76e15d3efefdcbbf, 0xc5004e441c522fb3, 0x77710069854ee241, 0x39109bb02acbe635 ] 45 | 46 | class Xoshiro256: 47 | def __init__(self, arr = None): 48 | self.s = [0] * 4 49 | if arr != None: 50 | self.s[0] = arr[0] 51 | self.s[1] = arr[1] 52 | self.s[2] = arr[2] 53 | self.s[3] = arr[3] 54 | 55 | 56 | def _set_s(self, arr): 57 | for i in range(4): 58 | o = i * 8 59 | v = 0 60 | for n in range(8): 61 | v <<= 8 62 | v |= (arr[o + n]) 63 | self.s[i] = v 64 | 65 | def _hash_then_set_s(self, buf): 66 | m = hashlib.sha256() 67 | m.update(buf) 68 | digest = m.digest() 69 | self._set_s(digest) 70 | 71 | @classmethod 72 | def from_int8_array(cls, arr): 73 | x = Xoshiro256() 74 | x._set_s(arr) 75 | return x 76 | 77 | @classmethod 78 | def from_bytes(cls, buf): 79 | x = Xoshiro256() 80 | x._hash_then_set_s(buf) 81 | return x 82 | 83 | @classmethod 84 | def from_crc32(cls, crc32): 85 | x = Xoshiro256() 86 | buf = int_to_bytes(crc32) 87 | x._hash_then_set_s(buf) 88 | return x 89 | 90 | @classmethod 91 | def from_string(cls, s): 92 | x = Xoshiro256() 93 | buf = string_to_bytes(s) 94 | x._hash_then_set_s(buf) 95 | return x 96 | 97 | def next(self): 98 | result = (rotl((self.s[1] * 5) & MAX_UINT64, 7) * 9) & MAX_UINT64 99 | t = (self.s[1] << 17) & MAX_UINT64 100 | 101 | self.s[2] ^= self.s[0] 102 | self.s[3] ^= self.s[1] 103 | self.s[1] ^= self.s[2] 104 | self.s[0] ^= self.s[3] 105 | 106 | self.s[2] ^= t 107 | 108 | self.s[3] = rotl(self.s[3], 45) & MAX_UINT64 109 | 110 | return result 111 | 112 | def next_double(self): 113 | m = float(MAX_UINT64) + 1 114 | nxt = self.next() 115 | return nxt / m 116 | 117 | def next_int(self, low, high): 118 | return int(self.next_double() * (high - low + 1) + low) & MAX_UINT64 119 | 120 | def next_byte(self): 121 | return self.next_int(0, 255) 122 | 123 | def next_data(self, count): 124 | result = bytearray() 125 | for i in range(count): 126 | result.append(self.next_byte()) 127 | return result 128 | 129 | def jump(self): 130 | global JUMP 131 | 132 | s0 = 0 133 | s1 = 0 134 | s2 = 0 135 | s3 = 0 136 | for i in range(len(JUMP)): 137 | for b in range(64): 138 | if JUMP[i] & (1 << b): 139 | s0 ^= self.s[0] 140 | s1 ^= self.s[1] 141 | s2 ^= self.s[2] 142 | s3 ^= self.s[3] 143 | self.next() 144 | 145 | self.s[0] = s0 146 | self.s[1] = s1 147 | self.s[2] = s2 148 | self.s[3] = s3 149 | 150 | def long_jump(self): 151 | global LONG_JUMP 152 | 153 | s0 = 0 154 | s1 = 0 155 | s2 = 0 156 | s3 = 0 157 | for i in range(len(LONG_JUMP)): 158 | for b in range(64): 159 | if LONG_JUMP[i] & (1 << b): 160 | s0 ^= self.s[0] 161 | s1 ^= self.s[1] 162 | s2 ^= self.s[2] 163 | s3 ^= self.s[3] 164 | self.next() 165 | 166 | self.s[0] = s0 167 | self.s[1] = s1 168 | self.s[2] = s2 169 | self.s[3] = s3 170 | -------------------------------------------------------------------------------- /src/seedsigner/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/models/__init__.py -------------------------------------------------------------------------------- /src/seedsigner/models/encryptedqr.py: -------------------------------------------------------------------------------- 1 | from seedsigner.models.encryption import EncryptedQRCode 2 | 3 | class EncryptedQR: 4 | def __init__(self, encrypted_qr: EncryptedQRCode=None, public_data: str=None): 5 | self._encrypted_qr: EncryptedQRCode = encrypted_qr 6 | self._public_data: str = public_data 7 | self._encryption_key: str = None 8 | 9 | 10 | @property 11 | def encrypted_qr(self) -> EncryptedQRCode: 12 | return self._encrypted_qr 13 | 14 | 15 | @property 16 | def public_data(self) -> str: 17 | return self._public_data 18 | 19 | 20 | @property 21 | def encryption_key(self) -> str: 22 | return self._encryption_key 23 | 24 | 25 | def set_encryption_key(self, encryption_key: str): 26 | self._encryption_key = encryption_key 27 | 28 | 29 | 30 | class EncryptedQRStorage: 31 | def __init__(self): 32 | self._encryptedqr: EncryptedQR = None 33 | 34 | 35 | @property 36 | def encryptedqr(self) -> EncryptedQR: 37 | return self._encryptedqr 38 | 39 | 40 | def set_encryptedqr(self, encryptedqr: EncryptedQR): 41 | self._encryptedqr = encryptedqr 42 | 43 | 44 | def clear_encryptedqr(self): 45 | self._encryptedqr = None 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/seedsigner/models/qr_type.py: -------------------------------------------------------------------------------- 1 | class QRType: 2 | """ 3 | Used with DecodeQR and EncodeQR to communicate qr encoding type 4 | """ 5 | PSBT__BASE64 = "psbt__base64" 6 | PSBT__SPECTER = "psbt__specter" 7 | PSBT__BASE43 = "psbt__base43" 8 | PSBT__UR2 = "psbt__ur2" 9 | 10 | SEED__SEEDQR = "seed__seedqr" 11 | SEED__COMPACTSEEDQR = "seed__compactseedqr" 12 | SEED__UR2 = "seed__ur2" 13 | SEED__MNEMONIC = "seed__mnemonic" 14 | SEED__FOUR_LETTER_MNEMONIC = "seed__four_letter_mnemonic" 15 | SEED__ENCRYPTEDQR = "seed__encryptedqr" 16 | 17 | SETTINGS = "settings" 18 | 19 | XPUB = "xpub" 20 | XPUB__SPECTER = "xpub__specter" 21 | XPUB__UR = "xpub__ur" 22 | 23 | BITCOIN_ADDRESS = "bitcoin_address" 24 | 25 | SIGN_MESSAGE = "sign_message" 26 | 27 | PASSPHRASE = "passphrase" 28 | 29 | ENCRYPTION_KEY = "encryption_key" 30 | 31 | TEXT = "text" 32 | 33 | WALLET__SPECTER = "wallet__specter" 34 | WALLET__UR = "wallet__ur" 35 | WALLET__CONFIGFILE = "wallet__configfile" 36 | WALLET__GENERIC = "wallet__generic" 37 | OUTPUT__UR = "output__ur" 38 | ACCOUNT__UR = "account__ur" 39 | BYTES__UR = "bytes__ur" 40 | 41 | GENERIC_STRING = "generic_string" 42 | 43 | INVALID = "invalid" -------------------------------------------------------------------------------- /src/seedsigner/models/seed_storage.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from seedsigner.models.seed import Seed, ElectrumSeed, InvalidSeedException 3 | from seedsigner.models.settings_definition import SettingsConstants 4 | 5 | 6 | 7 | class SeedStorage: 8 | def __init__(self) -> None: 9 | self.seeds: List[Seed] = [] 10 | self.pending_seed: Seed = None 11 | self._pending_mnemonic: List[str] = [] 12 | self._pending_is_electrum : bool = False 13 | 14 | 15 | def set_pending_seed(self, seed: Seed): 16 | self.pending_seed = seed 17 | 18 | 19 | def get_pending_seed(self) -> Seed: 20 | return self.pending_seed 21 | 22 | 23 | def finalize_pending_seed(self) -> int: 24 | # Finally store the pending seed and return its index 25 | if self.pending_seed in self.seeds: 26 | index = self.seeds.index(self.pending_seed) 27 | else: 28 | self.seeds.append(self.pending_seed) 29 | index = len(self.seeds) - 1 30 | self.pending_seed = None 31 | return index 32 | 33 | 34 | def clear_pending_seed(self): 35 | self.pending_seed = None 36 | 37 | 38 | def validate_mnemonic(self, mnemonic: List[str]) -> bool: 39 | try: 40 | Seed(mnemonic=mnemonic) 41 | except InvalidSeedException as e: 42 | return False 43 | 44 | return True 45 | 46 | 47 | def num_seeds(self): 48 | return len(self.seeds) 49 | 50 | 51 | @property 52 | def pending_mnemonic(self) -> List[str]: 53 | # Always return a copy so that the internal List can't be altered 54 | return list(self._pending_mnemonic) 55 | 56 | 57 | @property 58 | def pending_mnemonic_length(self) -> int: 59 | return len(self._pending_mnemonic) 60 | 61 | 62 | def init_pending_mnemonic(self, num_words:int = 12, is_electrum:bool = False): 63 | self._pending_mnemonic = [None] * num_words 64 | self._pending_is_electrum = is_electrum 65 | 66 | 67 | def update_pending_mnemonic(self, word: str, index: int): 68 | """ 69 | Replaces the nth word in the pending mnemonic. 70 | 71 | * may specify a negative `index` (e.g. -1 is the last word). 72 | """ 73 | if index >= len(self._pending_mnemonic): 74 | raise Exception(f"index {index} is too high") 75 | self._pending_mnemonic[index] = word 76 | 77 | 78 | def get_pending_mnemonic_word(self, index: int) -> str: 79 | if index < len(self._pending_mnemonic): 80 | return self._pending_mnemonic[index] 81 | return None 82 | 83 | 84 | def get_pending_mnemonic_fingerprint(self, network: str = SettingsConstants.MAINNET) -> str: 85 | try: 86 | if self._pending_is_electrum: 87 | seed = ElectrumSeed(self._pending_mnemonic) 88 | else: 89 | seed = Seed(self._pending_mnemonic) 90 | return seed.get_fingerprint(network) 91 | except InvalidSeedException: 92 | return None 93 | 94 | 95 | def convert_pending_mnemonic_to_pending_seed(self): 96 | if self._pending_is_electrum: 97 | self.pending_seed = ElectrumSeed(self._pending_mnemonic) 98 | else: 99 | self.pending_seed = Seed(self._pending_mnemonic) 100 | self.discard_pending_mnemonic() 101 | 102 | 103 | def discard_pending_mnemonic(self): 104 | self._pending_mnemonic = [] 105 | self._pending_is_electrum = False 106 | -------------------------------------------------------------------------------- /src/seedsigner/models/singleton.py: -------------------------------------------------------------------------------- 1 | class Singleton: 2 | _instance = None 3 | 4 | def __init__(self): 5 | # Singleton pattern must prevent normal instantiation 6 | raise Exception("Cannot directly instantiate a Singleton. Access via get_instance()") 7 | 8 | @classmethod 9 | def get_instance(cls): 10 | # This is the only way to access the one and only instance 11 | if cls._instance is None: 12 | cls._instance = cls.__new__(cls) 13 | return cls._instance 14 | 15 | 16 | 17 | class ConfigurableSingleton(Singleton): 18 | @classmethod 19 | def get_instance(cls): 20 | # This is the only way to access the one and only instance 21 | if cls._instance: 22 | return cls._instance 23 | else: 24 | raise Exception("Must call %s.configure_instance() first" % cls.__name__) 25 | 26 | 27 | @classmethod 28 | def configure_instance(cls, config: any = None): 29 | # Must be called before the first get_instance() call 30 | if cls._instance: 31 | raise Exception("Instance already configured") 32 | 33 | #TODO: Implementation classes should do something with incoming config 34 | -------------------------------------------------------------------------------- /src/seedsigner/models/threads.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from threading import Thread, Lock 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | 7 | class BaseThread(Thread): 8 | def __init__(self): 9 | super().__init__(daemon=True) 10 | 11 | def start(self): 12 | logger.debug(f"{self.__class__.__name__} STARTING") 13 | self.keep_running = True 14 | super().start() 15 | 16 | def stop(self): 17 | logger.debug(f"{self.__class__.__name__} EXITING") 18 | self.keep_running = False 19 | 20 | def run(self): 21 | while self.keep_running: 22 | # Do something 23 | raise Exception(f"Must implement run() in {self.__class__.__name__}") 24 | 25 | 26 | 27 | class ThreadsafeCounter: 28 | def __init__(self, initial_value: int = 0): 29 | self.count = initial_value 30 | self._lock = Lock() 31 | 32 | @property 33 | def cur_count(self): 34 | # Reads don't require the lock 35 | return self.count 36 | 37 | def increment(self, step: int = 1): 38 | # Updates must be locked 39 | with self._lock: 40 | self.count += step 41 | 42 | def set_value(self, value: int): 43 | with self._lock: 44 | self.count = value 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/seedsigner/resources/fonts/Font_Awesome_6_Free-Solid-900.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/fonts/Font_Awesome_6_Free-Solid-900.otf -------------------------------------------------------------------------------- /src/seedsigner/resources/fonts/Inconsolata-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/fonts/Inconsolata-Regular.ttf -------------------------------------------------------------------------------- /src/seedsigner/resources/fonts/Inconsolata-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/fonts/Inconsolata-SemiBold.ttf -------------------------------------------------------------------------------- /src/seedsigner/resources/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /src/seedsigner/resources/fonts/OpenSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/fonts/OpenSans-SemiBold.ttf -------------------------------------------------------------------------------- /src/seedsigner/resources/fonts/PlemolJPConsole-Regular-S.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/fonts/PlemolJPConsole-Regular-S.ttf -------------------------------------------------------------------------------- /src/seedsigner/resources/fonts/PlemolJPConsole-SemiBold-S.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/fonts/PlemolJPConsole-SemiBold-S.ttf -------------------------------------------------------------------------------- /src/seedsigner/resources/fonts/RobotoCondensed-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/fonts/RobotoCondensed-Bold.ttf -------------------------------------------------------------------------------- /src/seedsigner/resources/fonts/RobotoCondensed-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/fonts/RobotoCondensed-Regular.ttf -------------------------------------------------------------------------------- /src/seedsigner/resources/fonts/seedsigner-icons.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/fonts/seedsigner-icons.otf -------------------------------------------------------------------------------- /src/seedsigner/resources/icons/arrow-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/icons/arrow-down.png -------------------------------------------------------------------------------- /src/seedsigner/resources/icons/arrow-down_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/icons/arrow-down_selected.png -------------------------------------------------------------------------------- /src/seedsigner/resources/icons/arrow-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/icons/arrow-up.png -------------------------------------------------------------------------------- /src/seedsigner/resources/icons/arrow-up_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/icons/arrow-up_selected.png -------------------------------------------------------------------------------- /src/seedsigner/resources/icons/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/icons/back.png -------------------------------------------------------------------------------- /src/seedsigner/resources/icons/back_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/icons/back_selected.png -------------------------------------------------------------------------------- /src/seedsigner/resources/icons/btc_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/icons/btc_logo.png -------------------------------------------------------------------------------- /src/seedsigner/resources/icons/btc_logo_30x30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/icons/btc_logo_30x30.png -------------------------------------------------------------------------------- /src/seedsigner/resources/icons/btc_logo_bw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/icons/btc_logo_bw.png -------------------------------------------------------------------------------- /src/seedsigner/resources/icons/dire_warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/icons/dire_warning.png -------------------------------------------------------------------------------- /src/seedsigner/resources/icons/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/icons/warning.png -------------------------------------------------------------------------------- /src/seedsigner/resources/img/btc_logo_60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/img/btc_logo_60x60.png -------------------------------------------------------------------------------- /src/seedsigner/resources/img/logo_black_240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/img/logo_black_240.png -------------------------------------------------------------------------------- /src/seedsigner/resources/img/partners/hrf_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/src/seedsigner/resources/img/partners/hrf_logo.png -------------------------------------------------------------------------------- /src/seedsigner/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .view import * # base class has to be first 2 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Running Tests 2 | 3 | The tests are designed to be run on non-Raspi hardware. 4 | 5 | ## Setup 6 | On your testing machine you'll have to install: 7 | ```bash 8 | # general dependencies 9 | pip3 install -r requirements.txt 10 | 11 | # test suite dependencies 12 | pip3 install -r tests/requirements.txt 13 | ``` 14 | 15 | Then make the `seedsigner` python module visible/importable to the tests by installing it: 16 | ``` 17 | pip3 install -e . 18 | ``` 19 | 20 | ## Running all tests, calculating overall test coverage 21 | tldr: just run the convenience script from the project root: 22 | 23 | ```bash 24 | ./tests/run_full_coverage.sh 25 | ``` 26 | 27 | ## Running tests manually 28 | Run the whole test suite: 29 | ``` 30 | pytest 31 | ``` 32 | 33 | Run a specific test file: 34 | ``` 35 | pytest tests/test_this_file.py 36 | ``` 37 | 38 | Run a specific test: 39 | ``` 40 | pytest tests/test_this_file.py::test_this_specific_test 41 | ``` 42 | 43 | Force pytest to show logging output: 44 | ```bash 45 | pytest tests/test_this_file.py::test_this_specific_test -o log_cli=1 46 | 47 | # or (same result) 48 | 49 | pytest tests/test_this_file.py::test_this_specific_test --log-cli-level=DEBUG 50 | ``` 51 | 52 | Annoying complications: 53 | * If you want to see `print()` statements that are in a test file, add `-s` 54 | * Better idea: use a proper logger in the test file and use one of the above options to display logs 55 | 56 | 57 | ## Screenshot generator 58 | The screenshot generator is meant to mostly be a utility and not really part of the test suite. However, 59 | it is actually implemented to be run by `pytest`. 60 | 61 | see: [Screenshot generator README](screenshot_generator/README.md) 62 | 63 | 64 | ## Generate coverage manually 65 | Run tests and generate test coverage 66 | ```bash 67 | coverage run -m pytest 68 | ``` 69 | 70 | The screenshots can generate their own separate coverage report: 71 | ```bash 72 | coverage run -m pytest tests/screenshot_generator/generator.py --locale es 73 | ``` 74 | 75 | Show the resulting test coverage details: 76 | ```bash 77 | coverage report 78 | ``` 79 | 80 | Generate the interactive html report: 81 | ```bash 82 | coverage html 83 | ``` -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | coverage==7.3.1 2 | pytest==7.4.2 3 | pytest-cov==4.1.0 4 | -------------------------------------------------------------------------------- /tests/run_full_coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Clean up any prior coverage results 4 | coverage erase 5 | 6 | # Run the full test suite; `--parallel` writes coverage results to a separate file 7 | # (otherwise it'll be overwritten in the next step) 8 | coverage run --parallel -m pytest 9 | 10 | # Generate screenshots (only need to run for one locale to assess coverage) 11 | coverage run --parallel -m pytest tests/screenshot_generator/generator.py --locale es 12 | 13 | # Combine the above coverage results 14 | coverage combine 15 | 16 | # Show the report in the terminal 17 | coverage report 18 | 19 | # Generate the interactive html report 20 | coverage html 21 | -------------------------------------------------------------------------------- /tests/screenshot_generator/README.md: -------------------------------------------------------------------------------- 1 | # Screenshot Generator 2 | 3 | From the project root, run: 4 | ```bash 5 | # Generate screenshots for a specific locale 6 | pytest tests/screenshot_generator/generator.py --locale es 7 | 8 | # Generate screenshots for all supported locales 9 | pytest tests/screenshot_generator/generator.py 10 | ``` 11 | 12 | You can also run a `coverage` report to see exactly what the screenshots are and are not hitting: 13 | ```bash 14 | coverage erase 15 | coverage run -m pytest tests/screenshot_generator/generator.py --locale es && coverage combine && coverage report 16 | 17 | # Generate the interactive html report 18 | coverage html 19 | ``` 20 | 21 | Writes the screenshots to a dir in the project root: `seedsigner-screenshots`. 22 | -------------------------------------------------------------------------------- /tests/screenshot_generator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rdIteration/seedsigner/03f427b20b8acdceea9e064141bb8434727cc6aa/tests/screenshot_generator/__init__.py -------------------------------------------------------------------------------- /tests/screenshot_generator/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | 5 | def pytest_addoption(parser): 6 | parser.addoption("--locale", action="store", default=None) 7 | 8 | 9 | @pytest.fixture(scope='session') 10 | def target_locale(request): 11 | return request.config.option.locale 12 | -------------------------------------------------------------------------------- /tests/screenshot_generator/template.md: -------------------------------------------------------------------------------- 1 | # SeedSigner Screenshots 2 | 3 | SeedSigner screenshots can be freely used in any tutorial, article, video, etc. As a courtesy, please link back to this repo or the SeedSigner website in your attribution. 4 | 5 |   6 | 7 |   8 | 9 | 10 | ## Generating screenshots 11 | The screenshot generator is integrated into the SeedSigner test suite and requires a local copy of the SeedSigner repo with the test suite dependencies installed. 12 | 13 | see: https://github.com/SeedSigner/seedsigner/blob/dev/tests/screenshot_generator/README.md 14 | 15 | 16 | ## Currently supported or in-progress languages 17 | -------------------------------------------------------------------------------- /tests/screenshot_generator/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dataclasses import dataclass 4 | from PIL import Image, ImageDraw 5 | 6 | from seedsigner.gui.renderer import Renderer 7 | from seedsigner.gui.toast import BaseToastOverlayManagerThread 8 | from seedsigner.views.view import View 9 | 10 | 11 | 12 | class ScreenshotComplete(Exception): 13 | pass 14 | 15 | 16 | 17 | class ScreenshotRenderer(Renderer): 18 | screenshot_path: str = None 19 | screenshot_filename: str = None 20 | 21 | @classmethod 22 | def configure_instance(cls): 23 | # Instantiate the one and only Renderer instance 24 | renderer = cls.__new__(cls) 25 | cls._instance = renderer 26 | 27 | # Hard-coding output values for now 28 | renderer.canvas_width = 240 29 | renderer.canvas_height = 240 30 | 31 | renderer.canvas = Image.new('RGB', (renderer.canvas_width, renderer.canvas_height)) 32 | renderer.draw = ImageDraw.Draw(renderer.canvas) 33 | 34 | 35 | def set_screenshot_filename(self, filename:str): 36 | self.screenshot_filename = filename 37 | 38 | 39 | def set_screenshot_path(self, path): 40 | if not os.path.exists(path): 41 | os.makedirs(path) 42 | self.screenshot_path = path 43 | 44 | 45 | def show_image(self, image=None, alpha_overlay=None, is_background_thread: bool = False): 46 | if is_background_thread: 47 | return 48 | 49 | if alpha_overlay: 50 | if image == None: 51 | image = self.canvas 52 | image = Image.alpha_composite(image, alpha_overlay) 53 | 54 | if image: 55 | # Always write to the current canvas, rather than trying to replace it 56 | self.canvas.paste(image) 57 | 58 | self.canvas.save(os.path.join(self.screenshot_path, self.screenshot_filename)) 59 | raise ScreenshotComplete() 60 | 61 | 62 | 63 | @dataclass 64 | class ScreenshotConfig: 65 | View_cls: View 66 | view_kwargs: dict = None 67 | screenshot_name: str = None 68 | toast_thread: BaseToastOverlayManagerThread = None 69 | run_before: callable = None 70 | run_after: callable = None 71 | 72 | 73 | def __post_init__(self): 74 | if not self.view_kwargs: 75 | self.view_kwargs = {} 76 | if not self.screenshot_name: 77 | self.screenshot_name = self.View_cls.__name__ 78 | 79 | 80 | def run_callback_before(self): 81 | if self.run_before: 82 | self.run_before() 83 | 84 | 85 | def run_callback_after(self): 86 | if self.run_after: 87 | self.run_after() 88 | -------------------------------------------------------------------------------- /tests/test_bip85.py: -------------------------------------------------------------------------------- 1 | from seedsigner.models.seed import Seed 2 | 3 | 4 | def test_derive_child_mnemonic(): 5 | 6 | expected = "unusual topic foot figure pulp target glimpse core electric spot neglect fame" 7 | seed = Seed(mnemonic="resource timber firm banner horror pupil frozen main pear direct pioneer broken grid core insane begin sister pony end debate task silk empty curious".split()) 8 | 9 | actual = seed.get_bip85_child_mnemonic(0, 12) 10 | assert actual == expected 11 | 12 | 13 | expected = "imitate post very mandate retreat prevent tiny snow fetch canvas town shrug fix food summer library symptom occur slam style cruise wolf phone key" 14 | seed = Seed(mnemonic="resource timber firm banner horror pupil frozen main pear direct pioneer broken grid core insane begin sister pony end debate task silk empty curious".split()) 15 | 16 | actual = seed.get_bip85_child_mnemonic(0, 24) 17 | assert actual == expected 18 | -------------------------------------------------------------------------------- /tests/test_flows_l10n.py: -------------------------------------------------------------------------------- 1 | from gettext import gettext as _ 2 | 3 | # Must import test base before the Controller 4 | from base import FlowTest, FlowStep 5 | 6 | from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, ButtonOption 7 | from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition 8 | from seedsigner.views import settings_views 9 | from seedsigner.views.view import MainMenuView 10 | 11 | 12 | 13 | class TestL10nFlows(FlowTest): 14 | def test_change_locale(self): 15 | settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__LOCALE) 16 | spanish_display_name = [locale_tuple[1] for locale_tuple in SettingsConstants.ALL_LOCALES if locale_tuple[0] == SettingsConstants.LOCALE__SPANISH][0] 17 | 18 | # Initially we get English 19 | assert _(MainMenuView.SCAN.button_label) == "Scan" 20 | 21 | self.run_sequence([ 22 | FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS), 23 | FlowStep(settings_views.SettingsMenuView, button_data_selection=ButtonOption(settings_entry.display_name)), 24 | FlowStep(settings_views.SettingsEntryUpdateSelectionView, button_data_selection=ButtonOption(spanish_display_name)), 25 | ]) 26 | 27 | # Now we don't get English 28 | assert _(MainMenuView.SCAN.button_label) != "Scan" 29 | -------------------------------------------------------------------------------- /tests/test_flows_view.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | # Must import test base before the Controller 4 | from base import FlowTest, FlowStep 5 | 6 | from seedsigner.gui.screens.screen import RET_CODE__POWER_BUTTON 7 | from seedsigner.models.settings import Settings 8 | from seedsigner.views.tools_views import ToolsCalcFinalWordNumWordsView, ToolsMenuView 9 | from seedsigner.views.view import MainMenuView, NotYetImplementedView, PowerOptionsView, PowerOffView, RestartView, UnhandledExceptionView, View 10 | 11 | 12 | 13 | class TestViewFlows(FlowTest): 14 | 15 | def test_restart_flow(self): 16 | """ 17 | Basic flow from MainMenuView to RestartView 18 | """ 19 | with patch('seedsigner.views.view.RestartView.DoResetThread'): 20 | self.run_sequence([ 21 | FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON), 22 | FlowStep(PowerOptionsView, button_data_selection=PowerOptionsView.RESET), 23 | FlowStep(RestartView), 24 | ]) 25 | 26 | 27 | def test_power_off_flow(self): 28 | """ 29 | Basic flow from MainMenuView to PowerOffView 30 | """ 31 | Settings.HOSTNAME = Settings.SEEDSIGNER_OS 32 | self.run_sequence([ 33 | FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON), 34 | FlowStep(PowerOptionsView, button_data_selection=PowerOptionsView.POWER_OFF), 35 | FlowStep(PowerOffView), # returns BackStackView 36 | FlowStep(PowerOptionsView), 37 | ]) 38 | 39 | 40 | def test_not_yet_implemented_flow(self): 41 | """ 42 | Run an incomplete View that returns None and ensure that we get the NotYetImplementedView 43 | """ 44 | class IncompleteView(View): 45 | def run(self): 46 | self.run_screen(None) 47 | return None 48 | 49 | self.run_sequence([ 50 | FlowStep(IncompleteView), 51 | FlowStep(NotYetImplementedView), 52 | FlowStep(MainMenuView), 53 | ]) 54 | 55 | 56 | def test_unhandled_exception_flow(self): 57 | """ 58 | Basic flow from any arbitrary View to the UnhandledExceptionView 59 | """ 60 | self.run_sequence([ 61 | FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS), 62 | FlowStep(ToolsMenuView, button_data_selection=ToolsMenuView.KEYBOARD), 63 | FlowStep(ToolsCalcFinalWordNumWordsView, screen_return_value=Exception("Test exception")), # <-- force an exception 64 | FlowStep(UnhandledExceptionView), 65 | FlowStep(MainMenuView), 66 | ]) 67 | -------------------------------------------------------------------------------- /tests/test_l10n.py: -------------------------------------------------------------------------------- 1 | from gettext import gettext as _ 2 | 3 | from base import BaseTest 4 | from seedsigner.gui.screens.screen import ButtonOption 5 | from seedsigner.helpers.l10n import mark_for_translation as _mft 6 | from seedsigner.models.settings import Settings 7 | from seedsigner.models.settings_definition import SettingsConstants 8 | from seedsigner.views.view import MainMenuView 9 | 10 | 11 | 12 | class TestGettext(BaseTest): 13 | def test_english_as_default(self): 14 | # Key is available in other languages, but we get English back 15 | assert _("Home") == "Home" 16 | 17 | def test_missing_key_returns_english_key(self): 18 | test_str = "This is not in our translation library" 19 | assert _(test_str) == test_str 20 | 21 | 22 | def test_basic_spanish(self): 23 | settings = Settings.get_instance() 24 | settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH) 25 | assert _("Home") != "Home" 26 | 27 | 28 | def test_locale_changes(self): 29 | settings = Settings.get_instance() 30 | 31 | settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH) 32 | spanish_str = _("Home") 33 | 34 | settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__ENGLISH) 35 | assert spanish_str != _("Home") 36 | assert _("Home") == "Home" 37 | 38 | 39 | 40 | class TestButtonOption(BaseTest): 41 | def test_english_key_not_translated(self): 42 | """ ButtonOption should always return its English key, regardless of current locale setting. """ 43 | button_option = ButtonOption("Home") 44 | settings = Settings.get_instance() 45 | settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH) 46 | 47 | assert button_option.button_label == "Home" 48 | 49 | brand_new_button_option = ButtonOption("Tools") 50 | assert brand_new_button_option.button_label == "Tools" 51 | 52 | 53 | def test_class_level_button_option_english_key_not_translated(self): 54 | settings = Settings.get_instance() 55 | settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH) 56 | 57 | class FooClass: 58 | HOME = ButtonOption("Home") 59 | 60 | assert FooClass.HOME.button_label == "Home" 61 | 62 | 63 | def test_gettext_translates_class_level_button_option(self): 64 | settings = Settings.get_instance() 65 | settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH) 66 | 67 | class BarClass: 68 | HOME = ButtonOption("Home") 69 | 70 | assert _(BarClass.HOME.button_label) != "Home" 71 | 72 | 73 | 74 | class TestMarkForTranslation(BaseTest): 75 | def test_english_key_not_translated(self): 76 | """ _mft() should always return its English key, regardless of current locale setting. """ 77 | mft_attr = _mft("Home") 78 | settings = Settings.get_instance() 79 | settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH) 80 | 81 | assert mft_attr == "Home" 82 | 83 | brand_new_mft_attr = _mft("Tools") 84 | assert brand_new_mft_attr == "Tools" 85 | 86 | 87 | def test_class_level_mft_attr_english_key_not_translated(self): 88 | settings = Settings.get_instance() 89 | settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH) 90 | 91 | class FooClass: 92 | home = _mft("Home") 93 | 94 | assert FooClass.home == "Home" 95 | 96 | 97 | def test_gettext_translates_class_level_mft_attr(self): 98 | settings = Settings.get_instance() 99 | settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH) 100 | 101 | class BarClass: 102 | home = _mft("Home") 103 | 104 | assert _(BarClass.home) != "Home" 105 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pytest 3 | import sys 4 | from unittest.mock import patch, call 5 | 6 | # Must import from base.py before any other SeedSigner dependency to mock out certain imports 7 | from base import BaseTest 8 | 9 | sys.path.insert(0,'src') 10 | from main import main 11 | 12 | 13 | class TestMain(BaseTest): 14 | """ 15 | The `main.Controller` has to be patched / mocked out otherwise the virtual SeedSigner 16 | will just keep running and cause the test to hang. 17 | """ 18 | @patch("main.Controller") 19 | def test_main__argparse__default(self, patched_controller): 20 | main([]) 21 | assert logging.root.level == logging.INFO 22 | assert logging.getLogger().getEffectiveLevel() == logging.INFO 23 | patched_controller.assert_has_calls( 24 | [call.get_instance(), call.get_instance().start()] 25 | ) 26 | 27 | 28 | @patch("main.Controller") 29 | def test_main__argparse__enable_debug_logging(self, patched_controller): 30 | main(["--loglevel", "DEBUG"]) 31 | assert logging.root.level == logging.DEBUG 32 | assert logging.getLogger().getEffectiveLevel() == logging.DEBUG 33 | patched_controller.assert_has_calls( 34 | [call.get_instance(), call.get_instance().start()] 35 | ) 36 | 37 | 38 | def test_main__argparse__invalid_arg(self): 39 | with pytest.raises(SystemExit): 40 | main(["--invalid"]) 41 | 42 | 43 | @patch("main.Controller") 44 | def test_main__logging__writes_to_stderr(self, patched_controller, capsys): 45 | main([]) 46 | _, err = capsys.readouterr() 47 | assert "Starting SeedSigner" in err and "INFO" in err 48 | patched_controller.assert_has_calls( 49 | [call.get_instance(), call.get_instance().start()] 50 | ) -------------------------------------------------------------------------------- /tests/test_seed.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from seedsigner.models.seed import InvalidSeedException, Seed, ElectrumSeed 3 | 4 | from seedsigner.models.settings import SettingsConstants 5 | 6 | 7 | # TODO: Change TAB indents to SPACE 8 | 9 | def test_seed(): 10 | seed = Seed(mnemonic="obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash".split()) 11 | 12 | assert seed.seed_bytes == b'q\xb3\xd1i\x0c\x9b\x9b\xdf\xa7\xd9\xd97H\xa8,\xa7\xd9>\xeck\xc2\xf5ND?, \x88-\x07\x9aa\xc5\xee\xb7\xbf\xc4x\xd6\x07 X\xb6}?M\xaa\x05\xa6\xa7(>\xbf\x03\xb0\x9d\xef\xed":\xdf\x88w7' 13 | 14 | assert seed.mnemonic_str == "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" 15 | 16 | assert seed.passphrase == "" 17 | 18 | # TODO: Not yet supported in new implementation 19 | # seed.set_wordlist_language_code("es") 20 | 21 | # assert seed.mnemonic_str == "natural ayuda futuro nivel espejo abuelo vago bien repetir moreno relevo conga" 22 | 23 | # seed.set_wordlist_language_code(SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) 24 | 25 | # seed.mnemonic_str = "height demise useless trap grow lion found off key clown transfer enroll" 26 | 27 | # assert seed.mnemonic_str == "height demise useless trap grow lion found off key clown transfer enroll" 28 | 29 | # # TODO: Not yet supported in new implementation 30 | # seed.set_wordlist_language_code("es") 31 | 32 | # assert seed.mnemonic_str == "hebilla cría truco tigre gris llenar folio negocio laico casa tieso eludir" 33 | 34 | # seed.set_passphrase("test") 35 | 36 | # assert seed.seed_bytes == b'\xdd\r\xcb\x0b V\xb4@\xee+\x01`\xabem\xc1B\xfd\x8fba0\xab;[\xab\xc9\xf9\xba[F\x0c5,\x7fd8\xebI\x90"\xb8\x86C\x821\x01\xdb\xbe\xf3\xbc\x1cBH"%\x18\xc2{\x04\x08a]\xa5' 37 | 38 | # assert seed.passphrase == "test" 39 | 40 | 41 | def test_electrum_seed(): 42 | """ 43 | ElectrumSeed should correctly parse a modern Electrum mnemonic. 44 | """ 45 | seed = ElectrumSeed(mnemonic="regular reject rare profit once math fringe chase until ketchup century escape".split()) 46 | 47 | intended_seed = b'\xcan|\xf8\x8a\x8d\xf78=Pq\xc4_\xe6\x02\x91\xfcs\xb2[\xed*\xdc\xc7%\xb6[_-(~D\xe5\x1e\x85%N\x9c\x03\x9dh\xafX}\x16\xb1\x99,\xbe\xc4\x11\xfaW\x0f\xb0\x89yD\xf4\x0f\xd5?\x8eA' 48 | 49 | assert seed.seed_bytes == intended_seed 50 | 51 | 52 | def test_electrum_mnemonic_format(): 53 | """ 54 | ElectrumSeed should reject mnemonics that are not 12 words long. 55 | """ 56 | with pytest.raises(InvalidSeedException): 57 | ElectrumSeed(mnemonic=["regular"] * 11) 58 | 59 | with pytest.raises(InvalidSeedException): 60 | ElectrumSeed(mnemonic=["regular"] * 13) 61 | 62 | with pytest.raises(InvalidSeedException): 63 | ElectrumSeed(mnemonic=["regular"] * 24) 64 | 65 | 66 | def test_electrum_seed_rejects_most_bip39_mnemonics(): 67 | """ 68 | ElectrumSeed should throw an exception for most BIP-39 mnemonics. 69 | 70 | There are 1/16 odds that a seed will be valid for both formats. 71 | """ 72 | # Most BIP-39 seeds should fail; test seeds generated by bitcoiner.guide 73 | with pytest.raises(InvalidSeedException): 74 | ElectrumSeed(mnemonic="pioneer divide volcano art victory family grow novel mandate bicycle senior adjust".split()) 75 | 76 | with pytest.raises(InvalidSeedException): 77 | ElectrumSeed(mnemonic="gentle combine cool hamster ghost harvest gossip lend dismiss slam any toast".split()) 78 | 79 | with pytest.raises(InvalidSeedException): 80 | ElectrumSeed(mnemonic="enough board blossom stamp fire buffalo digital solution sadness random number stone".split()) 81 | 82 | # This one is valid for both formats 83 | mnemonic = "only gain spot output unknown craft simple cram absorb suggest ridge famous".split() 84 | Seed(mnemonic) 85 | ElectrumSeed(mnemonic) 86 | -------------------------------------------------------------------------------- /tests/test_seedqr.py: -------------------------------------------------------------------------------- 1 | import os 2 | from embit import bip39 3 | from seedsigner.helpers.qr import QR 4 | from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus 5 | from seedsigner.models.encode_qr import SeedQrEncoder, CompactSeedQrEncoder 6 | from seedsigner.models.qr_type import QRType 7 | 8 | 9 | 10 | def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type): 11 | """ Helper method to re-run multiple variations of the same encode/decode test """ 12 | mnemonic = bip39.mnemonic_from_bytes(entropy).split() 13 | assert len(mnemonic) == mnemonic_length 14 | 15 | if qr_type == QRType.SEED__SEEDQR: 16 | e = SeedQrEncoder(mnemonic=mnemonic) 17 | elif qr_type == QRType.SEED__COMPACTSEEDQR: 18 | e = CompactSeedQrEncoder(mnemonic=mnemonic) 19 | 20 | data = e.next_part() 21 | 22 | qr = QR() 23 | image = qr.qrimage( 24 | data=data, 25 | width=240, 26 | height=240, 27 | border=3 28 | ) 29 | 30 | decoder = DecodeQR() 31 | status = decoder.add_image(image) 32 | assert status == DecodeQRStatus.COMPLETE 33 | 34 | decoded_seed_phrase = decoder.get_seed_phrase() 35 | assert mnemonic == decoded_seed_phrase 36 | 37 | 38 | 39 | def test_standard_seedqr_encode_decode_(): 40 | """ Should encode 24- and 12- word mnemonics to Standard SeedQR format and decode 41 | them back again to their original mnemonic seed phrase. 42 | """ 43 | # 24-word seed 44 | run_encode_decode_test(os.urandom(32), mnemonic_length=24, qr_type=QRType.SEED__SEEDQR) 45 | 46 | # 12-word seed 47 | run_encode_decode_test(os.urandom(16), mnemonic_length=12, qr_type=QRType.SEED__SEEDQR) 48 | 49 | 50 | 51 | def test_compact_seedqr_encode_decode(): 52 | """ Should encode 24- and 12- word mnemonics to CompactSeedQR format and decode 53 | them back again to their original mnemonic seed phrase. 54 | """ 55 | # 24-word seed 56 | run_encode_decode_test(os.urandom(32), mnemonic_length=24, qr_type=QRType.SEED__COMPACTSEEDQR) 57 | 58 | # 12-word seed 59 | run_encode_decode_test(os.urandom(16), mnemonic_length=12, qr_type=QRType.SEED__COMPACTSEEDQR) 60 | 61 | 62 | 63 | def test_compact_seedqr_handles_null_bytes(): 64 | """ Should properly encode a CompactSeedQR with null bytes (b'\x00') in the input 65 | entropy and decode it back to the original mnemonic seed. 66 | """ 67 | # 24-word seed, null bytes at the front 68 | entropy = b'\x00' + os.urandom(31) 69 | run_encode_decode_test(entropy, mnemonic_length=24, qr_type=QRType.SEED__COMPACTSEEDQR) 70 | 71 | # 24-word seed, null bytes in the middle 72 | entropy = os.urandom(10) + b'\x00' + os.urandom(21) 73 | run_encode_decode_test(entropy, mnemonic_length=24, qr_type=QRType.SEED__COMPACTSEEDQR) 74 | 75 | # 24-word seed, null bytes at the end 76 | entropy = os.urandom(31) + b'\x00' 77 | run_encode_decode_test(entropy, mnemonic_length=24, qr_type=QRType.SEED__COMPACTSEEDQR) 78 | 79 | # 24-word seed, multiple null bytes 80 | entropy = os.urandom(5) + b'\x00' + os.urandom(5) + b'\x00' + os.urandom(20) 81 | run_encode_decode_test(entropy, mnemonic_length=24, qr_type=QRType.SEED__COMPACTSEEDQR) 82 | 83 | # 24-word seed, multiple null bytes in a row 84 | entropy = os.urandom(10) + b'\x00\x00' + os.urandom(20) 85 | run_encode_decode_test(entropy, mnemonic_length=24, qr_type=QRType.SEED__COMPACTSEEDQR) 86 | 87 | # 12-word seed, null bytes at the beginning 88 | entropy = b'\x00' + os.urandom(15) 89 | run_encode_decode_test(entropy, mnemonic_length=12, qr_type=QRType.SEED__COMPACTSEEDQR) 90 | 91 | # 12-word seed, null bytes in the middle 92 | entropy = os.urandom(5) + b'\x00' + os.urandom(10) 93 | run_encode_decode_test(entropy, mnemonic_length=12, qr_type=QRType.SEED__COMPACTSEEDQR) 94 | 95 | # 12-word seed, null bytes at the end 96 | entropy = os.urandom(15) + b'\x00' 97 | run_encode_decode_test(entropy, mnemonic_length=12, qr_type=QRType.SEED__COMPACTSEEDQR) 98 | 99 | # 12-word seed, multiple null bytes 100 | entropy = os.urandom(5) + b'\x00' + os.urandom(5) + b'\x00' + os.urandom(4) 101 | run_encode_decode_test(entropy, mnemonic_length=12, qr_type=QRType.SEED__COMPACTSEEDQR) 102 | 103 | # 12-word seed, multiple null bytes in a row 104 | entropy = os.urandom(10) + b'\x00\x00' + os.urandom(4) 105 | run_encode_decode_test(entropy, mnemonic_length=12, qr_type=QRType.SEED__COMPACTSEEDQR) 106 | 107 | 108 | def test_compact_seedqr_bytes_interpretable_as_str(): 109 | """ 110 | Should successfully decode a Compact SeedQR whose bytes can be interpreted as a valid 111 | string. Most Compact SeedQR byte data will raise a UnicodeDecodeError when attempting to 112 | interpret it as a string, but edge cases are possible. 113 | 114 | see: Issue #656 115 | """ 116 | # Randomly generated to pass the str.decode() step; 12- and 24-word entropy. 117 | entropy_bytes_tests = [ 118 | b'\x00' * 16, # abandon * 11 + about 119 | b'\x12\x15\\1j`3\x0bkL}f\x00ZYK', 120 | b'tv\x1bZjmqN@t\x13\x1aK\\v)', 121 | b'|9\x05\x1aHF9j\xda\xb6v\x05\x08#\x12=', 122 | b"iHK`4\x1a5\xd3\xaf\xd3\xb47htJ.}<\xea\xbf\x88Xh\x01.?R2^\xc2\xb1'", 123 | b'|Z\x11\x1dt\xdd\x97~t&f &G$H|^[\xd3\x9d\x19Z{\ng^', 125 | ] 126 | 127 | for entropy_bytes in entropy_bytes_tests: 128 | entropy_bytes.decode() # should not raise an exception 129 | mnemonic_length = 12 if len(entropy_bytes) == 16 else 24 130 | run_encode_decode_test(entropy_bytes, mnemonic_length=mnemonic_length, qr_type=QRType.SEED__COMPACTSEEDQR) 131 | -------------------------------------------------------------------------------- /tests/test_settings.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from base import BaseTest 3 | from seedsigner.models.settings import InvalidSettingsQRData, Settings 4 | from seedsigner.models.settings_definition import SettingsConstants 5 | 6 | 7 | 8 | class TestSettings(BaseTest): 9 | @classmethod 10 | def setup_class(cls): 11 | super().setup_class() 12 | cls.settings = Settings.get_instance() 13 | 14 | 15 | def test_reset_settings(self): 16 | """ BaseTest.reset_settings() should wipe out any previous Settings changes """ 17 | settings = Settings.get_instance() 18 | settings.set_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS, SettingsConstants.OPTION__ENABLED) 19 | assert settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__ENABLED 20 | 21 | BaseTest.reset_settings() 22 | settings = Settings.get_instance() 23 | assert settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED 24 | 25 | 26 | def test_parse_settingsqr_data(self): 27 | """ 28 | SettingsQR parser should successfully parse a valid settingsqr input string and 29 | return the resulting config_name and formatted settings_update_dict. 30 | """ 31 | settings_name = "Test SettingsQR" 32 | settingsqr_data = f"""settings::v1 name={ settings_name.replace(" ", "_") } persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss,ms scripts=nat,nes,tr xpub_details=E passphrase=E camera=180 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E""" 33 | 34 | # First explicitly set settings that differ from the settingsqr_data 35 | self.settings.set_value(SettingsConstants.SETTING__COMPACT_SEEDQR, SettingsConstants.OPTION__DISABLED) 36 | self.settings.set_value(SettingsConstants.SETTING__DIRE_WARNINGS, SettingsConstants.OPTION__DISABLED) 37 | self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [SettingsConstants.COORDINATOR__BLUE_WALLET, SettingsConstants.COORDINATOR__SPARROW]) 38 | 39 | # Now parse the settingsqr_data 40 | config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) 41 | assert config_name == settings_name 42 | self.settings.update(new_settings=settings_update_dict) 43 | 44 | # Now verify that the settings were updated correctly 45 | assert self.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__ENABLED 46 | assert self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__ENABLED 47 | 48 | coordinators = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS) 49 | assert SettingsConstants.COORDINATOR__BLUE_WALLET not in coordinators 50 | assert SettingsConstants.COORDINATOR__SPARROW in coordinators 51 | assert SettingsConstants.COORDINATOR__SPECTER_DESKTOP in coordinators 52 | 53 | 54 | def test_settingsqr_version(self): 55 | """ SettingsQR parser should accept SettingsQR v1 and reject any others """ 56 | settingsqr_data = "settings::v1 name=Foo" 57 | config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) 58 | 59 | # Accepts update with no Exceptions 60 | self.settings.update(new_settings=settings_update_dict) 61 | 62 | settingsqr_data = "settings::v2 name=Foo" 63 | with pytest.raises(InvalidSettingsQRData) as e: 64 | Settings.parse_settingsqr(settingsqr_data) 65 | assert "Unsupported SettingsQR version" in str(e.value) 66 | 67 | # Should also fail if version omitted 68 | settingsqr_data = "settings name=Foo" 69 | with pytest.raises(InvalidSettingsQRData) as e: 70 | Settings.parse_settingsqr(settingsqr_data) 71 | 72 | # And if "settings" is omitted entirely 73 | settingsqr_data = "name=Foo" 74 | with pytest.raises(InvalidSettingsQRData) as e: 75 | Settings.parse_settingsqr(settingsqr_data) 76 | 77 | 78 | def test_settingsqr_ignores_unrecognized_setting(self): 79 | """ SettingsQR parser should ignore unrecognized settings """ 80 | settingsqr_data = "settings::v1 name=Foo favorite_food=bacon xpub_export=D" 81 | config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) 82 | 83 | assert "favorite_food" not in settings_update_dict 84 | assert "xpub_export" in settings_update_dict 85 | 86 | # Accepts update with no Exceptions 87 | self.settings.update(new_settings=settings_update_dict) 88 | 89 | 90 | def test_settingsqr_fails_unrecognized_option(self): 91 | """ SettingsQR parser should fail if a settings has an unrecognized option """ 92 | settingsqr_data = "settings::v1 name=Foo xpub_export=Yep" 93 | with pytest.raises(InvalidSettingsQRData) as e: 94 | Settings.parse_settingsqr(settingsqr_data) 95 | assert "xpub_export" in str(e.value) 96 | 97 | 98 | def test_settingsqr_parses_line_break_separators(self): 99 | """ SettingsQR parser should read line breaks as acceptable separators """ 100 | settingsqr_data = "settings::v1\nname=Foo\nsigs=ss,ms\nscripts=nat,nes,tr\nxpub_export=E\n" 101 | config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) 102 | 103 | assert len(settings_update_dict.keys()) == 3 104 | 105 | # Accepts update with no Exceptions 106 | self.settings.update(new_settings=settings_update_dict) 107 | -------------------------------------------------------------------------------- /tests/test_settingsqr_decoder.py: -------------------------------------------------------------------------------- 1 | from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus 2 | 3 | 4 | 5 | class TestSettingsQRDecoder: 6 | def test_decode_settingsqr(self): 7 | """ 8 | Assume the QR reader decodes the SettingsQR content correctly and begin this test 9 | with parsing the result. 10 | """ 11 | settings_name = "Test SettingsQR" 12 | settings_qr_str = f"""settings::v1 name={ settings_name.replace(" ", "_") } persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss,ms scripts=nat,nes,tr xpub_details=E passphrase=E camera=180 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E""" 13 | 14 | # Now parse the settings_qr_str 15 | decoder = DecodeQR() 16 | status = decoder.add_data(settings_qr_str) 17 | assert decoder.is_settings 18 | assert status == DecodeQRStatus.COMPLETE 19 | 20 | data = decoder.get_settings_data() 21 | assert data == settings_qr_str 22 | 23 | 24 | def test_settingsqr_version(self): 25 | """ Should fail if the "settings" header is missing """ 26 | settings_qr_str = "name=Foo" 27 | decoder = DecodeQR() 28 | status = decoder.add_data(settings_qr_str) 29 | assert status == DecodeQRStatus.INVALID 30 | -------------------------------------------------------------------------------- /tools/javacard-build.xml.manual: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Builds, tests, and runs the project . 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /tools/javacard-build.xml.seedsigneros: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Builds, tests, and runs the project . 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /tools/seed_phrase_to_qr.py: -------------------------------------------------------------------------------- 1 | from seedsigner.models.encode_qr import CompactSeedQrEncoder, SeedQrEncoder 2 | from seedsigner.models.seed import Seed 3 | from seedsigner.models.settings_definition import SettingsConstants 4 | 5 | """ 6 | This is a utility for testing / dev purposes only. 7 | """ 8 | 9 | if __name__ == "__main__": 10 | import qrcode 11 | import sys 12 | 13 | print(""" 14 | ******************************************************************************* 15 | 16 | This is a utility for testing / dev purposes ONLY. 17 | 18 | A SeedQR for a real seed holding actual value should never be created 19 | this way. 20 | 21 | ******************************************************************************* 22 | """) 23 | 24 | COMPACT = 1 25 | STANDARD = 2 26 | format = int(input("1. Compact SeedQR\n2. Standard SeedQR\nEnter 1 or 2: ").strip()) 27 | if format not in [COMPACT, STANDARD]: 28 | print("Invalid option") 29 | sys.exit(1) 30 | 31 | seed_phrase = input("\nEnter 12- or 24-word test seed phrase: ").strip().split(" ") 32 | 33 | if format == COMPACT: 34 | encoder = CompactSeedQrEncoder(mnemonic=seed_phrase, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) 35 | else: 36 | encoder = SeedQrEncoder(mnemonic=seed_phrase, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) 37 | 38 | qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=5, border=3) 39 | qr.add_data(encoder.next_part()) 40 | qr.make(fit=True) 41 | qr.make_image(fill_color="black", back_color="white").resize((240,240)).convert('RGB').show() 42 | 43 | seed = Seed(seed_phrase) 44 | print(f"\nfingerprint: {seed.get_fingerprint()}\n") 45 | --------------------------------------------------------------------------------
55 | 56 | 57 | 58 | 59 |
69 | 70 | 71 | 72 |
81 | 82 | 83 | 84 |
93 | 94 | 95 | 96 |
107 | 108 | 109 | 110 | 111 |
\x19Z{\ng^', 125 | ] 126 | 127 | for entropy_bytes in entropy_bytes_tests: 128 | entropy_bytes.decode() # should not raise an exception 129 | mnemonic_length = 12 if len(entropy_bytes) == 16 else 24 130 | run_encode_decode_test(entropy_bytes, mnemonic_length=mnemonic_length, qr_type=QRType.SEED__COMPACTSEEDQR) 131 | -------------------------------------------------------------------------------- /tests/test_settings.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from base import BaseTest 3 | from seedsigner.models.settings import InvalidSettingsQRData, Settings 4 | from seedsigner.models.settings_definition import SettingsConstants 5 | 6 | 7 | 8 | class TestSettings(BaseTest): 9 | @classmethod 10 | def setup_class(cls): 11 | super().setup_class() 12 | cls.settings = Settings.get_instance() 13 | 14 | 15 | def test_reset_settings(self): 16 | """ BaseTest.reset_settings() should wipe out any previous Settings changes """ 17 | settings = Settings.get_instance() 18 | settings.set_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS, SettingsConstants.OPTION__ENABLED) 19 | assert settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__ENABLED 20 | 21 | BaseTest.reset_settings() 22 | settings = Settings.get_instance() 23 | assert settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED 24 | 25 | 26 | def test_parse_settingsqr_data(self): 27 | """ 28 | SettingsQR parser should successfully parse a valid settingsqr input string and 29 | return the resulting config_name and formatted settings_update_dict. 30 | """ 31 | settings_name = "Test SettingsQR" 32 | settingsqr_data = f"""settings::v1 name={ settings_name.replace(" ", "_") } persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss,ms scripts=nat,nes,tr xpub_details=E passphrase=E camera=180 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E""" 33 | 34 | # First explicitly set settings that differ from the settingsqr_data 35 | self.settings.set_value(SettingsConstants.SETTING__COMPACT_SEEDQR, SettingsConstants.OPTION__DISABLED) 36 | self.settings.set_value(SettingsConstants.SETTING__DIRE_WARNINGS, SettingsConstants.OPTION__DISABLED) 37 | self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [SettingsConstants.COORDINATOR__BLUE_WALLET, SettingsConstants.COORDINATOR__SPARROW]) 38 | 39 | # Now parse the settingsqr_data 40 | config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) 41 | assert config_name == settings_name 42 | self.settings.update(new_settings=settings_update_dict) 43 | 44 | # Now verify that the settings were updated correctly 45 | assert self.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__ENABLED 46 | assert self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__ENABLED 47 | 48 | coordinators = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS) 49 | assert SettingsConstants.COORDINATOR__BLUE_WALLET not in coordinators 50 | assert SettingsConstants.COORDINATOR__SPARROW in coordinators 51 | assert SettingsConstants.COORDINATOR__SPECTER_DESKTOP in coordinators 52 | 53 | 54 | def test_settingsqr_version(self): 55 | """ SettingsQR parser should accept SettingsQR v1 and reject any others """ 56 | settingsqr_data = "settings::v1 name=Foo" 57 | config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) 58 | 59 | # Accepts update with no Exceptions 60 | self.settings.update(new_settings=settings_update_dict) 61 | 62 | settingsqr_data = "settings::v2 name=Foo" 63 | with pytest.raises(InvalidSettingsQRData) as e: 64 | Settings.parse_settingsqr(settingsqr_data) 65 | assert "Unsupported SettingsQR version" in str(e.value) 66 | 67 | # Should also fail if version omitted 68 | settingsqr_data = "settings name=Foo" 69 | with pytest.raises(InvalidSettingsQRData) as e: 70 | Settings.parse_settingsqr(settingsqr_data) 71 | 72 | # And if "settings" is omitted entirely 73 | settingsqr_data = "name=Foo" 74 | with pytest.raises(InvalidSettingsQRData) as e: 75 | Settings.parse_settingsqr(settingsqr_data) 76 | 77 | 78 | def test_settingsqr_ignores_unrecognized_setting(self): 79 | """ SettingsQR parser should ignore unrecognized settings """ 80 | settingsqr_data = "settings::v1 name=Foo favorite_food=bacon xpub_export=D" 81 | config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) 82 | 83 | assert "favorite_food" not in settings_update_dict 84 | assert "xpub_export" in settings_update_dict 85 | 86 | # Accepts update with no Exceptions 87 | self.settings.update(new_settings=settings_update_dict) 88 | 89 | 90 | def test_settingsqr_fails_unrecognized_option(self): 91 | """ SettingsQR parser should fail if a settings has an unrecognized option """ 92 | settingsqr_data = "settings::v1 name=Foo xpub_export=Yep" 93 | with pytest.raises(InvalidSettingsQRData) as e: 94 | Settings.parse_settingsqr(settingsqr_data) 95 | assert "xpub_export" in str(e.value) 96 | 97 | 98 | def test_settingsqr_parses_line_break_separators(self): 99 | """ SettingsQR parser should read line breaks as acceptable separators """ 100 | settingsqr_data = "settings::v1\nname=Foo\nsigs=ss,ms\nscripts=nat,nes,tr\nxpub_export=E\n" 101 | config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) 102 | 103 | assert len(settings_update_dict.keys()) == 3 104 | 105 | # Accepts update with no Exceptions 106 | self.settings.update(new_settings=settings_update_dict) 107 | -------------------------------------------------------------------------------- /tests/test_settingsqr_decoder.py: -------------------------------------------------------------------------------- 1 | from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus 2 | 3 | 4 | 5 | class TestSettingsQRDecoder: 6 | def test_decode_settingsqr(self): 7 | """ 8 | Assume the QR reader decodes the SettingsQR content correctly and begin this test 9 | with parsing the result. 10 | """ 11 | settings_name = "Test SettingsQR" 12 | settings_qr_str = f"""settings::v1 name={ settings_name.replace(" ", "_") } persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss,ms scripts=nat,nes,tr xpub_details=E passphrase=E camera=180 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E""" 13 | 14 | # Now parse the settings_qr_str 15 | decoder = DecodeQR() 16 | status = decoder.add_data(settings_qr_str) 17 | assert decoder.is_settings 18 | assert status == DecodeQRStatus.COMPLETE 19 | 20 | data = decoder.get_settings_data() 21 | assert data == settings_qr_str 22 | 23 | 24 | def test_settingsqr_version(self): 25 | """ Should fail if the "settings" header is missing """ 26 | settings_qr_str = "name=Foo" 27 | decoder = DecodeQR() 28 | status = decoder.add_data(settings_qr_str) 29 | assert status == DecodeQRStatus.INVALID 30 | -------------------------------------------------------------------------------- /tools/javacard-build.xml.manual: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Builds, tests, and runs the project . 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /tools/javacard-build.xml.seedsigneros: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Builds, tests, and runs the project . 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /tools/seed_phrase_to_qr.py: -------------------------------------------------------------------------------- 1 | from seedsigner.models.encode_qr import CompactSeedQrEncoder, SeedQrEncoder 2 | from seedsigner.models.seed import Seed 3 | from seedsigner.models.settings_definition import SettingsConstants 4 | 5 | """ 6 | This is a utility for testing / dev purposes only. 7 | """ 8 | 9 | if __name__ == "__main__": 10 | import qrcode 11 | import sys 12 | 13 | print(""" 14 | ******************************************************************************* 15 | 16 | This is a utility for testing / dev purposes ONLY. 17 | 18 | A SeedQR for a real seed holding actual value should never be created 19 | this way. 20 | 21 | ******************************************************************************* 22 | """) 23 | 24 | COMPACT = 1 25 | STANDARD = 2 26 | format = int(input("1. Compact SeedQR\n2. Standard SeedQR\nEnter 1 or 2: ").strip()) 27 | if format not in [COMPACT, STANDARD]: 28 | print("Invalid option") 29 | sys.exit(1) 30 | 31 | seed_phrase = input("\nEnter 12- or 24-word test seed phrase: ").strip().split(" ") 32 | 33 | if format == COMPACT: 34 | encoder = CompactSeedQrEncoder(mnemonic=seed_phrase, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) 35 | else: 36 | encoder = SeedQrEncoder(mnemonic=seed_phrase, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) 37 | 38 | qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=5, border=3) 39 | qr.add_data(encoder.next_part()) 40 | qr.make(fit=True) 41 | qr.make_image(fill_color="black", back_color="white").resize((240,240)).convert('RGB').show() 42 | 43 | seed = Seed(seed_phrase) 44 | print(f"\nfingerprint: {seed.get_fingerprint()}\n") 45 | --------------------------------------------------------------------------------