diff --git a/CHANGELOG.md b/CHANGELOG.md index b0a29c704..175b2acfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,34 @@ +# Changelog 25.02.0 - February 2025 + +### Taproot and WSH Miniscript support + +- Provides an indented visualization of Miniscript for easier inspection. +- Includes policy and cosigner verification. +- Supports custom derivations. +- Detects unspendable internal keys in Taproot. +- Contains several UI and settings modifications. + +### More Intuitive Tamper Check +The Tamper Check Flash Hash now appears immediately after the Tamper Check code is created, clarifying its purpose and expected output. + +### More Camera Modes +A zoomed camera mode is available for all cameras, and an anti-glare mode has been added to the GC0328 camera. + +### Display Customization Options +Display orientation on Yahboom and WonderMV devices can now be flipped. + +### SD Card PSBT Signing Preserves All Fields +When signing via SD cards, all fields in a PSBT—including signatures from other keys—are preserved. This facilitates workflows across multiple devices and locations by allowing a single PSBT file to be sequentially signed by different devices. + +### Other Bug Fixes and Optimizations +- Stored mnemonics are now sorted alphabetically. +- Flash Map drawing errors have been corrected. +- Address scanning for Blue Wallet has been fixed following its export format change. +- The use of “h” to indicate hardened derivation path nodes has been standardized. +- A faster algorithm for double mnemonic calculation has been introduced. +- PSBT change detection has been made more restrictive. + + # Changelog 24.11.1 - November 2024 ### Security Fix diff --git a/docs/getting-started/features/QR-transcript-tools.en.md b/docs/getting-started/features/QR-transcript-tools.en.md index 0f528ca98..ab73b6341 100644 --- a/docs/getting-started/features/QR-transcript-tools.en.md +++ b/docs/getting-started/features/QR-transcript-tools.en.md @@ -1,40 +1,40 @@ When you export a mnemonic, encrypted mnemonic or a generic text QR code, alternative visualization modes will be available. Swipe left :material-gesture-swipe-left: or right :material-gesture-swipe-right: to change modes, or if your device doesn't have a touchscreen, press the `PAGE` buttons. Find transcribe templates [here](https://github.com/odudex/krux_binaries/tree/main/templates). ### Standard Mode - - + + This mode is optimized for scanning, the raw QR code will be displayed
### Lines Mode - - + + If you are good at transcribing things like handwritten text, with this mode one QR code line will be highlighted at a time. Press `Enter` to highlight the next line.
### Zoomed Regions Mode - - + + QR codes will be split into regions, of 5x5 or 7x7 "blocks". One QR code region will be shown at a time. Press `Enter` to display the next region.
### Highlighted Regions Mode - - + + QR codes will be split into regions, of 5x5 or 7x7 "blocks". One QR code region will be highlighted at a time. Press `Enter` to highlight the next region.
### Grided Mode - - + + Grids will be added to a standard QR code. In a dark room, if you place a sheet of paper over the device's screen, you'll notice QR code will be visible and it will be possible to copy it directly from above (tracing). Be careful not to damage your screen with pen and markers, use an insulating plastic tape or film to protect the device when using this method. diff --git a/docs/getting-started/features/printing.en.md b/docs/getting-started/features/printing.en.md index 5fb49e9de..1d0b35efa 100644 --- a/docs/getting-started/features/printing.en.md +++ b/docs/getting-started/features/printing.en.md @@ -4,8 +4,8 @@ warning-printer.en.txt Krux has the ability to print mnemonic backup (Words, Numbers, Tiny Seed template; but not Stackbit 1248) and any QR code (SeedQR, signed PSBT, Address, XPUB, Wallet output descriptor, ...) via a locally-connected TTL serial thermal printer. Consult the [parts list](../../parts.md/#optional-ttl-serial-thermal-printer) page for supported printers. - - + + - - + + Once a thermal printer and driver have been enabled in [Krux settings](../settings.md/#thermal), all screens that display a QR code will offer the option to `Print to QR`. Other formats of mnemonic backup will also ask if you want to `Print to QR?`. diff --git a/docs/getting-started/features/tools.en.md b/docs/getting-started/features/tools.en.md index 9370f1120..9f3eae92e 100644 --- a/docs/getting-started/features/tools.en.md +++ b/docs/getting-started/features/tools.en.md @@ -1,51 +1,51 @@ Here are some useful tools that are available as soon as Krux starts! These are offered as a complement to managing your device and wallets. - - + + ### Check SD Card - - + + You can check if a SD card can be detected and read by your device and explore its content. If there are too many files to fit on one screen, swipe up :material-gesture-swipe-up: or down :material-gesture-swipe-down: to navigate between the screens if your device has a touchscreen.
### Print Test QR - - + + Quickly print a test QR code to check and optimize your printer setup.
### Create QR Code - - + + Enter text to create, print or transcribe a QR code that can later be used as an encryption key or passphrase. Swipe left :material-gesture-swipe-left: or right :material-gesture-swipe-right: to change modes if your device has a touchscreen.
### Descriptor Addresses (Wallet Sans Key) - - + + Verify if an address or list of addresses belong to a wallet without needing to load private keys. Simply load a trusted wallet descriptor from a QR code or SD card.
### Flash Tools - - + + Tools to inspect the content of device's flash memory and clear user's area.
#### Flash Map - - + + Flash map indicates which memory blocks (4086 Bytes each) are empty. Memory is separated in two regions: Firmware and User's Data. White or colored blocks contain data, while grey blocks are empty. @@ -60,16 +60,16 @@ This is an interesting tool to visualize the effects of filling the memory with More information on [Tamper Detection page](tamper-detection.md). #### Erase User's Data - - + + This option permanently removes all stored encrypted mnemonics, settings and `TC Code` from the device's internal flash memory. It ensures that the data is irrecoverable, making it an adequate measure to take if any important mnemonics were stored with a [weak encryption key](https://www.hivesystems.com/blog/are-your-passwords-in-the-green).
### Remove Mnemonic - - + + This option allows you to remove any stored encrypted mnemonic from the device's internal memory or an SD card. For more information, see [Krux Encrypted Mnemonics](./encrypted-mnemonics.md). diff --git a/docs/getting-started/settings.en.md b/docs/getting-started/settings.en.md index 6297c9217..6462e44b8 100644 --- a/docs/getting-started/settings.en.md +++ b/docs/getting-started/settings.en.md @@ -1,37 +1,43 @@ In the Krux home menu, there is a `Settings` entry. Some submenu entries have too many options to fit on one screen, swipe up :material-gesture-swipe-up: or down :material-gesture-swipe-down: to navigate between the screens if your device has a touchscreen. Below is a breakdown of the options you can change: - - + + ### Default Wallet Set the default attributes for wallet loading. -#### Multisig +#### Network + + -Set this to true if you are more likely to use Krux for multisig setups. This way, you won't need to "Customize" your wallet attributes every time you load a key. +This option allows you to switch between `mainnet` (the default) and `testnet`. `Testnet` can be used to try out different wallet coordinators or for development.
-#### Network - - +#### Policy Type -This option allows you to switch between `mainnet` (the default) and `testnet`. `Testnet` can be used to try out different wallet coordinators or for development. +Choose between `Single-sig`, `Multisig`, or `Miniscript` to avoid having to customize the policy type every time you load a key. + +#### Script Type + +As with Policy Type, pre-select the most commonly used script type so that you don't have to change it every time you load a wallet. Your options are `Native Segwit`, `Neste Segwit`, `Taproot (Experimental)`, and `Legacy`. If you choose a script type that isn’t implemented for the selected policy type—such as a `Legacy` script for `Miniscript`—the system will default to `Native Segwit`.
+These settings do not restrict changes to these attributes during or after loading a wallet, they only set the default values. + ### Encryption - - + + Modify the encryption method and parameters to fit your needs. This will be used when storing encrypted mnemonics or creating encrypted QR codes. For more info see [Krux Encrypted Mnemonics](./features/encrypted-mnemonics.md).
#### PBKDF2 Iter. (Iterations) - - + + When you enter the encryption key, it is not directly used to encrypt your data. In order to protect against brute force attacks, the key is derived multiple times using hashing functions. PBKDF2 (Password-Based Key Derivation Function) iterations stands for the amount of derivations that will be performed over your key prior to encrypt/decrypt your mnemonic. @@ -42,8 +48,8 @@ Values must be multiple of 10,000. This was done to save data space on QR codes.
#### Encryption Mode - - + + Choose between well known and widely used AES (Advanced Encryption Standard) modes: @@ -58,8 +64,8 @@ Encryption will take longer because a snapshot will be needed to generate the IV
### Hardware - - + + Customize the parameters available for your device and change printer settings. @@ -70,16 +76,18 @@ If your device has a rotary encoder, you can change the debounce threshold in mi The caveat is low values can cause issues, such as double step and unexpected movements, especially with lower quality encoders. If this is the case increase the value to make navigation more stable. -#### Display (Maix Amigo only) - +#### Display + + +Available display settings vary based on your device’s hardware. Some Maix Amigo screens are different, here you can customize the `BGR Colors`, `Flipped X Coordinates`, `Inverted Colors` and `LCD Type`. For more info see [Troubleshooting](../troubleshooting.md/#troubleshooting-lcd-settings-on-maix-amigo)
### Printer - - + + You can set up a TTL serial thermal printer or tell Krux to store a GRBL CNC instructions file on a SD card to machine QR codes. @@ -97,23 +105,23 @@ Here you choose between Thermal, CNC or none (default). Leave this setting to "n
#### Touchscreen (Maix Amigo, Yahboom and WonderMV only) - + If your device has touchscreen you can change the touch detection threshold. If it is being too sensitive or detecting false or ghost touches, you should increase the threshold value, making it less sensitive. The other way is also valid, reduce the threshold to make the screen more sensitive to touches.
### Language - Locale - - + + Here you can change Krux to your desired language.
### Persist - - + + Choose between flash (device's internal memory) or SD card for the place where your settings will be stored. @@ -145,16 +153,16 @@ The filling process requires good entropy images. If, for any reason, such as st The *TC Code* will be deleted if the device is wiped or user data is erased, which will consequently disable *TC Flash Hash*. ### Appearance - - + + Configure screensaver time and change Krux to your desired theme.
#### Screensaver time - - + + Set how long to wait idle before the screensaver appears. Enter 0 to disable the screensaver. @@ -163,21 +171,21 @@ Set how long to wait idle before the screensaver appears. Enter 0 to disable the #### Theme Choose your color theme according to your preference. Some themes may be more suitable for some devices, coordinator cameras and environments. As an example, it may be easier to scan QR codes from Krux devices using light theme in brighter environments. - - - - - + + + + + - - - - - + + + + + ### Factory Settings - - + + Restore device to factory settings and reboot. diff --git a/docs/getting-started/usage/generating-a-mnemonic.en.md b/docs/getting-started/usage/generating-a-mnemonic.en.md index 8bacfb7d6..1ae099872 100644 --- a/docs/getting-started/usage/generating-a-mnemonic.en.md +++ b/docs/getting-started/usage/generating-a-mnemonic.en.md @@ -2,31 +2,29 @@ Krux supports creating 12 and 24-word BIP-39 mnemonic seed phrases. Since genera At the start screen, after selecting New Mnemonic, you will be taken to a second menu where you can choose to create a mnemonic via the camera, words, rolls of a D6 (standard six-sided die), or a D20 (20-sided die). - - + + ## Camera (Experimental!) Choose between 12, 24 words or double mnemonic, then take a random picture and Krux will generate a mnemonic from the hash of the image bytes. - - - - + + + + #### Image Entropy Quality Estimation - - + + -During image capture, entropy quality estimation is displayed to assist you in obtaining a high-quality image source for your key. After a snapshot is taken, Shannon's entropy and pixel deviation indices are presented. Minimum thresholds are established to prevent the use of poor-quality images with low entropy for key generation. It's important to note that these values serve as indicators or estimations of entropy quality, but they are not absolute entropy values in a cryptographic context. +During image capture, entropy quality estimation is displayed to assist you in obtaining a quality image source for your key. After a snapshot is taken, Shannon's entropy and pixel deviation indices are presented. Minimum thresholds are established to prevent the use of poor-quality images with low entropy for key generation. It's important to note that these values serve as indicators or estimations of entropy quality, but they are not absolute entropy values in a cryptographic context.
#### Double mnemonic It is the combination of two 12-word mnemonics that also forms a valid 24-word BIP-39 mnemonic. This is achieved by using the first 16 bytes (128 bits) of the image's entropy to generate the first 12-word, then using the next 16 bytes to generate the second 12-word and checking if these two 12-word together forms a valid 24-word, if not, we iterate over the second 12-word incrementing its entropy bytes until the two 12-word forms a valid 24-word. -Some might say that the name double mnemonic is incorrect because we end up with two 12-word plus a 24-word mnemonic (12 + 12 + 24), so it's a triple mnemonic! But we only use entropy for the two 12-word ones, hence the name double mnemonic. Also, this name has already been used in this [double mnemonic generator](https://stepansnigirev.github.io/seed-tools/double_mnemonic.html) since July 2023. - -Some may wonder what is the use of this, it may be useful to some [plausible deniability](https://en.bitcoin.it/wiki/Privacy) or even a way to improve your [OPSEC](https://en.wikipedia.org/wiki/Operations_security). +Double Mnemonic was first defined by Stepan Snigirev in his [Double Mnemonic Generator](https://stepansnigirev.github.io/seed-tools/double_mnemonic.html). It can be used for plausible deniability, or, as Stepan stated, to have fun and confuse everyone. ## Words Print the BIP39 word list in 3D or on paper, then cut out the words and place them in a bucket. Manually draw 11 or 23 words from the bucket. @@ -36,22 +34,22 @@ For the final word, Krux will assist you in picking a valid 12th or 24th word by ### Via D6 Choose between 12 or 24 words. The entropy in a single roll of a D6 is 2.585 bits ( log2(6) ); therefore a minimum of a 50 rolls will be required for 128 bits of entropy, enough to generate a 12-word mnemonic. For 24-word, or an entropy of 256 bits, a minimum of 99 rolls will be required. - - - - + + + + ### Via D20 Since a D20 has more possible outcomes, the entropy is increased per roll to 4.322 bits ( log2(20) ). This means that only 30 rolls are necessary to create a 12-word mnemonic and 60 rolls for a 24-word mnemonic. - - - - + + + + ### Dice Rolls Entropy Quality Estimation - - + + When you input your dice rolls, you'll see two progress bars filling up. The top progress bar shows how many rolls you've entered compared to the minimum number needed. The bottom progress bar shows the real-time calculated Shannon's entropy compared to the required minimum (128 bits for 12 words and 256 bits for 24 words). When the Shannon's entropy estimation reaches the recommended level, the progress bar will be full, and its frame will change color. If you've met the minimum number of rolls but the entropy estimation is still below the recommended level, a warning will appear, suggesting you add more rolls to increase entropy. Note: Similar to image entropy quality estimation, dice rolls Shannon's entropy serves as an indicator and should not be considered an absolute measure of cryptographic entropy. @@ -63,17 +61,17 @@ Learn more about [Krux Entropy Quality Estimation](../features/entropy.md). ### Stats for Nerds A low Shannon's entropy value could suggest that your dice are biased or that there's a problem with how you're gathering entropy. To investigate further, examine the "Stats for Nerds" section to check the distribution of your rolls and look for any abnormalities. - - - - + + + + ## Editing a New Mnemonic - Optional After entering dice rolls, random words, or captured entropy through the camera, you can manually add custom entropy by editing some of the words. Edited words will be highlighted, and the final word will automatically update to ensure a valid checksum. However, proceed with caution, modifying words can negatively impact the natural entropy previously captured. - - + + ## How Entropy Capture Works For dice rolls, Krux keeps track of every roll you enter and displays the cumulative string of outcomes after each roll. @@ -82,10 +80,10 @@ When you have entered your final roll, Krux will hash this string using [SHA256] In case a camera snapshot is used as a source, the image bytes, which contain pixels data in RGB565 format, will be hashed in the same way as the dice rolls. - - - - + + + + Krux then takes this hash, runs [`unhexlify`](https://docs.python.org/3/library/binascii.html#binascii.unhexlify) on it to encode it as bytes, and deterministically converts it into a mnemonic according to the [BIP-39 Reference Implementation](https://github.com/trezor/python-mnemonic/blob/6b7ebdb3624bbcae1a7b3c5485427a5587795120/src/mnemonic/mnemonic.py#L189-L207). diff --git a/docs/getting-started/usage/loading-a-mnemonic.en.md b/docs/getting-started/usage/loading-a-mnemonic.en.md index cb3c0f026..55eadaaac 100644 --- a/docs/getting-started/usage/loading-a-mnemonic.en.md +++ b/docs/getting-started/usage/loading-a-mnemonic.en.md @@ -1,11 +1,11 @@ Once you have either a 12 or 24-word BIP-39 mnemonic, choose `Load Mnemonic` on Krux's start menu (aka login menu), and you will be presented with several input methods: - - + + ## Input Methods - - + + ### Via Camera You can choose to use the camera to scan a `QR code`, `Tiny Seed`, `OneKey KeyTag` or a `Binary Grid`. @@ -34,24 +34,24 @@ You can also use [an offline QR code generator for this](https://iancoleman.io/b To properly scan, place the backup plate over a black background and paint the punched bits black to increase contrast. You can also scan the thermally printed version, or a filled template. You can find some [examples of mnemonics encoded here](../features/tinyseed.md). Alternatively, you can find [templates to scan or print here](https://github.com/odudex/krux_binaries/tree/main/templates). ### Via Manual Input - - + + Manually type `Words`, `Word Numbers`, `Tiny Seed` (toggle the bits or punches) or [`Stackbit 1248`](https://stackbit.me/produto/stackbit-1248/).
#### Words - - + + Enter each word of your BIP-39 mnemonic one at a time. Krux will disable impossible-to-reach letters as you type and will attempt to autocomplete your words to speed up the process.
#### Word Numbers - - + + ##### Decimal Enter each word of your BIP-39 mnemonic as a number (1-2048) one at a time. You can use [this list](https://github.com/bitcoin/bips/blob/master/bip-0039/english.txt) for reference. @@ -62,24 +62,24 @@ You can also enter your BIP-39 mnemonic word's numbers (1-2048) in hexadecimal f
#### Tiny Seed (Bits) - - + + Enter the BIP-39 mnemonic word's numbers (1-2048) in binary format, toggling necessary bits to recreate each of the word's respective number. The last word will have checksum bits dynamically toggled while you fill the bits.
#### Stackbit 1248 - - + + Enter the BIP-39 mnemonic word's numbers (1-2048) using the Stackbit 1248 metal plate backup method, where each of the four digits of the word's number is a sum of the numbers marked (punched) 1, 2, 4, or 8. For example, to enter the word "oyster", number 1268, you must punch (1)(2)(2,4)(8).
### From Storage - - + + You can retrieve mnemonics previously stored on device's internal flash or external (SD card). All stored mnemonics are encrypted, to load them you'll have to enter the same key you used to encrypt them. @@ -87,32 +87,32 @@ You can retrieve mnemonics previously stored on device's internal flash or exter ## Wallet Loading ### Confirm Mnemonic Words - - + + Once you have entered your mnemonic, you will be presented with the full list of words to confirm.
- - + + If you see an asterisk (`*`) in the header, it means this is a [double mnemonic](generating-a-mnemonic.md/#double-mnemonic).
### Edit Mnemonic - Optional - - + + If you make a mistake while loading a mnemonic, you can easily edit it. Simply touch or navigate to the word you want to change and replace it. Edited words will be highlighted in a different color. If the final word contains an invalid checksum, it will appear in red. If your checksum word is red, please review your mnemonic carefully, as there may be an error.
### Confirm Wallet Attributes - - + + You will be presented with a screen containing wallet attributes, if they are as expected just press `Load Wallet` and you'll be ready to use your loaded key. @@ -126,23 +126,24 @@ The BIP-32 master wallet's fingerprint, if you have it noted down, will help you * ` Mainnet `: Check if you are loading a `Testnet` or `Mainnet` wallet. -#### Single / Multisig -* ` Single-sig `: -Check if you are loading a `Single-sig` or `Multisig` wallet. +#### Policy Type +Check the wallet's policy type: `Single-sig`, `Multisig`, `Miniscript`, or `TR Miniscript` (Taproot). #### Derivation Path -* ` m/84'/0'/0' `: +* ` m/84h/0h/0h `: The derivation path is a sequence of numbers, or "nodes", that define the script type, network, and account index of your wallet. - * **Script Type** `84'`: The first number defines the script type. The default is `84'`, corresponding to a Native Segwit wallet. Other values include: - * `44'` for Legacy - * `49'` for Nested Segwit - * `86'` for Taproot - * `48'` for Multisig - * **Network** `0'`: The second number defines the network: - * `0'` for Mainnet - * `1'` for Testnet - * **Account Index** `0'`: The third number is the account index, with `0'` being the default. - * **Additional**: For multisig wallets, a fourth node with the value `2'` is added to the derivation path. + * **Script Type** `84h`: The first number defines the script type. The default is `84h`, corresponding to a Native Segwit wallet. Other values include: + * `44h` for Legacy + * `49h` for Nested Segwit + * `86h` for Taproot + * `48h` for Multisig + * **Network** `0h`: The second number defines the network: + * `0h` for Mainnet + * `1h` for Testnet + * **Account Index** `0h`: The third number is the account index, with `0h` being the default. + * **Additional**: For multisig wallets, a fourth node with the value `2h` is added to the derivation path. + + Default Miniscript derivation path is the same as for multisig: ` m/48'/0h/0h/2h `, but they can be fully customized #### Passphrase * ` No Passphrase `: @@ -150,21 +151,21 @@ Informs if the wallet has a loaded passphrase. ### Customize You can change any of the attributes before and after loading a wallet. -It is also possible to change default settings for `Network` and `Single/Multisig` on [settings](../settings.md). +It is also possible to change default settings for `Network`, `Policy Type` and `Script Type` on [Settings -> Default Wallet](../settings.md). #### Passphrase - - + + You can type or scan a BIP-39 passphrase. When typing, swipe left :material-gesture-swipe-left: or right :material-gesture-swipe-right: to change keypads if your device has a touchscreen. You can also hold the button `PAGE` or `PAGE_PREV` when navigating among letters while typing text to fast forward or backward. For scanning, you can also create a QR code from your offline passphrase using the [create QR code tool](../features/tools.md/#create-qr-code).
#### Customize - - + + -Press `Customize` to open a menu where you can change the `Network`, `Single/Multisig`, `Script Type` and `Account`. +Press `Customize` to open a menu where you can change the `Network`, `Policy Type`, `Script Type`, and `Account`. If `Policy Type` is Miniscript, instead of setting just an account, you can customize the entire derivation path.
diff --git a/docs/getting-started/usage/navigating-the-main-menu.en.md b/docs/getting-started/usage/navigating-the-main-menu.en.md index 8dcae8695..c58e43c2e 100644 --- a/docs/getting-started/usage/navigating-the-main-menu.en.md +++ b/docs/getting-started/usage/navigating-the-main-menu.en.md @@ -1,11 +1,30 @@ After entering your mnemonic, and loading a wallet, you will find yourself on Krux's main menu. Below is a breakdown of the entries available: - - +- [Backup Mnemonic](#backup-mnemonic) +- [Extended Public Key](#extended-public-key) +- Wallet + - [Wallet Descriptor](#wallet-descriptor) + - [Passphrase](#passphrase) + - [Customize](#customize) + - [BIP85](#bip85) +- Address + - [Scan Address](#scan-address) + - [Receive Addresses](#receive-addresses) + - [Change Addresses](#change-addresses) +- Sign + - [PSBT](#psbt) + - Message + - [Standard Messages and Files](#standard-messages-and-files) + - [Messages at Address](#messages-at-address) + +### Main Menu + + + ### Backup Mnemonic - - + + This will open a new submenu with different types of backups. `QR Code` based, `Encrypted` and `Other Formats` @@ -16,8 +35,8 @@ If you set a [printer](../settings.md/#printer), it will also give the option to #### QR Code - **Plaintext QR** - - + + Generate a QR containing the mnemonic words as regular text, where words are separated by spaces. Any QR code can be printed if a thermal printer driver is set. @@ -25,8 +44,8 @@ Generate a QR containing the mnemonic words as regular text, where words are sep - **Compact SeedQR** - - + + A QR code is created from a binary representation of mnemonic words. Format created by SeedSigner, more info [here](https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#compactseedqr-specification). @@ -34,8 +53,8 @@ A QR code is created from a binary representation of mnemonic words. Format crea - **SeedQR** - - + + Words are converted to their BIP-39 numeric indexes, those numbers are then concatenated as a string and finally converted to a QR code. Format created by SeedSigner, more info [here](https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md). @@ -45,9 +64,13 @@ Words are converted to their BIP-39 numeric indexes, those numbers are then conc This option converts the encrypted mnemonic into a QR code. Enter an encryption key and, optionally, a custom ID. When you scan this QR code through "Load Mnemonic" -> "Via Camera" -> "QR Code," you will be prompted to enter the decryption key to load the mnemonic stored in it. Like any QR code, it can be printed if a thermal printer driver is set up. +- **Transcribing QR Codes** + +Please refer to [Transcribing QR Codes](../../features/QR-transcript-tools) for details on transcription modes and helper tools. + #### Encrypted - - + + This feature allows you to back up your mnemonic by encrypting it and storing it on the device's flash memory, an SD card, or in QR code format. You can customize the encryption method and parameters in the settings. @@ -74,8 +97,8 @@ It's another path for the same functionality present on QR Code backups, describ - **Words** - - + + Display the BIP-39 mnemonic words as text so you can write them down. @@ -83,8 +106,8 @@ Display the BIP-39 mnemonic words as text so you can write them down. - **Numbers** - - + + Display the BIP-39 mnemonic word numbers (1-2048) in decimal, hex, or octal format. @@ -92,8 +115,8 @@ Display the BIP-39 mnemonic word numbers (1-2048) in decimal, hex, or octal form - **Stackbit 1248** - - + + This metal backup format represents the BIP-39 mnemonic word's numbers (1-2048). Each of the four digits is converted to a sum of 1, 2, 4 or 8. This option does not print even if a printer driver is set. @@ -101,8 +124,8 @@ This metal backup format represents the BIP-39 mnemonic word's numbers (1-2048). - **Tiny Seed** - - + + This metal backup format represents the BIP-39 mnemonic word's numbers (1-2048) in binary format on a metal plate, where the 1's are marked (punched) and the 0's are left intact. You can also print your mnemonic in this format if a thermal printer driver is set. @@ -112,38 +135,49 @@ This metal backup format represents the BIP-39 mnemonic word's numbers (1-2048) A menu will be presented with options to display your master extended public key (xpub) as text and as a QR code. Depending on the script type or whether a single-sig or multisig wallet was loaded, the options shown will be xpub, ypub, zpub, or Zpub. When displayed as text, the extended public key can be stored on an SD card if available. If you choose to export a QR code, you can not only scan it but also save it as an image on an SD card or print it if a thermal printer is attached. - - - - - - + + + + + + All QR codes will contain [key origin information in key expressions](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#Key_Expressions). If your coordinator cannot parse this information, it will not be capable of importing the wallet's fingerprint. As a result, Krux will not perform important verifications when signing transactions created by it unless you manually add the fingerprint so that it can be used to create Krux-compatible PSBTs. Always prefer to import extended public keys directly from Krux when setting up a coordinator instead of copying it (or parts of it) from other sources. +Some coordinators are phasing out support for variants like ypub and zpub in favor of xpubs that include key origin data, a clearer, more standardized approach. We therefore recommend using xpub exclusively. + ### Wallet -Here you can load view and save wallet descriptors, add or change passphrases, customize wallet's attributes, derive BIP85 mnemonics and passwords. +Here you can load, view and save wallet descriptors, add or change passphrases, customize wallet's attributes, derive BIP85 mnemonics and passwords. #### Wallet Descriptor -A Bitcoin Wallet Output Script Descriptor defines a set of addresses in a wallet. It includes the following information: -- Script Type: Specifies the type of script (e.g., P2PKH, P2SH, P2WPKH). -- Origin Info: Defines the master fingerprint and derivation path used to derive keys. -- Extended Public Keys: usually represented as an xpub, but could be ypub, zpub, etc. -Output descriptors standardize how wallets generate addresses, ensuring compatibility and security. They help wallets and other software understand how to derive and verify the addresses used in transactions. +A Bitcoin Wallet Output Script Descriptor defines addresses from a wallet. It encodes essential details such as: +- Script: Specifies the type of script (e.g., P2PKH, P2SH, P2WPKH, P2TR). For miniscript, it outlines advanced spending policies and conditions. +- Origin Info: For each key, it includes the corresponding master fingerprint and derivation path that was used to derive it. +- Extended Public Keys: Contains one or more extended public keys (e.g., xpub, ypub, zpub), each associated with its own origin information. + +Output descriptors standardize wallet address generation, ensuring accurate wallet restoration from backups and compatibility across different apps. -For multisig wallets, it is essential to load a descriptor to check addresses and perform full PSBT verification. For single-sig wallets, loading a descriptor is optional and serves as a redundancy check of the coordinator's wallet attributes. +For multisig and miniscript wallets, loading a descriptor is essential to verify addresses and perform full PSBT verification. For single-sig wallets, loading a descriptor remains optional and serves as a redundancy check of the coordinator's wallet attributes. When you select the "Wallet Descriptor" option for the first time, you will be prompted to load a wallet descriptor via QR code or SD card. After loading, a preview of the wallet attributes will be displayed for confirmation. - - - - + + + +You can verify each key’s fingerprint, derivation path, and abbreviated XPUB with the currently loaded key distinctly highlighted with a different color. + + + + + + + +**Miniscript Descriptors** present an indented view of the miniscript after the keys. When Taproot is used, Krux checks if the internal key is "provably unspendable", meaning funds can only be moved via Tap tree scripts, in which case the internal key is displayed in a distinct, disabled color. -If you access the "Wallet Descriptor" option again after loading your wallet, you will see the wallet's name, fingerprints, and the abbreviated XPUBs of all cosigners, along with a QR code containing the exact data that was initially loaded. If an SD card is inserted, you can save the descriptor to it for later use without the assistance of a coordinator. Additionally, if you have a thermal printer attached, you can print this QR code. +Re-access the "Wallet Descriptor" option after loading your wallet to view its name and a QR code containing the originally loaded data. If an SD card is inserted, you can save the descriptor for future use without a coordinator's assistance. Additionally, if a thermal printer is attached, you can print the QR code. Krux also allows you to verify a descriptor's receive and change addresses without the need to load private keys. Simply turn on your Krux, access "Tools" -> "Descriptor Addresses," and load a trusted descriptor from a QR code or SD card. @@ -152,8 +186,8 @@ Please note that if you customize the wallet parameters or restart the device, t
#### Passphrase - - + + If you forgot to load a passphrase while loading your wallet, or if you use multiple passphrases with the same mnemonic, you can add, replace, or remove a passphrase here. Simply choose between typing or scanning it. @@ -164,8 +198,8 @@ Don't forget to verify the resulting fingerprint in the status bar to ensure you
#### Customize - - + + Here you are presented to the exact same customization options you have while loading a key and wallet. You can change the Network, Single/Multisig, Script Type and Account. [More about wallet attributes](./loading-a-mnemonic.md/#confirm-wallet-attributes) @@ -176,10 +210,10 @@ Bitcoin BIP85, also known as Deterministic Entropy From BIP32 Keychains, allows **BIP39 Mnemonic** - - - - + + + + Choose between 12 or 24 words, then type the desired index to export a child mnemonic. After being presented with the new mnemonic, you can choose to load and use it right away. @@ -192,16 +226,16 @@ To create a Base64 password, which can be used in a variety of logins, from emai The resulting password will be displayed on the screen and can also be exported to an SD Card or as a QR code. ### Address - - + + Scan, verify, export or print your wallet addresses.
#### Scan Address - - + + This option turns on the camera and allows you to scan in a QR code of a receive address. Upon scanning, it will render its own QR code of the address back to the display along with the (text) address below it. You could use this feature to scan the address of someone you want to send coins to and display the QR back to your wallet coordinator rather than copy-pasting an address. If you have a thermal printer attached, you can also print this QR code. @@ -212,32 +246,32 @@ This option exists as an extra security check to verify that the address your wa
#### Receive Addresses - - + + List your wallet receiving addresses, you can browse to select an arbitrary address to show your QR code and print if you want.
#### Change Addresses - - + + List your wallet change addresses, you can browse to select an arbitrary address to show your QR code and print if you want.
### Sign - - + + Under *Sign*, you can choose to sign a PSBT or a message. You can load both PSBTs and messages scanning QR codes or loading from files on a SD card.
#### PSBT - - + + To sign a Bitcoin PSBT, you have the following options: @@ -259,8 +293,8 @@ If a thermal printer is attached to your device, you can also print the PSBT QR Similar to PSBTs, Krux can load, sign, and export signatures for messages. This feature allows you to attest not only to the ownership of the messages themselves but also to the ownership of Bitcoin addresses and the authorship of documents and files. ##### Standard Messages and Files - - + + You can scan or load a file from an SD card, the content can be plaintext or the SHA-256 hash of a message. Upon loading, you will be shown a preview of the message's SHA-256 hash for confirmation before signing. @@ -273,8 +307,8 @@ This feature is used to sign Krux releases, airgapped, using a Krux device.
##### Messages at Address - - + + Coordinators like Sparrow and Specter offer the possibility to sign messages at a Bitcoin receive address, allowing you to attest ownership of that address. Krux will detect if the message is of this type and present a similar workflow for signing. The main difference is that the address will be displayed along with the raw message, and since the message is signed with a derived address instead of the master public key, Krux won't offer the option to export the raw public key after the signature. diff --git a/docs/getting-started/usage/setting-a-coordinator-and-signing.en.md b/docs/getting-started/usage/setting-a-coordinator-and-signing.en.md index 0d79fe122..a4fea69bb 100644 --- a/docs/getting-started/usage/setting-a-coordinator-and-signing.en.md +++ b/docs/getting-started/usage/setting-a-coordinator-and-signing.en.md @@ -3,13 +3,12 @@ After creating a mnemonic, making a safe backup, and testing to recover your mne Krux can work with multiple coordinator wallets. Popular options include: - [Sparrow Wallet](https://www.sparrowwallet.com/) (desktop) - +- [Liana](https://wizardsardine.com/liana/) (desktop)* - [Specter Desktop](https://specter.solutions/) (desktop) - - [Nunchuk](https://nunchuk.io/) (mobile) - - [BlueWallet](https://bluewallet.io/) (mobile) +**Note: For Liana, the exchange of extended public keys, wallet output descriptors, and PSBTs is performed via copy and paste. On Krux, use SD cards and a standard text editor, or use QR codes via an intermediary application such as [SeedQReader](https://github.com/pythcoiner/SeedQReader).* ## Step 1: Install the Coordinator Wallet @@ -19,60 +18,65 @@ Download and install the appropriate version of your chosen coordinator wallet f Depending on the coordinator, the steps to add Krux as a signer may vary slightly: -**Specter and Nunchuk Single-sig:** Add Krux as signer device, then create a wallet that uses it. +**Specter and Nunchuk Single-sig:** Add Krux key, then create a wallet that uses it. -**Specter and Nunchuk Multisig:** Add Krux as signer device, add other devices, then create a wallet that uses them. +**Specter and Nunchuk Multisig:** Add Krux key, add other keys, then create a wallet that uses them. -**Sparrow and BlueWallet**: Create a wallet (or vault in Blue Wallet) first, then add signer device(s). +**Sparrow, Liana and BlueWallet**: Create a wallet (or vault in Blue Wallet) and add keys during setup. 1. Load a mnemonic and wallet in Krux. - - - - + + + + 2. On your coordinator, when presented with possible signer devices to add, choose Krux if available, otherwise choose "other" or even another QR code compatible signer. As Krux is compatible with many QR code formats, most of available alternatives should work. 3. When prompted by your coordinator to import signer's public key, access the "Extended Public Key" on Krux. - - + + 4. Export an XPUB (or YPUB, ZPUB, etc., based on the script type) as a QR code. - - - - + + + + 5. Scan this QR code with your coordinator. 6. Ensure the coordinator’s wallet attributes (policy type, script type, fingerprint, and derivation) match those in Krux. +Alternatively, you can export the extended public keys as files to an SD card. Instead of displaying them as QR codes, select the "XPUB - Text" option, then choose "Save to SD card". + + + + + + ## Step 3: Load and Backup Wallet Descriptor (Multisig Only) 1. In your coordinator, export the wallet descriptor containing information about the wallet and all cosigners: - Sparrow: "Descriptor" + - Liana: "Wallet Descriptor" - Specter: "Export Wallet" - Nunchuk: "Export Wallet Configuration" - BlueWallet: "Export Coordination Setup" 2. Export the descriptor as a QR code or file. 3. On Krux, go to "Wallet" -> "Wallet Descriptor" to scan the descriptor QR code or load it via SD card. - - - - - - - + + + + 4. If you access "Wallet" -> "Wallet Descriptor" again, you will be able to: - Check the wallet cosigners. - Save the descriptor on an SD card (useful if you initially loaded it from QR codes). - It is crucial to have a backup of this descriptor to recover your wallet in case one of the cosigners is lost. + Having a backup of descriptors is essential for recovering your wallet. ## Step 4: Verify Addresses @@ -82,8 +86,8 @@ For single-sig or multisig (after loading a descriptor): - List "Receive Addresses" and "Change Addresses" or use "Scan Address" to verify if addresses from your coordinator are matched by Krux. - - + + ## Step 5: Funding your Wallet @@ -115,5 +119,5 @@ Save the transaction as a file on an SD card. On Krux, go to "Sign" -> "PSBT" -> Some coordinators, like Sparrow, allow you to sign messages linked to your wallet's addresses. Signing and verifying a message signature attests to the ownership of an address and serves as an additional test for your setup. - - \ No newline at end of file + + \ No newline at end of file diff --git a/docs/img/maixpy_amigo/address-menu-150.en.png b/docs/img/maixpy_amigo/address-menu-150.en.png deleted file mode 100644 index 98507fee6..000000000 Binary files a/docs/img/maixpy_amigo/address-menu-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/address-menu-300.en.png b/docs/img/maixpy_amigo/address-menu-300.en.png new file mode 100644 index 000000000..246094889 Binary files /dev/null and b/docs/img/maixpy_amigo/address-menu-300.en.png differ diff --git a/docs/img/maixpy_amigo/backup-compact-qr-150.en.png b/docs/img/maixpy_amigo/backup-compact-qr-150.en.png deleted file mode 100644 index 372068c05..000000000 Binary files a/docs/img/maixpy_amigo/backup-compact-qr-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/backup-compact-qr-300.en.png b/docs/img/maixpy_amigo/backup-compact-qr-300.en.png new file mode 100644 index 000000000..1a840bb82 Binary files /dev/null and b/docs/img/maixpy_amigo/backup-compact-qr-300.en.png differ diff --git a/docs/img/maixpy_amigo/backup-mnemonic-numbers-150.en.png b/docs/img/maixpy_amigo/backup-mnemonic-numbers-150.en.png deleted file mode 100644 index 70cd07f5c..000000000 Binary files a/docs/img/maixpy_amigo/backup-mnemonic-numbers-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/backup-mnemonic-numbers-300.en.png b/docs/img/maixpy_amigo/backup-mnemonic-numbers-300.en.png new file mode 100644 index 000000000..fb8f9c00f Binary files /dev/null and b/docs/img/maixpy_amigo/backup-mnemonic-numbers-300.en.png differ diff --git a/docs/img/maixpy_amigo/backup-mnemonic-words-150.en.png b/docs/img/maixpy_amigo/backup-mnemonic-words-150.en.png deleted file mode 100644 index 55b6b8ca0..000000000 Binary files a/docs/img/maixpy_amigo/backup-mnemonic-words-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/backup-mnemonic-words-300.en.png b/docs/img/maixpy_amigo/backup-mnemonic-words-300.en.png new file mode 100644 index 000000000..6a6772c75 Binary files /dev/null and b/docs/img/maixpy_amigo/backup-mnemonic-words-300.en.png differ diff --git a/docs/img/maixpy_amigo/backup-options-150.en.png b/docs/img/maixpy_amigo/backup-options-150.en.png deleted file mode 100644 index ed641429b..000000000 Binary files a/docs/img/maixpy_amigo/backup-options-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/backup-options-300.en.png b/docs/img/maixpy_amigo/backup-options-300.en.png new file mode 100644 index 000000000..9e244b8af Binary files /dev/null and b/docs/img/maixpy_amigo/backup-options-300.en.png differ diff --git a/docs/img/maixpy_amigo/backup-qr-plain-text-150.en.png b/docs/img/maixpy_amigo/backup-qr-plain-text-150.en.png deleted file mode 100644 index 3ddb0e95b..000000000 Binary files a/docs/img/maixpy_amigo/backup-qr-plain-text-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/backup-qr-plain-text-300.en.png b/docs/img/maixpy_amigo/backup-qr-plain-text-300.en.png new file mode 100644 index 000000000..906e0c4b1 Binary files /dev/null and b/docs/img/maixpy_amigo/backup-qr-plain-text-300.en.png differ diff --git a/docs/img/maixpy_amigo/backup-seed-qr-150.en.png b/docs/img/maixpy_amigo/backup-seed-qr-150.en.png deleted file mode 100644 index 9549aaf97..000000000 Binary files a/docs/img/maixpy_amigo/backup-seed-qr-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/backup-seed-qr-300.en.png b/docs/img/maixpy_amigo/backup-seed-qr-300.en.png new file mode 100644 index 000000000..3aa8072d5 Binary files /dev/null and b/docs/img/maixpy_amigo/backup-seed-qr-300.en.png differ diff --git a/docs/img/maixpy_amigo/backup-stackbit-150.en.png b/docs/img/maixpy_amigo/backup-stackbit-150.en.png deleted file mode 100644 index a46735d37..000000000 Binary files a/docs/img/maixpy_amigo/backup-stackbit-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/backup-stackbit-300.en.png b/docs/img/maixpy_amigo/backup-stackbit-300.en.png new file mode 100644 index 000000000..d4dd0efaa Binary files /dev/null and b/docs/img/maixpy_amigo/backup-stackbit-300.en.png differ diff --git a/docs/img/maixpy_amigo/backup-tiny-seed-150.en.png b/docs/img/maixpy_amigo/backup-tiny-seed-150.en.png deleted file mode 100644 index 6ebb4e504..000000000 Binary files a/docs/img/maixpy_amigo/backup-tiny-seed-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/backup-tiny-seed-300.en.png b/docs/img/maixpy_amigo/backup-tiny-seed-300.en.png new file mode 100644 index 000000000..a805a7b18 Binary files /dev/null and b/docs/img/maixpy_amigo/backup-tiny-seed-300.en.png differ diff --git a/docs/img/maixpy_amigo/bip85-child-index-150.en.png b/docs/img/maixpy_amigo/bip85-child-index-150.en.png deleted file mode 100644 index 3925f5af7..000000000 Binary files a/docs/img/maixpy_amigo/bip85-child-index-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/bip85-child-index-300.en.png b/docs/img/maixpy_amigo/bip85-child-index-300.en.png new file mode 100644 index 000000000..c003c3b01 Binary files /dev/null and b/docs/img/maixpy_amigo/bip85-child-index-300.en.png differ diff --git a/docs/img/maixpy_amigo/bip85-load-child-150.en.png b/docs/img/maixpy_amigo/bip85-load-child-150.en.png deleted file mode 100644 index 5d88acd87..000000000 Binary files a/docs/img/maixpy_amigo/bip85-load-child-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/bip85-load-child-300.en.png b/docs/img/maixpy_amigo/bip85-load-child-300.en.png new file mode 100644 index 000000000..4ce684027 Binary files /dev/null and b/docs/img/maixpy_amigo/bip85-load-child-300.en.png differ diff --git a/docs/img/maixpy_amigo/check-sd-card-150.en.png b/docs/img/maixpy_amigo/check-sd-card-150.en.png deleted file mode 100644 index 4d543a11f..000000000 Binary files a/docs/img/maixpy_amigo/check-sd-card-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/check-sd-card-300.en.png b/docs/img/maixpy_amigo/check-sd-card-300.en.png new file mode 100644 index 000000000..6a0b878d1 Binary files /dev/null and b/docs/img/maixpy_amigo/check-sd-card-300.en.png differ diff --git a/docs/img/maixpy_amigo/create-qr-code-150.en.png b/docs/img/maixpy_amigo/create-qr-code-150.en.png deleted file mode 100644 index d8a0ff8da..000000000 Binary files a/docs/img/maixpy_amigo/create-qr-code-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/create-qr-code-300.en.png b/docs/img/maixpy_amigo/create-qr-code-300.en.png new file mode 100644 index 000000000..135d60f47 Binary files /dev/null and b/docs/img/maixpy_amigo/create-qr-code-300.en.png differ diff --git a/docs/img/maixpy_amigo/delete-mnemonic-150.en.png b/docs/img/maixpy_amigo/delete-mnemonic-150.en.png deleted file mode 100644 index 44eee09b7..000000000 Binary files a/docs/img/maixpy_amigo/delete-mnemonic-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/delete-mnemonic-300.en.png b/docs/img/maixpy_amigo/delete-mnemonic-300.en.png new file mode 100644 index 000000000..638e84274 Binary files /dev/null and b/docs/img/maixpy_amigo/delete-mnemonic-300.en.png differ diff --git a/docs/img/maixpy_amigo/descriptor-addresses-150.en.png b/docs/img/maixpy_amigo/descriptor-addresses-150.en.png deleted file mode 100644 index 356fd208f..000000000 Binary files a/docs/img/maixpy_amigo/descriptor-addresses-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/descriptor-addresses-300.en.png b/docs/img/maixpy_amigo/descriptor-addresses-300.en.png new file mode 100644 index 000000000..916fd6b01 Binary files /dev/null and b/docs/img/maixpy_amigo/descriptor-addresses-300.en.png differ diff --git a/docs/img/maixpy_amigo/encryption-options-150.en.png b/docs/img/maixpy_amigo/encryption-options-150.en.png deleted file mode 100644 index 4d7d7a2d0..000000000 Binary files a/docs/img/maixpy_amigo/encryption-options-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/encryption-options-300.en.png b/docs/img/maixpy_amigo/encryption-options-300.en.png new file mode 100644 index 000000000..837d89921 Binary files /dev/null and b/docs/img/maixpy_amigo/encryption-options-300.en.png differ diff --git a/docs/img/maixpy_amigo/encryption-options-mode-150.en.png b/docs/img/maixpy_amigo/encryption-options-mode-150.en.png deleted file mode 100644 index c4d2833b4..000000000 Binary files a/docs/img/maixpy_amigo/encryption-options-mode-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/encryption-options-mode-300.en.png b/docs/img/maixpy_amigo/encryption-options-mode-300.en.png new file mode 100644 index 000000000..46019899c Binary files /dev/null and b/docs/img/maixpy_amigo/encryption-options-mode-300.en.png differ diff --git a/docs/img/maixpy_amigo/encryption-options-pbkdf2-150.en.png b/docs/img/maixpy_amigo/encryption-options-pbkdf2-150.en.png deleted file mode 100644 index 8eca28bc3..000000000 Binary files a/docs/img/maixpy_amigo/encryption-options-pbkdf2-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/encryption-options-pbkdf2-300.en.png b/docs/img/maixpy_amigo/encryption-options-pbkdf2-300.en.png new file mode 100644 index 000000000..c6eb9cc67 Binary files /dev/null and b/docs/img/maixpy_amigo/encryption-options-pbkdf2-300.en.png differ diff --git a/docs/img/maixpy_amigo/erase-data-150.en.png b/docs/img/maixpy_amigo/erase-data-150.en.png deleted file mode 100644 index dfb010e0f..000000000 Binary files a/docs/img/maixpy_amigo/erase-data-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/erase-data-300.en.png b/docs/img/maixpy_amigo/erase-data-300.en.png new file mode 100644 index 000000000..b2afb5af4 Binary files /dev/null and b/docs/img/maixpy_amigo/erase-data-300.en.png differ diff --git a/docs/img/maixpy_amigo/extended-public-key-menu-150.en.png b/docs/img/maixpy_amigo/extended-public-key-menu-150.en.png deleted file mode 100644 index 24554b11b..000000000 Binary files a/docs/img/maixpy_amigo/extended-public-key-menu-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/extended-public-key-menu-300.en.png b/docs/img/maixpy_amigo/extended-public-key-menu-300.en.png new file mode 100644 index 000000000..258ed4575 Binary files /dev/null and b/docs/img/maixpy_amigo/extended-public-key-menu-300.en.png differ diff --git a/docs/img/maixpy_amigo/extended-public-key-selected-150.en.png b/docs/img/maixpy_amigo/extended-public-key-selected-150.en.png deleted file mode 100644 index b48c01465..000000000 Binary files a/docs/img/maixpy_amigo/extended-public-key-selected-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/extended-public-key-selected-300.en.png b/docs/img/maixpy_amigo/extended-public-key-selected-300.en.png new file mode 100644 index 000000000..4bc209fa8 Binary files /dev/null and b/docs/img/maixpy_amigo/extended-public-key-selected-300.en.png differ diff --git a/docs/img/maixpy_amigo/extended-public-key-wpkh-xpub-qr-150.en.png b/docs/img/maixpy_amigo/extended-public-key-wpkh-xpub-qr-150.en.png deleted file mode 100644 index 7b13f00f0..000000000 Binary files a/docs/img/maixpy_amigo/extended-public-key-wpkh-xpub-qr-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/extended-public-key-wpkh-xpub-qr-300.en.png b/docs/img/maixpy_amigo/extended-public-key-wpkh-xpub-qr-300.en.png new file mode 100644 index 000000000..2c5760b4e Binary files /dev/null and b/docs/img/maixpy_amigo/extended-public-key-wpkh-xpub-qr-300.en.png differ diff --git a/docs/img/maixpy_amigo/extended-public-key-wpkh-xpub-text-150.en.png b/docs/img/maixpy_amigo/extended-public-key-wpkh-xpub-text-150.en.png deleted file mode 100644 index 7e4ce7812..000000000 Binary files a/docs/img/maixpy_amigo/extended-public-key-wpkh-xpub-text-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/extended-public-key-wpkh-xpub-text-300.en.png b/docs/img/maixpy_amigo/extended-public-key-wpkh-xpub-text-300.en.png new file mode 100644 index 000000000..18873aa49 Binary files /dev/null and b/docs/img/maixpy_amigo/extended-public-key-wpkh-xpub-text-300.en.png differ diff --git a/docs/img/maixpy_amigo/extended-public-key-wpkh-zpub-qr-150.en.png b/docs/img/maixpy_amigo/extended-public-key-wpkh-zpub-qr-150.en.png deleted file mode 100644 index 3c8184e28..000000000 Binary files a/docs/img/maixpy_amigo/extended-public-key-wpkh-zpub-qr-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/extended-public-key-wpkh-zpub-qr-300.en.png b/docs/img/maixpy_amigo/extended-public-key-wpkh-zpub-qr-300.en.png new file mode 100644 index 000000000..b3f6cd691 Binary files /dev/null and b/docs/img/maixpy_amigo/extended-public-key-wpkh-zpub-qr-300.en.png differ diff --git a/docs/img/maixpy_amigo/extended-public-key-wsh-xpub-qr-150.en.png b/docs/img/maixpy_amigo/extended-public-key-wsh-xpub-qr-150.en.png deleted file mode 100644 index 7b13f00f0..000000000 Binary files a/docs/img/maixpy_amigo/extended-public-key-wsh-xpub-qr-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/extended-public-key-wsh-xpub-qr-300.en.png b/docs/img/maixpy_amigo/extended-public-key-wsh-xpub-qr-300.en.png new file mode 100644 index 000000000..2c5760b4e Binary files /dev/null and b/docs/img/maixpy_amigo/extended-public-key-wsh-xpub-qr-300.en.png differ diff --git a/docs/img/maixpy_amigo/extended-public-key-wsh-xpub-text-150.en.png b/docs/img/maixpy_amigo/extended-public-key-wsh-xpub-text-150.en.png deleted file mode 100644 index 7e4ce7812..000000000 Binary files a/docs/img/maixpy_amigo/extended-public-key-wsh-xpub-text-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/extended-public-key-wsh-xpub-text-300.en.png b/docs/img/maixpy_amigo/extended-public-key-wsh-xpub-text-300.en.png new file mode 100644 index 000000000..18873aa49 Binary files /dev/null and b/docs/img/maixpy_amigo/extended-public-key-wsh-xpub-text-300.en.png differ diff --git a/docs/img/maixpy_amigo/extended-public-key-xpub-qr-menu-selected-150.en.png b/docs/img/maixpy_amigo/extended-public-key-xpub-qr-menu-selected-150.en.png deleted file mode 100644 index 992fd59f1..000000000 Binary files a/docs/img/maixpy_amigo/extended-public-key-xpub-qr-menu-selected-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/extended-public-key-xpub-qr-menu-selected-300.en.png b/docs/img/maixpy_amigo/extended-public-key-xpub-qr-menu-selected-300.en.png new file mode 100644 index 000000000..2647273f1 Binary files /dev/null and b/docs/img/maixpy_amigo/extended-public-key-xpub-qr-menu-selected-300.en.png differ diff --git a/docs/img/maixpy_amigo/flash-map-150.en.png b/docs/img/maixpy_amigo/flash-map-150.en.png deleted file mode 100644 index 710d119ba..000000000 Binary files a/docs/img/maixpy_amigo/flash-map-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/flash-map-300.en.png b/docs/img/maixpy_amigo/flash-map-300.en.png new file mode 100644 index 000000000..712e308d4 Binary files /dev/null and b/docs/img/maixpy_amigo/flash-map-300.en.png differ diff --git a/docs/img/maixpy_amigo/flash-tools-150.en.png b/docs/img/maixpy_amigo/flash-tools-150.en.png deleted file mode 100644 index dd8035d1d..000000000 Binary files a/docs/img/maixpy_amigo/flash-tools-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/flash-tools-300.en.png b/docs/img/maixpy_amigo/flash-tools-300.en.png new file mode 100644 index 000000000..c0f2e9219 Binary files /dev/null and b/docs/img/maixpy_amigo/flash-tools-300.en.png differ diff --git a/docs/img/maixpy_amigo/grided-qr-code-150.en.png b/docs/img/maixpy_amigo/grided-qr-code-150.en.png deleted file mode 100644 index c98e42655..000000000 Binary files a/docs/img/maixpy_amigo/grided-qr-code-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/grided-qr-code-300.en.png b/docs/img/maixpy_amigo/grided-qr-code-300.en.png new file mode 100644 index 000000000..d8a37d99a Binary files /dev/null and b/docs/img/maixpy_amigo/grided-qr-code-300.en.png differ diff --git a/docs/img/maixpy_amigo/home-encrypt-options-150.en.png b/docs/img/maixpy_amigo/home-encrypt-options-150.en.png deleted file mode 100644 index fa538092c..000000000 Binary files a/docs/img/maixpy_amigo/home-encrypt-options-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/home-encrypt-options-300.en.png b/docs/img/maixpy_amigo/home-encrypt-options-300.en.png new file mode 100644 index 000000000..260c657c2 Binary files /dev/null and b/docs/img/maixpy_amigo/home-encrypt-options-300.en.png differ diff --git a/docs/img/maixpy_amigo/home-options-150.en.png b/docs/img/maixpy_amigo/home-options-150.en.png deleted file mode 100644 index 7f039aaf7..000000000 Binary files a/docs/img/maixpy_amigo/home-options-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/home-options-300.en.png b/docs/img/maixpy_amigo/home-options-300.en.png new file mode 100644 index 000000000..9e44454e5 Binary files /dev/null and b/docs/img/maixpy_amigo/home-options-300.en.png differ diff --git a/docs/img/maixpy_amigo/lines-qr-code-150.en.png b/docs/img/maixpy_amigo/lines-qr-code-150.en.png deleted file mode 100644 index 9e6889c93..000000000 Binary files a/docs/img/maixpy_amigo/lines-qr-code-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/lines-qr-code-300.en.png b/docs/img/maixpy_amigo/lines-qr-code-300.en.png new file mode 100644 index 000000000..d1872e56a Binary files /dev/null and b/docs/img/maixpy_amigo/lines-qr-code-300.en.png differ diff --git a/docs/img/maixpy_amigo/list-address-change-150.en.png b/docs/img/maixpy_amigo/list-address-change-150.en.png deleted file mode 100644 index af7c36ce9..000000000 Binary files a/docs/img/maixpy_amigo/list-address-change-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/list-address-change-300.en.png b/docs/img/maixpy_amigo/list-address-change-300.en.png new file mode 100644 index 000000000..9089854f6 Binary files /dev/null and b/docs/img/maixpy_amigo/list-address-change-300.en.png differ diff --git a/docs/img/maixpy_amigo/list-address-receive-150.en.png b/docs/img/maixpy_amigo/list-address-receive-150.en.png deleted file mode 100644 index feeedd906..000000000 Binary files a/docs/img/maixpy_amigo/list-address-receive-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/list-address-receive-300.en.png b/docs/img/maixpy_amigo/list-address-receive-300.en.png new file mode 100644 index 000000000..921150d26 Binary files /dev/null and b/docs/img/maixpy_amigo/list-address-receive-300.en.png differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-camera-options-150.en.png b/docs/img/maixpy_amigo/load-mnemonic-camera-options-150.en.png deleted file mode 100644 index 4aef1a273..000000000 Binary files a/docs/img/maixpy_amigo/load-mnemonic-camera-options-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-camera-options-300.en.png b/docs/img/maixpy_amigo/load-mnemonic-camera-options-300.en.png new file mode 100644 index 000000000..1e8f87ce5 Binary files /dev/null and b/docs/img/maixpy_amigo/load-mnemonic-camera-options-300.en.png differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-manual-options-150.en.png b/docs/img/maixpy_amigo/load-mnemonic-manual-options-150.en.png deleted file mode 100644 index 749201936..000000000 Binary files a/docs/img/maixpy_amigo/load-mnemonic-manual-options-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-manual-options-300.en.png b/docs/img/maixpy_amigo/load-mnemonic-manual-options-300.en.png new file mode 100644 index 000000000..d8cb8093b Binary files /dev/null and b/docs/img/maixpy_amigo/load-mnemonic-manual-options-300.en.png differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-options-150.en.png b/docs/img/maixpy_amigo/load-mnemonic-options-150.en.png deleted file mode 100644 index 49b0bebb4..000000000 Binary files a/docs/img/maixpy_amigo/load-mnemonic-options-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-options-300.en.png b/docs/img/maixpy_amigo/load-mnemonic-options-300.en.png new file mode 100644 index 000000000..1e4769996 Binary files /dev/null and b/docs/img/maixpy_amigo/load-mnemonic-options-300.en.png differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-seq-double-mnemonic-150.en.png b/docs/img/maixpy_amigo/load-mnemonic-seq-double-mnemonic-150.en.png deleted file mode 100644 index 7bfe52224..000000000 Binary files a/docs/img/maixpy_amigo/load-mnemonic-seq-double-mnemonic-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-seq-double-mnemonic-300.en.png b/docs/img/maixpy_amigo/load-mnemonic-seq-double-mnemonic-300.en.png new file mode 100644 index 000000000..dff6a9bcd Binary files /dev/null and b/docs/img/maixpy_amigo/load-mnemonic-seq-double-mnemonic-300.en.png differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-seq-mnemonic-150.en.png b/docs/img/maixpy_amigo/load-mnemonic-seq-mnemonic-150.en.png deleted file mode 100644 index 98ca3641a..000000000 Binary files a/docs/img/maixpy_amigo/load-mnemonic-seq-mnemonic-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-seq-mnemonic-300.en.png b/docs/img/maixpy_amigo/load-mnemonic-seq-mnemonic-300.en.png new file mode 100644 index 000000000..81430bec3 Binary files /dev/null and b/docs/img/maixpy_amigo/load-mnemonic-seq-mnemonic-300.en.png differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-seq-mnemonic-edited-wrong-150.en.png b/docs/img/maixpy_amigo/load-mnemonic-seq-mnemonic-edited-wrong-150.en.png deleted file mode 100644 index 1da8fc837..000000000 Binary files a/docs/img/maixpy_amigo/load-mnemonic-seq-mnemonic-edited-wrong-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-seq-mnemonic-edited-wrong-300.en.png b/docs/img/maixpy_amigo/load-mnemonic-seq-mnemonic-edited-wrong-300.en.png new file mode 100644 index 000000000..0da5aea8e Binary files /dev/null and b/docs/img/maixpy_amigo/load-mnemonic-seq-mnemonic-edited-wrong-300.en.png differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-seq-overview-150.en.png b/docs/img/maixpy_amigo/load-mnemonic-seq-overview-150.en.png deleted file mode 100644 index c5b72ee82..000000000 Binary files a/docs/img/maixpy_amigo/load-mnemonic-seq-overview-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-seq-overview-300.en.png b/docs/img/maixpy_amigo/load-mnemonic-seq-overview-300.en.png new file mode 100644 index 000000000..5616102a3 Binary files /dev/null and b/docs/img/maixpy_amigo/load-mnemonic-seq-overview-300.en.png differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-storage-options-150.en.png b/docs/img/maixpy_amigo/load-mnemonic-storage-options-150.en.png deleted file mode 100644 index 44eee09b7..000000000 Binary files a/docs/img/maixpy_amigo/load-mnemonic-storage-options-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-storage-options-300.en.png b/docs/img/maixpy_amigo/load-mnemonic-storage-options-300.en.png new file mode 100644 index 000000000..638e84274 Binary files /dev/null and b/docs/img/maixpy_amigo/load-mnemonic-storage-options-300.en.png differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-via-numbers-word-150.en.png b/docs/img/maixpy_amigo/load-mnemonic-via-numbers-word-150.en.png deleted file mode 100644 index c6a4cc169..000000000 Binary files a/docs/img/maixpy_amigo/load-mnemonic-via-numbers-word-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-via-numbers-word-300.en.png b/docs/img/maixpy_amigo/load-mnemonic-via-numbers-word-300.en.png new file mode 100644 index 000000000..182edab05 Binary files /dev/null and b/docs/img/maixpy_amigo/load-mnemonic-via-numbers-word-300.en.png differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-via-stackbit-filled-150.en.png b/docs/img/maixpy_amigo/load-mnemonic-via-stackbit-filled-150.en.png deleted file mode 100644 index 068a84f45..000000000 Binary files a/docs/img/maixpy_amigo/load-mnemonic-via-stackbit-filled-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-via-stackbit-filled-300.en.png b/docs/img/maixpy_amigo/load-mnemonic-via-stackbit-filled-300.en.png new file mode 100644 index 000000000..7c15ac3b3 Binary files /dev/null and b/docs/img/maixpy_amigo/load-mnemonic-via-stackbit-filled-300.en.png differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-via-text-word-150.en.png b/docs/img/maixpy_amigo/load-mnemonic-via-text-word-150.en.png deleted file mode 100644 index 4cc717fff..000000000 Binary files a/docs/img/maixpy_amigo/load-mnemonic-via-text-word-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-via-text-word-300.en.png b/docs/img/maixpy_amigo/load-mnemonic-via-text-word-300.en.png new file mode 100644 index 000000000..e87197d6e Binary files /dev/null and b/docs/img/maixpy_amigo/load-mnemonic-via-text-word-300.en.png differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-via-tinyseed-filled-150.en.png b/docs/img/maixpy_amigo/load-mnemonic-via-tinyseed-filled-150.en.png deleted file mode 100644 index dc6501a4e..000000000 Binary files a/docs/img/maixpy_amigo/load-mnemonic-via-tinyseed-filled-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/load-mnemonic-via-tinyseed-filled-300.en.png b/docs/img/maixpy_amigo/load-mnemonic-via-tinyseed-filled-300.en.png new file mode 100644 index 000000000..f983c5368 Binary files /dev/null and b/docs/img/maixpy_amigo/load-mnemonic-via-tinyseed-filled-300.en.png differ diff --git a/docs/img/maixpy_amigo/locale-options-150.en.png b/docs/img/maixpy_amigo/locale-options-150.en.png deleted file mode 100644 index 93195e4fe..000000000 Binary files a/docs/img/maixpy_amigo/locale-options-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/locale-options-300.en.png b/docs/img/maixpy_amigo/locale-options-300.en.png new file mode 100644 index 000000000..b79ebfc87 Binary files /dev/null and b/docs/img/maixpy_amigo/locale-options-300.en.png differ diff --git a/docs/img/maixpy_amigo/logo-150.en.png b/docs/img/maixpy_amigo/logo-150.en.png deleted file mode 100644 index 2fa40fa6d..000000000 Binary files a/docs/img/maixpy_amigo/logo-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/logo-300.en.png b/docs/img/maixpy_amigo/logo-300.en.png new file mode 100644 index 000000000..c7363e9e4 Binary files /dev/null and b/docs/img/maixpy_amigo/logo-300.en.png differ diff --git a/docs/img/maixpy_amigo/network-options-150.en.png b/docs/img/maixpy_amigo/network-options-150.en.png deleted file mode 100644 index 93bd07ea9..000000000 Binary files a/docs/img/maixpy_amigo/network-options-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/network-options-300.en.png b/docs/img/maixpy_amigo/network-options-300.en.png new file mode 100644 index 000000000..28537d750 Binary files /dev/null and b/docs/img/maixpy_amigo/network-options-300.en.png differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-edited-150.en.png b/docs/img/maixpy_amigo/new-mnemonic-edited-150.en.png deleted file mode 100644 index 296877776..000000000 Binary files a/docs/img/maixpy_amigo/new-mnemonic-edited-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-edited-300.en.png b/docs/img/maixpy_amigo/new-mnemonic-edited-300.en.png new file mode 100644 index 000000000..e25899a90 Binary files /dev/null and b/docs/img/maixpy_amigo/new-mnemonic-edited-300.en.png differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-options-150.en.png b/docs/img/maixpy_amigo/new-mnemonic-options-150.en.png deleted file mode 100644 index f65388a15..000000000 Binary files a/docs/img/maixpy_amigo/new-mnemonic-options-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-options-300.en.png b/docs/img/maixpy_amigo/new-mnemonic-options-300.en.png new file mode 100644 index 000000000..862dbd3de Binary files /dev/null and b/docs/img/maixpy_amigo/new-mnemonic-options-300.en.png differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-d20-last-n-rolls-150.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-d20-last-n-rolls-150.en.png deleted file mode 100644 index 704ff8d00..000000000 Binary files a/docs/img/maixpy_amigo/new-mnemonic-via-d20-last-n-rolls-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-d20-last-n-rolls-300.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-d20-last-n-rolls-300.en.png new file mode 100644 index 000000000..8ef3802cc Binary files /dev/null and b/docs/img/maixpy_amigo/new-mnemonic-via-d20-last-n-rolls-300.en.png differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-1-150.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-1-150.en.png deleted file mode 100644 index 4e99ddf9a..000000000 Binary files a/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-1-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-1-300.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-1-300.en.png new file mode 100644 index 000000000..92a08d75b Binary files /dev/null and b/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-1-300.en.png differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-nerd-stats-150.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-nerd-stats-150.en.png deleted file mode 100644 index 76937055d..000000000 Binary files a/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-nerd-stats-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-nerd-stats-300.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-nerd-stats-300.en.png new file mode 100644 index 000000000..55f05860c Binary files /dev/null and b/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-nerd-stats-300.en.png differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-sha256-150.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-sha256-150.en.png deleted file mode 100644 index 6844e4f4c..000000000 Binary files a/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-sha256-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-sha256-300.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-sha256-300.en.png new file mode 100644 index 000000000..9e8a36bd5 Binary files /dev/null and b/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-sha256-300.en.png differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-string-150.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-string-150.en.png deleted file mode 100644 index 9601e9ab9..000000000 Binary files a/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-string-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-string-300.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-string-300.en.png new file mode 100644 index 000000000..7adcda681 Binary files /dev/null and b/docs/img/maixpy_amigo/new-mnemonic-via-d20-roll-string-300.en.png differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-d6-last-n-rolls-150.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-d6-last-n-rolls-150.en.png deleted file mode 100644 index 9bc17945d..000000000 Binary files a/docs/img/maixpy_amigo/new-mnemonic-via-d6-last-n-rolls-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-d6-last-n-rolls-300.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-d6-last-n-rolls-300.en.png new file mode 100644 index 000000000..44cbaf9f3 Binary files /dev/null and b/docs/img/maixpy_amigo/new-mnemonic-via-d6-last-n-rolls-300.en.png differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-1-150.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-1-150.en.png deleted file mode 100644 index ae779473f..000000000 Binary files a/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-1-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-1-300.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-1-300.en.png new file mode 100644 index 000000000..c4cca62b3 Binary files /dev/null and b/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-1-300.en.png differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-nerd-stats-150.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-nerd-stats-150.en.png deleted file mode 100644 index 39bddd32a..000000000 Binary files a/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-nerd-stats-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-nerd-stats-300.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-nerd-stats-300.en.png new file mode 100644 index 000000000..263b88e64 Binary files /dev/null and b/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-nerd-stats-300.en.png differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-sha256-150.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-sha256-150.en.png deleted file mode 100644 index 43cce3868..000000000 Binary files a/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-sha256-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-sha256-300.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-sha256-300.en.png new file mode 100644 index 000000000..d29d3bd16 Binary files /dev/null and b/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-sha256-300.en.png differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-string-150.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-string-150.en.png deleted file mode 100644 index 2a78bfaff..000000000 Binary files a/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-string-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-string-300.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-string-300.en.png new file mode 100644 index 000000000..4cdee913a Binary files /dev/null and b/docs/img/maixpy_amigo/new-mnemonic-via-d6-roll-string-300.en.png differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-capturing-150.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-capturing-150.en.png deleted file mode 100644 index b9e656b0d..000000000 Binary files a/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-capturing-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-capturing-300.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-capturing-300.en.png new file mode 100644 index 000000000..e97926179 Binary files /dev/null and b/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-capturing-300.en.png differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-entropy-estimation-150.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-entropy-estimation-150.en.png deleted file mode 100644 index ff8b68186..000000000 Binary files a/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-entropy-estimation-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-entropy-estimation-300.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-entropy-estimation-300.en.png new file mode 100644 index 000000000..8ddea3692 Binary files /dev/null and b/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-entropy-estimation-300.en.png differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-prompt-150.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-prompt-150.en.png deleted file mode 100644 index 85ecc6226..000000000 Binary files a/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-prompt-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-prompt-300.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-prompt-300.en.png new file mode 100644 index 000000000..01622d97f Binary files /dev/null and b/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-prompt-300.en.png differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-sha256-150.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-sha256-150.en.png deleted file mode 100644 index a8fee5d5e..000000000 Binary files a/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-sha256-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-sha256-300.en.png b/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-sha256-300.en.png new file mode 100644 index 000000000..0ba8a13d6 Binary files /dev/null and b/docs/img/maixpy_amigo/new-mnemonic-via-snapshot-sha256-300.en.png differ diff --git a/docs/img/maixpy_amigo/passphrase-load-options-150.en.png b/docs/img/maixpy_amigo/passphrase-load-options-150.en.png deleted file mode 100644 index fdcfce1a1..000000000 Binary files a/docs/img/maixpy_amigo/passphrase-load-options-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/passphrase-load-options-300.en.png b/docs/img/maixpy_amigo/passphrase-load-options-300.en.png new file mode 100644 index 000000000..21f1d4c75 Binary files /dev/null and b/docs/img/maixpy_amigo/passphrase-load-options-300.en.png differ diff --git a/docs/img/maixpy_amigo/persist-options-150.en.png b/docs/img/maixpy_amigo/persist-options-150.en.png deleted file mode 100644 index c2655d750..000000000 Binary files a/docs/img/maixpy_amigo/persist-options-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/persist-options-300.en.png b/docs/img/maixpy_amigo/persist-options-300.en.png new file mode 100644 index 000000000..d9f473890 Binary files /dev/null and b/docs/img/maixpy_amigo/persist-options-300.en.png differ diff --git a/docs/img/maixpy_amigo/print-qr-printing-150.en.png b/docs/img/maixpy_amigo/print-qr-printing-150.en.png deleted file mode 100644 index efe645c65..000000000 Binary files a/docs/img/maixpy_amigo/print-qr-printing-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/print-qr-printing-300.en.png b/docs/img/maixpy_amigo/print-qr-printing-300.en.png new file mode 100644 index 000000000..a0beb55dd Binary files /dev/null and b/docs/img/maixpy_amigo/print-qr-printing-300.en.png differ diff --git a/docs/img/maixpy_amigo/print-qr-prompt-150.en.png b/docs/img/maixpy_amigo/print-qr-prompt-150.en.png deleted file mode 100644 index 2f85d97f4..000000000 Binary files a/docs/img/maixpy_amigo/print-qr-prompt-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/print-qr-prompt-300.en.png b/docs/img/maixpy_amigo/print-qr-prompt-300.en.png new file mode 100644 index 000000000..b5d65c416 Binary files /dev/null and b/docs/img/maixpy_amigo/print-qr-prompt-300.en.png differ diff --git a/docs/img/maixpy_amigo/print-test-qr-150.en.png b/docs/img/maixpy_amigo/print-test-qr-150.en.png deleted file mode 100644 index 8e5320d9b..000000000 Binary files a/docs/img/maixpy_amigo/print-test-qr-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/print-test-qr-300.en.png b/docs/img/maixpy_amigo/print-test-qr-300.en.png new file mode 100644 index 000000000..04f48a771 Binary files /dev/null and b/docs/img/maixpy_amigo/print-test-qr-300.en.png differ diff --git a/docs/img/maixpy_amigo/printer-options-150.en.png b/docs/img/maixpy_amigo/printer-options-150.en.png deleted file mode 100644 index 82f28e786..000000000 Binary files a/docs/img/maixpy_amigo/printer-options-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/printer-options-300.en.png b/docs/img/maixpy_amigo/printer-options-300.en.png new file mode 100644 index 000000000..d522d9138 Binary files /dev/null and b/docs/img/maixpy_amigo/printer-options-300.en.png differ diff --git a/docs/img/maixpy_amigo/regions-qr-code-150.en.png b/docs/img/maixpy_amigo/regions-qr-code-150.en.png deleted file mode 100644 index e639e993b..000000000 Binary files a/docs/img/maixpy_amigo/regions-qr-code-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/regions-qr-code-300.en.png b/docs/img/maixpy_amigo/regions-qr-code-300.en.png new file mode 100644 index 000000000..213ab0596 Binary files /dev/null and b/docs/img/maixpy_amigo/regions-qr-code-300.en.png differ diff --git a/docs/img/maixpy_amigo/scan-address-scanned-address-150.en.png b/docs/img/maixpy_amigo/scan-address-scanned-address-150.en.png deleted file mode 100644 index bd6be2d6a..000000000 Binary files a/docs/img/maixpy_amigo/scan-address-scanned-address-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/scan-address-scanned-address-300.en.png b/docs/img/maixpy_amigo/scan-address-scanned-address-300.en.png new file mode 100644 index 000000000..889b22e33 Binary files /dev/null and b/docs/img/maixpy_amigo/scan-address-scanned-address-300.en.png differ diff --git a/docs/img/maixpy_amigo/settings-options-150.en.png b/docs/img/maixpy_amigo/settings-options-150.en.png deleted file mode 100644 index 9e88b2144..000000000 Binary files a/docs/img/maixpy_amigo/settings-options-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/settings-options-300.en.png b/docs/img/maixpy_amigo/settings-options-300.en.png new file mode 100644 index 000000000..949a64bb4 Binary files /dev/null and b/docs/img/maixpy_amigo/settings-options-300.en.png differ diff --git a/docs/img/maixpy_amigo/settings-options-appearance-150.en.png b/docs/img/maixpy_amigo/settings-options-appearance-150.en.png deleted file mode 100644 index d591bf561..000000000 Binary files a/docs/img/maixpy_amigo/settings-options-appearance-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/settings-options-appearance-300.en.png b/docs/img/maixpy_amigo/settings-options-appearance-300.en.png new file mode 100644 index 000000000..c0b811245 Binary files /dev/null and b/docs/img/maixpy_amigo/settings-options-appearance-300.en.png differ diff --git a/docs/img/maixpy_amigo/settings-options-appearance-screensaver-150.en.png b/docs/img/maixpy_amigo/settings-options-appearance-screensaver-150.en.png deleted file mode 100644 index 692c86b54..000000000 Binary files a/docs/img/maixpy_amigo/settings-options-appearance-screensaver-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/settings-options-appearance-screensaver-300.en.png b/docs/img/maixpy_amigo/settings-options-appearance-screensaver-300.en.png new file mode 100644 index 000000000..cb1eca5b4 Binary files /dev/null and b/docs/img/maixpy_amigo/settings-options-appearance-screensaver-300.en.png differ diff --git a/docs/img/maixpy_amigo/settings-options-factory-settings-150.en.png b/docs/img/maixpy_amigo/settings-options-factory-settings-150.en.png deleted file mode 100644 index 2e78e038f..000000000 Binary files a/docs/img/maixpy_amigo/settings-options-factory-settings-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/settings-options-factory-settings-300.en.png b/docs/img/maixpy_amigo/settings-options-factory-settings-300.en.png new file mode 100644 index 000000000..dc335f894 Binary files /dev/null and b/docs/img/maixpy_amigo/settings-options-factory-settings-300.en.png differ diff --git a/docs/img/maixpy_amigo/settings-options-hardware-150.en.png b/docs/img/maixpy_amigo/settings-options-hardware-150.en.png deleted file mode 100644 index 1e9e7821a..000000000 Binary files a/docs/img/maixpy_amigo/settings-options-hardware-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/settings-options-hardware-300.en.png b/docs/img/maixpy_amigo/settings-options-hardware-300.en.png new file mode 100644 index 000000000..159b3c0e7 Binary files /dev/null and b/docs/img/maixpy_amigo/settings-options-hardware-300.en.png differ diff --git a/docs/img/maixpy_amigo/settings-options-hardware-display-150.en.png b/docs/img/maixpy_amigo/settings-options-hardware-display-150.en.png deleted file mode 100644 index 31df5361e..000000000 Binary files a/docs/img/maixpy_amigo/settings-options-hardware-display-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/settings-options-hardware-display-300.en.png b/docs/img/maixpy_amigo/settings-options-hardware-display-300.en.png new file mode 100644 index 000000000..0d6e5741a Binary files /dev/null and b/docs/img/maixpy_amigo/settings-options-hardware-display-300.en.png differ diff --git a/docs/img/maixpy_amigo/sign-message-at-address-prompt-150.en.png b/docs/img/maixpy_amigo/sign-message-at-address-prompt-150.en.png deleted file mode 100644 index 786f607cc..000000000 Binary files a/docs/img/maixpy_amigo/sign-message-at-address-prompt-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/sign-message-at-address-prompt-300.en.png b/docs/img/maixpy_amigo/sign-message-at-address-prompt-300.en.png new file mode 100644 index 000000000..229354891 Binary files /dev/null and b/docs/img/maixpy_amigo/sign-message-at-address-prompt-300.en.png differ diff --git a/docs/img/maixpy_amigo/sign-message-sha256-sign-prompt-150.en.png b/docs/img/maixpy_amigo/sign-message-sha256-sign-prompt-150.en.png deleted file mode 100644 index 3fee5c9ff..000000000 Binary files a/docs/img/maixpy_amigo/sign-message-sha256-sign-prompt-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/sign-message-sha256-sign-prompt-300.en.png b/docs/img/maixpy_amigo/sign-message-sha256-sign-prompt-300.en.png new file mode 100644 index 000000000..43d0d5ae9 Binary files /dev/null and b/docs/img/maixpy_amigo/sign-message-sha256-sign-prompt-300.en.png differ diff --git a/docs/img/maixpy_amigo/sign-options-150.en.png b/docs/img/maixpy_amigo/sign-options-150.en.png deleted file mode 100644 index 0bba85162..000000000 Binary files a/docs/img/maixpy_amigo/sign-options-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/sign-options-300.en.png b/docs/img/maixpy_amigo/sign-options-300.en.png new file mode 100644 index 000000000..bccb86172 Binary files /dev/null and b/docs/img/maixpy_amigo/sign-options-300.en.png differ diff --git a/docs/img/maixpy_amigo/sign-psbt-sign-prompt-150.en.png b/docs/img/maixpy_amigo/sign-psbt-sign-prompt-150.en.png deleted file mode 100644 index 2c5d41afc..000000000 Binary files a/docs/img/maixpy_amigo/sign-psbt-sign-prompt-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/sign-psbt-sign-prompt-300.en.png b/docs/img/maixpy_amigo/sign-psbt-sign-prompt-300.en.png new file mode 100644 index 000000000..f7a3eb732 Binary files /dev/null and b/docs/img/maixpy_amigo/sign-psbt-sign-prompt-300.en.png differ diff --git a/docs/img/maixpy_amigo/sign-psbt-signed-qr-150.en.png b/docs/img/maixpy_amigo/sign-psbt-signed-qr-150.en.png deleted file mode 100644 index 4aa670216..000000000 Binary files a/docs/img/maixpy_amigo/sign-psbt-signed-qr-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/sign-psbt-signed-qr-300.en.png b/docs/img/maixpy_amigo/sign-psbt-signed-qr-300.en.png new file mode 100644 index 000000000..c2ff37089 Binary files /dev/null and b/docs/img/maixpy_amigo/sign-psbt-signed-qr-300.en.png differ diff --git a/docs/img/maixpy_amigo/standard-qr-code-150.en.png b/docs/img/maixpy_amigo/standard-qr-code-150.en.png deleted file mode 100644 index 2cac06eec..000000000 Binary files a/docs/img/maixpy_amigo/standard-qr-code-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/standard-qr-code-300.en.png b/docs/img/maixpy_amigo/standard-qr-code-300.en.png new file mode 100644 index 000000000..4fe20a006 Binary files /dev/null and b/docs/img/maixpy_amigo/standard-qr-code-300.en.png differ diff --git a/docs/img/maixpy_amigo/theme-1-150.en.png b/docs/img/maixpy_amigo/theme-1-150.en.png deleted file mode 100644 index d51be85ea..000000000 Binary files a/docs/img/maixpy_amigo/theme-1-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/theme-1-300.en.png b/docs/img/maixpy_amigo/theme-1-300.en.png new file mode 100644 index 000000000..4ff154b0b Binary files /dev/null and b/docs/img/maixpy_amigo/theme-1-300.en.png differ diff --git a/docs/img/maixpy_amigo/theme-2-150.en.png b/docs/img/maixpy_amigo/theme-2-150.en.png deleted file mode 100644 index abf86ddfd..000000000 Binary files a/docs/img/maixpy_amigo/theme-2-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/theme-2-300.en.png b/docs/img/maixpy_amigo/theme-2-300.en.png new file mode 100644 index 000000000..c1f6209da Binary files /dev/null and b/docs/img/maixpy_amigo/theme-2-300.en.png differ diff --git a/docs/img/maixpy_amigo/theme-3-150.en.png b/docs/img/maixpy_amigo/theme-3-150.en.png deleted file mode 100644 index 057daf47d..000000000 Binary files a/docs/img/maixpy_amigo/theme-3-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/theme-3-300.en.png b/docs/img/maixpy_amigo/theme-3-300.en.png new file mode 100644 index 000000000..d02882a7a Binary files /dev/null and b/docs/img/maixpy_amigo/theme-3-300.en.png differ diff --git a/docs/img/maixpy_amigo/theme-4-150.en.png b/docs/img/maixpy_amigo/theme-4-150.en.png deleted file mode 100644 index 49d00ed94..000000000 Binary files a/docs/img/maixpy_amigo/theme-4-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/theme-4-300.en.png b/docs/img/maixpy_amigo/theme-4-300.en.png new file mode 100644 index 000000000..51ff0dec9 Binary files /dev/null and b/docs/img/maixpy_amigo/theme-4-300.en.png differ diff --git a/docs/img/maixpy_amigo/theme-5-150.en.png b/docs/img/maixpy_amigo/theme-5-150.en.png deleted file mode 100644 index fe2365445..000000000 Binary files a/docs/img/maixpy_amigo/theme-5-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/theme-5-300.en.png b/docs/img/maixpy_amigo/theme-5-300.en.png new file mode 100644 index 000000000..2ec34d401 Binary files /dev/null and b/docs/img/maixpy_amigo/theme-5-300.en.png differ diff --git a/docs/img/maixpy_amigo/tools-options-150.en.png b/docs/img/maixpy_amigo/tools-options-150.en.png deleted file mode 100644 index 959fb3618..000000000 Binary files a/docs/img/maixpy_amigo/tools-options-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/tools-options-300.en.png b/docs/img/maixpy_amigo/tools-options-300.en.png new file mode 100644 index 000000000..379c46b0d Binary files /dev/null and b/docs/img/maixpy_amigo/tools-options-300.en.png differ diff --git a/docs/img/maixpy_amigo/touchscreen-150.en.png b/docs/img/maixpy_amigo/touchscreen-150.en.png deleted file mode 100644 index 293581ccb..000000000 Binary files a/docs/img/maixpy_amigo/touchscreen-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/touchscreen-300.en.png b/docs/img/maixpy_amigo/touchscreen-300.en.png new file mode 100644 index 000000000..050381882 Binary files /dev/null and b/docs/img/maixpy_amigo/touchscreen-300.en.png differ diff --git a/docs/img/maixpy_amigo/wallet-customization-options-150.en.png b/docs/img/maixpy_amigo/wallet-customization-options-150.en.png deleted file mode 100644 index c9c31882b..000000000 Binary files a/docs/img/maixpy_amigo/wallet-customization-options-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/wallet-customization-options-300.en.png b/docs/img/maixpy_amigo/wallet-customization-options-300.en.png new file mode 100644 index 000000000..a88369cc5 Binary files /dev/null and b/docs/img/maixpy_amigo/wallet-customization-options-300.en.png differ diff --git a/docs/img/maixpy_amigo/wallet-descriptor-tr-minis-1-300.en.png b/docs/img/maixpy_amigo/wallet-descriptor-tr-minis-1-300.en.png new file mode 100644 index 000000000..ca2125cb7 Binary files /dev/null and b/docs/img/maixpy_amigo/wallet-descriptor-tr-minis-1-300.en.png differ diff --git a/docs/img/maixpy_amigo/wallet-descriptor-tr-minis-2-300.en.png b/docs/img/maixpy_amigo/wallet-descriptor-tr-minis-2-300.en.png new file mode 100644 index 000000000..a07fe843d Binary files /dev/null and b/docs/img/maixpy_amigo/wallet-descriptor-tr-minis-2-300.en.png differ diff --git a/docs/img/maixpy_amigo/wallet-load-prompt-150.en.png b/docs/img/maixpy_amigo/wallet-load-prompt-150.en.png deleted file mode 100644 index b898128ac..000000000 Binary files a/docs/img/maixpy_amigo/wallet-load-prompt-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/wallet-load-prompt-300.en.png b/docs/img/maixpy_amigo/wallet-load-prompt-300.en.png new file mode 100644 index 000000000..2d4cbe4c2 Binary files /dev/null and b/docs/img/maixpy_amigo/wallet-load-prompt-300.en.png differ diff --git a/docs/img/maixpy_amigo/wallet-wpkh-load-prompt-150.en.png b/docs/img/maixpy_amigo/wallet-wpkh-load-prompt-150.en.png deleted file mode 100644 index 708fd9bd9..000000000 Binary files a/docs/img/maixpy_amigo/wallet-wpkh-load-prompt-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/wallet-wpkh-load-prompt-300.en.png b/docs/img/maixpy_amigo/wallet-wpkh-load-prompt-300.en.png new file mode 100644 index 000000000..63f286fca Binary files /dev/null and b/docs/img/maixpy_amigo/wallet-wpkh-load-prompt-300.en.png differ diff --git a/docs/img/maixpy_amigo/wallet-wsh-load-prompt-300.en.png b/docs/img/maixpy_amigo/wallet-wsh-load-prompt-300.en.png new file mode 100644 index 000000000..9d457443a Binary files /dev/null and b/docs/img/maixpy_amigo/wallet-wsh-load-prompt-300.en.png differ diff --git a/docs/img/maixpy_amigo/wallet-wsh-load-prompt-fingerprints-150.en.png b/docs/img/maixpy_amigo/wallet-wsh-load-prompt-fingerprints-150.en.png deleted file mode 100644 index b18fded59..000000000 Binary files a/docs/img/maixpy_amigo/wallet-wsh-load-prompt-fingerprints-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/wallet-wsh-load-prompt-xpubs-150.en.png b/docs/img/maixpy_amigo/wallet-wsh-load-prompt-xpubs-150.en.png deleted file mode 100644 index 326696f93..000000000 Binary files a/docs/img/maixpy_amigo/wallet-wsh-load-prompt-xpubs-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/zoomed-qr-code-150.en.png b/docs/img/maixpy_amigo/zoomed-qr-code-150.en.png deleted file mode 100644 index ddcc4b9d0..000000000 Binary files a/docs/img/maixpy_amigo/zoomed-qr-code-150.en.png and /dev/null differ diff --git a/docs/img/maixpy_amigo/zoomed-qr-code-300.en.png b/docs/img/maixpy_amigo/zoomed-qr-code-300.en.png new file mode 100644 index 000000000..8dbef6af7 Binary files /dev/null and b/docs/img/maixpy_amigo/zoomed-qr-code-300.en.png differ diff --git a/docs/img/maixpy_cube/logo-200.en.png b/docs/img/maixpy_cube/logo-200.en.png deleted file mode 100644 index 423844762..000000000 Binary files a/docs/img/maixpy_cube/logo-200.en.png and /dev/null differ diff --git a/docs/img/maixpy_cube/logo-400.en.png b/docs/img/maixpy_cube/logo-400.en.png new file mode 100644 index 000000000..1bf115e56 Binary files /dev/null and b/docs/img/maixpy_cube/logo-400.en.png differ diff --git a/docs/img/maixpy_dock/logo-151.en.png b/docs/img/maixpy_dock/logo-151.en.png deleted file mode 100644 index dbba9ea68..000000000 Binary files a/docs/img/maixpy_dock/logo-151.en.png and /dev/null differ diff --git a/docs/img/maixpy_dock/logo-302.en.png b/docs/img/maixpy_dock/logo-302.en.png new file mode 100644 index 000000000..181ec569f Binary files /dev/null and b/docs/img/maixpy_dock/logo-302.en.png differ diff --git a/docs/img/maixpy_m5stickv/address-menu-125.en.png b/docs/img/maixpy_m5stickv/address-menu-125.en.png deleted file mode 100644 index 760d2ed96..000000000 Binary files a/docs/img/maixpy_m5stickv/address-menu-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/address-menu-250.en.png b/docs/img/maixpy_m5stickv/address-menu-250.en.png new file mode 100644 index 000000000..7afa4be3d Binary files /dev/null and b/docs/img/maixpy_m5stickv/address-menu-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/backup-compact-qr-125.en.png b/docs/img/maixpy_m5stickv/backup-compact-qr-125.en.png deleted file mode 100644 index 92522c5bf..000000000 Binary files a/docs/img/maixpy_m5stickv/backup-compact-qr-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/backup-compact-qr-250.en.png b/docs/img/maixpy_m5stickv/backup-compact-qr-250.en.png new file mode 100644 index 000000000..29a4756bc Binary files /dev/null and b/docs/img/maixpy_m5stickv/backup-compact-qr-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/backup-mnemonic-numbers-125.en.png b/docs/img/maixpy_m5stickv/backup-mnemonic-numbers-125.en.png deleted file mode 100644 index 2f0a12213..000000000 Binary files a/docs/img/maixpy_m5stickv/backup-mnemonic-numbers-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/backup-mnemonic-numbers-250.en.png b/docs/img/maixpy_m5stickv/backup-mnemonic-numbers-250.en.png new file mode 100644 index 000000000..1901cfb1c Binary files /dev/null and b/docs/img/maixpy_m5stickv/backup-mnemonic-numbers-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/backup-mnemonic-words-125.en.png b/docs/img/maixpy_m5stickv/backup-mnemonic-words-125.en.png deleted file mode 100644 index e7c948410..000000000 Binary files a/docs/img/maixpy_m5stickv/backup-mnemonic-words-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/backup-mnemonic-words-250.en.png b/docs/img/maixpy_m5stickv/backup-mnemonic-words-250.en.png new file mode 100644 index 000000000..c0103089a Binary files /dev/null and b/docs/img/maixpy_m5stickv/backup-mnemonic-words-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/backup-options-125.en.png b/docs/img/maixpy_m5stickv/backup-options-125.en.png deleted file mode 100644 index 6f479a933..000000000 Binary files a/docs/img/maixpy_m5stickv/backup-options-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/backup-options-250.en.png b/docs/img/maixpy_m5stickv/backup-options-250.en.png new file mode 100644 index 000000000..54b268c18 Binary files /dev/null and b/docs/img/maixpy_m5stickv/backup-options-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/backup-qr-plain-text-125.en.png b/docs/img/maixpy_m5stickv/backup-qr-plain-text-125.en.png deleted file mode 100644 index 03fc94595..000000000 Binary files a/docs/img/maixpy_m5stickv/backup-qr-plain-text-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/backup-qr-plain-text-250.en.png b/docs/img/maixpy_m5stickv/backup-qr-plain-text-250.en.png new file mode 100644 index 000000000..08a4d1fdc Binary files /dev/null and b/docs/img/maixpy_m5stickv/backup-qr-plain-text-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/backup-seed-qr-125.en.png b/docs/img/maixpy_m5stickv/backup-seed-qr-125.en.png deleted file mode 100644 index 8cfba2c81..000000000 Binary files a/docs/img/maixpy_m5stickv/backup-seed-qr-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/backup-seed-qr-250.en.png b/docs/img/maixpy_m5stickv/backup-seed-qr-250.en.png new file mode 100644 index 000000000..62e6a626b Binary files /dev/null and b/docs/img/maixpy_m5stickv/backup-seed-qr-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/backup-stackbit-125.en.png b/docs/img/maixpy_m5stickv/backup-stackbit-125.en.png deleted file mode 100644 index 93e4e4296..000000000 Binary files a/docs/img/maixpy_m5stickv/backup-stackbit-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/backup-stackbit-250.en.png b/docs/img/maixpy_m5stickv/backup-stackbit-250.en.png new file mode 100644 index 000000000..2676584b1 Binary files /dev/null and b/docs/img/maixpy_m5stickv/backup-stackbit-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/backup-tiny-seed-125.en.png b/docs/img/maixpy_m5stickv/backup-tiny-seed-125.en.png deleted file mode 100644 index 60c2887fa..000000000 Binary files a/docs/img/maixpy_m5stickv/backup-tiny-seed-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/backup-tiny-seed-250.en.png b/docs/img/maixpy_m5stickv/backup-tiny-seed-250.en.png new file mode 100644 index 000000000..ecc1d7873 Binary files /dev/null and b/docs/img/maixpy_m5stickv/backup-tiny-seed-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/bip85-child-index-125.en.png b/docs/img/maixpy_m5stickv/bip85-child-index-125.en.png deleted file mode 100644 index 2f609bce3..000000000 Binary files a/docs/img/maixpy_m5stickv/bip85-child-index-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/bip85-child-index-250.en.png b/docs/img/maixpy_m5stickv/bip85-child-index-250.en.png new file mode 100644 index 000000000..b7edd27e3 Binary files /dev/null and b/docs/img/maixpy_m5stickv/bip85-child-index-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/bip85-load-child-125.en.png b/docs/img/maixpy_m5stickv/bip85-load-child-125.en.png deleted file mode 100644 index ef5df2ef0..000000000 Binary files a/docs/img/maixpy_m5stickv/bip85-load-child-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/bip85-load-child-250.en.png b/docs/img/maixpy_m5stickv/bip85-load-child-250.en.png new file mode 100644 index 000000000..4001f7e05 Binary files /dev/null and b/docs/img/maixpy_m5stickv/bip85-load-child-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/check-sd-card-125.en.png b/docs/img/maixpy_m5stickv/check-sd-card-125.en.png deleted file mode 100644 index ba8ac1eb4..000000000 Binary files a/docs/img/maixpy_m5stickv/check-sd-card-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/check-sd-card-250.en.png b/docs/img/maixpy_m5stickv/check-sd-card-250.en.png new file mode 100644 index 000000000..d82ee49d6 Binary files /dev/null and b/docs/img/maixpy_m5stickv/check-sd-card-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/create-qr-code-125.en.png b/docs/img/maixpy_m5stickv/create-qr-code-125.en.png deleted file mode 100644 index 856fc85d4..000000000 Binary files a/docs/img/maixpy_m5stickv/create-qr-code-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/create-qr-code-250.en.png b/docs/img/maixpy_m5stickv/create-qr-code-250.en.png new file mode 100644 index 000000000..776c0bed4 Binary files /dev/null and b/docs/img/maixpy_m5stickv/create-qr-code-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/delete-mnemonic-125.en.png b/docs/img/maixpy_m5stickv/delete-mnemonic-125.en.png deleted file mode 100644 index 68795aece..000000000 Binary files a/docs/img/maixpy_m5stickv/delete-mnemonic-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/delete-mnemonic-250.en.png b/docs/img/maixpy_m5stickv/delete-mnemonic-250.en.png new file mode 100644 index 000000000..d9127a8d9 Binary files /dev/null and b/docs/img/maixpy_m5stickv/delete-mnemonic-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/descriptor-addresses-125.en.png b/docs/img/maixpy_m5stickv/descriptor-addresses-125.en.png deleted file mode 100644 index 4495e4e54..000000000 Binary files a/docs/img/maixpy_m5stickv/descriptor-addresses-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/descriptor-addresses-250.en.png b/docs/img/maixpy_m5stickv/descriptor-addresses-250.en.png new file mode 100644 index 000000000..d122db6bc Binary files /dev/null and b/docs/img/maixpy_m5stickv/descriptor-addresses-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/encryption-options-125.en.png b/docs/img/maixpy_m5stickv/encryption-options-125.en.png deleted file mode 100644 index ceaba82f8..000000000 Binary files a/docs/img/maixpy_m5stickv/encryption-options-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/encryption-options-250.en.png b/docs/img/maixpy_m5stickv/encryption-options-250.en.png new file mode 100644 index 000000000..d7b8b6020 Binary files /dev/null and b/docs/img/maixpy_m5stickv/encryption-options-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/encryption-options-mode-125.en.png b/docs/img/maixpy_m5stickv/encryption-options-mode-125.en.png deleted file mode 100644 index 2cb271c6c..000000000 Binary files a/docs/img/maixpy_m5stickv/encryption-options-mode-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/encryption-options-mode-250.en.png b/docs/img/maixpy_m5stickv/encryption-options-mode-250.en.png new file mode 100644 index 000000000..ef1d628ba Binary files /dev/null and b/docs/img/maixpy_m5stickv/encryption-options-mode-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/encryption-options-pbkdf2-125.en.png b/docs/img/maixpy_m5stickv/encryption-options-pbkdf2-125.en.png deleted file mode 100644 index c3fcefb61..000000000 Binary files a/docs/img/maixpy_m5stickv/encryption-options-pbkdf2-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/encryption-options-pbkdf2-250.en.png b/docs/img/maixpy_m5stickv/encryption-options-pbkdf2-250.en.png new file mode 100644 index 000000000..8f760f89c Binary files /dev/null and b/docs/img/maixpy_m5stickv/encryption-options-pbkdf2-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/erase-data-125.en.png b/docs/img/maixpy_m5stickv/erase-data-125.en.png deleted file mode 100644 index 23b95fbc4..000000000 Binary files a/docs/img/maixpy_m5stickv/erase-data-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/erase-data-250.en.png b/docs/img/maixpy_m5stickv/erase-data-250.en.png new file mode 100644 index 000000000..54ab07f4d Binary files /dev/null and b/docs/img/maixpy_m5stickv/erase-data-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/extended-public-key-menu-125.en.png b/docs/img/maixpy_m5stickv/extended-public-key-menu-125.en.png deleted file mode 100644 index 6207a92b7..000000000 Binary files a/docs/img/maixpy_m5stickv/extended-public-key-menu-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/extended-public-key-menu-250.en.png b/docs/img/maixpy_m5stickv/extended-public-key-menu-250.en.png new file mode 100644 index 000000000..92c1c53ff Binary files /dev/null and b/docs/img/maixpy_m5stickv/extended-public-key-menu-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/extended-public-key-selected-125.en.png b/docs/img/maixpy_m5stickv/extended-public-key-selected-125.en.png deleted file mode 100644 index b6d11bf7d..000000000 Binary files a/docs/img/maixpy_m5stickv/extended-public-key-selected-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/extended-public-key-selected-250.en.png b/docs/img/maixpy_m5stickv/extended-public-key-selected-250.en.png new file mode 100644 index 000000000..74b291af8 Binary files /dev/null and b/docs/img/maixpy_m5stickv/extended-public-key-selected-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/extended-public-key-wpkh-xpub-qr-125.en.png b/docs/img/maixpy_m5stickv/extended-public-key-wpkh-xpub-qr-125.en.png deleted file mode 100644 index 8791bbfc7..000000000 Binary files a/docs/img/maixpy_m5stickv/extended-public-key-wpkh-xpub-qr-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/extended-public-key-wpkh-xpub-qr-250.en.png b/docs/img/maixpy_m5stickv/extended-public-key-wpkh-xpub-qr-250.en.png new file mode 100644 index 000000000..4580d66ae Binary files /dev/null and b/docs/img/maixpy_m5stickv/extended-public-key-wpkh-xpub-qr-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/extended-public-key-wpkh-xpub-text-125.en.png b/docs/img/maixpy_m5stickv/extended-public-key-wpkh-xpub-text-125.en.png deleted file mode 100644 index ee582cc3a..000000000 Binary files a/docs/img/maixpy_m5stickv/extended-public-key-wpkh-xpub-text-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/extended-public-key-wpkh-xpub-text-250.en.png b/docs/img/maixpy_m5stickv/extended-public-key-wpkh-xpub-text-250.en.png new file mode 100644 index 000000000..a7020d99a Binary files /dev/null and b/docs/img/maixpy_m5stickv/extended-public-key-wpkh-xpub-text-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/extended-public-key-wpkh-zpub-qr-125.en.png b/docs/img/maixpy_m5stickv/extended-public-key-wpkh-zpub-qr-125.en.png deleted file mode 100644 index 133596645..000000000 Binary files a/docs/img/maixpy_m5stickv/extended-public-key-wpkh-zpub-qr-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/extended-public-key-wpkh-zpub-qr-250.en.png b/docs/img/maixpy_m5stickv/extended-public-key-wpkh-zpub-qr-250.en.png new file mode 100644 index 000000000..07842697f Binary files /dev/null and b/docs/img/maixpy_m5stickv/extended-public-key-wpkh-zpub-qr-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/extended-public-key-wsh-xpub-qr-125.en.png b/docs/img/maixpy_m5stickv/extended-public-key-wsh-xpub-qr-125.en.png deleted file mode 100644 index 8791bbfc7..000000000 Binary files a/docs/img/maixpy_m5stickv/extended-public-key-wsh-xpub-qr-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/extended-public-key-wsh-xpub-qr-250.en.png b/docs/img/maixpy_m5stickv/extended-public-key-wsh-xpub-qr-250.en.png new file mode 100644 index 000000000..4580d66ae Binary files /dev/null and b/docs/img/maixpy_m5stickv/extended-public-key-wsh-xpub-qr-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/extended-public-key-wsh-xpub-text-125.en.png b/docs/img/maixpy_m5stickv/extended-public-key-wsh-xpub-text-125.en.png deleted file mode 100644 index ee582cc3a..000000000 Binary files a/docs/img/maixpy_m5stickv/extended-public-key-wsh-xpub-text-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/extended-public-key-wsh-xpub-text-250.en.png b/docs/img/maixpy_m5stickv/extended-public-key-wsh-xpub-text-250.en.png new file mode 100644 index 000000000..a7020d99a Binary files /dev/null and b/docs/img/maixpy_m5stickv/extended-public-key-wsh-xpub-text-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/extended-public-key-xpub-qr-menu-selected-125.en.png b/docs/img/maixpy_m5stickv/extended-public-key-xpub-qr-menu-selected-125.en.png deleted file mode 100644 index cbde81251..000000000 Binary files a/docs/img/maixpy_m5stickv/extended-public-key-xpub-qr-menu-selected-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/extended-public-key-xpub-qr-menu-selected-250.en.png b/docs/img/maixpy_m5stickv/extended-public-key-xpub-qr-menu-selected-250.en.png new file mode 100644 index 000000000..d9799113e Binary files /dev/null and b/docs/img/maixpy_m5stickv/extended-public-key-xpub-qr-menu-selected-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/flash-map-125.en.png b/docs/img/maixpy_m5stickv/flash-map-125.en.png deleted file mode 100644 index 7cd217c50..000000000 Binary files a/docs/img/maixpy_m5stickv/flash-map-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/flash-map-250.en.png b/docs/img/maixpy_m5stickv/flash-map-250.en.png new file mode 100644 index 000000000..ca09062cd Binary files /dev/null and b/docs/img/maixpy_m5stickv/flash-map-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/flash-tools-125.en.png b/docs/img/maixpy_m5stickv/flash-tools-125.en.png deleted file mode 100644 index 62a2a052b..000000000 Binary files a/docs/img/maixpy_m5stickv/flash-tools-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/flash-tools-250.en.png b/docs/img/maixpy_m5stickv/flash-tools-250.en.png new file mode 100644 index 000000000..501bfa2b1 Binary files /dev/null and b/docs/img/maixpy_m5stickv/flash-tools-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/grided-qr-code-125.en.png b/docs/img/maixpy_m5stickv/grided-qr-code-125.en.png deleted file mode 100644 index f9cc5d183..000000000 Binary files a/docs/img/maixpy_m5stickv/grided-qr-code-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/grided-qr-code-250.en.png b/docs/img/maixpy_m5stickv/grided-qr-code-250.en.png new file mode 100644 index 000000000..5cde3e1b7 Binary files /dev/null and b/docs/img/maixpy_m5stickv/grided-qr-code-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/home-encrypt-options-125.en.png b/docs/img/maixpy_m5stickv/home-encrypt-options-125.en.png deleted file mode 100644 index 010eede00..000000000 Binary files a/docs/img/maixpy_m5stickv/home-encrypt-options-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/home-encrypt-options-250.en.png b/docs/img/maixpy_m5stickv/home-encrypt-options-250.en.png new file mode 100644 index 000000000..ac3cb9da6 Binary files /dev/null and b/docs/img/maixpy_m5stickv/home-encrypt-options-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/home-options-125.en.png b/docs/img/maixpy_m5stickv/home-options-125.en.png deleted file mode 100644 index d7f462984..000000000 Binary files a/docs/img/maixpy_m5stickv/home-options-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/home-options-250.en.png b/docs/img/maixpy_m5stickv/home-options-250.en.png new file mode 100644 index 000000000..7242d9bd3 Binary files /dev/null and b/docs/img/maixpy_m5stickv/home-options-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/lines-qr-code-125.en.png b/docs/img/maixpy_m5stickv/lines-qr-code-125.en.png deleted file mode 100644 index 999d717b8..000000000 Binary files a/docs/img/maixpy_m5stickv/lines-qr-code-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/lines-qr-code-250.en.png b/docs/img/maixpy_m5stickv/lines-qr-code-250.en.png new file mode 100644 index 000000000..012ac68b3 Binary files /dev/null and b/docs/img/maixpy_m5stickv/lines-qr-code-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/list-address-change-125.en.png b/docs/img/maixpy_m5stickv/list-address-change-125.en.png deleted file mode 100644 index 74923903e..000000000 Binary files a/docs/img/maixpy_m5stickv/list-address-change-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/list-address-change-250.en.png b/docs/img/maixpy_m5stickv/list-address-change-250.en.png new file mode 100644 index 000000000..2af5b3f2a Binary files /dev/null and b/docs/img/maixpy_m5stickv/list-address-change-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/list-address-receive-125.en.png b/docs/img/maixpy_m5stickv/list-address-receive-125.en.png deleted file mode 100644 index 0b356bbca..000000000 Binary files a/docs/img/maixpy_m5stickv/list-address-receive-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/list-address-receive-250.en.png b/docs/img/maixpy_m5stickv/list-address-receive-250.en.png new file mode 100644 index 000000000..b1cf018a8 Binary files /dev/null and b/docs/img/maixpy_m5stickv/list-address-receive-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-camera-options-125.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-camera-options-125.en.png deleted file mode 100644 index fd47cf038..000000000 Binary files a/docs/img/maixpy_m5stickv/load-mnemonic-camera-options-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-camera-options-250.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-camera-options-250.en.png new file mode 100644 index 000000000..a89e67cf4 Binary files /dev/null and b/docs/img/maixpy_m5stickv/load-mnemonic-camera-options-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-manual-options-125.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-manual-options-125.en.png deleted file mode 100644 index 2e3a98cd4..000000000 Binary files a/docs/img/maixpy_m5stickv/load-mnemonic-manual-options-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-manual-options-250.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-manual-options-250.en.png new file mode 100644 index 000000000..d7d4f65df Binary files /dev/null and b/docs/img/maixpy_m5stickv/load-mnemonic-manual-options-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-options-125.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-options-125.en.png deleted file mode 100644 index d04c2939b..000000000 Binary files a/docs/img/maixpy_m5stickv/load-mnemonic-options-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-options-250.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-options-250.en.png new file mode 100644 index 000000000..8fd99d00c Binary files /dev/null and b/docs/img/maixpy_m5stickv/load-mnemonic-options-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-seq-double-mnemonic-125.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-seq-double-mnemonic-125.en.png deleted file mode 100644 index acbd53f1b..000000000 Binary files a/docs/img/maixpy_m5stickv/load-mnemonic-seq-double-mnemonic-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-seq-double-mnemonic-250.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-seq-double-mnemonic-250.en.png new file mode 100644 index 000000000..5a61c3c27 Binary files /dev/null and b/docs/img/maixpy_m5stickv/load-mnemonic-seq-double-mnemonic-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-seq-mnemonic-125.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-seq-mnemonic-125.en.png deleted file mode 100644 index 6c09de296..000000000 Binary files a/docs/img/maixpy_m5stickv/load-mnemonic-seq-mnemonic-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-seq-mnemonic-250.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-seq-mnemonic-250.en.png new file mode 100644 index 000000000..d71f1e429 Binary files /dev/null and b/docs/img/maixpy_m5stickv/load-mnemonic-seq-mnemonic-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-seq-mnemonic-edited-wrong-125.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-seq-mnemonic-edited-wrong-125.en.png deleted file mode 100644 index 8c808d5a0..000000000 Binary files a/docs/img/maixpy_m5stickv/load-mnemonic-seq-mnemonic-edited-wrong-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-seq-mnemonic-edited-wrong-250.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-seq-mnemonic-edited-wrong-250.en.png new file mode 100644 index 000000000..ceee0edb1 Binary files /dev/null and b/docs/img/maixpy_m5stickv/load-mnemonic-seq-mnemonic-edited-wrong-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-seq-overview-125.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-seq-overview-125.en.png deleted file mode 100644 index 05dc38b80..000000000 Binary files a/docs/img/maixpy_m5stickv/load-mnemonic-seq-overview-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-seq-overview-250.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-seq-overview-250.en.png new file mode 100644 index 000000000..32a51e704 Binary files /dev/null and b/docs/img/maixpy_m5stickv/load-mnemonic-seq-overview-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-storage-options-125.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-storage-options-125.en.png deleted file mode 100644 index 68795aece..000000000 Binary files a/docs/img/maixpy_m5stickv/load-mnemonic-storage-options-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-storage-options-250.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-storage-options-250.en.png new file mode 100644 index 000000000..d9127a8d9 Binary files /dev/null and b/docs/img/maixpy_m5stickv/load-mnemonic-storage-options-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-via-numbers-word-125.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-via-numbers-word-125.en.png deleted file mode 100644 index d8aec5a9b..000000000 Binary files a/docs/img/maixpy_m5stickv/load-mnemonic-via-numbers-word-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-via-numbers-word-250.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-via-numbers-word-250.en.png new file mode 100644 index 000000000..dfba0fac2 Binary files /dev/null and b/docs/img/maixpy_m5stickv/load-mnemonic-via-numbers-word-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-via-stackbit-filled-125.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-via-stackbit-filled-125.en.png deleted file mode 100644 index 252089337..000000000 Binary files a/docs/img/maixpy_m5stickv/load-mnemonic-via-stackbit-filled-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-via-stackbit-filled-250.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-via-stackbit-filled-250.en.png new file mode 100644 index 000000000..6d54fd515 Binary files /dev/null and b/docs/img/maixpy_m5stickv/load-mnemonic-via-stackbit-filled-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-via-text-word-125.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-via-text-word-125.en.png deleted file mode 100644 index e7ef59a5e..000000000 Binary files a/docs/img/maixpy_m5stickv/load-mnemonic-via-text-word-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-via-text-word-250.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-via-text-word-250.en.png new file mode 100644 index 000000000..5abf8af69 Binary files /dev/null and b/docs/img/maixpy_m5stickv/load-mnemonic-via-text-word-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-via-tinyseed-filled-125.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-via-tinyseed-filled-125.en.png deleted file mode 100644 index c6c68b35b..000000000 Binary files a/docs/img/maixpy_m5stickv/load-mnemonic-via-tinyseed-filled-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/load-mnemonic-via-tinyseed-filled-250.en.png b/docs/img/maixpy_m5stickv/load-mnemonic-via-tinyseed-filled-250.en.png new file mode 100644 index 000000000..34a14d7ed Binary files /dev/null and b/docs/img/maixpy_m5stickv/load-mnemonic-via-tinyseed-filled-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/locale-options-125.en.png b/docs/img/maixpy_m5stickv/locale-options-125.en.png deleted file mode 100644 index ee796c0bc..000000000 Binary files a/docs/img/maixpy_m5stickv/locale-options-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/locale-options-250.en.png b/docs/img/maixpy_m5stickv/locale-options-250.en.png new file mode 100644 index 000000000..1067d33f6 Binary files /dev/null and b/docs/img/maixpy_m5stickv/locale-options-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/logo-125.en.png b/docs/img/maixpy_m5stickv/logo-125.en.png deleted file mode 100644 index ca57c9cef..000000000 Binary files a/docs/img/maixpy_m5stickv/logo-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/logo-250.en.png b/docs/img/maixpy_m5stickv/logo-250.en.png new file mode 100644 index 000000000..f332d927a Binary files /dev/null and b/docs/img/maixpy_m5stickv/logo-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/network-options-125.en.png b/docs/img/maixpy_m5stickv/network-options-125.en.png deleted file mode 100644 index 2a226e1e4..000000000 Binary files a/docs/img/maixpy_m5stickv/network-options-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/network-options-250.en.png b/docs/img/maixpy_m5stickv/network-options-250.en.png new file mode 100644 index 000000000..76e4c332a Binary files /dev/null and b/docs/img/maixpy_m5stickv/network-options-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-edited-125.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-edited-125.en.png deleted file mode 100644 index 2c83d95d8..000000000 Binary files a/docs/img/maixpy_m5stickv/new-mnemonic-edited-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-edited-250.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-edited-250.en.png new file mode 100644 index 000000000..863321bef Binary files /dev/null and b/docs/img/maixpy_m5stickv/new-mnemonic-edited-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-options-125.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-options-125.en.png deleted file mode 100644 index dcfb8773d..000000000 Binary files a/docs/img/maixpy_m5stickv/new-mnemonic-options-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-options-250.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-options-250.en.png new file mode 100644 index 000000000..e12f1bb92 Binary files /dev/null and b/docs/img/maixpy_m5stickv/new-mnemonic-options-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-last-n-rolls-125.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-last-n-rolls-125.en.png deleted file mode 100644 index 597d63647..000000000 Binary files a/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-last-n-rolls-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-last-n-rolls-250.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-last-n-rolls-250.en.png new file mode 100644 index 000000000..3cc416adb Binary files /dev/null and b/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-last-n-rolls-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-1-125.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-1-125.en.png deleted file mode 100644 index 7c30e115d..000000000 Binary files a/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-1-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-1-250.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-1-250.en.png new file mode 100644 index 000000000..6dd2b873b Binary files /dev/null and b/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-1-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-nerd-stats-125.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-nerd-stats-125.en.png deleted file mode 100644 index 0e7d09784..000000000 Binary files a/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-nerd-stats-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-nerd-stats-250.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-nerd-stats-250.en.png new file mode 100644 index 000000000..d5bb85d89 Binary files /dev/null and b/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-nerd-stats-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-sha256-125.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-sha256-125.en.png deleted file mode 100644 index 33a93d362..000000000 Binary files a/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-sha256-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-sha256-250.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-sha256-250.en.png new file mode 100644 index 000000000..dac965141 Binary files /dev/null and b/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-sha256-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-string-125.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-string-125.en.png deleted file mode 100644 index 2bc97c99a..000000000 Binary files a/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-string-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-string-250.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-string-250.en.png new file mode 100644 index 000000000..68ac75e4a Binary files /dev/null and b/docs/img/maixpy_m5stickv/new-mnemonic-via-d20-roll-string-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-last-n-rolls-125.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-last-n-rolls-125.en.png deleted file mode 100644 index 7f3dd9697..000000000 Binary files a/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-last-n-rolls-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-last-n-rolls-250.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-last-n-rolls-250.en.png new file mode 100644 index 000000000..6f8f2367c Binary files /dev/null and b/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-last-n-rolls-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-1-125.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-1-125.en.png deleted file mode 100644 index 382190591..000000000 Binary files a/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-1-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-1-250.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-1-250.en.png new file mode 100644 index 000000000..6b4f03d56 Binary files /dev/null and b/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-1-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-nerd-stats-125.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-nerd-stats-125.en.png deleted file mode 100644 index 52e32de83..000000000 Binary files a/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-nerd-stats-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-nerd-stats-250.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-nerd-stats-250.en.png new file mode 100644 index 000000000..d6febf772 Binary files /dev/null and b/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-nerd-stats-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-sha256-125.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-sha256-125.en.png deleted file mode 100644 index f6b39f094..000000000 Binary files a/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-sha256-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-sha256-250.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-sha256-250.en.png new file mode 100644 index 000000000..a849d25ae Binary files /dev/null and b/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-sha256-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-string-125.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-string-125.en.png deleted file mode 100644 index dd3653feb..000000000 Binary files a/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-string-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-string-250.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-string-250.en.png new file mode 100644 index 000000000..b667344ae Binary files /dev/null and b/docs/img/maixpy_m5stickv/new-mnemonic-via-d6-roll-string-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-capturing-125.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-capturing-125.en.png deleted file mode 100644 index 22204692e..000000000 Binary files a/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-capturing-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-capturing-250.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-capturing-250.en.png new file mode 100644 index 000000000..ce79ed0ba Binary files /dev/null and b/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-capturing-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-entropy-estimation-125.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-entropy-estimation-125.en.png deleted file mode 100644 index f2ec1a5c2..000000000 Binary files a/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-entropy-estimation-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-entropy-estimation-250.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-entropy-estimation-250.en.png new file mode 100644 index 000000000..422301af6 Binary files /dev/null and b/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-entropy-estimation-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-prompt-125.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-prompt-125.en.png deleted file mode 100644 index e36a481d8..000000000 Binary files a/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-prompt-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-prompt-250.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-prompt-250.en.png new file mode 100644 index 000000000..dd8619594 Binary files /dev/null and b/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-prompt-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-sha256-125.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-sha256-125.en.png deleted file mode 100644 index 23def258b..000000000 Binary files a/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-sha256-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-sha256-250.en.png b/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-sha256-250.en.png new file mode 100644 index 000000000..fcc79dbe1 Binary files /dev/null and b/docs/img/maixpy_m5stickv/new-mnemonic-via-snapshot-sha256-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/passphrase-load-options-125.en.png b/docs/img/maixpy_m5stickv/passphrase-load-options-125.en.png deleted file mode 100644 index 359601cd8..000000000 Binary files a/docs/img/maixpy_m5stickv/passphrase-load-options-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/passphrase-load-options-250.en.png b/docs/img/maixpy_m5stickv/passphrase-load-options-250.en.png new file mode 100644 index 000000000..72b4cb899 Binary files /dev/null and b/docs/img/maixpy_m5stickv/passphrase-load-options-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/persist-options-125.en.png b/docs/img/maixpy_m5stickv/persist-options-125.en.png deleted file mode 100644 index eda4772af..000000000 Binary files a/docs/img/maixpy_m5stickv/persist-options-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/persist-options-250.en.png b/docs/img/maixpy_m5stickv/persist-options-250.en.png new file mode 100644 index 000000000..4fa2b4df3 Binary files /dev/null and b/docs/img/maixpy_m5stickv/persist-options-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/print-qr-printing-125.en.png b/docs/img/maixpy_m5stickv/print-qr-printing-125.en.png deleted file mode 100644 index b4eebb9d9..000000000 Binary files a/docs/img/maixpy_m5stickv/print-qr-printing-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/print-qr-printing-250.en.png b/docs/img/maixpy_m5stickv/print-qr-printing-250.en.png new file mode 100644 index 000000000..dce753b9b Binary files /dev/null and b/docs/img/maixpy_m5stickv/print-qr-printing-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/print-qr-prompt-125.en.png b/docs/img/maixpy_m5stickv/print-qr-prompt-125.en.png deleted file mode 100644 index b8f6f18b6..000000000 Binary files a/docs/img/maixpy_m5stickv/print-qr-prompt-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/print-qr-prompt-250.en.png b/docs/img/maixpy_m5stickv/print-qr-prompt-250.en.png new file mode 100644 index 000000000..eae4fea40 Binary files /dev/null and b/docs/img/maixpy_m5stickv/print-qr-prompt-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/print-test-qr-125.en.png b/docs/img/maixpy_m5stickv/print-test-qr-125.en.png deleted file mode 100644 index 93a1cff5d..000000000 Binary files a/docs/img/maixpy_m5stickv/print-test-qr-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/print-test-qr-250.en.png b/docs/img/maixpy_m5stickv/print-test-qr-250.en.png new file mode 100644 index 000000000..bd5e9cf8d Binary files /dev/null and b/docs/img/maixpy_m5stickv/print-test-qr-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/printer-options-125.en.png b/docs/img/maixpy_m5stickv/printer-options-125.en.png deleted file mode 100644 index b7ec2d13d..000000000 Binary files a/docs/img/maixpy_m5stickv/printer-options-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/printer-options-250.en.png b/docs/img/maixpy_m5stickv/printer-options-250.en.png new file mode 100644 index 000000000..092a2c822 Binary files /dev/null and b/docs/img/maixpy_m5stickv/printer-options-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/regions-qr-code-125.en.png b/docs/img/maixpy_m5stickv/regions-qr-code-125.en.png deleted file mode 100644 index 8a210ba74..000000000 Binary files a/docs/img/maixpy_m5stickv/regions-qr-code-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/regions-qr-code-250.en.png b/docs/img/maixpy_m5stickv/regions-qr-code-250.en.png new file mode 100644 index 000000000..08ffc5790 Binary files /dev/null and b/docs/img/maixpy_m5stickv/regions-qr-code-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/scan-address-scanned-address-125.en.png b/docs/img/maixpy_m5stickv/scan-address-scanned-address-125.en.png deleted file mode 100644 index cd8d470c1..000000000 Binary files a/docs/img/maixpy_m5stickv/scan-address-scanned-address-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/scan-address-scanned-address-250.en.png b/docs/img/maixpy_m5stickv/scan-address-scanned-address-250.en.png new file mode 100644 index 000000000..3be3e6d72 Binary files /dev/null and b/docs/img/maixpy_m5stickv/scan-address-scanned-address-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/settings-options-125.en.png b/docs/img/maixpy_m5stickv/settings-options-125.en.png deleted file mode 100644 index fcc22aa49..000000000 Binary files a/docs/img/maixpy_m5stickv/settings-options-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/settings-options-250.en.png b/docs/img/maixpy_m5stickv/settings-options-250.en.png new file mode 100644 index 000000000..43f52c2d9 Binary files /dev/null and b/docs/img/maixpy_m5stickv/settings-options-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/settings-options-appearance-125.en.png b/docs/img/maixpy_m5stickv/settings-options-appearance-125.en.png deleted file mode 100644 index ba8606b59..000000000 Binary files a/docs/img/maixpy_m5stickv/settings-options-appearance-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/settings-options-appearance-250.en.png b/docs/img/maixpy_m5stickv/settings-options-appearance-250.en.png new file mode 100644 index 000000000..dd9036798 Binary files /dev/null and b/docs/img/maixpy_m5stickv/settings-options-appearance-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/settings-options-appearance-screensaver-125.en.png b/docs/img/maixpy_m5stickv/settings-options-appearance-screensaver-125.en.png deleted file mode 100644 index 90c95d5c6..000000000 Binary files a/docs/img/maixpy_m5stickv/settings-options-appearance-screensaver-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/settings-options-appearance-screensaver-250.en.png b/docs/img/maixpy_m5stickv/settings-options-appearance-screensaver-250.en.png new file mode 100644 index 000000000..c881cf9e1 Binary files /dev/null and b/docs/img/maixpy_m5stickv/settings-options-appearance-screensaver-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/settings-options-factory-settings-125.en.png b/docs/img/maixpy_m5stickv/settings-options-factory-settings-125.en.png deleted file mode 100644 index 0eb3751f6..000000000 Binary files a/docs/img/maixpy_m5stickv/settings-options-factory-settings-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/settings-options-factory-settings-250.en.png b/docs/img/maixpy_m5stickv/settings-options-factory-settings-250.en.png new file mode 100644 index 000000000..49715afbb Binary files /dev/null and b/docs/img/maixpy_m5stickv/settings-options-factory-settings-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/settings-options-hardware-125.en.png b/docs/img/maixpy_m5stickv/settings-options-hardware-125.en.png deleted file mode 100644 index b7ec2d13d..000000000 Binary files a/docs/img/maixpy_m5stickv/settings-options-hardware-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/settings-options-hardware-250.en.png b/docs/img/maixpy_m5stickv/settings-options-hardware-250.en.png new file mode 100644 index 000000000..3d6a2f5ab Binary files /dev/null and b/docs/img/maixpy_m5stickv/settings-options-hardware-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/settings-options-hardware-display-125.en.png b/docs/img/maixpy_m5stickv/settings-options-hardware-display-125.en.png deleted file mode 100644 index b7ec2d13d..000000000 Binary files a/docs/img/maixpy_m5stickv/settings-options-hardware-display-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/settings-options-hardware-display-250.en.png b/docs/img/maixpy_m5stickv/settings-options-hardware-display-250.en.png new file mode 100644 index 000000000..55bfd1ad5 Binary files /dev/null and b/docs/img/maixpy_m5stickv/settings-options-hardware-display-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/sign-message-at-address-prompt-125.en.png b/docs/img/maixpy_m5stickv/sign-message-at-address-prompt-125.en.png deleted file mode 100644 index d89950048..000000000 Binary files a/docs/img/maixpy_m5stickv/sign-message-at-address-prompt-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/sign-message-at-address-prompt-250.en.png b/docs/img/maixpy_m5stickv/sign-message-at-address-prompt-250.en.png new file mode 100644 index 000000000..adb88e370 Binary files /dev/null and b/docs/img/maixpy_m5stickv/sign-message-at-address-prompt-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/sign-message-sha256-sign-prompt-125.en.png b/docs/img/maixpy_m5stickv/sign-message-sha256-sign-prompt-125.en.png deleted file mode 100644 index 256e9d6b4..000000000 Binary files a/docs/img/maixpy_m5stickv/sign-message-sha256-sign-prompt-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/sign-message-sha256-sign-prompt-250.en.png b/docs/img/maixpy_m5stickv/sign-message-sha256-sign-prompt-250.en.png new file mode 100644 index 000000000..7b997a630 Binary files /dev/null and b/docs/img/maixpy_m5stickv/sign-message-sha256-sign-prompt-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/sign-options-125.en.png b/docs/img/maixpy_m5stickv/sign-options-125.en.png deleted file mode 100644 index e3eba5fe5..000000000 Binary files a/docs/img/maixpy_m5stickv/sign-options-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/sign-options-250.en.png b/docs/img/maixpy_m5stickv/sign-options-250.en.png new file mode 100644 index 000000000..e21e29339 Binary files /dev/null and b/docs/img/maixpy_m5stickv/sign-options-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/sign-psbt-sign-prompt-125.en.png b/docs/img/maixpy_m5stickv/sign-psbt-sign-prompt-125.en.png deleted file mode 100644 index 7a0b1e8b9..000000000 Binary files a/docs/img/maixpy_m5stickv/sign-psbt-sign-prompt-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/sign-psbt-sign-prompt-250.en.png b/docs/img/maixpy_m5stickv/sign-psbt-sign-prompt-250.en.png new file mode 100644 index 000000000..28b7fcac3 Binary files /dev/null and b/docs/img/maixpy_m5stickv/sign-psbt-sign-prompt-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/sign-psbt-signed-qr-125.en.png b/docs/img/maixpy_m5stickv/sign-psbt-signed-qr-125.en.png deleted file mode 100644 index 55c8c34a6..000000000 Binary files a/docs/img/maixpy_m5stickv/sign-psbt-signed-qr-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/sign-psbt-signed-qr-250.en.png b/docs/img/maixpy_m5stickv/sign-psbt-signed-qr-250.en.png new file mode 100644 index 000000000..37e43ccaa Binary files /dev/null and b/docs/img/maixpy_m5stickv/sign-psbt-signed-qr-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/standard-qr-code-125.en.png b/docs/img/maixpy_m5stickv/standard-qr-code-125.en.png deleted file mode 100644 index ab0b30b5c..000000000 Binary files a/docs/img/maixpy_m5stickv/standard-qr-code-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/standard-qr-code-250.en.png b/docs/img/maixpy_m5stickv/standard-qr-code-250.en.png new file mode 100644 index 000000000..a5d0d6c0c Binary files /dev/null and b/docs/img/maixpy_m5stickv/standard-qr-code-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/theme-1-125.en.png b/docs/img/maixpy_m5stickv/theme-1-125.en.png deleted file mode 100644 index cc28c0db3..000000000 Binary files a/docs/img/maixpy_m5stickv/theme-1-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/theme-1-250.en.png b/docs/img/maixpy_m5stickv/theme-1-250.en.png new file mode 100644 index 000000000..cfaa2d568 Binary files /dev/null and b/docs/img/maixpy_m5stickv/theme-1-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/theme-2-125.en.png b/docs/img/maixpy_m5stickv/theme-2-125.en.png deleted file mode 100644 index ba1f991c8..000000000 Binary files a/docs/img/maixpy_m5stickv/theme-2-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/theme-2-250.en.png b/docs/img/maixpy_m5stickv/theme-2-250.en.png new file mode 100644 index 000000000..aee1a9176 Binary files /dev/null and b/docs/img/maixpy_m5stickv/theme-2-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/theme-3-125.en.png b/docs/img/maixpy_m5stickv/theme-3-125.en.png deleted file mode 100644 index 2b94da418..000000000 Binary files a/docs/img/maixpy_m5stickv/theme-3-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/theme-3-250.en.png b/docs/img/maixpy_m5stickv/theme-3-250.en.png new file mode 100644 index 000000000..dc148ac72 Binary files /dev/null and b/docs/img/maixpy_m5stickv/theme-3-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/theme-4-125.en.png b/docs/img/maixpy_m5stickv/theme-4-125.en.png deleted file mode 100644 index 5eeead1f9..000000000 Binary files a/docs/img/maixpy_m5stickv/theme-4-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/theme-4-250.en.png b/docs/img/maixpy_m5stickv/theme-4-250.en.png new file mode 100644 index 000000000..2cd66eefa Binary files /dev/null and b/docs/img/maixpy_m5stickv/theme-4-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/theme-5-125.en.png b/docs/img/maixpy_m5stickv/theme-5-125.en.png deleted file mode 100644 index 4a157ebee..000000000 Binary files a/docs/img/maixpy_m5stickv/theme-5-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/theme-5-250.en.png b/docs/img/maixpy_m5stickv/theme-5-250.en.png new file mode 100644 index 000000000..e28ccb6ac Binary files /dev/null and b/docs/img/maixpy_m5stickv/theme-5-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/tools-options-125.en.png b/docs/img/maixpy_m5stickv/tools-options-125.en.png deleted file mode 100644 index 15ee81363..000000000 Binary files a/docs/img/maixpy_m5stickv/tools-options-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/tools-options-250.en.png b/docs/img/maixpy_m5stickv/tools-options-250.en.png new file mode 100644 index 000000000..1fd732f40 Binary files /dev/null and b/docs/img/maixpy_m5stickv/tools-options-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/touchscreen-125.en.png b/docs/img/maixpy_m5stickv/touchscreen-125.en.png deleted file mode 100644 index b7ec2d13d..000000000 Binary files a/docs/img/maixpy_m5stickv/touchscreen-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/touchscreen-250.en.png b/docs/img/maixpy_m5stickv/touchscreen-250.en.png new file mode 100644 index 000000000..c4e93d5f5 Binary files /dev/null and b/docs/img/maixpy_m5stickv/touchscreen-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/wallet-customization-options-125.en.png b/docs/img/maixpy_m5stickv/wallet-customization-options-125.en.png deleted file mode 100644 index f8035f857..000000000 Binary files a/docs/img/maixpy_m5stickv/wallet-customization-options-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/wallet-customization-options-250.en.png b/docs/img/maixpy_m5stickv/wallet-customization-options-250.en.png new file mode 100644 index 000000000..ce03498d4 Binary files /dev/null and b/docs/img/maixpy_m5stickv/wallet-customization-options-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/wallet-descriptor-tr-minis-1-250.en.png b/docs/img/maixpy_m5stickv/wallet-descriptor-tr-minis-1-250.en.png new file mode 100644 index 000000000..74ab9f4dc Binary files /dev/null and b/docs/img/maixpy_m5stickv/wallet-descriptor-tr-minis-1-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/wallet-descriptor-tr-minis-2-250.en.png b/docs/img/maixpy_m5stickv/wallet-descriptor-tr-minis-2-250.en.png new file mode 100644 index 000000000..ed01eee24 Binary files /dev/null and b/docs/img/maixpy_m5stickv/wallet-descriptor-tr-minis-2-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/wallet-descriptor-tr-minis-3-250.en.png b/docs/img/maixpy_m5stickv/wallet-descriptor-tr-minis-3-250.en.png new file mode 100644 index 000000000..b531935f6 Binary files /dev/null and b/docs/img/maixpy_m5stickv/wallet-descriptor-tr-minis-3-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/wallet-load-prompt-125.en.png b/docs/img/maixpy_m5stickv/wallet-load-prompt-125.en.png deleted file mode 100644 index eae3c5224..000000000 Binary files a/docs/img/maixpy_m5stickv/wallet-load-prompt-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/wallet-load-prompt-250.en.png b/docs/img/maixpy_m5stickv/wallet-load-prompt-250.en.png new file mode 100644 index 000000000..9d6ed8b77 Binary files /dev/null and b/docs/img/maixpy_m5stickv/wallet-load-prompt-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/wallet-wpkh-load-prompt-125.en.png b/docs/img/maixpy_m5stickv/wallet-wpkh-load-prompt-125.en.png deleted file mode 100644 index 25ebd3fe2..000000000 Binary files a/docs/img/maixpy_m5stickv/wallet-wpkh-load-prompt-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/wallet-wpkh-load-prompt-250.en.png b/docs/img/maixpy_m5stickv/wallet-wpkh-load-prompt-250.en.png new file mode 100644 index 000000000..879b3eb7b Binary files /dev/null and b/docs/img/maixpy_m5stickv/wallet-wpkh-load-prompt-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/wallet-wsh-load-prompt-250.en.png b/docs/img/maixpy_m5stickv/wallet-wsh-load-prompt-250.en.png new file mode 100644 index 000000000..f3b79c4cd Binary files /dev/null and b/docs/img/maixpy_m5stickv/wallet-wsh-load-prompt-250.en.png differ diff --git a/docs/img/maixpy_m5stickv/wallet-wsh-load-prompt-fingerprints-125.en.png b/docs/img/maixpy_m5stickv/wallet-wsh-load-prompt-fingerprints-125.en.png deleted file mode 100644 index 8f7f015f4..000000000 Binary files a/docs/img/maixpy_m5stickv/wallet-wsh-load-prompt-fingerprints-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/wallet-wsh-load-prompt-xpubs-125.en.png b/docs/img/maixpy_m5stickv/wallet-wsh-load-prompt-xpubs-125.en.png deleted file mode 100644 index 46ebc5f96..000000000 Binary files a/docs/img/maixpy_m5stickv/wallet-wsh-load-prompt-xpubs-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/zoomed-qr-code-125.en.png b/docs/img/maixpy_m5stickv/zoomed-qr-code-125.en.png deleted file mode 100644 index 74bb651fd..000000000 Binary files a/docs/img/maixpy_m5stickv/zoomed-qr-code-125.en.png and /dev/null differ diff --git a/docs/img/maixpy_m5stickv/zoomed-qr-code-250.en.png b/docs/img/maixpy_m5stickv/zoomed-qr-code-250.en.png new file mode 100644 index 000000000..157994cf8 Binary files /dev/null and b/docs/img/maixpy_m5stickv/zoomed-qr-code-250.en.png differ diff --git a/docs/img/maixpy_wonder_mv/logo-152.en.png b/docs/img/maixpy_wonder_mv/logo-152.en.png deleted file mode 100644 index d419d6c5f..000000000 Binary files a/docs/img/maixpy_wonder_mv/logo-152.en.png and /dev/null differ diff --git a/docs/img/maixpy_wonder_mv/logo-304.en.png b/docs/img/maixpy_wonder_mv/logo-304.en.png new file mode 100644 index 000000000..229a89e3a Binary files /dev/null and b/docs/img/maixpy_wonder_mv/logo-304.en.png differ diff --git a/docs/img/maixpy_yahboom/logo-156.en.png b/docs/img/maixpy_yahboom/logo-156.en.png deleted file mode 100644 index 1cbc6070e..000000000 Binary files a/docs/img/maixpy_yahboom/logo-156.en.png and /dev/null differ diff --git a/docs/img/maixpy_yahboom/logo-312.en.png b/docs/img/maixpy_yahboom/logo-312.en.png new file mode 100644 index 000000000..5fdf71abf Binary files /dev/null and b/docs/img/maixpy_yahboom/logo-312.en.png differ diff --git a/docs/index.en.md b/docs/index.en.md index 96d9ae2b1..0de501aa9 100644 --- a/docs/index.en.md +++ b/docs/index.en.md @@ -4,12 +4,12 @@ hide: - toc --- # Krux - - - - - - + + + + + + Krux is an open-source firmware that transforms off-the-shelf Kendryte K210 devices, such as the Maix Amigo, M5StickV and [more](parts.md), into versatile bitcoin transaction signers. Beyond its core functionality, Krux is a flexible platform that can adapt to devices with different form factors, providing a suite of tools to assist with the creation and recovery of mnemonic backups, some of which include encryption options for enhanced security. diff --git a/docs/parts.en.md b/docs/parts.en.md index ea5ba3bf8..7257d7305 100644 --- a/docs/parts.en.md +++ b/docs/parts.en.md @@ -39,7 +39,7 @@ Some stores ship the Maix Dock with soldered pin connectors that do not fit into ### M5StickV - + Below is a list of some distributors where you can find this device: @@ -56,7 +56,7 @@ Below is a list of some distributors where you can find this device:
### Maix Amigo - + Below is a list of some distributors where you can find this device: @@ -69,7 +69,7 @@ Below is a list of some distributors where you can find this device:
### Yahboom k210 module - + It comes with a compatible 32G card, an USB card reader, one PH2.0 4Pin male-to-male connector and one PH2.0 female adapter (to connect to a [thermal printer](#optional-ttl-serial-thermal-printer)). Below is a list of some distributors where you can find this device: @@ -81,7 +81,7 @@ It comes with a compatible 32G card, an USB card reader, one PH2.0 4Pin male-to-
### Maix Cube - + Below is a list of some distributors where you can find this device: @@ -95,7 +95,7 @@ Below is a list of some distributors where you can find this device:
### WonderMV - + It comes with a compatible 32G card, an USB card reader, and two Molex 51004 4-pin male-to-male cable (to connect to a [thermal printer](#optional-ttl-serial-thermal-printer)). Below is a list of some distributors where you can find this device: @@ -108,7 +108,7 @@ It comes with a compatible 32G card, an USB card reader, and two Molex 51004 4-p
### Maix Dock and Maix Bit - + For the DIYers, the Maix Dock and Maix Bit are also supported but will require sourcing the parts individually and building the device yourself. diff --git a/docs/snippets/flash-krux-logo.en.txt b/docs/snippets/flash-krux-logo.en.txt index 8d3056ccf..82012bfc9 100644 --- a/docs/snippets/flash-krux-logo.en.txt +++ b/docs/snippets/flash-krux-logo.en.txt @@ -1,7 +1,7 @@ When the flashing process completes, you should see the Krux logo: - - + + If it doesn't, try turning your device off and on by holding down the power button for six seconds. diff --git a/docs/snippets/sparrow/import-xpub-sparrow.en.txt b/docs/snippets/sparrow/import-xpub-sparrow.en.txt index 7aa385d10..1b1fb13d0 100644 --- a/docs/snippets/sparrow/import-xpub-sparrow.en.txt +++ b/docs/snippets/sparrow/import-xpub-sparrow.en.txt @@ -1,12 +1,12 @@ Now, you will need to import your public key. To do so, press the *Airgapped Hardware Wallet* button under *Keystores* section. On the screen that pops up, look for *Krux* option and click its :camera: *Scan...* button. - + On your Krux device, navigate to the *Extended Public Key* option in the main menu and click on **XPUB - QR Code** and show it to Sparrow. - - + + It should import the xpub and show a key tab under *Keystores* section: \ No newline at end of file diff --git a/docs/snippets/sparrow/load-wallet-output-sparrow.en.txt b/docs/snippets/sparrow/load-wallet-output-sparrow.en.txt index 7a75a7b07..819affa2f 100644 --- a/docs/snippets/sparrow/load-wallet-output-sparrow.en.txt +++ b/docs/snippets/sparrow/load-wallet-output-sparrow.en.txt @@ -1,6 +1,6 @@ In Krux, navigate to "Wallet" -> "Wallet Descriptor" and choose to load a descriptor from QR code or SD card. - - + + After the descriptor is loaded, Krux will present basic information about it: \ No newline at end of file diff --git a/docs/snippets/sparrow/send-coins-sparrow.en.txt b/docs/snippets/sparrow/send-coins-sparrow.en.txt index c8ef12212..b6b7b789b 100644 --- a/docs/snippets/sparrow/send-coins-sparrow.en.txt +++ b/docs/snippets/sparrow/send-coins-sparrow.en.txt @@ -13,12 +13,12 @@ On the next screen, click *Show QR* to make Sparrow display an animated QR code After scanning, Krux should display info about the transaction for you to confirm before signing. - - + + Once you have confirmed, Krux will begin animating a QR code of the signed transaction that you can scan into Sparrow. - - + + In Sparrow, click *Scan QR* and show it the QR. A progress bar will indicate how many parts of the QR have been read. \ No newline at end of file diff --git a/i18n/translations/de-DE.json b/i18n/translations/de-DE.json index ae76038c3..b7b2d3bef 100644 --- a/i18n/translations/de-DE.json +++ b/i18n/translations/de-DE.json @@ -17,8 +17,7 @@ "Additional entropy from camera required for AES-CBC mode": "Zusätzliche Entropie von der Kamera für den AES-CBC-Modus erforderlich", "Address": "Adresse", "Align camera and backup plate properly.": "Richte Kamera und Sicherungsplatte richtig aus.", - "Anti-glare disabled": "Blendschutz deaktiviert", - "Anti-glare enabled": "Blendschutz aktiviert", + "Anti-glare mode": "Blendschutzmodus", "Appearance": "Aussehen", "Are you sure?": "Bist Du sicher?", "BGR Colors": "BGR-Farben", @@ -58,6 +57,7 @@ "Decrypt?": "Entschlüsseln?", "Default Wallet": "Standard-Wallet", "Depth Per Pass": "Tiefe pro Durchgang", + "Derivation Path": "Derivation-Pfad", "Derive BIP85 entropy?": "BIP85-Entropie ableiten?", "Descriptor Addresses": "Deskriptor-Adressen", "Display": "Bildschirm", @@ -100,6 +100,7 @@ "Flash Map": "Flash-Karte", "Flash Tools": "Flash-Tools", "Flash filled with camera entropy": "Flash gefüllt mit Kameraentropie", + "Flipped Orientation": "Umgekehrte Ausrichtung", "Flipped X Coordinates": "Umgedrehte X-Koordinaten", "Flute Diameter": "Flötendurchmesser", "Free:": "Frei:", @@ -122,6 +123,7 @@ "Insufficient entropy!": "Unzureichende Entropie!", "Invalid Tamper Check Code": "Ungültiger Tamper Check Code", "Invalid address": "Ungültige Adresse", + "Invalid derivation path": "Ungültiger Derivation-Pfad", "Invalid mnemonic length": "Ungültige mnemonische Lange", "Invalid wallet:": "Ungültige Wallet:", "Invert": "Umkehren", @@ -198,6 +200,7 @@ "Proceed anyway?": "Trotzdem fortfahren?", "Proceed?": "Weiter?", "Processing..": "Wird bearbeitet..", + "Provably unspendable": "Nachweislich nicht ausgebbar", "QR Code": "QR-Code", "RX Pin": "RX Pin", "Reboot": "Neustart", @@ -253,8 +256,10 @@ "Single-sig": "Single-Sig", "Size:": "Größe:", "Some checks cannot be performed.": "Einige Schecks können nicht durchgeführt werden.", + "Some nodes are not hardened:": "Einige Knoten sind nicht gehärtet:", "Spend (%d):": "Ausgabe (%d):", "Spend:": "Ausgaben:", + "Standard mode": "Standardmodus", "Stats for Nerds": "Statistiken für Nerds", "Store on Flash": "Auf Flash speichern", "Store on SD Card": "Auf der SD-Karte speichern", @@ -262,6 +267,7 @@ "TC Flash Hash": "TC Flash-Hash", "TC Flash Hash at Boot": "TC Flash-Hash beim Start", "TOUCH or ENTER to capture": "TOUCH oder ENTER zum Erfassen", + "TR internal key": "TR interner Schlüssel", "TX Pin": "TX Pin", "Tamper Check Code": "Tamper Check Code", "Tamper check code set successfully": "Tamper Check Code erfolgreich gesetzt", @@ -303,6 +309,7 @@ "Word Numbers": "Wortnummern", "Words": "Wörter", "Yes": "Ja", + "Zoomed mode": "Zoommodus", "is a valid address!": "ist eine gültige Adresse!", "unknown": "unbekannt", "was NOT FOUND in the first %d addresses": "wurde in den ersten %d Adressen nicht gefunden" diff --git a/i18n/translations/es-MX.json b/i18n/translations/es-MX.json index 05998691d..54e245e12 100644 --- a/i18n/translations/es-MX.json +++ b/i18n/translations/es-MX.json @@ -17,8 +17,7 @@ "Additional entropy from camera required for AES-CBC mode": "Se requiere entropía adicional de la cámara para el modo AES-CBC", "Address": "Dirección", "Align camera and backup plate properly.": "Alinea la cámara y la placa de respaldo correctamente.", - "Anti-glare disabled": "Anti-reflejo desactivado", - "Anti-glare enabled": "Anti-reflejo habilitado", + "Anti-glare mode": "Modo antirreflejo", "Appearance": "Apariencia", "Are you sure?": "¿Estás seguro?", "BGR Colors": "Colores BGR", @@ -58,6 +57,7 @@ "Decrypt?": "¿Descifrar?", "Default Wallet": "Cartera Predeterminada", "Depth Per Pass": "Profundidad por Pasada", + "Derivation Path": "Ruta de derivación", "Derive BIP85 entropy?": "¿Derivar entropía BIP85?", "Descriptor Addresses": "Direcciones del descriptor", "Display": "Pantalla", @@ -100,6 +100,7 @@ "Flash Map": "Mapa Flash", "Flash Tools": "Flash Tools", "Flash filled with camera entropy": "Flash lleno de entropía de cámara", + "Flipped Orientation": "Orientación Invertida", "Flipped X Coordinates": "Coordenadas X Invertidas", "Flute Diameter": "Diámetro de la Flauta", "Free:": "Libre:", @@ -122,6 +123,7 @@ "Insufficient entropy!": "¡Entropía Insuficiente!", "Invalid Tamper Check Code": "Código de verificación no válido", "Invalid address": "Dirección inválida", + "Invalid derivation path": "Ruta de derivación no válida", "Invalid mnemonic length": "Longitud mnemónica no válida", "Invalid wallet:": "Cartera inválida:", "Invert": "Invertir", @@ -198,6 +200,7 @@ "Proceed anyway?": "¿Proceder de todas maneras?", "Proceed?": "¿Continuar?", "Processing..": "Procesando..", + "Provably unspendable": "No se puede gastar", "QR Code": "Código QR", "RX Pin": "RX Pin", "Reboot": "Reiniciar", @@ -253,8 +256,10 @@ "Single-sig": "Single-sig", "Size:": "Tamaño:", "Some checks cannot be performed.": "Algunas comprobaciones no se pueden realizar.", + "Some nodes are not hardened:": "Algunos nodos no están endurecidos:", "Spend (%d):": "Gastos (%d):", "Spend:": "Gasto:", + "Standard mode": "Modo estándar", "Stats for Nerds": "Estadísticas para Entendidos", "Store on Flash": "Almacenar en Flash", "Store on SD Card": "Almacenar en la Tarjeta SD", @@ -262,6 +267,7 @@ "TC Flash Hash": "TC Hash Flash", "TC Flash Hash at Boot": "TC Flash Hash al arranque", "TOUCH or ENTER to capture": "TOCA o ENTER para capturar", + "TR internal key": "Clave interna TR", "TX Pin": "TX Pin", "Tamper Check Code": "Código de verificación", "Tamper check code set successfully": "Código de verificación establecido con éxito", @@ -303,6 +309,7 @@ "Word Numbers": "Números de Palabra", "Words": "Palabras", "Yes": "Sí", + "Zoomed mode": "Modo ampliado", "is a valid address!": "es una dirección válida!", "unknown": "desconocido", "was NOT FOUND in the first %d addresses": "NO FUE ENCONTRADO en las primeras %d direcciones" diff --git a/i18n/translations/fr-FR.json b/i18n/translations/fr-FR.json index a1eb5acc5..5aed4a425 100644 --- a/i18n/translations/fr-FR.json +++ b/i18n/translations/fr-FR.json @@ -17,8 +17,7 @@ "Additional entropy from camera required for AES-CBC mode": "Entropie supplémentaire de la caméra requise pour le mode AES-CBC", "Address": "Adresse", "Align camera and backup plate properly.": "Alignez correctement la caméra et plaque de sauvegarde.", - "Anti-glare disabled": "Anti-éblouissement désactivé", - "Anti-glare enabled": "Anti-éblouissement activé", + "Anti-glare mode": "Mode anti-reflets", "Appearance": "Apparence", "Are you sure?": "Es-tu sûr ?", "BGR Colors": "Couleurs BGR", @@ -58,6 +57,7 @@ "Decrypt?": "Déchiffrer ?", "Default Wallet": "Portefeuille par défaut", "Depth Per Pass": "Profondeur par passage", + "Derivation Path": "Chemin de dérivation", "Derive BIP85 entropy?": "Dériver l'entropie BIP85 ?", "Descriptor Addresses": "Adresses du descripteur", "Display": "Affichage", @@ -100,6 +100,7 @@ "Flash Map": "Plan du Flash", "Flash Tools": "Outils Flash", "Flash filled with camera entropy": "Flash rempli par l'entropie de la caméra", + "Flipped Orientation": "Orientation inversée", "Flipped X Coordinates": "Coordonnées X inversées", "Flute Diameter": "Diamètre de flûte", "Free:": "Libre :", @@ -122,6 +123,7 @@ "Insufficient entropy!": "Entropie insuffisante !", "Invalid Tamper Check Code": "Code de non compromis non valide", "Invalid address": "Adresse invalide", + "Invalid derivation path": "Chemin de dérivation non valide", "Invalid mnemonic length": "Longueur mnémonique invalide", "Invalid wallet:": "Portefeuille invalide :", "Invert": "Inverser", @@ -198,6 +200,7 @@ "Proceed anyway?": "Procéder quand même ?", "Proceed?": "Procéder ?", "Processing..": "Traitement..", + "Provably unspendable": "Provably unspendable", "QR Code": "Code QR", "RX Pin": "RX Fiche", "Reboot": "Redémarrer", @@ -253,8 +256,10 @@ "Single-sig": "Clé unique", "Size:": "Capacité :", "Some checks cannot be performed.": "Certains vérifications ne peuvent pas être effectués.", + "Some nodes are not hardened:": "Certains nœuds ne sont pas durcis :", "Spend (%d):": "Dépense (%d) :", "Spend:": "Dépense :", + "Standard mode": "Mode standard", "Stats for Nerds": "Statistiques pour les geeks", "Store on Flash": "Stocker sur flash", "Store on SD Card": "Stocker sur la carte SD", @@ -262,6 +267,7 @@ "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "TC Flash Hash au démarrage", "TOUCH or ENTER to capture": "TOUCHEZ ou ENTRER pour capturer", + "TR internal key": "Clé interne TR", "TX Pin": "TX Fiche", "Tamper Check Code": "Code de non compromis", "Tamper check code set successfully": "Code de non compromis défini avec succès", @@ -303,6 +309,7 @@ "Word Numbers": "Numéros de mots", "Words": "Mots", "Yes": "Oui", + "Zoomed mode": "Mode zoomé", "is a valid address!": "Adresse non valide !", "unknown": "inconnu", "was NOT FOUND in the first %d addresses": "INTROUVABLE dans les %d premières adresses" diff --git a/i18n/translations/ja-JP.json b/i18n/translations/ja-JP.json index 124e8b373..83ecbfb55 100644 --- a/i18n/translations/ja-JP.json +++ b/i18n/translations/ja-JP.json @@ -17,8 +17,7 @@ "Additional entropy from camera required for AES-CBC mode": "AES-CBCモードにはカメラからの追加エントロピーが必要です", "Address": "アドレス", "Align camera and backup plate properly.": "カメラとバックプレートを正しく整列させてください.", - "Anti-glare disabled": "アンチグレアが無効", - "Anti-glare enabled": "アンチグレアが有効", + "Anti-glare mode": "アンチグレアモード", "Appearance": "外観", "Are you sure?": "よろしいですか?", "BGR Colors": "BGRカラー", @@ -58,6 +57,7 @@ "Decrypt?": "デクリプト?", "Default Wallet": "デフォルトの財布", "Depth Per Pass": "パスごとの深さ", + "Derivation Path": "導出パス", "Derive BIP85 entropy?": "BIP85エントロピーを導出しますか?", "Descriptor Addresses": "ディスクリプタアドレス", "Display": "ディスプレイ", @@ -100,6 +100,7 @@ "Flash Map": "フラッシュマップ", "Flash Tools": "Flashツール", "Flash filled with camera entropy": "カメラエントロピーで満たされたフラッシュ", + "Flipped Orientation": "翻转方向", "Flipped X Coordinates": "X座標が反転しました", "Flute Diameter": "フルートディアメーター", "Free:": "フリー:", @@ -122,6 +123,7 @@ "Insufficient entropy!": "不十分なエントロピー!", "Invalid Tamper Check Code": "無効な改ざんチェックコード", "Invalid address": "無効なアドレス", + "Invalid derivation path": "無効な導出パス", "Invalid mnemonic length": "無効なニーモニックの長さ", "Invalid wallet:": "無効なウォレット:", "Invert": "反転する", @@ -198,6 +200,7 @@ "Proceed anyway?": "そのまま進みますか?", "Proceed?": "進みますか?", "Processing..": "処理中..", + "Provably unspendable": "証明可能に使用不能", "QR Code": "QRコード", "RX Pin": "RXピン", "Reboot": "再起動", @@ -253,8 +256,10 @@ "Single-sig": "シングルサイン", "Size:": "サイズ:", "Some checks cannot be performed.": "一部のチェックを実行できません.", + "Some nodes are not hardened:": "一部のノードは硬化されていません:", "Spend (%d):": "支出(%d):", "Spend:": "支出:", + "Standard mode": "標準モード", "Stats for Nerds": "オタクのための統計", "Store on Flash": "フラッシュに保存する", "Store on SD Card": "SDカードに保存する", @@ -262,6 +267,7 @@ "TC Flash Hash": "TCフラッシュハッシュ", "TC Flash Hash at Boot": "起動時のTCフラッシュハッシュ", "TOUCH or ENTER to capture": "タッチまたはENTERでキャプチャする", + "TR internal key": "TR内部キー", "TX Pin": "TXピン", "Tamper Check Code": "改ざんチェックコード", "Tamper check code set successfully": "改ざんチェックコードが正常に設定されました", @@ -303,6 +309,7 @@ "Word Numbers": "単語番号", "Words": "単語", "Yes": "はい", + "Zoomed mode": "ズームモード", "is a valid address!": "有効なアドレスです!", "unknown": "不明", "was NOT FOUND in the first %d addresses": "最初の%dアドレスに見つかりませんでした" diff --git a/i18n/translations/ko-KR.json b/i18n/translations/ko-KR.json index 2ccd4f9ec..5a1f81614 100644 --- a/i18n/translations/ko-KR.json +++ b/i18n/translations/ko-KR.json @@ -17,8 +17,7 @@ "Additional entropy from camera required for AES-CBC mode": "AES-CBC 모드를 위해 카메라의 추가 엔트로피가 필요합니다", "Address": "주소", "Align camera and backup plate properly.": "카메라와 보조 플레이트를 올바르게 정렬하십시오.", - "Anti-glare disabled": "눈부심방지 비활성화", - "Anti-glare enabled": "눈부심방지 활성화", + "Anti-glare mode": "불너미 방지 모드", "Appearance": "디스플레이", "Are you sure?": "계속하시겠습니까?", "BGR Colors": "BGR 색상", @@ -58,6 +57,7 @@ "Decrypt?": "복호화하시겠습니까?", "Default Wallet": "지갑 기본설정", "Depth Per Pass": "Depth Per Pass", + "Derivation Path": "파생 경로", "Derive BIP85 entropy?": "BIP85 엔트로피를 유독하시겠습니까?", "Descriptor Addresses": "디스크립터 주소", "Display": "디스플레이", @@ -100,6 +100,7 @@ "Flash Map": "플래시 맵", "Flash Tools": "플래시 도구", "Flash filled with camera entropy": "카메라 엔트로피로 가득 찬 플래시", + "Flipped Orientation": "지옥 방향 바뀌기", "Flipped X Coordinates": "X 좌표 반전", "Flute Diameter": "플루트 직경", "Free:": "여유 공간:", @@ -122,6 +123,7 @@ "Insufficient entropy!": "엔트로피가 충분하지 않습니다!", "Invalid Tamper Check Code": "유효하지 않은 탬퍼 체크 코드", "Invalid address": "주소가 잘못되었습니다", + "Invalid derivation path": "잘못된 파생 경로", "Invalid mnemonic length": "니모닉 길이가 잘못되었습니다", "Invalid wallet:": "지갑이 잘못되었습니다:", "Invert": "반전", @@ -198,6 +200,7 @@ "Proceed anyway?": "계속하시겠습니까?", "Proceed?": "계속하시겠습니까?", "Processing..": "처리...", + "Provably unspendable": "충분히 사용할 수 없음", "QR Code": "QR 코드", "RX Pin": "RX 핀", "Reboot": "다시 반복", @@ -253,8 +256,10 @@ "Single-sig": "단일서명", "Size:": "크기:", "Some checks cannot be performed.": "일부 검사를 수행할 수 없습니다.", + "Some nodes are not hardened:": "일부 노드가 경화되지 않습니다:", "Spend (%d):": "Spend (%d):", "Spend:": "지출", + "Standard mode": "표준 모드", "Stats for Nerds": "전문가를 위한 통계", "Store on Flash": "플래시 메모리에 저장", "Store on SD Card": "SD카드에 저장", @@ -262,6 +267,7 @@ "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "부팅 시 플래시 탬퍼 확인 해시", "TOUCH or ENTER to capture": "터치하거나 엔터를 눌러 캡처하십시오", + "TR internal key": "TR 내부 키", "TX Pin": "TX 핀", "Tamper Check Code": "탬퍼 체크 코드", "Tamper check code set successfully": "탬퍼 검사 코드가 성공적으로 설정되었습니다", @@ -303,6 +309,7 @@ "Word Numbers": "단어 번호(1-2048)", "Words": "시드문구", "Yes": "예", + "Zoomed mode": "확대 모드", "is a valid address!": "는 유효한 주소입니다!", "unknown": "알 수 없음", "was NOT FOUND in the first %d addresses": "첫 번째 %d개의 주소에서 찾을 수 없습니다" diff --git a/i18n/translations/nl-NL.json b/i18n/translations/nl-NL.json index 2400de46b..285230fd0 100644 --- a/i18n/translations/nl-NL.json +++ b/i18n/translations/nl-NL.json @@ -17,8 +17,7 @@ "Additional entropy from camera required for AES-CBC mode": "Extra entropie van de camera vereist voor AES-CBC-modus", "Address": "Adres", "Align camera and backup plate properly.": "Richt de camera en back-upplaat op de juiste manier.", - "Anti-glare disabled": "Anti reflecterend uitgeschakeld", - "Anti-glare enabled": "Anti reflecterend ingeschakeld", + "Anti-glare mode": "Anti-verblindingsmodus", "Appearance": "Uiterlijk", "Are you sure?": "Weet je het zeker?", "BGR Colors": "BGR-kleuren", @@ -58,6 +57,7 @@ "Decrypt?": "Ontsleutelen?", "Default Wallet": "Standaard portemonnee", "Depth Per Pass": "Diepte per pas", + "Derivation Path": "Afleidingspad", "Derive BIP85 entropy?": "BIP85-entropie afleiden?", "Descriptor Addresses": "Descriptoradressen", "Display": "Weergave", @@ -100,6 +100,7 @@ "Flash Map": "Flash Map", "Flash Tools": "Flash Tools", "Flash filled with camera entropy": "Flash gevuld met camera-entropie", + "Flipped Orientation": "Omgekeerde oriëntering", "Flipped X Coordinates": "Geflipte X-coördinaten", "Flute Diameter": "Fluit diameter", "Free:": "Vrij:", @@ -122,6 +123,7 @@ "Insufficient entropy!": "Onvoldoende Entropie!", "Invalid Tamper Check Code": "Ongeldige sabotagecontrolecode", "Invalid address": "Ongeldig adres", + "Invalid derivation path": "Ongeldige afleidingspad", "Invalid mnemonic length": "Ongeldige geheugensteun lengte", "Invalid wallet:": "Ongeldige portemonnee:", "Invert": "Omkeren", @@ -198,6 +200,7 @@ "Proceed anyway?": "Toch doorgaan?", "Proceed?": "Doorgaan?", "Processing..": "Verwerken..", + "Provably unspendable": "Bewijsbaar niet besteedbaar", "QR Code": "QR code", "RX Pin": "RX pin", "Reboot": "Opnieuw opstarten", @@ -253,8 +256,10 @@ "Single-sig": "Enkele sleutel", "Size:": "Grootte:", "Some checks cannot be performed.": "Sommige controles kunnen niet worden uitgevoerd.", + "Some nodes are not hardened:": "Sommige knooppunten zijn niet gehard:", "Spend (%d):": "Uitgaven (%d):", "Spend:": "Uitgaven:", + "Standard mode": "Standaardmodus", "Stats for Nerds": "Statistieken voor nerds", "Store on Flash": "Opslaan op apparaat", "Store on SD Card": "Opslaan op SD kaart", @@ -262,6 +267,7 @@ "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "Hash Flash bij het opstarten", "TOUCH or ENTER to capture": "TIK of ENTER voor opname", + "TR internal key": "TR interne sleutel", "TX Pin": "TX pin", "Tamper Check Code": "Sabotagecontrolecode", "Tamper check code set successfully": "Sabotagecontrolecode succesvol ingesteld", @@ -303,6 +309,7 @@ "Word Numbers": "Woord nummers", "Words": "Woorden", "Yes": "Yes", + "Zoomed mode": "Ingezoomde modus", "is a valid address!": "is geen geldig adres", "unknown": "onbekend", "was NOT FOUND in the first %d addresses": "werd NIET GEVONDEN in de eerste %d adressen" diff --git a/i18n/translations/pt-BR.json b/i18n/translations/pt-BR.json index 73236695b..86bb2788a 100644 --- a/i18n/translations/pt-BR.json +++ b/i18n/translations/pt-BR.json @@ -17,8 +17,7 @@ "Additional entropy from camera required for AES-CBC mode": "Entropia adicional da câmera necessária para o modo AES-CBC", "Address": "Endereço", "Align camera and backup plate properly.": "Alinhe a câmera e a placa de backup corretamente.", - "Anti-glare disabled": "Antirreflexo desativado", - "Anti-glare enabled": "Antirreflexo ativado", + "Anti-glare mode": "Modo antirreflexo", "Appearance": "Aparência", "Are you sure?": "Tem certeza?", "BGR Colors": "Cores BGR", @@ -58,6 +57,7 @@ "Decrypt?": "Descriptografar?", "Default Wallet": "Carteira Padrão", "Depth Per Pass": "Profundidade da Passagem", + "Derivation Path": "Caminho de Derivação", "Derive BIP85 entropy?": "Derivar entropia BIP85?", "Descriptor Addresses": "Endereços do Descritor", "Display": "Display", @@ -100,6 +100,7 @@ "Flash Map": "Mapa da Flash", "Flash Tools": "Ferramentas da Flash", "Flash filled with camera entropy": "Flash preenchida com entropia da câmera", + "Flipped Orientation": "Orientação Invertida", "Flipped X Coordinates": "Coordenadas X invertidas", "Flute Diameter": "Diâmetro da Fresa", "Free:": "Livre:", @@ -122,6 +123,7 @@ "Insufficient entropy!": "Entropia insuficiente!", "Invalid Tamper Check Code": "Código de verificação inválido", "Invalid address": "Endereço inválido", + "Invalid derivation path": "Caminho de derivação inválido", "Invalid mnemonic length": "Comprimento de mnemônico inválido", "Invalid wallet:": "Carteira inválida:", "Invert": "Invertido", @@ -198,6 +200,7 @@ "Proceed anyway?": "Continuar mesmo assim?", "Proceed?": "Seguir?", "Processing..": "Processando..", + "Provably unspendable": "Não pode gastar", "QR Code": "Código QR", "RX Pin": "Pino RX", "Reboot": "Reiniciar", @@ -253,8 +256,10 @@ "Single-sig": "Single-sig", "Size:": "Total:", "Some checks cannot be performed.": "Algumas verificações não podem ser realizadas.", + "Some nodes are not hardened:": "Alguns nós não são hardened:", "Spend (%d):": "Gastos (%d):", "Spend:": "Gasto:", + "Standard mode": "Modo padrão", "Stats for Nerds": "Estatísticas para Nerds", "Store on Flash": "Armazene na Flash", "Store on SD Card": "Armazene no Cartão SD", @@ -262,6 +267,7 @@ "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "TC Hash Flash na Inicialização", "TOUCH or ENTER to capture": "TOQUE ou ENTER para capturar", + "TR internal key": "Chave interna TR", "TX Pin": "Pino TX", "Tamper Check Code": "Código de Verificação", "Tamper check code set successfully": "Código de verificação definido com sucesso", @@ -303,6 +309,7 @@ "Word Numbers": "Números das Palavras", "Words": "Palavras", "Yes": "Sim", + "Zoomed mode": "Modo ampliado", "is a valid address!": "é um endereço válido!", "unknown": "desconhecida", "was NOT FOUND in the first %d addresses": "NÃO FOI ENCONTRADO nos primeiros %d endereços" diff --git a/i18n/translations/ru-RU.json b/i18n/translations/ru-RU.json index e9f78351f..d3f99c502 100644 --- a/i18n/translations/ru-RU.json +++ b/i18n/translations/ru-RU.json @@ -17,8 +17,7 @@ "Additional entropy from camera required for AES-CBC mode": "Для режима AES-CBC требуется дополнительная энтропия от камеры", "Address": "Адрес", "Align camera and backup plate properly.": "Правильно совместите камеру и резервную пластину.", - "Anti-glare disabled": "Антиблик отключен", - "Anti-glare enabled": "Антиблик включен", + "Anti-glare mode": "Антибликовый режим", "Appearance": "Внешний Вид", "Are you sure?": "Вы уверены?", "BGR Colors": "Цвета BGR", @@ -58,6 +57,7 @@ "Decrypt?": "Расшифровать?", "Default Wallet": "Кошелек по умолчанию", "Depth Per Pass": "Глубина за Проход", + "Derivation Path": "Путь деривации", "Derive BIP85 entropy?": "Вывести энтропию BIP85?", "Descriptor Addresses": "Адреса дескрипторов", "Display": "Дисплеи", @@ -100,6 +100,7 @@ "Flash Map": "Карта флэша", "Flash Tools": "Flash Tools", "Flash filled with camera entropy": "Флэш заполнен энтропией камеры", + "Flipped Orientation": "Перевернутая Oриентация", "Flipped X Coordinates": "Перевернутые координаты X", "Flute Diameter": "Диаметр Флюта", "Free:": "Свободно:", @@ -122,6 +123,7 @@ "Insufficient entropy!": "Недостаточная Энтропия!", "Invalid Tamper Check Code": "Недействительный код проверки вскрытия", "Invalid address": "Неверный адрес", + "Invalid derivation path": "Недопустимый путь деривации", "Invalid mnemonic length": "Неверная длина мнемоники", "Invalid wallet:": "Неверный кошелек:", "Invert": "Инвертировать", @@ -198,6 +200,7 @@ "Proceed anyway?": "Все равно продолжить?", "Proceed?": "Продолжить?", "Processing..": "Обработка..", + "Provably unspendable": "Доказуемо непотратимый", "QR Code": "QR Код", "RX Pin": "RX Пин", "Reboot": "Перезагрузить", @@ -253,8 +256,10 @@ "Single-sig": "Одна подпись", "Size:": "Размер:", "Some checks cannot be performed.": "Некоторые проверки не могут быть выполнены.", + "Some nodes are not hardened:": "Некоторые узлы не укреплены:", "Spend (%d):": "Расход (%d):", "Spend:": "Расход:", + "Standard mode": "Стандартный режим", "Stats for Nerds": "Статистика для Гиков", "Store on Flash": "Сохранить на Флэш Память", "Store on SD Card": "Сохранить на SD Карту", @@ -262,6 +267,7 @@ "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "Проверка хэша Flash при загрузке", "TOUCH or ENTER to capture": "ПРИКОСНИТЕСЬ или нажмите ВВОД, чтобы захватить", + "TR internal key": "внутренний ключ", "TX Pin": "TX Пин", "Tamper Check Code": "Код проверки вскрытия", "Tamper check code set successfully": "Код проверки вскрытия успешно установлен", @@ -303,6 +309,7 @@ "Word Numbers": "Числа Слов", "Words": "Слова", "Yes": "Да", + "Zoomed mode": "Режим масштабирования", "is a valid address!": "некорректный адрес", "unknown": "неизвестный", "was NOT FOUND in the first %d addresses": "нЕ НАЙДЕНО в первых %d адресах" diff --git a/i18n/translations/tr-TR.json b/i18n/translations/tr-TR.json index 25104988b..760e05fcf 100644 --- a/i18n/translations/tr-TR.json +++ b/i18n/translations/tr-TR.json @@ -17,8 +17,7 @@ "Additional entropy from camera required for AES-CBC mode": "AES-CBC modu için kameradan ek entropi gereklidir", "Address": "Adres", "Align camera and backup plate properly.": "Kamerayı ve yedek plakay'ı düzgün bir şekilde hizalayın.", - "Anti-glare disabled": "Parlama önleyici devre dışı", - "Anti-glare enabled": "Parlama önleyici etkin", + "Anti-glare mode": "Parlama Önleyici Mod", "Appearance": "Görünüm", "Are you sure?": "Emin misiniz?", "BGR Colors": "BGR Renkleri", @@ -58,6 +57,7 @@ "Decrypt?": "Şifre çözülsün mü?", "Default Wallet": "Varsayılan Cüzdan", "Depth Per Pass": "Geçiş Başına Derinlik", + "Derivation Path": "Türetim Yolu", "Derive BIP85 entropy?": "BIP85 entropisi türetilsin mi?", "Descriptor Addresses": "Tanımlayıcı Adresler", "Display": "Ekran", @@ -100,6 +100,7 @@ "Flash Map": "Flash Haritası", "Flash Tools": "Flash Araçları", "Flash filled with camera entropy": "Flash kamera entropisi ile dolduruldu", + "Flipped Orientation": "Ters Çevrilmiş Yönlendirme", "Flipped X Coordinates": "X Koordinatları Tersine Çevrildi", "Flute Diameter": "Flute Çapı", "Free:": "Boş:", @@ -122,6 +123,7 @@ "Insufficient entropy!": "Yetersiz entropi!", "Invalid Tamper Check Code": "Geçersiz Kurcalama Kontrol Kodu", "Invalid address": "Geçersiz adres", + "Invalid derivation path": "Geçersiz türetim yolu", "Invalid mnemonic length": "Geçersiz mnemonic uzunluğu", "Invalid wallet:": "Geçersiz cüzdan:", "Invert": "Ters Çevir", @@ -198,6 +200,7 @@ "Proceed anyway?": "Yine de devam edilsin mi?", "Proceed?": "Devam edilsin mi?", "Processing..": "İşleniyor..", + "Provably unspendable": "Kanıtlanabilir harcanamaz", "QR Code": "QR Kodu", "RX Pin": "RX Pini", "Reboot": "Yeniden Başlat", @@ -253,8 +256,10 @@ "Single-sig": "Tek-imza", "Size:": "Boyut:", "Some checks cannot be performed.": "Bazı kontroller yerine getirilemedi.", + "Some nodes are not hardened:": "Bazı düğümler sertleştirilmemiş:", "Spend (%d):": "Harcama (%d):", "Spend:": "Harcama:", + "Standard mode": "Standart Mod", "Stats for Nerds": "İnekler İçin İstatistikler", "Store on Flash": "Flash'ta Sakla", "Store on SD Card": "SD Kartta Sakla", @@ -262,6 +267,7 @@ "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "Önyüklemede TC Flash Hash", "TOUCH or ENTER to capture": "Yakalamak için DOKUN veya GİR", + "TR internal key": "TR iç anahtar", "TX Pin": "TX Pini", "Tamper Check Code": "Kurcalama Kontrol Kodu", "Tamper check code set successfully": "Kurcalama kontrol kodu başarıyla ayarlandı", @@ -303,6 +309,7 @@ "Word Numbers": "Kelime Numaraları", "Words": "Kelimeler", "Yes": "Evet", + "Zoomed mode": "Zoom Mod", "is a valid address!": "geçerli bir adres!", "unknown": "bilinmiyor", "was NOT FOUND in the first %d addresses": "ilk %d adreste BULUNAMADI" diff --git a/i18n/translations/vi-VN.json b/i18n/translations/vi-VN.json index bee1fec41..0cbdd119a 100644 --- a/i18n/translations/vi-VN.json +++ b/i18n/translations/vi-VN.json @@ -17,8 +17,7 @@ "Additional entropy from camera required for AES-CBC mode": "Cần thêm entropy từ camera cho chế độ AES-CBC", "Address": "Địa chỉ", "Align camera and backup plate properly.": "Căn chỉnh camera và tấm dự phòng đúng cách.", - "Anti-glare disabled": "Chống lóa bị vô hiệu hóa", - "Anti-glare enabled": "Đã bật chống lóa", + "Anti-glare mode": "Chế độ chống lóa", "Appearance": "Giao diện", "Are you sure?": "Bạn có chắc không?", "BGR Colors": "Màu BGR", @@ -58,6 +57,7 @@ "Decrypt?": "Giải mã?", "Default Wallet": "Ví mặc định", "Depth Per Pass": "Độ sâu mỗi lần cắt CNC", + "Derivation Path": "Đường dẫn phái sinh", "Derive BIP85 entropy?": "Suy ra entropy BIP85?", "Descriptor Addresses": "Địa chỉ người mô tả", "Display": "Hiển thị", @@ -100,6 +100,7 @@ "Flash Map": "Bản đồ Flash", "Flash Tools": "Công cụ Flash", "Flash filled with camera entropy": "Đèn flash chứa đầy entropy của máy ảnh", + "Flipped Orientation": "Hướng lật", "Flipped X Coordinates": "Tọa độ X bị lật", "Flute Diameter": "Đường kính mũi cắt CNC", "Free:": "Khả dụng:", @@ -122,6 +123,7 @@ "Insufficient entropy!": "Entropy không đủ!", "Invalid Tamper Check Code": "Mã kiểm tra giả mạo không hợp lệ", "Invalid address": "Địa chỉ không hợp lệ", + "Invalid derivation path": "Đường dẫn phái sinh không hợp lệ", "Invalid mnemonic length": "Độ dài mã Mnemonic không hợp lệ", "Invalid wallet:": "Ví không hợp lệ:", "Invert": "Đảo ngược", @@ -198,6 +200,7 @@ "Proceed anyway?": "Vẫn tiếp tục?", "Proceed?": "Thực hiện?", "Processing..": "Đang xử lý..", + "Provably unspendable": "Chắc chắn không thể chi tiêu", "QR Code": "Mã QR", "RX Pin": "RX Pin", "Reboot": "Khởi động lại", @@ -253,8 +256,10 @@ "Single-sig": "Khóa đơn", "Size:": "Dung lượng:", "Some checks cannot be performed.": "Một số kiểm tra không thể được thực hiện.", + "Some nodes are not hardened:": "Một số nút không được làm cứng:", "Spend (%d):": "Chi tiêu (%d):", "Spend:": "Chi tiêu:", + "Standard mode": "Chế độ Tiêu chuẩn", "Stats for Nerds": "Số liệu thống kê cho Mọt sách", "Store on Flash": "Lưu trữ trên flash", "Store on SD Card": "Lưu trữ trên thẻ SD", @@ -262,6 +267,7 @@ "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "Hash Flash TC khi khởi động", "TOUCH or ENTER to capture": "Chạm màn hình hoặc nhấn nút ENTER để chụp", + "TR internal key": "Khóa nội bộ TR", "TX Pin": "TX Pin", "Tamper Check Code": "Mã kiểm tra giả mạo", "Tamper check code set successfully": "Đã đặt mã kiểm tra giả mạo thành công", @@ -303,6 +309,7 @@ "Word Numbers": "Từ số", "Words": "Từ ngữ", "Yes": "Đúng", + "Zoomed mode": "Chế độ thu phóng", "is a valid address!": "là một địa chỉ hợp lệ!", "unknown": "không rõ", "was NOT FOUND in the first %d addresses": "kHÔNG TÌM THẤY trong %d địa chỉ đầu tiên" diff --git a/i18n/translations/zh-CN.json b/i18n/translations/zh-CN.json index 0ed7b9ae8..c4d63a922 100644 --- a/i18n/translations/zh-CN.json +++ b/i18n/translations/zh-CN.json @@ -17,8 +17,7 @@ "Additional entropy from camera required for AES-CBC mode": "AES-CBC 模式需要相机的额外熵", "Address": "地址", "Align camera and backup plate properly.": "正确对齐摄像头和背板。", - "Anti-glare disabled": "防眩光已禁用", - "Anti-glare enabled": "防眩光已启用", + "Anti-glare mode": "防闪模式", "Appearance": "界面", "Are you sure?": "确定?", "BGR Colors": "BGR 颜色", @@ -58,6 +57,7 @@ "Decrypt?": "解密?", "Default Wallet": "默认钱包", "Depth Per Pass": "每次通过的深度", + "Derivation Path": "源路径", "Derive BIP85 entropy?": "导出BIP85熵?", "Descriptor Addresses": "描述符地址", "Display": "显示", @@ -100,6 +100,7 @@ "Flash Map": "Flash地图", "Flash Tools": "Flash工具", "Flash filled with camera entropy": "Flash已用摄像头熵填充", + "Flipped Orientation": "翻转方向", "Flipped X Coordinates": "翻转 X 坐标", "Flute Diameter": "刀具直径", "Free:": "空闲:", @@ -122,6 +123,7 @@ "Insufficient entropy!": "熵不足!", "Invalid Tamper Check Code": "无效的防篡改检查码", "Invalid address": "无效地址", + "Invalid derivation path": "无效的源路径", "Invalid mnemonic length": "助记词长度无效", "Invalid wallet:": "无效钱包:", "Invert": "反转", @@ -198,6 +200,7 @@ "Proceed anyway?": "继续吗?", "Proceed?": "继续?", "Processing..": "正在处理..", + "Provably unspendable": "可证明不可使用", "QR Code": "二维码", "RX Pin": "RX 引脚", "Reboot": "重启", @@ -253,8 +256,10 @@ "Single-sig": "单签", "Size:": "大小:", "Some checks cannot be performed.": "无法执行某些检查。", + "Some nodes are not hardened:": "有些节点未硬化:", "Spend (%d):": "花费 (%d):", "Spend:": "花费", + "Standard mode": "标准模式", "Stats for Nerds": "极客统计数据", "Store on Flash": "存储到 Flash", "Store on SD Card": "存储到 SD 卡", @@ -262,6 +267,7 @@ "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "启动时的 TC Flash Hash", "TOUCH or ENTER to capture": "点击或按下 ENTER 截图", + "TR internal key": "TR内部密钥", "TX Pin": "TX 引脚", "Tamper Check Code": "防篡改检查码", "Tamper check code set successfully": "防篡改检查码设置成功", @@ -303,6 +309,7 @@ "Word Numbers": "单词序号", "Words": "单词", "Yes": "是", + "Zoomed mode": "放大模式", "is a valid address!": " 非有效地址", "unknown": "未知", "was NOT FOUND in the first %d addresses": "在前 %d 个地址中未找到" diff --git a/mkdocs.yml b/mkdocs.yml index d9894579a..b8322b9df 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -51,7 +51,7 @@ edit_uri: edit/main/docs docs_dir: docs site_dir: public extra: - latest_krux: krux-v25.01.beta4 + latest_krux: krux-v25.01.beta12 latest_installer: v0.0.20-beta latest_installer_rpm: krux-installer-0.0.20_beta-1.x86_64.rpm latest_installer_deb: krux-installer_0.0.20-beta_amd64.deb diff --git a/pyproject.toml b/pyproject.toml index f2e229ec1..8ad8b623a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ [tool.poetry] name = "krux" -version = "25.01.beta4" +version = "25.01.beta12" description = "Open-source signing device firmware for Bitcoin" authors = ["Jeff S "] diff --git a/simulator/generate-device-screenshots.sh b/simulator/generate-device-screenshots.sh index 0750a68a9..50cea6197 100755 --- a/simulator/generate-device-screenshots.sh +++ b/simulator/generate-device-screenshots.sh @@ -46,8 +46,8 @@ poetry run poe simulator --sequence sequences/logo.txt --device $device poetry run poe simulator --sequence sequences/load-mnemonic-options.txt --sd --device $device poetry run poe simulator --sequence sequences/new-mnemonic-options.txt --sd --device $device poetry run poe simulator --sequence sequences/load-mnemonic-sequence.txt --sd --device $device -poetry run poe simulator --sequence sequences/edit-mnemonic.txt --sd --device $device poetry run poe simulator --sequence sequences/load-mnemonic-double-mnemonic.txt --sd --device $device +poetry run poe simulator --sequence sequences/edit-mnemonic.txt --sd --device $device # Home poetry run poe simulator --sequence sequences/home-options.txt --device $device @@ -56,6 +56,7 @@ poetry run poe simulator --sequence sequences/extended-public-key-wpkh.txt --de poetry run poe simulator --sequence sequences/extended-public-key-wsh.txt --device $device poetry run poe simulator --sequence sequences/wallet-descriptor-wsh.txt --device $device poetry run poe simulator --sequence sequences/wallet-descriptor-wpkh.txt --device $device +poetry run poe simulator --sequence sequences/wallet-descriptor-exp-tr-minis.txt --device $device poetry run poe simulator --sequence sequences/bip85.txt --device $device poetry run poe simulator --sequence sequences/scan-address.txt --device $device poetry run poe simulator --sequence sequences/list-address.txt --device $device @@ -74,11 +75,6 @@ poetry run poe simulator --sequence sequences/tools-flash.txt --sd --device $de # Settings poetry run poe simulator --sequence sequences/all-settings.txt --device $device - # Other poetry run poe simulator --sequence sequences/qr-transcript.txt --device $device poetry run poe simulator --sequence sequences/print-qr.txt --sd --printer --device $device - - - - diff --git a/simulator/sequences/all-settings.txt b/simulator/sequences/all-settings.txt index 3ca251dcf..8098b9ded 100644 --- a/simulator/sequences/all-settings.txt +++ b/simulator/sequences/all-settings.txt @@ -5,14 +5,12 @@ press BUTTON_B press BUTTON_B press BUTTON_A -# Wait message -wait 2.5 - screenshot settings-options.png -# Navigate to Bitcoin +# Navigate to Default Wallet press BUTTON_A -press BUTTON_B + +# Network press BUTTON_A screenshot network-options.png @@ -21,7 +19,7 @@ screenshot network-options.png # Return press BUTTON_A -press BUTTON_B +press BUTTON_C press BUTTON_A # Navigate to Encryption @@ -52,23 +50,26 @@ press BUTTON_A screenshot settings-options-hardware.png -# Amigo only - Display -press_amigo_only BUTTON_A +# Display +press BUTTON_B +press BUTTON_A screenshot settings-options-hardware-display.png +# Amigo only - Display press_amigo_only BUTTON_C -press_amigo_only BUTTON_A +press BUTTON_A + # Move to printer -x2 press_amigo_only BUTTON_B -press_amigo_only BUTTON_A +press BUTTON_B +press BUTTON_A screenshot printer-options.png # Return to hardware -press_amigo_only BUTTON_C -press_amigo_only BUTTON_A +press BUTTON_C +press BUTTON_A # Move to touchscreen press_amigo_only BUTTON_B @@ -81,8 +82,7 @@ x2 press_amigo_only BUTTON_C press_amigo_only BUTTON_A # Return to settings -press_amigo_only BUTTON_B -press_m5stickv_only BUTTON_C +press BUTTON_B press BUTTON_A # Navigate to Language diff --git a/simulator/sequences/new-mnemonic-options.txt b/simulator/sequences/new-mnemonic-options.txt index 7546a3a76..89ee6609c 100644 --- a/simulator/sequences/new-mnemonic-options.txt +++ b/simulator/sequences/new-mnemonic-options.txt @@ -16,6 +16,8 @@ screenshot new-mnemonic-via-snapshot-prompt.png press BUTTON_A +qrcode arara.png +qrcode arara.png qrcode arara.png screenshot new-mnemonic-via-snapshot-capturing.png diff --git a/simulator/sequences/print-qr.txt b/simulator/sequences/print-qr.txt index 0e02e6e5c..c268ccc34 100644 --- a/simulator/sequences/print-qr.txt +++ b/simulator/sequences/print-qr.txt @@ -1,7 +1,11 @@ include _load-12-word-mnemonic.txt # Navigate to Mnemonic Plain Text QR print promt -x5 press BUTTON_A +press BUTTON_A +press BUTTON_A +press BUTTON_A +press BUTTON_A +press BUTTON_A wait 0.1 screenshot print-qr-prompt.png diff --git a/simulator/sequences/qrcodes/liana-expanding-tr-descriptor.png b/simulator/sequences/qrcodes/liana-expanding-tr-descriptor.png new file mode 100644 index 000000000..90998afc0 Binary files /dev/null and b/simulator/sequences/qrcodes/liana-expanding-tr-descriptor.png differ diff --git a/simulator/sequences/set-to-miniscript-testnet.txt b/simulator/sequences/set-to-miniscript-testnet.txt new file mode 100644 index 000000000..5dec84555 --- /dev/null +++ b/simulator/sequences/set-to-miniscript-testnet.txt @@ -0,0 +1,27 @@ +# Navigate to wallet +x2 press BUTTON_B +press BUTTON_A + +# Customize +x2 press BUTTON_B +x2 press BUTTON_A + +# Miniscript Testnet +press BUTTON_A +press BUTTON_B +press BUTTON_A +press BUTTON_B +press BUTTON_A +x2 press BUTTON_B +press BUTTON_A + +# Taproot +press BUTTON_B +press BUTTON_A + +# Return to initial state +press BUTTON_C +press BUTTON_A +x2 press BUTTON_B +press BUTTON_A +x2 press BUTTON_C \ No newline at end of file diff --git a/simulator/sequences/tools-mnemonic.txt b/simulator/sequences/tools-mnemonic.txt index 98bde570a..d9fd6c873 100644 --- a/simulator/sequences/tools-mnemonic.txt +++ b/simulator/sequences/tools-mnemonic.txt @@ -5,7 +5,7 @@ x3 press BUTTON_B press BUTTON_A # Delete Mnemonic -x4 press BUTTON_B +x5 press BUTTON_B press BUTTON_A screenshot delete-mnemonic.png diff --git a/simulator/sequences/wallet-descriptor-exp-tr-minis.txt b/simulator/sequences/wallet-descriptor-exp-tr-minis.txt new file mode 100644 index 000000000..ce787b742 --- /dev/null +++ b/simulator/sequences/wallet-descriptor-exp-tr-minis.txt @@ -0,0 +1,23 @@ +include _load-12-word-mnemonic.txt + +include set-to-miniscript-testnet.txt + +# Navigate to Wallet Descriptor +x2 press BUTTON_B +x2 press BUTTON_A + +#Load from QR code +x2 press BUTTON_A + +qrcode liana-expanding-tr-descriptor.png +wait 0.1 + +screenshot wallet-descriptor-tr-minis-1.png + +press BUTTON_A + +screenshot wallet-descriptor-tr-minis-2.png + +press BUTTON_A + +screenshot wallet-descriptor-tr-minis-3.png diff --git a/simulator/sequences/wallet-descriptor-wsh.txt b/simulator/sequences/wallet-descriptor-wsh.txt index 75d55e81c..fee9483d7 100644 --- a/simulator/sequences/wallet-descriptor-wsh.txt +++ b/simulator/sequences/wallet-descriptor-wsh.txt @@ -28,9 +28,5 @@ wait 0.1 qrcode specter-desktop-multisig-wallet-p8of8.png wait 0.1 -screenshot wallet-wsh-load-prompt-fingerprints.png - -press BUTTON_A - -screenshot wallet-wsh-load-prompt-xpubs.png +screenshot wallet-wsh-load-prompt.png diff --git a/simulator/simulator.py b/simulator/simulator.py index a1a729ab4..eccd8038b 100644 --- a/simulator/simulator.py +++ b/simulator/simulator.py @@ -148,12 +148,12 @@ def run_krux(): device_image = devices.load_image(args.device) # Scale screenshots for docs -AMIGO_SIZE = (150, 252) -M5STICKV_SIZE = (125, 247) -DOCK_SIZE = (151, 258) -YAHBOOM_SIZE = (156, 220) -CUBE_SIZE = (200, 212) -WONDER_MV_SIZE = (152, 220) +AMIGO_SIZE = (300, 504) +M5STICKV_SIZE = (250, 494) +DOCK_SIZE = (302, 516) +YAHBOOM_SIZE = (312, 440) +CUBE_SIZE = (400, 424) +WONDER_MV_SIZE = (304, 440) # Handle screenshots scale and alpha bg # When exporting the mask from GIMP uncheck "Save info about transparent pixels color" diff --git a/src/krux/bip39.py b/src/krux/bip39.py index 4c797fe8a..ba2ffe05f 100644 --- a/src/krux/bip39.py +++ b/src/krux/bip39.py @@ -7,8 +7,19 @@ WORDINDEX = {word: i for i, word in enumerate(WORDLIST)} -def mnemonic_to_bytes(mnemonic: str, ignore_checksum: bool = False, wordlist=WORDLIST): - """Verifies the mnemonic checksum and returns it in bytes""" +def entropy_checksum(entropy: bytes, checksum_length_bits: int = 4): + """ + Computes checksum for the given entropy + """ + h = hashlib.sha256(entropy).digest() + return int(h[0]) >> (8 - checksum_length_bits) + + +def k_mnemonic_bytes(mnemonic: str, ignore_checksum: bool = False, wordlist=WORDLIST): + """ + Verifies the mnemonic checksum and returns it in bytes + Equivalent to embit.bip39.mnemonic_to_bytes + """ words = mnemonic.strip().split() if len(words) % 3 != 0 or not 12 <= len(words) <= 24: raise ValueError("Invalid recovery phrase") @@ -29,17 +40,21 @@ def mnemonic_to_bytes(mnemonic: str, ignore_checksum: bool = False, wordlist=WOR checksum = accumulator & (2**checksum_length_bits - 1) accumulator >>= checksum_length_bits data = accumulator.to_bytes(entropy_length_bits // 8, "big") - computed_checksum = hashlib.sha256(data).digest()[0] >> 8 - checksum_length_bits + if ignore_checksum: + return data - if not ignore_checksum and checksum != computed_checksum: + if checksum != entropy_checksum(data, checksum_length_bits): raise ValueError("Checksum verification failed") return data -def mnemonic_is_valid(mnemonic: str, wordlist=WORDLIST): - """Checks if mnemonic is valid (checksum and words)""" +def k_mnemonic_is_valid(mnemonic: str, wordlist=WORDLIST): + """ + Checks if mnemonic is valid (checksum and words) + Equivalent to embit.bip39.mnemonic_is_valid + """ try: - mnemonic_to_bytes(mnemonic, wordlist=wordlist) + k_mnemonic_bytes(mnemonic, wordlist=wordlist) return True except: return False diff --git a/src/krux/camera.py b/src/krux/camera.py index 3f94329f4..aeacc1e76 100644 --- a/src/krux/camera.py +++ b/src/krux/camera.py @@ -24,6 +24,7 @@ import gc import sensor import board +from .krux_settings import Settings OV2640_ID = 0x2642 # Lenses, vertical flip - Bit OV5642_ID = 0x5642 # Lenses, horizontal flip - Bit @@ -35,39 +36,44 @@ ANTI_GLARE_MODE = 1 ENTROPY_MODE = 2 BINARY_GRID_MODE = 3 +ZOOMED_MODE = 4 + +OV2640Z_OFFSET_X = 160 +OV2640Z_OFFSET_Y = 120 +OV2640Z_MAX_X = 400 // 4 +OV2640Z_MAX_Y = 360 // 4 +OV2640Z_WIDTH = OV2640Z_MAX_X - OV2640Z_OFFSET_X // 4 +OV2640Z_HEIGHT = OV2640Z_MAX_Y - OV2640Z_OFFSET_Y // 4 + +# Luminosity threshold values for each camera and mode +LUM_TH = { + # flat dictionary using composite keys (camera_id, mode) + (OV2640_ID, QR_SCAN_MODE): (0x60, 0x70), + (OV2640_ID, ANTI_GLARE_MODE): (0x20, 0x28), + (OV2640_ID, ENTROPY_MODE): (0x68, 0x78), + (OV2640_ID, BINARY_GRID_MODE): (0x44, 0x48), + (OV2640_ID, ZOOMED_MODE): (0x35, 0x50), + (OV7740_ID, QR_SCAN_MODE): (0x60, 0x70), + (OV7740_ID, ANTI_GLARE_MODE): (0x20, 0x28), + (OV7740_ID, ENTROPY_MODE): (0x68, 0x78), + (OV7740_ID, BINARY_GRID_MODE): (0x44, 0x48), + (OV7740_ID, ZOOMED_MODE): (0x35, 0x50), + (GC2145_ID, QR_SCAN_MODE): (0x30, 0x55), + (GC2145_ID, ANTI_GLARE_MODE): (0x25, 0x40), + (GC2145_ID, ENTROPY_MODE): (0x20, 0xF2), + (GC2145_ID, BINARY_GRID_MODE): (0x30, 0x50), + (GC2145_ID, ZOOMED_MODE): (0x27, 0x45), + (GC0328_ID, QR_SCAN_MODE): 0x70, + (GC0328_ID, ANTI_GLARE_MODE): 0x40, + (GC0328_ID, ENTROPY_MODE): 0x80, + (GC0328_ID, BINARY_GRID_MODE): 0x70, + (GC0328_ID, ZOOMED_MODE): 0x55, +} class Camera: """Camera is a singleton interface for interacting with the device's camera""" - # Luminosity thresholds for each camera and mode - lum_th = { - OV2640_ID: { - QR_SCAN_MODE: (0x60, 0x70), - ANTI_GLARE_MODE: (0x20, 0x28), - ENTROPY_MODE: (0x68, 0x78), - BINARY_GRID_MODE: (0x44, 0x48), - }, - OV7740_ID: { - QR_SCAN_MODE: (0x60, 0x70), - ANTI_GLARE_MODE: (0x20, 0x28), - ENTROPY_MODE: (0x68, 0x78), - BINARY_GRID_MODE: (0x44, 0x48), - }, - GC2145_ID: { - QR_SCAN_MODE: (0x30, 0x55), - ANTI_GLARE_MODE: (0x25, 0x40), - ENTROPY_MODE: (0x20, 0xF2), - BINARY_GRID_MODE: (0x30, 0x50), - }, - GC0328_ID: { - QR_SCAN_MODE: 0x70, - ANTI_GLARE_MODE: 0x40, - ENTROPY_MODE: 0x80, - BINARY_GRID_MODE: 0x70, - }, - } - def __init__(self): self.cam_id = None self.mode = None @@ -81,7 +87,11 @@ def initialize_sensor(self, mode=QR_SCAN_MODE): """Initializes the camera""" sensor.reset(freq=18200000) self.cam_id = sensor.get_id() - if board.config["type"] == "cube": + if board.config["type"] == "cube" or ( + board.config["type"] in ["yahboom", "wonder_mv"] + and hasattr(Settings().hardware, "display") + and getattr(Settings().hardware.display, "flipped_orientation", False) + ): # Rotate camera 180 degrees on Cube sensor.set_hmirror(1) sensor.set_vflip(1) @@ -155,56 +165,184 @@ def has_antiglare(self): return self.cam_id in (OV7740_ID, OV2640_ID, GC2145_ID, GC0328_ID) def luminosity_threshold(self): - """Set luminosity thresholds for cameras""" - - if self.cam_id == GC0328_ID: - target = self.lum_th.get(self.cam_id, {}).get(self.mode, 0x80) - # Set register bank 1 - sensor.__write_reg(0xFE, 0x01) - # Expected luminance level, default=0x50 - sensor.__write_reg(0x13, target) + """Configures the luminosity thresholds for the camera""" + config_map = { + GC0328_ID: self._config_gc0328_lum, + OV2640_ID: self._config_ovxx40_lum, + OV7740_ID: self._config_ovxx40_lum, # Same as OV2640 + GC2145_ID: self._config_gc2145_lum, + } + + config_func = config_map.get(self.cam_id) + if config_func: + config_func() + sensor.skip_frames() + + def _config_gc0328_lum(self): + key = (self.cam_id, self.mode) + target = LUM_TH.get(key, 0x80) # Default value if key not found + # Set register bank 1 + sensor.__write_reg(0xFE, 0x01) + # Expected luminance level, default=0x50 + sensor.__write_reg(0x13, target) + + def _config_ovxx40_lum(self): + key = (self.cam_id, self.mode) + thresholds = LUM_TH.get(key, (0, 0)) # Default to (0, 0) if key not found + low, high = thresholds + + if low < 0x10 or high > 0xF0: return - (low, high) = self.lum_th.get(self.cam_id, {}).get(self.mode, (0, 0)) + # luminance high level, default=0x78 + sensor.__write_reg(0x24, high) + # luminance low level, default=0x68 + sensor.__write_reg(0x25, low) + vpt_low = (low - 0x10) >> 4 + vpt_high = (high + 0x10) >> 4 + vpt = (vpt_high << 4) | vpt_low + # VPT - fast convergence zone, default=0xD4 + sensor.__write_reg(0x26, vpt) + if self.mode in (QR_SCAN_MODE, ANTI_GLARE_MODE): + # Disable frame integration (bad for animated QR codes) + sensor.__write_reg(0x15, 0x00) + + def _config_gc2145_lum(self): + key = (self.cam_id, self.mode) + thresholds = LUM_TH.get(key, (0, 0)) # Default to (0, 0) if key not found + low, high = thresholds + if low < 0x10 or high > 0xF0: return - if self.cam_id == OV2640_ID: - # Set register bank 1 - sensor.__write_reg(0xFF, 0x01) - if self.cam_id in (OV7740_ID, OV2640_ID): - # luminance high level, default=0x78 - sensor.__write_reg(0x24, high) - # luminance low level, default=0x68 - sensor.__write_reg(0x25, low) - vpt_low = (low - 0x10) >> 4 - vpt_high = (high + 0x10) >> 4 - vpt = (vpt_high << 4) | vpt_low - # VPT - fast convergence zone, default=0xD4 - sensor.__write_reg(0x26, vpt) - if self.mode in (QR_SCAN_MODE, ANTI_GLARE_MODE): - # Disable frame integration (bad for animated QR codes) - sensor.__write_reg(0x15, 0x00) # pylint: disable=W0212 - elif self.cam_id == GC2145_ID: - # Set register bank 1 - sensor.__write_reg(0xFE, 0x01) - # luminance high level, default=0xF2 - sensor.__write_reg(0x0E, high) - # luminance low level, default=0x20 - sensor.__write_reg(0x0F, low) - # Expected luminance level, default=0x50 - sensor.__write_reg(0x13, (low + high) // 2) - sensor.skip_frames() + # Set register bank 1 + sensor.__write_reg(0xFE, 0x01) + # luminance high level, default=0xF2 + sensor.__write_reg(0x0E, high) + # luminance low level, default=0x20 + sensor.__write_reg(0x0F, low) + # Expected luminance level, default=0x50 + sensor.__write_reg(0x13, (low + high) // 2) + + def _gc2145_crop(self): + sensor.run(0) + sensor.set_framesize(sensor.VGA) + sensor.set_windowing((0, 0, 240, 240)) + # Set register bank 0 + sensor.__write_reg(0xFE, 0x00) + # Crop enable + sensor.__write_reg(0x90, 0x01) + # Crop window + # Registers: X[10:8]=0x93, X[7:0]=0x94, Y[10:8]=0x91, Y[7:0]=0x92 + # X_HEIGHT[10:8]=0x95, X_HEIGHT[7:0]=0x96, Y_WIDTH[10:8]=0x97, Y_WIDTH[7:0]=0x98 + sensor.__write_reg(0x93, 0x01) + sensor.__write_reg(0x94, 0x00) + sensor.__write_reg(0x91, 0x00) + sensor.__write_reg(0x92, 0xC0) + # 240x240 + sensor.__write_reg(0x95, 0x00) + sensor.__write_reg(0x96, 0xF0) + sensor.__write_reg(0x97, 0x00) + sensor.__write_reg(0x98, 0xF0) + + sensor.run(1) + + def _gc0328_crop(self): + sensor.run(0) + sensor.set_framesize(sensor.VGA) + sensor.set_windowing((0, 0, 240, 240)) + # Setting registers to crop GC0328 are the same as GC2145 but starting from 0x50 + # Set register bank 0 + sensor.__write_reg(0xFE, 0x00) + # Crop enable + sensor.__write_reg(0x50, 0x01) + # Crop window + # Registers: X[10:8]=0x53, X[7:0]=0x54, Y[10:8]=0x51, Y[7:0]=0x52 + # X_HEIGHT[10:8]=0x55, X_HEIGHT[7:0]=0x56, Y_WIDTH[10:8]=0x57, Y_WIDTH[7:0]=0x58 + sensor.__write_reg(0x53, 0x00) + sensor.__write_reg(0x54, 0xD0) + sensor.__write_reg(0x51, 0x00) + sensor.__write_reg(0x52, 0x90) + # 240x240 + sensor.__write_reg(0x55, 0x00) + sensor.__write_reg(0x56, 0xF0) + sensor.__write_reg(0x57, 0x00) + sensor.__write_reg(0x58, 0xF0) + + sensor.run(1) - def toggle_antiglare(self): + def _ov2640_crop(self): + sensor.run(0) + sensor.set_framesize(sensor.VGA) + sensor.set_windowing((0, 0, 240, 240)) + + # Prepare register list + win_regs = ( + (0xFF, 0x00), + (0x51, OV2640Z_MAX_X & 0xFF), + (0x52, OV2640Z_MAX_Y & 0xFF), + (0x53, OV2640Z_OFFSET_X & 0xFF), + (0x54, OV2640Z_OFFSET_Y & 0xFF), + ( + 0x55, + ((OV2640Z_MAX_Y >> 1) & 0x80) # bit 7 from (OV2640Z_MAX_Y >> 1) + | ( + (OV2640Z_OFFSET_Y >> 4) & 0x70 + ) # bits 6:4 from (OV2640Z_OFFSET_Y >> 4) + | ((OV2640Z_MAX_X >> 5) & 0x08) # bit 3 from (OV2640Z_MAX_X >> 5) + | ( + (OV2640Z_OFFSET_X >> 8) & 0x07 + ), # bits 2:0 from (OV2640Z_OFFSET_X >> 8) + ), + (0x57, (OV2640Z_MAX_X >> 2) & 0x80), # bit 7 from (OV2640Z_MAX_X >> 2) + (0x5A, OV2640Z_WIDTH & 0xFF), + (0x5B, OV2640Z_HEIGHT & 0xFF), + ( + 0x5C, + ((OV2640Z_HEIGHT >> 6) & 0x04) # bit 2 from (h >> 6) + | ((OV2640Z_WIDTH >> 8) & 0x03), # bits 1:0 from (w >> 8) + ), + ) + + # Write each register to the sensor + for reg, val in win_regs: + sensor.__write_reg(reg, val) + + sensor.run(1) + + def zoom_mode(self): + """Zooms in the camera to the center of the image""" + if self.cam_id == GC2145_ID: + self._gc2145_crop() + elif self.cam_id == GC0328_ID: + self._gc0328_crop() + elif self.cam_id == OV7740_ID: + sensor.__write_reg(0xD5, 0x00) + elif self.cam_id == OV2640_ID: + self._ov2640_crop() + + def toggle_camera_mode(self): """Toggles anti-glare mode and returns the new state""" if self.mode == ANTI_GLARE_MODE: + # Enter zoomed mode + self.zoom_mode() + self.mode = ZOOMED_MODE + elif self.mode == ZOOMED_MODE: + # Turn off zoomed mode + if self.cam_id in (GC0328_ID, GC2145_ID, OV2640_ID): + sensor.run(0) + sensor.set_framesize(sensor.QVGA) + sensor.set_windowing((0, 0, 320, 240)) + sensor.run(1) + elif self.cam_id == OV7740_ID: + sensor.__write_reg(0xD5, 0x30) + sensor.skip_frames() + # Go back to standard mode self.mode = QR_SCAN_MODE - self.luminosity_threshold() - return False - self.mode = ANTI_GLARE_MODE + else: + self.mode = ANTI_GLARE_MODE self.luminosity_threshold() - return True + return self.mode def snapshot(self): """Helper to take a customized snapshot from sensor""" diff --git a/src/krux/display.py b/src/krux/display.py index f834b7d7d..19a3ba888 100644 --- a/src/krux/display.py +++ b/src/krux/display.py @@ -227,13 +227,23 @@ def qr_data_width(self): def to_landscape(self): """Changes the rotation of the display to landscape""" if self.portrait: - lcd.rotation(LANDSCAPE) + lcd.rotation( + (LANDSCAPE + 2) % 4 + if hasattr(Settings().hardware, "display") + and getattr(Settings().hardware.display, "flipped_orientation", False) + else LANDSCAPE + ) self.portrait = False def to_portrait(self): """Changes the rotation of the display to portrait""" if not self.portrait: - lcd.rotation(PORTRAIT) + lcd.rotation( + (PORTRAIT + 2) % 4 + if hasattr(Settings().hardware, "display") + and getattr(Settings().hardware.display, "flipped_orientation", False) + else PORTRAIT + ) self.portrait = True def to_lines(self, text, max_lines=None): diff --git a/src/krux/key.py b/src/krux/key.py index 61c5d5b8e..eecbed745 100644 --- a/src/krux/key.py +++ b/src/krux/key.py @@ -42,33 +42,32 @@ DER_SINGLE = "m/%dh/%dh/%dh" DER_MULTI = "m/%dh/%dh/%dh/2h" DER_MINISCRIPT = "m/%dh/%dh/%dh/2h" -HARDENED_STR_REPLACE = "'" -# Pay To Public Key Hash - 44' Legacy single-sig +# Pay To Public Key Hash - 44h Legacy single-sig # address starts with 1 (mainnet) or m (testnet) P2PKH = "p2pkh" -# Pay To Script Hash - 45' Legacy multisig +# Pay To Script Hash - 45h Legacy multisig # address starts with 3 (mainnet) or 2 (testnet) P2SH = "p2sh" -# Pay To Witness Public Key Hash Wrapped In P2SH - 49' Nested Segwit single-sig +# Pay To Witness Public Key Hash Wrapped In P2SH - 49h Nested Segwit single-sig # address starts with 3 (mainnet) or 2 (testnet) P2SH_P2WPKH = "p2sh-p2wpkh" -# Pay To Witness Script Hash Wrapped In P2SH - 48'/0'/0'/1' Nested Segwit multisig +# Pay To Witness Script Hash Wrapped In P2SH - 48h/0h/0h/1h Nested Segwit multisig # address starts with 3 (mainnet) or 2 (testnet) P2SH_P2WSH = "p2sh-p2wsh" -# Pay To Witness Public Key Hash - 84' Native Segwit single-sig +# Pay To Witness Public Key Hash - 84h Native Segwit single-sig # address starts with bc1q (mainnet) or tb1q (testnet) P2WPKH = "p2wpkh" -# Pay To Witness Script Hash - 48'/0'/0'/2' Native Segwit multisig +# Pay To Witness Script Hash - 48h/0h/0h/2h Native Segwit multisig # address starts with bc1q (mainnet) or tb1q (testnet) P2WSH = "p2wsh" -# Pay To Taproot - 86' Taproot single-sig +# Pay To Taproot - 86h Taproot single-sig # address starts with bc1p (mainnet) or tb1p (testnet) P2TR = "p2tr" @@ -114,20 +113,30 @@ def __init__( passphrase="", account_index=0, script_type=P2WPKH, + custom_derivation="", ): self.mnemonic = mnemonic self.policy_type = policy_type self.network = network self.passphrase = passphrase self.account_index = account_index - self.script_type = script_type if policy_type == TYPE_SINGLESIG else P2WSH + if policy_type == TYPE_MULTISIG and script_type != P2WSH: + script_type = P2WSH + if policy_type == TYPE_MINISCRIPT and script_type not in (P2WSH, P2TR): + script_type = P2WSH + self.script_type = script_type self.root = bip32.HDKey.from_seed( bip39.mnemonic_to_seed(mnemonic, passphrase), version=network["xprv"] ) self.fingerprint = self.root.child(0).fingerprint - self.derivation = self.get_default_derivation( - self.policy_type, self.network, self.account_index, self.script_type - ) + if not custom_derivation: + self.derivation = self.get_default_derivation( + self.policy_type, self.network, self.account_index, self.script_type + ) + self.custom_derivation = False + else: + self.derivation = custom_derivation + self.custom_derivation = True self.account = self.root.derive(self.derivation).to_public() def xpub(self, version=None): @@ -214,7 +223,7 @@ def get_default_derivation(policy_type, network, account=0, script_type=P2WPKH): def format_derivation(derivation, pretty=False): """Helper method to display the derivation path formatted""" formatted_txt = DERIVATION_PATH_SYMBOL + THIN_SPACE + "%s" if pretty else "%s" - return (formatted_txt % derivation).replace("h", HARDENED_STR_REPLACE) + return formatted_txt % derivation @staticmethod def format_fingerprint(fingerprint, pretty=False): diff --git a/src/krux/krux_settings.py b/src/krux/krux_settings.py index 5de4cf7c1..24f18e922 100644 --- a/src/krux/krux_settings.py +++ b/src/krux/krux_settings.py @@ -306,16 +306,25 @@ class DisplaySettings(SettingsNamespace): """Custom display settings for Maix Cube""" namespace = "settings.display" - default_brightness = "1" if board.config["type"] == "m5stickv" else "3" - brightness = CategorySetting( - "brightness", default_brightness, ["1", "2", "3", "4", "5"] - ) + if board.config["type"] in ["cube", "m5stickv", "wonder_mv"]: + default_brightness = "1" if board.config["type"] == "m5stickv" else "3" + brightness = CategorySetting( + "brightness", default_brightness, ["1", "2", "3", "4", "5"] + ) + if board.config["type"] in ["yahboom", "wonder_mv"]: + flipped_orientation = CategorySetting( + "flipped_orientation", False, [False, True] + ) def label(self, attr): """Returns a label for UI when given a setting name or namespace""" - return { - "brightness": t("Brightness"), - }[attr] + options = {} + if board.config["type"] in ["wonder_mv", "cube", "m5stickv"]: + options["brightness"] = t("Brightness") + if board.config["type"] in ["wonder_mv", "yahboom"]: + options["flipped_orientation"] = t("Flipped Orientation") + + return options[attr] class HardwareSettings(SettingsNamespace): @@ -330,7 +339,7 @@ def __init__(self): self.touch = TouchSettings() if board.config["type"] == "amigo": self.display = DisplayAmgSettings() - elif board.config["type"] in ["cube", "m5stickv", "wonder_mv"]: + elif board.config["type"] in ["cube", "m5stickv", "wonder_mv", "yahboom"]: self.display = DisplaySettings() def label(self, attr): @@ -344,7 +353,7 @@ def label(self, attr): hardware_menu["touchscreen"] = t("Touchscreen") if board.config["type"] == "amigo": hardware_menu["display_amg"] = t("Display") - elif board.config["type"] in ["cube", "m5stickv", "wonder_mv"]: + elif board.config["type"] in ["cube", "m5stickv", "wonder_mv", "yahboom"]: hardware_menu["display"] = t("Display") return hardware_menu[attr] diff --git a/src/krux/metadata.py b/src/krux/metadata.py index 369513b2b..eb4c6ce57 100644 --- a/src/krux/metadata.py +++ b/src/krux/metadata.py @@ -19,5 +19,5 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -VERSION = "25.01.beta4" +VERSION = "25.01.beta12" SIGNER_PUBKEY = "03339e883157e45891e61ca9df4cd3bb895ef32d475b8e793559ea10a36766689b" diff --git a/src/krux/pages/__init__.py b/src/krux/pages/__init__.py index d1677d5dd..9d34525ae 100644 --- a/src/krux/pages/__init__.py +++ b/src/krux/pages/__init__.py @@ -20,7 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. import gc -import math import time import board import lcd @@ -74,6 +73,8 @@ LOAD_FROM_CAMERA = 0 LOAD_FROM_SD = 1 +EXTRA_MNEMONIC_LENGTH_FLAG = 48 + class Page: """Represents a page in the app, with helper methods for common display and @@ -95,9 +96,7 @@ def esc_prompt(self): answer = self.prompt(t("Are you sure?"), self.ctx.display.height() // 2) if self.ctx.input.touch is not None: self.ctx.input.touch.clear_regions() - if answer: - return ESC_KEY - return None + return ESC_KEY if answer else None def load_method(self): """Prompts user to choose a method to load data from""" @@ -115,12 +114,7 @@ def load_method(self): index, _ = load_menu.run_loop() return index - def flash_text( - self, - text, - color=theme.fg_color, - duration=FLASH_MSG_TIME, - ): + def flash_text(self, text, color=theme.fg_color, duration=FLASH_MSG_TIME): """Flashes text centered on the display for duration ms""" self.ctx.display.flash_text(text, color, duration) # Discard button presses that occurred during the message @@ -153,12 +147,8 @@ def capture_from_keypad( offset_y = MINIMAL_PADDING if big_title else DEFAULT_PADDING if lcd.string_width_px(buffer) < self.ctx.display.width(): self.ctx.display.draw_hcentered_text(title, offset_y) - if big_title: - offset_y += 2 * FONT_HEIGHT - else: - offset_y += FONT_HEIGHT * 3 // 2 + offset_y += 2 * FONT_HEIGHT if big_title else (FONT_HEIGHT * 3 // 2) self.ctx.display.draw_hcentered_text(buffer, offset_y) - if progress_bar_fn: progress_bar_fn() pad.compute_possible_keys(buffer) @@ -172,10 +162,7 @@ def capture_from_keypad( pad.moving_forward = True changed = False if pad.cur_key_index == pad.del_index: - if delete_key_fn is not None: - buffer = delete_key_fn(buffer) - else: - buffer = buffer[: len(buffer) - 1] + buffer = delete_key_fn(buffer) if delete_key_fn else buffer[:-1] changed = True elif pad.cur_key_index == pad.esc_index: if esc_prompt: @@ -202,7 +189,6 @@ def capture_from_keypad( break else: pad.navigate(btn) - if self.ctx.input.touch is not None: self.ctx.input.touch.clear_regions() return buffer @@ -215,10 +201,7 @@ def display_qr_codes(self, data, qr_format, title=""): i = 0 code_generator = to_qr_codes(data, self.ctx.display.qr_data_width(), qr_format) self.ctx.display.clear() - if theme.bg_color == WHITE: - qr_foreground = WHITE - else: - qr_foreground = None + qr_foreground = WHITE if theme.bg_color == WHITE else None extra_debounce_flag = True self.ctx.input.buttons_active = True while not done: @@ -237,7 +220,7 @@ def display_qr_codes(self, data, qr_format, title=""): else: self.ctx.display.draw_qr_code(0, code) subtitle = ( - t("Part") + "\n%d / %d" % (i + 1, num_parts) if not title else title + (t("Part") + "\n%d / %d" % (i + 1, num_parts)) if not title else title ) offset_y = self.ctx.display.qr_offset() if subtitle and self.ctx.display.height() > self.ctx.display.width(): @@ -279,42 +262,43 @@ def display_mnemonic( """Displays the 12 or 24-word list of words to the user""" from ..wallet import is_double_mnemonic - if display_mnemonic is None: - display_mnemonic = mnemonic + display_mnemonic = display_mnemonic or mnemonic words = display_mnemonic.split(" ") word_list = [ - str(i + 1) + "." + (" " if i + 1 < 10 else " ") + word + "{}.{}{}".format(i + 1, " " if i + 1 < 10 else " ", word) for i, word in enumerate(words) ] - if is_double_mnemonic(mnemonic): suffix += "*" if fingerprint: fingerprint = "\n" + fingerprint - header = "BIP39" + " " + suffix + fingerprint + header = "BIP39 {}{}".format(suffix, fingerprint) self.ctx.display.clear() self.ctx.display.draw_hcentered_text(header) + lines = self.ctx.display.to_lines(header) starting_y_offset = DEFAULT_PADDING // 4 + ( - len(self.ctx.display.to_lines(header)) * FONT_HEIGHT + FONT_HEIGHT + len(lines) * FONT_HEIGHT + FONT_HEIGHT ) for i, word in enumerate(word_list[:12]): - offset_x = DEFAULT_PADDING - offset_y = starting_y_offset + (i * FONT_HEIGHT) - self.ctx.display.draw_string(offset_x, offset_y, word) + self.ctx.display.draw_string( + DEFAULT_PADDING, starting_y_offset + (i * FONT_HEIGHT), word + ) if len(word_list) > 12: if board.config["type"] == "m5stickv": self.ctx.input.wait_for_button() self.ctx.display.clear() self.ctx.display.draw_hcentered_text(header) for i, word in enumerate(word_list[12:]): - offset_x = DEFAULT_PADDING - offset_y = starting_y_offset + (i * FONT_HEIGHT) - self.ctx.display.draw_string(offset_x, offset_y, word) + self.ctx.display.draw_string( + DEFAULT_PADDING, starting_y_offset + (i * FONT_HEIGHT), word + ) else: for i, word in enumerate(word_list[12:]): - offset_x = self.ctx.display.width() // 2 - offset_y = starting_y_offset + (i * FONT_HEIGHT) - self.ctx.display.draw_string(offset_x, offset_y, word) + self.ctx.display.draw_string( + self.ctx.display.width() // 2, + starting_y_offset + (i * FONT_HEIGHT), + word, + ) def print_prompt(self, text): """Prompts the user to print a QR code in the specified format @@ -322,19 +306,14 @@ def print_prompt(self, text): """ if not self.has_printer(): return False - self.ctx.display.clear() - if self.prompt( - (text + "\n\n%s\n\n") % Settings().hardware.printer.driver, - self.ctx.display.height() // 2, - ): - return True - return False + prompt_text = (text + "\n\n%s\n\n") % Settings().hardware.printer.driver + return self.prompt(prompt_text, self.ctx.display.height() // 2) def prompt(self, text, offset_y=0): """Prompts user to answer Yes or No""" - # Go up if question has multiple lines - offset_y -= (len(self.ctx.display.to_lines(text)) - 1) * FONT_HEIGHT + lines = self.ctx.display.to_lines(text) + offset_y -= (len(lines) - 1) * FONT_HEIGHT self.ctx.display.draw_hcentered_text( text, offset_y, theme.fg_color, theme.bg_color ) @@ -342,32 +321,34 @@ def prompt(self, text, offset_y=0): self.x_keypad_map = [] if MINIMAL_DISPLAY: return self.ctx.input.wait_for_button() == BUTTON_ENTER - offset_y += (len(self.ctx.display.to_lines(text)) + 1) * FONT_HEIGHT - self.x_keypad_map.append(0) - self.x_keypad_map.append(self.ctx.display.width() // 2) - self.x_keypad_map.append(self.ctx.display.width()) + offset_y += (len(lines) + 1) * FONT_HEIGHT + self.x_keypad_map.extend( + [0, self.ctx.display.width() // 2, self.ctx.display.width()] + ) y_key_map = offset_y - (3 * FONT_HEIGHT // 2) self.y_keypad_map.append(y_key_map) y_key_map += 4 * FONT_HEIGHT - y_key_map = min(y_key_map, self.ctx.display.height()) - self.y_keypad_map.append(y_key_map) + self.y_keypad_map.append(min(y_key_map, self.ctx.display.height())) if self.ctx.input.touch is not None: self.ctx.input.touch.clear_regions() self.ctx.input.touch.x_regions = self.x_keypad_map self.ctx.input.touch.y_regions = self.y_keypad_map - btn = None answer = True while btn != BUTTON_ENTER: - offset_x = (self.ctx.display.width() * 3) // 4 - offset_x -= (lcd.string_width_px(t("Yes"))) // 2 + go_str = t("Yes") + no_str = t("No") + offset_x = (self.ctx.display.width() * 3) // 4 - ( + lcd.string_width_px(go_str) // 2 + ) self.ctx.display.draw_string( - offset_x, offset_y, t("Yes"), theme.go_color, theme.bg_color + offset_x, offset_y, go_str, theme.go_color, theme.bg_color + ) + offset_x = self.ctx.display.width() // 4 - ( + lcd.string_width_px(no_str) // 2 ) - offset_x = self.ctx.display.width() // 4 - offset_x -= (lcd.string_width_px(t("No"))) // 2 self.ctx.display.draw_string( - offset_x, offset_y, t("No"), theme.no_esc_color, theme.bg_color + offset_x, offset_y, no_str, theme.no_esc_color, theme.bg_color ) if self.ctx.input.buttons_active: if answer: @@ -417,20 +398,14 @@ def prompt(self, text, offset_y=0): def fit_to_line(self, text, prefix="", fixed_chars=0, crop_middle=True): """Fits text with prefix plus fixed_chars at the beginning into one line, removing the central content and leaving the ends""" - - add_chars_amount = self.ctx.display.usable_width() // FONT_WIDTH - if len(text) + len(prefix) <= add_chars_amount: + usable_chars = self.ctx.display.usable_width() // FONT_WIDTH + if len(text) + len(prefix) <= usable_chars: return prefix + text - if not crop_middle: # Crop from the end - return prefix + text[: add_chars_amount - 2] + ".." - add_chars_amount -= len(prefix) + fixed_chars + 2 - add_chars_amount //= 2 - return ( - prefix - + text[: add_chars_amount + fixed_chars] - + ".." - + text[-add_chars_amount:] - ) + if not crop_middle: + return "{}{}..".format(prefix, text[: usable_chars - 2]) + usable_chars -= len(prefix) + fixed_chars + 2 + half = usable_chars // 2 + return "{}{}..{}".format(prefix, text[: half + fixed_chars], text[-half:]) def has_printer(self): """Checks if the device has a printer setup""" @@ -480,12 +455,13 @@ def __iter__(self): def __next__(self): if self.iter_index < len(self): + item = self.__getitem__(self.iter_index) self.iter_index += 1 - return self.__getitem__(self.iter_index - 1) + return item raise StopIteration def __len__(self): - return min(self.max_size, len(self.list[self.offset :])) + return min(self.max_size, len(self.list) - self.offset) def move_forward(self): """Slides the window one size-increment forward, wrapping around""" @@ -497,9 +473,9 @@ def move_backward(self): """Slides the window one size-increment backward, wrapping around""" self.offset -= self.max_size if self.offset < 0: - self.offset = int( - (math.ceil(len(self.list) / self.max_size) - 1) * self.max_size - ) + self.offset = ( + (len(self.list) + self.max_size - 1) // self.max_size - 1 + ) * self.max_size def index(self, i): """Returns the true index of an element in the underlying list""" @@ -525,7 +501,6 @@ def __init__( if back_label: back_label = t("Back") if back_label == "Back" else back_label self.menu += [("< " + back_label, back_status)] - self.disable_statusbar = disable_statusbar if offset is None: # Default offset for status bar @@ -535,8 +510,7 @@ def __init__( self.disable_statusbar = True self.menu_offset = offset max_viewable = min( - self.ctx.display.max_menu_lines(self.menu_offset), - len(self.menu), + self.ctx.display.max_menu_lines(self.menu_offset), len(self.menu) ) self.menu_view = ListView(self.menu, max_viewable) @@ -544,8 +518,7 @@ def screensaver(self): """Loads and starts screensaver""" from .screensaver import ScreenSaver - screen_saver = ScreenSaver(self.ctx) - screen_saver.start() + ScreenSaver(self.ctx).start() def run_loop(self, start_from_index=None): """Runs the menu loop until one of the menu items returns either a MENU_EXIT @@ -573,10 +546,8 @@ def run_loop(self, start_from_index=None): self._draw_touch_menu(selected_item_index) else: self._draw_menu(selected_item_index) - self.draw_status_bar() self.ctx.input.reset_ios_state() - if start_from_submenu: status = self._clicked_item(selected_item_index) if status != MENU_CONTINUE: @@ -621,11 +592,12 @@ def run_loop(self, start_from_index=None): self.screensaver() def _clicked_item(self, selected_item_index): - if self.menu_view[selected_item_index][1] is None: + item = self.menu_view[selected_item_index] + if item[1] is None: return MENU_CONTINUE try: self.ctx.display.clear() - status = self.menu_view[selected_item_index][1]() + status = item[1]() if status != MENU_CONTINUE: return status except Exception as e: @@ -641,11 +613,7 @@ def draw_status_bar(self): """Draws a status bar along the top of the UI""" if not self.disable_statusbar: self.ctx.display.fill_rectangle( - 0, - 0, - self.ctx.display.width(), - STATUS_BAR_HEIGHT, - theme.info_bg_color, + 0, 0, self.ctx.display.width(), STATUS_BAR_HEIGHT, theme.info_bg_color ) self.draw_network_indicator() self.draw_wallet_indicator() @@ -696,33 +664,23 @@ def draw_battery_indicator(self): if self.ctx.power_manager.usb_connected(): battery_color = theme.go_color else: - if charge < 0.3: - battery_color = theme.error_color - else: - battery_color = theme.fg_color - - # Draw (filled) outline of battery in top-right corner of display + battery_color = theme.error_color if charge < 0.3 else theme.fg_color + width = self.ctx.display.width() x_padding = FONT_HEIGHT // 3 y_padding = (STATUS_BAR_HEIGHT // 2) - (BATTERY_HEIGHT // 2) self.ctx.display.outline( - self.ctx.display.width() - x_padding - BATTERY_WIDTH, + width - x_padding - BATTERY_WIDTH, y_padding, BATTERY_WIDTH, BATTERY_HEIGHT, battery_color, ) self.ctx.display.fill_rectangle( - self.ctx.display.width() - x_padding + 1, - y_padding + 2, - 2, - BATTERY_HEIGHT - 3, - battery_color, + width - x_padding + 1, y_padding + 2, 2, BATTERY_HEIGHT - 3, battery_color ) - - # Indicate how much battery is depleted charge_length = int((BATTERY_WIDTH - 3) * charge) self.ctx.display.fill_rectangle( - self.ctx.display.width() - x_padding - BATTERY_WIDTH + 2, + width - x_padding - BATTERY_WIDTH + 2, y_padding + 2, charge_length, BATTERY_HEIGHT - 3, @@ -732,9 +690,10 @@ def draw_battery_indicator(self): def draw_wallet_indicator(self): """Draws wallet fingerprint or BIP85 child at top if wallet is loaded""" if self.ctx.is_logged_in(): + fingerprint = self.ctx.wallet.key.fingerprint_hex_str(True) if self.ctx.display.width() > NARROW_SCREEN_WITH: self.ctx.display.draw_hcentered_text( - self.ctx.wallet.key.fingerprint_hex_str(True), + fingerprint, STATUS_BAR_HEIGHT - FONT_HEIGHT - 1, theme.highlight_color, theme.info_bg_color, @@ -743,7 +702,7 @@ def draw_wallet_indicator(self): self.ctx.display.draw_string( 24, STATUS_BAR_HEIGHT - FONT_HEIGHT - 1, - self.ctx.wallet.key.fingerprint_hex_str(True), + fingerprint, theme.highlight_color, theme.info_bg_color, ) @@ -772,23 +731,22 @@ def _draw_touch_menu(self, selected_item_index): # map regions with dynamic height to fill screen self.ctx.input.touch.clear_regions() offset_y = 0 - Page.y_keypad_map = [offset_y] + y_keypad_map = [offset_y] for menu_item in self.menu_view: offset_y += len(self.ctx.display.to_lines(menu_item[0])) + 1 - Page.y_keypad_map.append(offset_y) - height_multiplier = self.ctx.display.height() - height_multiplier -= self.menu_offset # Top offset - height_multiplier -= DEFAULT_PADDING # Bottom padding - height_multiplier /= max(offset_y, 1) - Page.y_keypad_map = [ - int(n * height_multiplier) + self.menu_offset for n in Page.y_keypad_map + y_keypad_map.append(offset_y) + height_multiplier = ( + self.ctx.display.height() - self.menu_offset - DEFAULT_PADDING + ) / max(offset_y, 1) + y_keypad_map = [ + int(n * height_multiplier) + self.menu_offset for n in y_keypad_map ] - # Expand last region to the bottom of the screen - Page.y_keypad_map[-1] = self.ctx.display.height() - self.ctx.input.touch.y_regions = Page.y_keypad_map + # Expand last region to fill the screen + y_keypad_map[-1] = self.ctx.display.height() + self.ctx.input.touch.y_regions = y_keypad_map - # draw dividers and outline - for i, y in enumerate(Page.y_keypad_map[:-1]): + # Draw dividers + for i, y in enumerate(y_keypad_map[:-1]): if i and not self.ctx.input.buttons_active: self.ctx.display.draw_line( 0, y, self.ctx.display.width(), y, theme.frame_color @@ -797,20 +755,17 @@ def _draw_touch_menu(self, selected_item_index): # draw centralized strings in regions for i, menu_item in enumerate(self.menu_view): menu_item_lines = self.ctx.display.to_lines(menu_item[0]) - offset_y = Page.y_keypad_map[i + 1] - Page.y_keypad_map[i] - offset_y -= len(menu_item_lines) * FONT_HEIGHT - if i == len(self.menu_view) - 1: - # Compensate for the expanded last region - offset_y -= DEFAULT_PADDING - offset_y //= 2 - offset_y += Page.y_keypad_map[i] + region_height = y_keypad_map[i + 1] - y_keypad_map[i] + offset_y_item = ( + region_height - len(menu_item_lines) * FONT_HEIGHT + ) // 2 + y_keypad_map[i] fg_color = ( theme.fg_color if menu_item[1] is not None else theme.disabled_color ) if selected_item_index == i and self.ctx.input.buttons_active: self.ctx.display.fill_rectangle( 0, - offset_y + 1 - FONT_HEIGHT // 2, + offset_y_item + 1 - FONT_HEIGHT // 2, self.ctx.display.width(), (len(menu_item_lines) + 1) * FONT_HEIGHT, fg_color, @@ -818,48 +773,40 @@ def _draw_touch_menu(self, selected_item_index): for j, text in enumerate(menu_item_lines): if selected_item_index == i and self.ctx.input.buttons_active: self.ctx.display.draw_hcentered_text( - text, - offset_y + FONT_HEIGHT * j, - theme.bg_color, - fg_color, + text, offset_y_item + FONT_HEIGHT * j, theme.bg_color, fg_color ) else: self.ctx.display.draw_hcentered_text( - text, offset_y + FONT_HEIGHT * j, fg_color + text, offset_y_item + FONT_HEIGHT * j, fg_color ) def _draw_menu(self, selected_item_index): - extra_lines = 0 - for menu_item in self.menu_view: - # Count extra lines for multi-line menu items - extra_lines += len(self.ctx.display.to_lines(menu_item[0])) - 1 + extra_lines = sum( + len(self.ctx.display.to_lines(item[0])) - 1 for item in self.menu_view + ) if self.menu_offset > STATUS_BAR_HEIGHT: offset_y = self.menu_offset + FONT_HEIGHT else: - offset_y = len(self.menu_view) * 2 - offset_y += extra_lines - offset_y *= FONT_HEIGHT - offset_y = self.ctx.display.height() - offset_y + offset_y = self.ctx.display.height() - ( + (len(self.menu_view) * 2 + extra_lines) * FONT_HEIGHT + ) offset_y //= 2 offset_y += FONT_HEIGHT // 2 offset_y = max(offset_y, STATUS_BAR_HEIGHT) - # Usable pixels height - items_pad = self.ctx.display.height() - STATUS_BAR_HEIGHT - # Usable pixes for padding - items_pad -= (len(self.menu_view) + extra_lines) * FONT_HEIGHT - # Ensure padding is positive - items_pad = max(items_pad, 0) - # Padding between items + items_pad = max( + self.ctx.display.height() + - STATUS_BAR_HEIGHT + - (len(self.menu_view) + extra_lines) * FONT_HEIGHT, + 0, + ) items_pad //= max(len(self.menu_view) - 1, 1) - # Limit padding to font height items_pad = min(items_pad, FONT_HEIGHT) for i, menu_item in enumerate(self.menu_view): fg_color = ( theme.fg_color if menu_item[1] is not None else theme.disabled_color ) menu_item_lines = self.ctx.display.to_lines(menu_item[0]) - delta_y = len(menu_item_lines) * FONT_HEIGHT - delta_y += items_pad + delta_y = len(menu_item_lines) * FONT_HEIGHT + items_pad if selected_item_index == i: self.ctx.display.fill_rectangle( 0, @@ -870,10 +817,7 @@ def _draw_menu(self, selected_item_index): ) for j, text in enumerate(menu_item_lines): self.ctx.display.draw_hcentered_text( - text, - offset_y + FONT_HEIGHT * j, - theme.bg_color, - fg_color, + text, offset_y + FONT_HEIGHT * j, theme.bg_color, fg_color ) else: for j, text in enumerate(menu_item_lines): @@ -890,12 +834,8 @@ def choose_len_mnemonic(ctx, extra_option=""): (t("24 words"), lambda: 24), ] if extra_option: - items += [(extra_option, lambda: 48)] - submenu = Menu( - ctx, - items, - back_status=lambda: None, - ) + items.append((extra_option, lambda: EXTRA_MNEMONIC_LENGTH_FLAG)) + submenu = Menu(ctx, items, back_status=lambda: None) _, num_words = submenu.run_loop() ctx.display.clear() return num_words @@ -905,18 +845,13 @@ def proceed_menu( ctx, y_offset=0, menu_index=None, proceed_txt="Go", esc_txt="Esc", go_enabled=True ): """Reusable 'Esc' and 'Go' menu choice""" - - go_x_offset = ctx.display.width() // 2 - go_x_offset -= lcd.string_width_px(proceed_txt) - go_x_offset //= 2 - go_x_offset += ctx.display.width() // 2 - esc_x_offset = ctx.display.width() // 2 - esc_x_offset -= lcd.string_width_px(esc_txt) - esc_x_offset //= 2 - go_esc_y_offset = ctx.display.height() - go_esc_y_offset -= y_offset + FONT_HEIGHT + MINIMAL_PADDING - go_esc_y_offset //= 2 - go_esc_y_offset += y_offset + go_x_offset = ( + ctx.display.width() // 2 - lcd.string_width_px(proceed_txt) + ) // 2 + ctx.display.width() // 2 + esc_x_offset = (ctx.display.width() // 2 - lcd.string_width_px(esc_txt)) // 2 + go_esc_y_offset = ( + ctx.display.height() - (y_offset + FONT_HEIGHT + MINIMAL_PADDING) + ) // 2 + y_offset if menu_index == 0 and ctx.input.buttons_active: ctx.display.outline( DEFAULT_PADDING, diff --git a/src/krux/pages/home_pages/home.py b/src/krux/pages/home_pages/home.py index 5ee5dc147..ed764e595 100644 --- a/src/krux/pages/home_pages/home.py +++ b/src/krux/pages/home_pages/home.py @@ -36,9 +36,6 @@ from ...key import TYPE_SINGLESIG -MAX_POLICY_COSIGNERS_DISPLAYED = 5 - - class Home(Page): """Home is the main menu page of the app""" @@ -134,19 +131,20 @@ def customize(self): from ...wallet import Wallet wallet_settings = WalletSettings(self.ctx) - network, multisig, script_type, account = wallet_settings.customize_wallet( - self.ctx.wallet.key + network, policy_type, script_type, account, derivation_path = ( + wallet_settings.customize_wallet(self.ctx.wallet.key) ) mnemonic = self.ctx.wallet.key.mnemonic passphrase = self.ctx.wallet.key.passphrase self.ctx.wallet = Wallet( Key( mnemonic, - multisig, + policy_type, network, passphrase, account, script_type, + derivation_path, ) ) return MENU_CONTINUE @@ -329,29 +327,7 @@ def sign_psbt(self): not self.ctx.wallet.is_loaded() and not self.ctx.wallet.key.policy_type == TYPE_SINGLESIG ): - from ...key import Key - from ...psbt import is_multisig - - policy_str = "PSBT policy:\n" - policy_str += signer.policy["type"] + "\n" - if is_multisig(signer.policy): - policy_str += ( - str(signer.policy["m"]) + " of " + str(signer.policy["n"]) + "\n" - ) - fingerprints = [] - for inp in signer.psbt.inputs: - # Do we need to loop through all the inputs or just one? - for pub in inp.bip32_derivations: - fingerprint_srt = Key.format_fingerprint( - inp.bip32_derivations[pub].fingerprint, True - ) - if fingerprint_srt not in fingerprints: - if len(fingerprints) > MAX_POLICY_COSIGNERS_DISPLAYED: - fingerprints[-1] = "..." - break - fingerprints.append(fingerprint_srt) - - policy_str += "\n".join(fingerprints) + policy_str = signer.psbt_policy_string() self.ctx.display.clear() self.ctx.display.draw_centered_text(policy_str) if not self.prompt(t("Proceed?"), BOTTOM_PROMPT_LINE): @@ -400,11 +376,10 @@ def sign_psbt(self): self.ctx.display.clear() self.ctx.display.draw_centered_text(t("Signing..")) - signer.sign() - title = t("Signed PSBT") if index == 0: # Sign to QR code + signer.sign() signed_psbt, qr_format = signer.psbt_qr() # memory management @@ -420,6 +395,7 @@ def sign_psbt(self): return MENU_CONTINUE # index == 1: Sign to SD card + signer.sign(trim=False) psbt_filename = self._format_psbt_file_extension(psbt_filename) gc.collect() diff --git a/src/krux/pages/home_pages/miniscript_indenter.py b/src/krux/pages/home_pages/miniscript_indenter.py new file mode 100644 index 000000000..33e3178be --- /dev/null +++ b/src/krux/pages/home_pages/miniscript_indenter.py @@ -0,0 +1,194 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +class Node: + """ + A simple tree node that only stores: + - text: The 'prefix' of the expression before an open parenthesis, + or the entire expression if no parentheses exist. + - children: Any sub-expressions contained within parentheses (split by commas at top level). + - level: An integer used to control indentation depth. + """ + + def __init__(self, text, children=None, level=0): + self.text = text + self.children = children if children is not None else [] + self.level = level + + +class MiniScriptIndenter: + """ + Indent MiniScript expressions. + """ + + def indent(self, expression, max_line_width=25): + """ + Indent a MiniScript expression, breaking lines as needed to fit within max_line_width. + """ + multiple_tap_scripts = "{" in expression + tree = self._parse_expression(expression) + indented_lines = self._node_to_indented_string(tree, max_line_width) + final_lines = [] + for line in indented_lines: + final_lines.extend(self._break_lines(line, max_line_width)) + if multiple_tap_scripts: + # replace penultimate ")" of the last line by "}" + line = final_lines[-1] + final_lines[-1] = line[:-2] + "}" + line[-1] + return final_lines + + def _split_top_level_args(self, s, delimiter=","): + """ + Splits string s by the top-level delimiter (by default a comma). + """ + parts = [] + bracket_level = 0 + current = [] + for ch in s: + if ch == "(": + bracket_level += 1 + current.append(ch) + elif ch == ")": + bracket_level -= 1 + current.append(ch) + elif ch == delimiter and bracket_level == 0: + parts.append("".join(current).strip()) + current = [] + else: + current.append(ch) + if current: + parts.append("".join(current).strip()) + return parts + + def _parse_expression(self, expr, level=0): + """ + Recursively parse the expression into a Node that reflects parentheses structure. + + Steps: + - If there's no parenthesis at all, this is a leaf node. + - Otherwise: + prefix: the text before '(' + inside: the substring inside the outermost parentheses + children: sub-expressions inside 'inside', separated at top-level commas + """ + expr = expr.strip() + pos = expr.find("(") + if pos == -1: + # No '(' => leaf node + return Node(text=expr, children=[], level=level) + + prefix = expr[:pos].strip() + inside = expr[pos + 1 : -1].strip() + sub_expressions = self._split_top_level_args(inside) + children = [ + self._parse_expression(sub_expr, level=level + 1) + for sub_expr in sub_expressions + ] + + return Node(text=prefix, children=children, level=level) + + def _join_closing_parens(self, lines): + """ + Post-processing step: + If a line is only one or more closing parentheses (possibly with indentation), + append them to the end of the previous line, so no line is just ')'. + + """ + i = len(lines) - 1 + while i > 0: + stripped = lines[i].strip() + # Check if stripped is only some number of ')' + if stripped and all(ch == ")" for ch in stripped): + # Append them (minus indentation) to previous line + lines[i - 1] = "{0}{1}".format(lines[i - 1], stripped) + lines.pop(i) + i -= 1 + return lines + + def _node_to_indented_string(self, node, max_line_width=25): + """ + Convert the Node tree into an indented list of strings with two rules: + 1) If any child is a leaf, flatten all children into a single line with the parent. + 2) Remove lines that only contain ')', by merging them into the previous line. + 3) Ensure no line exceeds max_line_width by breaking long lines. + """ + + # If no children => leaf node + if not node.children: + indent = " " * node.level + return ["{0}{1}".format(indent, node.text)] + + # If any child is a leaf, flatten them all + # It's likely it will group multi and thresh children together + any_child_is_leaf = any(not c.children for c in node.children) + if any_child_is_leaf and node.level > 0: + # Flatten children on one line + indent = " " * node.level + line = "{0}{1}(".format(indent, node.text) + child_texts = [] + for child in node.children: + child_str = self._node_to_indented_string(child, max_line_width) + # # compress any newlines to single space + # child_str = child_str.replace("\n", " ") + child_texts.append(child_str[0].strip()) + line += ",".join(child_texts) + ")" + # If flattened line is short enough, return it + if len(line) <= max_line_width: + return [line] + # Else break it into multiple lines as if here was no leaf children + + # Multi-line approach + indent = " " * node.level + lines = [] + lines.append("{0}{1}(".format(indent, node.text)) + for i, child in enumerate(node.children): + child_lines = self._node_to_indented_string(child, max_line_width) + # If not the last child, add a trailing comma + if i < len(node.children) - 1: + child_lines[-1] = "{0},".format(child_lines[-1]) + lines.extend(child_lines) + lines.append("{0})".format(indent)) + + # Post-process lines to join any lines that are just ')' + lines = self._join_closing_parens(lines) + + return lines + + def _break_lines(self, line, max_line_width): + """ + Break a line into multiple lines if it exceeds max_line_width. + """ + if len(line) <= max_line_width: + return [line] + indent = 0 + while True: + if line[indent] != " ": + break + indent += 1 + data = line[indent:] + parts = [] + while len(data) > max_line_width - indent: + parts.append(" " * indent + data[: max_line_width - indent]) + data = data[max_line_width - indent :] + parts.append(" " * indent + data) + return parts diff --git a/src/krux/pages/home_pages/pub_key_view.py b/src/krux/pages/home_pages/pub_key_view.py index ca668a994..cdf5194b0 100644 --- a/src/krux/pages/home_pages/pub_key_view.py +++ b/src/krux/pages/home_pages/pub_key_view.py @@ -102,7 +102,7 @@ def _pub_key_qr(version): versions.append(self.ctx.wallet.key.network["ypub"]) elif self.ctx.wallet.key.script_type == P2SH_P2WSH: versions.append(self.ctx.wallet.key.network["Ypub"]) - elif self.ctx.wallet.key.script_type == P2WSH: + elif self.ctx.wallet.key.script_type == P2WSH and self.ctx.wallet.is_multisig(): versions.append(self.ctx.wallet.key.network["Zpub"]) pub_key_menu_items = [] for version in versions: diff --git a/src/krux/pages/home_pages/wallet_descriptor.py b/src/krux/pages/home_pages/wallet_descriptor.py index 2d8c2dbdd..4897b14c2 100644 --- a/src/krux/pages/home_pages/wallet_descriptor.py +++ b/src/krux/pages/home_pages/wallet_descriptor.py @@ -26,12 +26,19 @@ LOAD_FROM_CAMERA, LOAD_FROM_SD, ) -from ...display import DEFAULT_PADDING, BOTTOM_PROMPT_LINE +from ...display import ( + DEFAULT_PADDING, + BOTTOM_PROMPT_LINE, + FONT_HEIGHT, + FONT_WIDTH, + MINIMAL_DISPLAY, + MINIMAL_PADDING, +) from ...krux_settings import t -from ...qr import FORMAT_NONE +from ...qr import FORMAT_NONE, FORMAT_PMOFN from ...sd_card import DESCRIPTOR_FILE_EXTENSION, JSON_FILE_EXTENSION from ...themes import theme -from ...key import FINGERPRINT_SYMBOL +from ...key import FINGERPRINT_SYMBOL, DERIVATION_PATH_SYMBOL, P2TR class WalletDescriptor(Page): @@ -106,6 +113,8 @@ def _load_wallet(self): else: # Cancel return MENU_CONTINUE + self.ctx.display.clear() + self.ctx.display.draw_centered_text(t("Processing..")) if wallet_data is None: # Camera or SD card loading failed! self.flash_error(t("Failed to load")) @@ -138,67 +147,139 @@ def _load_wallet(self): if wallet.is_loaded(): self.ctx.display.clear() - self.display_wallet(wallet, is_loading=True) + self.display_loading_wallet(wallet) if self.prompt(t("Load?"), BOTTOM_PROMPT_LINE): self.ctx.wallet = wallet self.flash_text(t("Wallet output descriptor loaded!")) return MENU_CONTINUE - def display_wallet(self, wallet, is_loading=False): - """Displays a wallet, including its label and abbreviated xpubs. - If include_qr is True, a QR code of the wallet will be shown - which will contain the same data as was originally loaded, in - the same QR format - """ - import binascii + def display_wallet(self, wallet): + """Try to show the wallet output descriptor as a QRCode""" + try: + w_data, qr_format = wallet.wallet_qr() + if qr_format == FORMAT_NONE: + qr_format = FORMAT_PMOFN + w_data = w_data.decode() if not isinstance(w_data, str) else w_data + self.display_qr_codes(w_data, qr_format, title=wallet.label) + except Exception as e: + self.ctx.display.clear() + self.ctx.display.draw_centered_text( + t("Error:") + "\n%s" % repr(e), theme.error_color + ) + self.ctx.input.wait_for_button() + + def display_loading_wallet(self, wallet): + """Displays wallet descriptor attributes while loading""" + + def draw_header(): + nonlocal offset_y + self.ctx.display.clear() + self.ctx.display.draw_hcentered_text(wallet.label, DEFAULT_PADDING) + offset_y = DEFAULT_PADDING + (3 * FONT_HEIGHT) // 2 - about = [wallet.label] - fingerprints = [] + offset_y = DEFAULT_PADDING + self.ctx.display.draw_hcentered_text(wallet.label, offset_y) + offset_y += (3 * FONT_HEIGHT) // 2 + our_key_indexes_chars = [] + unused_key_index = None for i, key in enumerate(wallet.descriptor.keys): - label = str(i + 1) + ". " if wallet.is_multisig() else "" - fingerprints.append( - label - + FINGERPRINT_SYMBOL - + " " - + ( - binascii.hexlify(key.origin.fingerprint).decode() - if key.origin - else t("unknown") - ) + label_color = theme.fg_color + padding = DEFAULT_PADDING if not MINIMAL_DISPLAY else MINIMAL_PADDING + key_label = ( + "{}: ".format(chr(65 + i)) + if (wallet.is_multisig() or wallet.is_miniscript()) + else (" " * 3 if not MINIMAL_DISPLAY else "") ) - about.extend(fingerprints) - if not wallet.is_multisig() and not wallet.is_miniscript(): - about.append(self.fit_to_line(str(wallet.descriptor.keys[0].key))) - if is_loading: - self.ctx.display.draw_hcentered_text(about, offset_y=DEFAULT_PADDING) + key_fingerprint = FINGERPRINT_SYMBOL + " " + if key.origin: + key_origin_str = str(key.origin) + key_fingerprint += key_origin_str[:8] else: - wallet_data, qr_format = wallet.wallet_qr() - self.display_qr_codes(wallet_data, qr_format, title=about) - else: - # Display fingerprints - self.ctx.display.draw_hcentered_text(about, offset_y=DEFAULT_PADDING) - self.ctx.input.wait_for_button() + if ( + i == 0 + and wallet.is_miniscript() + and wallet.policy.get("type") == P2TR + ): + key_fingerprint = t("TR internal key") + label_color = theme.disabled_color + unused_key_index = chr(65 + i) + else: + key_fingerprint += t("unknown") + # Check if the key is the one loaded in the wallet + if ( + self.ctx.wallet.key + and len(wallet.descriptor.keys) > 1 + and key.fingerprint == self.ctx.wallet.key.fingerprint + ): + label_color = theme.highlight_color + our_key_indexes_chars.append(chr(65 + i)) + # Draw header and fingerprint lines + for line in self.ctx.display.to_lines(key_label + key_fingerprint): + self.ctx.display.draw_string(padding, offset_y, line, label_color) + offset_y += FONT_HEIGHT - # Display XPUBs - about = [wallet.label] - xpubs = [] - for i, xpub in enumerate(wallet.policy["cosigners"]): - xpubs.append(self.fit_to_line(xpub, str(i + 1) + ". ")) - about.extend(xpubs) - self.ctx.display.clear() - self.ctx.display.draw_hcentered_text(about, offset_y=DEFAULT_PADDING) - if is_loading: - # Skip the QR code if we're loading the wallet - return - self.ctx.input.wait_for_button() - # Try to show the wallet output descriptor as a QRCode - try: - wallet_data, qr_format = wallet.wallet_qr() - self.display_qr_codes(wallet_data, qr_format, title=wallet.label) - except Exception as e: - self.ctx.display.clear() - self.ctx.display.draw_centered_text( - t("Error:") + "\n%s" % repr(e), theme.error_color + sub_padding = padding + (0 if MINIMAL_DISPLAY else 3 * FONT_WIDTH) + + if key.origin: + key_derivation_str = "{} m{}".format( + DERIVATION_PATH_SYMBOL, key_origin_str[8:] ) + self.ctx.display.draw_string( + sub_padding, offset_y, key_derivation_str, label_color + ) + offset_y += FONT_HEIGHT + elif ( + i == 0 and wallet.is_miniscript() and wallet.policy.get("type") == P2TR + ): + for line in self.ctx.display.to_lines(t("Provably unspendable")): + self.ctx.display.draw_string( + sub_padding, offset_y, line, label_color + ) + offset_y += FONT_HEIGHT + + xpub_text = self.fit_to_line( + ("" if MINIMAL_DISPLAY else " " * 3) + key.key.to_base58() + ) + self.ctx.display.draw_string(padding, offset_y, xpub_text, label_color) + offset_y += (FONT_HEIGHT * 3) // 2 + + # Check if there's another key and room for it + if ( + i + 1 < len(wallet.descriptor.keys) + and offset_y + (FONT_HEIGHT * 4) > self.ctx.display.height() + ): self.ctx.input.wait_for_button() + draw_header() + + # Display miniscript policies if available + if wallet.is_miniscript(): + from .miniscript_indenter import MiniScriptIndenter + + max_width = self.ctx.display.width() // FONT_WIDTH + miniscript_policy = MiniScriptIndenter().indent( + wallet.descriptor.full_policy, max_width + ) + lines_left = (BOTTOM_PROMPT_LINE - offset_y) // FONT_HEIGHT + + if len(miniscript_policy) > lines_left: + self.ctx.input.wait_for_button() + draw_header() + + for line in miniscript_policy: + self.ctx.display.draw_string(padding, offset_y, line) + for idx, char in enumerate(line): + char_x = padding + idx * FONT_WIDTH + if char == unused_key_index: + self.ctx.display.draw_string( + char_x, offset_y, char, theme.disabled_color + ) + elif char in our_key_indexes_chars: + self.ctx.display.draw_string( + char_x, offset_y, char, theme.highlight_color + ) + offset_y += FONT_HEIGHT + if offset_y >= BOTTOM_PROMPT_LINE: + self.ctx.display.draw_hcentered_text("...", offset_y) + self.ctx.input.wait_for_button() + draw_header() diff --git a/src/krux/pages/login.py b/src/krux/pages/login.py index 4a70ea03d..9ce9902af 100644 --- a/src/krux/pages/login.py +++ b/src/krux/pages/login.py @@ -23,12 +23,24 @@ import sys from embit.networks import NETWORKS from embit.wordlists.bip39 import WORDLIST +from . import ( + Page, + Menu, + DIGITS, + MENU_CONTINUE, + MENU_EXIT, + ESC_KEY, + LETTERS, + EXTRA_MNEMONIC_LENGTH_FLAG, + choose_len_mnemonic, +) from ..display import DEFAULT_PADDING, FONT_HEIGHT, BOTTOM_PROMPT_LINE from ..krux_settings import Settings from ..qr import FORMAT_UR from ..key import ( Key, P2WSH, + P2TR, SCRIPT_LONG_NAMES, TYPE_SINGLESIG, TYPE_MULTISIG, @@ -36,20 +48,16 @@ POLICY_TYPE_IDS, ) from ..krux_settings import t -from . import ( - Page, - Menu, - DIGITS, - MENU_CONTINUE, - MENU_EXIT, - ESC_KEY, - LETTERS, - choose_len_mnemonic, -) +from ..settings import NAME_SINGLE_SIG, NAME_MULTISIG, NAME_MINISCRIPT + DIGITS_HEX = "0123456789ABCDEF" DIGITS_OCT = "01234567" +DOUBLE_MNEMONICS_MAX_TRIES = 200 +MASK256 = (1 << 256) - 1 +MASK128 = (1 << 128) - 1 + class Login(Page): """Represents the login page of the app""" @@ -189,6 +197,7 @@ def new_key_from_snapshot(self): if entropy_bytes is not None: import binascii from embit.bip39 import mnemonic_from_bytes + from ..bip39 import entropy_checksum entropy_hash = binascii.hexlify(entropy_bytes).decode() self.ctx.display.clear() @@ -197,47 +206,58 @@ def new_key_from_snapshot(self): ) self.ctx.input.wait_for_button() - self.ctx.display.clear() - self.ctx.display.draw_centered_text(t("Processing..")) - - num_bytes = 16 if len_mnemonic == 12 else 32 - entropy_mnemonic = mnemonic_from_bytes(entropy_bytes[:num_bytes]) + # Checks if user wants to create a double mnemonic + if len_mnemonic == EXTRA_MNEMONIC_LENGTH_FLAG: - # Double mnemonic check - if len_mnemonic == 48: - from ..wallet import is_double_mnemonic + # import time # Debug + # pre_t = time.ticks_ms() # Debug - if not is_double_mnemonic(entropy_mnemonic): - from ..wdt import wdt - import time - from krux.bip39 import mnemonic_is_valid + # split the mnemonic into two parts + first_12_entropy = entropy_bytes[:16] + second_12_entropy = entropy_bytes[16:32] - pre_t = time.ticks_ms() - tries = 0 + # calculate the checksum for the first 12 words + checksum1 = entropy_checksum(first_12_entropy, 4) + # print first 12 words - # create two 12w mnemonic with the provided entropy - first_12 = mnemonic_from_bytes(entropy_bytes[:16]) - second_entropy_mnemonic_int = int.from_bytes( - entropy_bytes[16:32], "big" - ) - double_mnemonic = False - while not double_mnemonic: - wdt.feed() - tries += 1 - # increment the second mnemonic entropy - second_entropy_mnemonic_int += 1 - second_12 = mnemonic_from_bytes( - second_entropy_mnemonic_int.to_bytes(16, "big") - ) - entropy_mnemonic = first_12 + " " + second_12 - double_mnemonic = mnemonic_is_valid(entropy_mnemonic) + # replace checksum1 as first 4 bits of second 12 words + snd_12_array = bytearray(second_12_entropy) + snd_12_array[0] = (snd_12_array[0] & 0x0F) | ( + (checksum1 & 0x0F) << 4 + ) + second_12_entropy = bytes(snd_12_array) + # reassemble the 256 bits entropy that has first 12 words with valid checksum + entropy_bytes = first_12_entropy + second_12_entropy + + # Increment 1 to full 24 words entropy until + # both last 12 words and all 24 have valid checksum + tries = 0 + entropy_int = int.from_bytes(entropy_bytes, "big") + while True: + # calculate the checksum for the new 24 words + ck_sum_24 = entropy_checksum(entropy_bytes, 8) + + # Extract the lower 128 bits from the integer. + snd_12_int = entropy_int & MASK128 + # Shift and combine with first 4 bits of the 24 wwords checksum + shifted_entr = ((snd_12_int << 4) & MASK128) | (ck_sum_24 >> 4) + shifted_entropy_bytes = shifted_entr.to_bytes(16, "big") + checksum_l_12 = entropy_checksum(shifted_entropy_bytes, 4) + # check if checksum_l_12 is equal to the last 4 bits of the + # checksum of the full 24 words + if checksum_l_12 == (ck_sum_24 & 0x0F): + break - print( - "Tries: %d" % tries, - "/ %d" % (time.ticks_ms() - pre_t), - "ms", - ) + # Increment the integer value and mask to 256 bits. + entropy_int = (entropy_int + 1) & MASK256 + entropy_bytes = entropy_int.to_bytes(32, "big") + tries += 1 + if tries > DOUBLE_MNEMONICS_MAX_TRIES: + raise ValueError("Failed to find a valid double mnemonic") + # print("Tries: {} / {} ms".format(tries, time.ticks_ms() - pre_t)) # Debug + num_bytes = 16 if len_mnemonic == 12 else 32 + entropy_mnemonic = mnemonic_from_bytes(entropy_bytes[:num_bytes]) return self._load_key_from_words(entropy_mnemonic.split(), new=True) return MENU_CONTINUE @@ -263,12 +283,11 @@ def _load_key_from_words(self, words, charset=LETTERS, new=False): return MENU_CONTINUE self.ctx.display.clear() - from .mnemonic_editor import MnemonicEditor - # If the mnemonic is not hidden, show the mnemonic editor if not Settings().security.hide_mnemonic: - mnemonic_editor = MnemonicEditor(self.ctx, mnemonic, new) - mnemonic = mnemonic_editor.edit() + from .mnemonic_editor import MnemonicEditor + + mnemonic = MnemonicEditor(self.ctx, mnemonic, new).edit() if mnemonic is None: return MENU_CONTINUE self.ctx.display.clear() @@ -289,24 +308,36 @@ def _load_key_from_words(self, words, charset=LETTERS, new=False): account = 0 if policy_type == TYPE_SINGLESIG: script_type = SCRIPT_LONG_NAMES.get(Settings().wallet.script_type) + elif policy_type == TYPE_MINISCRIPT and Settings().wallet.script_type == P2TR: + script_type = P2TR else: script_type = P2WSH + derivation_path = "" from ..wallet import Wallet while True: - key = Key(mnemonic, policy_type, network, passphrase, account, script_type) - + key = Key( + mnemonic, + policy_type, + network, + passphrase, + account, + script_type, + derivation_path, + ) + if not derivation_path: + derivation_path = key.derivation wallet_info = key.fingerprint_hex_str(True) + "\n" wallet_info += network["name"] + "\n" if policy_type == TYPE_SINGLESIG: - wallet_info += "Single-sig" + "\n" + wallet_info += NAME_SINGLE_SIG + "\n" elif policy_type == TYPE_MULTISIG: - wallet_info += "Multisig" + "\n" + wallet_info += NAME_MULTISIG + "\n" elif policy_type == TYPE_MINISCRIPT: - wallet_info += "Miniscript" + "\n" - wallet_info += ( - self.fit_to_line(key.derivation_str(True), crop_middle=False) + "\n" - ) + if script_type == P2TR: + wallet_info += "TR " + wallet_info += NAME_MINISCRIPT + "\n" + wallet_info += key.derivation_str(True) + "\n" wallet_info += ( t("No Passphrase") if not passphrase else t("Passphrase") + ": *..*" ) @@ -342,7 +373,7 @@ def _load_key_from_words(self, words, charset=LETTERS, new=False): from .wallet_settings import WalletSettings wallet_settings = WalletSettings(self.ctx) - network, policy_type, script_type, account = ( + network, policy_type, script_type, account, derivation_path = ( wallet_settings.customize_wallet(key) ) diff --git a/src/krux/pages/new_mnemonic/dice_rolls.py b/src/krux/pages/new_mnemonic/dice_rolls.py index fd0289b5f..0ebd42a19 100644 --- a/src/krux/pages/new_mnemonic/dice_rolls.py +++ b/src/krux/pages/new_mnemonic/dice_rolls.py @@ -29,7 +29,13 @@ ) from ...themes import theme from ...krux_settings import t -from ...display import DEFAULT_PADDING, FONT_HEIGHT, TOTAL_LINES, BOTTOM_PROMPT_LINE +from ...display import ( + DEFAULT_PADDING, + FONT_HEIGHT, + TOTAL_LINES, + BOTTOM_PROMPT_LINE, + MINIMAL_DISPLAY, +) D6_STATES = [str(i + 1) for i in range(6)] D20_STATES = [str(i + 1) for i in range(20)] @@ -130,34 +136,37 @@ def stats_for_nerds(self): self.ctx.display.clear() self.ctx.display.draw_hcentered_text(t("Rolls distribution:"), FONT_HEIGHT) shannon_entropy = self.calculate_entropy() - max_count = max(self.roll_counts) - - scale_factor = (self.ctx.display.height() * BAR_GRAPH_SIZE) / 100 - scale_factor /= max_count - bar_graph = [] - for count in self.roll_counts: - bar_graph.append(int(count * scale_factor)) - bar_pad = self.ctx.display.width() // (self.num_sides + 2) + max_count = max(self.roll_counts) or 1 # Prevent division by zero + + # Calculate scale factor based on display height and BAR_GRAPH_SIZE percentage + display_height = self.ctx.display.height() + scale = (display_height * BAR_GRAPH_SIZE) / (100 * max_count) + + # Generate bar heights using list comprehension + bar_graph = [int(count * scale) for count in self.roll_counts] + + # Calculate horizontal padding and vertical offset for the bar graph + display_width = self.ctx.display.width() + bar_pad = display_width // (self.num_sides + 2) offset_x = bar_pad - # Bar graph offset (bottom of the graph) - offset_y = BAR_GRAPH_POSITION + BAR_GRAPH_SIZE - offset_y *= self.ctx.display.height() - offset_y //= 100 + offset_y = (BAR_GRAPH_POSITION + BAR_GRAPH_SIZE) * display_height // 100 - for individual_bar in bar_graph: - bar_offset = offset_y - individual_bar + # Draw each bar in the graph + for height in bar_graph: self.ctx.display.fill_rectangle( offset_x + 1, - bar_offset, + offset_y - height, bar_pad - 2, - individual_bar, + height, theme.highlight_color, ) offset_x += bar_pad - offset_y += FONT_HEIGHT + + # Draw Shannon's entropy below the graph + suffix = " bits" if not MINIMAL_DISPLAY else "b" self.ctx.display.draw_hcentered_text( - t("Shannon's entropy:") + " " + str(shannon_entropy) + " " + "bits", - offset_y, + t("Shannon's entropy:") + " " + str(shannon_entropy) + suffix, + offset_y + FONT_HEIGHT, ) self.ctx.input.wait_for_button() diff --git a/src/krux/pages/qr_capture.py b/src/krux/pages/qr_capture.py index 8f9bdc1a0..db50d6b6f 100644 --- a/src/krux/pages/qr_capture.py +++ b/src/krux/pages/qr_capture.py @@ -28,6 +28,7 @@ from ..qr import QRPartParser, FORMAT_UR from ..wdt import wdt from ..krux_settings import t +from ..camera import QR_SCAN_MODE, ANTI_GLARE_MODE, ZOOMED_MODE ANTI_GLARE_WAIT_TIME = 500 @@ -54,10 +55,14 @@ def light_control(self): def anti_glare_control(self): """Controls the anti-glare based on the user input""" self.ctx.display.to_portrait() - if self.ctx.camera.toggle_antiglare(): - self.ctx.display.draw_centered_text(t("Anti-glare enabled")) - else: - self.ctx.display.draw_centered_text(t("Anti-glare disabled")) + mode = self.ctx.camera.toggle_camera_mode() + if mode == QR_SCAN_MODE: + self.ctx.display.draw_centered_text(t("Standard mode")) + elif mode == ANTI_GLARE_MODE: + self.ctx.display.draw_centered_text(t("Anti-glare mode")) + elif mode == ZOOMED_MODE: + self.ctx.display.clear() + self.ctx.display.draw_centered_text(t("Zoomed mode")) time.sleep_ms(ANTI_GLARE_WAIT_TIME) # Erase the message from the screen self.ctx.display.fill_rectangle( diff --git a/src/krux/pages/settings_page.py b/src/krux/pages/settings_page.py index f4bee7372..4fe79253d 100644 --- a/src/krux/pages/settings_page.py +++ b/src/krux/pages/settings_page.py @@ -379,6 +379,10 @@ def category_setting(self, settings_namespace, setting): locale_control.load_locale(new_category) if setting.attr == "theme": theme.update() + # Update screen in case orientation has changed + if setting.attr == "flipped_orientation": + self.ctx.display.to_landscape() + self.ctx.display.to_portrait() if setting.attr == "brightness": if board.config["type"] in ["cube", "wonder_mv"]: self.ctx.display.gpio_backlight_ctrl(new_category) diff --git a/src/krux/pages/tiny_seed.py b/src/krux/pages/tiny_seed.py index 30edf783f..44fd61733 100644 --- a/src/krux/pages/tiny_seed.py +++ b/src/krux/pages/tiny_seed.py @@ -20,7 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -import hashlib import board import lcd import image @@ -42,6 +41,7 @@ ) from ..camera import BINARY_GRID_MODE from ..input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV, BUTTON_TOUCH +from ..bip39 import entropy_checksum # Tiny Seed last bit index positions according to checksums TS_LAST_BIT_NO_CS = 143 @@ -54,7 +54,7 @@ class TinySeed(Page): - """Class for handling Tinyseed fomat""" + """Class for handling Tinyseed format""" def __init__(self, ctx, label="Tiny Seed"): super().__init__(ctx, None) @@ -75,31 +75,22 @@ def __init__(self, ctx, label="Tiny Seed"): def _draw_grid(self): """Draws grid for import and export Tinyseed UI""" - y_var = self.y_offset - x_offset = self.x_offset + y = self.y_offset + x = self.x_offset for _ in range(13): - self.ctx.display.draw_line( - x_offset, - self.y_offset, - x_offset, - self.y_offset + 12 * self.y_pad, - theme.frame_color, + self.ctx.display.draw_vline( + x, self.y_offset, 12 * self.y_pad, theme.frame_color ) - x_offset += self.x_pad - self.ctx.display.draw_line( - self.x_offset, - y_var, - self.x_offset + 12 * (self.x_pad), - y_var, - theme.frame_color, + x += self.x_pad + self.ctx.display.draw_hline( + self.x_offset, y, 12 * self.x_pad, theme.frame_color ) - y_var += self.y_pad + y += self.y_pad def _draw_labels(self, page): """Draws labels for import and export Tinyseed UI""" self.ctx.display.draw_hcentered_text(self.label) - - # case for non m5stickv, cube + # For non‑minimal displays, show extra bit numbers (rotate to landscape temporarily) if not MINIMAL_DISPLAY: self.ctx.display.to_landscape() bit_number = 2048 @@ -115,47 +106,49 @@ def _draw_labels(self, page): bit_number //= 2 bit_offset += self.x_pad self.ctx.display.to_portrait() - y_offset = self.y_offset - y_offset += (self.y_pad - FONT_HEIGHT) // 2 - for x in range(12): - line = str(page * 12 + x + 1) - if (page * 12 + x + 1) < 10: - line = " " + line - self.ctx.display.draw_string(MINIMAL_PADDING, y_offset, line) - y_offset += self.y_pad + # Draw row numbers on the left side + y = self.y_offset + (self.y_pad - FONT_HEIGHT) // 2 + for i in range(12): + line = "{:2}".format(page * 12 + i + 1) + self.ctx.display.draw_string(MINIMAL_PADDING, y, line) + y += self.y_pad def _draw_punched(self, words, page): """Draws punched bits for import and export Tinyseed UI""" - y_offset = self.y_offset - if self.x_pad < self.y_pad: - radius = (self.x_pad - 5) // 3 - else: - radius = (self.y_pad - 5) // 3 + y = self.y_offset + # Compute radius for rounded corners if possible. + radius = ( + ((self.x_pad - 5) // 3) + if self.x_pad < self.y_pad + else ((self.y_pad - 5) // 3) + ) if radius < 4: - radius = 0 # Don't round corners if too small - for x in range(12): - if isinstance(words[0], str): - word_list_index = WORDLIST.index(words[page * 12 + x]) + 1 + radius = 0 + # Determine whether we are working with words or numbers. + is_word_list = isinstance(words[0], str) + for row in range(12): + if is_word_list: + word_list_index = WORDLIST.index(words[page * 12 + row]) + 1 else: - word_list_index = words[page * 12 + x] - for y in range(12): - if (word_list_index >> (11 - y)) & 1: - x_offset = self.x_offset + 3 - x_offset += y * (self.x_pad) + word_list_index = words[page * 12 + row] + for bit in range(12): + if (word_list_index >> (11 - bit)) & 1: + x = self.x_offset + 3 + bit * self.x_pad self.ctx.display.fill_rectangle( - x_offset, - y_offset + 3, + x, + y + 3, self.x_pad - 5, self.y_pad - 5, theme.highlight_color, radius, ) - y_offset += self.y_pad + y += self.y_pad def export(self): """Shows seed as a punch pattern for Tiny Seed layout""" words = self.ctx.wallet.key.mnemonic.split(" ") - for page in range(len(words) // 12): + num_pages = len(words) // 12 + for page in range(num_pages): self._draw_labels(page) self._draw_grid() self._draw_punched(words, page) @@ -164,8 +157,6 @@ def export(self): def print_tiny_seed(self): """Creates a bitmap image of a punched Tiny Seed and sends it to a thermal printer""" - # Scale from original: 1.5X - from ..printers import create_printer self.printer = create_printer() @@ -173,144 +164,100 @@ def print_tiny_seed(self): image_size = 156 border_y = 8 border_x = 16 - grid_x_offset = border_x + 17 # border + 4,3mm*8px - grid_y_offset = border_y + 26 # border + 6,5mm*8px - pad_x = 7 # 1,75mm*8px - pad_y = 8 # 2mm*8px + grid_x_offset = border_x + 17 # border + offset + grid_y_offset = border_y + 26 # border + offset + pad_x = 7 # grid cell width in px + pad_y = 8 # grid cell height in px self.ctx.display.clear() self.ctx.display.draw_hcentered_text( t("Printing.."), self.ctx.display.height() // 2 ) self.printer.print_string("Tiny Seed\n\n") - for page in range(len(words) // 12): - # creates an image + num_pages = len(words) // 12 + for page in range(num_pages): ts_image = image.Image(size=(image_size, image_size), copy_to_fb=True) ts_image.clear() - # Frame - # Upper + # Draw the frame (borders) ts_image.draw_rectangle( - 0, - 0, - 109 + 2 * border_x, # 27,2mm*8px + ... - border_y, - lcd.WHITE, - fill=True, + 0, 0, 109 + 2 * border_x, border_y, lcd.WHITE, fill=True ) - # Lower ts_image.draw_rectangle( - 0, - 138 + border_y, - 109 + 2 * border_x, # 27,2mm*8px + ... - border_y, - lcd.WHITE, - fill=True, + 0, 138 + border_y, 109 + 2 * border_x, border_y, lcd.WHITE, fill=True ) - # Left ts_image.draw_rectangle(0, border_y, border_x, 138, lcd.WHITE, fill=True) - # Right ts_image.draw_rectangle( 109 + border_x, border_y, border_x, 138, lcd.WHITE, fill=True ) - # labels - y_offset = grid_y_offset - if FONT_HEIGHT > pad_y: - y_offset -= (FONT_HEIGHT - pad_y) // 2 + 1 - - # grid - y_offset = grid_y_offset - x_offset = grid_x_offset + # Draw grid lines + x = grid_x_offset + y = grid_y_offset for _ in range(13): - ts_image.draw_line( - x_offset, - y_offset, - x_offset, - y_offset + 12 * pad_y, - lcd.WHITE, - ) - x_offset += pad_x + ts_image.draw_line(x, y, x, y + 12 * pad_y, lcd.WHITE) + x += pad_x + y = grid_y_offset for _ in range(13): ts_image.draw_line( - grid_x_offset, - y_offset, - grid_x_offset + 12 * pad_x, - y_offset, - lcd.WHITE, + grid_x_offset, y, grid_x_offset + 12 * pad_x, y, lcd.WHITE ) - y_offset += pad_y - - # draw punched - y_offset = grid_y_offset - for y in range(12): - word_list_index = WORDLIST.index(words[page * 12 + y]) + 1 - for x in range(12): - if (word_list_index >> (11 - x)) & 1: - x_offset = grid_x_offset - x_offset += x * (pad_x) + y += pad_y + + # Draw punched bits into the grid. + y = grid_y_offset + for row in range(12): + if isinstance(words[0], str): + word_list_index = WORDLIST.index(words[page * 12 + row]) + 1 + else: + word_list_index = words[page * 12 + row] + for col in range(12): + if (word_list_index >> (11 - col)) & 1: + x = grid_x_offset + col * pad_x ts_image.draw_rectangle( - x_offset, y_offset, pad_x, pad_y, lcd.WHITE, fill=True + x, y, pad_x, pad_y, lcd.WHITE, fill=True ) - y_offset += pad_y + y += pad_y - # Convert image to bitmap bytes ts_image.to_grayscale() ts_image.binary([(125, 255)]) - # # Debug image - # lcd.display(ts_image, roi=(0, 0, image_size, image_size)) - # self.ctx.input.wait_for_button() - - # Print + # Print image line by line self.printer.set_bitmap_mode(image_size // 8, image_size, 3) for y in range(image_size): - line_bytes = bytes([]) + line_bytes = bytearray() x = 0 for _ in range(image_size // 8): im_byte = 0 for _ in range(8): - im_byte <<= 1 - if ts_image.get_pixel(x, y): - im_byte |= 1 + im_byte = (im_byte << 1) | ( + 1 if ts_image.get_pixel(x, y) else 0 + ) x += 1 - line_bytes += bytes([im_byte]) - # send line by line to be printed - self.printer.print_bitmap_line(line_bytes) + line_bytes.append(im_byte) + self.printer.print_bitmap_line(bytes(line_bytes)) wdt.feed() - grid_x_offset = border_x + 16 # 4,1mm*8px - grid_y_offset = border_y + 25 # 6,2mm*8px + # Adjust grid offsets for subsequent pages if needed. + grid_x_offset = border_x + 16 + grid_y_offset = border_y + 25 self.printer.feed(4) self.ctx.display.clear() def _draw_index(self, index): - """Outline index postition""" - width = 6 * self.x_pad - 2 + """Outline index position""" height = self.y_pad - 2 - y_position = index // 12 - y_position *= self.y_pad - y_position += self.y_offset + 1 + y_pos = (index // 12) * self.y_pad + self.y_offset + 1 if index < TS_LAST_BIT_NO_CS: - x_position = index % 12 - x_position *= self.x_pad - x_position += self.x_offset + 1 + x_pos = (index % 12) * self.x_pad + self.x_offset + 1 width = self.x_pad - 2 - self.ctx.display.outline( - x_position, - y_position, - width, - height, - theme.fg_color, - ) + self.ctx.display.outline(x_pos, y_pos, width, height, theme.fg_color) def _map_keys_array(self): """Maps an array of regions for keys to be placed in""" if self.ctx.input.touch is not None: - x_region = self.x_offset - for _ in range(13): - self.ctx.input.touch.x_regions.append(x_region) - x_region += self.x_pad - y_region = self.y_offset - for _ in range(13): - self.ctx.input.touch.y_regions.append(y_region) - y_region += self.y_pad + self.ctx.input.touch.x_regions = [ + self.x_offset + i * self.x_pad for i in range(13) + ] + self.ctx.input.touch.y_regions = [ + self.y_offset + i * self.y_pad for i in range(13) + ] self.ctx.input.touch.y_regions.append(self.ctx.display.height()) def _draw_disabled(self, w24=False): @@ -326,7 +273,7 @@ def _draw_disabled(self, w24=False): self.ctx.display.fill_rectangle( self.x_offset + 7 * self.x_pad, self.y_offset + 11 * self.y_pad, - 1 * self.x_pad, + self.x_pad, self.y_pad, theme.disabled_color, ) @@ -341,87 +288,29 @@ def _draw_disabled(self, w24=False): self.ctx.display.fill_rectangle( self.x_offset + 3 * self.x_pad, self.y_offset + 11 * self.y_pad, - 1 * self.x_pad, + self.x_pad, self.y_pad, theme.disabled_color, ) - def check_sum(self, tiny_seed_numbers): - """Dinamically calculates checksum""" - # Inspired in Jimmy Song's HDPrivateKey.from_mnemonic() method - binary_seed = bytearray() - offset = 0 - for tiny_seed_number in tiny_seed_numbers: - index = tiny_seed_number - 1 if tiny_seed_number > 1 else 0 - remaining = 11 - while remaining > 0: - bits_needed = 8 - offset - if remaining == bits_needed: - if bits_needed == 8: - binary_seed.append(index) - else: - binary_seed[-1] |= index - offset = 0 - remaining = 0 - elif remaining > bits_needed: - if bits_needed == 8: - binary_seed.append(index >> (remaining - 8)) - else: - binary_seed[-1] |= index >> (remaining - bits_needed) - remaining -= bits_needed - offset = 0 - # lop off the top 8 bits - index &= (1 << remaining) - 1 - else: - binary_seed.append(index << (8 - remaining)) - offset = remaining - remaining = 0 - checksum_length_bits = len(tiny_seed_numbers) * 11 // 33 - num_remainder = checksum_length_bits % 8 - if num_remainder: - checksum_length = checksum_length_bits // 8 + 1 - bits_to_ignore = 8 - num_remainder - else: - checksum_length = checksum_length_bits // 8 - bits_to_ignore = 0 - raw = bytes(binary_seed) - data = raw[:-checksum_length] - computed_checksum = bytearray(hashlib.sha256(data).digest()[:checksum_length]) - computed_checksum[-1] &= 256 - (1 << (bits_to_ignore + 1) - 1) - checksum = int.from_bytes(computed_checksum, "big") - checksum = checksum >> (8 - checksum_length_bits) - return checksum - - def toggle_bit(self, word, bit): - """Toggle bit state according to pressed index""" - return word ^ (1 << bit) - - def to_words(self, tiny_seed_numbers): - """Converts a list of numbers 1-2048 to a list of respective BIP39 words""" - words = [] - for number in tiny_seed_numbers: - words.append(WORDLIST[number - 1]) - return words - def _auto_checksum(self, word_numbers): - """Automatically modify last word do add checksum""" - checksum = self.check_sum(word_numbers) + """Automatically modify last word to add checksum""" + checksum = check_sum_bg(word_numbers) if len(word_numbers) == 12: - word_numbers[11] -= 1 - word_numbers[11] &= 0b11111110000 - word_numbers[11] += checksum + 1 + last_index = 11 + mask = 0b11111110000 else: - word_numbers[23] -= 1 - word_numbers[23] &= 0b11100000000 - word_numbers[23] += checksum + 1 + last_index = 23 + mask = 0b11100000000 + word_numbers[last_index] = ( + ((word_numbers[last_index] - 1) & mask) + checksum + 1 + ) return word_numbers def _new_index(self, index, btn, w24, page): def _last_editable_bit(): if w24: - if page == 0: - return TS_LAST_BIT_NO_CS - return TS_LAST_BIT_24W_CS + return TS_LAST_BIT_NO_CS if page == 0 else TS_LAST_BIT_24W_CS return TS_LAST_BIT_12W_CS if btn == BUTTON_PAGE: @@ -439,13 +328,11 @@ def _last_editable_bit(): elif index <= _last_editable_bit(): index -= 1 elif index <= TS_ESC_END_POSITION: - if w24: - if not page: - index = TS_LAST_BIT_NO_CS - else: - index = TS_LAST_BIT_24W_CS - else: - index = TS_LAST_BIT_12W_CS + index = ( + TS_LAST_BIT_NO_CS + if w24 and not page + else (TS_LAST_BIT_24W_CS if w24 else TS_LAST_BIT_12W_CS) + ) elif index <= TS_GO_POSITION: index = TS_ESC_END_POSITION return index @@ -455,25 +342,16 @@ def enter_tiny_seed(self, w24=False, seed_numbers=None, scanning_24=False): def _editable_bit(): if w24: - if page == 0: - if index <= TS_LAST_BIT_NO_CS: - return True - elif index <= TS_LAST_BIT_24W_CS: - return True - elif index <= TS_LAST_BIT_12W_CS: - return True - return False + return index <= (TS_LAST_BIT_NO_CS if page == 0 else TS_LAST_BIT_24W_CS) + return index <= TS_LAST_BIT_12W_CS - index = 0 + index = TS_GO_POSITION if seed_numbers else 0 if seed_numbers: tiny_seed_numbers = seed_numbers - # move index to Go position - index = TS_GO_POSITION elif w24: tiny_seed_numbers = [2048] * 23 + [1] else: tiny_seed_numbers = [2048] * 11 + [433] - btn = None self._map_keys_array() page = 0 menu_offset = self.y_offset + 12 * self.y_pad @@ -484,11 +362,11 @@ def _editable_bit(): self._draw_disabled(w24) tiny_seed_numbers = self._auto_checksum(tiny_seed_numbers) self._draw_punched(tiny_seed_numbers, page) - menu_index = None - if index >= TS_GO_POSITION: - menu_index = 1 - elif index >= TS_ESC_END_POSITION: - menu_index = 0 + menu_index = ( + 1 + if index >= TS_GO_POSITION + else (0 if index >= TS_ESC_START_POSITION else None) + ) proceed_menu(self.ctx, menu_offset, menu_index, t("Go"), t("Esc")) if self.ctx.input.buttons_active: self._draw_index(index) @@ -497,26 +375,25 @@ def _editable_bit(): btn = BUTTON_ENTER index = self.ctx.input.touch.current_index() if btn == BUTTON_ENTER: - if index > TS_ESC_END_POSITION: # go + if index > TS_ESC_END_POSITION: # "Go" if not w24 or (w24 and (page or scanning_24)): if scanning_24: return tiny_seed_numbers - return self.to_words(tiny_seed_numbers) + return to_words(tiny_seed_numbers) page += 1 - elif index >= TS_ESC_START_POSITION: # ESC + elif index >= TS_ESC_START_POSITION: # "Esc" self.ctx.display.clear() if self.prompt(t("Are you sure?"), self.ctx.display.height() // 2): break self._map_keys_array() elif _editable_bit(): - word_index = index // 12 - word_index += page * 12 + word_index = (index // 12) + page * 12 bit = 11 - (index % 12) if bit == 11: tiny_seed_numbers[word_index] = 2048 else: tiny_seed_numbers[word_index] = ( - self.toggle_bit(tiny_seed_numbers[word_index], bit) % 2048 + toggle_bit(tiny_seed_numbers[word_index], bit) % 2048 ) if tiny_seed_numbers[word_index] == 0: tiny_seed_numbers[word_index] = 2048 @@ -578,65 +455,30 @@ class TinyScanner(Page): def __init__(self, ctx, grid_type="Tiny Seed"): super().__init__(ctx, None) self.ctx = ctx - # Capturing flag used for first page of 24 words seed - self.capturing = False - # X, Y array map for punched area + self.capturing = False # Flag used for first page of 24-word seed self.x_regions = [] self.y_regions = [] self.time_frame = time.ticks_ms() self.previous_seed_numbers = [1] * 12 self.grid_settings = self.binary_grid_settings[grid_type] - if grid_type == "Binary Grid": - label = t("Binary Grid") - else: - label = grid_type - self.tiny_seed = TinySeed(self.ctx, label=label) + self.label = t("Binary Grid") if grid_type == "Binary Grid" else grid_type self.g_corners = (0x80, 0x80, 0x80, 0x80) self.blob_otsu = 0x80 + # Cache board type to avoid repeated lookups. + self.board_type = board.config.get("type", "") def _map_punches_region(self, rect_size, page=0): - # Think in portrait mode, with Tiny Seed tilted 90 degrees + """Calculate x and y coordinates for punched grid regions.""" self.x_regions = [] self.y_regions = [] - if not page: - if board.config["type"] == "amigo": - # Amigo has mirrored coordinates - x_offset = ( - rect_size[0] - + rect_size[2] * self.grid_settings["x_offset_factor_amigo_p0"] - ) - y_offset = ( - rect_size[1] - + rect_size[3] * self.grid_settings["y_offset_factor_amigo_p0"] - ) - else: - x_offset = ( - rect_size[0] - + rect_size[2] * self.grid_settings["x_offset_factor_p0"] - ) - y_offset = ( - rect_size[1] - + rect_size[3] * self.grid_settings["y_offset_factor_p0"] - ) + if self.board_type == "amigo": + offset_key_x = "x_offset_factor_amigo_p{}".format(page) + offset_key_y = "y_offset_factor_amigo_p{}".format(page) else: - if board.config["type"] == "amigo": - x_offset = ( - rect_size[0] - + rect_size[2] * self.grid_settings["x_offset_factor_amigo_p1"] - ) - y_offset = ( - rect_size[1] - + rect_size[3] * self.grid_settings["y_offset_factor_amigo_p1"] - ) - else: - x_offset = ( - rect_size[0] - + rect_size[2] * self.grid_settings["x_offset_factor_p1"] - ) - y_offset = ( - rect_size[1] - + rect_size[3] * self.grid_settings["y_offset_factor_p1"] - ) + offset_key_x = "x_offset_factor_p{}".format(page) + offset_key_y = "y_offset_factor_p{}".format(page) + x_offset = rect_size[0] + rect_size[2] * self.grid_settings[offset_key_x] + y_offset = rect_size[1] + rect_size[3] * self.grid_settings[offset_key_y] self.x_regions.append(int(x_offset)) self.y_regions.append(int(y_offset)) x_pad = rect_size[2] * self.grid_settings["xpad_factor"] @@ -654,12 +496,8 @@ def _valid_numbers(self, data): return True def _gradient_corners(self, rect, img): - """Calcule histogram for four corners of tinyseed to be later - used as a gradient reference threshold""" - - # Regions: Upper left, upper right, lower left and lower right - # are corner fractions of main TinySeed rectangle - if not board.config["type"] == "amigo": + """Compute histogram thresholds from four corners of the Tiny Seed region.""" + if self.board_type != "amigo": region_ul = ( rect[0] + rect[2] // 8, rect[1] + rect[3] // 30, @@ -726,68 +564,40 @@ def _gradient_corners(self, rect, img): # img.draw_string(70,40,str(gradient_bg_ur)) # img.draw_string(10,55,str(gradient_bg_ll)) # img.draw_string(70,55,str(gradient_bg_lr)) - # Compute Otsu threshold for each corner try: - gradient_bg_ul = img.get_histogram(roi=region_ul).get_threshold().value() - gradient_bg_ur = img.get_histogram(roi=region_ur).get_threshold().value() - gradient_bg_ll = img.get_histogram(roi=region_ll).get_threshold().value() - gradient_bg_lr = img.get_histogram(roi=region_lr).get_threshold().value() - self.g_corners = ( - gradient_bg_ul, - gradient_bg_ur, - gradient_bg_ll, - gradient_bg_lr, - ) - except: + grad_ul = img.get_histogram(roi=region_ul).get_threshold().value() + grad_ur = img.get_histogram(roi=region_ur).get_threshold().value() + grad_ll = img.get_histogram(roi=region_ll).get_threshold().value() + grad_lr = img.get_histogram(roi=region_lr).get_threshold().value() + self.g_corners = (grad_ul, grad_ur, grad_ll, grad_lr) + except Exception: pass def _gradient_value(self, index): - """Calculates a reference threshold according to a linear - interpolation gradient of luminosity from 4 corners of Tiny Seed""" - ( - gradient_bg_ul, - gradient_bg_ur, - gradient_bg_ll, - gradient_bg_lr, - ) = self.g_corners - - y_position = index % 12 - x_position = index // 12 - gradient_upper_x = ( - gradient_bg_ul * (11 - x_position) + gradient_bg_ur * x_position - ) // 11 - gradient_lower_x = ( - gradient_bg_ll * (11 - x_position) + gradient_bg_lr * x_position - ) // 11 - gradient = ( - gradient_upper_x * (11 - y_position) + gradient_lower_x * y_position - ) // 11 - - # Average filter - # Here you can change the relevance of the gradient vs medium luminance as a reference - filtered = ( - gradient_bg_ul + gradient_bg_ur + gradient_bg_ll + gradient_bg_lr - ) # weight 4/6 - 67% average - filtered += 6 * gradient # weight 6/10 = 60% raw gradient - filtered //= 10 + """Calculate an interpolated threshold from the four corner gradients.""" + grad_ul, grad_ur, grad_ll, grad_lr = self.g_corners + y_pos = index % 12 + x_pos = index // 12 + gradient_upper = (grad_ul * (11 - x_pos) + grad_ur * x_pos) // 11 + gradient_lower = (grad_ll * (11 - x_pos) + grad_lr * x_pos) // 11 + gradient = (gradient_upper * (11 - y_pos) + gradient_lower * y_pos) // 11 + # Weighted average with the overall corner average: + filtered = (grad_ul + grad_ur + grad_ll + grad_lr + 6 * gradient) // 10 return filtered - # return gradient #pure gradient - def _detect_tiny_seed(self, img): - """Detects Tiny Seed as a bright blob against a dark surface""" - - # Load settings for the grid type we are using + """Detect the Tiny Seed region as a bright blob.""" aspect_low = self.grid_settings["aspect_low"] aspect_high = self.grid_settings["aspect_high"] - def _choose_rect(rects): - # Choose the best rectangle based on aspect ratio + def choose_rect(rects): best_rect = None - best_aspect_diff = float("inf") + best_diff = float("inf") medium_aspect = (aspect_low + aspect_high) / 2 for rect in rects: + if rect[3] == 0: + continue aspect = rect[2] / rect[3] if ( rect[0] >= 0 @@ -796,9 +606,9 @@ def _choose_rect(rects): and (rect[1] + rect[3]) < img.height() and aspect_low < aspect < aspect_high ): - aspect_diff = abs(aspect - medium_aspect) - if aspect_diff < best_aspect_diff: - best_aspect_diff = aspect_diff + diff = abs(aspect - medium_aspect) + if diff < best_diff: + best_diff = diff best_rect = rect return best_rect @@ -808,30 +618,16 @@ def _choose_rect(rects): pass blob_threshold = [(self.blob_otsu, 255)] blobs = img.find_blobs( - blob_threshold, - x_stride=30, - y_stride=30, - area_threshold=5000, + blob_threshold, x_stride=30, y_stride=30, area_threshold=5000 ) - # Debug: Optionally draw the blobs to see them during development # for blob in blobs: # img.draw_rectangle(blob.rect(), color=(255, 125 * attempts, 0), thickness=3) - - # Choose the best rectangle that matches the aspect ratio - rect = _choose_rect([blob.rect() for blob in blobs]) - - # If a rectangle was found, draw it + rect = choose_rect([blob.rect() for blob in blobs]) if rect: - outline = ( - rect[0] - 1, - rect[1] - 1, - rect[2] + 1, - rect[3] + 1, - ) + outline = (rect[0] - 1, rect[1] - 1, rect[2] + 1, rect[3] + 1) thickness = 4 if self.capturing else 2 img.draw_rectangle(outline, lcd.WHITE, thickness=thickness) - return rect def _draw_grid(self, img): @@ -853,16 +649,18 @@ def _draw_grid(self, img): ) def _detect_and_draw_punches(self, img): - """Applies gradient threshold to detect punched(black painted) bits""" + """Detect punched bits on the grid and update the seed numbers accordingly.""" page_seed_numbers = [0] * 12 index = 0 pad_x = self.x_regions[1] - self.x_regions[0] pad_y = self.y_regions[1] - self.y_regions[0] - if pad_x < 4 or pad_y < 4: # Punches are too small + if pad_x < 4 or pad_y < 4: return page_seed_numbers - y_map = self.y_regions[0:-1] - x_map = self.x_regions[0:-1] - if board.config["type"] == "amigo": + + # Prepare mapping (reverse one axis based on board type) + y_map = self.y_regions[:-1][:] + x_map = self.x_regions[:-1][:] + if self.board_type == "amigo": x_map.reverse() else: y_map.reverse() @@ -870,9 +668,7 @@ def _detect_and_draw_punches(self, img): # Loop ahead will sweep TinySeed bits/dots and evaluate its luminosity for x in x_map: for y in y_map: - # Define the dot rectangle area to be evaluated eval_rect = (x + 2, y + 2, pad_x - 3, pad_y - 3) - # Evaluate dot's median luminosity dot_l = img.get_statistics(roi=eval_rect).median() # # Debug gradient @@ -887,18 +683,16 @@ def _detect_and_draw_punches(self, img): # Defines a threshold to evaluate if the dot is considered punched punch_threshold = self._gradient_value(index) - # Sensor image will be downscaled on small displays punch_thickness = ( 1 if self.ctx.display.height() > SMALLEST_HEIGHT else 2 ) - # If the dot is punched, draws a rectangle and toggle respective bit if dot_l < punch_threshold: - _ = img.draw_rectangle( + img.draw_rectangle( eval_rect, thickness=punch_thickness, color=lcd.WHITE ) word_index = index // 12 bit = 11 - (index % 12) - page_seed_numbers[word_index] = self.tiny_seed.toggle_bit( + page_seed_numbers[word_index] = toggle_bit( page_seed_numbers[word_index], bit ) index += 1 @@ -916,22 +710,22 @@ def _exit_camera(self): self.ctx.display.clear() def _check_buttons(self, w24, page): - enter_or_touch = self.ctx.input.enter_event() or self.ctx.input.touch_event( + if self.ctx.input.enter_event() or self.ctx.input.touch_event( validate_position=False - ) - if w24: - if page == 0 and enter_or_touch: + ): + if w24 and page == 0: self.capturing = True - elif enter_or_touch: - return True + else: + return True if self.ctx.input.page_event() or self.ctx.input.page_prev_event(): return True return False def _process_12w_scan(self, page_seed_numbers): - if ( - self.tiny_seed.check_sum(page_seed_numbers) - == (page_seed_numbers[11] - 1) & 0b00000001111 + if not self._valid_numbers(page_seed_numbers): + return None + if check_sum_bg(page_seed_numbers) == ( + (page_seed_numbers[11] - 1) & 0b00000001111 ): if page_seed_numbers == self.previous_seed_numbers: self._exit_camera() @@ -940,10 +734,11 @@ def _process_12w_scan(self, page_seed_numbers): ) self.ctx.input.wait_for_button() self.ctx.display.clear() - words = self.tiny_seed.enter_tiny_seed(seed_numbers=page_seed_numbers) - if words: # If words confirmed + words = TinySeed(self.ctx, label=self.label).enter_tiny_seed( + seed_numbers=page_seed_numbers + ) + if words: return words - # Else esc command was given, turn camera on again and reset words self.flash_text( t("Scanning words 1-12 again") + "\n\n" + t("Wait for the capture") ) @@ -955,18 +750,18 @@ def _process_12w_scan(self, page_seed_numbers): def _process_24w_pg0_scan(self, page_seed_numbers): if page_seed_numbers == self.previous_seed_numbers and self.capturing: - # Flush events ocurred while processing self.ctx.input.reset_ios_state() - self._exit_camera() self.ctx.display.draw_centered_text( t("Review scanned data, edit if necessary") ) self.ctx.input.wait_for_button() self.ctx.display.clear() - words = self.tiny_seed.enter_tiny_seed(True, page_seed_numbers, True) + words = TinySeed(self.ctx, label=self.label).enter_tiny_seed( + True, page_seed_numbers, True + ) self.capturing = False - if words is not None: # Fisrt 12 words confirmed, moving to 13-24 + if words is not None: self.flash_text( t("Scanning words 13-24") + "\n\n" + t("Wait for the capture") ) @@ -976,76 +771,67 @@ def _process_24w_pg0_scan(self, page_seed_numbers): self.flash_text( t("Scanning words 1-12 again") + "\n\n" + t("TOUCH or ENTER to capture") ) - self._run_camera() # Run camera and rotate screen after message was given + self._run_camera() elif self._valid_numbers(page_seed_numbers): self.previous_seed_numbers = page_seed_numbers return None def scanner(self, w24=False): - """Uses camera sensor to scan punched pattern on Tiny Seed format""" + """Scans the Tiny Seed using the camera sensor.""" page = 0 if w24: w24_seed_numbers = [0] * 24 self.previous_seed_numbers = [1] * 12 self.ctx.display.clear() - message = t("Wait for the capture") - if w24 and page == 0: - message = t("TOUCH or ENTER to capture") + message = ( + t("TOUCH or ENTER to capture") + if (w24 and page == 0) + else t("Wait for the capture") + ) self.ctx.display.draw_centered_text(message) precamera_ticks = time.ticks_ms() self.ctx.camera.initialize_run(mode=BINARY_GRID_MODE) + self.ctx.camera.zoom_mode() self.ctx.display.to_landscape() postcamera_ticks = time.ticks_ms() - # check how much time camera took to retain message on the screen - if precamera_ticks + FLASH_MSG_TIME > postcamera_ticks: - time.sleep_ms(precamera_ticks + FLASH_MSG_TIME - postcamera_ticks) - del message, precamera_ticks, postcamera_ticks - # Flush events ocurred while starting + delay = precamera_ticks + FLASH_MSG_TIME - postcamera_ticks + if delay > 0: + time.sleep_ms(delay) self.ctx.input.reset_ios_state() - # # Debug FPS 1/4 - # clock = time.clock() - # fps = 0 self.ctx.display.clear() + while True: - # # Debug FPS 2/4 - # clock.tick() wdt.feed() page_seed_numbers = None img = self.ctx.camera.snapshot() rect = self._detect_tiny_seed(img) if rect: self._gradient_corners(rect, img) - # print(self.g_corners) - # map_regions self._map_punches_region(rect, page) page_seed_numbers = self._detect_and_draw_punches(img) self._draw_grid(img) - if board.config["type"] == "m5stickv": + if self.board_type == "m5stickv": img.lens_corr(strength=1.0, zoom=0.56) - # # Debug FPS 3/4 - # img.draw_string(10,100,str(fps)) - if board.config["type"] == "amigo": - # Centralize image on top Amigo's screen + if self.board_type == "amigo": lcd.display(img, oft=(80, 40)) else: lcd.display(img) if page_seed_numbers: if w24: - if page == 0: # Scanning first 12 words (page 0) + if page == 0: first_page = self._process_24w_pg0_scan(page_seed_numbers) - if first_page: # If page 0 confirmed, move to page 1 + if first_page: w24_seed_numbers[0:12] = first_page page = 1 - else: # Scanning words 13-24 (page 1) + else: w24_seed_numbers[12:24] = page_seed_numbers - if ( - self.tiny_seed.check_sum(w24_seed_numbers) - == (w24_seed_numbers[23] - 1) & 0b00011111111 + if check_sum_bg(w24_seed_numbers) == ( + (w24_seed_numbers[23] - 1) & 0b00011111111 ): if page_seed_numbers == self.previous_seed_numbers: self._exit_camera() - return self.tiny_seed.to_words(w24_seed_numbers) + return to_words(w24_seed_numbers) self.previous_seed_numbers = page_seed_numbers else: words = self._process_12w_scan(page_seed_numbers) @@ -1053,8 +839,39 @@ def scanner(self, w24=False): return words if self._check_buttons(w24, page): break - # # Debug FPS 4/4 - # fps = clock.fps() self._exit_camera() return None + + +def toggle_bit(word, bit): + """Toggle bit state according to pressed index""" + return word ^ (1 << bit) + + +def to_words(tiny_seed_numbers): + """Converts a list of numbers 1-2048 to a list of respective BIP39 words""" + return [WORDLIST[num - 1] for num in tiny_seed_numbers] + + +def check_sum_bg(tiny_seed_numbers): + """Dinamically calculates checksum of binary grid""" + total_bits = len(tiny_seed_numbers) * 11 + # Calculate the number of checksum bits. + checksum_length = total_bits // 33 + + # Accumulate the bits from the Binary Grid words. + acc = 0 + for n in tiny_seed_numbers: + if not n: + raise ValueError("Invalid Binary Grid number") + # Subtract 1 because Binary Grid values are 1-indexed. + acc = (acc << 11) | (n - 1) + + # The bitstream is: [entropy bits][checksum bits] + # Remove the checksum bits by right-shifting. + entropy_int = acc >> checksum_length + entropy_bytes_count = 16 if checksum_length == 4 else 32 + data = entropy_int.to_bytes(entropy_bytes_count, "big") + + return entropy_checksum(data, checksum_length) diff --git a/src/krux/pages/wallet_settings.py b/src/krux/pages/wallet_settings.py index ac920a27a..3a16464cf 100644 --- a/src/krux/pages/wallet_settings.py +++ b/src/krux/pages/wallet_settings.py @@ -43,14 +43,22 @@ TYPE_SINGLESIG, TYPE_MULTISIG, TYPE_MINISCRIPT, + DERIVATION_PATH_SYMBOL, + POLICY_TYPE_IDS, ) from ..settings import ( MAIN_TXT, TEST_TXT, + NAME_SINGLE_SIG, + NAME_MULTISIG, + NAME_MINISCRIPT, ) from ..key import P2PKH, P2SH_P2WPKH, P2WPKH, P2WSH, P2TR PASSPHRASE_MAX_LEN = 200 +DERIVATION_KEYPAD = "123456789/0h" + +MINISCRIPT_DEFAULT_DERIVATION = "m/48h/0h/0h/2h" class PassphraseEditor(Page): @@ -119,26 +127,29 @@ def customize_wallet(self, key): policy_type = key.policy_type script_type = key.script_type account = key.account_index + custom_derivation = key.derivation if key.custom_derivation else None while True: - derivation_path = "m/" - if policy_type == TYPE_SINGLESIG: - derivation_path += str(SINGLESIG_SCRIPT_PURPOSE[script_type]) - elif policy_type == TYPE_MULTISIG: - derivation_path += str(MULTISIG_SCRIPT_PURPOSE) - elif policy_type == TYPE_MINISCRIPT: - # For now, miniscript is the same as multisig - derivation_path += str(MINISCRIPT_PURPOSE) - derivation_path += "'/" - derivation_path += "0'" if network == NETWORKS[MAIN_TXT] else "1'" - derivation_path += "/" + str(account) + "'" - if policy_type in (TYPE_MULTISIG, TYPE_MINISCRIPT): - derivation_path += "/2'" + wallet_info = network["name"] + "\n" + # Find the policy type string from the POLICY_TYPE_IDS dictionary + policy_type_str = "" + for k, v in POLICY_TYPE_IDS.items(): + if v == policy_type: + policy_type_str = k + break + wallet_info += policy_type_str + "\n" + wallet_info += str(script_type).upper() + "\n" + if policy_type != TYPE_MINISCRIPT or not custom_derivation: + derivation_path = self._derivation_path_str( + policy_type, script_type, network, account + ) + custom_derivation = "" + else: + derivation_path = custom_derivation + wallet_info += DERIVATION_PATH_SYMBOL + " " + derivation_path self.ctx.display.clear() derivation_path = self.fit_to_line(derivation_path, crop_middle=False) - info_len = self.ctx.display.draw_hcentered_text( - derivation_path, info_box=True - ) + info_len = self.ctx.display.draw_hcentered_text(wallet_info, info_box=True) submenu = Menu( self.ctx, [ @@ -146,9 +157,16 @@ def customize_wallet(self, key): (t("Policy Type"), lambda: None), ( t("Script Type"), - (lambda: None) if policy_type == TYPE_SINGLESIG else None, + (lambda: None) if policy_type != TYPE_MULTISIG else None, + ), + ( + ( + t("Account") + if policy_type != TYPE_MINISCRIPT + else t("Derivation Path") + ), + lambda: None, ), - (t("Account"), lambda: None), ], offset=info_len * FONT_HEIGHT + DEFAULT_PADDING, ) @@ -167,17 +185,36 @@ def customize_wallet(self, key): # If is single-sig, and script is p2wsh, force to pick a new type script_type = self._script_type() script_type = P2WPKH if script_type is None else script_type + + elif policy_type == TYPE_MULTISIG: + # If is multisig, force to p2wsh + script_type = P2WSH + + elif policy_type == TYPE_MINISCRIPT and script_type not in ( + P2WSH, + P2TR, + ): + # If is miniscript, pick P2WSH or P2TR + script_type = self._miniscript_type() + script_type = P2WSH if script_type is None else script_type + elif index == 2: - new_script_type = self._script_type() + if policy_type == TYPE_MINISCRIPT: + new_script_type = self._miniscript_type() + else: + new_script_type = self._script_type() if new_script_type is not None: script_type = new_script_type elif index == 3: - account_temp = self._account(account) - if account_temp is not None: - account = account_temp - if policy_type != TYPE_SINGLESIG: - script_type = P2WSH - return network, policy_type, script_type, account + if policy_type != TYPE_MINISCRIPT: + new_account = self._account(account) + if new_account is not None: + account = new_account + else: + new_derivation_path = self._derivation_path(derivation_path) + if new_derivation_path is not None: + custom_derivation = new_derivation_path + return network, policy_type, script_type, account, derivation_path def _coin_type(self): """Network selection menu""" @@ -199,9 +236,9 @@ def _policy_type(self): submenu = Menu( self.ctx, [ - ("Single-sig", lambda: MENU_EXIT), - ("Multisig", lambda: MENU_EXIT), - ("Miniscript (Experimental)", lambda: MENU_EXIT), + (NAME_SINGLE_SIG, lambda: MENU_EXIT), + (NAME_MULTISIG, lambda: MENU_EXIT), + (NAME_MINISCRIPT + " (Experimental)", lambda: MENU_EXIT), ], disable_statusbar=True, ) @@ -227,6 +264,21 @@ def _script_type(self): return None return script_type + def _miniscript_type(self): + """Script type selection menu for miniscript policy type""" + submenu = Menu( + self.ctx, + [ + ("Native Segwit - P2WSH", lambda: P2WSH), + ("Taproot - P2TR", lambda: P2TR), + ], + disable_statusbar=True, + ) + index, script_type = submenu.run_loop() + if index == len(submenu.menu) - 1: + return None + return script_type + def _account(self, initial_account=None): """Account input""" account = self.capture_from_keypad( @@ -248,3 +300,70 @@ def _account(self, initial_account=None): ) return None return account + + def _derivation_path_str(self, policy_type, script_type, network, account): + derivation_path = "m/" + if policy_type == TYPE_SINGLESIG: + derivation_path += str(SINGLESIG_SCRIPT_PURPOSE[script_type]) + elif policy_type == TYPE_MULTISIG: + derivation_path += str(MULTISIG_SCRIPT_PURPOSE) + elif policy_type == TYPE_MINISCRIPT: + # For now, miniscript is the same as multisig + derivation_path += str(MINISCRIPT_PURPOSE) + derivation_path += "h/" + derivation_path += "0h" if network == NETWORKS[MAIN_TXT] else "1h" + derivation_path += "/" + str(account) + "h" + if policy_type in (TYPE_MULTISIG, TYPE_MINISCRIPT): + derivation_path += "/2h" + return derivation_path + + def _derivation_path(self, derivation): + """Derivation path input""" + while True: + derivation = self.capture_from_keypad( + t("Derivation Path"), + [DERIVATION_KEYPAD], + starting_buffer=(str(derivation) if derivation is not None else ""), + delete_key_fn=lambda x: x[:-1] if len(x) > 2 else x, + ) + if derivation == ESC_KEY: + return None + nodes = derivation.split("/")[1:] + + # Check node numbers are valid + valid_nodes = True + for node in nodes: + if not node: + valid_nodes = False + break + try: + if node[-1] == "h": + node = node[:-1] + if not 0 <= int(node) < HARDENED_INDEX: + raise ValueError + except ValueError: + valid_nodes = False + break + if not valid_nodes: + self.flash_error( + t("Invalid derivation path"), + ) + continue + + # Check if all nodes are hardened + not_hardened_txt = "" + for i, node in enumerate(nodes): + if node[-1] != "h": + not_hardened_txt += "Node {}: {}\n".format(i, node) + if not_hardened_txt: + self.ctx.display.clear() + if not self.prompt( + t("Some nodes are not hardened:") + + "\n" + + not_hardened_txt + + t("Proceed?"), + self.ctx.display.height() // 2, + ): + # Allow user to edit the derivation path + continue + return derivation diff --git a/src/krux/psbt.py b/src/krux/psbt.py index 7e7a01b6a..4431c82ee 100644 --- a/src/krux/psbt.py +++ b/src/krux/psbt.py @@ -20,8 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. import gc -from embit.psbt import PSBT -from embit.bip32 import HARDENED_INDEX +from embit.psbt import PSBT, CompressMode from ur.ur import UR import urtypes from urtypes.crypto import CRYPTO_PSBT @@ -29,7 +28,7 @@ from .krux_settings import t from .settings import THIN_SPACE from .qr import FORMAT_PMOFN, FORMAT_BBQR -from .key import Key, P2PKH, P2SH, P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH, P2TR +from .key import Key, P2SH, P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH, P2TR from .sats_vb import SatsVB # PSBT Output Types: @@ -40,6 +39,8 @@ # We always uses thin spaces after the ₿ in this file BTC_SYMBOL = "₿" +MAX_POLICY_COSIGNERS_DISPLAYED = 5 + class Counter(dict): """Helper class for dict""" @@ -68,7 +69,7 @@ def __init__(self, wallet, psbt_data, qr_format, psbt_filename=None): file_path = "/%s/%s" % (SD_PATH, psbt_filename) try: with open(file_path, "rb") as file: - self.psbt = PSBT.read_from(file, compress=1) + self.psbt = PSBT.read_from(file) self.validate() except: try: @@ -81,11 +82,12 @@ def __init__(self, wallet, psbt_data, qr_format, psbt_filename=None): psbt_data = file.read() self.psbt = PSBT.parse(base_decode(psbt_data, 64)) else: - # Legacy will fail to get policy from compressed PSBT - # so we load it uncompressed + # Try to load the PSBT in compressed mode with open(file_path, "rb") as file: file.seek(0) # Reset the file pointer to the beginning - self.psbt = PSBT.read_from(file) + self.psbt = PSBT.read_from( + file, compress=CompressMode.CLEAR_ALL + ) except Exception as e: raise ValueError("Error loading PSBT file: %s" % e) self.base_encoding = 64 # In case it is exported as QR code @@ -148,15 +150,18 @@ def validate(self): """Validates the PSBT""" # From: https://github.com/diybitcoinhardware/embit/blob/master/examples/change.py#L110 xpubs = [] + origin_less_xpub = None try: - xpubs = self.xpubs() + xpubs, origin_less_xpub = self.xpubs() except: # Expected to fail to get xpubs from Miniscript PSBT pass for inp in self.psbt.inputs: # get policy of the input try: - inp_policy = self.get_policy_from_psbt_input(inp, xpubs) + inp_policy = self.get_policy_from_psbt_input( + inp, xpubs, origin_less_xpub + ) except: raise ValueError("Unable to get policy") # if policy is None - assign current @@ -183,7 +188,7 @@ def validate(self): if self.wallet.policy != self.policy: raise ValueError("policy mismatch") - def get_policy_from_psbt_input(self, tx_input, xpubs): + def get_policy_from_psbt_input(self, tx_input, xpubs, origin_less_xpub=None): """Extracts the scriptPubKey from an input's UTXO and determines the policy.""" if tx_input.witness_utxo: scriptpubkey = tx_input.witness_utxo.script_pubkey @@ -193,10 +198,10 @@ def get_policy_from_psbt_input(self, tx_input, xpubs): else: raise ValueError("No UTXO information available in the input.") - return get_policy(tx_input, scriptpubkey, xpubs) + return get_policy(tx_input, scriptpubkey, xpubs, origin_less_xpub) def path_mismatch(self): - """Verifies if the PSBT path matches wallet's derivation path""" + """Verifies if the PSBT key path matches loaded keys's derivation path""" mismatched_paths = [] der_path_nodes = len(self.wallet.key.derivation.split("/")) - 1 for _input in self.psbt.inputs: @@ -205,10 +210,16 @@ def path_mismatch(self): else: derivations = _input.bip32_derivations for pubkey in derivations: + # Checks if fingerprint belongs to loaded key + if self.policy["type"] == P2TR: + fingerprint = derivations[pubkey][1].fingerprint + else: + fingerprint = derivations[pubkey].fingerprint + if fingerprint != self.wallet.key.fingerprint: + # Not our key, won't check derivation path mismatch + continue if self.policy["type"] == P2TR: - derivation_path = derivations[pubkey][ - 1 - ].derivation # ignore taproot leaf + derivation_path = derivations[pubkey][1].derivation else: derivation_path = derivations[pubkey].derivation textual_path = "m" @@ -224,32 +235,15 @@ def path_mismatch(self): return Key.format_derivation(", ".join(mismatched_paths)) return "" - def is_our_miniscript_output(self, psbt_output): - """Check if the output is from our wallet""" - - # TODO: Is comparing fingerprints and paths a trustable method? - derivations = psbt_output.bip32_derivations - if not derivations: - return False - for _, derivation in derivations.items(): - # Check if the fingerprint matches - if derivation.fingerprint == self.wallet.key.fingerprint: - # Verify the derivation path - expected_path_str = self.wallet.key.derivation.split("/")[1:] - expected_path_nodes = [] - for p in expected_path_str: - if "h" in p: - expected_path_nodes.append(int(p[:-1]) + HARDENED_INDEX) - else: - expected_path_nodes.append(int(p)) + def address_belongs_to_descriptor(self, psbt_output): + """Check if the output is from our wallet descriptor""" - if derivation.derivation[:4] == expected_path_nodes: - return True + if self.wallet.descriptor: + return self.wallet.descriptor.owns(psbt_output) return False - def _classify_output(self, out_policy, i, out): + def _classify_output(self, out_policy, out): """Classify the output based on its properties and policy""" - from embit import script address_from_my_wallet = False address_is_change = False @@ -262,45 +256,14 @@ def _classify_output(self, out_policy, i, out): # so we only need to check that scriptpubkey is generated from # witness script - # empty script by default - sc = script.Script(b"") - if self.policy["type"] == P2WSH: - try: - # if multisig, we know witness script - sc = script.p2wsh(out.witness_script) - except: - # Expected to fail with Miniscript PSBT - pass - elif self.policy["type"] == P2SH_P2WSH: - sc = script.p2sh(script.p2wsh(out.witness_script)) - # single-sig - elif "pkh" in self.policy["type"]: - if len(list(out.bip32_derivations.values())) > 0: - der = list(out.bip32_derivations.values())[0].derivation - my_hd_prvkey = self.wallet.key.root.derive(der) - if self.policy["type"] == P2WPKH: - sc = script.p2wpkh(my_hd_prvkey) - elif self.policy["type"] == P2SH_P2WPKH: - sc = script.p2sh(script.p2wpkh(my_hd_prvkey)) - elif self.policy["type"] == P2PKH: - sc = script.p2pkh(my_hd_prvkey) + address_from_my_wallet = self.address_belongs_to_descriptor(out) if self.policy["type"] == P2TR: - address_from_my_wallet = ( - len(list(out.taproot_bip32_derivations.values())) > 0 - ) if address_from_my_wallet: # _ = leafs _, der = list(out.taproot_bip32_derivations.values())[0] - address_is_change = der.derivation[3] == 1 + address_is_change = der.derivation[-2] == 1 else: - if sc.data: - address_from_my_wallet = ( - sc.data == self.psbt.tx.vout[i].script_pubkey.data - ) - else: - # If the script is empty, we compare fingerprints and paths - address_from_my_wallet = self.is_our_miniscript_output(out) if address_from_my_wallet: address_is_change = ( len(list(out.bip32_derivations.values())) > 0 @@ -342,15 +305,18 @@ def outputs(self): output_policy_count = Counter() xpubs = [] + origin_less_xpub = None try: - xpubs = self.xpubs() + xpubs, origin_less_xpub = self.xpubs() except: # Expected to fail to get xpubs from Miniscript PSBT pass for i, out in enumerate(self.psbt.outputs): - out_policy = get_policy(out, self.psbt.tx.vout[i].script_pubkey, xpubs) + out_policy = get_policy( + out, self.psbt.tx.vout[i].script_pubkey, xpubs, origin_less_xpub + ) output_policy_count[out_policy["type"]] += 1 - output_type = self._classify_output(out_policy, i, out) + output_type = self._classify_output(out_policy, out) if output_type == CHANGE: change_list.append( @@ -504,35 +470,47 @@ def _fill_zero_fingerprint_scope(self, scope): filled += 1 return filled - def sign(self): - """Signs the PSBT removing all irrelevant data""" + def sign(self, trim=True): + """Signs the PSBT and preserves necessary fields for the final transaction""" self.add_signatures() + if not trim: + return + trimmed_psbt = PSBT(self.psbt.tx) for i, inp in enumerate(self.psbt.inputs): - # Copy the final_scriptwitness if it's present (Taproot case) + # Copy the final_scriptwitness if present if inp.final_scriptwitness: trimmed_psbt.inputs[i].final_scriptwitness = inp.final_scriptwitness - # Copy partial signatures for multisig or other script types + + # Copy any partial signatures if inp.partial_sigs: trimmed_psbt.inputs[i].partial_sigs = inp.partial_sigs - # Include the PSBT_IN_WITNESS_UTXO field if it exists - if hasattr(inp, "witness_utxo"): + # Preserve witness UTXO if present + if inp.witness_utxo: trimmed_psbt.inputs[i].witness_utxo = inp.witness_utxo - # Include the PSBT_IN_NON_WITNESS_UTXO field if it exists (Legacy) - if hasattr(inp, "non_witness_utxo"): + # Preserve non-witness UTXO if present (for legacy inputs) + if inp.non_witness_utxo: trimmed_psbt.inputs[i].non_witness_utxo = inp.non_witness_utxo - # Check for P2SH (Nested SegWit) and copy redeem_script if present - if hasattr(inp, "redeem_script"): + # Preserve redeem_script for P2SH or nested SegWit + if inp.redeem_script: trimmed_psbt.inputs[i].redeem_script = inp.redeem_script - # Check for P2WSH (SegWit multisig) and copy witness_script if present - if hasattr(inp, "witness_script"): + # Preserve witness_script for P2WSH multisig + if inp.witness_script: trimmed_psbt.inputs[i].witness_script = inp.witness_script + # Preserve taproot_key_sig for Taproot inputs + if inp.taproot_key_sig: + trimmed_psbt.inputs[i].taproot_key_sig = inp.taproot_key_sig + + # Preserve taproot script path sigs + if inp.taproot_sigs: + trimmed_psbt.inputs[i].taproot_sigs = inp.taproot_sigs + self.psbt = trimmed_psbt def psbt_qr(self): @@ -570,24 +548,65 @@ def xpubs(self): from embit.psbt import DerivationPath if self.psbt.xpubs: - return self.psbt.xpubs + return self.psbt.xpubs, None if not self.wallet.descriptor: raise ValueError("missing xpubs") descriptor_keys = ( - [self.wallet.descriptor.key] - if self.wallet.descriptor.key - else self.wallet.descriptor.keys + self.wallet.descriptor.keys + if self.wallet.descriptor.keys + else [self.wallet.descriptor.key] ) xpubs = {} + origin_less_xpub = None for descriptor_key in descriptor_keys: if descriptor_key.origin: # Pure xpub descriptors (Blue Wallet) don't have origin data xpubs[descriptor_key.key] = DerivationPath( descriptor_key.origin.fingerprint, descriptor_key.origin.derivation ) - return xpubs + elif len(descriptor_keys) > 1: + # Allow one descriptor key without origin data for taproot + # Pure taptree descriptors won't have origin data for internal key + origin_less_xpub = descriptor_key.key + + return xpubs, origin_less_xpub + + def psbt_policy_string(self): + """Returns the policy string containing script type and cosigners' fingerprints""" + + policy_str = "PSBT policy:\n" + policy_str += self.policy["type"] + "\n" + if is_multisig(self.policy): + policy_str += str(self.policy["m"]) + " of " + str(self.policy["n"]) + "\n" + fingerprints = [] + for inp in self.psbt.inputs: + # Do we need to loop through all the inputs or just one? + if self.policy["type"] == P2WSH: + for pub in inp.bip32_derivations: + fingerprint_srt = Key.format_fingerprint( + inp.bip32_derivations[pub].fingerprint, True + ) + if fingerprint_srt not in fingerprints: + if len(fingerprints) > MAX_POLICY_COSIGNERS_DISPLAYED: + fingerprints[-1] = "..." + break + fingerprints.append(fingerprint_srt) + elif self.policy["type"] == P2TR: + for pub in inp.taproot_bip32_derivations: + _, derivation_path = inp.taproot_bip32_derivations[pub] + fingerprint_srt = Key.format_fingerprint( + derivation_path.fingerprint, True + ) + if fingerprint_srt not in fingerprints: + if len(fingerprints) > MAX_POLICY_COSIGNERS_DISPLAYED: + fingerprints[-1] = "..." + break + fingerprints.append(fingerprint_srt) + + policy_str += "\n".join(fingerprints) + return policy_str def is_multisig(policy): @@ -606,7 +625,8 @@ def is_miniscript(policy): # m and n will not be present in miniscript policies return ( "type" in policy - and P2WSH in policy["type"] + and policy["type"] in (P2WSH, P2TR) + and "miniscript" in policy and "m" not in policy and "n" not in policy ) @@ -637,7 +657,7 @@ def get_cosigners(pubkeys, derivations, xpubs): def get_cosigners_miniscript(derivations, xpubs): - """Returns xpubs used to derive pubkeys listed in derivations.""" + """Compares the derivations with the xpubs to check and get the cosigners""" cosigners = [] for pubkey, der in derivations.items(): for xpub in xpubs: @@ -653,12 +673,50 @@ def get_cosigners_miniscript(derivations, xpubs): break # Ensure all pubkeys have a matching xpub if len(cosigners) != len(derivations): - raise ValueError("Not all pubkeys in derivations have corresponding xpubs") + raise ValueError("cannot get all cosigners") + return sorted(cosigners) + + +def get_cosigners_taproot_miniscript(taproot_derivations, xpubs, origin_less_xpub=None): + """ + Compares the taproot derivations with the xpubs to check get the cosigners + """ + + cosigners = [] + for xonly_pubkey, der_info in taproot_derivations.items(): + _, der = der_info # tap_leaf_hashes are not used + fp = der.fingerprint + full_path = der.derivation + + for xpub in xpubs: + origin_der = xpubs[xpub] + # Check that the fingerprint matches + if origin_der.fingerprint == fp: + # Check that the origin derivation is a prefix of the full derivation path + if full_path[: len(origin_der.derivation)] == origin_der.derivation: + # Derive the remainder of the path + remainder = full_path[len(origin_der.derivation) :] + # Verify that the xpub derives to the given xonly_pubkey + derived_key = xpub.derive(remainder).key + if derived_key.xonly() == xonly_pubkey.xonly(): + # Append the xpub as a base58 string + cosigners.append(xpub.to_base58()) + break + + if origin_less_xpub: + # Pocicies which don't cover internal key spending (e.g. taptree only) + # can have an origin-less xpub for internal key derivation + cosigners.append(origin_less_xpub.to_base58()) + + # Ensure all pubkeys have a matching xpub + if len(cosigners) != len(taproot_derivations): + raise ValueError("cannot get all cosigners") + return sorted(cosigners) # Modified from: https://github.com/diybitcoinhardware/embit/blob/master/examples/change.py#L64 -def get_policy(scope, scriptpubkey, xpubs): +def get_policy(scope, scriptpubkey, xpubs, origin_less_xpub=None): """Parse scope and get policy""" from embit.finalizer import parse_multisig @@ -688,9 +746,28 @@ def get_policy(scope, scriptpubkey, xpubs): except: try: # Try to parse as miniscript + policy.update({"miniscript": P2WSH}) # Will succeed to verify cosigners only if the descriptor is loaded cosigners = get_cosigners_miniscript(scope.bip32_derivations, xpubs) policy.update({"cosigners": cosigners}) except: pass + elif script_type == P2TR: + try: + # Try to parse as taproot miniscript + if len(scope.taproot_bip32_derivations) > 1: + # Assume is miniscript if there are multiple cosigners + policy.update({"miniscript": P2TR}) + + # Will succeed to verify cosigners only if the descriptor is loaded + cosigners = get_cosigners_taproot_miniscript( + scope.taproot_bip32_derivations, xpubs, origin_less_xpub + ) + # Only add cosigners if is miniscript (multiple cosigners), + # otherwise it probably is single-sig taproot + if len(cosigners) > 1: + policy.update({"cosigners": cosigners}) + except Exception as e: + print(e) + return policy diff --git a/src/krux/touch.py b/src/krux/touch.py index 699ac3f4c..e93ab3082 100644 --- a/src/krux/touch.py +++ b/src/krux/touch.py @@ -78,6 +78,13 @@ def add_x_delimiter(self, region): def valid_position(self, data): """Checks if touch position is within buttons area""" + + if ( + hasattr(Settings().hardware.display, "flipped_orientation") + and Settings().hardware.display.flipped_orientation + ): + data = (self.height - data[0], self.width - data[1]) + if self.x_regions and data[0] < self.x_regions[0]: return False if self.x_regions and data[0] > self.x_regions[-1]: @@ -151,6 +158,13 @@ def _extract_index(self, data): def _store_points(self, data): """Store pressed points and calculare an average pressed point""" + + if ( + hasattr(Settings().hardware.display, "flipped_orientation") + and Settings().hardware.display.flipped_orientation + ): + data = (self.height - data[0], self.width - data[1]) + if self.state == IDLE: self.state = PRESSED self.press_point = [data] diff --git a/src/krux/translations/__init__.py b/src/krux/translations/__init__.py index b790f3a0c..54c1c4298 100644 --- a/src/krux/translations/__init__.py +++ b/src/krux/translations/__init__.py @@ -51,8 +51,7 @@ 2143824150, 3270727197, 900375497, - 88746165, - 1521033296, + 2693258820, 3857613120, 1056821534, 1868069640, @@ -92,6 +91,7 @@ 2751113454, 1272005728, 4102535566, + 3200465747, 2940689088, 1712856005, 3278654271, @@ -134,6 +134,7 @@ 828589330, 2777318640, 1384661010, + 1058624625, 3748840176, 1406590538, 1077771640, @@ -156,6 +157,7 @@ 649035497, 3248804547, 2585599782, + 870847764, 4093416954, 237577215, 4122897393, @@ -232,6 +234,7 @@ 3593149291, 2580599003, 3108881025, + 1693597581, 1848310591, 710709610, 3338633658, @@ -287,8 +290,10 @@ 2281377987, 2019512665, 2344747135, + 2863098142, 2090568351, 1260825919, + 1075810813, 1232757391, 3303592908, 720041451, @@ -296,6 +301,7 @@ 2596024031, 2440924821, 1898550184, + 2270688958, 4228215415, 2336603177, 3679411849, @@ -337,6 +343,7 @@ 3742424146, 2965123464, 1303016265, + 619317523, 2171149824, 2904991687, 1875891934, diff --git a/src/krux/translations/de.py b/src/krux/translations/de.py index f22d081fc..dbc8e4c35 100644 --- a/src/krux/translations/de.py +++ b/src/krux/translations/de.py @@ -39,8 +39,7 @@ "Zusätzliche Entropie von der Kamera für den AES-CBC-Modus erforderlich", "Adresse", "Richte Kamera und Sicherungsplatte richtig aus.", - "Blendschutz deaktiviert", - "Blendschutz aktiviert", + "Blendschutzmodus", "Aussehen", "Bist Du sicher?", "BGR-Farben", @@ -80,6 +79,7 @@ "Entschlüsseln?", "Standard-Wallet", "Tiefe pro Durchgang", + "Derivation-Pfad", "BIP85-Entropie ableiten?", "Deskriptor-Adressen", "Bildschirm", @@ -122,6 +122,7 @@ "Flash-Karte", "Flash-Tools", "Flash gefüllt mit Kameraentropie", + "Umgekehrte Ausrichtung", "Umgedrehte X-Koordinaten", "Flötendurchmesser", "Frei:", @@ -144,6 +145,7 @@ "Unzureichende Entropie!", "Ungültiger Tamper Check Code", "Ungültige Adresse", + "Ungültiger Derivation-Pfad", "Ungültige mnemonische Lange", "Ungültige Wallet:", "Umkehren", @@ -220,6 +222,7 @@ "Trotzdem fortfahren?", "Weiter?", "Wird bearbeitet..", + "Nachweislich nicht ausgebbar", "QR-Code", "RX Pin", "Neustart", @@ -275,8 +278,10 @@ "Single-Sig", "Größe:", "Einige Schecks können nicht durchgeführt werden.", + "Einige Knoten sind nicht gehärtet:", "Ausgabe (%d):", "Ausgaben:", + "Standardmodus", "Statistiken für Nerds", "Auf Flash speichern", "Auf der SD-Karte speichern", @@ -284,6 +289,7 @@ "TC Flash-Hash", "TC Flash-Hash beim Start", "TOUCH oder ENTER zum Erfassen", + "TR interner Schlüssel", "TX Pin", "Tamper Check Code", "Tamper Check Code erfolgreich gesetzt", @@ -325,6 +331,7 @@ "Wortnummern", "Wörter", "Ja", + "Zoommodus", "ist eine gültige Adresse!", "unbekannt", "wurde in den ersten %d Adressen nicht gefunden", diff --git a/src/krux/translations/es.py b/src/krux/translations/es.py index 89336a8fc..0f9e3cf60 100644 --- a/src/krux/translations/es.py +++ b/src/krux/translations/es.py @@ -39,8 +39,7 @@ "Se requiere entropía adicional de la cámara para el modo AES-CBC", "Dirección", "Alinea la cámara y la placa de respaldo correctamente.", - "Anti-reflejo desactivado", - "Anti-reflejo habilitado", + "Modo antirreflejo", "Apariencia", "¿Estás seguro?", "Colores BGR", @@ -80,6 +79,7 @@ "¿Descifrar?", "Cartera Predeterminada", "Profundidad por Pasada", + "Ruta de derivación", "¿Derivar entropía BIP85?", "Direcciones del descriptor", "Pantalla", @@ -122,6 +122,7 @@ "Mapa Flash", "Flash Tools", "Flash lleno de entropía de cámara", + "Orientación Invertida", "Coordenadas X Invertidas", "Diámetro de la Flauta", "Libre:", @@ -144,6 +145,7 @@ "¡Entropía Insuficiente!", "Código de verificación no válido", "Dirección inválida", + "Ruta de derivación no válida", "Longitud mnemónica no válida", "Cartera inválida:", "Invertir", @@ -220,6 +222,7 @@ "¿Proceder de todas maneras?", "¿Continuar?", "Procesando..", + "No se puede gastar", "Código QR", "RX Pin", "Reiniciar", @@ -275,8 +278,10 @@ "Single-sig", "Tamaño:", "Algunas comprobaciones no se pueden realizar.", + "Algunos nodos no están endurecidos:", "Gastos (%d):", "Gasto:", + "Modo estándar", "Estadísticas para Entendidos", "Almacenar en Flash", "Almacenar en la Tarjeta SD", @@ -284,6 +289,7 @@ "TC Hash Flash", "TC Flash Hash al arranque", "TOCA o ENTER para capturar", + "Clave interna TR", "TX Pin", "Código de verificación", "Código de verificación establecido con éxito", @@ -325,6 +331,7 @@ "Números de Palabra", "Palabras", "Sí", + "Modo ampliado", "es una dirección válida!", "desconocido", "NO FUE ENCONTRADO en las primeras %d direcciones", diff --git a/src/krux/translations/fr.py b/src/krux/translations/fr.py index ad1696fdc..99a2dc00f 100644 --- a/src/krux/translations/fr.py +++ b/src/krux/translations/fr.py @@ -39,8 +39,7 @@ "Entropie supplémentaire de la caméra requise pour le mode AES-CBC", "Adresse", "Alignez correctement la caméra et plaque de sauvegarde.", - "Anti-éblouissement désactivé", - "Anti-éblouissement activé", + "Mode anti-reflets", "Apparence", "Es-tu sûr\u2009?", "Couleurs BGR", @@ -80,6 +79,7 @@ "Déchiffrer\u2009?", "Portefeuille par défaut", "Profondeur par passage", + "Chemin de dérivation", "Dériver l'entropie BIP85\u2009?", "Adresses du descripteur", "Affichage", @@ -122,6 +122,7 @@ "Plan du Flash", "Outils Flash", "Flash rempli par l'entropie de la caméra", + "Orientation inversée", "Coordonnées X inversées", "Diamètre de flûte", "Libre\u2009:", @@ -144,6 +145,7 @@ "Entropie insuffisante\u2009!", "Code de non compromis non valide", "Adresse invalide", + "Chemin de dérivation non valide", "Longueur mnémonique invalide", "Portefeuille invalide\u2009:", "Inverser", @@ -220,6 +222,7 @@ "Procéder quand même\u2009?", "Procéder\u2009?", "Traitement..", + "Provably unspendable", "Code QR", "RX Fiche", "Redémarrer", @@ -275,8 +278,10 @@ "Clé unique", "Capacité\u2009:", "Certains vérifications ne peuvent pas être effectués.", + "Certains nœuds ne sont pas durcis :", "Dépense (%d)\u2009:", "Dépense\u2009:", + "Mode standard", "Statistiques pour les geeks", "Stocker sur flash", "Stocker sur la carte SD", @@ -284,6 +289,7 @@ "TC Flash Hash", "TC Flash Hash au démarrage", "TOUCHEZ ou ENTRER pour capturer", + "Clé interne TR", "TX Fiche", "Code de non compromis", "Code de non compromis défini avec succès", @@ -325,6 +331,7 @@ "Numéros de mots", "Mots", "Oui", + "Mode zoomé", "Adresse non valide\u2009!", "inconnu", "INTROUVABLE dans les %d premières adresses", diff --git a/src/krux/translations/ja.py b/src/krux/translations/ja.py index f8188c42b..0ec2442ea 100644 --- a/src/krux/translations/ja.py +++ b/src/krux/translations/ja.py @@ -39,8 +39,7 @@ "AES-CBCモードにはカメラからの追加エントロピーが必要です", "アドレス", "カメラとバックプレートを正しく整列させてください.", - "アンチグレアが無効", - "アンチグレアが有効", + "アンチグレアモード", "外観", "よろしいですか?", "BGRカラー", @@ -80,6 +79,7 @@ "デクリプト?", "デフォルトの財布", "パスごとの深さ", + "導出パス", "BIP85エントロピーを導出しますか?", "ディスクリプタアドレス", "ディスプレイ", @@ -122,6 +122,7 @@ "フラッシュマップ", "Flashツール", "カメラエントロピーで満たされたフラッシュ", + "翻转方向", "X座標が反転しました", "フルートディアメーター", "フリー:", @@ -144,6 +145,7 @@ "不十分なエントロピー!", "無効な改ざんチェックコード", "無効なアドレス", + "無効な導出パス", "無効なニーモニックの長さ", "無効なウォレット:", "反転する", @@ -220,6 +222,7 @@ "そのまま進みますか?", "進みますか?", "処理中..", + "証明可能に使用不能", "QRコード", "RXピン", "再起動", @@ -275,8 +278,10 @@ "シングルサイン", "サイズ:", "一部のチェックを実行できません.", + "一部のノードは硬化されていません:", "支出(%d):", "支出:", + "標準モード", "オタクのための統計", "フラッシュに保存する", "SDカードに保存する", @@ -284,6 +289,7 @@ "TCフラッシュハッシュ", "起動時のTCフラッシュハッシュ", "タッチまたはENTERでキャプチャする", + "TR内部キー", "TXピン", "改ざんチェックコード", "改ざんチェックコードが正常に設定されました", @@ -325,6 +331,7 @@ "単語番号", "単語", "はい", + "ズームモード", "有効なアドレスです!", "不明", "最初の%dアドレスに見つかりませんでした", diff --git a/src/krux/translations/ko.py b/src/krux/translations/ko.py index cd322cf36..71b447598 100644 --- a/src/krux/translations/ko.py +++ b/src/krux/translations/ko.py @@ -39,8 +39,7 @@ "AES-CBC 모드를 위해 카메라의 추가 엔트로피가 필요합니다", "주소", "카메라와 보조 플레이트를 올바르게 정렬하십시오.", - "눈부심방지 비활성화", - "눈부심방지 활성화", + "불너미 방지 모드", "디스플레이", "계속하시겠습니까?", "BGR 색상", @@ -80,6 +79,7 @@ "복호화하시겠습니까?", "지갑 기본설정", "Depth Per Pass", + "파생 경로", "BIP85 엔트로피를 유독하시겠습니까?", "디스크립터 주소", "디스플레이", @@ -122,6 +122,7 @@ "플래시 맵", "플래시 도구", "카메라 엔트로피로 가득 찬 플래시", + "지옥 방향 바뀌기", "X 좌표 반전", "플루트 직경", "여유 공간:", @@ -144,6 +145,7 @@ "엔트로피가 충분하지 않습니다!", "유효하지 않은 탬퍼 체크 코드", "주소가 잘못되었습니다", + "잘못된 파생 경로", "니모닉 길이가 잘못되었습니다", "지갑이 잘못되었습니다:", "반전", @@ -220,6 +222,7 @@ "계속하시겠습니까?", "계속하시겠습니까?", "처리...", + "충분히 사용할 수 없음", "QR 코드", "RX 핀", "다시 반복", @@ -275,8 +278,10 @@ "단일서명", "크기:", "일부 검사를 수행할 수 없습니다.", + "일부 노드가 경화되지 않습니다:", "Spend (%d):", "지출", + "표준 모드", "전문가를 위한 통계", "플래시 메모리에 저장", "SD카드에 저장", @@ -284,6 +289,7 @@ "TC Flash Hash", "부팅 시 플래시 탬퍼 확인 해시", "터치하거나 엔터를 눌러 캡처하십시오", + "TR 내부 키", "TX 핀", "탬퍼 체크 코드", "탬퍼 검사 코드가 성공적으로 설정되었습니다", @@ -325,6 +331,7 @@ "단어 번호(1-2048)", "시드문구", "예", + "확대 모드", "는 유효한 주소입니다!", "알 수 없음", "첫 번째 %d개의 주소에서 찾을 수 없습니다", diff --git a/src/krux/translations/nl.py b/src/krux/translations/nl.py index a4fe59625..7f888ff29 100644 --- a/src/krux/translations/nl.py +++ b/src/krux/translations/nl.py @@ -39,8 +39,7 @@ "Extra entropie van de camera vereist voor AES-CBC-modus", "Adres", "Richt de camera en back-upplaat op de juiste manier.", - "Anti reflecterend uitgeschakeld", - "Anti reflecterend ingeschakeld", + "Anti-verblindingsmodus", "Uiterlijk", "Weet je het zeker?", "BGR-kleuren", @@ -80,6 +79,7 @@ "Ontsleutelen?", "Standaard portemonnee", "Diepte per pas", + "Afleidingspad", "BIP85-entropie afleiden?", "Descriptoradressen", "Weergave", @@ -122,6 +122,7 @@ "Flash Map", "Flash Tools", "Flash gevuld met camera-entropie", + "Omgekeerde oriëntering", "Geflipte X-coördinaten", "Fluit diameter", "Vrij:", @@ -144,6 +145,7 @@ "Onvoldoende Entropie!", "Ongeldige sabotagecontrolecode", "Ongeldig adres", + "Ongeldige afleidingspad", "Ongeldige geheugensteun lengte", "Ongeldige portemonnee:", "Omkeren", @@ -220,6 +222,7 @@ "Toch doorgaan?", "Doorgaan?", "Verwerken..", + "Bewijsbaar niet besteedbaar", "QR code", "RX pin", "Opnieuw opstarten", @@ -275,8 +278,10 @@ "Enkele sleutel", "Grootte:", "Sommige controles kunnen niet worden uitgevoerd.", + "Sommige knooppunten zijn niet gehard:", "Uitgaven (%d):", "Uitgaven:", + "Standaardmodus", "Statistieken voor nerds", "Opslaan op apparaat", "Opslaan op SD kaart", @@ -284,6 +289,7 @@ "TC Flash Hash", "Hash Flash bij het opstarten", "TIK of ENTER voor opname", + "TR interne sleutel", "TX pin", "Sabotagecontrolecode", "Sabotagecontrolecode succesvol ingesteld", @@ -325,6 +331,7 @@ "Woord nummers", "Woorden", "Yes", + "Ingezoomde modus", "is geen geldig adres", "onbekend", "werd NIET GEVONDEN in de eerste %d adressen", diff --git a/src/krux/translations/pt.py b/src/krux/translations/pt.py index a60394dda..5542bb396 100644 --- a/src/krux/translations/pt.py +++ b/src/krux/translations/pt.py @@ -39,8 +39,7 @@ "Entropia adicional da câmera necessária para o modo AES-CBC", "Endereço", "Alinhe a câmera e a placa de backup corretamente.", - "Antirreflexo desativado", - "Antirreflexo ativado", + "Modo antirreflexo", "Aparência", "Tem certeza?", "Cores BGR", @@ -80,6 +79,7 @@ "Descriptografar?", "Carteira Padrão", "Profundidade da Passagem", + "Caminho de Derivação", "Derivar entropia BIP85?", "Endereços do Descritor", "Display", @@ -122,6 +122,7 @@ "Mapa da Flash", "Ferramentas da Flash", "Flash preenchida com entropia da câmera", + "Orientação Invertida", "Coordenadas X invertidas", "Diâmetro da Fresa", "Livre:", @@ -144,6 +145,7 @@ "Entropia insuficiente!", "Código de verificação inválido", "Endereço inválido", + "Caminho de derivação inválido", "Comprimento de mnemônico inválido", "Carteira inválida:", "Invertido", @@ -220,6 +222,7 @@ "Continuar mesmo assim?", "Seguir?", "Processando..", + "Não pode gastar", "Código QR", "Pino RX", "Reiniciar", @@ -275,8 +278,10 @@ "Single-sig", "Total:", "Algumas verificações não podem ser realizadas.", + "Alguns nós não são hardened:", "Gastos (%d):", "Gasto:", + "Modo padrão", "Estatísticas para Nerds", "Armazene na Flash", "Armazene no Cartão SD", @@ -284,6 +289,7 @@ "TC Flash Hash", "TC Hash Flash na Inicialização", "TOQUE ou ENTER para capturar", + "Chave interna TR", "Pino TX", "Código de Verificação", "Código de verificação definido com sucesso", @@ -325,6 +331,7 @@ "Números das Palavras", "Palavras", "Sim", + "Modo ampliado", "é um endereço válido!", "desconhecida", "NÃO FOI ENCONTRADO nos primeiros %d endereços", diff --git a/src/krux/translations/ru.py b/src/krux/translations/ru.py index a33bf25a3..a541bd6df 100644 --- a/src/krux/translations/ru.py +++ b/src/krux/translations/ru.py @@ -39,8 +39,7 @@ "Для режима AES-CBC требуется дополнительная энтропия от камеры", "Адрес", "Правильно совместите камеру и резервную пластину.", - "Антиблик отключен", - "Антиблик включен", + "Антибликовый режим", "Внешний Вид", "Вы уверены?", "Цвета BGR", @@ -80,6 +79,7 @@ "Расшифровать?", "Кошелек по умолчанию", "Глубина за Проход", + "Путь деривации", "Вывести энтропию BIP85?", "Адреса дескрипторов", "Дисплеи", @@ -122,6 +122,7 @@ "Карта флэша", "Flash Tools", "Флэш заполнен энтропией камеры", + "Перевернутая Oриентация", "Перевернутые координаты X", "Диаметр Флюта", "Свободно:", @@ -144,6 +145,7 @@ "Недостаточная Энтропия!", "Недействительный код проверки вскрытия", "Неверный адрес", + "Недопустимый путь деривации", "Неверная длина мнемоники", "Неверный кошелек:", "Инвертировать", @@ -220,6 +222,7 @@ "Все равно продолжить?", "Продолжить?", "Обработка..", + "Доказуемо непотратимый", "QR Код", "RX Пин", "Перезагрузить", @@ -275,8 +278,10 @@ "Одна подпись", "Размер:", "Некоторые проверки не могут быть выполнены.", + "Некоторые узлы не укреплены:", "Расход (%d):", "Расход:", + "Стандартный режим", "Статистика для Гиков", "Сохранить на Флэш Память", "Сохранить на SD Карту", @@ -284,6 +289,7 @@ "TC Flash Hash", "Проверка хэша Flash при загрузке", "ПРИКОСНИТЕСЬ или нажмите ВВОД, чтобы захватить", + "внутренний ключ", "TX Пин", "Код проверки вскрытия", "Код проверки вскрытия успешно установлен", @@ -325,6 +331,7 @@ "Числа Слов", "Слова", "Да", + "Режим масштабирования", "некорректный адрес", "неизвестный", "нЕ НАЙДЕНО в первых %d адресах", diff --git a/src/krux/translations/tr.py b/src/krux/translations/tr.py index 83dc19a5a..2ca6b73f3 100644 --- a/src/krux/translations/tr.py +++ b/src/krux/translations/tr.py @@ -39,8 +39,7 @@ "AES-CBC modu için kameradan ek entropi gereklidir", "Adres", "Kamerayı ve yedek plakay'ı düzgün bir şekilde hizalayın.", - "Parlama önleyici devre dışı", - "Parlama önleyici etkin", + "Parlama Önleyici Mod", "Görünüm", "Emin misiniz?", "BGR Renkleri", @@ -80,6 +79,7 @@ "Şifre çözülsün mü?", "Varsayılan Cüzdan", "Geçiş Başına Derinlik", + "Türetim Yolu", "BIP85 entropisi türetilsin mi?", "Tanımlayıcı Adresler", "Ekran", @@ -122,6 +122,7 @@ "Flash Haritası", "Flash Araçları", "Flash kamera entropisi ile dolduruldu", + "Ters Çevrilmiş Yönlendirme", "X Koordinatları Tersine Çevrildi", "Flute Çapı", "Boş:", @@ -144,6 +145,7 @@ "Yetersiz entropi!", "Geçersiz Kurcalama Kontrol Kodu", "Geçersiz adres", + "Geçersiz türetim yolu", "Geçersiz mnemonic uzunluğu", "Geçersiz cüzdan:", "Ters Çevir", @@ -220,6 +222,7 @@ "Yine de devam edilsin mi?", "Devam edilsin mi?", "İşleniyor..", + "Kanıtlanabilir harcanamaz", "QR Kodu", "RX Pini", "Yeniden Başlat", @@ -275,8 +278,10 @@ "Tek-imza", "Boyut:", "Bazı kontroller yerine getirilemedi.", + "Bazı düğümler sertleştirilmemiş:", "Harcama (%d):", "Harcama:", + "Standart Mod", "İnekler İçin İstatistikler", "Flash'ta Sakla", "SD Kartta Sakla", @@ -284,6 +289,7 @@ "TC Flash Hash", "Önyüklemede TC Flash Hash", "Yakalamak için DOKUN veya GİR", + "TR iç anahtar", "TX Pini", "Kurcalama Kontrol Kodu", "Kurcalama kontrol kodu başarıyla ayarlandı", @@ -325,6 +331,7 @@ "Kelime Numaraları", "Kelimeler", "Evet", + "Zoom Mod", "geçerli bir adres!", "bilinmiyor", "ilk %d adreste BULUNAMADI", diff --git a/src/krux/translations/vi.py b/src/krux/translations/vi.py index ed87f213a..aa01b5ba9 100644 --- a/src/krux/translations/vi.py +++ b/src/krux/translations/vi.py @@ -39,8 +39,7 @@ "Cần thêm entropy từ camera cho chế độ AES-CBC", "Địa chỉ", "Căn chỉnh camera và tấm dự phòng đúng cách.", - "Chống lóa bị vô hiệu hóa", - "Đã bật chống lóa", + "Chế độ chống lóa", "Giao diện", "Bạn có chắc không?", "Màu BGR", @@ -80,6 +79,7 @@ "Giải mã?", "Ví mặc định", "Độ sâu mỗi lần cắt CNC", + "Đường dẫn phái sinh", "Suy ra entropy BIP85?", "Địa chỉ người mô tả", "Hiển thị", @@ -122,6 +122,7 @@ "Bản đồ Flash", "Công cụ Flash", "Đèn flash chứa đầy entropy của máy ảnh", + "Hướng lật", "Tọa độ X bị lật", "Đường kính mũi cắt CNC", "Khả dụng:", @@ -144,6 +145,7 @@ "Entropy không đủ!", "Mã kiểm tra giả mạo không hợp lệ", "Địa chỉ không hợp lệ", + "Đường dẫn phái sinh không hợp lệ", "Độ dài mã Mnemonic không hợp lệ", "Ví không hợp lệ:", "Đảo ngược", @@ -220,6 +222,7 @@ "Vẫn tiếp tục?", "Thực hiện?", "Đang xử lý..", + "Chắc chắn không thể chi tiêu", "Mã QR", "RX Pin", "Khởi động lại", @@ -275,8 +278,10 @@ "Khóa đơn", "Dung lượng:", "Một số kiểm tra không thể được thực hiện.", + "Một số nút không được làm cứng:", "Chi tiêu (%d):", "Chi tiêu:", + "Chế độ Tiêu chuẩn", "Số liệu thống kê cho Mọt sách", "Lưu trữ trên flash", "Lưu trữ trên thẻ SD", @@ -284,6 +289,7 @@ "TC Flash Hash", "Hash Flash TC khi khởi động", "Chạm màn hình hoặc nhấn nút ENTER để chụp", + "Khóa nội bộ TR", "TX Pin", "Mã kiểm tra giả mạo", "Đã đặt mã kiểm tra giả mạo thành công", @@ -325,6 +331,7 @@ "Từ số", "Từ ngữ", "Đúng", + "Chế độ thu phóng", "là một địa chỉ hợp lệ!", "không rõ", "kHÔNG TÌM THẤY trong %d địa chỉ đầu tiên", diff --git a/src/krux/translations/zh.py b/src/krux/translations/zh.py index c2d2383ba..92e7fd50d 100644 --- a/src/krux/translations/zh.py +++ b/src/krux/translations/zh.py @@ -39,8 +39,7 @@ "AES-CBC 模式需要相机的额外熵", "地址", "正确对齐摄像头和背板。", - "防眩光已禁用", - "防眩光已启用", + "防闪模式", "界面", "确定?", "BGR 颜色", @@ -80,6 +79,7 @@ "解密?", "默认钱包", "每次通过的深度", + "源路径", "导出BIP85熵?", "描述符地址", "显示", @@ -122,6 +122,7 @@ "Flash地图", "Flash工具", "Flash已用摄像头熵填充", + "翻转方向", "翻转 X 坐标", "刀具直径", "空闲:", @@ -144,6 +145,7 @@ "熵不足!", "无效的防篡改检查码", "无效地址", + "无效的源路径", "助记词长度无效", "无效钱包:", "反转", @@ -220,6 +222,7 @@ "继续吗?", "继续?", "正在处理..", + "可证明不可使用", "二维码", "RX 引脚", "重启", @@ -275,8 +278,10 @@ "单签", "大小:", "无法执行某些检查。", + "有些节点未硬化:", "花费 (%d):", "花费", + "标准模式", "极客统计数据", "存储到 Flash", "存储到 SD 卡", @@ -284,6 +289,7 @@ "TC Flash Hash", "启动时的 TC Flash Hash", "点击或按下 ENTER 截图", + "TR内部密钥", "TX 引脚", "防篡改检查码", "防篡改检查码设置成功", @@ -325,6 +331,7 @@ "单词序号", "单词", "是", + "放大模式", " 非有效地址", "未知", "在前 %d 个地址中未找到", diff --git a/src/krux/wallet.py b/src/krux/wallet.py index f6ccd6edd..503e1a339 100644 --- a/src/krux/wallet.py +++ b/src/krux/wallet.py @@ -108,13 +108,40 @@ def is_miniscript(self): if self.descriptor: if self.descriptor.is_basic_multisig: return False - return self.descriptor.miniscript is not None + return ( + self.descriptor.miniscript is not None + or self.descriptor.taptree # taptree is "" when not present + ) return False def is_loaded(self): """Returns a boolean indicating whether or not this wallet has been loaded""" return self.wallet_data is not None + def _validate_descriptor(self, descriptor, descriptor_xpubs): + """Validates the descriptor against the current key and policy type""" + + if self.is_multisig(): + if not descriptor.is_basic_multisig: + raise ValueError("not multisig") + if self.key.xpub() not in descriptor_xpubs: + raise ValueError("xpub not a multisig cosigner") + elif self.is_miniscript(): + if self.key.script_type == P2WSH: + if descriptor.miniscript is None or descriptor.is_basic_multisig: + raise ValueError("not P2WSH miniscript") + elif self.key.script_type == P2TR: + if not descriptor.taptree: + raise ValueError("not P2TR miniscript") + if self.key.xpub() not in descriptor_xpubs: + raise ValueError("xpub not a miniscript cosigner") + else: + if not descriptor.key: + if len(descriptor.keys) > 1: + raise ValueError("not single-sig") + if self.key.xpub() != descriptor_xpubs[0]: + raise ValueError("xpub does not match") + def load(self, wallet_data, qr_format, allow_assumption=None): """Loads the wallet from the given data""" descriptor, label = parse_wallet(wallet_data, allow_assumption) @@ -128,29 +155,16 @@ def load(self, wallet_data, qr_format, allow_assumption=None): ) if self.key: - if self.is_multisig(): - if not descriptor.is_basic_multisig: - raise ValueError("not multisig") - if self.key.xpub() not in descriptor_xpubs: - raise ValueError("xpub not a multisig cosigner") - elif self.is_miniscript(): - if descriptor.miniscript is None or descriptor.is_basic_multisig: - raise ValueError("not miniscript") - if self.key.xpub() not in descriptor_xpubs: - raise ValueError("xpub not a miniscript cosigner") - else: - if not descriptor.key: - if len(descriptor.keys) > 1: - raise ValueError("not single-sig") - if self.key.xpub() != descriptor_xpubs[0]: - raise ValueError("xpub does not match") + try: + self._validate_descriptor(descriptor, descriptor_xpubs) + except ValueError as e: + raise ValueError("Invalid Descriptor: %s" % e) self.wallet_data = wallet_data self.wallet_qr_format = qr_format self.descriptor = to_unambiguous_descriptor(descriptor) self.label = label - - if self.descriptor.key: + if self.descriptor.key and not self.descriptor.taptree: if not self.label: self.label = t("Single-sig") self.policy = {"type": self.descriptor.scriptpubkey_type()} @@ -168,14 +182,48 @@ def load(self, wallet_data, qr_format, allow_assumption=None): "n": n, "cosigners": cosigners, } - elif self.descriptor.miniscript is not None: + elif self.descriptor.miniscript is not None or self.descriptor.taptree: + if self.descriptor.taptree: + if not descriptor.keys[0].origin: + import hashlib + from embit.ec import NUMS_PUBKEY + from embit.bip32 import HDKey + + # In case internal key is disabled, check if NUMS is known + + # Hash all pubkeys, except internal, to compute deterministic chain code + hasher = hashlib.sha256() + for key in descriptor.keys[1:]: + hasher.update(key.sec()) + det_chain_code = hasher.digest() + + # Create provably unspendable deterministic key + version = self.descriptor.keys[0].key.version + provably_unspendable = HDKey( + NUMS_PUBKEY, det_chain_code, version=version + ) + + # Compare expected provably unspendable key with first descriptor key + if ( + descriptor.keys[0].key.to_base58() + != provably_unspendable.to_base58() + ): + self.wallet_data = None + raise ValueError("Internal key not provably unspendable") + + taproot_txt = "TR " + miniscript_type = P2TR + else: + taproot_txt = "" + miniscript_type = P2WSH if not self.label: - self.label = t("Miniscript") + self.label = taproot_txt + t("Miniscript") cosigners = [key.key.to_base58() for key in self.descriptor.keys] cosigners = sorted(cosigners) self.policy = { "type": self.descriptor.scriptpubkey_type(), "cosigners": cosigners, + "miniscript": miniscript_type, } def wallet_qr(self): @@ -194,6 +242,10 @@ def wallet_qr(self): def obtain_addresses(self, i=0, limit=None, branch_index=0): """Returns an iterator deriving addresses (default branch_index is receive) for the wallet up to the provided limit""" + + if self.descriptor is None: + raise ValueError("No descriptor to derive addresses from") + starting_index = i while limit is None or i < starting_index + limit: @@ -382,17 +434,29 @@ def parse_address(address_data): from embit.script import Script, address_to_scriptpubkey addr = address_data + sc = None if address_data.lower().startswith("bitcoin:"): addr_end = address_data.find("?") if addr_end == -1: addr_end = len(address_data) addr = address_data[8:addr_end] - try: - sc = address_to_scriptpubkey(addr) - if not isinstance(sc, Script): + if addr == addr.upper(): + # bip173 suggests bech32 in uppercase for compact QR-Code + try: + sc = address_to_scriptpubkey(addr.lower()) + if isinstance(sc, Script): + return addr.lower() + except: + pass + + if not isinstance(sc, Script): + try: + sc = address_to_scriptpubkey(addr) + except: raise ValueError("invalid address") - except: + + if not isinstance(sc, Script): raise ValueError("invalid address") return addr @@ -505,12 +569,12 @@ def is_double_mnemonic(mnemonic: str): words = mnemonic.split(" ") if len(words) > 12: - from krux.bip39 import mnemonic_is_valid + from krux.bip39 import k_mnemonic_is_valid if ( - mnemonic_is_valid(" ".join(words[:12])) - and mnemonic_is_valid(" ".join(words[12:])) - and mnemonic_is_valid(mnemonic) + k_mnemonic_is_valid(" ".join(words[:12])) + and k_mnemonic_is_valid(" ".join(words[12:])) + and k_mnemonic_is_valid(mnemonic) ): return True diff --git a/tests/pages/home_pages/test_home.py b/tests/pages/home_pages/test_home.py index dabe3fe8d..3f459b8ed 100644 --- a/tests/pages/home_pages/test_home.py +++ b/tests/pages/home_pages/test_home.py @@ -1,6 +1,7 @@ import pytest from ...shared_mocks import MockPrinter from .. import create_ctx +from ...test_psbt import tdata as psbt_tdata @pytest.fixture @@ -55,13 +56,17 @@ def tdata(mocker): P2WPKH_PSBT = b'psbt\xff\x01\x00q\x02\x00\x00\x00\x01\xcfe\xff;L\xd4\x7f\x12\x1f\xa7\xc9\x82(F\x18\xdb\x801G\xb0V\xd3\x93\x94\xd4\xecB\x0e\xfd\xfck\xa1\x02 l\xbd\xd8\x8a\xc5\x18l?.\xfd$%1\xedy\x17uvQ\xac&#t\xf3\xd3\x1d\x85\xd6\x16\xcdj\x81\x01\x00\x00\x00' + SIGNED_P2WPKH_PSBT_SD = b'psbt\xff\x01\x00q\x02\x00\x00\x00\x01\xcfe\xff;L\xd4\x7f\x12\x1f\xa7\xc9\x82(F\x18\xdb\x801G\xb0V\xd3\x93\x94\xd4\xecB\x0e\xfd\xfck\xa1\x02 l\xbd\xd8\x8a\xc5\x18l?.\xfd$%1\xedy\x17uvQ\xac&#t\xf3\xd3\x1d\x85\xd6\x16\xcdj\x81\x01"\x06\x02\xe7\xab%7\xb5\xd4\x9e\x97\x03\t\xaa\xe0n\x9eI\xf3l\xe1\xc9\xfe\xbb\xd4N\xc8\xe0\xd1\xcc\xa0\xb4\xf9\xc3\x19\x18s\xc5\xda\nT\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00"\x02\x03]I\xec\xcdT\xd0\t\x9eCgbw\xc7\xa6\xd4b]a\x1d\xa8\x8a]\xf4\x9b\xf9Qzw\x91\xa7w\xa5\x18s\xc5\xda\nT\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00' P2WPKH_PSBT_B64 = "cHNidP8BAHECAAAAAc88WMMpgq4gUIjZvUnrmwKs3009rnalFsazBrFd46FOAAAAAAD9////Anw/XQUAAAAAFgAULzSqHPAKU7BVopGgOn1F8KaYi1KAlpgAAAAAABYAFOZq/v/Dg45x8KJ7B+OwDt5q6OFgAAAAAAABAR8A4fUFAAAAABYAFNDEo+8J6Ze26Z45flGP4+QaEYyhIgYC56slN7XUnpcDCargbp5J82zhyf671E7I4NHMoLT5wxkYc8XaClQAAIABAACAAAAAgAAAAAAAAAAAACICA11J7M1U0AmeQ2did8em1GJdYR2oil30m/lReneRp3elGHPF2gpUAACAAQAAgAAAAIABAAAAAAAAAAAA" P2WPKH_PSBT_B64_ZEROES_FINGERPRINT = "cHNidP8BAHECAAAAAc88WMMpgq4gUIjZvUnrmwKs3009rnalFsazBrFd46FOAAAAAAD9////Anw/XQUAAAAAFgAULzSqHPAKU7BVopGgOn1F8KaYi1KAlpgAAAAAABYAFOZq/v/Dg45x8KJ7B+OwDt5q6OFgAAAAAAABAR8A4fUFAAAAABYAFNDEo+8J6Ze26Z45flGP4+QaEYyhIgYC56slN7XUnpcDCargbp5J82zhyf671E7I4NHMoLT5wxkYAAAAAFQAAIABAACAAAAAgAAAAAAAAAAAACICA11J7M1U0AmeQ2did8em1GJdYR2oil30m/lReneRp3elGHPF2gpUAACAAQAAgAAAAIABAAAAAAAAAAAA" P2TR_PSBT_BIN_ZEROES_FINGERPRINT = b"psbt\xff\x01\x00R\x02\x00\x00\x00\x01\xe6\x02\x02c\xc5\xfdX\xa1\x17[\xf2\xc0Z-\xfd\xa9\x84\xc8H\xf0\x84B)\xa7\x0b\xf6WA\xfaE\xde\xf4\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01-\xe1\x01\x00\x00\x00\x00\x00\x16\x00\x14\xb1P%\xed\xdb8\x87^\xc7h\x8d]6srC\x81)gq\x00\x00\x00\x00O\x01\x045\x87\xcf\x03\xca\xd0\xf4J\x80\x00\x00\x00\x07\x1aOa\x83\xb1T7u\x18\xe8\xbd|\x9e\x0c\x9c\xa0\t\x8cV\x8a:J\x96\xa3\x9eK\xd9\xb4\xff\x9f4\x02\x8c\xc0\x83\xa0\x96^\x8c@A!\xf6\xd7\xa46#?3E\x89p\xb1E\xd3rk2lDL\x84\xfd\xdf\x10\x00\x00\x00\x00V\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x00\x01\x00\x87\x02\x00\x00\x00\x02\xc6\xfcO\x0f\t|9X\x93\xfc\x051\x12\x03(\xe7\xc38\x87\xee\xaf\xf9\x84\x06\xea\xf5\xa6)#\xf7\xa8;\x00\x00\x00\x00\x00\xfd\xff\xff\xff\xe1\xa1U,\x01\n?\x8e:Y1e\xf8\xc7`$\xb0\xa2\xb6V\xa5\x9e\x01\n\xcd0P\xe9%\x18n\xb5\x01\x00\x00\x00\x00\xfd\xff\xff\xff\x01\xe8\xe7\x01\x00\x00\x00\x00\x00\"Q \x0f\xdf\xc7Q\x92}\xd5t\xbe'\\\xd6m\xb4\x84\xc8t.\x8f\xcc\xaa\xff\x04*\xf8\xc5\xe9(\x83\x0fX\xac\xd4\xc1+\x00\x01\x01+\xe8\xe7\x01\x00\x00\x00\x00\x00\"Q \x0f\xdf\xc7Q\x92}\xd5t\xbe'\\\xd6m\xb4\x84\xc8t.\x8f\xcc\xaa\xff\x04*\xf8\xc5\xe9(\x83\x0fX\xac\x01\x03\x04\x00\x00\x00\x00!\x16\x8b4{\xa5\xc1l\n&\xcd3\xfefv\xbbA~\xe8\x1fB\xf1(\x17\xa6\xe4\x11\xee\x8a\xb2\x00\xdf\xe9`\x19\x00\x00\x00\x00\x00V\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x01\x17 \x8b4{\xa5\xc1l\n&\xcd3\xfefv\xbbA~\xe8\x1fB\xf1(\x17\xa6\xe4\x11\xee\x8a\xb2\x00\xdf\xe9`\x00\x00" SIGNED_P2WPKH_PSBT_B64 = "cHNidP8BAHECAAAAAc88WMMpgq4gUIjZvUnrmwKs3009rnalFsazBrFd46FOAAAAAAD9////Anw/XQUAAAAAFgAULzSqHPAKU7BVopGgOn1F8KaYi1KAlpgAAAAAABYAFOZq/v/Dg45x8KJ7B+OwDt5q6OFgAAAAAAABAR8A4fUFAAAAABYAFNDEo+8J6Ze26Z45flGP4+QaEYyhIgIC56slN7XUnpcDCargbp5J82zhyf671E7I4NHMoLT5wxlHMEQCID5l/ztM1H8SH6fJgihGGNuAMUewVtOTlNTsQg79/GuhAiBsvdiKxRhsPy79JCUx7XkXdXZRrCYjdPPTHYXWFs1qgQEAAAA=" + SIGNED_P2WPKH_PSBT_B64_SD = "cHNidP8BAHECAAAAAc88WMMpgq4gUIjZvUnrmwKs3009rnalFsazBrFd46FOAAAAAAD9////Anw/XQUAAAAAFgAULzSqHPAKU7BVopGgOn1F8KaYi1KAlpgAAAAAABYAFOZq/v/Dg45x8KJ7B+OwDt5q6OFgAAAAAAABAR8A4fUFAAAAABYAFNDEo+8J6Ze26Z45flGP4+QaEYyhIgIC56slN7XUnpcDCargbp5J82zhyf671E7I4NHMoLT5wxlHMEQCID5l/ztM1H8SH6fJgihGGNuAMUewVtOTlNTsQg79/GuhAiBsvdiKxRhsPy79JCUx7XkXdXZRrCYjdPPTHYXWFs1qgQEiBgLnqyU3tdSelwMJquBunknzbOHJ/rvUTsjg0cygtPnDGRhzxdoKVAAAgAEAAIAAAACAAAAAAAAAAAAAIgIDXUnszVTQCZ5DZ2J3x6bUYl1hHaiKXfSb+VF6d5Gnd6UYc8XaClQAAIABAACAAAAAgAEAAAAAAAAAAAA=" SIGNED_P2TR_PSBT_BIN = b"psbt\xff\x01\x00R\x02\x00\x00\x00\x01\xe6\x02\x02c\xc5\xfdX\xa1\x17[\xf2\xc0Z-\xfd\xa9\x84\xc8H\xf0\x84B)\xa7\x0b\xf6WA\xfaE\xde\xf4\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01-\xe1\x01\x00\x00\x00\x00\x00\x16\x00\x14\xb1P%\xed\xdb8\x87^\xc7h\x8d]6srC\x81)gq\x00\x00\x00\x00\x00\x01\x01+\xe8\xe7\x01\x00\x00\x00\x00\x00\"Q \x0f\xdf\xc7Q\x92}\xd5t\xbe'\\\xd6m\xb4\x84\xc8t.\x8f\xcc\xaa\xff\x04*\xf8\xc5\xe9(\x83\x0fX\xac\x01\x08B\x01@jRXU\x1f\x0f2\xd2\xd8?\x08a\x089\xfa\x936\x13_\x8d\x0f\xcb\xb9\x04\xc0T\xe4\xea\xc6~\xf2A\xa6K\xc8Lu\r\x1b\x8aN\xca\xf6\x95\xce\xa6p\x0e\xc1\x95\xcbd\t\xfc\xa3^,lQF\xca'9\xa6\x00\x00" + SIGNED_P2TR_PSBT_BIN_SD = b"psbt\xff\x01\x00R\x02\x00\x00\x00\x01\xe6\x02\x02c\xc5\xfdX\xa1\x17[\xf2\xc0Z-\xfd\xa9\x84\xc8H\xf0\x84B)\xa7\x0b\xf6WA\xfaE\xde\xf4\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01-\xe1\x01\x00\x00\x00\x00\x00\x16\x00\x14\xb1P%\xed\xdb8\x87^\xc7h\x8d]6srC\x81)gq\x00\x00\x00\x00O\x01\x045\x87\xcf\x03\xca\xd0\xf4J\x80\x00\x00\x00\x07\x1aOa\x83\xb1T7u\x18\xe8\xbd|\x9e\x0c\x9c\xa0\t\x8cV\x8a:J\x96\xa3\x9eK\xd9\xb4\xff\x9f4\x02\x8c\xc0\x83\xa0\x96^\x8c@A!\xf6\xd7\xa46#?3E\x89p\xb1E\xd3rk2lDL\x84\xfd\xdf\x10\x00\x00\x00\x00V\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x00\x01\x00\x87\x02\x00\x00\x00\x02\xc6\xfcO\x0f\t|9X\x93\xfc\x051\x12\x03(\xe7\xc38\x87\xee\xaf\xf9\x84\x06\xea\xf5\xa6)#\xf7\xa8;\x00\x00\x00\x00\x00\xfd\xff\xff\xff\xe1\xa1U,\x01\n?\x8e:Y1e\xf8\xc7`$\xb0\xa2\xb6V\xa5\x9e\x01\n\xcd0P\xe9%\x18n\xb5\x01\x00\x00\x00\x00\xfd\xff\xff\xff\x01\xe8\xe7\x01\x00\x00\x00\x00\x00\"Q \x0f\xdf\xc7Q\x92}\xd5t\xbe'\\\xd6m\xb4\x84\xc8t.\x8f\xcc\xaa\xff\x04*\xf8\xc5\xe9(\x83\x0fX\xac\xd4\xc1+\x00\x01\x01+\xe8\xe7\x01\x00\x00\x00\x00\x00\"Q \x0f\xdf\xc7Q\x92}\xd5t\xbe'\\\xd6m\xb4\x84\xc8t.\x8f\xcc\xaa\xff\x04*\xf8\xc5\xe9(\x83\x0fX\xac\x01\x03\x04\x00\x00\x00\x00\x01\x08B\x01@jRXU\x1f\x0f2\xd2\xd8?\x08a\x089\xfa\x936\x13_\x8d\x0f\xcb\xb9\x04\xc0T\xe4\xea\xc6~\xf2A\xa6K\xc8Lu\r\x1b\x8aN\xca\xf6\x95\xce\xa6p\x0e\xc1\x95\xcbd\t\xfc\xa3^,lQF\xca'9\xa6\x01\x13@jRXU\x1f\x0f2\xd2\xd8?\x08a\x089\xfa\x936\x13_\x8d\x0f\xcb\xb9\x04\xc0T\xe4\xea\xc6~\xf2A\xa6K\xc8Lu\r\x1b\x8aN\xca\xf6\x95\xce\xa6p\x0e\xc1\x95\xcbd\t\xfc\xa3^,lQF\xca'9\xa6!\x16\x8b4{\xa5\xc1l\n&\xcd3\xfefv\xbbA~\xe8\x1fB\xf1(\x17\xa6\xe4\x11\xee\x8a\xb2\x00\xdf\xe9`\x19\x00\xe0\xc5\x95\xc5V\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x01\x17 \x8b4{\xa5\xc1l\n&\xcd3\xfefv\xbbA~\xe8\x1fB\xf1(\x17\xa6\xe4\x11\xee\x8a\xb2\x00\xdf\xe9`\x00\x00" P2WSH_PSBT = b'psbt\xff\x01\x00\xb2\x02\x00\x00\x00\x02\xadC\x87\x14J\xfae\x07\xe1>\xaeP\xda\x1b\xf1\xb5\x1ag\xb3\x0f\xfb\x8e\x0c[\x8f\x98\xf5\xb3\xb1\xa68Y\x00\x00\x00\x00\x00\xfd\xff\xff\xffig%Y\x0f\xb8\xe4r\xab#N\xeb\xf3\xbf\x04\xd9J\xc0\xba\x94\xf6\xa5\xa4\xf8B\xea\xdb\x9a\xd3c`\xd4\x01\x00\x00\x00\x00\xfd\xff\xff\xff\x02@B\x0f\x00\x00\x00\x00\x00"\x00 \xa9\x903\xc3\x86b3>Y\t\xae<=\x03\xbdq\x8d\xb2\x14Y\xfd\xd5P\x1e\xe8\xa0RaMY\xb4\xe2\xd8\xd2!\x01\x00\x00\x00\x00"\x00 \x8d\x02\x85\r\xab\x88^\xc5y\xbbm\xcb\x05\xd6 ;\x05\xf5\x17\x01\x86\xac\xb8\x90}l\xc1\xb4R\x99\xed\xd2\x00\x00\x00\x00O\x01\x045\x87\xcf\x04>b\xdf~\x80\x00\x00\x02A+I\x84\xd5I\xba^\xef\x1c\xa6\xe8\xf3u]\x9a\xe0\x16\xdam\x16ir\xca\x0eQ@6~\xddP\xda\x025\xb8K1\xdc8*|\xfbC\xba:{\x17K\xe9AaA\xe8\x16\xf6r[\xd1%\x12\xb5\xb2\xc4\xa5\xac\x14\x02\x08\xcbw0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80O\x01\x045\x87\xcf\x04\x9d\xb1\xd0\x00\x80\x00\x00\x02?\xd8\xd7;\xc7\xb8\x8c\xa4\x93Z\xa57\xbf8\x94\xd5\xe2\x88\x9f\xab4\x1ca\x8fJWo\x8f\x19\x18\xc2u\x02h\xc3\rV\x9d#j}\xccW\x1b+\xb1\xd2\xadO\xa9\xf9\xb3R\xa8\t6\xa2\x89\n\x99\xaa#\xdbx\xec\x14&\xbb\x83\xc40\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80O\x01\x045\x87\xcf\x04\xba\xc1H9\x80\x00\x00\x02\x1dO\xbe\xbd\xd9g\xe1\xafqL\t\x97\xd3\x8f\xcfg\x0b\\\xe9\xd3\x01\xc0D\x0b\xbc\xc3\xb6\xa2\x0e\xb7r\x1c\x03V\x8e\xa1\xf3`Q\x91n\xd1\xb6\x90\xc3\x9e\x12\xa8\xe7\x06\x03\xb2\x80\xbd0\xce_(\x1f)\x18\xa5Sc\xaa\x14s\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x01\x01+\x80\x96\x98\x00\x00\x00\x00\x00"\x00 \x89\x801pn\xdd\x9e\xb1"g\x85G\x15Q\xce\xa3_\x17\t\xa9o\x85\x96.2\xa0k\xf6~\xc7\x11$\x01\x05iR!\x02N\x8d\x08\x0c}}\xba\\G\xfe\xb6\xb1\xc8\x12M\xebbA\x17\xe5\x8d\x8d~\xb1J@\x04Oq\xdd\x97\xf2!\x03\x05a\xd4\x82\xad\xb9=\xf1\xef\x13\xe8ep\x1a\xf2$n\xf0\xa3l\xbc\x8c\xa5\x12=\x8e\xecw\xceN8\xc7!\x03h\x95r\xe2\x8b\x0f\xed\xa9\xd6\x98\x1c\x027\xd9\xe5\xde\xdb\xfe\xc1m\xe7\x14?h\n\x02\xed]\x15\x9fu\x87S\xae"\x06\x02N\x8d\x08\x0c}}\xba\\G\xfe\xb6\xb1\xc8\x12M\xebbA\x17\xe5\x8d\x8d~\xb1J@\x04Oq\xdd\x97\xf2\x1c&\xbb\x83\xc40\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x01\x00\x00\x00"\x06\x03\x05a\xd4\x82\xad\xb9=\xf1\xef\x13\xe8ep\x1a\xf2$n\xf0\xa3l\xbc\x8c\xa5\x12=\x8e\xecw\xceN8\xc7\x1c\x02\x08\xcbw0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x01\x00\x00\x00"\x06\x03h\x95r\xe2\x8b\x0f\xed\xa9\xd6\x98\x1c\x027\xd9\xe5\xde\xdb\xfe\xc1m\xe7\x14?h\n\x02\xed]\x15\x9fu\x87\x1cs\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x01\x00\x00\x00\x00\x01\x01+\x80\x96\x98\x00\x00\x00\x00\x00"\x00 3w\xad03\xd1\x05\x9c\xf1\xd25\xbb\x12%\xfc\xa2\xa4\xbf&\xc9R\xd5?o\xef\xc3:-UD\x8d\xc5\x01\x05iR!\x02"\x821\x12\xe5\xcc\x88K\x91\x16\xcb!B\x0c\xc7\x92\x98$\xcd/\xe8\xb7#[\xf9\x92\xe8\xae\xde\x14l"!\x02\x83\xcdG\xe5Sm\xcby\xe7\x11\x830\xe8\xe4\x80B\x12\xf6\x96\x19\xf1\xd6\xec\x99\r\xc75\xef\xb9\xce\xc5t!\x03\x0b\x90\xed.\x86\xba\xd7\xf2\xa4\xfe\x97i\xbbA}{\xa9\xca\xa1\x12H\x07\xdb\xfb6-\xfb\xee\xb6^~\x01S\xae"\x06\x02"\x821\x12\xe5\xcc\x88K\x91\x16\xcb!B\x0c\xc7\x92\x98$\xcd/\xe8\xb7#[\xf9\x92\xe8\xae\xde\x14l"\x1c\x02\x08\xcbw0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00"\x06\x02\x83\xcdG\xe5Sm\xcby\xe7\x11\x830\xe8\xe4\x80B\x12\xf6\x96\x19\xf1\xd6\xec\x99\r\xc75\xef\xb9\xce\xc5t\x1c&\xbb\x83\xc40\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00"\x06\x03\x0b\x90\xed.\x86\xba\xd7\xf2\xa4\xfe\x97i\xbbA}{\xa9\xca\xa1\x12H\x07\xdb\xfb6-\xfb\xee\xb6^~\x01\x1cs\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01iR!\x02\xad!\xd9\xad(\xab\x99\xac~\xdf\xd9\x1e"!O\x11YS\xab\t\xd1\xd5X\x10\x92\xfbG\xbd\xa5\x92r\xfe!\x03\xa0};\xe0\xba\xd6<\x805\xd2\x1c\x97\xb4\x10\x89\r=:\x19\xd2\xe4\x03\xaf\xb3\xfc\xfch&\xaa&\x02\xb8\xf4\xcfb\xbc\xc6\xa7\xa2kS\xae"\x02\x02\xad!\xd9\xad(\xab\x99\xac~\xdf\xd9\x1e"!O\x11YS\xab\t\xd1\xd5X\x10\x92\xfbG\xbd\xa5\x92r\xfe\x1c\x02\x08\xcbw0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x01\x00\x00\x00\x00\x00\x00\x00"\x02\x03\xa0};\xe0\xba\xd6<\x805\xd2\x1c\x97\xb4\x10\x89\r=:\x19\xd2\xe4\x03\xaf\xb3\xfc\xfch&\xaa&\x02\xb8\xf4\xcfb\xbc\xc6\xa7\xa2k\x1c&\xbb\x83\xc40\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00' SIGNED_P2WSH_PSBT = b'psbt\xff\x01\x00\xb2\x02\x00\x00\x00\x02\xadC\x87\x14J\xfae\x07\xe1>\xaeP\xda\x1b\xf1\xb5\x1ag\xb3\x0f\xfb\x8e\x0c[\x8f\x98\xf5\xb3\xb1\xa68Y\x00\x00\x00\x00\x00\xfd\xff\xff\xffig%Y\x0f\xb8\xe4r\xab#N\xeb\xf3\xbf\x04\xd9J\xc0\xba\x94\xf6\xa5\xa4\xf8B\xea\xdb\x9a\xd3c`\xd4\x01\x00\x00\x00\x00\xfd\xff\xff\xff\x02@B\x0f\x00\x00\x00\x00\x00"\x00 \xa9\x903\xc3\x86b3>Y\t\xae<=\x03\xbdq\x8d\xb2\x14Y\xfd\xd5P\x1e\xe8\xa0RaMY\xb4\xe2\xd8\xd2!\x01\x00\x00\x00\x00"\x00 \x8d\x02\x85\r\xab\x88^\xc5y\xbbm\xcb\x05\xd6 ;\x05\xf5\x17\x01\x86\xac\xb8\x90}l\xc1\xb4R\x99\xed\xd2\x00\x00\x00\x00\x00\x01\x01+\x80\x96\x98\x00\x00\x00\x00\x00"\x00 \x89\x801pn\xdd\x9e\xb1"g\x85G\x15Q\xce\xa3_\x17\t\xa9o\x85\x96.2\xa0k\xf6~\xc7\x11$"\x02\x03h\x95r\xe2\x8b\x0f\xed\xa9\xd6\x98\x1c\x027\xd9\xe5\xde\xdb\xfe\xc1m\xe7\x14?h\n\x02\xed]\x15\x9fu\x87G0D\x02 h?m\x19\x04C\x89\x95\x8b\xba\xed\xbb\xba8)\t\xae^\xe3`\x16G\xc8\x8bq\x9c\x0e\xbc\xc5\xb1j\xa2\x02 \x05\rP(\xe0\x9cc])q\xe5\xe2S\x9f\xaf+\xe4_\xa9\xc6\xf9\r"%\xf4\xa2\x00;\xa2\xaf2W\x01\x01\x05iR!\x02N\x8d\x08\x0c}}\xba\\G\xfe\xb6\xb1\xc8\x12M\xebbA\x17\xe5\x8d\x8d~\xb1J@\x04Oq\xdd\x97\xf2!\x03\x05a\xd4\x82\xad\xb9=\xf1\xef\x13\xe8ep\x1a\xf2$n\xf0\xa3l\xbc\x8c\xa5\x12=\x8e\xecw\xceN8\xc7!\x03h\x95r\xe2\x8b\x0f\xed\xa9\xd6\x98\x1c\x027\xd9\xe5\xde\xdb\xfe\xc1m\xe7\x14?h\n\x02\xed]\x15\x9fu\x87S\xae\x00\x01\x01+\x80\x96\x98\x00\x00\x00\x00\x00"\x00 3w\xad03\xd1\x05\x9c\xf1\xd25\xbb\x12%\xfc\xa2\xa4\xbf&\xc9R\xd5?o\xef\xc3:-UD\x8d\xc5"\x02\x03\x0b\x90\xed.\x86\xba\xd7\xf2\xa4\xfe\x97i\xbbA}{\xa9\xca\xa1\x12H\x07\xdb\xfb6-\xfb\xee\xb6^~\x01G0D\x02 ~O\x1b\x8c\xbb\x87x\xa3\xbb\xff\x04\xd8\x10Cq\xc8Y\x0f;N6\x97\xd8S\xfeti\x80\xb3\x12\xe0>\x02 l\x93=\x02m\xb4<\x90\xf4%\xf9Z${\xb7\xecO\x19\x15\xa3\xa3S\xf2Q\x81\xdcX\xfb\xd5&\x9e\xc5\x01\x01\x05iR!\x02"\x821\x12\xe5\xcc\x88K\x91\x16\xcb!B\x0c\xc7\x92\x98$\xcd/\xe8\xb7#[\xf9\x92\xe8\xae\xde\x14l"!\x02\x83\xcdG\xe5Sm\xcby\xe7\x11\x830\xe8\xe4\x80B\x12\xf6\x96\x19\xf1\xd6\xec\x99\r\xc75\xef\xb9\xce\xc5t!\x03\x0b\x90\xed.\x86\xba\xd7\xf2\xa4\xfe\x97i\xbbA}{\xa9\xca\xa1\x12H\x07\xdb\xfb6-\xfb\xee\xb6^~\x01S\xae\x00\x00\x00' + SIGNED_P2WSH_PSBT_SD = b'psbt\xff\x01\x00\xb2\x02\x00\x00\x00\x02\xadC\x87\x14J\xfae\x07\xe1>\xaeP\xda\x1b\xf1\xb5\x1ag\xb3\x0f\xfb\x8e\x0c[\x8f\x98\xf5\xb3\xb1\xa68Y\x00\x00\x00\x00\x00\xfd\xff\xff\xffig%Y\x0f\xb8\xe4r\xab#N\xeb\xf3\xbf\x04\xd9J\xc0\xba\x94\xf6\xa5\xa4\xf8B\xea\xdb\x9a\xd3c`\xd4\x01\x00\x00\x00\x00\xfd\xff\xff\xff\x02@B\x0f\x00\x00\x00\x00\x00"\x00 \xa9\x903\xc3\x86b3>Y\t\xae<=\x03\xbdq\x8d\xb2\x14Y\xfd\xd5P\x1e\xe8\xa0RaMY\xb4\xe2\xd8\xd2!\x01\x00\x00\x00\x00"\x00 \x8d\x02\x85\r\xab\x88^\xc5y\xbbm\xcb\x05\xd6 ;\x05\xf5\x17\x01\x86\xac\xb8\x90}l\xc1\xb4R\x99\xed\xd2\x00\x00\x00\x00O\x01\x045\x87\xcf\x04>b\xdf~\x80\x00\x00\x02A+I\x84\xd5I\xba^\xef\x1c\xa6\xe8\xf3u]\x9a\xe0\x16\xdam\x16ir\xca\x0eQ@6~\xddP\xda\x025\xb8K1\xdc8*|\xfbC\xba:{\x17K\xe9AaA\xe8\x16\xf6r[\xd1%\x12\xb5\xb2\xc4\xa5\xac\x14\x02\x08\xcbw0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80O\x01\x045\x87\xcf\x04\x9d\xb1\xd0\x00\x80\x00\x00\x02?\xd8\xd7;\xc7\xb8\x8c\xa4\x93Z\xa57\xbf8\x94\xd5\xe2\x88\x9f\xab4\x1ca\x8fJWo\x8f\x19\x18\xc2u\x02h\xc3\rV\x9d#j}\xccW\x1b+\xb1\xd2\xadO\xa9\xf9\xb3R\xa8\t6\xa2\x89\n\x99\xaa#\xdbx\xec\x14&\xbb\x83\xc40\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80O\x01\x045\x87\xcf\x04\xba\xc1H9\x80\x00\x00\x02\x1dO\xbe\xbd\xd9g\xe1\xafqL\t\x97\xd3\x8f\xcfg\x0b\\\xe9\xd3\x01\xc0D\x0b\xbc\xc3\xb6\xa2\x0e\xb7r\x1c\x03V\x8e\xa1\xf3`Q\x91n\xd1\xb6\x90\xc3\x9e\x12\xa8\xe7\x06\x03\xb2\x80\xbd0\xce_(\x1f)\x18\xa5Sc\xaa\x14s\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x01\x01+\x80\x96\x98\x00\x00\x00\x00\x00"\x00 \x89\x801pn\xdd\x9e\xb1"g\x85G\x15Q\xce\xa3_\x17\t\xa9o\x85\x96.2\xa0k\xf6~\xc7\x11$"\x02\x03h\x95r\xe2\x8b\x0f\xed\xa9\xd6\x98\x1c\x027\xd9\xe5\xde\xdb\xfe\xc1m\xe7\x14?h\n\x02\xed]\x15\x9fu\x87G0D\x02 h?m\x19\x04C\x89\x95\x8b\xba\xed\xbb\xba8)\t\xae^\xe3`\x16G\xc8\x8bq\x9c\x0e\xbc\xc5\xb1j\xa2\x02 \x05\rP(\xe0\x9cc])q\xe5\xe2S\x9f\xaf+\xe4_\xa9\xc6\xf9\r"%\xf4\xa2\x00;\xa2\xaf2W\x01\x01\x05iR!\x02N\x8d\x08\x0c}}\xba\\G\xfe\xb6\xb1\xc8\x12M\xebbA\x17\xe5\x8d\x8d~\xb1J@\x04Oq\xdd\x97\xf2!\x03\x05a\xd4\x82\xad\xb9=\xf1\xef\x13\xe8ep\x1a\xf2$n\xf0\xa3l\xbc\x8c\xa5\x12=\x8e\xecw\xceN8\xc7!\x03h\x95r\xe2\x8b\x0f\xed\xa9\xd6\x98\x1c\x027\xd9\xe5\xde\xdb\xfe\xc1m\xe7\x14?h\n\x02\xed]\x15\x9fu\x87S\xae"\x06\x02N\x8d\x08\x0c}}\xba\\G\xfe\xb6\xb1\xc8\x12M\xebbA\x17\xe5\x8d\x8d~\xb1J@\x04Oq\xdd\x97\xf2\x1c&\xbb\x83\xc40\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x01\x00\x00\x00"\x06\x03\x05a\xd4\x82\xad\xb9=\xf1\xef\x13\xe8ep\x1a\xf2$n\xf0\xa3l\xbc\x8c\xa5\x12=\x8e\xecw\xceN8\xc7\x1c\x02\x08\xcbw0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x01\x00\x00\x00"\x06\x03h\x95r\xe2\x8b\x0f\xed\xa9\xd6\x98\x1c\x027\xd9\xe5\xde\xdb\xfe\xc1m\xe7\x14?h\n\x02\xed]\x15\x9fu\x87\x1cs\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x01\x00\x00\x00\x00\x01\x01+\x80\x96\x98\x00\x00\x00\x00\x00"\x00 3w\xad03\xd1\x05\x9c\xf1\xd25\xbb\x12%\xfc\xa2\xa4\xbf&\xc9R\xd5?o\xef\xc3:-UD\x8d\xc5"\x02\x03\x0b\x90\xed.\x86\xba\xd7\xf2\xa4\xfe\x97i\xbbA}{\xa9\xca\xa1\x12H\x07\xdb\xfb6-\xfb\xee\xb6^~\x01G0D\x02 ~O\x1b\x8c\xbb\x87x\xa3\xbb\xff\x04\xd8\x10Cq\xc8Y\x0f;N6\x97\xd8S\xfeti\x80\xb3\x12\xe0>\x02 l\x93=\x02m\xb4<\x90\xf4%\xf9Z${\xb7\xecO\x19\x15\xa3\xa3S\xf2Q\x81\xdcX\xfb\xd5&\x9e\xc5\x01\x01\x05iR!\x02"\x821\x12\xe5\xcc\x88K\x91\x16\xcb!B\x0c\xc7\x92\x98$\xcd/\xe8\xb7#[\xf9\x92\xe8\xae\xde\x14l"!\x02\x83\xcdG\xe5Sm\xcby\xe7\x11\x830\xe8\xe4\x80B\x12\xf6\x96\x19\xf1\xd6\xec\x99\r\xc75\xef\xb9\xce\xc5t!\x03\x0b\x90\xed.\x86\xba\xd7\xf2\xa4\xfe\x97i\xbbA}{\xa9\xca\xa1\x12H\x07\xdb\xfb6-\xfb\xee\xb6^~\x01S\xae"\x06\x02"\x821\x12\xe5\xcc\x88K\x91\x16\xcb!B\x0c\xc7\x92\x98$\xcd/\xe8\xb7#[\xf9\x92\xe8\xae\xde\x14l"\x1c\x02\x08\xcbw0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00"\x06\x02\x83\xcdG\xe5Sm\xcby\xe7\x11\x830\xe8\xe4\x80B\x12\xf6\x96\x19\xf1\xd6\xec\x99\r\xc75\xef\xb9\xce\xc5t\x1c&\xbb\x83\xc40\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00"\x06\x03\x0b\x90\xed.\x86\xba\xd7\xf2\xa4\xfe\x97i\xbbA}{\xa9\xca\xa1\x12H\x07\xdb\xfb6-\xfb\xee\xb6^~\x01\x1cs\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01iR!\x02\xad!\xd9\xad(\xab\x99\xac~\xdf\xd9\x1e"!O\x11YS\xab\t\xd1\xd5X\x10\x92\xfbG\xbd\xa5\x92r\xfe!\x03\xa0};\xe0\xba\xd6<\x805\xd2\x1c\x97\xb4\x10\x89\r=:\x19\xd2\xe4\x03\xaf\xb3\xfc\xfch&\xaa&\x02\xb8\xf4\xcfb\xbc\xc6\xa7\xa2kS\xae"\x02\x02\xad!\xd9\xad(\xab\x99\xac~\xdf\xd9\x1e"!O\x11YS\xab\t\xd1\xd5X\x10\x92\xfbG\xbd\xa5\x92r\xfe\x1c\x02\x08\xcbw0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x01\x00\x00\x00\x00\x00\x00\x00"\x02\x03\xa0};\xe0\xba\xd6<\x805\xd2\x1c\x97\xb4\x10\x89\r=:\x19\xd2\xe4\x03\xaf\xb3\xfc\xfch&\xaa&\x02\xb8\xf4\xcfb\xbc\xc6\xa7\xa2k\x1c&\xbb\x83\xc40\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00' P2WSH_PSBT_B64 = "cHNidP8BALICAAAAAq1DhxRK+mUH4T6uUNob8bUaZ7MP+44MW4+Y9bOxpjhZAAAAAAD9////aWclWQ+45HKrI07r878E2UrAupT2paT4QurbmtNjYNQBAAAAAP3///8CQEIPAAAAAAAiACCpkDPDhmIzPlkJrjw9A71xjbIUWf3VUB7ooFJhTVm04tjSIQEAAAAAIgAgjQKFDauIXsV5u23LBdYgOwX1FwGGrLiQfWzBtFKZ7dIAAAAATwEENYfPBD5i336AAAACQStJhNVJul7vHKbo83VdmuAW2m0WaXLKDlFANn7dUNoCNbhLMdw4Knz7Q7o6exdL6UFhQegW9nJb0SUStbLEpawUAgjLdzAAAIABAACAAAAAgAIAAIBPAQQ1h88EnbHQAIAAAAI/2Nc7x7iMpJNapTe/OJTV4oifqzQcYY9KV2+PGRjCdQJoww1WnSNqfcxXGyux0q1PqfmzUqgJNqKJCpmqI9t47BQmu4PEMAAAgAEAAIAAAACAAgAAgE8BBDWHzwS6wUg5gAAAAh1Pvr3ZZ+GvcUwJl9OPz2cLXOnTAcBEC7zDtqIOt3IcA1aOofNgUZFu0baQw54SqOcGA7KAvTDOXygfKRilU2OqFHPF2gowAACAAQAAgAAAAIACAACAAAEBK4CWmAAAAAAAIgAgiYAxcG7dnrEiZ4VHFVHOo18XCalvhZYuMqBr9n7HESQBBWlSIQJOjQgMfX26XEf+trHIEk3rYkEX5Y2NfrFKQARPcd2X8iEDBWHUgq25PfHvE+hlcBryJG7wo2y8jKUSPY7sd85OOMchA2iVcuKLD+2p1pgcAjfZ5d7b/sFt5xQ/aAoC7V0Vn3WHU64iBgJOjQgMfX26XEf+trHIEk3rYkEX5Y2NfrFKQARPcd2X8hwmu4PEMAAAgAEAAIAAAACAAgAAgAAAAAABAAAAIgYDBWHUgq25PfHvE+hlcBryJG7wo2y8jKUSPY7sd85OOMccAgjLdzAAAIABAACAAAAAgAIAAIAAAAAAAQAAACIGA2iVcuKLD+2p1pgcAjfZ5d7b/sFt5xQ/aAoC7V0Vn3WHHHPF2gowAACAAQAAgAAAAIACAACAAAAAAAEAAAAAAQErgJaYAAAAAAAiACAzd60wM9EFnPHSNbsSJfyipL8myVLVP2/vwzotVUSNxQEFaVIhAiKCMRLlzIhLkRbLIUIMx5KYJM0v6LcjW/mS6K7eFGwiIQKDzUflU23LeecRgzDo5IBCEvaWGfHW7JkNxzXvuc7FdCEDC5DtLoa61/Kk/pdpu0F9e6nKoRJIB9v7Ni377rZefgFTriIGAiKCMRLlzIhLkRbLIUIMx5KYJM0v6LcjW/mS6K7eFGwiHAIIy3cwAACAAQAAgAAAAIACAACAAAAAAAAAAAAiBgKDzUflU23LeecRgzDo5IBCEvaWGfHW7JkNxzXvuc7FdBwmu4PEMAAAgAEAAIAAAACAAgAAgAAAAAAAAAAAIgYDC5DtLoa61/Kk/pdpu0F9e6nKoRJIB9v7Ni377rZefgEcc8XaCjAAAIABAACAAAAAgAIAAIAAAAAAAAAAAAABAWlSIQKtIdmtKKuZrH7f2R4iIU8RWVOrCdHVWBCS+0e9pZJy/iEDoH074LrWPIA10hyXtBCJDT06GdLkA6+z/PxoJqomPHYhA6GoQ/otQdk71nUpYZFfbkSKdBkkSj4CuPTPYrzGp6JrU64iAgKtIdmtKKuZrH7f2R4iIU8RWVOrCdHVWBCS+0e9pZJy/hwCCMt3MAAAgAEAAIAAAACAAgAAgAEAAAAAAAAAIgIDoH074LrWPIA10hyXtBCJDT06GdLkA6+z/PxoJqomPHYcc8XaCjAAAIABAACAAAAAgAIAAIABAAAAAAAAACICA6GoQ/otQdk71nUpYZFfbkSKdBkkSj4CuPTPYrzGp6JrHCa7g8QwAACAAQAAgAAAAIACAACAAQAAAAAAAAAAAA==" SIGNED_P2WSH_PSBT_B64 = "cHNidP8BALICAAAAAq1DhxRK+mUH4T6uUNob8bUaZ7MP+44MW4+Y9bOxpjhZAAAAAAD9////aWclWQ+45HKrI07r878E2UrAupT2paT4QurbmtNjYNQBAAAAAP3///8CQEIPAAAAAAAiACCpkDPDhmIzPlkJrjw9A71xjbIUWf3VUB7ooFJhTVm04tjSIQEAAAAAIgAgjQKFDauIXsV5u23LBdYgOwX1FwGGrLiQfWzBtFKZ7dIAAAAAAAEBK4CWmAAAAAAAIgAgiYAxcG7dnrEiZ4VHFVHOo18XCalvhZYuMqBr9n7HESQiAgNolXLiiw/tqdaYHAI32eXe2/7BbecUP2gKAu1dFZ91h0cwRAIgaD9tGQRDiZWLuu27ujgpCa5e42AWR8iLcZwOvMWxaqICIAUNUCjgnGNdKXHl4lOfryvkX6nG+Q0iJfSiADuirzJXAQEFaVIhAk6NCAx9fbpcR/62scgSTetiQRfljY1+sUpABE9x3ZfyIQMFYdSCrbk98e8T6GVwGvIkbvCjbLyMpRI9jux3zk44xyEDaJVy4osP7anWmBwCN9nl3tv+wW3nFD9oCgLtXRWfdYdTrgABASuAlpgAAAAAACIAIDN3rTAz0QWc8dI1uxIl/KKkvybJUtU/b+/DOi1VRI3FIgIDC5DtLoa61/Kk/pdpu0F9e6nKoRJIB9v7Ni377rZefgFHMEQCIH5PG4y7h3iju/8E2BBDcchZDztONpfYU/50aYCzEuA+AiBskz0CbbQ8kPQl+Voke7fsTxkVo6NT8lGB3Fj71SaexQEBBWlSIQIigjES5cyIS5EWyyFCDMeSmCTNL+i3I1v5kuiu3hRsIiECg81H5VNty3nnEYMw6OSAQhL2lhnx1uyZDcc177nOxXQhAwuQ7S6GutfypP6XabtBfXupyqESSAfb+zYt++62Xn4BU64AAAA=" P2WPKH_HIGH_FEE_PSBT = "cHNidP8BAP1JAQIAAAAHx8+VLZG8q9fE/9TFGaUDMxlyks8pM6wE1sUzUmlPlDsGAAAAAP3////Hz5Utkbyr18T/1MUZpQMzGXKSzykzrATWxTNSaU+UOwcAAAAA/f///8fPlS2RvKvXxP/UxRmlAzMZcpLPKTOsBNbFM1JpT5Q7BQAAAAD9////x8+VLZG8q9fE/9TFGaUDMxlyks8pM6wE1sUzUmlPlDsCAAAAAP3////Hz5Utkbyr18T/1MUZpQMzGXKSzykzrATWxTNSaU+UOwAAAAAA/f///8fPlS2RvKvXxP/UxRmlAzMZcpLPKTOsBNbFM1JpT5Q7AwAAAAD9////x8+VLZG8q9fE/9TFGaUDMxlyks8pM6wE1sUzUmlPlDsBAAAAAP3///8BhAMAAAAAAAAXqRRiWIJrJ8MsDs5aLI2HPOHxoohj04cU+CoATwEENYfPA04BoMaAAAAADqwTbEcFGhxvEHabuwbcm8HLo8fY/7oVTxfPbpMs1/4DHz9uZifqAdnopjmilOHYCN/7ewoGlnjSzn1pi1wSdj4Q4MWVxVQAAIABAACAAAAAgAABAP19AQIAAAADBmHTgkZvesndEkYg1//mU0bdEWT0NDJ+pookrffo5xoBAAAAAP3///+lkTtN5yuncT07kkZIuZrgPlMIbk3e4Ph14s4MG+S3gQAAAAAA/f///6WRO03nK6dxPTuSRki5muA+UwhuTd7g+HXizgwb5LeBAQAAAAD9////CCwBAAAAAAAAFgAUBFtVYlpUm5iHZvf8cW1Te1D16gIsAQAAAAAAABYAFPiBJvmTv+DdmsRcvIcWU/8LXU0ULAEAAAAAAAAWABTTg2VJqeeWiJbBke8bW5/1ovQ0+iwBAAAAAAAAFgAU0MnlyP/ofvZQ1P3mcA4vKVen9LksAQAAAAAAABYAFOPwOp2QPepm6NFFGXgeWlZoANQHLAEAAAAAAAAWABSY8opy2FeyAGUjkBlsm10RSsV7qCwBAAAAAAAAFgAUXWUVxmbYT+CMAs0oR8G3MYyyuyqZGAAAAAAAABYAFGT8/EuuiDNH8x7K3O7S/vZEyAaHzvwkAAEBHywBAAAAAAAAFgAUXWUVxmbYT+CMAs0oR8G3MYyyuyoBAwQBAAAAIgYDz3BdBJxvxLD1uljlVV9xoAvqKB/2UpNWWX24J9399i8Y4MWVxVQAAIABAACAAAAAgAAAAABdAAAAAAEA/X0BAgAAAAMGYdOCRm96yd0SRiDX/+ZTRt0RZPQ0Mn6miiSt9+jnGgEAAAAA/f///6WRO03nK6dxPTuSRki5muA+UwhuTd7g+HXizgwb5LeBAAAAAAD9////pZE7Tecrp3E9O5JGSLma4D5TCG5N3uD4deLODBvkt4EBAAAAAP3///8ILAEAAAAAAAAWABQEW1ViWlSbmIdm9/xxbVN7UPXqAiwBAAAAAAAAFgAU+IEm+ZO/4N2axFy8hxZT/wtdTRQsAQAAAAAAABYAFNODZUmp55aIlsGR7xtbn/Wi9DT6LAEAAAAAAAAWABTQyeXI/+h+9lDU/eZwDi8pV6f0uSwBAAAAAAAAFgAU4/A6nZA96mbo0UUZeB5aVmgA1AcsAQAAAAAAABYAFJjyinLYV7IAZSOQGWybXRFKxXuoLAEAAAAAAAAWABRdZRXGZthP4IwCzShHwbcxjLK7KpkYAAAAAAAAFgAUZPz8S66IM0fzHsrc7tL+9kTIBofO/CQAAQEfmRgAAAAAAAAWABRk/PxLrogzR/Meytzu0v72RMgGhwEDBAEAAAAiBgJaEB9rY25tmsmbSW9hm9I7kjSB/TCKBH19lFtMxN5f0hjgxZXFVAAAgAEAAIAAAACAAAAAAGMAAAAAAQD9fQECAAAAAwZh04JGb3rJ3RJGINf/5lNG3RFk9DQyfqaKJK336OcaAQAAAAD9////pZE7Tecrp3E9O5JGSLma4D5TCG5N3uD4deLODBvkt4EAAAAAAP3///+lkTtN5yuncT07kkZIuZrgPlMIbk3e4Ph14s4MG+S3gQEAAAAA/f///wgsAQAAAAAAABYAFARbVWJaVJuYh2b3/HFtU3tQ9eoCLAEAAAAAAAAWABT4gSb5k7/g3ZrEXLyHFlP/C11NFCwBAAAAAAAAFgAU04NlSannloiWwZHvG1uf9aL0NPosAQAAAAAAABYAFNDJ5cj/6H72UNT95nAOLylXp/S5LAEAAAAAAAAWABTj8DqdkD3qZujRRRl4HlpWaADUBywBAAAAAAAAFgAUmPKKcthXsgBlI5AZbJtdEUrFe6gsAQAAAAAAABYAFF1lFcZm2E/gjALNKEfBtzGMsrsqmRgAAAAAAAAWABRk/PxLrogzR/Meytzu0v72RMgGh878JAABAR8sAQAAAAAAABYAFJjyinLYV7IAZSOQGWybXRFKxXuoAQMEAQAAACIGA1C98tTHdZErY3znQNhaS7Vs/9iqmv2XajGsFi4kBRHlGODFlcVUAACAAQAAgAAAAIAAAAAAYQAAAAABAP19AQIAAAADBmHTgkZvesndEkYg1//mU0bdEWT0NDJ+pookrffo5xoBAAAAAP3///+lkTtN5yuncT07kkZIuZrgPlMIbk3e4Ph14s4MG+S3gQAAAAAA/f///6WRO03nK6dxPTuSRki5muA+UwhuTd7g+HXizgwb5LeBAQAAAAD9////CCwBAAAAAAAAFgAUBFtVYlpUm5iHZvf8cW1Te1D16gIsAQAAAAAAABYAFPiBJvmTv+DdmsRcvIcWU/8LXU0ULAEAAAAAAAAWABTTg2VJqeeWiJbBke8bW5/1ovQ0+iwBAAAAAAAAFgAU0MnlyP/ofvZQ1P3mcA4vKVen9LksAQAAAAAAABYAFOPwOp2QPepm6NFFGXgeWlZoANQHLAEAAAAAAAAWABSY8opy2FeyAGUjkBlsm10RSsV7qCwBAAAAAAAAFgAUXWUVxmbYT+CMAs0oR8G3MYyyuyqZGAAAAAAAABYAFGT8/EuuiDNH8x7K3O7S/vZEyAaHzvwkAAEBHywBAAAAAAAAFgAU04NlSannloiWwZHvG1uf9aL0NPoBAwQBAAAAIgYC1sS/lSW4MscM8RNpfaFkTeTr3NEapRcqIRsX0yMSYk0Y4MWVxVQAAIABAACAAAAAgAAAAABeAAAAAAEA/X0BAgAAAAMGYdOCRm96yd0SRiDX/+ZTRt0RZPQ0Mn6miiSt9+jnGgEAAAAA/f///6WRO03nK6dxPTuSRki5muA+UwhuTd7g+HXizgwb5LeBAAAAAAD9////pZE7Tecrp3E9O5JGSLma4D5TCG5N3uD4deLODBvkt4EBAAAAAP3///8ILAEAAAAAAAAWABQEW1ViWlSbmIdm9/xxbVN7UPXqAiwBAAAAAAAAFgAU+IEm+ZO/4N2axFy8hxZT/wtdTRQsAQAAAAAAABYAFNODZUmp55aIlsGR7xtbn/Wi9DT6LAEAAAAAAAAWABTQyeXI/+h+9lDU/eZwDi8pV6f0uSwBAAAAAAAAFgAU4/A6nZA96mbo0UUZeB5aVmgA1AcsAQAAAAAAABYAFJjyinLYV7IAZSOQGWybXRFKxXuoLAEAAAAAAAAWABRdZRXGZthP4IwCzShHwbcxjLK7KpkYAAAAAAAAFgAUZPz8S66IM0fzHsrc7tL+9kTIBofO/CQAAQEfLAEAAAAAAAAWABQEW1ViWlSbmIdm9/xxbVN7UPXqAgEDBAEAAAAiBgOJsnJY/31qHnpEdTEO2Vlnov5bpTUARCgRgnglWJAFXRjgxZXFVAAAgAEAAIAAAACAAAAAAF8AAAAAAQD9fQECAAAAAwZh04JGb3rJ3RJGINf/5lNG3RFk9DQyfqaKJK336OcaAQAAAAD9////pZE7Tecrp3E9O5JGSLma4D5TCG5N3uD4deLODBvkt4EAAAAAAP3///+lkTtN5yuncT07kkZIuZrgPlMIbk3e4Ph14s4MG+S3gQEAAAAA/f///wgsAQAAAAAAABYAFARbVWJaVJuYh2b3/HFtU3tQ9eoCLAEAAAAAAAAWABT4gSb5k7/g3ZrEXLyHFlP/C11NFCwBAAAAAAAAFgAU04NlSannloiWwZHvG1uf9aL0NPosAQAAAAAAABYAFNDJ5cj/6H72UNT95nAOLylXp/S5LAEAAAAAAAAWABTj8DqdkD3qZujRRRl4HlpWaADUBywBAAAAAAAAFgAUmPKKcthXsgBlI5AZbJtdEUrFe6gsAQAAAAAAABYAFF1lFcZm2E/gjALNKEfBtzGMsrsqmRgAAAAAAAAWABRk/PxLrogzR/Meytzu0v72RMgGh878JAABAR8sAQAAAAAAABYAFNDJ5cj/6H72UNT95nAOLylXp/S5AQMEAQAAACIGApFgNphi/Y+tOwzEH2UfKClwfJeJJJzSgzTqK01oIqC8GODFlcVUAACAAQAAgAAAAIAAAAAAYAAAAAABAP19AQIAAAADBmHTgkZvesndEkYg1//mU0bdEWT0NDJ+pookrffo5xoBAAAAAP3///+lkTtN5yuncT07kkZIuZrgPlMIbk3e4Ph14s4MG+S3gQAAAAAA/f///6WRO03nK6dxPTuSRki5muA+UwhuTd7g+HXizgwb5LeBAQAAAAD9////CCwBAAAAAAAAFgAUBFtVYlpUm5iHZvf8cW1Te1D16gIsAQAAAAAAABYAFPiBJvmTv+DdmsRcvIcWU/8LXU0ULAEAAAAAAAAWABTTg2VJqeeWiJbBke8bW5/1ovQ0+iwBAAAAAAAAFgAU0MnlyP/ofvZQ1P3mcA4vKVen9LksAQAAAAAAABYAFOPwOp2QPepm6NFFGXgeWlZoANQHLAEAAAAAAAAWABSY8opy2FeyAGUjkBlsm10RSsV7qCwBAAAAAAAAFgAUXWUVxmbYT+CMAs0oR8G3MYyyuyqZGAAAAAAAABYAFGT8/EuuiDNH8x7K3O7S/vZEyAaHzvwkAAEBHywBAAAAAAAAFgAU+IEm+ZO/4N2axFy8hxZT/wtdTRQBAwQBAAAAIgYCpGJuz21gtqi+5Um21HYWtiEz04i8VYjEkhjL64TSTYcY4MWVxVQAAIABAACAAAAAgAAAAABcAAAAAAA=" @@ -93,13 +98,17 @@ def tdata(mocker): "SPECTER_MULTISIG_WALLET_DATA", "P2WPKH_PSBT", "SIGNED_P2WPKH_PSBT", + "SIGNED_P2WPKH_PSBT_SD", "P2WPKH_PSBT_B64", "P2WPKH_PSBT_B64_ZEROES_FINGERPRINT", "P2TR_PSBT_BIN_ZEROES_FINGERPRINT", "SIGNED_P2WPKH_PSBT_B64", + "SIGNED_P2WPKH_PSBT_B64_SD", "SIGNED_P2TR_PSBT_BIN", + "SIGNED_P2TR_PSBT_BIN_SD", "P2WSH_PSBT", "SIGNED_P2WSH_PSBT", + "SIGNED_P2WSH_PSBT_SD", "P2WSH_PSBT_B64", "SIGNED_P2WSH_PSBT_B64", "P2WPKH_HIGH_FEE_PSBT", @@ -125,13 +134,17 @@ def tdata(mocker): SPECTER_MULTISIG_WALLET_DATA, P2WPKH_PSBT, SIGNED_P2WPKH_PSBT, + SIGNED_P2WPKH_PSBT_SD, P2WPKH_PSBT_B64, P2WPKH_PSBT_B64_ZEROES_FINGERPRINT, P2TR_PSBT_BIN_ZEROES_FINGERPRINT, SIGNED_P2WPKH_PSBT_B64, + SIGNED_P2WPKH_PSBT_B64_SD, SIGNED_P2TR_PSBT_BIN, + SIGNED_P2TR_PSBT_BIN_SD, P2WSH_PSBT, SIGNED_P2WSH_PSBT, + SIGNED_P2WSH_PSBT_SD, P2WSH_PSBT_B64, SIGNED_P2WSH_PSBT_B64, P2WPKH_HIGH_FEE_PSBT, @@ -566,7 +579,7 @@ def test_sign_psbt(mocker, m5stickv, tdata): BUTTON_PAGE, # Move to "Sign to QR SD card" BUTTON_ENTER, # Sign to SD card ], - tdata.SIGNED_P2WPKH_PSBT, # 8 SD avaiable + tdata.SIGNED_P2WPKH_PSBT_SD, # 8 SD avaiable ), # Multisig, not loaded, load from microSD, sign, save to microSD, No print prompt ( @@ -590,7 +603,7 @@ def test_sign_psbt(mocker, m5stickv, tdata): BUTTON_PAGE, # Move to "Sign to QR SD card" BUTTON_ENTER, # Sign to SD card ], - tdata.SIGNED_P2WSH_PSBT, # 8 SD avaiable + tdata.SIGNED_P2WSH_PSBT_SD, # 8 SD avaiable ), # Single-sig base64, not loaded, load from microSD, sign to microSD ( @@ -612,7 +625,7 @@ def test_sign_psbt(mocker, m5stickv, tdata): BUTTON_PAGE, # Move to "Sign to QR SD card" BUTTON_ENTER, # Sign to SD card ], - tdata.SIGNED_P2WPKH_PSBT_B64, # 8 SD avaiable + tdata.SIGNED_P2WPKH_PSBT_B64_SD, # 8 SD avaiable ), ] # Case X @@ -804,7 +817,7 @@ def test_psbt_warnings(mocker, m5stickv, tdata): ctx.display.draw_centered_text.assert_has_calls( [ mocker.call( - "Warning: Path mismatch\nWallet: m/48'/0'/0'/2'\nPSBT: m/48'/1'/0'/2'" + "Warning: Path mismatch\nWallet: m/48h/0h/0h/2h\nPSBT: m/48h/1h/0h/2h" ), mocker.call( "PSBT policy:\np2wsh\n2 of 3\n⊚" @@ -835,6 +848,120 @@ def test_psbt_warnings(mocker, m5stickv, tdata): home.display_qr_codes.assert_not_called() +def test_psbt_warnings_taproot_miniscript(mocker, m5stickv, psbt_tdata): + from krux.pages.home_pages.home import Home + from krux.wallet import Wallet + from krux.key import Key, NETWORKS, TYPE_MINISCRIPT, P2TR + from krux.input import BUTTON_ENTER, BUTTON_PAGE + from krux.sd_card import ( + PSBT_FILE_EXTENSION, + B64_FILE_EXTENSION, + SIGNED_FILE_SUFFIX, + ) + from krux.settings import THIN_SPACE + + PSBT_FILE_NAME = "test.psbt" + SIGNED_PSBT_FILE_NAME = "test-signed.psbt" + + wallet = Wallet( + # Use mainnet key with testnet psbt to trigger a path mismatch warning + Key( + psbt_tdata.TEST_MNEMONIC, + TYPE_MINISCRIPT, + NETWORKS["main"], + script_type=P2TR, + ) + ) + + btn_seq = [ + BUTTON_ENTER, # Wallet not loaded, proceed? + BUTTON_PAGE, # Move to "Load from SD card" + BUTTON_ENTER, # Load from SD card + BUTTON_ENTER, # Path mismatch ACK + BUTTON_ENTER, # PSBT Policy ACK + BUTTON_ENTER, # PSBT resume + BUTTON_ENTER, # output 1 + BUTTON_PAGE, # Move to "Sign to QR SD card" + BUTTON_ENTER, # Sign to SD card + ] + + ctx = create_ctx(mocker, btn_seq, wallet) + home = Home(ctx) + + mocker.spy(home, "display_qr_codes") + mocker.spy(ctx.display, "draw_centered_text") + + # SD available + mocker.patch.object(home, "has_sd_card", new=lambda: True) + mock_utils = mocker.patch("krux.pages.utils.Utils") + mock_utils.return_value.load_file.return_value = (PSBT_FILE_NAME, None) + # Mock for reading from input file + mock_open_read = mocker.mock_open(read_data=psbt_tdata.RECOV_M_TR_PSBT) + # Mock for writing to output file + mock_open_write = mocker.mock_open() + # Ensure the write method returns the number of bytes written + mock_open_write.return_value.write.side_effect = lambda x: len(x) + mocker.patch( + "builtins.open", + side_effect=[mock_open_read.return_value, mock_open_write.return_value], + ) + mock_set_filename = mocker.patch( + "krux.pages.file_operations.SaveFile.set_filename", + return_value=SIGNED_PSBT_FILE_NAME, + ) + + # Wallet output descriptor not loaded + assert ctx.wallet.is_loaded() == wallet.is_loaded() == False + + # Wallet is multisig + assert ctx.wallet.is_miniscript() == wallet.is_miniscript() == True + + home.sign_psbt() + + # all inputs were used/consumed + print(ctx.input.wait_for_button.call_count, len(btn_seq)) + assert ctx.input.wait_for_button.call_count == len(btn_seq) + + # Wallet output descriptor not loaded had to show a warning + ctx.display.draw_centered_text.assert_any_call( + "Warning:\nWallet output descriptor not found.\n\nSome checks cannot be performed." + ) + + # These two calls must have occured in sequence + ctx.display.draw_centered_text.assert_has_calls( + [ + mocker.call( + "Warning: Path mismatch\nWallet: m/48h/0h/0h/2h\nPSBT: m/48h/1h/0h/2h" + ), + mocker.call( + "PSBT policy:\np2tr" + + "\n⊚" + + THIN_SPACE + + "02e8bff2" + + "\n⊚" + + THIN_SPACE + + "73c5da0a" + ), + ] + ) + + # signed from/to SD card + mock_utils.return_value.load_file.assert_called_once_with( + [PSBT_FILE_EXTENSION, B64_FILE_EXTENSION], + prompt=False, + only_get_filename=True, + ) + mock_set_filename.assert_called_once_with( + PSBT_FILE_NAME, + "QRCode", + SIGNED_FILE_SUFFIX, + PSBT_FILE_EXTENSION, + ) + + # no qrcode + home.display_qr_codes.assert_not_called() + + def test_sign_wrong_key(mocker, m5stickv, tdata): from krux.pages.home_pages.home import Home from krux.wallet import Wallet @@ -844,7 +971,6 @@ def test_sign_wrong_key(mocker, m5stickv, tdata): btn_seq = [ BUTTON_ENTER, # Load from QR code - BUTTON_ENTER, # Path mismatch ACK BUTTON_ENTER, # PSBT resume BUTTON_ENTER, # output 1 BUTTON_ENTER, # output 2 @@ -886,7 +1012,6 @@ def test_sign_zeroes_fingerprint(mocker, m5stickv, tdata): btn_seq = [ BUTTON_ENTER, # Load from QR code - BUTTON_ENTER, # Path mismatch ACK BUTTON_ENTER, # Confirm fingerprint missing BUTTON_ENTER, # PSBT resume BUTTON_ENTER, # output 1 @@ -992,7 +1117,7 @@ def test_sign_p2tr_zeroes_fingerprint(mocker, m5stickv, tdata): handle_write = mock_open_write() # # Embit will write the signed PSBT to the output file in chunks. Capture all write calls written_data = b"".join(call.args[0] for call in handle_write.write.call_args_list) - assert written_data == tdata.SIGNED_P2TR_PSBT_BIN + assert written_data == tdata.SIGNED_P2TR_PSBT_BIN_SD def test_sign_high_fee(mocker, m5stickv, tdata): @@ -1035,7 +1160,7 @@ def test_sign_high_fee(mocker, m5stickv, tdata): ctx.display.draw_centered_text.assert_has_calls( [ mocker.call( - "Warning: Path mismatch\nWallet: m/84'/0'/0'\nPSBT: m/84'/1'/0'" + "Warning: Path mismatch\nWallet: m/84h/0h/0h\nPSBT: m/84h/1h/0h" ), mocker.call("Processing.."), mocker.call("Warning: High fees!\n799.7% of the amount."), @@ -1085,7 +1210,7 @@ def test_sign_self(mocker, m5stickv, tdata): ctx.display.draw_centered_text.assert_has_calls( [ mocker.call( - "Warning: Path mismatch\nWallet: m/84'/0'/0'\nPSBT: m/84'/1'/0'" + "Warning: Path mismatch\nWallet: m/84h/0h/0h\nPSBT: m/84h/1h/0h" ), mocker.call("Processing.."), mocker.call("Warning: High fees!\n799.7% of the amount."), @@ -1136,7 +1261,7 @@ def test_sign_spent_and_self(mocker, m5stickv, tdata): ctx.display.draw_centered_text.assert_has_calls( [ mocker.call( - "Warning: Path mismatch\nWallet: m/84'/0'/0'\nPSBT: m/84'/1'/0'" + "Warning: Path mismatch\nWallet: m/84h/0h/0h\nPSBT: m/84h/1h/0h" ), mocker.call("Processing.."), mocker.call("Warning: High fees!\n235.9% of the amount."), diff --git a/tests/pages/home_pages/test_miniscript_indenter.py b/tests/pages/home_pages/test_miniscript_indenter.py new file mode 100644 index 000000000..d1963a326 --- /dev/null +++ b/tests/pages/home_pages/test_miniscript_indenter.py @@ -0,0 +1,259 @@ +def test_intender(mocker, amigo): + from krux.pages.home_pages.miniscript_indenter import MiniScriptIndenter + + cases = [ + # Sipa's example scripts + ( + "pk(A)", + """ +pk( + A) +""", + ), + ( + "or_b(pk(A),s:pk(B))", + """ +or_b( + pk(A), + s:pk(B)) +""", + ), + ( + "or_d(pk(A),pkh(B))", + """ +or_d( + pk(A), + pkh(B)) +""", + ), + ( + "and_v(v:pk(A),or_d(pk(B),older(12960)))", + """ +and_v( + v:pk(A), + or_d( + pk(B), + older(12960))) +""", + ), + ( + "thresh(3,pk(A),pk(B),pk(C),older(12960))", + """ +thresh( + 3, + pk(A), + pk(B), + pk(C), + older(12960)) +""", + ), + ( + "andor(pk(A),older(1008),pk(B))", + """ +andor( + pk(A), + older(1008), + pk(B)) +""", + ), + ( + "t:or_c(pk(A),and_v(v:pk(B),or_c(pk(C),v:hash160(e7d285b4817f83f724cd29394da75dfc84fe639e))))", + """ +t:or_c( + pk(A), + and_v( + v:pk(B), + or_c( + pk(C), + v:hash160( + e7d285b4817f83f724cd2 + 9394da75dfc84fe639e)) + )) +""", + """ +t:or_c( + pk(A), + and_v( + v:pk(B), + or_c( + pk(C), + v:hash160( + e7d285b4817f83f724cd293 + 94da75dfc84fe639e)))) +""", + ), + ( + "andor(pk(A),or_i(and_v(v:pkh(B),hash160(e7d285b4817f83f724cd29394da75dfc84fe639e)),older(1008)),pk(C))", + """ +andor( + pk(A), + or_i( + and_v( + v:pkh(B), + hash160( + e7d285b4817f83f724cd2 + 9394da75dfc84fe639e)) + , + older(1008)), + pk(C)) +""", + """ +andor( + pk(A), + or_i( + and_v( + v:pkh(B), + hash160( + e7d285b4817f83f724cd293 + 94da75dfc84fe639e)), + older(1008)), + pk(C)) +""", + ), + # # Other examples + ( + "or_d(pk(A),and_v(v:pkh(B),older(6)))", + """ +or_d( + pk(A), + and_v( + v:pkh(B), + older(6))) +""", + ), + ( + "and_v(or_c(pk(B),or_c(pk(C),v:older(1000))),pk(A))", + """ +and_v( + or_c( + pk(B), + or_c( + pk(C), + v:older(1000))), + pk(A)) +""", + ), + ( + "or_d(multi(2,A,B),and_v(v:thresh(2,pkh(C),a:pkh(D),a:pkh(E)),older(144)))", + """ +or_d( + multi(2,A,B), + and_v( + v:thresh( + 2, + pkh(C), + a:pkh(D), + a:pkh(E)), + older(144))) +""", + ), + ( + "andor(multi(2,A,B,C),or_i(and_v(v:pkh(D),after(230436)),thresh(2,pk(E),s:pk(F),s:pk(G),snl:after(230220))),and_v(v:thresh(2,pkh(H),a:pkh(I),a:pkh(J)),after(230775)))", + """ +andor( + multi(2,A,B,C), + or_i( + and_v( + v:pkh(D), + after(230436)), + thresh( + 2, + pk(E), + s:pk(F), + s:pk(G), + snl:after(230220))), + and_v( + v:thresh( + 2, + pkh(H), + a:pkh(I), + a:pkh(J)), + after(230775))) +""", + ), + ( + "andor(multi(2,A,B,C),or_i(and_v(v:pkh(D),after(1737233087)),thresh(2,pk(E),s:pk(F),s:pk(G),snl:after(1737146691))),and_v(v:thresh(2,pkh(H),a:pkh(I),a:pkh(J)),after(1737319495)))", + """ +andor( + multi(2,A,B,C), + or_i( + and_v( + v:pkh(D), + after(1737233087)), + thresh( + 2, + pk(E), + s:pk(F), + s:pk(G), + snl:after(1737146691)) + ), + and_v( + v:thresh( + 2, + pkh(H), + a:pkh(I), + a:pkh(J)), + after(1737319495))) +""", + """ +andor( + multi(2,A,B,C), + or_i( + and_v( + v:pkh(D), + after(1737233087)), + thresh( + 2, + pk(E), + s:pk(F), + s:pk(G), + snl:after(1737146691))), + and_v( + v:thresh( + 2, + pkh(H), + a:pkh(I), + a:pkh(J)), + after(1737319495))) +""", + ), + ( + "tr(A,and_v(v:pk(B),older(65535)))", + """ +tr( + A, + and_v( + v:pk(B), + older(65535))) +""", + ), + ( + "tr(A,{and_v(v:multi_a(2,B,C,D),older(6)),multi_a(2,F,G)})", + """ +tr( + A, + {and_v( + v:multi_a(2,B,C,D), + older(6)), + multi_a(2,F,G)}) +""", + ), + ] + + for case in cases: + # Case for Amigo + indented = MiniScriptIndenter().indent(case[0], max_line_width=25) + indented = "\n".join(indented) + assert indented == case[1].strip() + assert indented.replace("\n", "").replace(" ", "") == case[0] + + # Case for Yahboom/WonderMV + indented = MiniScriptIndenter().indent(case[0], max_line_width=27) + indented = "\n".join(indented) + if len(case) > 2: + # Has a special case for Yahboom/WonderMV + assert indented == case[2].strip() + else: + # Results are the same as for Amigo + assert indented == case[1].strip() + assert indented.replace("\n", "").replace(" ", "") == case[0] diff --git a/tests/pages/home_pages/test_pub_key_view.py b/tests/pages/home_pages/test_pub_key_view.py index 675e08f79..4fe5f88d0 100644 --- a/tests/pages/home_pages/test_pub_key_view.py +++ b/tests/pages/home_pages/test_pub_key_view.py @@ -79,7 +79,6 @@ def test_public_key(mocker, m5stickv, tdata): pub_key_viewer.public_key() version = "Zpub" if ctx.wallet.key.policy_type == TYPE_MULTISIG else "zpub" - print(ctx.key.policy_type, version) qr_view_calls = [] print_qr_calls = [] diff --git a/tests/pages/home_pages/test_wallet_descriptor.py b/tests/pages/home_pages/test_wallet_descriptor.py index 2f43e6e58..4f9030238 100644 --- a/tests/pages/home_pages/test_wallet_descriptor.py +++ b/tests/pages/home_pages/test_wallet_descriptor.py @@ -1,5 +1,6 @@ from ...shared_mocks import MockPrinter from .test_home import tdata, create_ctx +from ...test_wallet import tdata as wallet_tdata def test_wallet(mocker, m5stickv, tdata): @@ -18,7 +19,7 @@ def test_wallet(mocker, m5stickv, tdata): None, [BUTTON_PAGE], ), - # 1 Load, from camera, good data, accept + # 1 Load, from camera, good data - accept ( False, tdata.SINGLESIG_12_WORD_KEY, @@ -74,7 +75,7 @@ def test_wallet(mocker, m5stickv, tdata): tdata.MULTISIG_12_WORD_KEY, tdata.SPECTER_MULTISIG_WALLET_DATA, None, - [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], + [BUTTON_ENTER], ), # 9 vague BlueWallet-ish p2pkh, requires allow_assumption ( @@ -154,6 +155,7 @@ def test_wallet(mocker, m5stickv, tdata): "display_qr_codes", new=lambda data, qr_format, title=None: ctx.input.wait_for_button(), ) + mocker.spy(wallet_descriptor, "display_loading_wallet") mocker.spy(wallet_descriptor, "display_wallet") # Mock SD card descriptor loading @@ -164,15 +166,98 @@ def test_wallet(mocker, m5stickv, tdata): wallet_descriptor.wallet() if case[0]: + # If wallet is already loaded wallet_descriptor.display_wallet.assert_called_once() else: # If accepted the message and choose to load from camera if case[4][:2] == [BUTTON_ENTER, BUTTON_ENTER]: qr_capturer.assert_called_once() if case[2] is not None and case[2] != "{}": - wallet_descriptor.display_wallet.assert_called_once() + wallet_descriptor.display_loading_wallet.assert_called_once() # If accepted the message and choose to load from SD elif case[4][:3] == [BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER]: if case[2] is not None and case[2] != "{}": - wallet_descriptor.display_wallet.assert_called_once() + wallet_descriptor.display_loading_wallet.assert_called_once() assert ctx.input.wait_for_button.call_count == len(case[4]) + + +def test_loading_miniscript_descriptors(mocker, amigo, wallet_tdata): + """Miniscript specific tests. Always load from camera""" + from krux.pages.home_pages.wallet_descriptor import WalletDescriptor + from krux.wallet import Wallet + from krux.input import BUTTON_ENTER, BUTTON_PAGE + from krux.qr import FORMAT_PMOFN + from krux.pages.qr_capture import QRCodeCapture + + TRIDENT_DESCRIPTOR = "wsh(andor(multi(2,[fbf14e49/45h/1h/0h/3h]tpubDEPmZmWcL9G3XEbhBy6A5UG7tR4hAT7zvhu4cVmCSbVPhjkfuYRgqFnUfG4Gm1NSaoo412nzyRe3UAtC73BHQbVDLz4nAkrhJDSxcYSpUnz/<0;1>/*,[525cb3d5/45h/1h/0h/3h]tpubDF4yVr6ohjK1hQgyHvtLpanC4JxkshsMVUDHfmDvpXcBzdD2peXKdhfLFVNWQekAYAN1vU81dUNfgokZb1foUQfDMtf6X8mb3vMs7cYHbcr/<0;1>/*,[5fc83bce/45h/1h/0h/3h]tpubDFMqbP9gd34rd5Db2hHVYsJA3LnBD2fZo6zWFzeAA2kUC27cndyN2axBs55K9qJSghbvZx1Nyrrvb2ixgLXRzyK7dLLnXHGAmHe7apv4XwU/<0;1>/*),or_i(and_v(v:pkh([5acaced1/49h/1h/0h]tpubDDXMHf1PVPUPYHKyR9b5pbsfcd4SDC5FHtx7msTwazX4gkZPCRjoTYB2mFR4HsiybdptPtKH7yyoogx9d2gvc92SaoCYANEdZYqRR6FJKGx/<0;1>/*),after(230436)),thresh(2,pk([5ecc195f/48h/1h/0h/2h]tpubDFfTpjFSFT9FFvWwXand2JfnRBSpekQQpzdoz5qm8fy6cUhjLdTBuNrqxdsFgyTJ6xr5oeUAqa28VHPMprbosXLhGEgJW4SPa31tuSmp9Ub/<0;1>/*),s:pk([a1088994/48h/1h/0h/2h]tpubDFE64qjVGZ8L31gXFNtRUUpbaZ5viPgkFpth8j3XfGNWgaM6Vsm3F4z1nNE1soY3cQc6YZtNqMqfrywkeAQMiiYnR8N1oyFP5YuuFYTQ2nx/<0;1>/*),s:pk([8faeabe8/48h/1h/0h/2h]tpubDEMz5Gib3V3i1xzY4yaKH2k2J4MBRjNYNSace1YHMr6MgaM1oLZ4qiF7mWQvGPm9gH5bgroqPMr44viw16XWYoig6rbCQrkzakJw6hsapFw/<0;1>/*),snl:after(230220))),and_v(v:thresh(2,pkh([2bd4a49f/84h/1h/1h]tpubDCtwDKhf7tMtt2NDNrWsN7tFQSEvoKt9qvSBMUPuZVnoR52FwSaQS37UT5skDddUyzhVEGJozGxu8CBJPPc8MXhXidD7azaubMHgNCPvq28/<0;1>/*),a:pkh([d38f3599/84h/1h/2h]tpubDDYgycbJd7DgJjKFd4W8Dp8RRNhDDYfLs93cjhBP6boyXiZxdUyZc8fuLMJyetQXq6i9xfYSJwEf1GYxmND6jXExLS9q9ibP2YXZxtqe7mK/<0;1>/*),a:pkh([001ceab0/84h/1h/3h]tpubDCuJUyHrMq4PY4fXEHyADTkFwgy498AnuhrhFzgT7tWuuwp9JAeopqMTre99nzEVnqJNsJk21VRLeLsGz4cA5hboULrupdHqiZdxKRLJV9R/<0;1>/*)),after(230775))))#5flg0r73" + + cases = [ + # 0 - Key + # 1 - Wallet + # 2 - Button presses + ( # Miniscript key, Liana miniscript descriptor + wallet_tdata.MINISCRIPT_KEY, + wallet_tdata.LIANA_MINISCRIPT_DESCRIPTOR, + [ + BUTTON_ENTER, # Load? Yes + BUTTON_ENTER, # Load from camera + BUTTON_ENTER, # Accept + ], + ), + ( # No key, Liana miniscript descriptor + None, + wallet_tdata.LIANA_MINISCRIPT_DESCRIPTOR, + [ + BUTTON_ENTER, # Load? Yes + BUTTON_ENTER, # Load from camera + BUTTON_ENTER, # Accept + ], + ), + ( # No Key, Trident descriptor + None, + TRIDENT_DESCRIPTOR, + [ + BUTTON_ENTER, # Load? Yes + BUTTON_ENTER, # Load from camera + BUTTON_ENTER, # To next page + BUTTON_ENTER, # To next page + BUTTON_ENTER, # To next page + BUTTON_ENTER, # Accept + ], + ), + ( # Taproot miniscript key, Liana taproot miniscript descriptor + wallet_tdata.TAP_MINISCRIPT_KEY, + wallet_tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, + [ + BUTTON_ENTER, # Load? Yes + BUTTON_ENTER, # Load from camera + BUTTON_ENTER, # Accept + ], + ), + ( # Taproot miniscript key, Liana taproot expanding multisig miniscript descriptor + wallet_tdata.TAP_MINISCRIPT_KEY, + wallet_tdata.LIANA_TAP_EXPANDING_MINISCRIPT_DESCRIPTOR, + [ + BUTTON_ENTER, # Load? Yes + BUTTON_ENTER, # Load from camera + BUTTON_ENTER, # To next page + BUTTON_ENTER, # Accept + ], + ), + ] + + for case in cases: + ctx = create_ctx(mocker, case[2], Wallet(case[0])) + wallet_descriptor = WalletDescriptor(ctx) + mocker.patch.object( + QRCodeCapture, "qr_capture_loop", new=lambda self: (case[1], FORMAT_PMOFN) + ) + mocker.patch.object( + wallet_descriptor, + "display_qr_codes", + new=lambda data, qr_format, title=None: ctx.input.wait_for_button(), + ) + mocker.spy(wallet_descriptor, "display_loading_wallet") + wallet_descriptor.wallet() + wallet_descriptor.display_loading_wallet.assert_called_once() + assert ctx.input.wait_for_button.call_count == len(case[2]) diff --git a/tests/pages/test_encryption_ui.py b/tests/pages/test_encryption_ui.py index 7473394f0..84ed5a0b9 100644 --- a/tests/pages/test_encryption_ui.py +++ b/tests/pages/test_encryption_ui.py @@ -117,7 +117,7 @@ def test_encrypt_cbc_sd_ui(m5stickv, mocker, mock_file_operations): from krux.krux_settings import Settings from krux.input import BUTTON_ENTER, BUTTON_PAGE from krux.pages.encryption_ui import EncryptMnemonic - from krux.key import Key + from krux.key import Key, TYPE_SINGLESIG from embit.networks import NETWORKS BTN_SEQUENCE = ( @@ -128,7 +128,7 @@ def test_encrypt_cbc_sd_ui(m5stickv, mocker, mock_file_operations): + [BUTTON_ENTER] # Confirm encryption ID ) ctx = create_ctx(mocker, BTN_SEQUENCE) - ctx.wallet = Wallet(Key(CBC_WORDS, False, NETWORKS["main"])) + ctx.wallet = Wallet(Key(CBC_WORDS, TYPE_SINGLESIG, NETWORKS["main"])) storage_ui = EncryptMnemonic(ctx) mocker.patch( "krux.pages.encryption_ui.EncryptionKey.encryption_key", @@ -222,7 +222,7 @@ def test_encrypt_to_qrcode_ecb_ui(m5stickv, mocker): from krux.krux_settings import Settings from krux.input import BUTTON_ENTER, BUTTON_PAGE from krux.pages.encryption_ui import EncryptMnemonic - from krux.key import Key + from krux.key import Key, TYPE_SINGLESIG from embit.networks import NETWORKS BTN_SEQUENCE = ( @@ -233,7 +233,7 @@ def test_encrypt_to_qrcode_ecb_ui(m5stickv, mocker): # QR view is mocked here, no press needed ) ctx = create_ctx(mocker, BTN_SEQUENCE) - ctx.wallet = Wallet(Key(ECB_WORDS, False, NETWORKS["main"])) + ctx.wallet = Wallet(Key(ECB_WORDS, TYPE_SINGLESIG, NETWORKS["main"])) ctx.printer = None storage_ui = EncryptMnemonic(ctx) mocker.patch( @@ -258,7 +258,7 @@ def test_encrypt_to_qrcode_cbc_ui(m5stickv, mocker): from krux.krux_settings import Settings from krux.input import BUTTON_ENTER, BUTTON_PAGE from krux.pages.encryption_ui import EncryptMnemonic - from krux.key import Key + from krux.key import Key, TYPE_SINGLESIG from embit.networks import NETWORKS BTN_SEQUENCE = ( @@ -270,7 +270,7 @@ def test_encrypt_to_qrcode_cbc_ui(m5stickv, mocker): # QR view is mocked here, no press needed ) ctx = create_ctx(mocker, BTN_SEQUENCE) - ctx.wallet = Wallet(Key(CBC_WORDS, False, NETWORKS["main"])) + ctx.wallet = Wallet(Key(CBC_WORDS, TYPE_SINGLESIG, NETWORKS["main"])) ctx.printer = None storage_ui = EncryptMnemonic(ctx) mocker.patch( diff --git a/tests/pages/test_login.py b/tests/pages/test_login.py index f96aacc0a..a7c6aff46 100644 --- a/tests/pages/test_login.py +++ b/tests/pages/test_login.py @@ -234,12 +234,14 @@ def test_new_double_mnemonic_from_snapshot(m5stickv, mocker): from krux.pages.login import Login from krux.input import BUTTON_ENTER, BUTTON_PAGE from krux.wallet import is_double_mnemonic - from krux.display import DEFAULT_PADDING - from krux.themes import WHITE, BLACK + from embit.bip39 import mnemonic_from_bytes + + ORIGINAL_ENTROPY = b"\x01" * 32 # mocks a result of a hashed image mocker.patch( - "krux.pages.capture_entropy.CameraEntropy.capture", return_value=b"\x01" * 32 + "krux.pages.capture_entropy.CameraEntropy.capture", + return_value=ORIGINAL_ENTROPY, ) BTN_SEQUENCE = ( @@ -258,7 +260,7 @@ def test_new_double_mnemonic_from_snapshot(m5stickv, mocker): # Load Wallet [BUTTON_ENTER] ) - MNEMONIC = "absurd amount doctor acoustic avoid letter advice cage absurd amount doctor adjust absurd amount doctor acoustic avoid letter advice cage absurd amount doll fancy" + MNEMONIC = "absurd amount doctor acoustic avoid letter advice cage absurd amount doctor adjust avoid letter advice cage absurd amount doctor acoustic avoid letter affair embark" ctx = create_ctx(mocker, BTN_SEQUENCE) login = Login(ctx) login.new_key_from_snapshot() @@ -269,6 +271,15 @@ def test_new_double_mnemonic_from_snapshot(m5stickv, mocker): ctx.display.draw_hcentered_text.assert_has_calls( [mocker.call("BIP39 Mnemonic*", 5)] ) + original_mnemonic_words = mnemonic_from_bytes(ORIGINAL_ENTROPY).split(" ") + converted_mnemonic_words = ctx.wallet.key.mnemonic.split(" ") + + # Assert only words of indexes 11, 22 and 23 are different + assert original_mnemonic_words[:11] == converted_mnemonic_words[:11] + assert original_mnemonic_words[11] != converted_mnemonic_words[11] + assert original_mnemonic_words[12:22] == converted_mnemonic_words[12:22] + assert original_mnemonic_words[22] != converted_mnemonic_words[22] + assert original_mnemonic_words[23] != converted_mnemonic_words[23] ########## load words from qrcode tests diff --git a/tests/pages/test_qr_capture.py b/tests/pages/test_qr_capture.py index 2ef317a11..8a756de9b 100644 --- a/tests/pages/test_qr_capture.py +++ b/tests/pages/test_qr_capture.py @@ -71,14 +71,27 @@ def test_capture_qr_code(mocker, multiple_devices, tdata): def test_camera_antiglare(mocker, m5stickv): from krux.pages.qr_capture import QRCodeCapture - from krux.camera import Camera, OV7740_ID, OV2640_ID, GC2145_ID + from krux.camera import ( + OV7740_ID, + OV2640_ID, + GC2145_ID, + GC0328_ID, + ANTI_GLARE_MODE, + ZOOMED_MODE, + QR_SCAN_MODE, + ) - antiglare_on = False + mode = QR_SCAN_MODE - def toggle_antiglare(): - nonlocal antiglare_on - antiglare_on = not antiglare_on - return antiglare_on + def toggle_camera_mode(): + nonlocal mode + if mode == QR_SCAN_MODE: + mode = ANTI_GLARE_MODE + elif mode == ANTI_GLARE_MODE: + mode = ZOOMED_MODE + elif mode == ZOOMED_MODE: + mode = QR_SCAN_MODE + return mode time_mocker = TimeMocker(1001) ctx = mock_context(mocker) @@ -89,29 +102,29 @@ def toggle_antiglare(): ) mocker.patch.object( ctx.camera, - "toggle_antiglare", - side_effect=toggle_antiglare, + "toggle_camera_mode", + side_effect=toggle_camera_mode, ) - cameras = [OV7740_ID, OV2640_ID, GC2145_ID] + cameras = [OV7740_ID, OV2640_ID, GC2145_ID, GC0328_ID] for cam_id in cameras: mocker.patch.object(ctx.camera.sensor, "get_id", lambda: cam_id) - PAGE_SEQ = [False, True, False, True, False] - PAGE_PREV_SEQ = [False, False, False, False, True] + PAGE_SEQ = [False, True, False, True, False, True, False] + PAGE_PREV_SEQ = [False, False, False, False, False, False, True] mocker.patch("time.ticks_ms", time_mocker.tick) ctx.input.page_event = mocker.MagicMock(side_effect=PAGE_SEQ) ctx.input.page_prev_event = mocker.MagicMock(side_effect=PAGE_PREV_SEQ) - mocker.spy(ctx.camera, "toggle_antiglare") + mocker.spy(ctx.camera, "toggle_camera_mode") qr_capturer = QRCodeCapture(ctx) - qr_code, qr_format = qr_capturer.qr_capture_loop() + qr_code, _ = qr_capturer.qr_capture_loop() assert qr_code == None - ctx.camera.toggle_antiglare.assert_has_calls([mocker.call()] * 2) + ctx.camera.toggle_camera_mode.assert_has_calls([mocker.call()] * 2) + assert ctx.camera.toggle_camera_mode.call_count == 3 ctx.light.turn_on.call_count == 0 ctx.display.draw_centered_text.assert_has_calls( - [mocker.call("Anti-glare enabled")] - ) - ctx.display.draw_centered_text.assert_has_calls( - [mocker.call("Anti-glare disabled")] + [mocker.call("Anti-glare mode")] ) + ctx.display.draw_centered_text.assert_has_calls([mocker.call("Zoomed mode")]) + ctx.display.draw_centered_text.assert_has_calls([mocker.call("Standard mode")]) def test_light_control(mocker, multiple_devices): diff --git a/tests/pages/test_settings_page.py b/tests/pages/test_settings_page.py index 583123faf..09b57773a 100644 --- a/tests/pages/test_settings_page.py +++ b/tests/pages/test_settings_page.py @@ -172,12 +172,35 @@ def test_change_brightness(bkl_control_devices, mocker): *([BUTTON_PAGE_PREV] * 3), # Move to "Back" BUTTON_ENTER, # Confirm "Back" ] - ctx = create_ctx(mocker, BTN_SEQUENCE) + BTN_SEQUENCE_WONDER_MV = [ + *([BUTTON_PAGE] * 2), # Move to "Hardware" + BUTTON_ENTER, # Enter "Hardware" + BUTTON_PAGE, # Move to "Display" + BUTTON_ENTER, # Enter "Display" + BUTTON_ENTER, # Enter "Brightness" + BUTTON_PAGE, # Change "Brightness" + BUTTON_ENTER, # Enter "Brightness" + BUTTON_PAGE_PREV, # Move to "Back" + BUTTON_ENTER, # Confirm "Back" + *([BUTTON_PAGE_PREV] * 2), # Move to "Back" + BUTTON_ENTER, # Confirm "Back" + *([BUTTON_PAGE_PREV] * 3), # Move to "Back" + BUTTON_ENTER, # Confirm "Back" + ] + if board.config["type"] == "wonder_mv": + ctx = create_ctx(mocker, BTN_SEQUENCE_WONDER_MV) + else: + ctx = create_ctx(mocker, BTN_SEQUENCE) settings_page = SettingsPage(ctx) previous_brightness = int(Settings().hardware.display.brightness) settings_page.settings() - assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) + len_sequence = ( + len(BTN_SEQUENCE) + if board.config["type"] != "wonder_mv" + else len(BTN_SEQUENCE_WONDER_MV) + ) + assert ctx.input.wait_for_button.call_count == len_sequence assert Settings().hardware.display.brightness == str(previous_brightness + 1) diff --git a/tests/pages/test_tiny_seed.py b/tests/pages/test_tiny_seed.py index 0f95a1ba8..bc6ad16fa 100644 --- a/tests/pages/test_tiny_seed.py +++ b/tests/pages/test_tiny_seed.py @@ -39,7 +39,7 @@ def test_export_tiny_seed(m5stickv, mocker): from krux.pages.tiny_seed import TinySeed from krux.input import BUTTON_ENTER from krux.wallet import Wallet - from krux.key import Key + from krux.key import Key, TYPE_SINGLESIG from embit.networks import NETWORKS BTN_SEQUENCE = [ @@ -50,7 +50,7 @@ def test_export_tiny_seed(m5stickv, mocker): TEST_24_WORD_MNEMONIC = "brush badge sing still venue panther kitchen please help panel bundle excess sign couch stove increase human once effort candy goat top tiny major" # Amount of rectangles filled for this mnemonic + menus FILLED_RECTANGLES = 137 - SINGLESIG_24_WORD_KEY = Key(TEST_24_WORD_MNEMONIC, False, NETWORKS["main"]) + SINGLESIG_24_WORD_KEY = Key(TEST_24_WORD_MNEMONIC, TYPE_SINGLESIG, NETWORKS["main"]) ctx = create_ctx(mocker, BTN_SEQUENCE, Wallet(SINGLESIG_24_WORD_KEY), MockPrinter()) tiny_seed = TinySeed(ctx) tiny_seed.export() @@ -330,7 +330,7 @@ def test_tinyscanner_initializes_tinyseed_with_label(multiple_devices, mocker): tiny_scanner = TinyScanner(ctx, grid_type=grid_type) else: tiny_scanner = TinyScanner(ctx) - assert tiny_scanner.tiny_seed.label == expected_label + assert tiny_scanner.label == expected_label else: with pytest.raises(expected_exception): tiny_scanner = TinyScanner(ctx, grid_type=grid_type) diff --git a/tests/pages/test_wallet_settings.py b/tests/pages/test_wallet_settings.py index 52dce7de2..e91fbe7b9 100644 --- a/tests/pages/test_wallet_settings.py +++ b/tests/pages/test_wallet_settings.py @@ -92,26 +92,33 @@ def test_qr_passphrase_fail(m5stickv, mocker): qr_capturer.assert_called_once() -def test_change_multisig_changes(m5stickv, mocker, tdata): +def test_change_policy_types(m5stickv, mocker, tdata): from krux.pages.wallet_settings import WalletSettings from krux.wallet import Wallet - from krux.key import Key + from krux.key import Key, TYPE_SINGLESIG, TYPE_MULTISIG, TYPE_MINISCRIPT from krux.pages import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV - BTN_SEQUENCE_1 = [ - BUTTON_PAGE, # Go to "Single/Multisig" - BUTTON_ENTER, # Enter "Single/Multisig" + BTN_SEQUENCE = [ + BUTTON_PAGE, # Go to "Policy Type" + BUTTON_ENTER, # Enter "Policy Type" BUTTON_PAGE, # Change to Multisig BUTTON_ENTER, # Confirm Multisig BUTTON_PAGE_PREV, # Go to Back BUTTON_ENTER, # Leave ] - ctx = create_ctx(mocker, BTN_SEQUENCE_1, Wallet(tdata.SINGLESIG_12_WORD_KEY)) + ctx = create_ctx(mocker, BTN_SEQUENCE, Wallet(tdata.SINGLESIG_12_WORD_KEY)) + + # assert wallet starts as single-sig, native segwit + assert ctx.wallet.is_multisig() is False + assert ctx.wallet.is_miniscript() is False + assert ctx.wallet.key.policy_type == TYPE_SINGLESIG + assert ctx.wallet.key.script_type == "p2wpkh" + mnemonic = ctx.wallet.key.mnemonic wallet_settings = WalletSettings(ctx) - network, policy_type, script_type, account = wallet_settings.customize_wallet( - ctx.wallet.key + network, policy_type, script_type, account, derivation_path = ( + wallet_settings.customize_wallet(ctx.wallet.key) ) ctx.wallet = Wallet( Key( @@ -121,13 +128,52 @@ def test_change_multisig_changes(m5stickv, mocker, tdata): "", # passphrase account, script_type, + derivation_path, ) ) + # Assert wallet is now multisig and changed to p2wsh assert ctx.wallet.is_multisig() is True + assert ctx.wallet.is_miniscript() is False + assert ctx.wallet.key.policy_type == TYPE_MULTISIG + assert ctx.wallet.key.script_type == "p2wsh" + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) + + # Change to miniscript + BTN_SEQUENCE = [ + BUTTON_PAGE, # Go to "Policy Type" + BUTTON_ENTER, # Enter "Policy Type" + *([BUTTON_PAGE] * 2), # Change to Miniscript + BUTTON_ENTER, # Confirm Miniscript + BUTTON_PAGE_PREV, # Go to Back + BUTTON_ENTER, # Leave + ] + ctx = create_ctx(mocker, BTN_SEQUENCE, ctx.wallet) + wallet_settings = WalletSettings(ctx) + network, policy_type, script_type, account, derivation_path = ( + wallet_settings.customize_wallet(ctx.wallet.key) + ) + ctx.wallet = Wallet( + Key( + mnemonic, + policy_type, + network, + "", # passphrase + account, + script_type, + derivation_path, + ) + ) + + # Assert wallet is now miniscript and remained p2wsh + assert ctx.wallet.is_multisig() is False + assert ctx.wallet.is_miniscript() is True + assert ctx.wallet.key.policy_type == TYPE_MINISCRIPT + assert ctx.wallet.key.script_type == "p2wsh" + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) # Change back to singlesig - BTN_SEQUENCE_2 = [ + BTN_SEQUENCE = [ BUTTON_PAGE, # Go to "Single/Multisig" BUTTON_ENTER, # Enter "Single/Multisig" BUTTON_ENTER, # Confirm Single @@ -136,10 +182,45 @@ def test_change_multisig_changes(m5stickv, mocker, tdata): BUTTON_PAGE_PREV, # Go to Back BUTTON_ENTER, # Leave ] - ctx = create_ctx(mocker, BTN_SEQUENCE_2, ctx.wallet) + + ctx = create_ctx(mocker, BTN_SEQUENCE, ctx.wallet) + wallet_settings = WalletSettings(ctx) + network, policy_type, script_type, account, derivation_path = ( + wallet_settings.customize_wallet(ctx.wallet.key) + ) + ctx.wallet = Wallet( + Key( + mnemonic, + policy_type, + network, + "", # passphrase + account, + script_type, + derivation_path, + ) + ) + assert ctx.wallet.is_multisig() is False + assert ctx.wallet.is_miniscript() is False + assert ctx.wallet.key.policy_type == TYPE_SINGLESIG + assert ctx.wallet.key.script_type == "p2wpkh" + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) + + # Going from single-sig to miniscript forces to change to script type + BTN_SEQUENCE = [ + BUTTON_PAGE, # Go to "Policy Type" + BUTTON_ENTER, # Enter "Policy Type" + *([BUTTON_PAGE] * 2), # Change to Miniscript + BUTTON_ENTER, # Confirm Miniscript + BUTTON_PAGE, # Change from Native Segwit to Taproot + BUTTON_ENTER, # Confirm Taproot + BUTTON_PAGE_PREV, # Go to Back + BUTTON_ENTER, # Leave + ] + + ctx = create_ctx(mocker, BTN_SEQUENCE, ctx.wallet) wallet_settings = WalletSettings(ctx) - network, policy_type, script_type, account = wallet_settings.customize_wallet( - ctx.wallet.key + network, policy_type, script_type, account, derivation_path = ( + wallet_settings.customize_wallet(ctx.wallet.key) ) ctx.wallet = Wallet( Key( @@ -149,9 +230,14 @@ def test_change_multisig_changes(m5stickv, mocker, tdata): "", # passphrase account, script_type, + derivation_path, ) ) assert ctx.wallet.is_multisig() is False + assert ctx.wallet.is_miniscript() is True + assert ctx.wallet.key.policy_type == TYPE_MINISCRIPT + assert ctx.wallet.key.script_type == "p2tr" + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) def test_change_script_type(m5stickv, mocker, tdata): @@ -168,12 +254,11 @@ def test_change_script_type(m5stickv, mocker, tdata): BUTTON_PAGE_PREV, # Go to Back BUTTON_ENTER, # Leave ] - ctx = create_ctx(mocker, BTN_SEQUENCE_1, Wallet(tdata.SINGLESIG_12_WORD_KEY)) mnemonic = ctx.wallet.key.mnemonic wallet_settings = WalletSettings(ctx) - network, policy_type, script_type, account = wallet_settings.customize_wallet( - ctx.wallet.key + network, policy_type, script_type, account, derivation_path = ( + wallet_settings.customize_wallet(ctx.wallet.key) ) ctx.wallet = Wallet( Key( @@ -187,6 +272,7 @@ def test_change_script_type(m5stickv, mocker, tdata): ) assert ctx.wallet.key.script_type == "p2tr" + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE_1) # Change back to legacy BTN_SEQUENCE_2 = [ @@ -198,8 +284,8 @@ def test_change_script_type(m5stickv, mocker, tdata): ] ctx = create_ctx(mocker, BTN_SEQUENCE_2, ctx.wallet) wallet_settings = WalletSettings(ctx) - network, policy_type, script_type, account = wallet_settings.customize_wallet( - ctx.wallet.key + network, policy_type, script_type, account, derivation_path = ( + wallet_settings.customize_wallet(ctx.wallet.key) ) ctx.wallet = Wallet( Key( @@ -212,6 +298,7 @@ def test_change_script_type(m5stickv, mocker, tdata): ) ) assert ctx.wallet.key.script_type == "p2pkh" + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE_2) def test_change_account(m5stickv, mocker, tdata): @@ -230,12 +317,11 @@ def test_change_account(m5stickv, mocker, tdata): BUTTON_PAGE_PREV, # Go to Back BUTTON_ENTER, # Leave ] - ctx = create_ctx(mocker, BTN_SEQUENCE_1, Wallet(tdata.SINGLESIG_12_WORD_KEY)) mnemonic = ctx.wallet.key.mnemonic wallet_settings = WalletSettings(ctx) - network, policy_type, script_type, account = wallet_settings.customize_wallet( - ctx.wallet.key + network, policy_type, script_type, account, derivation_path = ( + wallet_settings.customize_wallet(ctx.wallet.key) ) ctx.wallet = Wallet( Key( @@ -249,6 +335,7 @@ def test_change_account(m5stickv, mocker, tdata): ) assert ctx.wallet.key.account_index == 2 + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE_1) # Change back to account 0 BTN_SEQUENCE_2 = [ @@ -265,8 +352,8 @@ def test_change_account(m5stickv, mocker, tdata): ] ctx = create_ctx(mocker, BTN_SEQUENCE_2, ctx.wallet) wallet_settings = WalletSettings(ctx) - network, policy_type, script_type, account = wallet_settings.customize_wallet( - ctx.wallet.key + network, policy_type, script_type, account, derivation_path = ( + wallet_settings.customize_wallet(ctx.wallet.key) ) ctx.wallet = Wallet( Key( @@ -279,6 +366,7 @@ def test_change_account(m5stickv, mocker, tdata): ) ) assert ctx.wallet.key.account_index == 0 + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE_2) def test_change_account_esc(m5stickv, mocker, tdata): @@ -300,8 +388,8 @@ def test_change_account_esc(m5stickv, mocker, tdata): ctx = create_ctx(mocker, BTN_SEQUENCE_1, Wallet(tdata.SINGLESIG_12_WORD_KEY)) mnemonic = ctx.wallet.key.mnemonic wallet_settings = WalletSettings(ctx) - network, policy_type, script_type, account = wallet_settings.customize_wallet( - ctx.wallet.key + network, policy_type, script_type, account, derivation_path = ( + wallet_settings.customize_wallet(ctx.wallet.key) ) ctx.wallet = Wallet( Key( @@ -314,6 +402,7 @@ def test_change_account_esc(m5stickv, mocker, tdata): ) ) assert ctx.wallet.key.account_index == 0 + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE_1) def test_account_out_of_range(m5stickv, mocker, tdata): @@ -337,8 +426,8 @@ def test_account_out_of_range(m5stickv, mocker, tdata): mnemonic = ctx.wallet.key.mnemonic wallet_settings = WalletSettings(ctx) wallet_settings.flash_error = mocker.MagicMock() - network, policy_type, script_type, account = wallet_settings.customize_wallet( - ctx.wallet.key + network, policy_type, script_type, account, derivation_path = ( + wallet_settings.customize_wallet(ctx.wallet.key) ) ctx.wallet = Wallet( Key( @@ -355,3 +444,101 @@ def test_account_out_of_range(m5stickv, mocker, tdata): "Value 22222222222 out of range: [0, 2147483647]" ) assert ctx.wallet.key.account_index == 0 + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE_1) + + +def test_change_derivation_path(amigo, mocker, tdata): + from krux.pages.wallet_settings import WalletSettings + from krux.wallet import Wallet + from krux.key import Key + from krux.pages import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV + + BTN_SEQUENCE = [ + BUTTON_PAGE, # Go to "Policy Type" + BUTTON_ENTER, # Enter "Policy Type" + *([BUTTON_PAGE] * 2), # Change to Miniscript + BUTTON_ENTER, # Confirm Miniscript + BUTTON_PAGE, # Change from Native Segwit to Taproot + BUTTON_ENTER, # Confirm Taproot + *([BUTTON_PAGE] * 3), # Go to "Derivation Path" + BUTTON_ENTER, # Enter "Derivation Path" + *([BUTTON_PAGE_PREV] * 3), # Move to "Del" + *([BUTTON_ENTER] * 2), # Del "2h" from the derivation path + *([BUTTON_PAGE] * 2), # Move to "Go" + BUTTON_ENTER, # Go + # Get invalid derivation path (m/48h/0h/0h/), we need to delete trailing "/" + *([BUTTON_PAGE_PREV] * 3), # Move to "Del" again + BUTTON_ENTER, # Del "/" + *([BUTTON_PAGE] * 2), # Move to "Go" + BUTTON_ENTER, # Go + BUTTON_PAGE_PREV, # Go to Back + BUTTON_ENTER, # Leave + ] + + ctx = create_ctx(mocker, BTN_SEQUENCE, Wallet(tdata.SINGLESIG_12_WORD_KEY)) + mnemonic = ctx.wallet.key.mnemonic + wallet_settings = WalletSettings(ctx) + network, policy_type, script_type, account, derivation_path = ( + wallet_settings.customize_wallet(ctx.wallet.key) + ) + ctx.wallet = Wallet( + Key( + mnemonic, + policy_type, + network, + "", # passphrase + account, + script_type, + derivation_path, + ) + ) + + assert ctx.wallet.key.derivation_str() == "m/48h/0h/0h" + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) + + +def test_change_derivation_path_not_hardened_node(amigo, mocker, tdata): + from krux.pages.wallet_settings import WalletSettings + from krux.wallet import Wallet + from krux.key import Key + from krux.pages import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV + + BTN_SEQUENCE = [ + BUTTON_PAGE, # Go to "Policy Type" + BUTTON_ENTER, # Enter "Policy Type" + *([BUTTON_PAGE] * 2), # Change to Miniscript + BUTTON_ENTER, # Confirm Miniscript + BUTTON_PAGE, # Change from Native Segwit to Taproot + BUTTON_ENTER, # Confirm Taproot + *([BUTTON_PAGE] * 3), # Go to "Derivation Path" + BUTTON_ENTER, # Enter "Derivation Path" + *([BUTTON_PAGE_PREV] * 3), # Move to "Del" + BUTTON_ENTER, # Del last "h" from the derivation path + *([BUTTON_PAGE] * 2), # Move to "Go" + BUTTON_ENTER, # Go + # Get not hardened warning/pompt + BUTTON_ENTER, # Accept warning + BUTTON_PAGE_PREV, # Go to Back + BUTTON_ENTER, # Leave + ] + + ctx = create_ctx(mocker, BTN_SEQUENCE, Wallet(tdata.SINGLESIG_12_WORD_KEY)) + mnemonic = ctx.wallet.key.mnemonic + wallet_settings = WalletSettings(ctx) + network, policy_type, script_type, account, derivation_path = ( + wallet_settings.customize_wallet(ctx.wallet.key) + ) + ctx.wallet = Wallet( + Key( + mnemonic, + policy_type, + network, + "", # passphrase + account, + script_type, + derivation_path, + ) + ) + + assert ctx.wallet.key.derivation_str() == "m/48h/0h/0h/2" + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) diff --git a/tests/test_bip39.py b/tests/test_bip39.py index fd804be52..f4e625def 100644 --- a/tests/test_bip39.py +++ b/tests/test_bip39.py @@ -9,7 +9,7 @@ def test_one_word_mnemonics(): for numwords in (12, 15, 18, 21, 24): for word in WORDLIST: mnemonic = (word + " ") * numwords - assert kruxbip39.mnemonic_is_valid(mnemonic) == bip39.mnemonic_is_valid( + assert kruxbip39.k_mnemonic_is_valid(mnemonic) == bip39.mnemonic_is_valid( mnemonic ) @@ -21,11 +21,11 @@ def test_edge_cases(): ALL_ONE_BYTES = b"\xff" * case assert ( - kruxbip39.mnemonic_to_bytes(bip39.mnemonic_from_bytes(ALL_ZERO_BYTES)) + kruxbip39.k_mnemonic_bytes(bip39.mnemonic_from_bytes(ALL_ZERO_BYTES)) == ALL_ZERO_BYTES ) assert ( - kruxbip39.mnemonic_to_bytes(bip39.mnemonic_from_bytes(ALL_ONE_BYTES)) + kruxbip39.k_mnemonic_bytes(bip39.mnemonic_from_bytes(ALL_ONE_BYTES)) == ALL_ONE_BYTES ) @@ -33,10 +33,10 @@ def test_edge_cases(): while int_val > 0: int_val = int_val // 2 b = int_val.to_bytes(case, "big") - assert kruxbip39.mnemonic_to_bytes(bip39.mnemonic_from_bytes(b)) == b + assert kruxbip39.k_mnemonic_bytes(bip39.mnemonic_from_bytes(b)) == b b = (max_val - int_val).to_bytes(case, "big") - assert kruxbip39.mnemonic_to_bytes(bip39.mnemonic_from_bytes(b)) == b + assert kruxbip39.k_mnemonic_bytes(bip39.mnemonic_from_bytes(b)) == b def test_random_cases(): @@ -44,7 +44,7 @@ def test_random_cases(): for size in (16, 20, 24, 28, 32): token_bytes = secrets.token_bytes(size) assert ( - kruxbip39.mnemonic_to_bytes(bip39.mnemonic_from_bytes(token_bytes)) + kruxbip39.k_mnemonic_bytes(bip39.mnemonic_from_bytes(token_bytes)) == token_bytes ) @@ -55,7 +55,7 @@ def test_random_cases_custom_wordlist(): for size in (16, 20, 24, 28, 32): token_bytes = secrets.token_bytes(size) assert ( - kruxbip39.mnemonic_to_bytes( + kruxbip39.k_mnemonic_bytes( bip39.mnemonic_from_bytes(token_bytes), wordlist=wordlist ) == token_bytes @@ -72,7 +72,7 @@ def test_invalid_words(): ] for case in cases: with pytest.raises(ValueError, match=" is not in the dictionary"): - kruxbip39.mnemonic_to_bytes(case) + kruxbip39.k_mnemonic_bytes(case) def test_invalid_mnemonic_length(): @@ -83,4 +83,4 @@ def test_invalid_mnemonic_length(): ] for case in cases: with pytest.raises(ValueError, match="Invalid recovery phrase"): - kruxbip39.mnemonic_to_bytes(case) + kruxbip39.k_mnemonic_bytes(case) diff --git a/tests/test_camera.py b/tests/test_camera.py index fffa4d258..aceb4a2c9 100644 --- a/tests/test_camera.py +++ b/tests/test_camera.py @@ -118,6 +118,7 @@ def test_toggle_antiglare(mocker, m5stickv): GC2145_ID, QR_SCAN_MODE, ANTI_GLARE_MODE, + ZOOMED_MODE, ) SENSORS_LIST = [OV7740_ID, OV2640_ID, GC0328_ID, GC2145_ID] @@ -129,7 +130,9 @@ def test_toggle_antiglare(mocker, m5stickv): c.initialize_sensor() if c.has_antiglare(): assert c.mode == QR_SCAN_MODE - c.toggle_antiglare() + c.toggle_camera_mode() assert c.mode == ANTI_GLARE_MODE - c.toggle_antiglare() + c.toggle_camera_mode() + assert c.mode == ZOOMED_MODE + c.toggle_camera_mode() assert c.mode == QR_SCAN_MODE diff --git a/tests/test_key.py b/tests/test_key.py index 3a3249729..402709855 100644 --- a/tests/test_key.py +++ b/tests/test_key.py @@ -92,7 +92,7 @@ def test_init(mocker, m5stickv, tdata): cases = [ ( - [tdata.TEST_12_WORD_MNEMONIC, False], + [tdata.TEST_12_WORD_MNEMONIC, TYPE_SINGLESIG], { "mnemonic": tdata.TEST_12_WORD_MNEMONIC, "policy_type": TYPE_SINGLESIG, @@ -103,7 +103,7 @@ def test_init(mocker, m5stickv, tdata): }, ), ( - [tdata.TEST_12_WORD_MNEMONIC, True], + [tdata.TEST_12_WORD_MNEMONIC, TYPE_MULTISIG], { "mnemonic": tdata.TEST_12_WORD_MNEMONIC, "policy_type": TYPE_MULTISIG, @@ -114,7 +114,7 @@ def test_init(mocker, m5stickv, tdata): }, ), ( - [tdata.TEST_12_WORD_MNEMONIC, False, NETWORKS["main"]], + [tdata.TEST_12_WORD_MNEMONIC, TYPE_SINGLESIG, NETWORKS["main"]], { "mnemonic": tdata.TEST_12_WORD_MNEMONIC, "policy_type": TYPE_SINGLESIG, @@ -125,7 +125,7 @@ def test_init(mocker, m5stickv, tdata): }, ), ( - [tdata.TEST_12_WORD_MNEMONIC, True, NETWORKS["main"]], + [tdata.TEST_12_WORD_MNEMONIC, TYPE_MULTISIG, NETWORKS["main"]], { "mnemonic": tdata.TEST_12_WORD_MNEMONIC, "policy_type": TYPE_MULTISIG, @@ -136,7 +136,7 @@ def test_init(mocker, m5stickv, tdata): }, ), ( - [tdata.TEST_24_WORD_MNEMONIC, False], + [tdata.TEST_24_WORD_MNEMONIC, TYPE_SINGLESIG], { "mnemonic": tdata.TEST_24_WORD_MNEMONIC, "policy_type": TYPE_SINGLESIG, @@ -147,7 +147,7 @@ def test_init(mocker, m5stickv, tdata): }, ), ( - [tdata.TEST_24_WORD_MNEMONIC, True], + [tdata.TEST_24_WORD_MNEMONIC, TYPE_MULTISIG], { "mnemonic": tdata.TEST_24_WORD_MNEMONIC, "policy_type": TYPE_MULTISIG, @@ -158,7 +158,7 @@ def test_init(mocker, m5stickv, tdata): }, ), ( - [tdata.TEST_24_WORD_MNEMONIC, False, NETWORKS["main"]], + [tdata.TEST_24_WORD_MNEMONIC, TYPE_SINGLESIG, NETWORKS["main"]], { "mnemonic": tdata.TEST_24_WORD_MNEMONIC, "policy_type": TYPE_SINGLESIG, @@ -169,7 +169,7 @@ def test_init(mocker, m5stickv, tdata): }, ), ( - [tdata.TEST_24_WORD_MNEMONIC, True, NETWORKS["main"]], + [tdata.TEST_24_WORD_MNEMONIC, TYPE_MULTISIG, NETWORKS["main"]], { "mnemonic": tdata.TEST_24_WORD_MNEMONIC, "policy_type": TYPE_MULTISIG, @@ -195,9 +195,9 @@ def test_init(mocker, m5stickv, tdata): def test_xpub(mocker, m5stickv, tdata): mock_modules(mocker) - from krux.key import Key + from krux.key import Key, TYPE_SINGLESIG - key = Key(tdata.TEST_MNEMONIC, False) + key = Key(tdata.TEST_MNEMONIC, TYPE_SINGLESIG) mocker.spy(key.account, "to_base58") assert key.xpub() == tdata.TEST_XPUB @@ -207,9 +207,9 @@ def test_xpub(mocker, m5stickv, tdata): def test_key_expression(mocker, m5stickv, tdata): mock_modules(mocker) import krux - from krux.key import Key + from krux.key import Key, TYPE_SINGLESIG - key = Key(tdata.TEST_MNEMONIC, False) + key = Key(tdata.TEST_MNEMONIC, TYPE_SINGLESIG) mocker.spy(key.account, "to_base58") cases = [ @@ -227,9 +227,9 @@ def test_key_expression(mocker, m5stickv, tdata): def test_sign(mocker, m5stickv, tdata): mock_modules(mocker) from embit import ec - from krux.key import Key + from krux.key import Key, TYPE_SINGLESIG - key = Key(tdata.TEST_MNEMONIC, False) + key = Key(tdata.TEST_MNEMONIC, TYPE_SINGLESIG) signature = key.sign(tdata.TEST_HASH) assert isinstance(signature, ec.Signature) @@ -238,9 +238,9 @@ def test_sign(mocker, m5stickv, tdata): def test_sign_fails_with_invalid_hash(mocker, m5stickv, tdata): mock_modules(mocker) - from krux.key import Key + from krux.key import Key, TYPE_SINGLESIG - key = Key(tdata.TEST_MNEMONIC, False) + key = Key(tdata.TEST_MNEMONIC, TYPE_SINGLESIG) with pytest.raises(ValueError): key.sign(tdata.TEST_INVALID_HASH) diff --git a/tests/test_psbt.py b/tests/test_psbt.py index e26bb36d8..f84d99731 100644 --- a/tests/test_psbt.py +++ b/tests/test_psbt.py @@ -1,6 +1,11 @@ import pytest from .shared_mocks import MockFile, mock_open +WSH_MULTISIG = "wsh(sortedmulti(2,[02e8bff2/48h/1h/0h/2h]tpubDESmyzX6RMHRHvzxgLVXymd193CSYYT6C9M9wa4XK649tbAy2WeEvkPwBgoC7i76MypQxpuruUazQjqibbCogTZuENJX6YiuZ5Fy8sf7GNi/<0;1>/*,[8cb36b38/48h/1h/0h/2h]tpubDESTVwqbbaSoN2mPq7tcWkPBpRBkaEADrUzUhRTVnNef6oVn6w2PHL4zoUjUAJSPLJQRBetkgX4sDRcoaCyFHxqHGyWyaiV8REKDkh7zQac/<0;1>/*,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/<0;1>/*))#jpxgnm0a" +WSH_MINISCRIPT = "wsh(or_d(pk([73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/<0;1>/*),and_v(v:pkh([02e8bff2/48h/1h/0h/2h]tpubDESmyzX6RMHRHvzxgLVXymd193CSYYT6C9M9wa4XK649tbAy2WeEvkPwBgoC7i76MypQxpuruUazQjqibbCogTZuENJX6YiuZ5Fy8sf7GNi/<0;1>/*),older(65535))))#466dtswe" +TR_MINISCRIPT = "tr([73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/<0;1>/*,and_v(v:pk([02e8bff2/48h/1h/0h/2h]tpubDESmyzX6RMHRHvzxgLVXymd193CSYYT6C9M9wa4XK649tbAy2WeEvkPwBgoC7i76MypQxpuruUazQjqibbCogTZuENJX6YiuZ5Fy8sf7GNi/<0;1>/*),older(6)))#rfuhsd9c" +TR_EXP_MULTI_MINISCRIPT = "tr(tpubD6NzVbkrYhZ4YYQjuKXFp85eRGCk4oA94MXbbHJW6zNAibMfkwZh7JQNVHEhpcQYaRCUs3b5PhvKPKESGoAJptiLvF8Rm1jhoFsryyFuR1P/<0;1>/*,{and_v(v:multi_a(2,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/<2;3>/*,[02e8bff2/48h/1h/0h/2h]tpubDESmyzX6RMHRHvzxgLVXymd193CSYYT6C9M9wa4XK649tbAy2WeEvkPwBgoC7i76MypQxpuruUazQjqibbCogTZuENJX6YiuZ5Fy8sf7GNi/<2;3>/*,[8cb36b38/48h/1h/0h/2h]tpubDESTVwqbbaSoN2mPq7tcWkPBpRBkaEADrUzUhRTVnNef6oVn6w2PHL4zoUjUAJSPLJQRBetkgX4sDRcoaCyFHxqHGyWyaiV8REKDkh7zQac/<0;1>/*),older(6)),multi_a(2,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/<0;1>/*,[02e8bff2/48h/1h/0h/2h]tpubDESmyzX6RMHRHvzxgLVXymd193CSYYT6C9M9wa4XK649tbAy2WeEvkPwBgoC7i76MypQxpuruUazQjqibbCogTZuENJX6YiuZ5Fy8sf7GNi/<0;1>/*)})#6ga85u82" + @pytest.fixture def tdata(mocker): @@ -10,6 +15,7 @@ def tdata(mocker): from krux.bbqr import encode_bbqr TEST_MNEMONIC = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + TEST_MNEMONIC_BIP85_I0 = "prosper short ramp prepare exchange stove life snack client enough purpose fold" # Legacy Singlesig P2PKH_PSBT = b'psbt\xff\x01\x00\xa4\x02\x00\x00\x00\x03\xae\xe25\xaf(\x9a\xc9\xee\xc23\xca"\x15\xbf?\xf4\xc1\xcaxAP\xd6\x0f[\x94kA\x87\x8b\x15\x04,\x00\x00\x00\x00\x00\xfd\xff\xff\xffT\xc9\x91i\xc4ZIg Z!\xd6)\xbf+z\x161\xc4uoS \xf0\x9d\x96\xcf#\xdc\xdbc\xa0\x00\x00\x00\x00\x00\xfd\xff\xff\xff\x04\x1eVt\x8d\x80H\x1f\x89k\x07T(\xca\xaf\x91\x1e"\x1a2\xef\xa5_\\s\xf9\x8b\xc2J\xa0\xc8\x11\x00\x00\x00\x00\x00\xfd\xff\xff\xff\x01\xe8\x03\x00\x00\x00\x00\x00\x00\x16\x00\x14\xae\xcd\x1e\xdc>\xffe\xaa \x9d\x02\x15\xe7=p\x90]\xc1hlZ\x0f+\x00O\x01\x045\x87\xcf\x03\x06\xb07\xf6\x80\x00\x00\x00k"\xc5\x12;\xa1\n\xde\xafK\xfc\xbbE\xd1\xa0-\x82\x8f%\xbf\x86F\x95z\x98\xd0b\x87\xc4\xe2\xb8P\x02\x8bB\xcdGv7l\x82y\x1bIAU\x15\x1fV\xc2\xd7\xb4q\xe0\xc7\xa5&\xa7\xce`\xdd\x87.8g\x10s\xc5\xda\n,\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x00\x01\x00~\x02\x00\x00\x00\x02\x9a\x9b\xe1\xca)\x10\\\x97t<\x0f\xd1\xeey\xc0\xe6\r"\x8aa\xc8\xec\xbft\xf9\xe7\xcf\xfa\x01\x19\x0c{\x02\x00\x00\x00\x00\xfd\xff\xff\xff\x9a\x9b\xe1\xca)\x10\\\x97t<\x0f\xd1\xeey\xc0\xe6\r"\x8aa\xc8\xec\xbft\xf9\xe7\xcf\xfa\x01\x19\x0c{\x01\x00\x00\x00\x00\xfd\xff\xff\xff\x01@\x07\x00\x00\x00\x00\x00\x00\x19v\xa9\x14:-AE\xa4\xf0\x98R;>\x81\'\xf1\xda\x87\xcf\xc5[\x8ey\x88\xacZ\x0f+\x00\x01\x03\x04\x01\x00\x00\x00"\x06\x02\xa7E\x13\x95sSi\xf2\xec\xdf\xc8)\xc0\xf7t\xe8\x8e\xf10=\xfe[/\x04\xdb\xaa\xb3\nS]\xfd\xd6\x18s\xc5\xda\n,\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00U\x02\x00\x00\x00\x01\x873 i\xd7\x11\xa7\xcd*`\xd5\x84\x1c8\n,U\xe8\xa2\n\xdd\xdaV\xa2\x9c\x00\x84e\xef\xf6[\xa2\x01\x00\x00\x00\x00\xff\xff\xff\xff\x01\x00\x00\x00\x00\x00\x00\x00\x00\x19v\xa9\x14:-AE\xa4\xf0\x98R;>\x81\'\xf1\xda\x87\xcf\xc5[\x8ey\x88\xac\x00\x00\x00\x00\x01\x03\x04\x01\x00\x00\x00"\x06\x02\xa7E\x13\x95sSi\xf2\xec\xdf\xc8)\xc0\xf7t\xe8\x8e\xf10=\xfe[/\x04\xdb\xaa\xb3\nS]\xfd\xd6\x18s\xc5\xda\n,\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00U\x02\x00\x00\x00\x01\x05j\xc0\xa7"\x1f\xe5\x82x\xc9\xa7h\xd0\x1a!\xe6\xb6GJ\x01\x9a\xf6\xe7?\x08\xc8R\xe6\x86|\xad\xa5\x00\x00\x00\x00\x00\xff\xff\xff\xff\x01\x00\x00\x00\x00\x00\x00\x00\x00\x19v\xa9\x14:-AE\xa4\xf0\x98R;>\x81\'\xf1\xda\x87\xcf\xc5[\x8ey\x88\xac\x00\x00\x00\x00\x01\x03\x04\x01\x00\x00\x00"\x06\x02\xa7E\x13\x95sSi\xf2\xec\xdf\xc8)\xc0\xf7t\xe8\x8e\xf10=\xfe[/\x04\xdb\xaa\xb3\nS]\xfd\xd6\x18s\xc5\xda\n,\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' @@ -23,6 +29,8 @@ def tdata(mocker): SIGNED_P2PKH_PSBT_B58 = "6XT9mnsYS6MLhfs4uWirQJGRNWXt7Qrs2f94yDPQ3y9c7ZmRhzpuGCvKnZLfFgDepYGRtqF4sDyfwykXMjppJZy4zxgkatKvZaoB8M9cp4jEYeWjTeqxTNXKVjDCHVRhswofDYqtEowhUsg76HhHzL1sZ4RitQJ1oQoqojkphmX1jjHL4AzJzkgoSCpMLvKh2TAVmqfhuv4Vid9gLiVBu5X1jHVdwUqQ8qnmcoP6ypGaVVoYWWaDhf9vrWi2tUhP4fNPWTVfj4txdC4a6yui28fdPW8UWu9f4h85tekokRpUWCHzJUhuXpahtiTbFsLhaw1PhpAbN5M3PEbs53poHiNFbG6rjLveyiDnyeW6vTGEecGcAgktvVzv2f5cBc9VwcfLUDqYN4bwdUpbzeuuHC3JeNwx2Q1pEspqZDhm94R3a6jw3g35CzaHZ5cMDfMKp7LFsbh4AgKD2HhvWMLk2EpSRc31Avet6j8ARCUXHRNRE9eay3NKy9AhE2zdQnEg42PVK5Sjs7PpT6QcvifdfaNAZ852eBQSTUk9X3oLXG43RaA2eNXmu92ydAfdWr9SyQ5WHKDhZq1xzcGnB4eaz1qJ1TpbWhkTnsEqsw3h6aqL2MWEpKjopKLn88gFSKN4nBUTz2ZvXdPqyZMuFVqnZ79LY1JaM77pyrdyc8G68VFhyMWADd4ABEeLdFq2a8yrTGkdq1V1ef3KWLJCKFV3WHEXcwQkKyS2t8XFgpKMJ1gGu5tQ8BYEzboUjP6EJs5dVZmcjNgwyh1MYpSHorm4AUyfGAoSiNjwWjjfM2F8z4JEWxLdfk4ufoG25gbrzbKrhQJjFNr3TeqZJBfVpEgsn8QjmaV7RBzrk7Whx3K51hx6HZe9JkUG9z5abmBA7FZQMACx8FRLRaZBxc7GiEf3hiT4Sv7fyUmTagViamSXbzvpHGqyaVnN7PD62yfxSzkFWwjVfPG78ufotrQ7fxJ4gfAcz7DgpH8xBe1GhPLuDZP1kDHi3JcoJ6CF8w9w3q4pkZVCtwB7AqTwimMXztLhdULWGDjBc3PVXmzfziCbc2CzJHzfzbxVqSAi8UA5WJzNXaiZhFTXV" SIGNED_P2PKH_PSBT_B64 = "cHNidP8BAKQCAAAAA67iNa8omsnuwjPKIhW/P/TBynhBUNYPW5RrQYeLFQQsAAAAAAD9////VMmRacRaSWcgWiHWKb8rehYxxHVvUyDwnZbPI9zbY6AAAAAAAP3///8EHlZ0jYBIH4lrB1Qoyq+RHiIaMu+lX1xz+YvCSqDIEQAAAAAA/f///wHoAwAAAAAAABYAFK7NHtw+/2WqIJ0CFec9cJBdwWhsWg8rAAABAH4CAAAAApqb4copEFyXdDwP0e55wOYNIophyOy/dPnnz/oBGQx7AgAAAAD9////mpvhyikQXJd0PA/R7nnA5g0iimHI7L90+efP+gEZDHsBAAAAAP3///8BQAcAAAAAAAAZdqkUOi1BRaTwmFI7PoEn8dqHz8VbjnmIrFoPKwAiAgKnRROVc1Np8uzfyCnA93TojvEwPf5bLwTbqrMKU1391kcwRAIgFB6kWP8+bJ3YRGfarxu6OUKUQGVivLNc5KVPOk7InOsCIC+jJbZRd44c2soNoLEsLFpDTKMyMHXVx2dUJyuCgADlAQABAFUCAAAAAYczIGnXEafNKmDVhBw4CixV6KIK3dpWopwAhGXv9luiAQAAAAD/////AQAAAAAAAAAAGXapFDotQUWk8JhSOz6BJ/Hah8/FW455iKwAAAAAIgICp0UTlXNTafLs38gpwPd06I7xMD3+Wy8E26qzClNd/dZHMEQCIGH0YKVXvcUrpK5q/fUsFphs0tNYGkLu7RPu1Q3wSsmNAiBSqP6iXeZZIPNRsdVeR+pFcHLPLC2Q5SwY1JKE5XmMAgEAAQBVAgAAAAEFasCnIh/lgnjJp2jQGiHmtkdKAZr25z8IyFLmhnytpQAAAAAA/////wEAAAAAAAAAABl2qRQ6LUFFpPCYUjs+gSfx2ofPxVuOeYisAAAAACICAqdFE5VzU2ny7N/IKcD3dOiO8TA9/lsvBNuqswpTXf3WRzBEAiAv+XdRSoVK1sCnZlk5Uxplf+1xwMCYiYCafuLwNJButQIgKJt4meDvwgsBDeFKk/xdUe0mUc59tZFhwDbdGq/UuK8BAAA=" SIGNED_P2PKH_PSBT_UR_PSBT = UR("crypto-psbt", PSBT(SIGNED_P2PKH_PSBT).to_cbor()) + SIGNED_P2PKH_PSBT_SD = b'psbt\xff\x01\x00\xa4\x02\x00\x00\x00\x03\xae\xe25\xaf(\x9a\xc9\xee\xc23\xca"\x15\xbf?\xf4\xc1\xcaxAP\xd6\x0f[\x94kA\x87\x8b\x15\x04,\x00\x00\x00\x00\x00\xfd\xff\xff\xffT\xc9\x91i\xc4ZIg Z!\xd6)\xbf+z\x161\xc4uoS \xf0\x9d\x96\xcf#\xdc\xdbc\xa0\x00\x00\x00\x00\x00\xfd\xff\xff\xff\x04\x1eVt\x8d\x80H\x1f\x89k\x07T(\xca\xaf\x91\x1e"\x1a2\xef\xa5_\\s\xf9\x8b\xc2J\xa0\xc8\x11\x00\x00\x00\x00\x00\xfd\xff\xff\xff\x01\xe8\x03\x00\x00\x00\x00\x00\x00\x16\x00\x14\xae\xcd\x1e\xdc>\xffe\xaa \x9d\x02\x15\xe7=p\x90]\xc1hlZ\x0f+\x00O\x01\x045\x87\xcf\x03\x06\xb07\xf6\x80\x00\x00\x00k"\xc5\x12;\xa1\n\xde\xafK\xfc\xbbE\xd1\xa0-\x82\x8f%\xbf\x86F\x95z\x98\xd0b\x87\xc4\xe2\xb8P\x02\x8bB\xcdGv7l\x82y\x1bIAU\x15\x1fV\xc2\xd7\xb4q\xe0\xc7\xa5&\xa7\xce`\xdd\x87.8g\x10s\xc5\xda\n,\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x00\x01\x00~\x02\x00\x00\x00\x02\x9a\x9b\xe1\xca)\x10\\\x97t<\x0f\xd1\xeey\xc0\xe6\r"\x8aa\xc8\xec\xbft\xf9\xe7\xcf\xfa\x01\x19\x0c{\x02\x00\x00\x00\x00\xfd\xff\xff\xff\x9a\x9b\xe1\xca)\x10\\\x97t<\x0f\xd1\xeey\xc0\xe6\r"\x8aa\xc8\xec\xbft\xf9\xe7\xcf\xfa\x01\x19\x0c{\x01\x00\x00\x00\x00\xfd\xff\xff\xff\x01@\x07\x00\x00\x00\x00\x00\x00\x19v\xa9\x14:-AE\xa4\xf0\x98R;>\x81\'\xf1\xda\x87\xcf\xc5[\x8ey\x88\xacZ\x0f+\x00"\x02\x02\xa7E\x13\x95sSi\xf2\xec\xdf\xc8)\xc0\xf7t\xe8\x8e\xf10=\xfe[/\x04\xdb\xaa\xb3\nS]\xfd\xd6G0D\x02 \x14\x1e\xa4X\xff>l\x9d\xd8Dg\xda\xaf\x1b\xba9B\x94@eb\xbc\xb3\\\xe4\xa5O:N\xc8\x9c\xeb\x02 /\xa3%\xb6Qw\x8e\x1c\xda\xca\r\xa0\xb1,,ZCL\xa320u\xd5\xc7gT\'+\x82\x80\x00\xe5\x01\x01\x03\x04\x01\x00\x00\x00"\x06\x02\xa7E\x13\x95sSi\xf2\xec\xdf\xc8)\xc0\xf7t\xe8\x8e\xf10=\xfe[/\x04\xdb\xaa\xb3\nS]\xfd\xd6\x18s\xc5\xda\n,\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00U\x02\x00\x00\x00\x01\x873 i\xd7\x11\xa7\xcd*`\xd5\x84\x1c8\n,U\xe8\xa2\n\xdd\xdaV\xa2\x9c\x00\x84e\xef\xf6[\xa2\x01\x00\x00\x00\x00\xff\xff\xff\xff\x01\x00\x00\x00\x00\x00\x00\x00\x00\x19v\xa9\x14:-AE\xa4\xf0\x98R;>\x81\'\xf1\xda\x87\xcf\xc5[\x8ey\x88\xac\x00\x00\x00\x00"\x02\x02\xa7E\x13\x95sSi\xf2\xec\xdf\xc8)\xc0\xf7t\xe8\x8e\xf10=\xfe[/\x04\xdb\xaa\xb3\nS]\xfd\xd6G0D\x02 a\xf4`\xa5W\xbd\xc5+\xa4\xaej\xfd\xf5,\x16\x98l\xd2\xd3X\x1aB\xee\xed\x13\xee\xd5\r\xf0J\xc9\x8d\x02 R\xa8\xfe\xa2]\xe6Y \xf3Q\xb1\xd5^G\xeaEpr\xcf,-\x90\xe5,\x18\xd4\x92\x84\xe5y\x8c\x02\x01\x01\x03\x04\x01\x00\x00\x00"\x06\x02\xa7E\x13\x95sSi\xf2\xec\xdf\xc8)\xc0\xf7t\xe8\x8e\xf10=\xfe[/\x04\xdb\xaa\xb3\nS]\xfd\xd6\x18s\xc5\xda\n,\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00U\x02\x00\x00\x00\x01\x05j\xc0\xa7"\x1f\xe5\x82x\xc9\xa7h\xd0\x1a!\xe6\xb6GJ\x01\x9a\xf6\xe7?\x08\xc8R\xe6\x86|\xad\xa5\x00\x00\x00\x00\x00\xff\xff\xff\xff\x01\x00\x00\x00\x00\x00\x00\x00\x00\x19v\xa9\x14:-AE\xa4\xf0\x98R;>\x81\'\xf1\xda\x87\xcf\xc5[\x8ey\x88\xac\x00\x00\x00\x00"\x02\x02\xa7E\x13\x95sSi\xf2\xec\xdf\xc8)\xc0\xf7t\xe8\x8e\xf10=\xfe[/\x04\xdb\xaa\xb3\nS]\xfd\xd6G0D\x02 /\xf9wQJ\x85J\xd6\xc0\xa7fY9S\x1ae\x7f\xedq\xc0\xc0\x98\x89\x80\x9a~\xe2\xf04\x90n\xb5\x02 (\x9bx\x99\xe0\xef\xc2\x0b\x01\r\xe1J\x93\xfc]Q\xed&Q\xce}\xb5\x91a\xc06\xdd\x1a\xaf\xd4\xb8\xaf\x01\x01\x03\x04\x01\x00\x00\x00"\x06\x02\xa7E\x13\x95sSi\xf2\xec\xdf\xc8)\xc0\xf7t\xe8\x8e\xf10=\xfe[/\x04\xdb\xaa\xb3\nS]\xfd\xd6\x18s\xc5\xda\n,\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + SIGNED_P2PKH_PSBT_B64_SD = "cHNidP8BAKQCAAAAA67iNa8omsnuwjPKIhW/P/TBynhBUNYPW5RrQYeLFQQsAAAAAAD9////VMmRacRaSWcgWiHWKb8rehYxxHVvUyDwnZbPI9zbY6AAAAAAAP3///8EHlZ0jYBIH4lrB1Qoyq+RHiIaMu+lX1xz+YvCSqDIEQAAAAAA/f///wHoAwAAAAAAABYAFK7NHtw+/2WqIJ0CFec9cJBdwWhsWg8rAE8BBDWHzwMGsDf2gAAAAGsixRI7oQrer0v8u0XRoC2CjyW/hkaVepjQYofE4rhQAotCzUd2N2yCeRtJQVUVH1bC17Rx4MelJqfOYN2HLjhnEHPF2gosAACAAQAAgAAAAIAAAQB+AgAAAAKam+HKKRBcl3Q8D9HuecDmDSKKYcjsv3T558/6ARkMewIAAAAA/f///5qb4copEFyXdDwP0e55wOYNIophyOy/dPnnz/oBGQx7AQAAAAD9////AUAHAAAAAAAAGXapFDotQUWk8JhSOz6BJ/Hah8/FW455iKxaDysAIgICp0UTlXNTafLs38gpwPd06I7xMD3+Wy8E26qzClNd/dZHMEQCIBQepFj/Pmyd2ERn2q8bujlClEBlYryzXOSlTzpOyJzrAiAvoyW2UXeOHNrKDaCxLCxaQ0yjMjB11cdnVCcrgoAA5QEBAwQBAAAAIgYCp0UTlXNTafLs38gpwPd06I7xMD3+Wy8E26qzClNd/dYYc8XaCiwAAIABAACAAAAAgAAAAAAAAAAAAAEAVQIAAAABhzMgadcRp80qYNWEHDgKLFXoogrd2lainACEZe/2W6IBAAAAAP////8BAAAAAAAAAAAZdqkUOi1BRaTwmFI7PoEn8dqHz8VbjnmIrAAAAAAiAgKnRROVc1Np8uzfyCnA93TojvEwPf5bLwTbqrMKU1391kcwRAIgYfRgpVe9xSukrmr99SwWmGzS01gaQu7tE+7VDfBKyY0CIFKo/qJd5lkg81Gx1V5H6kVwcs8sLZDlLBjUkoTleYwCAQEDBAEAAAAiBgKnRROVc1Np8uzfyCnA93TojvEwPf5bLwTbqrMKU1391hhzxdoKLAAAgAEAAIAAAACAAAAAAAAAAAAAAQBVAgAAAAEFasCnIh/lgnjJp2jQGiHmtkdKAZr25z8IyFLmhnytpQAAAAAA/////wEAAAAAAAAAABl2qRQ6LUFFpPCYUjs+gSfx2ofPxVuOeYisAAAAACICAqdFE5VzU2ny7N/IKcD3dOiO8TA9/lsvBNuqswpTXf3WRzBEAiAv+XdRSoVK1sCnZlk5Uxplf+1xwMCYiYCafuLwNJButQIgKJt4meDvwgsBDeFKk/xdUe0mUc59tZFhwDbdGq/UuK8BAQMEAQAAACIGAqdFE5VzU2ny7N/IKcD3dOiO8TA9/lsvBNuqswpTXf3WGHPF2gosAACAAQAAgAAAAIAAAAAAAAAAAAAA" # Native Segwit Singlesig P2WPKH_PSBT = b'psbt\xff\x01\x00q\x02\x00\x00\x00\x01\xcfe\xff;L\xd4\x7f\x12\x1f\xa7\xc9\x82(F\x18\xdb\x801G\xb0V\xd3\x93\x94\xd4\xecB\x0e\xfd\xfck\xa1\x02 l\xbd\xd8\x8a\xc5\x18l?.\xfd$%1\xedy\x17uvQ\xac&#t\xf3\xd3\x1d\x85\xd6\x16\xcdj\x81\x01"\x06\x02\xe7\xab%7\xb5\xd4\x9e\x97\x03\t\xaa\xe0n\x9eI\xf3l\xe1\xc9\xfe\xbb\xd4N\xc8\xe0\xd1\xcc\xa0\xb4\xf9\xc3\x19\x18s\xc5\xda\nT\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00"\x02\x03]I\xec\xcdT\xd0\t\x9eCgbw\xc7\xa6\xd4b]a\x1d\xa8\x8a]\xf4\x9b\xf9Qzw\x91\xa7w\xa5\x18s\xc5\xda\nT\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00' + SIGNED_P2WPKH_PSBT_B64_SD = "cHNidP8BAHECAAAAAc88WMMpgq4gUIjZvUnrmwKs3009rnalFsazBrFd46FOAAAAAAD9////Anw/XQUAAAAAFgAULzSqHPAKU7BVopGgOn1F8KaYi1KAlpgAAAAAABYAFOZq/v/Dg45x8KJ7B+OwDt5q6OFgAAAAAAABAR8A4fUFAAAAABYAFNDEo+8J6Ze26Z45flGP4+QaEYyhIgIC56slN7XUnpcDCargbp5J82zhyf671E7I4NHMoLT5wxlHMEQCID5l/ztM1H8SH6fJgihGGNuAMUewVtOTlNTsQg79/GuhAiBsvdiKxRhsPy79JCUx7XkXdXZRrCYjdPPTHYXWFs1qgQEiBgLnqyU3tdSelwMJquBunknzbOHJ/rvUTsjg0cygtPnDGRhzxdoKVAAAgAEAAIAAAACAAAAAAAAAAAAAIgIDXUnszVTQCZ5DZ2J3x6bUYl1hHaiKXfSb+VF6d5Gnd6UYc8XaClQAAIABAACAAAAAgAEAAAAAAAAAAAA=" # Nested Segwit Singlesig P2SH_P2WPKH_PSBT = b'psbt\xff\x01\x00r\x02\x00\x00\x00\x01v\xefk\xf2\xbd\xd0@\xf3\xc1\xd8:\xcc\xb9t9\xf1\xab\xb1\xa5V\xad\x1d\x0fR\x96\x81\xff\xa7\xe8\xca\x94\x8a\x01\x00\x00\x00\x00\xfd\xff\xff\xff\x02\x9c=]\x05\x00\x00\x00\x00\x17\xa9\x14%\x1d\xd1\x14W\xa2Y\xc3\xbaG\xe5\xcc\xa3q\x7f\xe4!N\x02\x98\x87\x80\x96\x98\x00\x00\x00\x00\x00\x16\x00\x14\xe6j\xfe\xff\xc3\x83\x8eq\xf0\xa2{\x07\xe3\xb0\x0e\xdej\xe8\xe1`\x00\x00\x00\x00\x00\x01\x01 \x00\xe1\xf5\x05\x00\x00\x00\x00\x17\xa9\x143l\xaa\x13\xe0\x8b\x96\x08\n2\xb5\xd8\x18\xd5\x9bJ\xb3\xb3gB\x87\x01\x04\x16\x00\x148\x97\x1fs\x93\x0fl\x14\x1d\x97z\xc4\xfdJr|\x85I5\xb3"\x06\x03\xa1\xaf\x80J\xc1\x08\xa8\xa5\x17\x82\x19\x8c-\x03K(\xbf\x90\xc8\x80?ZS\xf7bv\xfai\xa4\xea\xe7\x7f\x18s\xc5\xda\n1\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x16\x00\x14p\xbe\xb1\xe0JP\t@\xe9\xf3\xab\xaaf\xe1\xa4\x9a\xc5[\x8f5"\x02\x02\xa2\xfc\x89\x96\xc5&"H\xb5\xda\xef\xc5\xa4\xd0\xcd\xcd\x00\xc10G\xd0\xcb\x13\x02\x816\xeac\r\x87Z\x87\x18s\xc5\xda\n1\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00' @@ -53,6 +63,8 @@ def tdata(mocker): SIGNED_P2SH_P2WPKH_PSBT_UR_PSBT = UR( "crypto-psbt", PSBT(SIGNED_P2SH_P2WPKH_PSBT).to_cbor() ) + SIGNED_P2SH_P2WPKH_PSBT_SD = b'psbt\xff\x01\x00r\x02\x00\x00\x00\x01v\xefk\xf2\xbd\xd0@\xf3\xc1\xd8:\xcc\xb9t9\xf1\xab\xb1\xa5V\xad\x1d\x0fR\x96\x81\xff\xa7\xe8\xca\x94\x8a\x01\x00\x00\x00\x00\xfd\xff\xff\xff\x02\x9c=]\x05\x00\x00\x00\x00\x17\xa9\x14%\x1d\xd1\x14W\xa2Y\xc3\xbaG\xe5\xcc\xa3q\x7f\xe4!N\x02\x98\x87\x80\x96\x98\x00\x00\x00\x00\x00\x16\x00\x14\xe6j\xfe\xff\xc3\x83\x8eq\xf0\xa2{\x07\xe3\xb0\x0e\xdej\xe8\xe1`\x00\x00\x00\x00\x00\x01\x01 \x00\xe1\xf5\x05\x00\x00\x00\x00\x17\xa9\x143l\xaa\x13\xe0\x8b\x96\x08\n2\xb5\xd8\x18\xd5\x9bJ\xb3\xb3gB\x87"\x02\x03\xa1\xaf\x80J\xc1\x08\xa8\xa5\x17\x82\x19\x8c-\x03K(\xbf\x90\xc8\x80?ZS\xf7bv\xfai\xa4\xea\xe7\x7fG0D\x02 u\x0c\xf8\xe6\x03\x15l\xab\xaa7a`\x1f\xcb\xc5\xd92TC\x97\xbd\xed\xfeS\xeeC\xf4\x1d\xddc\x1cx\x02 4{\xe5K\xe5\xf2F\x04\xd5\x05V\xe8}K\x00\xcc\x93)\x90\x1f\r\x02,\xee?\xd8\xed\xd2\xb8\x97\xcaS\x01\x01\x04\x16\x00\x148\x97\x1fs\x93\x0fl\x14\x1d\x97z\xc4\xfdJr|\x85I5\xb3"\x06\x03\xa1\xaf\x80J\xc1\x08\xa8\xa5\x17\x82\x19\x8c-\x03K(\xbf\x90\xc8\x80?ZS\xf7bv\xfai\xa4\xea\xe7\x7f\x18s\xc5\xda\n1\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x16\x00\x14p\xbe\xb1\xe0JP\t@\xe9\xf3\xab\xaaf\xe1\xa4\x9a\xc5[\x8f5"\x02\x02\xa2\xfc\x89\x96\xc5&"H\xb5\xda\xef\xc5\xa4\xd0\xcd\xcd\x00\xc10G\xd0\xcb\x13\x02\x816\xeac\r\x87Z\x87\x18s\xc5\xda\n1\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00' + SIGNED_P2SH_P2WPKH_PSBT_B64_SD = "cHNidP8BAHICAAAAAXbva/K90EDzwdg6zLl0OfGrsaVWrR0PUpaB/6foypSKAQAAAAD9////Apw9XQUAAAAAF6kUJR3RFFeiWcO6R+XMo3F/5CFOApiHgJaYAAAAAAAWABTmav7/w4OOcfCiewfjsA7eaujhYAAAAAAAAQEgAOH1BQAAAAAXqRQzbKoT4IuWCAoytdgY1ZtKs7NnQociAgOhr4BKwQiopReCGYwtA0sov5DIgD9aU/didvpppOrnf0cwRAIgdQz45gMVbKuqN2FgH8vF2TJUQ5e97f5T7kP0Hd1jHHgCIDR75Uvl8kYE1QVW6H1LAMyTKZAfDQIs7j/Y7dK4l8pTAQEEFgAUOJcfc5MPbBQdl3rE/UpyfIVJNbMiBgOhr4BKwQiopReCGYwtA0sov5DIgD9aU/didvpppOrnfxhzxdoKMQAAgAEAAIAAAACAAAAAAAAAAAAAAQAWABRwvrHgSlAJQOnzq6pm4aSaxVuPNSICAqL8iZbFJiJItdrvxaTQzc0AwTBH0MsTAoE26mMNh1qHGHPF2goxAACAAQAAgAAAAIABAAAAAAAAAAAA" # Taproot Singlesig P2TR_PSBT = b"psbt\xff\x01\x00\xa8\x02\x00\x00\x00\x01\xf9\xfe\xa6\x15\x16\xa0\x07\xe4v2WHAq\x8d\xc0\xda\\\x1a\xf6\xd9\x173\x7f\x06\x8eT\xda\xbeI\xbe?\x00\x00\x00\x00\x00\xfd\xff\xff\xff\x03:\x1e\x00\x00\x00\x00\x00\x00\"Q u\xe6_\x88=\xe5\x87'1\xd9\x8e\xa8o_\x08b\xf0\x929\xd0\xe9\xb0\x0fI\xf5\x92\x06\x9c\x18M\x02\xa2\xe8\x03\x00\x00\x00\x00\x00\x00\"Q \x9d-\x9bm\xe6\x0c\xdc\xddY\x07\xc1\xc0\x961I\x1b\xddCN\xee\xaa\x8a\xaf;;\xf1\x9dc\xd5>@\x99\xe8\x03\x00\x00\x00\x00\x00\x00\x16\x00\x14\xae\xcd\x1e\xdc>\xffe\xaa \x9d\x02\x15\xe7=p\x90]\xc1hl\x83\xbf+\x00O\x01\x045\x87\xcf\x03\xe0\x17\xd1\xbb\x80\x00\x00\x00\x01\x83\\\x0bQ!\x83v\xc6\x14UB\x8a\x9fG\xbf\xcc\xe1\xf6\xce\x83\x97\xea\xd7\xb0\x03\x87\xe9\xd9\xeaV\x83\x02\xcf\xbdq\x001\x1e\x0e\x85\x84L78r\x83\x149N\xb80*kPp\xd6\x92\xe4\x1b\x14\xba\x81\x80\x90\x10s\xc5\xda\nV\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x00\x01\x01+\x7f'\x00\x00\x00\x00\x00\x00\"Q \x06\xac\x82\xfc6\xdb\xb6\rHy\x1d\x81\xc3\xc1\xeb9\x7f\xa9s\xe4V\xc8\xc6\xfbT\xc1\x04!\x1f\x04{\x1a\x01\x03\x04\x00\x00\x00\x00!\x16E\xc6\xb1\xe44\x82<\xe9\xe6\xb4H\xfb\xc1\xe4\x05' 8:\r!(\x82\xbc\xbavSMwR\x8a\xad\x19\x00s\xc5\xda\nV\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x00\x00\x00\x00\t\x00\x00\x00\x01\x17 E\xc6\xb1\xe44\x82<\xe9\xe6\xb4H\xfb\xc1\xe4\x05' 8:\r!(\x82\xbc\xbavSMwR\x8a\xad\x00!\x07 \xeb\x90P\x06\x8b\x01;\xde= M\xb5\x9a\xc5\xdb*\x1cx\xa8\xaa%\x07?\xbf.z\xd4\x9d\x05\x15\xc6\x19\x00s\xc5\xda\nV\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x00\x01\x00\x00\x00\x01\x05 \xeb\x90P\x06\x8b\x01;\xde= M\xb5\x9a\xc5\xdb*\x1cx\xa8\xaa%\x07?\xbf.z\xd4\x9d\x05\x15\xc6\x00!\x07\xdc\xab\x8eB3\xd9\x14\xf2\x98\x08\x9e]\x8d;'a\x8a\x07\xf5!?\x89\x9a+\xbd\xbax\xfd\xb9\x02\xc5\xcf\x19\x00s\xc5\xda\nV\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x00\x00\x00\x00\n\x00\x00\x00\x01\x05 \xdc\xab\x8eB3\xd9\x14\xf2\x98\x08\x9e]\x8d;'a\x8a\x07\xf5!?\x89\x9a+\xbd\xbax\xfd\xb9\x02\xc5\xcf\x00\x00" @@ -61,11 +73,13 @@ def tdata(mocker): P2TR_PSBT_B64 = "cHNidP8BAKgCAAAAAfn+phUWoAfkdjJXSEFxjcDaXBr22RczfwaOVNq+Sb4/AAAAAAD9////AzoeAAAAAAAAIlEgdeZfiD3lhycx2Y6ob18IYvCSOdDpsA9J9ZIGnBhNAqLoAwAAAAAAACJRIJ0tm23mDNzdWQfBwJYxSRvdQ07uqoqvOzvxnWPVPkCZ6AMAAAAAAAAWABSuzR7cPv9lqiCdAhXnPXCQXcFobIO/KwBPAQQ1h88D4BfRu4AAAAABg1wLUSGDdsYUVUKKn0e/zOH2zoOX6tewA4fp2epWgwLPvXEAMR4OhYRMNzhygxQ5TrgwKmtQcNaS5BsUuoGAkBBzxdoKVgAAgAEAAIAAAACAAAEBK38nAAAAAAAAIlEgBqyC/Dbbtg1IeR2Bw8HrOX+pc+RWyMb7VMEEIR8EexoBAwQAAAAAIRZFxrHkNII86ea0SPvB5AUnIDg6DSEogry6dlNNd1KKrRkAc8XaClYAAIABAACAAAAAgAAAAAAJAAAAARcgRcax5DSCPOnmtEj7weQFJyA4Og0hKIK8unZTTXdSiq0AIQcg65BQBosBO949IE21msXbKhx4qKolBz+/LnrUnQUVxhkAc8XaClYAAIABAACAAAAAgAEAAAABAAAAAQUgIOuQUAaLATvePSBNtZrF2yoceKiqJQc/vy561J0FFcYAIQfcq45CM9kU8pgInl2NOydhigf1IT+Jmiu9unj9uQLFzxkAc8XaClYAAIABAACAAAAAgAAAAAAKAAAAAQUg3KuOQjPZFPKYCJ5djTsnYYoH9SE/iZorvbp4/bkCxc8AAA==" P2TR_PSBT_UR_PSBT = UR("crypto-psbt", PSBT(P2TR_PSBT).to_cbor()) - SIGNED_P2TR_PSBT = b'psbt\xff\x01\x00\xa8\x02\x00\x00\x00\x01\xf9\xfe\xa6\x15\x16\xa0\x07\xe4v2WHAq\x8d\xc0\xda\\\x1a\xf6\xd9\x173\x7f\x06\x8eT\xda\xbeI\xbe?\x00\x00\x00\x00\x00\xfd\xff\xff\xff\x03:\x1e\x00\x00\x00\x00\x00\x00"Q u\xe6_\x88=\xe5\x87\'1\xd9\x8e\xa8o_\x08b\xf0\x929\xd0\xe9\xb0\x0fI\xf5\x92\x06\x9c\x18M\x02\xa2\xe8\x03\x00\x00\x00\x00\x00\x00"Q \x9d-\x9bm\xe6\x0c\xdc\xddY\x07\xc1\xc0\x961I\x1b\xddCN\xee\xaa\x8a\xaf;;\xf1\x9dc\xd5>@\x99\xe8\x03\x00\x00\x00\x00\x00\x00\x16\x00\x14\xae\xcd\x1e\xdc>\xffe\xaa \x9d\x02\x15\xe7=p\x90]\xc1hl\x83\xbf+\x00\x00\x01\x01+\x7f\'\x00\x00\x00\x00\x00\x00"Q \x06\xac\x82\xfc6\xdb\xb6\rHy\x1d\x81\xc3\xc1\xeb9\x7f\xa9s\xe4V\xc8\xc6\xfbT\xc1\x04!\x1f\x04{\x1a\x01\x08B\x01@(\xb0\x7f\x8d\x19\x9a\xa5\xa3\xae_ ti\x10G\x95\xc3)\x18\x1e\xca=Lq^\x9ah\xe7\xb58=\x87\x80\xd8\x1a\xbd\x0bb\x06@}\xd2\x0c?\xd9G\xac\xe9!%{\xe0E\x8bY\xca\xedH\xebh`\xbc\xbb\x13\x00\x00\x00\x00' - SIGNED_P2TR_PSBT_B43 = "1.W6Z+4SFE49JJT-J.9MUIM8BKT/3M25COGC72:/-9L4Z*6IXB2KK$H2IC/44*CKWH2801.OB1F+5M*MTBOXC8*$8NXVL40OY41IL*F5QJ1AT4ZL4G8J.8X7IFBM-:XE+QQOY*4ODVY-67NBDJ.ZKY:KQ1S3O.ZJV*14GS7Z/KU+.9G8SBDR1NWSCJWD*7$/63DQ+ZW$4BU7.00/QBF$VXSTW0A9LK52C7RBVSQ4+S4+HSC2159G:4HJ8+QWX-DU.LK43L-7CHLHHKWRL/7ZK8S.A-T578YCDHTCH5O/3N/96SV$5ADQ+S82KECF-T7868W*INCWZ3U4G$V8$ON-/KWV3Z+4N37DNH.8U:-9:KJ5WW*7U67S-0V9K3T:GO19R-/Z*XBW$N9OE7P.I912.1JN+EM5KJXR*2NAB/WON6RP03U6YIP0C" - SIGNED_P2TR_PSBT_B58 = "297QokMrvCwY2Pjf3b1GUWQsM8EhryP2qMWTDeTDRo6WNptEQH54WkFSybF6uYEbxSJja2nwMM9mHqEbtkxYqriFdRs7fvU2tEHXK2Hx2Xycz7AEx2LRvADfUGtycCxvVV155wRPwruzecqi5pTAbYnh8Ds5pznRzTkgcnCVaoJbLvmjbE7b6BkWiY5LEYUvZ1wBdzaVhewCS32CXfQ516AC1Ky4G7Xf1rvoQeAdfoD4YV13zV8ifoWWz6A9avCZMREzXartA7igxQZSwZS3o36yrr6MTkfKgqRYs46fHNKud78PUugnEA3kDDKrTRUhioptPQ52hFrN6iswVE2EuHGttaFKZmtQa6Z2gBDrydus7iwrqx14EdzSVzKmY54AHWn23u6u9F1StHsW3sj7m" - SIGNED_P2TR_PSBT_B64 = "cHNidP8BAKgCAAAAAfn+phUWoAfkdjJXSEFxjcDaXBr22RczfwaOVNq+Sb4/AAAAAAD9////AzoeAAAAAAAAIlEgdeZfiD3lhycx2Y6ob18IYvCSOdDpsA9J9ZIGnBhNAqLoAwAAAAAAACJRIJ0tm23mDNzdWQfBwJYxSRvdQ07uqoqvOzvxnWPVPkCZ6AMAAAAAAAAWABSuzR7cPv9lqiCdAhXnPXCQXcFobIO/KwAAAQErfycAAAAAAAAiUSAGrIL8Ntu2DUh5HYHDwes5f6lz5FbIxvtUwQQhHwR7GgEIQgFAKLB/jRmapaOuXyB0aRBHlcMpGB7KPUxxXppo57U4PYeA2Bq9C2IGQH3SDD/ZR6zpISV74EWLWcrtSOtoYLy7EwAAAAA=" + SIGNED_P2TR_PSBT = b'psbt\xff\x01\x00\xa8\x02\x00\x00\x00\x01\xf9\xfe\xa6\x15\x16\xa0\x07\xe4v2WHAq\x8d\xc0\xda\\\x1a\xf6\xd9\x173\x7f\x06\x8eT\xda\xbeI\xbe?\x00\x00\x00\x00\x00\xfd\xff\xff\xff\x03:\x1e\x00\x00\x00\x00\x00\x00"Q u\xe6_\x88=\xe5\x87\'1\xd9\x8e\xa8o_\x08b\xf0\x929\xd0\xe9\xb0\x0fI\xf5\x92\x06\x9c\x18M\x02\xa2\xe8\x03\x00\x00\x00\x00\x00\x00"Q \x9d-\x9bm\xe6\x0c\xdc\xddY\x07\xc1\xc0\x961I\x1b\xddCN\xee\xaa\x8a\xaf;;\xf1\x9dc\xd5>@\x99\xe8\x03\x00\x00\x00\x00\x00\x00\x16\x00\x14\xae\xcd\x1e\xdc>\xffe\xaa \x9d\x02\x15\xe7=p\x90]\xc1hl\x83\xbf+\x00\x00\x01\x01+\x7f\'\x00\x00\x00\x00\x00\x00"Q \x06\xac\x82\xfc6\xdb\xb6\rHy\x1d\x81\xc3\xc1\xeb9\x7f\xa9s\xe4V\xc8\xc6\xfbT\xc1\x04!\x1f\x04{\x1a\x01\x08B\x01@(\xb0\x7f\x8d\x19\x9a\xa5\xa3\xae_ ti\x10G\x95\xc3)\x18\x1e\xca=Lq^\x9ah\xe7\xb58=\x87\x80\xd8\x1a\xbd\x0bb\x06@}\xd2\x0c?\xd9G\xac\xe9!%{\xe0E\x8bY\xca\xedH\xebh`\xbc\xbb\x13\x01\x13@(\xb0\x7f\x8d\x19\x9a\xa5\xa3\xae_ ti\x10G\x95\xc3)\x18\x1e\xca=Lq^\x9ah\xe7\xb58=\x87\x80\xd8\x1a\xbd\x0bb\x06@}\xd2\x0c?\xd9G\xac\xe9!%{\xe0E\x8bY\xca\xedH\xebh`\xbc\xbb\x13\x00\x00\x00\x00' + SIGNED_P2TR_PSBT_B43 = "$JBJ$DT-D-U/1AEWDTQ*PH4J+H:1+I467L27$UINP.QOHP:ZYLDCPD8/LI+DN/A.X7-PC4UCX-K$/OQO-G-H.CGX.+9:5JKUVI:PVG+C37HW*MVZDUS$J-CLTWQ6WQ.A0MGXBUZF:X:276$9AZTBLV82BDNXAV1XQ6..R3I+.LV4OLAXU0MO:KY3KB.:-2PFJOYQ/*95CD:5HPWA/HJ2OW1ZPNB*S39+EAP+60B73/9W9KU.A:DTLVNCOCV1/BBLE9O14Z5ACDF0N$PHZ-B:IEJHV17V91KA/WPGFPSZNSIB/-WNYY./+5VQV.DUAIILF88LFSGJ/E36.B6WJ1$I/FWPOI/L*3.8F2B1TNQS-24N8QJAF**+S13/RFDR9ZZ3/N4CP7S9Y9HKJ-$/B43SC*+BRP44+DUH+$79DJB1-9AY7N/SZPZSHJP6+KYW+4/GIWNVRW$DUE08UMY-7/FH3*ZPPAL2PWR$1+R996Y.E9H:PIB*SJQY.JOEQL/X+2Q5V91UL*/WNCH$$DY*GK.I9XN" + SIGNED_P2TR_PSBT_B58 = "9eehoGbwQ78EoHBL2mrxCSrMophCYTK9PxBW3WqL69ZFwj5F1ESdYXt9GdSKA3xxCgfDKhQt9PqBtSkNoKRFgnm6WyTASro6r5za7Nd2JtywPgYe1JdErssp47WvjrbkBBYV5jvWrzPTJhFrpyis8LLyKExp1XY4VwfSTrcFXfDEM7QoRDehks7s7w7CbLHg8zJEDqRjCdcZpzbVunb3FR7ypZHv41S8VKvhiGHFq1nW99F3dErPYbEs8Pc7gVwhJ6XuhmkpHArZp8pyFd3pQJhyBYQwqUR4QU41buYuFQ465SyMuCLyXhcevALrcjJRJVJsCpsEEZgqTk5z2oqsNPXNVHweNjeHBUdN9uJBgtQ6v5mVWRdsYUfmwFeQqyvDEaioqAnNNSpSe2ByNfcv8ftSfzocbNHLahUqTbjSvxThW84LpEwMUitaUQoguJweVscdRRitkx1kCmsaMC5JwrRH13zHVoFnJqPEUHZwqYTzAJCf" + SIGNED_P2TR_PSBT_B64 = "cHNidP8BAKgCAAAAAfn+phUWoAfkdjJXSEFxjcDaXBr22RczfwaOVNq+Sb4/AAAAAAD9////AzoeAAAAAAAAIlEgdeZfiD3lhycx2Y6ob18IYvCSOdDpsA9J9ZIGnBhNAqLoAwAAAAAAACJRIJ0tm23mDNzdWQfBwJYxSRvdQ07uqoqvOzvxnWPVPkCZ6AMAAAAAAAAWABSuzR7cPv9lqiCdAhXnPXCQXcFobIO/KwAAAQErfycAAAAAAAAiUSAGrIL8Ntu2DUh5HYHDwes5f6lz5FbIxvtUwQQhHwR7GgEIQgFAKLB/jRmapaOuXyB0aRBHlcMpGB7KPUxxXppo57U4PYeA2Bq9C2IGQH3SDD/ZR6zpISV74EWLWcrtSOtoYLy7EwETQCiwf40ZmqWjrl8gdGkQR5XDKRgeyj1McV6aaOe1OD2HgNgavQtiBkB90gw/2Ues6SEle+BFi1nK7UjraGC8uxMAAAAA" SIGNED_P2TR_PSBT_UR_PSBT = UR("crypto-psbt", PSBT(SIGNED_P2TR_PSBT).to_cbor()) + SIGNED_P2TR_PSBT_SD = b"psbt\xff\x01\x00\xa8\x02\x00\x00\x00\x01\xf9\xfe\xa6\x15\x16\xa0\x07\xe4v2WHAq\x8d\xc0\xda\\\x1a\xf6\xd9\x173\x7f\x06\x8eT\xda\xbeI\xbe?\x00\x00\x00\x00\x00\xfd\xff\xff\xff\x03:\x1e\x00\x00\x00\x00\x00\x00\"Q u\xe6_\x88=\xe5\x87'1\xd9\x8e\xa8o_\x08b\xf0\x929\xd0\xe9\xb0\x0fI\xf5\x92\x06\x9c\x18M\x02\xa2\xe8\x03\x00\x00\x00\x00\x00\x00\"Q \x9d-\x9bm\xe6\x0c\xdc\xddY\x07\xc1\xc0\x961I\x1b\xddCN\xee\xaa\x8a\xaf;;\xf1\x9dc\xd5>@\x99\xe8\x03\x00\x00\x00\x00\x00\x00\x16\x00\x14\xae\xcd\x1e\xdc>\xffe\xaa \x9d\x02\x15\xe7=p\x90]\xc1hl\x83\xbf+\x00O\x01\x045\x87\xcf\x03\xe0\x17\xd1\xbb\x80\x00\x00\x00\x01\x83\\\x0bQ!\x83v\xc6\x14UB\x8a\x9fG\xbf\xcc\xe1\xf6\xce\x83\x97\xea\xd7\xb0\x03\x87\xe9\xd9\xeaV\x83\x02\xcf\xbdq\x001\x1e\x0e\x85\x84L78r\x83\x149N\xb80*kPp\xd6\x92\xe4\x1b\x14\xba\x81\x80\x90\x10s\xc5\xda\nV\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x00\x01\x01+\x7f'\x00\x00\x00\x00\x00\x00\"Q \x06\xac\x82\xfc6\xdb\xb6\rHy\x1d\x81\xc3\xc1\xeb9\x7f\xa9s\xe4V\xc8\xc6\xfbT\xc1\x04!\x1f\x04{\x1a\x01\x03\x04\x00\x00\x00\x00\x01\x08B\x01@(\xb0\x7f\x8d\x19\x9a\xa5\xa3\xae_ ti\x10G\x95\xc3)\x18\x1e\xca=Lq^\x9ah\xe7\xb58=\x87\x80\xd8\x1a\xbd\x0bb\x06@}\xd2\x0c?\xd9G\xac\xe9!%{\xe0E\x8bY\xca\xedH\xebh`\xbc\xbb\x13\x01\x13@(\xb0\x7f\x8d\x19\x9a\xa5\xa3\xae_ ti\x10G\x95\xc3)\x18\x1e\xca=Lq^\x9ah\xe7\xb58=\x87\x80\xd8\x1a\xbd\x0bb\x06@}\xd2\x0c?\xd9G\xac\xe9!%{\xe0E\x8bY\xca\xedH\xebh`\xbc\xbb\x13!\x16E\xc6\xb1\xe44\x82<\xe9\xe6\xb4H\xfb\xc1\xe4\x05' 8:\r!(\x82\xbc\xbavSMwR\x8a\xad\x19\x00s\xc5\xda\nV\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x00\x00\x00\x00\t\x00\x00\x00\x01\x17 E\xc6\xb1\xe44\x82<\xe9\xe6\xb4H\xfb\xc1\xe4\x05' 8:\r!(\x82\xbc\xbavSMwR\x8a\xad\x00\x01\x05 \xeb\x90P\x06\x8b\x01;\xde= M\xb5\x9a\xc5\xdb*\x1cx\xa8\xaa%\x07?\xbf.z\xd4\x9d\x05\x15\xc6!\x07 \xeb\x90P\x06\x8b\x01;\xde= M\xb5\x9a\xc5\xdb*\x1cx\xa8\xaa%\x07?\xbf.z\xd4\x9d\x05\x15\xc6\x19\x00s\xc5\xda\nV\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x00\x01\x00\x00\x00\x00\x01\x05 \xdc\xab\x8eB3\xd9\x14\xf2\x98\x08\x9e]\x8d;'a\x8a\x07\xf5!?\x89\x9a+\xbd\xbax\xfd\xb9\x02\xc5\xcf!\x07\xdc\xab\x8eB3\xd9\x14\xf2\x98\x08\x9e]\x8d;'a\x8a\x07\xf5!?\x89\x9a+\xbd\xbax\xfd\xb9\x02\xc5\xcf\x19\x00s\xc5\xda\nV\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x00\x00\x00\x00\n\x00\x00\x00\x00\x00" + SIGNED_P2TR_PSBT_B64_SD = "cHNidP8BAKgCAAAAAfn+phUWoAfkdjJXSEFxjcDaXBr22RczfwaOVNq+Sb4/AAAAAAD9////AzoeAAAAAAAAIlEgdeZfiD3lhycx2Y6ob18IYvCSOdDpsA9J9ZIGnBhNAqLoAwAAAAAAACJRIJ0tm23mDNzdWQfBwJYxSRvdQ07uqoqvOzvxnWPVPkCZ6AMAAAAAAAAWABSuzR7cPv9lqiCdAhXnPXCQXcFobIO/KwBPAQQ1h88D4BfRu4AAAAABg1wLUSGDdsYUVUKKn0e/zOH2zoOX6tewA4fp2epWgwLPvXEAMR4OhYRMNzhygxQ5TrgwKmtQcNaS5BsUuoGAkBBzxdoKVgAAgAEAAIAAAACAAAEBK38nAAAAAAAAIlEgBqyC/Dbbtg1IeR2Bw8HrOX+pc+RWyMb7VMEEIR8EexoBAwQAAAAAAQhCAUAosH+NGZqlo65fIHRpEEeVwykYHso9THFemmjntTg9h4DYGr0LYgZAfdIMP9lHrOkhJXvgRYtZyu1I62hgvLsTARNAKLB/jRmapaOuXyB0aRBHlcMpGB7KPUxxXppo57U4PYeA2Bq9C2IGQH3SDD/ZR6zpISV74EWLWcrtSOtoYLy7EyEWRcax5DSCPOnmtEj7weQFJyA4Og0hKIK8unZTTXdSiq0ZAHPF2gpWAACAAQAAgAAAAIAAAAAACQAAAAEXIEXGseQ0gjzp5rRI+8HkBScgODoNISiCvLp2U013UoqtAAEFICDrkFAGiwE73j0gTbWaxdsqHHioqiUHP78uetSdBRXGIQcg65BQBosBO949IE21msXbKhx4qKolBz+/LnrUnQUVxhkAc8XaClYAAIABAACAAAAAgAEAAAABAAAAAAEFINyrjkIz2RTymAieXY07J2GKB/UhP4maK726eP25AsXPIQfcq45CM9kU8pgInl2NOydhigf1IT+Jmiu9unj9uQLFzxkAc8XaClYAAIABAACAAAAAgAAAAAAKAAAAAAA=" # Native Segwit Multisig P2WSH_PSBT = b'psbt\xff\x01\x00\xb2\x02\x00\x00\x00\x02\xadC\x87\x14J\xfae\x07\xe1>\xaeP\xda\x1b\xf1\xb5\x1ag\xb3\x0f\xfb\x8e\x0c[\x8f\x98\xf5\xb3\xb1\xa68Y\x00\x00\x00\x00\x00\xfd\xff\xff\xffig%Y\x0f\xb8\xe4r\xab#N\xeb\xf3\xbf\x04\xd9J\xc0\xba\x94\xf6\xa5\xa4\xf8B\xea\xdb\x9a\xd3c`\xd4\x01\x00\x00\x00\x00\xfd\xff\xff\xff\x02@B\x0f\x00\x00\x00\x00\x00"\x00 \xa9\x903\xc3\x86b3>Y\t\xae<=\x03\xbdq\x8d\xb2\x14Y\xfd\xd5P\x1e\xe8\xa0RaMY\xb4\xe2\xd8\xd2!\x01\x00\x00\x00\x00"\x00 \x8d\x02\x85\r\xab\x88^\xc5y\xbbm\xcb\x05\xd6 ;\x05\xf5\x17\x01\x86\xac\xb8\x90}l\xc1\xb4R\x99\xed\xd2\x00\x00\x00\x00O\x01\x045\x87\xcf\x04>b\xdf~\x80\x00\x00\x02A+I\x84\xd5I\xba^\xef\x1c\xa6\xe8\xf3u]\x9a\xe0\x16\xdam\x16ir\xca\x0eQ@6~\xddP\xda\x025\xb8K1\xdc8*|\xfbC\xba:{\x17K\xe9AaA\xe8\x16\xf6r[\xd1%\x12\xb5\xb2\xc4\xa5\xac\x14\x02\x08\xcbw0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80O\x01\x045\x87\xcf\x04\x9d\xb1\xd0\x00\x80\x00\x00\x02?\xd8\xd7;\xc7\xb8\x8c\xa4\x93Z\xa57\xbf8\x94\xd5\xe2\x88\x9f\xab4\x1ca\x8fJWo\x8f\x19\x18\xc2u\x02h\xc3\rV\x9d#j}\xccW\x1b+\xb1\xd2\xadO\xa9\xf9\xb3R\xa8\t6\xa2\x89\n\x99\xaa#\xdbx\xec\x14&\xbb\x83\xc40\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80O\x01\x045\x87\xcf\x04\xba\xc1H9\x80\x00\x00\x02\x1dO\xbe\xbd\xd9g\xe1\xafqL\t\x97\xd3\x8f\xcfg\x0b\\\xe9\xd3\x01\xc0D\x0b\xbc\xc3\xb6\xa2\x0e\xb7r\x1c\x03V\x8e\xa1\xf3`Q\x91n\xd1\xb6\x90\xc3\x9e\x12\xa8\xe7\x06\x03\xb2\x80\xbd0\xce_(\x1f)\x18\xa5Sc\xaa\x14s\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x01\x01+\x80\x96\x98\x00\x00\x00\x00\x00"\x00 \x89\x801pn\xdd\x9e\xb1"g\x85G\x15Q\xce\xa3_\x17\t\xa9o\x85\x96.2\xa0k\xf6~\xc7\x11$\x01\x05iR!\x02N\x8d\x08\x0c}}\xba\\G\xfe\xb6\xb1\xc8\x12M\xebbA\x17\xe5\x8d\x8d~\xb1J@\x04Oq\xdd\x97\xf2!\x03\x05a\xd4\x82\xad\xb9=\xf1\xef\x13\xe8ep\x1a\xf2$n\xf0\xa3l\xbc\x8c\xa5\x12=\x8e\xecw\xceN8\xc7!\x03h\x95r\xe2\x8b\x0f\xed\xa9\xd6\x98\x1c\x027\xd9\xe5\xde\xdb\xfe\xc1m\xe7\x14?h\n\x02\xed]\x15\x9fu\x87S\xae"\x06\x02N\x8d\x08\x0c}}\xba\\G\xfe\xb6\xb1\xc8\x12M\xebbA\x17\xe5\x8d\x8d~\xb1J@\x04Oq\xdd\x97\xf2\x1c&\xbb\x83\xc40\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x01\x00\x00\x00"\x06\x03\x05a\xd4\x82\xad\xb9=\xf1\xef\x13\xe8ep\x1a\xf2$n\xf0\xa3l\xbc\x8c\xa5\x12=\x8e\xecw\xceN8\xc7\x1c\x02\x08\xcbw0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x01\x00\x00\x00"\x06\x03h\x95r\xe2\x8b\x0f\xed\xa9\xd6\x98\x1c\x027\xd9\xe5\xde\xdb\xfe\xc1m\xe7\x14?h\n\x02\xed]\x15\x9fu\x87\x1cs\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x01\x00\x00\x00\x00\x01\x01+\x80\x96\x98\x00\x00\x00\x00\x00"\x00 3w\xad03\xd1\x05\x9c\xf1\xd25\xbb\x12%\xfc\xa2\xa4\xbf&\xc9R\xd5?o\xef\xc3:-UD\x8d\xc5\x01\x05iR!\x02"\x821\x12\xe5\xcc\x88K\x91\x16\xcb!B\x0c\xc7\x92\x98$\xcd/\xe8\xb7#[\xf9\x92\xe8\xae\xde\x14l"!\x02\x83\xcdG\xe5Sm\xcby\xe7\x11\x830\xe8\xe4\x80B\x12\xf6\x96\x19\xf1\xd6\xec\x99\r\xc75\xef\xb9\xce\xc5t!\x03\x0b\x90\xed.\x86\xba\xd7\xf2\xa4\xfe\x97i\xbbA}{\xa9\xca\xa1\x12H\x07\xdb\xfb6-\xfb\xee\xb6^~\x01S\xae"\x06\x02"\x821\x12\xe5\xcc\x88K\x91\x16\xcb!B\x0c\xc7\x92\x98$\xcd/\xe8\xb7#[\xf9\x92\xe8\xae\xde\x14l"\x1c\x02\x08\xcbw0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00"\x06\x02\x83\xcdG\xe5Sm\xcby\xe7\x11\x830\xe8\xe4\x80B\x12\xf6\x96\x19\xf1\xd6\xec\x99\r\xc75\xef\xb9\xce\xc5t\x1c&\xbb\x83\xc40\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00"\x06\x03\x0b\x90\xed.\x86\xba\xd7\xf2\xa4\xfe\x97i\xbbA}{\xa9\xca\xa1\x12H\x07\xdb\xfb6-\xfb\xee\xb6^~\x01\x1cs\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01iR!\x02\xad!\xd9\xad(\xab\x99\xac~\xdf\xd9\x1e"!O\x11YS\xab\t\xd1\xd5X\x10\x92\xfbG\xbd\xa5\x92r\xfe!\x03\xa0};\xe0\xba\xd6<\x805\xd2\x1c\x97\xb4\x10\x89\r=:\x19\xd2\xe4\x03\xaf\xb3\xfc\xfch&\xaa&\x02\xb8\xf4\xcfb\xbc\xc6\xa7\xa2kS\xae"\x02\x02\xad!\xd9\xad(\xab\x99\xac~\xdf\xd9\x1e"!O\x11YS\xab\t\xd1\xd5X\x10\x92\xfbG\xbd\xa5\x92r\xfe\x1c\x02\x08\xcbw0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x01\x00\x00\x00\x00\x00\x00\x00"\x02\x03\xa0};\xe0\xba\xd6<\x805\xd2\x1c\x97\xb4\x10\x89\r=:\x19\xd2\xe4\x03\xaf\xb3\xfc\xfch&\xaa&\x02\xb8\xf4\xcfb\xbc\xc6\xa7\xa2k\x1c&\xbb\x83\xc40\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00' @@ -79,6 +93,24 @@ def tdata(mocker): SIGNED_P2WSH_PSBT_B58 = "8zF2eD2uMaxb2DsPbbKRp9Mpm1UH66wJ4uSMZPn5MA1nJwE8S3RAGDWCCvsguUhQDPCPVw5S8GsAJvAc811WwP71guKubiPxppUKqaWUkQN71B18krk1K2NrWCA9ewdJE1d1BopmzNBr5Qg3qZNCsN6EDSCn8uow2r97h2rnokSq4nXfy3KE2v7hBrGvSZw8A96WAJDSEnS1sP7U5wgtiSYdWxEHBfKocVC3G3sayPuMEjzA9QcYWpy1U6S3EW8YWw5kvQW88LKg8b3js9R7w9aWFnQwMdRg3CFSfGkoHegiWSufPejQ65Airoo8WqNvGVacmSZybkmcvu57KRobPuhZCDPbtJ5435eJsarQ9nmRpac6VJNoLrdHvkeWRZ9QEXk654TW7z6NV87it5vPVDNEnFW3c9pAdtZsYXHafw7CMSiWRPEsCoRKzodieHJFfeHiJGT5zqcP3bgmHTszp3GPMdRy3onHeFRCoyftuAEBkwsCyaYFwoSBE6YUgjHT1T4mWCoe7W4ktoDEKXsHawFC6sw6KCPzDbuTZJuaDoWijHeo7FnC97QVvMNHdVgz1ZmCerxQLrkFJsT8x5WYuVoMmcHRpbyRcuCV64YYPgFFfCZs7VVedPsgSNdkQ7qKCqVYnTaNiBpeE5NB4gwcMcCYUJMWXkqCq7t91o9Mi5ozv4R9yPonSUaTH13D6FSPY9RSZbU1F9SMgFXBoB4DdssC36RGpqZ3Fnm77RJB5v1WJAQ95RNXHuSuKB43X1HiiKYKohtFrwd7jCtbUGuUUs9mpd7VqdvVtbSE9jpQxgxJHvF983X43ZxKcE2K8AMr4WyL8s2Pi2Xj644V9zauy2qmLeNusxudGNgPdTsktjEoWCSuFPtxuqcmC8MwXh5cSQqiFkULgFNuGtkUajJQEnfW9jMfjm5y7Va5DhJN4S7MN371DgiusGnsmb5YZGBeycptyDykdsFXYs" SIGNED_P2WSH_PSBT_B64 = "cHNidP8BALICAAAAAq1DhxRK+mUH4T6uUNob8bUaZ7MP+44MW4+Y9bOxpjhZAAAAAAD9////aWclWQ+45HKrI07r878E2UrAupT2paT4QurbmtNjYNQBAAAAAP3///8CQEIPAAAAAAAiACCpkDPDhmIzPlkJrjw9A71xjbIUWf3VUB7ooFJhTVm04tjSIQEAAAAAIgAgjQKFDauIXsV5u23LBdYgOwX1FwGGrLiQfWzBtFKZ7dIAAAAAAAEBK4CWmAAAAAAAIgAgiYAxcG7dnrEiZ4VHFVHOo18XCalvhZYuMqBr9n7HESQiAgNolXLiiw/tqdaYHAI32eXe2/7BbecUP2gKAu1dFZ91h0cwRAIgaD9tGQRDiZWLuu27ujgpCa5e42AWR8iLcZwOvMWxaqICIAUNUCjgnGNdKXHl4lOfryvkX6nG+Q0iJfSiADuirzJXAQEFaVIhAk6NCAx9fbpcR/62scgSTetiQRfljY1+sUpABE9x3ZfyIQMFYdSCrbk98e8T6GVwGvIkbvCjbLyMpRI9jux3zk44xyEDaJVy4osP7anWmBwCN9nl3tv+wW3nFD9oCgLtXRWfdYdTrgABASuAlpgAAAAAACIAIDN3rTAz0QWc8dI1uxIl/KKkvybJUtU/b+/DOi1VRI3FIgIDC5DtLoa61/Kk/pdpu0F9e6nKoRJIB9v7Ni377rZefgFHMEQCIH5PG4y7h3iju/8E2BBDcchZDztONpfYU/50aYCzEuA+AiBskz0CbbQ8kPQl+Voke7fsTxkVo6NT8lGB3Fj71SaexQEBBWlSIQIigjES5cyIS5EWyyFCDMeSmCTNL+i3I1v5kuiu3hRsIiECg81H5VNty3nnEYMw6OSAQhL2lhnx1uyZDcc177nOxXQhAwuQ7S6GutfypP6XabtBfXupyqESSAfb+zYt++62Xn4BU64AAAA=" SIGNED_P2WSH_PSBT_UR_PSBT = UR("crypto-psbt", PSBT(SIGNED_P2WSH_PSBT).to_cbor()) + SIGNED_P2WSH_PSBT_SD = b'psbt\xff\x01\x00\xb2\x02\x00\x00\x00\x02\xadC\x87\x14J\xfae\x07\xe1>\xaeP\xda\x1b\xf1\xb5\x1ag\xb3\x0f\xfb\x8e\x0c[\x8f\x98\xf5\xb3\xb1\xa68Y\x00\x00\x00\x00\x00\xfd\xff\xff\xffig%Y\x0f\xb8\xe4r\xab#N\xeb\xf3\xbf\x04\xd9J\xc0\xba\x94\xf6\xa5\xa4\xf8B\xea\xdb\x9a\xd3c`\xd4\x01\x00\x00\x00\x00\xfd\xff\xff\xff\x02@B\x0f\x00\x00\x00\x00\x00"\x00 \xa9\x903\xc3\x86b3>Y\t\xae<=\x03\xbdq\x8d\xb2\x14Y\xfd\xd5P\x1e\xe8\xa0RaMY\xb4\xe2\xd8\xd2!\x01\x00\x00\x00\x00"\x00 \x8d\x02\x85\r\xab\x88^\xc5y\xbbm\xcb\x05\xd6 ;\x05\xf5\x17\x01\x86\xac\xb8\x90}l\xc1\xb4R\x99\xed\xd2\x00\x00\x00\x00O\x01\x045\x87\xcf\x04>b\xdf~\x80\x00\x00\x02A+I\x84\xd5I\xba^\xef\x1c\xa6\xe8\xf3u]\x9a\xe0\x16\xdam\x16ir\xca\x0eQ@6~\xddP\xda\x025\xb8K1\xdc8*|\xfbC\xba:{\x17K\xe9AaA\xe8\x16\xf6r[\xd1%\x12\xb5\xb2\xc4\xa5\xac\x14\x02\x08\xcbw0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80O\x01\x045\x87\xcf\x04\x9d\xb1\xd0\x00\x80\x00\x00\x02?\xd8\xd7;\xc7\xb8\x8c\xa4\x93Z\xa57\xbf8\x94\xd5\xe2\x88\x9f\xab4\x1ca\x8fJWo\x8f\x19\x18\xc2u\x02h\xc3\rV\x9d#j}\xccW\x1b+\xb1\xd2\xadO\xa9\xf9\xb3R\xa8\t6\xa2\x89\n\x99\xaa#\xdbx\xec\x14&\xbb\x83\xc40\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80O\x01\x045\x87\xcf\x04\xba\xc1H9\x80\x00\x00\x02\x1dO\xbe\xbd\xd9g\xe1\xafqL\t\x97\xd3\x8f\xcfg\x0b\\\xe9\xd3\x01\xc0D\x0b\xbc\xc3\xb6\xa2\x0e\xb7r\x1c\x03V\x8e\xa1\xf3`Q\x91n\xd1\xb6\x90\xc3\x9e\x12\xa8\xe7\x06\x03\xb2\x80\xbd0\xce_(\x1f)\x18\xa5Sc\xaa\x14s\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x01\x01+\x80\x96\x98\x00\x00\x00\x00\x00"\x00 \x89\x801pn\xdd\x9e\xb1"g\x85G\x15Q\xce\xa3_\x17\t\xa9o\x85\x96.2\xa0k\xf6~\xc7\x11$"\x02\x03h\x95r\xe2\x8b\x0f\xed\xa9\xd6\x98\x1c\x027\xd9\xe5\xde\xdb\xfe\xc1m\xe7\x14?h\n\x02\xed]\x15\x9fu\x87G0D\x02 h?m\x19\x04C\x89\x95\x8b\xba\xed\xbb\xba8)\t\xae^\xe3`\x16G\xc8\x8bq\x9c\x0e\xbc\xc5\xb1j\xa2\x02 \x05\rP(\xe0\x9cc])q\xe5\xe2S\x9f\xaf+\xe4_\xa9\xc6\xf9\r"%\xf4\xa2\x00;\xa2\xaf2W\x01\x01\x05iR!\x02N\x8d\x08\x0c}}\xba\\G\xfe\xb6\xb1\xc8\x12M\xebbA\x17\xe5\x8d\x8d~\xb1J@\x04Oq\xdd\x97\xf2!\x03\x05a\xd4\x82\xad\xb9=\xf1\xef\x13\xe8ep\x1a\xf2$n\xf0\xa3l\xbc\x8c\xa5\x12=\x8e\xecw\xceN8\xc7!\x03h\x95r\xe2\x8b\x0f\xed\xa9\xd6\x98\x1c\x027\xd9\xe5\xde\xdb\xfe\xc1m\xe7\x14?h\n\x02\xed]\x15\x9fu\x87S\xae"\x06\x02N\x8d\x08\x0c}}\xba\\G\xfe\xb6\xb1\xc8\x12M\xebbA\x17\xe5\x8d\x8d~\xb1J@\x04Oq\xdd\x97\xf2\x1c&\xbb\x83\xc40\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x01\x00\x00\x00"\x06\x03\x05a\xd4\x82\xad\xb9=\xf1\xef\x13\xe8ep\x1a\xf2$n\xf0\xa3l\xbc\x8c\xa5\x12=\x8e\xecw\xceN8\xc7\x1c\x02\x08\xcbw0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x01\x00\x00\x00"\x06\x03h\x95r\xe2\x8b\x0f\xed\xa9\xd6\x98\x1c\x027\xd9\xe5\xde\xdb\xfe\xc1m\xe7\x14?h\n\x02\xed]\x15\x9fu\x87\x1cs\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x01\x00\x00\x00\x00\x01\x01+\x80\x96\x98\x00\x00\x00\x00\x00"\x00 3w\xad03\xd1\x05\x9c\xf1\xd25\xbb\x12%\xfc\xa2\xa4\xbf&\xc9R\xd5?o\xef\xc3:-UD\x8d\xc5"\x02\x03\x0b\x90\xed.\x86\xba\xd7\xf2\xa4\xfe\x97i\xbbA}{\xa9\xca\xa1\x12H\x07\xdb\xfb6-\xfb\xee\xb6^~\x01G0D\x02 ~O\x1b\x8c\xbb\x87x\xa3\xbb\xff\x04\xd8\x10Cq\xc8Y\x0f;N6\x97\xd8S\xfeti\x80\xb3\x12\xe0>\x02 l\x93=\x02m\xb4<\x90\xf4%\xf9Z${\xb7\xecO\x19\x15\xa3\xa3S\xf2Q\x81\xdcX\xfb\xd5&\x9e\xc5\x01\x01\x05iR!\x02"\x821\x12\xe5\xcc\x88K\x91\x16\xcb!B\x0c\xc7\x92\x98$\xcd/\xe8\xb7#[\xf9\x92\xe8\xae\xde\x14l"!\x02\x83\xcdG\xe5Sm\xcby\xe7\x11\x830\xe8\xe4\x80B\x12\xf6\x96\x19\xf1\xd6\xec\x99\r\xc75\xef\xb9\xce\xc5t!\x03\x0b\x90\xed.\x86\xba\xd7\xf2\xa4\xfe\x97i\xbbA}{\xa9\xca\xa1\x12H\x07\xdb\xfb6-\xfb\xee\xb6^~\x01S\xae"\x06\x02"\x821\x12\xe5\xcc\x88K\x91\x16\xcb!B\x0c\xc7\x92\x98$\xcd/\xe8\xb7#[\xf9\x92\xe8\xae\xde\x14l"\x1c\x02\x08\xcbw0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00"\x06\x02\x83\xcdG\xe5Sm\xcby\xe7\x11\x830\xe8\xe4\x80B\x12\xf6\x96\x19\xf1\xd6\xec\x99\r\xc75\xef\xb9\xce\xc5t\x1c&\xbb\x83\xc40\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00"\x06\x03\x0b\x90\xed.\x86\xba\xd7\xf2\xa4\xfe\x97i\xbbA}{\xa9\xca\xa1\x12H\x07\xdb\xfb6-\xfb\xee\xb6^~\x01\x1cs\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01iR!\x02\xad!\xd9\xad(\xab\x99\xac~\xdf\xd9\x1e"!O\x11YS\xab\t\xd1\xd5X\x10\x92\xfbG\xbd\xa5\x92r\xfe!\x03\xa0};\xe0\xba\xd6<\x805\xd2\x1c\x97\xb4\x10\x89\r=:\x19\xd2\xe4\x03\xaf\xb3\xfc\xfch&\xaa&\x02\xb8\xf4\xcfb\xbc\xc6\xa7\xa2kS\xae"\x02\x02\xad!\xd9\xad(\xab\x99\xac~\xdf\xd9\x1e"!O\x11YS\xab\t\xd1\xd5X\x10\x92\xfbG\xbd\xa5\x92r\xfe\x1c\x02\x08\xcbw0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x01\x00\x00\x00\x00\x00\x00\x00"\x02\x03\xa0};\xe0\xba\xd6<\x805\xd2\x1c\x97\xb4\x10\x89\r=:\x19\xd2\xe4\x03\xaf\xb3\xfc\xfch&\xaa&\x02\xb8\xf4\xcfb\xbc\xc6\xa7\xa2k\x1c&\xbb\x83\xc40\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00' + + # Native Segwit Multisig with descriptor + # wsh(sortedmulti(2,[02e8bff2/48h/1h/0h/2h]tpubDESmyzX6RMHRHvzxgLVXymd193CSYYT6C9M9wa4XK649tbAy2WeEvkPwBgoC7i76MypQxpuruUazQjqibbCogTZuENJX6YiuZ5Fy8sf7GNi/<0;1>/*,[8cb36b38/48h/1h/0h/2h]tpubDESTVwqbbaSoN2mPq7tcWkPBpRBkaEADrUzUhRTVnNef6oVn6w2PHL4zoUjUAJSPLJQRBetkgX4sDRcoaCyFHxqHGyWyaiV8REKDkh7zQac/<0;1>/*,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/<0;1>/*))#jpxgnm0a + # tb1qv5msefky74hy2q3jtws7dn4cwjlzhtuhm4vdkvsrfar63adxs0tqj22u6n + DESC_P2WSH_PSBT = b'psbt\xff\x01\x00\xa8\x02\x00\x00\x00\x01\'\xc8\x13r\x92m\x18\xc4\x9a\x98\xfe\xca\xf3F\x11c5\xc7\xb71:pz\xc8\xddbO\xd0d\xd3vi\x01\x00\x00\x00\x00\xfd\xff\xff\xff\x03\xa0\x86\x01\x00\x00\x00\x00\x00\x16\x00\x14\x81@~\xa4-\xd4\xf1\xe3\x8c\xff\xf5i\xae\xa3\xb2\xb8[\x9dS\xdc@\r\x03\x00\x00\x00\x00\x00"\x00 \xd3{\x8a\xe1\xa4I`\x9e~o\xb0\x92\x88;\x05\xd4\xffu \xa7\x85xL\x8a\x8bf\xfcM\xdc\x05\xe7_\xc8\xbd\t\x00\x00\x00\x00\x00"\x00 y\xb6\xed,\xc2\xd7\xc2\xae\xf6\xcd\xe4\x9d{\x8f?~\xfc\x81\xa5\x8c\x9b\xdf1:M\xef\x83\x07\xeb\x11\x93\xa6>\x06\x01\x00O\x01\x045\x87\xcf\x04\xba\xc1H9\x80\x00\x00\x02\x1dO\xbe\xbd\xd9g\xe1\xafqL\t\x97\xd3\x8f\xcfg\x0b\\\xe9\xd3\x01\xc0D\x0b\xbc\xc3\xb6\xa2\x0e\xb7r\x1c\x03V\x8e\xa1\xf3`Q\x91n\xd1\xb6\x90\xc3\x9e\x12\xa8\xe7\x06\x03\xb2\x80\xbd0\xce_(\x1f)\x18\xa5Sc\xaa\x14s\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80O\x01\x045\x87\xcf\x04II\x11\x9b\x80\x00\x00\x02!b@\xe6\xb4\xfd`\xba^x!\x05\x06vc\xf3\xce\x17\xfc\xaf\x89;2Wk\x14\x05\xfb\x8f\x7f\xbc\xf3\x02\xe6\xa6\x9e\xe0i|^\x8e\xd6X\x93T\xdc\xf7\x9aKE\x90\xe5\x805m\xd7\x87\x1a\xe3\xac:p@\x1f\x0f\x14\x02\xe8\xbf\xf20\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80O\x01\x045\x87\xcf\x04H\x89\xb3(\x80\x00\x00\x02x\x087\xf3\xb3\x13+\xc3dW2rH\xe4\xaauW\xc1p^#\xcaQ\x1a2\xd0guibb2\x03jUu5\xb0U\xa8\x19w\xdeXmsN,\xa1[\xd8T\xbd\xaaZ\x03|\xfeL\x00KJ\xe4\xdf\xb1\x14\x8c\xb3k80\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x01\x01+@B\x0f\x00\x00\x00\x00\x00"\x00 e7\x0c\xa6\xc4\xf5nE\x022[\xa1\xe6\xce\xb8t\xbe+\xaf\x97\xddX\xdb2\x03OG\xa8\xf5\xa6\x83\xd6\x01\x03\x04\x01\x00\x00\x00\x01\x05iR!\x02Bi\x80\xf9J\x9c\xbd:\xeeJ\x99\xa8\x04$\x1c\x98\xe9!\xc8Q4 \xe9f3\'\xffbtAc\n!\x02\xfcV-\xf5\xday-\x1a!\xb2\xac\x82\xe5\xe2\xd6\'u\x01\xf9Z\xf3\xdaZ\r\xac\xa4v\xb8@\xd6,w!\x03\x0b\x90\xed.\x86\xba\xd7\xf2\xa4\xfe\x97i\xbbA}{\xa9\xca\xa1\x12H\x07\xdb\xfb6-\xfb\xee\xb6^~\x01S\xae"\x06\x03\x0b\x90\xed.\x86\xba\xd7\xf2\xa4\xfe\x97i\xbbA}{\xa9\xca\xa1\x12H\x07\xdb\xfb6-\xfb\xee\xb6^~\x01\x1cs\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00"\x06\x02Bi\x80\xf9J\x9c\xbd:\xeeJ\x99\xa8\x04$\x1c\x98\xe9!\xc8Q4 \xe9f3\'\xffbtAc\n\x1c\x02\xe8\xbf\xf20\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00"\x06\x02\xfcV-\xf5\xday-\x1a!\xb2\xac\x82\xe5\xe2\xd6\'u\x01\xf9Z\xf3\xdaZ\r\xac\xa4v\xb8@\xd6,w\x1c\x8c\xb3k80\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01iR!\x03\x12q\x1a\x15(m\xd0<\'G\xfb\x13\xb5h\x9aq$\xbcE11\xfc\x13D\xce\xc7\xfa\xc4\x82\xe9\x1b\xdd!\x03,\xc4Rd\x176!k\xcaX\x9c\x0c\xc1\x018\xddWsSu3\x98\xb3\x12\x9c\x15\x1a\xf4\x7f\xf96\x92!\x03h\x95r\xe2\x8b\x0f\xed\xa9\xd6\x98\x1c\x027\xd9\xe5\xde\xdb\xfe\xc1m\xe7\x14?h\n\x02\xed]\x15\x9fu\x87S\xae"\x02\x03h\x95r\xe2\x8b\x0f\xed\xa9\xd6\x98\x1c\x027\xd9\xe5\xde\xdb\xfe\xc1m\xe7\x14?h\n\x02\xed]\x15\x9fu\x87\x1cs\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x01\x00\x00\x00"\x02\x03,\xc4Rd\x176!k\xcaX\x9c\x0c\xc1\x018\xddWsSu3\x98\xb3\x12\x9c\x15\x1a\xf4\x7f\xf96\x92\x1c\x02\xe8\xbf\xf20\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x01\x00\x00\x00"\x02\x03\x12q\x1a\x15(m\xd0<\'G\xfb\x13\xb5h\x9aq$\xbcE11\xfc\x13D\xce\xc7\xfa\xc4\x82\xe9\x1b\xdd\x1c\x8c\xb3k80\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x01\x00\x00\x00\x00\x01\x01iR!\x02*\xd3\x9d\'a\x88\xa4U\xbb\x00\xcf\xd1s\xce\x1az\x8b\xae6\xf0\xebx\xe8\x02\x9eD\xfe\xc1\t:\xc5?!\x03\xa0};\xe0\xba\xd6<\x805\xd2\x1c\x97\xb4\x10\x89\r=:\x19\xd2\xe4\x03\xaf\xb3\xfc\xfch&\xaa&\xa9Sb\x8eWH\x14\x7f^6o\xd37\xe9\x12\x07\xdcS\xae"\x02\x03\xa0};\xe0\xba\xd6<\x805\xd2\x1c\x97\xb4\x10\x89\r=:\x19\xd2\xe4\x03\xaf\xb3\xfc\xfch&\xaa&\xa9Sb\x8eWH\x14\x7f^6o\xd37\xe9\x12\x07\xdc\x1c\x8c\xb3k80\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x01\x00\x00\x00\x00\x00\x00\x00\x00' + DESC_P2WSH_PSBT_B43 = "3/6JFP0WH4Z9X7CPKDVO$8*3DH$NZCPN-$*3TN/E/KJNHYTO:W07H:D0BZAF74VNH+GPE2YELQ$X3F.5XI9F$BJ$25I1U.82XUY1HPDK/US8NGLTF.3JII1**LKAF9GQ5ZOUP0R.C127IUN6B7Z7US6M$8FN6/:*4PFJ//R3B2.NT3ITZO+7N24RQJW.XR9S5HU9SQX+5V+7Y/5T5LLMN3-64CIX:/+99881O9TCG*YH*PDT3+MJ9TI3MNM0GWAIEZVW*B*L6ZTYPHOCDO1+HHK*I+1YO.IVWBC:0585+J:DCG.**86V-EVV-NHS*/GPL/O*HQA11ZW7MH9-C:PM++$980X.YAMKQE7CSTUXVZNXFBARLIJPZ/KY0OG2:K:./1:LX-Z-DELF7$WQKJ07EPGC6F9DA4O8M9/RC6F.6BI2P2.1HKO2K3UMVV*85S-$MYK$60IP3E+J5E07L03TWZKIZ26R-G2:92Q24MZB*2FURCVC8I41B5OGFFZ/*5X7P1GQ*RGI65K1NO2-9-KXI3Y9.D.32A2ZKAXJF$*JP$OK8PGH38*+S:ZZO+S$ZJLRZ.VNSDR$T3G6KE-6FH+:W7*U921QCUI4G7Q6OMVKXN:XAI-T.TC4::5AEGYJKPWYCZ/KPRP4KMNHP4J4/C9Y/AX8JC:KFTJ3S5Z:/B:F9$Y1Y26GOF+6EEMYN.0LKWTKKQZ+:WCM-8IALYPY5H.KP8+6UDCD42W1N*0035.M*T4Y11ZL3BWFK06+79+Q1383T.H2AL8E-/3$4X$P*-PX/Y8DPLFTF8V$FAKBWMGJ4+:KNN4K7WWU0$EC+9ZQD2LFOP4GXN3/P-01-95QDT:V-M9$8:VQGA*1DM6QITM9I/SLTXYIQRQT5FV9VR6-B.HYAW9K-8+BD$$49+-ZEEXQJ:ULTAX5KB5VLXTN-YG77MJUEL.87-.EFWE10R8MGEE5I3*1C2F6I3*:BAPR17HF6DK7BBGVYG1:A514W:1+X*A8IZETH-GQ$APGF+XLF:3U:8GACB-S-$B.V1SK7B:TKYPF5BDG$JZ4A:SZ.F5XRQXH+H.:K948+VTDTASOJIP7XLHYFMXGL56US//B/I+HG/+.5$NOKLI3AIH/XO*HNXLG20RA60M7GU$VA22WC-B*3MHBTCHB170YB952IDBI2/0UB*MPS44$.V*863ZR:7PJ6:/*0XSG$M*QMIK*MZ2:970S0LB2TV5FP2J.B.M2PGKKH$4E94TCAT**HK78XO/0$RJ8.DBW/GT8-//P$QJ$1YA2YY$516:M$SKLBJO.GZOWG*LWUXIGJ5HI0IT/88HMDFT7P8WF9N$CD42-OKKQ+OHRNW$3$-X$$J-GG+9VFOLPOZ2G37ELDLR9U:$7+V-B$PV*N82AJ3K8O24NZ$Y*RQ0EW.33:D.+AFY85M4RQBJRRU38PO*VQZDAM9ZPBY..9*DOTH6IT/3UWUC*1:UVKCX/-3A7P/FVU$F.PZ8K71JZF*IYY.PMF4P2Y$UPLNRT*D7K4GE3SHC0OZDS7A0F7THR-COAA6LBX$4DM8+P4NL-NTH:7:A$OC08AG2MHE0CU6HTL52C2O/OWG/XRV+CHNCJEEEZQLJJ-AO3YJVLK:B9L4/1HTC1/0W.:+R8QEA7Q.TRH1H6D66JE$M8EMZXZ0MYE729E.-7CN020Q4C+YS0H/7MZ5K$0O0/PW6R4CMGYSJNSI:N+G2/.H4XQCQI2J221US22CPO18-K/3WO*D.:W::ZQ5S2+$HZPH-VX+C6RZD0.CTD/R9OIB7Y6NW+*K/R/CZN4VDV:-HA25I7CJR*0T0KZ0PZT+YIUP:Z81FIAN6W*S-I4YQ/D.Z2U3XHSY$3G4RRJL+G$M4OQESTFTJN.S/GNQKI:+.*43D/SKKGN2-59O3/YBSZ0./QXW**O.D5-E85SYXARPXUVWUQ/41EXU-QP587RGJ2PFP23GJ+6.NS-LN:R5GCDVN1L--0KV:Y00L6-J0-R+6/JE17K3N5IWPOS8FTG/3N381678BT1ZG86MFT/4VTL:/BX-N/QF:BG1PQLMNMLWD3*42E1.FSWSX29H*HJEBXP7O7GQOK$KN-JCT5B6YQD0IOV198W7:ZXP5" + DESC_P2WSH_PSBT_B58 = "3jx2X8ZwCij2rNtfMg7mupXikVogYxqSzch7asET73FeLCVjUDuMSCmyaT7QYNGSpubqL1hGQVrB7d3m7bq5DeMEg6ndo3hSMvp7P9qrB71vQuJLogN8yrD7dFuQVhgGVCC7bHR7GjaWBEPS33rN8HycsqZxqaYR9KpVehrXRb6RUdrZ9TYuwDA1BEDuEsFmb746DPEfaFodqaRCJAo6RqKntThwN1fcXSxSu4yURrLer1aBGBvXEwX3yNMkSFignLGGjRV9uvjE8AQQSZpph39B5GDvopCUbcGDKh8EoN6NRzZmxoCgynfwzfreBBzgKtFjKfEAb3YFVVkuzUpGhtgNrvT1uAEWprFn7gqF9yHErZXUpGnsWoboFRG8zfvpcsagtx95PetX6DGrq7Z1Fz1MovpT4Z2YF7aytwuKx44pWU5765PXAfUegR7CXNkRsYpteZ5zjs2tuPK3AjAGvjKCfmidb1tbgtVmWPP2Mrc13h9ygdzpKCp6PJ3t78EGeKZFqEjxvV5BcjFdVSVG4JqMEEC3qbhn9jnVsKz1susod1YfyDAUAR7VZbvcNmzUjaCUs4hpMUZmJkZjun57Zi7h1Ua8tyJYphAMtmXqzSHLnKMpsHBhr9jpY66b16uo3FMcHULqVgeHs1mJZvEk7e3smpJBr6k1rifAkFvTyUYKpte3ytEZiyjvjGKuexLAm6QP7BVfmqTo2G2tPwj5F9VmjvRKoNXbmxACHvvRDdbVPutrAYWGBNBhq8ZfNBQy3aJEqC7gHk2mhUhVBiCYbSNRXja4fyK9vb78v2Madh67sBThaHtQH91L1Lb9jhDos1gsJvs67HpbBNcJk1KFXDvLwQJGDnXubyW8iKh3pff5qumQxk9GL6jN8MqDrCQWheRKEEsQcNVAZPZ2sK5DETMuaAbcpHvEjm484ZdZetPUM8vkZ769BSzipYXq7UKzPAzKbtSpyaavE6uSN9bETaKjBpmTeg1ftrb4Kt9GHGUwqqUUZc8KhR3ebCuGZH8CwzYAQmkFStR3oHJ3McTUrv1wSNL9QneQqEVhr2gk11TPodbQP3uWh2w6RaUfy37zAWWen4AbhzdGWjRNqikHzFYpb1nbzCdqjegS6hH1y7GV2NAfTdNMtUWhgL913T7649LCyywKQhmJLTcKtoRj1JafydzGQWMwWLc7HJKDgm5u9rcEouYiEaN6sXFmzgoihee9dCzQ5znEwPrnRVTryXhGfmZaGcbCV6S7JTFSKyPGGBroQiiXyo8XA9k18mtwF16D6rW7uZFPGswKX4zMEf2W4StPB91rjULYtCkB8PyZhKRf74kf5mV2tYLRxLHmUEsmqs73iZfb9wz6Ejk23zrhqzscEzip5U2UtN331VUEiw4cD3UVH2HqZSWQKvxRBZHuqaHty8FXo5LLWvDD691Ku8KjRL5GJbVNT7ywRxUY3XYhkvCRJLZ9xuNt6v6irbJnMDEuN9ygtDz32mWKzdJwCKwe4Ds5ZU1FWjSuwk52Z2Hw3q2rvfCvhRLBRkYFbgjC8ZSCnVakdjVe63ZH1aG71nt8FLdnwWGQa7u9KN3X7rAbaEKcTSEwjzUNdEjf8P5hsE2QdKY1X2FtXxi6cwKMzPvJna73s2mhAJBygSiJMSiEQfS4xn2rVrtTgHjj1drJ6jji3MTGj5nSzz7WoDAJYdd5orgdboqzdw9KaVrkXFoKg3QoiWhX55mHpYZcV6p976GD8EXacWgLJ2iCGWddGs2vhf1LPFs2Yc2cN4ddAhHHyGVUPuH95RobwoV14kqzqCjnAiQHmMyohmvVBUjRek6cBxpEuEixkJbAy2J4YguX3cBUafUg6UBmapY7GC938r2QWHWVHu6TtvzQNsbJT6cYiZUjDyHuiLggt15NJRe84fqDB7Ss9acefv5uhGA3UsFkbUfnsdmS3jZafEUGghVNLs74aDmn36nCDRy" + DESC_P2WSH_PSBT_B64 = "cHNidP8BAKgCAAAAASfIE3KSbRjEmpj+yvNGEWM1x7cxOnB6yN1iT9Bk03ZpAQAAAAD9////A6CGAQAAAAAAFgAUgUB+pC3U8eOM//VprqOyuFudU9xADQMAAAAAACIAINN7iuGkSWCefm+wkog7BdT/dSCnhXhMiotm/E3cBedfyL0JAAAAAAAiACB5tu0swtfCrvbN5J17jz9+/IGljJvfMTpN74MH6xGTpj4GAQBPAQQ1h88EusFIOYAAAAIdT7692Wfhr3FMCZfTj89nC1zp0wHARAu8w7aiDrdyHANWjqHzYFGRbtG2kMOeEqjnBgOygL0wzl8oHykYpVNjqhRzxdoKMAAAgAEAAIAAAACAAgAAgE8BBDWHzwRJSRGbgAAAAiFiQOa0/WC6XnghBQZ2Y/POF/yviTsyV2sUBfuPf7zzAuamnuBpfF6O1liTVNz3mktFkOWANW3XhxrjrDpwQB8PFALov/IwAACAAQAAgAAAAIACAACATwEENYfPBEiJsyiAAAACeAg387MTK8NkVzJySOSqdVfBcF4jylEaMtBndWliYjIDalV1NbBVqBl33lhtc04soVvYVL2qWgN8/kwAS0rk37EUjLNrODAAAIABAACAAAAAgAIAAIAAAQErQEIPAAAAAAAiACBlNwymxPVuRQIyW6Hmzrh0viuvl91Y2zIDT0eo9aaD1gEDBAEAAAABBWlSIQJCaYD5Spy9Ou5KmagEJByY6SHIUTQg6WYzJ/9idEFjCiEC/FYt9dp5LRohsqyC5eLWJ3UB+Vrz2loNrKR2uEDWLHchAwuQ7S6GutfypP6XabtBfXupyqESSAfb+zYt++62Xn4BU64iBgMLkO0uhrrX8qT+l2m7QX17qcqhEkgH2/s2Lfvutl5+ARxzxdoKMAAAgAEAAIAAAACAAgAAgAAAAAAAAAAAIgYCQmmA+UqcvTruSpmoBCQcmOkhyFE0IOlmMyf/YnRBYwocAui/8jAAAIABAACAAAAAgAIAAIAAAAAAAAAAACIGAvxWLfXaeS0aIbKsguXi1id1Afla89paDaykdrhA1ix3HIyzazgwAACAAQAAgAAAAIACAACAAAAAAAAAAAAAAAEBaVIhAxJxGhUobdA8J0f7E7VomnEkvEUxMfwTRM7H+sSC6RvdIQMsxFJkFzYha8pYnAzBATjdV3NTdTOYsxKcFRr0f/k2kiEDaJVy4osP7anWmBwCN9nl3tv+wW3nFD9oCgLtXRWfdYdTriICA2iVcuKLD+2p1pgcAjfZ5d7b/sFt5xQ/aAoC7V0Vn3WHHHPF2gowAACAAQAAgAAAAIACAACAAAAAAAEAAAAiAgMsxFJkFzYha8pYnAzBATjdV3NTdTOYsxKcFRr0f/k2khwC6L/yMAAAgAEAAIAAAACAAgAAgAAAAAABAAAAIgIDEnEaFSht0DwnR/sTtWiacSS8RTEx/BNEzsf6xILpG90cjLNrODAAAIABAACAAAAAgAIAAIAAAAAAAQAAAAABAWlSIQIq050nYYikVbsAz9Fzzhp6i6428Ot46AKeRP7BCTrFPyEDoH074LrWPIA10hyXtBCJDT06GdLkA6+z/PxoJqomPHYhA6S8pGLg1aITEVC5wrJQPqlTYo5XSBR/XjZv0zfpEgfcU64iAgOgfTvgutY8gDXSHJe0EIkNPToZ0uQDr7P8/GgmqiY8dhxzxdoKMAAAgAEAAIAAAACAAgAAgAEAAAAAAAAAIgICKtOdJ2GIpFW7AM/Rc84aeouuNvDreOgCnkT+wQk6xT8cAui/8jAAAIABAACAAAAAgAIAAIABAAAAAAAAACICA6S8pGLg1aITEVC5wrJQPqlTYo5XSBR/XjZv0zfpEgfcHIyzazgwAACAAQAAgAAAAIACAACAAQAAAAAAAAAA" + DESC_P2WSH_PSBT_UR_PSBT = UR("crypto-psbt", PSBT(DESC_P2WSH_PSBT).to_cbor()) + + DESC_SIGNED_P2WSH_PSBT = b'psbt\xff\x01\x00\xa8\x02\x00\x00\x00\x01\'\xc8\x13r\x92m\x18\xc4\x9a\x98\xfe\xca\xf3F\x11c5\xc7\xb71:pz\xc8\xddbO\xd0d\xd3vi\x01\x00\x00\x00\x00\xfd\xff\xff\xff\x03\xa0\x86\x01\x00\x00\x00\x00\x00\x16\x00\x14\x81@~\xa4-\xd4\xf1\xe3\x8c\xff\xf5i\xae\xa3\xb2\xb8[\x9dS\xdc@\r\x03\x00\x00\x00\x00\x00"\x00 \xd3{\x8a\xe1\xa4I`\x9e~o\xb0\x92\x88;\x05\xd4\xffu \xa7\x85xL\x8a\x8bf\xfcM\xdc\x05\xe7_\xc8\xbd\t\x00\x00\x00\x00\x00"\x00 y\xb6\xed,\xc2\xd7\xc2\xae\xf6\xcd\xe4\x9d{\x8f?~\xfc\x81\xa5\x8c\x9b\xdf1:M\xef\x83\x07\xeb\x11\x93\xa6>\x06\x01\x00\x00\x01\x01+@B\x0f\x00\x00\x00\x00\x00"\x00 e7\x0c\xa6\xc4\xf5nE\x022[\xa1\xe6\xce\xb8t\xbe+\xaf\x97\xddX\xdb2\x03OG\xa8\xf5\xa6\x83\xd6"\x02\x03\x0b\x90\xed.\x86\xba\xd7\xf2\xa4\xfe\x97i\xbbA}{\xa9\xca\xa1\x12H\x07\xdb\xfb6-\xfb\xee\xb6^~\x01G0D\x02 %W.f$%C\x19\xde|\x8b\x83J>\xb7\xcc\xdd\xcf0\xe3\x1e\xe5j\x90\x99\xa5OD"\xf5u\x85\x02 \x0b\x9a\x92\xb8\xa9\x9f[\xb9\xe4\xc5\xd6\\\x83\xc1\x8f9\xdbB?\x8b\xc0\xffW\x0b\x15\x98{\xd8\x92;\x8el\x01\x01\x05iR!\x02Bi\x80\xf9J\x9c\xbd:\xeeJ\x99\xa8\x04$\x1c\x98\xe9!\xc8Q4 \xe9f3\'\xffbtAc\n!\x02\xfcV-\xf5\xday-\x1a!\xb2\xac\x82\xe5\xe2\xd6\'u\x01\xf9Z\xf3\xdaZ\r\xac\xa4v\xb8@\xd6,w!\x03\x0b\x90\xed.\x86\xba\xd7\xf2\xa4\xfe\x97i\xbbA}{\xa9\xca\xa1\x12H\x07\xdb\xfb6-\xfb\xee\xb6^~\x01S\xae\x00\x00\x00\x00' + DESC_SIGNED_P2WSH_PSBT_B43 = "4/.G:UYEK5AMD0MHBHG13G.LD0DMSN0DAO:0IJPF8RF0JH4ZNAC:TYRA2JIO2QX:GRSJ8E:QGO0M3VZCDZMCOTMN9E63370-UTOCR31LI024JWT$REDBLL6YC*.J2R0KOHHEKJ.XHZL8J3FK2W5M402A7/FF$IL24B:UAFC1YC/4342.B$P*BLP0N35YHQ$G7V3*V:+CKF$L7V5.T1U78YG6*B20KIQBOG56QQK8KS/X2G3O6W6HS.JXETL:-5S$EON-ZZ6FO.DN03G-6HUHI:Y8FQTZ6.B/VG/R8:Z.RDX9LUY+XWUYX-T8D017HPLG+9V397ZT3:T*7ZTAYD.NW.SI:Y3GMA72:9L:7GDAEKTS-CR2:RTDE9:JJ-JMBJLAPBYLNZ1267-EI-QOR84L-QNISTKW+Y8T9QDIMF7*ZS+H4AED7EH7R01-JE9CC7E//87KCNFPA+BB277SZRI41.F/EZJ5V7WZXL$KU/BEFG1GXM17EN+EAFVL7UE7D+MIW1IWNTA$R9GTT8.V$-EMGV7FVWK.UVVE$LU81JSD7GYM+NO.2WSXG17.BUP2Z.--UKUWUMU7-PUZ32FT2VTTLXTG4A/ERGP01/.823-PNP9T$QBGS5PKO66W6VN.0KB7G+H6T8FLH0V9" + DESC_SIGNED_P2WSH_PSBT_B58 = "6U4CYuYnKrhe8dUK8v7w2Jg9m3oSVzMPucwDuvsiMsxPZSbBbHesQk5q3egGVfbe553KnFcwEFjPhRPWQ7MXPyZVinB2MPA3zeVuZHAHTphXCN3uS2TAiSpwGEAY1EWxjUywu3dPPhVDHUzz8sjiCfmvQYmXcLu9nBVG2S1v4sVWii3BvvxDmihkdSMDuoouFysRjttBB7RN9H2tDiSSNLRwWey6dMKr6hADHTXozKcgEr9R5sPhkXBmwMGwW2AbDAjaLBX3fAtDr8ExUtHBhADgE9JNZWRroRgHH9hkmG1FZkEQBGhpv1QuH8gdhonpgY7HYUXGmz1YUKuvjPybvQZrbwwGHViVNuiMvNFBsNJsyURz6U3Xv4RzQ3PwxrTkGwSW6wa4TqiLZACesQBB9taGVFEiVS7aNFAoSdRaTb5n1FsUR8rHiyPY2XsVM67ENHZ7TcGdpwsyeGL5aTm1Bu9GWKYxzFnhA7AW2ojjhZ7CT28rxg5cqCHZJWBL42ZowTiNVKRS87YqYQE7E5DHzh71jqVQ9dDPZAbec4QDnnT1LohBjsS9TWWevGYJ5dPFz93sMJyKstCjqcaqX94TP1LnQAo9" + DESC_SIGNED_P2WSH_PSBT_B64 = "cHNidP8BAKgCAAAAASfIE3KSbRjEmpj+yvNGEWM1x7cxOnB6yN1iT9Bk03ZpAQAAAAD9////A6CGAQAAAAAAFgAUgUB+pC3U8eOM//VprqOyuFudU9xADQMAAAAAACIAINN7iuGkSWCefm+wkog7BdT/dSCnhXhMiotm/E3cBedfyL0JAAAAAAAiACB5tu0swtfCrvbN5J17jz9+/IGljJvfMTpN74MH6xGTpj4GAQAAAQErQEIPAAAAAAAiACBlNwymxPVuRQIyW6Hmzrh0viuvl91Y2zIDT0eo9aaD1iICAwuQ7S6GutfypP6XabtBfXupyqESSAfb+zYt++62Xn4BRzBEAiAlVy5mJCVDGd58i4NKPrfM3c8w4x7lapCZpU9EIvV1hQIgC5qSuKmfW7nkxdZcg8GPOdtCP4vA/1cLFZh72JI7jmwBAQVpUiECQmmA+UqcvTruSpmoBCQcmOkhyFE0IOlmMyf/YnRBYwohAvxWLfXaeS0aIbKsguXi1id1Afla89paDaykdrhA1ix3IQMLkO0uhrrX8qT+l2m7QX17qcqhEkgH2/s2Lfvutl5+AVOuAAAAAA==" + DESC_SIGNED_P2WSH_PSBT_UR_PSBT = UR( + "crypto-psbt", PSBT(DESC_SIGNED_P2WSH_PSBT).to_cbor() + ) # Nested Segwit Multisig P2SH_P2WSH_PSBT = b'psbt\xff\x01\x00r\x02\x00\x00\x00\x01\x1d\xf4\'\xad\xbd\x8bv?G\xcc(F\x92\xd0\xf4\x95\x1a\xdfZ\xca\xc7#>7.\r\x12\xc9\x9e\xe3\xc1\x96\x00\x00\x00\x00\x00\xfd\xff\xff\xff\x02\xdc9]\x05\x00\x00\x00\x00\x17\xa9\x14u!\x12s7Xj\x012N\x85TI|\x93\xf1T\xcd\xcf\xe3\x87\x80\x96\x98\x00\x00\x00\x00\x00\x16\x00\x14\xe6j\xfe\xff\xc3\x83\x8eq\xf0\xa2{\x07\xe3\xb0\x0e\xdej\xe8\xe1`\x00\x00\x00\x00O\x01\x045\x87\xcf\x04>b\xdf~\x80\x00\x00\x01\xdd\\H\xf6v\x7f\x04`\x9f\xabE\xd5\xc4b\xeeej\xae\xa5$\x8eL\xa7\xed\xed\xebw$\xc2\xdc\xb4\xe5\x02\xab$\x13O{\x08pA\xa1\x8fa\x18\x9f\xeb\xe5\xda\xc6\x8c\xc5^\xf4\xd7\x9f\xbaT\xdb\x81\xfa}\x1c\xb5\x17\x14\x02\x08\xcbw0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x80O\x01\x045\x87\xcf\x04\x9d\xb1\xd0\x00\x80\x00\x00\x01\xe7\x8e\xbf\x9e\xa8y\xa6\x85N\xb3h\x9c\xc2\x83\x1eMB\xf1\xba\xdbXaovW\x9cV\xe7\xbe\xbfO\xd1\x02\x7f\xe0\xe3"7\xa1\x8b2z~\xce9\xc4\xfbq\xa6%\xe0\xc9\xfb\x9d\x06\xf2\xa2q\xdc\xba\xc5\x11\xf8hs\x14&\xbb\x83\xc40\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x80O\x01\x045\x87\xcf\x04\xba\xc1H9\x80\x00\x00\x01\xa7:\xdb\xe2\x87\x84\x87cM\xcb\xfc?~\xbd\xe8\xb1\xfc\x99O\x1e\xc0h`\xcf\x01\xc3\xfe.\xa7\x91\xdd\xb6\x02\xe6**\x99s\xeek:z\xf4|"\x9a[\xdep\xbc\xa5\x9b\xd0K\xbb)\x7fV\x93\xd7\xaa%k\x97m\x14s\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x80\x00\x01\x01 \x00\xe1\xf5\x05\x00\x00\x00\x00\x17\xa9\x14\xaf\xde\t\'\xf3\xbd\xf5\xa9\xc3\xdbH;\xb8L\x93\xa5$\x96\x7f>\x87\x01\x04"\x00 \xe7\xa2\x14\x15\xf9\xc7K\xd7\xe8&\x9c\xac\x05\x15\xa2\xfa\xec\xd40\xc2p\xa2R\xe6\xaam\x15-\xec\x8e\x90\xe9\x01\x05iR!\x02g\xeaEbC\x93V0~xo\xaf@P70\xd8\xd9Z :\x0e4\\\xb3U\xa5\xdf\xa0?\xce\x03!\x03f rK\xb0\x8d\xa8v\xf7\x08\x95F\x00:\xc0\\y\xd7\xee\x9a\xca\xbc\xde\x08\x846xN3\x7f\x13\xed!\x03\xa7RPg\xdbg\'\xd8#\xc2fC\x12#\xa7\x03i\x92\xb6JR\xd5\xdbJ\xd3\xea\x9a\x8c\xa1\x00\x89\xb0S\xae"\x06\x02g\xeaEbC\x93V0~xo\xaf@P70\xd8\xd9Z :\x0e4\\\xb3U\xa5\xdf\xa0?\xce\x03\x1cs\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00"\x06\x03f rK\xb0\x8d\xa8v\xf7\x08\x95F\x00:\xc0\\y\xd7\xee\x9a\xca\xbc\xde\x08\x846xN3\x7f\x13\xed\x1c&\xbb\x83\xc40\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00"\x06\x03\xa7RPg\xdbg\'\xd8#\xc2fC\x12#\xa7\x03i\x92\xb6JR\xd5\xdbJ\xd3\xea\x9a\x8c\xa1\x00\x89\xb0\x1c\x02\x08\xcbw0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00"\x00 d\x1c\x13\xabNQ\x92\x9a\x8a\xbf\xa1U\xe8\xb4#\xb8\xcd;B\xfd?m\x87\xd6U\xcfMQ\x03\x85\xbf\x04\x01\x01iR!\x027\xda5Ru8\x1e\xbb\xbf\x98d\xaa\xd6~\x03\x99\xd8\x9f\xcbW~\x9c\xe6\xb0h\x02\x94\xf3\x86\xb3e\x0c!\x02\xfa\x11*\x9bu\xe6F\xdf:\xd8\x01E{"{\xa7Y\xbc\x03\xe5{s8\xfa9\xa8\xd46\x00\x9f\x83\x81!\x03\xf3\x14U\xfcF\x87\x897>\x8d\xcb\x07\xc0\xa61\x1b/ w\x064\xed\x1e\x95H\x04M\xa2\x13d\r\xd4S\xae"\x02\x027\xda5Ru8\x1e\xbb\xbf\x98d\xaa\xd6~\x03\x99\xd8\x9f\xcbW~\x9c\xe6\xb0h\x02\x94\xf3\x86\xb3e\x0c\x1c\x02\x08\xcbw0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x80\x01\x00\x00\x00\x00\x00\x00\x00"\x02\x02\xfa\x11*\x9bu\xe6F\xdf:\xd8\x01E{"{\xa7Y\xbc\x03\xe5{s8\xfa9\xa8\xd46\x00\x9f\x83\x81\x1c&\xbb\x83\xc40\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x80\x01\x00\x00\x00\x00\x00\x00\x00"\x02\x03\xf3\x14U\xfcF\x87\x897>\x8d\xcb\x07\xc0\xa61\x1b/ w\x064\xed\x1e\x95H\x04M\xa2\x13d\r\xd4\x1cs\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x80\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00' @@ -94,13 +126,90 @@ def tdata(mocker): SIGNED_P2SH_P2WSH_PSBT_UR_PSBT = UR( "crypto-psbt", PSBT(SIGNED_P2SH_P2WSH_PSBT).to_cbor() ) + SIGNED_P2SH_P2WSH_PSBT_SD = b'psbt\xff\x01\x00r\x02\x00\x00\x00\x01\x1d\xf4\'\xad\xbd\x8bv?G\xcc(F\x92\xd0\xf4\x95\x1a\xdfZ\xca\xc7#>7.\r\x12\xc9\x9e\xe3\xc1\x96\x00\x00\x00\x00\x00\xfd\xff\xff\xff\x02\xdc9]\x05\x00\x00\x00\x00\x17\xa9\x14u!\x12s7Xj\x012N\x85TI|\x93\xf1T\xcd\xcf\xe3\x87\x80\x96\x98\x00\x00\x00\x00\x00\x16\x00\x14\xe6j\xfe\xff\xc3\x83\x8eq\xf0\xa2{\x07\xe3\xb0\x0e\xdej\xe8\xe1`\x00\x00\x00\x00O\x01\x045\x87\xcf\x04>b\xdf~\x80\x00\x00\x01\xdd\\H\xf6v\x7f\x04`\x9f\xabE\xd5\xc4b\xeeej\xae\xa5$\x8eL\xa7\xed\xed\xebw$\xc2\xdc\xb4\xe5\x02\xab$\x13O{\x08pA\xa1\x8fa\x18\x9f\xeb\xe5\xda\xc6\x8c\xc5^\xf4\xd7\x9f\xbaT\xdb\x81\xfa}\x1c\xb5\x17\x14\x02\x08\xcbw0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x80O\x01\x045\x87\xcf\x04\x9d\xb1\xd0\x00\x80\x00\x00\x01\xe7\x8e\xbf\x9e\xa8y\xa6\x85N\xb3h\x9c\xc2\x83\x1eMB\xf1\xba\xdbXaovW\x9cV\xe7\xbe\xbfO\xd1\x02\x7f\xe0\xe3"7\xa1\x8b2z~\xce9\xc4\xfbq\xa6%\xe0\xc9\xfb\x9d\x06\xf2\xa2q\xdc\xba\xc5\x11\xf8hs\x14&\xbb\x83\xc40\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x80O\x01\x045\x87\xcf\x04\xba\xc1H9\x80\x00\x00\x01\xa7:\xdb\xe2\x87\x84\x87cM\xcb\xfc?~\xbd\xe8\xb1\xfc\x99O\x1e\xc0h`\xcf\x01\xc3\xfe.\xa7\x91\xdd\xb6\x02\xe6**\x99s\xeek:z\xf4|"\x9a[\xdep\xbc\xa5\x9b\xd0K\xbb)\x7fV\x93\xd7\xaa%k\x97m\x14s\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x80\x00\x01\x01 \x00\xe1\xf5\x05\x00\x00\x00\x00\x17\xa9\x14\xaf\xde\t\'\xf3\xbd\xf5\xa9\xc3\xdbH;\xb8L\x93\xa5$\x96\x7f>\x87"\x02\x02g\xeaEbC\x93V0~xo\xaf@P70\xd8\xd9Z :\x0e4\\\xb3U\xa5\xdf\xa0?\xce\x03G0D\x02 \x1f\xa0f\x1ct\xd6\xb9S\xbd\xc4"\x0cY\x19\xe0\xe4p\xdc\xe8qR\xc8$\xf4Hf\xa6\x07\x8e\xda\x16b\x02 W;\xfb\xc0\xbaWo]/\xcc\xd8\xdb\xe8\xc85\xee\x9bx\x1c\xea\xba\xf4[vM\xac\xc2\x11\xae-G\xc7\x01\x01\x04"\x00 \xe7\xa2\x14\x15\xf9\xc7K\xd7\xe8&\x9c\xac\x05\x15\xa2\xfa\xec\xd40\xc2p\xa2R\xe6\xaam\x15-\xec\x8e\x90\xe9\x01\x05iR!\x02g\xeaEbC\x93V0~xo\xaf@P70\xd8\xd9Z :\x0e4\\\xb3U\xa5\xdf\xa0?\xce\x03!\x03f rK\xb0\x8d\xa8v\xf7\x08\x95F\x00:\xc0\\y\xd7\xee\x9a\xca\xbc\xde\x08\x846xN3\x7f\x13\xed!\x03\xa7RPg\xdbg\'\xd8#\xc2fC\x12#\xa7\x03i\x92\xb6JR\xd5\xdbJ\xd3\xea\x9a\x8c\xa1\x00\x89\xb0S\xae"\x06\x02g\xeaEbC\x93V0~xo\xaf@P70\xd8\xd9Z :\x0e4\\\xb3U\xa5\xdf\xa0?\xce\x03\x1cs\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00"\x06\x03f rK\xb0\x8d\xa8v\xf7\x08\x95F\x00:\xc0\\y\xd7\xee\x9a\xca\xbc\xde\x08\x846xN3\x7f\x13\xed\x1c&\xbb\x83\xc40\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00"\x06\x03\xa7RPg\xdbg\'\xd8#\xc2fC\x12#\xa7\x03i\x92\xb6JR\xd5\xdbJ\xd3\xea\x9a\x8c\xa1\x00\x89\xb0\x1c\x02\x08\xcbw0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00"\x00 d\x1c\x13\xabNQ\x92\x9a\x8a\xbf\xa1U\xe8\xb4#\xb8\xcd;B\xfd?m\x87\xd6U\xcfMQ\x03\x85\xbf\x04\x01\x01iR!\x027\xda5Ru8\x1e\xbb\xbf\x98d\xaa\xd6~\x03\x99\xd8\x9f\xcbW~\x9c\xe6\xb0h\x02\x94\xf3\x86\xb3e\x0c!\x02\xfa\x11*\x9bu\xe6F\xdf:\xd8\x01E{"{\xa7Y\xbc\x03\xe5{s8\xfa9\xa8\xd46\x00\x9f\x83\x81!\x03\xf3\x14U\xfcF\x87\x897>\x8d\xcb\x07\xc0\xa61\x1b/ w\x064\xed\x1e\x95H\x04M\xa2\x13d\r\xd4S\xae"\x02\x027\xda5Ru8\x1e\xbb\xbf\x98d\xaa\xd6~\x03\x99\xd8\x9f\xcbW~\x9c\xe6\xb0h\x02\x94\xf3\x86\xb3e\x0c\x1c\x02\x08\xcbw0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x80\x01\x00\x00\x00\x00\x00\x00\x00"\x02\x02\xfa\x11*\x9bu\xe6F\xdf:\xd8\x01E{"{\xa7Y\xbc\x03\xe5{s8\xfa9\xa8\xd46\x00\x9f\x83\x81\x1c&\xbb\x83\xc40\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x80\x01\x00\x00\x00\x00\x00\x00\x00"\x02\x03\xf3\x14U\xfcF\x87\x897>\x8d\xcb\x07\xc0\xa61\x1b/ w\x064\xed\x1e\x95H\x04M\xa2\x13d\r\xd4\x1cs\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x01\x00\x00\x80\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00' MISSING_GLOBAL_XPUBS_PSBT = "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriIGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GELSmumcAAACAAAAAgAQAAIAiBgPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvRC0prpnAAAAgAAAAIAFAACAAAA=" + # WSH_MINISCRIPT = "wsh(or_d(pk([73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/<0;1>/*),and_v(v:pkh([02e8bff2/48h/1h/0h/2h]tpubDESmyzX6RMHRHvzxgLVXymd193CSYYT6C9M9wa4XK649tbAy2WeEvkPwBgoC7i76MypQxpuruUazQjqibbCogTZuENJX6YiuZ5Fy8sf7GNi/<0;1>/*),older(65535))))#466dtswe" + # tb1qrtvmveqwrzsndt8tzzkgepp2mw8de95dk7hz70kr95f5dt7axvrsgq8lcp + MINIS_P2WSH_PSBT = b'psbt\xff\x01\x00\xa8\x02\x00\x00\x00\x01\x96Y\x17$\xc8\xc8p\x00\x1e\x981\xe1\x14\xc9\nV7\x0c\x10\xff\xc2\xf6-\xbcyP\x05h\x88q\x9c\x9d\x00\x00\x00\x00\x00\xfd\xff\xff\xff\x03 \xa1\x07\x00\x00\x00\x00\x00\x16\x00\x14V\x911\x9a\xfbg\x8b\xbfC\xad&\x8cE\xa8\x8a\x88\xcd\x8dOk@B\x0f\x00\x00\x00\x00\x00"\x00 \x9f\xef\xdeW\x0e\x8a}\xabP\xf15\x8e\x1df\xcdz\xba\xf6\xb2\xfb\x04\x85\xffu\xda\xcd\xdb\xd3\x8f\xdc\r\xf3\xab\x8d\x07\x00\x00\x00\x00\x00"\x00 \xdffN{{P\x15\\\xc0s\xeb\xa4n[\xfd(G\xe8T\x84\x13\xa4Eb\xf2\xbc\t\xcd\xa7/\x1cj\x00\x00\x00\x00\x00\x01\x00\xa2\x02\x00\x00\x00\x00\x01\x01O#\x9f\xea\xd3\xa8\x1f4|WJn=Bt\xeaV/\xdfR\xcb\xc7\xce^\xfb:\x88\xd4\x020c&\x01\x00\x00\x00\x00\xfd\xff\xff\xff\x01\x06y\x1e\x00\x00\x00\x00\x00"\x00 \x1a\xd9\xb6d\x0e\x18\xa16\xac\xeb\x10\xac\x8c\x84*\xdb\x8e\xdc\x96\x8d\xb7\xae/>\xc3-\x13F\xaf\xdd3\x07\x01@?\xd8Y\xef$\xc2\xc61\x90\xce\xa2\x11\xc7\xfe\xed\x16\x9a\x14}\xc9S\xe9\xc5)s\xbf\x80U\x17\xdbp\xe7\x803\xe9l\xd08\xb2\xaa\x13D\x9d\xff\xe6*5<\xb2YuF~O\xaf}\xc1\xf0,6\x1ca<\xe4\x00\x00\x00\x00\x01\x01+\x06y\x1e\x00\x00\x00\x00\x00"\x00 \x1a\xd9\xb6d\x0e\x18\xa16\xac\xeb\x10\xac\x8c\x84*\xdb\x8e\xdc\x96\x8d\xb7\xae/>\xc3-\x13F\xaf\xdd3\x07\x01\x05D!\x03h\x95r\xe2\x8b\x0f\xed\xa9\xd6\x98\x1c\x027\xd9\xe5\xde\xdb\xfe\xc1m\xe7\x14?h\n\x02\xed]\x15\x9fu\x87\xacsdv\xa9\x14Pf\xd5\xb4\x9de\xad\xceB\xdd\xc8\x8f\x05\xf1D?\xec\xa9\x90\x8a\x88\xad\x03\xff\xff\x00\xb2h"\x06\x03,\xc4Rd\x176!k\xcaX\x9c\x0c\xc1\x018\xddWsSu3\x98\xb3\x12\x9c\x15\x1a\xf4\x7f\xf96\x92\x1c\x02\xe8\xbf\xf20\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x01\x00\x00\x00"\x06\x03h\x95r\xe2\x8b\x0f\xed\xa9\xd6\x98\x1c\x027\xd9\xe5\xde\xdb\xfe\xc1m\xe7\x14?h\n\x02\xed]\x15\x9fu\x87\x1cs\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00"\x02\x02q\x1f\xb6a\x95\x93\xfb\xc9\x82\xdf\x18\x9f\xd6P\xd2\x1bb\xbc\xf7#\xd3\xdd\xbdy7\xe4\t\xaej\x9b\xea\x04\x1c\x02\xe8\xbf\xf20\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x03\x00\x00\x00"\x02\x02\xb1\xa15\xd5*\x95\x1e\x01\x00\xe2\xc6L\xca\xd8\xd9\x92XM\x03Q\xb9\xda{}N\xe2\xf1\xca\xd1&\xa8\x04\x1cs\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x03\x00\x00\x00\x00"\x02\x02|n:1\xc3+\xcb\x9e\xa2\x05ds")l[\xa3i6?\x13n\xf8\xeeZ\xcb!\xc8\xf9\xd5\x1eP\x1c\x02\xe8\xbf\xf20\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x01\x00\x00\x00\x01\x00\x00\x00"\x02\x02\xf6l0\xa0}\xbc\xfda\xe7\xc5\xb6\xfa\xcfNl\'\xf9G\xba\xfa\xcb\xb3\xfc\x0e\xa2\x81\xe0\x16~n\x94\x8d\x1cs\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x01\x00\x00\x00\x01\x00\x00\x00\x00' + MINIS_P2WSH_PSBT_B43 = "75W:GGD-BWMM*4TP9NAF77L:6V/PUOLESPFPQKQ57CC5ELTGF:IWI89XQDFGV.VM2.-S*0PRBXHCPG8$52N:X.MU2Z29SS11P67*86UF8VKH05CL7GB5BRT:7O*VSFVEA$5Q0NHIQ+F+W2R.9LLY26K9IM*LWSAN3797LD8RP.1NPFJO:Q9Z+7TW.74-NW95D$6M1Q+FVC9PFF:LIL59I-PJYPPG6B58LYGOY.CC9X+DDN9*W1GQ02.3YDQT4H/3E.JV+ZSFC8P0Q8HV8BY7*VLG5RI/Z+301:T2G56ODNRAN$*RX:N$9YP1::D0*V96PZX7W/L3VI8UPP4ET:::ZJ4-F84FOTK7FP25:H+Y+2:48LLR-E74EHPB435$X1P6JZ**C*84Y8UIOVP*/VL:UG9ID2Q.66G$ON70V7KJ9K15ZMFMFEY3ADI0T7NEFT81AD8/1SIT5Y$ZZ3JB*AXVCPB3/0UAX8-D8M1SWD7L.I2-+JW8QPA$5J:6BNVZW1BIU4OUFJE131SX68GV/ZE6E:E.Y.6Q-JVPP.S2GDXNEXNR2GL4L+7I6X66CW3G1KUF6XL--5$AA74+R.-DC$0E5PHHB*CDN+Y-6*N66.T79*9N$1*9EGMWJSZGMKK7PBY-QF6Y-XRPIZ$$2*MVRG5*:DACDHHU/N725G:$OPTU8Q1XJH9C*0/TTPY67P-JM-432EC+1+TK:J4JGH24:*J:QFRE3VP.YF71.KLG-.9D-H6L+UZ4J3DFJK*BXPIWUKQUTGLKEY3XVXDEV+$PI0VH+NNQ1.QQ1LDVJO-OLA9H7*072I25H99+8745AWL2AET$Z:+USI*R$W+AH54B.D1YFO5421NGLTSQZ:+ZP2-YW*72YCB$E$F37$++9*X.BU/W1-M:OT0-19EW4ILXCD+0W*9$$ZUF49HX2BETK:9RC9XZZO2IKA.HQ9KSB2SPWM/NEJ3L9K++*0AZC*EIYQ0JD9V3C8J93HPLSG795DB$D:IJBQS.C4.LG-OCY0FOSGEB8O*VK*98LQVWMAXQNR/RUQDM5YJLMKNHAZ2VW$CZ5+93WJ4C.AH-*WISRKMN:SE.L*ZU/I9N2RR+71W:B0IQU2/5R6N.6W2TF+68.ZTPBOJZUL1FL*PA-J0/1L0-O.XDAG:65BS49/S933SNXT/4.*8Q779GOHHE8.53V39WKO1VE*.ZXV$$IMZG*WT6UI+A.72MH+-.8-LVOJHRKHOP:U69IG.H2WJ7:" + MINIS_P2WSH_PSBT_B58 = "8vsnQ1aCezSMDDvQnutdZoPfBE3jeAtaVyAaJ2MFA3okEUQwcUsgJKkSo97SQe8yns793CxoYrMB7yDs2UFGvLJP4xwFaod14K14By9ByDVkSvKhka27fTSJd5aU4arYaUugGDcYVACZR1ZdK5k2bAPtjneKhq6CU7cqV9mz3ZRHskKw4zonXfMPZkfcwPxoYoW83aj2YbNaCmLyLvKnX3oJuniExGkHnBGkxjLNYyiHMZ17eTCzwactEEjgnbgRtHFdQv8sYQf6r6jjZsZ8UeLbKYoR43TFg27nz9AGhNQvA4KSH7wAm1r5x2WQh6oae9pRVKfQFKxzRAdqoYA1DAjXtWbqDXZ5wxhqqrFxBtns8kGjGqjmgvJarjGkjrXjQ9YpQRtrU8fzkVjfD8yMRZ8m1xeznQ3G5XCYN9XDkXTJ9hmLiK1qhtvU7thTZ1rFeP9rGgJbbrV1KWdd7s6G1BZfacSzWapoMgKUBZurcTPH5GD7BmTug5VK8bmyvQzn7FDrLXGwdDzHiVcBqfbHFTHgPmemNYRgjFvHC3v6MMd2rim3TBWCTJmo38SHmR4nJuZUy1JqyLnHndN5yJo5uhTwL54r68ReUw5uj8yjXdbszWqWPm4uZ42uV3VC6pcfu87nbPubYue3VFEHcW579EMB4bQXb3PLsBRuqAkbWDKFpfUEvzfqbjSJSzPM6446BTFkDg1wqTinJSfbcuaMj5jGzNC2RRDTey6haNEM18nhPHWtpyfVyynKNAQbT6U4UVMGfK5P52V3U36VLmNVy2PCXciBPwsCYtPoiJ9LECz3K2n2o1xqttFeEkHrVXTeVBgCs2bBG1ithZeoyXPfxayuxNEKw2DpmoH4qBFYmKSSdz1SeRn1rHdiN1BQ4HijPXtbjKm6ak3QvNWtCsNdJGFbG8aUN8HHTxcLCRyfRpL1beDPUauVXQr76jZoQLUrV3Ep3LX27uf1tbLp7J4CV1KWuET4YPcV48dE1PMwCzJKqrt7o2ukNGHuXXdf8X2fp5Y56kGvddL3Bk96VKKxUXVd6serKdebyNcCnS7vUjov92ph1LvbZ8byDvYf6jAe43tdNmuhNcvAgY76CghBbJpfsC2TJbHXyTZWoML5vJNnEW1seG1Let6gQ6yLXt4ZPQodh" + MINIS_P2WSH_PSBT_B64 = "cHNidP8BAKgCAAAAAZZZFyTIyHAAHpgx4RTJClY3DBD/wvYtvHlQBWiIcZydAAAAAAD9////AyChBwAAAAAAFgAUVpExmvtni79DrSaMRaiKiM2NT2tAQg8AAAAAACIAIJ/v3lcOin2rUPE1jh1mzXq69rL7BIX/ddrN29OP3A3zq40HAAAAAAAiACDfZk57e1AVXMBz66RuW/0oR+hUhBOkRWLyvAnNpy8cagAAAAAAAQCiAgAAAAABAU8jn+rTqB80fFdKbj1CdOpWL99Sy8fOXvs6iNQCMGMmAQAAAAD9////AQZ5HgAAAAAAIgAgGtm2ZA4YoTas6xCsjIQq247clo23ri8+wy0TRq/dMwcBQD/YWe8kwsYxkM6iEcf+7RaaFH3JU+nFKXO/gFUX23DngDPpbNA4sqoTRJ3/5io1PLJZdUZ+T699wfAsNhxhPOQAAAAAAQErBnkeAAAAAAAiACAa2bZkDhihNqzrEKyMhCrbjtyWjbeuLz7DLRNGr90zBwEFRCEDaJVy4osP7anWmBwCN9nl3tv+wW3nFD9oCgLtXRWfdYesc2R2qRRQZtW0nWWtzkLdyI8F8UQ/7KmQioitA///ALJoIgYDLMRSZBc2IWvKWJwMwQE43VdzU3UzmLMSnBUa9H/5NpIcAui/8jAAAIABAACAAAAAgAIAAIAAAAAAAQAAACIGA2iVcuKLD+2p1pgcAjfZ5d7b/sFt5xQ/aAoC7V0Vn3WHHHPF2gowAACAAQAAgAAAAIACAACAAAAAAAEAAAAAACICAnEftmGVk/vJgt8Yn9ZQ0htivPcj0929eTfkCa5qm+oEHALov/IwAACAAQAAgAAAAIACAACAAAAAAAMAAAAiAgKxoTXVKpUeAQDixkzK2NmSWE0DUbnae31O4vHK0SaoBBxzxdoKMAAAgAEAAIAAAACAAgAAgAAAAAADAAAAACICAnxuOjHDK8ueogVkcyIpbFujaTY/E2747lrLIcj51R5QHALov/IwAACAAQAAgAAAAIACAACAAQAAAAEAAAAiAgL2bDCgfbz9YefFtvrPTmwn+Ue6+suz/A6igeAWfm6UjRxzxdoKMAAAgAEAAIAAAACAAgAAgAEAAAABAAAAAA==" + MINIS_P2WSH_PSBT_UR_PSBT = UR("crypto-psbt", PSBT(MINIS_P2WSH_PSBT).to_cbor()) + + SIGNED_MINIS_P2WSH_PSBT = b'psbt\xff\x01\x00\xa8\x02\x00\x00\x00\x01\x96Y\x17$\xc8\xc8p\x00\x1e\x981\xe1\x14\xc9\nV7\x0c\x10\xff\xc2\xf6-\xbcyP\x05h\x88q\x9c\x9d\x00\x00\x00\x00\x00\xfd\xff\xff\xff\x03 \xa1\x07\x00\x00\x00\x00\x00\x16\x00\x14V\x911\x9a\xfbg\x8b\xbfC\xad&\x8cE\xa8\x8a\x88\xcd\x8dOk@B\x0f\x00\x00\x00\x00\x00"\x00 \x9f\xef\xdeW\x0e\x8a}\xabP\xf15\x8e\x1df\xcdz\xba\xf6\xb2\xfb\x04\x85\xffu\xda\xcd\xdb\xd3\x8f\xdc\r\xf3\xab\x8d\x07\x00\x00\x00\x00\x00"\x00 \xdffN{{P\x15\\\xc0s\xeb\xa4n[\xfd(G\xe8T\x84\x13\xa4Eb\xf2\xbc\t\xcd\xa7/\x1cj\x00\x00\x00\x00\x00\x01\x00\xa2\x02\x00\x00\x00\x00\x01\x01O#\x9f\xea\xd3\xa8\x1f4|WJn=Bt\xeaV/\xdfR\xcb\xc7\xce^\xfb:\x88\xd4\x020c&\x01\x00\x00\x00\x00\xfd\xff\xff\xff\x01\x06y\x1e\x00\x00\x00\x00\x00"\x00 \x1a\xd9\xb6d\x0e\x18\xa16\xac\xeb\x10\xac\x8c\x84*\xdb\x8e\xdc\x96\x8d\xb7\xae/>\xc3-\x13F\xaf\xdd3\x07\x01@?\xd8Y\xef$\xc2\xc61\x90\xce\xa2\x11\xc7\xfe\xed\x16\x9a\x14}\xc9S\xe9\xc5)s\xbf\x80U\x17\xdbp\xe7\x803\xe9l\xd08\xb2\xaa\x13D\x9d\xff\xe6*5<\xb2YuF~O\xaf}\xc1\xf0,6\x1ca<\xe4\x00\x00\x00\x00\x01\x01+\x06y\x1e\x00\x00\x00\x00\x00"\x00 \x1a\xd9\xb6d\x0e\x18\xa16\xac\xeb\x10\xac\x8c\x84*\xdb\x8e\xdc\x96\x8d\xb7\xae/>\xc3-\x13F\xaf\xdd3\x07"\x02\x03h\x95r\xe2\x8b\x0f\xed\xa9\xd6\x98\x1c\x027\xd9\xe5\xde\xdb\xfe\xc1m\xe7\x14?h\n\x02\xed]\x15\x9fu\x87G0D\x02 +\xe4\x00\xa1\xcb\xf1\x87\x8b\xef\x07\x86\x89\x1aV\x99\x0b%3W\xfb\xe0v\xc2S\\\xaf\xc4T\x8ej\xe6p\x02 \x1d/\\\x9eS\x0e<\xff\xd0\x81UL\xd1\x15|\x1c}y\x8e?\xdc\xaa\xbc=6\xc1\xcc\x06ll\xca\xc9\x01\x01\x05D!\x03h\x95r\xe2\x8b\x0f\xed\xa9\xd6\x98\x1c\x027\xd9\xe5\xde\xdb\xfe\xc1m\xe7\x14?h\n\x02\xed]\x15\x9fu\x87\xacsdv\xa9\x14Pf\xd5\xb4\x9de\xad\xceB\xdd\xc8\x8f\x05\xf1D?\xec\xa9\x90\x8a\x88\xad\x03\xff\xff\x00\xb2h\x00\x00\x00\x00' + SIGNED_MINIS_P2WSH_PSBT_B43 = "1TFBDGI0U9N.JNF9JXZO5QI.R..ULE3SYFCOR2+C/2/N6GRF$UFW0$ONNTH.6JYIB/NLQX6H4IPU+P+BHLUB-40/ZB8IK$01$$CS3:5K6MD5R-1U7Z4Y$4QON+B9T:L3$V1OWNAAAK9PHL6:J:1FEHNJ1OV74-I*KNUCQ6G*JGP0/JC:N3Q-X-2-3+OF-:B$--I1TX8X7N+8606XP01.OV1CGGZ2R6R9+Z-KX9ZQXH-LXX75-P43.X6TO44.ARWD85RI1WHNLD903+IAV2FV6T5//T1EM9HNP*1EYDKUZN3*$U8L944W3K*Y2N0N7:AW-M:X0:PZURT:+4:ZBVOZ3$1BKQGHG1DDP8T8YO:NI7.NBVUVPN2-0-4:STS*GJM28W+B-J/T+IJTK4U.OV+MFEYLOC012A22J+L/37BE+E7PZ-2.7F:-UY/E$ZN1ZC2+P7V5FS4CIPRTDAGGCB-XBGZ7.3T7929P8$VPZQSAPU4V8/8BAR:PP/IE+DP.YWW1HJW/3JPDXE.Z90.BN91FW1HQSUVQDHL46*UXBX2BL9T23HT7F.TBS3:86MGX/I8M:YD*:+JMS$WKPWB65.T36WJP1.7A7H*K03GCUW0KGLU-H*5C6Z/0P*LQY2CL$-.P7XC1XG0HQPJM.FKGOC6S3+3KLZX:XW79I.-*4G.S6A05R:YE*6AU*UHA89R$RV.XJX5OQH:9UGKJ0X-JQ2EUTP2TBB/2B6933J.CNJW6M711I2GG-S9*8NTM*ONCBIBQ3BR87.HP17HKP*1P6$8*WRDVWUXRYRMDD9B-OG0+CZ7L3SKK0W64SUVH1-TRP8N$:.F1+6BQ0" + SIGNED_MINIS_P2WSH_PSBT_B58 = "3UCmDenEJ8kw38Fft6J4JkeeDFYGE139af8mZigLkDsjrthMUBvRfHGs8vFUB8R4LP71PWpogqs6qnCbALrfbFCntEtFGPUNLR4WPrZAe1NCDVwGKHiJfBhxZidHBW2AzLStnJ5utyBZaWG1EjbiPbnhjVNymwy83FQjHjJNr6cqYf71efwP1QXqM33cT5SiFiFw2ANxSKyJxbvWkvVexKqc46fBBkbBnhnPzh5ycnjjNL8v6vx2yzigCPNpRs1xXrPWvdCKf6dFQbuDneqS4uFuUvR1eWv7DfWDVoMZaxvvttKAoUhEELibnSJFkuQoh4637PRbRaAL4EeyRnYP4mqiKEXvTbT4Dp2mYBvW28eZ1irrXkWsLhNyRyuE54NahJpd9vVfpBpa7U5EDRReN83xSaiboWNJ8dxc6UdGAkKqYmDu5RmPAxKYfmTWmA2s1o1WWpNchgaqkYPygMR1VyV8jrbNc8jnhfzGiH9Jo2Wjc8snnUPHY7mGXMmcgvtZtVQqBgMpqfgo1p5JP94LdiTrvu6fPxA9a8GpRw2hXBjeZd1BszHXcPeVWWx8LLkCAn5BiVmRG4RFh8pSfz4yQd5VDWGvrSepRnToPDQNPAYLbQCMKMgUPf3m2uRQtRRqf5t7wcwnSAdCzYVqKScZV3SRWZM2aNZD8fDnj4XKd6TJmmBTuxe51h3odc7fJWqZju2MuJQqAV4grSwE24xk478pyfXSN7Jg9f54MucGTeGFuVFYyiTkozkLjfaXY9mB6SpCBP5wo1R" + SIGNED_MINIS_P2WSH_PSBT_B64 = "cHNidP8BAKgCAAAAAZZZFyTIyHAAHpgx4RTJClY3DBD/wvYtvHlQBWiIcZydAAAAAAD9////AyChBwAAAAAAFgAUVpExmvtni79DrSaMRaiKiM2NT2tAQg8AAAAAACIAIJ/v3lcOin2rUPE1jh1mzXq69rL7BIX/ddrN29OP3A3zq40HAAAAAAAiACDfZk57e1AVXMBz66RuW/0oR+hUhBOkRWLyvAnNpy8cagAAAAAAAQCiAgAAAAABAU8jn+rTqB80fFdKbj1CdOpWL99Sy8fOXvs6iNQCMGMmAQAAAAD9////AQZ5HgAAAAAAIgAgGtm2ZA4YoTas6xCsjIQq247clo23ri8+wy0TRq/dMwcBQD/YWe8kwsYxkM6iEcf+7RaaFH3JU+nFKXO/gFUX23DngDPpbNA4sqoTRJ3/5io1PLJZdUZ+T699wfAsNhxhPOQAAAAAAQErBnkeAAAAAAAiACAa2bZkDhihNqzrEKyMhCrbjtyWjbeuLz7DLRNGr90zByICA2iVcuKLD+2p1pgcAjfZ5d7b/sFt5xQ/aAoC7V0Vn3WHRzBEAiAr5AChy/GHi+8HhokaVpkLJTNX++B2wlNcr8RUjmrmcAIgHS9cnlMOPP/QgVVM0RV8HH15jj/cqrw9NsHMBmxsyskBAQVEIQNolXLiiw/tqdaYHAI32eXe2/7BbecUP2gKAu1dFZ91h6xzZHapFFBm1bSdZa3OQt3IjwXxRD/sqZCKiK0D//8AsmgAAAAA" + SIGNED_MINIS_P2WSH_PSBT_UR_PSBT = UR( + "crypto-psbt", PSBT(SIGNED_MINIS_P2WSH_PSBT).to_cbor() + ) + SIGNED_MINIS_P2WSH_PSBT_SD = b'psbt\xff\x01\x00\xa8\x02\x00\x00\x00\x01\x96Y\x17$\xc8\xc8p\x00\x1e\x981\xe1\x14\xc9\nV7\x0c\x10\xff\xc2\xf6-\xbcyP\x05h\x88q\x9c\x9d\x00\x00\x00\x00\x00\xfd\xff\xff\xff\x03 \xa1\x07\x00\x00\x00\x00\x00\x16\x00\x14V\x911\x9a\xfbg\x8b\xbfC\xad&\x8cE\xa8\x8a\x88\xcd\x8dOk@B\x0f\x00\x00\x00\x00\x00"\x00 \x9f\xef\xdeW\x0e\x8a}\xabP\xf15\x8e\x1df\xcdz\xba\xf6\xb2\xfb\x04\x85\xffu\xda\xcd\xdb\xd3\x8f\xdc\r\xf3\xab\x8d\x07\x00\x00\x00\x00\x00"\x00 \xdffN{{P\x15\\\xc0s\xeb\xa4n[\xfd(G\xe8T\x84\x13\xa4Eb\xf2\xbc\t\xcd\xa7/\x1cj\x00\x00\x00\x00\x00\x01\x00\xa2\x02\x00\x00\x00\x00\x01\x01O#\x9f\xea\xd3\xa8\x1f4|WJn=Bt\xeaV/\xdfR\xcb\xc7\xce^\xfb:\x88\xd4\x020c&\x01\x00\x00\x00\x00\xfd\xff\xff\xff\x01\x06y\x1e\x00\x00\x00\x00\x00"\x00 \x1a\xd9\xb6d\x0e\x18\xa16\xac\xeb\x10\xac\x8c\x84*\xdb\x8e\xdc\x96\x8d\xb7\xae/>\xc3-\x13F\xaf\xdd3\x07\x01@?\xd8Y\xef$\xc2\xc61\x90\xce\xa2\x11\xc7\xfe\xed\x16\x9a\x14}\xc9S\xe9\xc5)s\xbf\x80U\x17\xdbp\xe7\x803\xe9l\xd08\xb2\xaa\x13D\x9d\xff\xe6*5<\xb2YuF~O\xaf}\xc1\xf0,6\x1ca<\xe4\x00\x00\x00\x00\x01\x01+\x06y\x1e\x00\x00\x00\x00\x00"\x00 \x1a\xd9\xb6d\x0e\x18\xa16\xac\xeb\x10\xac\x8c\x84*\xdb\x8e\xdc\x96\x8d\xb7\xae/>\xc3-\x13F\xaf\xdd3\x07"\x02\x03h\x95r\xe2\x8b\x0f\xed\xa9\xd6\x98\x1c\x027\xd9\xe5\xde\xdb\xfe\xc1m\xe7\x14?h\n\x02\xed]\x15\x9fu\x87G0D\x02 +\xe4\x00\xa1\xcb\xf1\x87\x8b\xef\x07\x86\x89\x1aV\x99\x0b%3W\xfb\xe0v\xc2S\\\xaf\xc4T\x8ej\xe6p\x02 \x1d/\\\x9eS\x0e<\xff\xd0\x81UL\xd1\x15|\x1c}y\x8e?\xdc\xaa\xbc=6\xc1\xcc\x06ll\xca\xc9\x01\x01\x05D!\x03h\x95r\xe2\x8b\x0f\xed\xa9\xd6\x98\x1c\x027\xd9\xe5\xde\xdb\xfe\xc1m\xe7\x14?h\n\x02\xed]\x15\x9fu\x87\xacsdv\xa9\x14Pf\xd5\xb4\x9de\xad\xceB\xdd\xc8\x8f\x05\xf1D?\xec\xa9\x90\x8a\x88\xad\x03\xff\xff\x00\xb2h"\x06\x03,\xc4Rd\x176!k\xcaX\x9c\x0c\xc1\x018\xddWsSu3\x98\xb3\x12\x9c\x15\x1a\xf4\x7f\xf96\x92\x1c\x02\xe8\xbf\xf20\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x01\x00\x00\x00"\x06\x03h\x95r\xe2\x8b\x0f\xed\xa9\xd6\x98\x1c\x027\xd9\xe5\xde\xdb\xfe\xc1m\xe7\x14?h\n\x02\xed]\x15\x9fu\x87\x1cs\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00"\x02\x02q\x1f\xb6a\x95\x93\xfb\xc9\x82\xdf\x18\x9f\xd6P\xd2\x1bb\xbc\xf7#\xd3\xdd\xbdy7\xe4\t\xaej\x9b\xea\x04\x1c\x02\xe8\xbf\xf20\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x03\x00\x00\x00"\x02\x02\xb1\xa15\xd5*\x95\x1e\x01\x00\xe2\xc6L\xca\xd8\xd9\x92XM\x03Q\xb9\xda{}N\xe2\xf1\xca\xd1&\xa8\x04\x1cs\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x03\x00\x00\x00\x00"\x02\x02|n:1\xc3+\xcb\x9e\xa2\x05ds")l[\xa3i6?\x13n\xf8\xeeZ\xcb!\xc8\xf9\xd5\x1eP\x1c\x02\xe8\xbf\xf20\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x01\x00\x00\x00\x01\x00\x00\x00"\x02\x02\xf6l0\xa0}\xbc\xfda\xe7\xc5\xb6\xfa\xcfNl\'\xf9G\xba\xfa\xcb\xb3\xfc\x0e\xa2\x81\xe0\x16~n\x94\x8d\x1cs\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x01\x00\x00\x00\x01\x00\x00\x00\x00' + SIGNED_MINIS_P2WSH_PSBT_B64_SD = "cHNidP8BAKgCAAAAAZZZFyTIyHAAHpgx4RTJClY3DBD/wvYtvHlQBWiIcZydAAAAAAD9////AyChBwAAAAAAFgAUVpExmvtni79DrSaMRaiKiM2NT2tAQg8AAAAAACIAIJ/v3lcOin2rUPE1jh1mzXq69rL7BIX/ddrN29OP3A3zq40HAAAAAAAiACDfZk57e1AVXMBz66RuW/0oR+hUhBOkRWLyvAnNpy8cagAAAAAAAQCiAgAAAAABAU8jn+rTqB80fFdKbj1CdOpWL99Sy8fOXvs6iNQCMGMmAQAAAAD9////AQZ5HgAAAAAAIgAgGtm2ZA4YoTas6xCsjIQq247clo23ri8+wy0TRq/dMwcBQD/YWe8kwsYxkM6iEcf+7RaaFH3JU+nFKXO/gFUX23DngDPpbNA4sqoTRJ3/5io1PLJZdUZ+T699wfAsNhxhPOQAAAAAAQErBnkeAAAAAAAiACAa2bZkDhihNqzrEKyMhCrbjtyWjbeuLz7DLRNGr90zByICA2iVcuKLD+2p1pgcAjfZ5d7b/sFt5xQ/aAoC7V0Vn3WHRzBEAiAr5AChy/GHi+8HhokaVpkLJTNX++B2wlNcr8RUjmrmcAIgHS9cnlMOPP/QgVVM0RV8HH15jj/cqrw9NsHMBmxsyskBAQVEIQNolXLiiw/tqdaYHAI32eXe2/7BbecUP2gKAu1dFZ91h6xzZHapFFBm1bSdZa3OQt3IjwXxRD/sqZCKiK0D//8AsmgiBgMsxFJkFzYha8pYnAzBATjdV3NTdTOYsxKcFRr0f/k2khwC6L/yMAAAgAEAAIAAAACAAgAAgAAAAAABAAAAIgYDaJVy4osP7anWmBwCN9nl3tv+wW3nFD9oCgLtXRWfdYccc8XaCjAAAIABAACAAAAAgAIAAIAAAAAAAQAAAAAAIgICcR+2YZWT+8mC3xif1lDSG2K89yPT3b15N+QJrmqb6gQcAui/8jAAAIABAACAAAAAgAIAAIAAAAAAAwAAACICArGhNdUqlR4BAOLGTMrY2ZJYTQNRudp7fU7i8crRJqgEHHPF2gowAACAAQAAgAAAAIACAACAAAAAAAMAAAAAIgICfG46McMry56iBWRzIilsW6NpNj8TbvjuWsshyPnVHlAcAui/8jAAAIABAACAAAAAgAIAAIABAAAAAQAAACICAvZsMKB9vP1h58W2+s9ObCf5R7r6y7P8DqKB4BZ+bpSNHHPF2gowAACAAQAAgAAAAIACAACAAQAAAAEAAAAA" + + # TR Miniscript - tr([73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/<0;1>/*,and_v(v:pk([02e8bff2/48h/1h/0h/2h]tpubDESmyzX6RMHRHvzxgLVXymd193CSYYT6C9M9wa4XK649tbAy2WeEvkPwBgoC7i76MypQxpuruUazQjqibbCogTZuENJX6YiuZ5Fy8sf7GNi/<0;1>/*),older(6)))#rfuhsd9c + # tb1p3gev77tasfmd45w64dq5azkdl3y02cxlnkykw2v00x3jnu6yu8pskn06gu + # tb1ph0aq68587rflxewpvs05axhn0lv4jes9zggzsc5jde05auag2mcqx6prnr + # Liana simple inheritance, internal key signed + + MINIS_TR_PSBT = b'psbt\xff\x01\x00\xb4\x02\x00\x00\x00\x01]p\x9b\x12\xc2\xd4\x8d\xb3\x84\x1b\xdb\xff\x1c\xfd\x0f\x8e\xeb\xe8\xb8\xf5("\xf4\xc6\xbdc\xa9-\xf2L\x85\xff\x00\x00\x00\x00\x00\xfd\xff\xff\xff\x03@B\x0f\x00\x00\x00\x00\x00"\x00 \x1a\xd9\xb6d\x0e\x18\xa16\xac\xeb\x10\xac\x8c\x84*\xdb\x8e\xdc\x96\x8d\xb7\xae/>\xc3-\x13F\xaf\xdd3\x07 \xa1\x07\x00\x00\x00\x00\x00"Q @\x9b\x0e\x0b\x19s\xa6\xb0\xefX\x89\x869\xd3\xfbn\x1f\x86L0i\x07\x01y\x0c\xc0(\xb7\xd8\x03"\x87\xdd\x88\x07\x00\x00\x00\x00\x00"Q [\xf5T\x10]\x86\x9b0\x00\xa1-(\x1fr\\\x81\xc5\xef\xc4\xc8\x8fBm\xd5T\xe7\x94\xde\x04\x91rR\x00\x00\x00\x00\x00\x01\x01+\xefs\x1e\x00\x00\x00\x00\x00"Q \x8a2\xcfy}\x82v\xda\xd1\xda\xabAN\x8a\xcd\xfcH\xf5`\xdf\x9d\x89g)\x8fy\xa3)\xf3D\xe1\xc3"\x15\xc0\xb1\xa15\xd5*\x95\x1e\x01\x00\xe2\xc6L\xca\xd8\xd9\x92XM\x03Q\xb9\xda{}N\xe2\xf1\xca\xd1&\xa8\x04% q\x1f\xb6a\x95\x93\xfb\xc9\x82\xdf\x18\x9f\xd6P\xd2\x1bb\xbc\xf7#\xd3\xdd\xbdy7\xe4\t\xaej\x9b\xea\x04\xadV\xb2\xc0!\x16q\x1f\xb6a\x95\x93\xfb\xc9\x82\xdf\x18\x9f\xd6P\xd2\x1bb\xbc\xf7#\xd3\xdd\xbdy7\xe4\t\xaej\x9b\xea\x04=\x01\xf7\x05{\xfb{\xd7\xb3{\x88\xd2N\xbf\x8f\x82\xf7\xe2\xb8"\xe4^\xce\xdbH\xe9@x#\xd0\xa9\xa0gX\x02\xe8\xbf\xf20\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x03\x00\x00\x00!\x16\xb1\xa15\xd5*\x95\x1e\x01\x00\xe2\xc6L\xca\xd8\xd9\x92XM\x03Q\xb9\xda{}N\xe2\xf1\xca\xd1&\xa8\x04\x1d\x00s\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x03\x00\x00\x00\x01\x17 \xb1\xa15\xd5*\x95\x1e\x01\x00\xe2\xc6L\xca\xd8\xd9\x92XM\x03Q\xb9\xda{}N\xe2\xf1\xca\xd1&\xa8\x04\x01\x18 \xf7\x05{\xfb{\xd7\xb3{\x88\xd2N\xbf\x8f\x82\xf7\xe2\xb8"\xe4^\xce\xdbH\xe9@x#\xd0\xa9\xa0gX\x00\x00\x01\x05 \n[Y\xc1!\xee\xb8/DNTh\'*\xcdb\xffM\x1dA\xfc4\xbcQ\xe6l)Q/\xb8\xa2\xeb\x01\x06\'\x00\xc0$ 4!\x8d\x9e\x94\x17\x93)Xk\xd7\xfa\x9d\x88\xf5\x1cl\x15y\x93u\xc5\xf9Y\\\xea\x10\xbfl#\xd4\x83\xadV\xb2!\x07\n[Y\xc1!\xee\xb8/DNTh\'*\xcdb\xffM\x1dA\xfc4\xbcQ\xe6l)Q/\xb8\xa2\xeb\x1d\x00s\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x05\x00\x00\x00!\x074!\x8d\x9e\x94\x17\x93)Xk\xd7\xfa\x9d\x88\xf5\x1cl\x15y\x93u\xc5\xf9Y\\\xea\x10\xbfl#\xd4\x83=\x01\xf8s\xc9\xec\x12zCG\xb2\x13\xc4l\x893M\x98\xe4\xc8\xd3\x9d\xc9\xda\x11\xf8\xd1\x10\xfe2\x90\xd6d\xfd\x02\xe8\xbf\xf20\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x05\x00\x00\x00\x00\x01\x05 \xf6l0\xa0}\xbc\xfda\xe7\xc5\xb6\xfa\xcfNl\'\xf9G\xba\xfa\xcb\xb3\xfc\x0e\xa2\x81\xe0\x16~n\x94\x8d\x01\x06\'\x00\xc0$ |n:1\xc3+\xcb\x9e\xa2\x05ds")l[\xa3i6?\x13n\xf8\xeeZ\xcb!\xc8\xf9\xd5\x1eP\xadV\xb2!\x07|n:1\xc3+\xcb\x9e\xa2\x05ds")l[\xa3i6?\x13n\xf8\xeeZ\xcb!\xc8\xf9\xd5\x1eP=\x01\xbf|\xea~\xde\xcc\xf2r\x0e\xd1\xbd\xcd\x00\xf6\xc4\xec\xc0\x057\xe5U\xff\x19\xb8\xce\xdc\t\xc7\xe6\xb7:T\x02\xe8\xbf\xf20\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x01\x00\x00\x00\x01\x00\x00\x00!\x07\xf6l0\xa0}\xbc\xfda\xe7\xc5\xb6\xfa\xcfNl\'\xf9G\xba\xfa\xcb\xb3\xfc\x0e\xa2\x81\xe0\x16~n\x94\x8d\x1d\x00s\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x01\x00\x00\x00\x01\x00\x00\x00\x00' + MINIS_TR_PSBT_B43 = "C.ZHL9FPQGNRBX4AMRTEV*BIH.4L3ZH**OGKXMO2GFGP56LMHQJ-C+EVKTIKZQUT0SM+JBF9S/PTF$Y-1YCH+*ZD$6:$L5H5X5N*/5I-1CD7K0CN:ATQ0QSZRJI64B-6M/3GVU0D0TSGORS9JP:+:IOJERDBGFH-G/$C*HA7EIG81125R41B*11/*96/YZ8GE1*830+J19NCJIDAJ5D1JRAX2+Z-$-GPZ49RUSYH.IJ1E$Z2C$5XPIDKTMC:A7X/Q8EBPM.HG9*LAUZDHIM+GAJ/W-J5D/:NY8:A++I6YWHD97292:85NTU1B7/X0P.3L9OU:TF0AQPV6IC3IWR4D6Z163.S/C05PAABYKWF$.WFJTGI7/KD/0D52H3OYSIH5369MW8G+:NWBXW+*6N1QZZ*-6SNG3JJIF8K/$D4W1BT15VCTHHT$TA8J:.X.V/ZN1A9WJD1QRX1E.OX73Y1BMI5U6*6TUVK-DK64L563I/*F439WUVLDQU-IUQAAJHH+*$OUD73FY7DYJTBL*9BS/12F/B5TEPDI1QQFLPP*Q6/RH2UK1U.TINN7XS:$Q2ZNGPRUZLGW/9ZI+$8AHVGTH86ZUYE9GA$0IQ-1*Y$11/J$DTO78H$VU4L4*SB59O1YDP4MZA5U*JZ7QB7:V4C321JC/1D309MEKMWT5--2N1OLQG63/BAF$8TOGVILJ8Q5JYIS0M652TE:QA6LN$FRVPQE*BXCPFP50SNO6*OHTG.R+G7KAGO:.JXA/U7MSM6DU5MQ3IVW$UJHI4RI2Z/.OZIIGUX3+MZT.ZJH/5Q.27YVSU0V3I1$9CV-.+*9.FGHDJWJTMNPZA4VK0TIVDVNL+/.A-/UQVPALKZ93+C5VY839JHJK5CPK*J:W7F:Q6T*1UYDW65WZG:$9P3ONB7SRMHXQ1LIB40TH+7/PFU:FY1CV7ZHG6-.3SJ:6Y5HHVGT1JZ/5-XAYLF1$2LO6$SGG1WQ677LWIP1:ML/BQX1HR1$V*+X//TEGE-SFTJ3/RYHH*$$W+66WLAGH6PCQB-56/DS:B7DCIXDV34S.ZI*FC*+KRV5D8V2KL2Z$K*-X/2M1CW+WQG0PXPHSZ66Y7:1KIW19+R2.68$.65*F/89.:EXFYXW54TK*9$77Y-OIBH7H+++TEEW9AEN+SYFPM.BOVRB+KZ$UC:$BNBNH8*TJDLSQ:IJFRZ:BHW969+Y0468-PXBJ6$V/2ZZ9J+/1PPW03QCO4I89GZ1BQY9QD2AMD85//O+-MQK66Q-EDFYRYQ20L$JO3M.*JYZ$TM2F202TZICVC7FQC-:HQRI7L2-5MC5W8S2P+CRPC4.:7DT5/Z7FWUN$7JPDGTRBD3BBV59KL.9EWJ9RF:AM/EWI4FT8O7CR7L82N+7FV9CXQ-Z7UL$W$QDWAOBP+7G1X+K4GTSN6C6Y+Q48HDN9ZAH-A/VHYW43-9S/CC$W.T.15/HP-4883MZGFEYXORA6R17E+XU*UUJH" + MINIS_TR_PSBT_B58 = "4U4YPeYHn8UsQ7XVsTRqLuoxaMQ8rhJsySbuRmzDdWziE1usHZu3cCA4rMpTKgF4dgauY6yGLhVQrz9jf2AF3LmW56uE5mPyCaxkW6GNWGY68jQUSE1Mf6VEdLT3BcA3HjspQGwk6FpRx6x7xYbonv3dU1CsWfp9YPDhqjfWipSCVf8H8YaaV7BwBeA9y9WyxUQjQ5oXHMyLPtCT2bryTj4G2J5QQCyUzvE9pKsn49rPMrXbVexjFPMWFG2XNxNs6uwrD4o4twbpBjohs3FiU2sgW96JzQ1PgBHN9TNLVmikHKsg4cgCfGKEmbWmN5Rmo1wHbqNmV77rR9txQxYMArfvmADbziQqEomVwi1sYjLysfRGdYe7dSe8uH2734aFyFs8oaRQx3ms8jRjnMYJpez3PyZa87NE3zAAwTNiEw6QJ2VgdgY3X1YAofvys2n9Qtt2ve6ehPCsCQvzbJe1MihCV8QAzc8t8LCJNcLcW87ZxMkoNdtJoszdhkZguoFs6xpFneS7VX4M3mzhxPtMz98i1z9eNQKKq1o8tWQHWt9znaMHsaLMtcDdaXXCHKQAdqvpWDWEtXgHy2J5T7jgzFMhreoDBkR3SAkr6D6fmdoNdHr6QPD1MFKP6RN97HC5tc4K1wNpyNTPHWNqLDAuakaKT85mNGX7Kk92suRG3vw6gqAQ1rvKUCWMe8GF4dzX5pCGuQiY1s3gq1pprApWiGKzEwDrvFL9XadKQpJGWHCTkKCaQVCt94ZT2ZtwkghMhaQ45ofehjU9RVE7gtnC64gCvrupg7zXAihGwxfqYPVfaiTyC4NaPU4CzKDDGwi133pV4W6RKYf2BBx4zJqEwwQSJFbPE1Y4fcfL2Zy9xe3p1v7HTp7H3h62uPBVDaRHWHnsmw2oAPdYVGFLbs3YyZKFpnMZCzvhLcF4BjoVdDe7nusjWfnZQx3ZN1fayr6hD8FZR2UzV8E8A9zV8GivvViHaQoSNygyQasGzLjgqYkYpcAykW8QvucGvjJWJdLRk8azXeGFmzzgCZxe1nY4MpSSTmZHpvEV15uLVuDvDHruBUzE2A5HEiaZUrQTJr4gUipK7Y9PATw3J1nencUT99noPJTFA5Kbq7P3rd8k8ruGswuYesuS91chmEW8ssEAhtTVpGmrY4oGrbuLo6F3EXntmLX6p6UKpuWCg8XgZWRdePnC5KcJc2AiKtBBVzu2dXFZgTyrkzpQkYBkHTvz6P4bM6oUBinwVBitt29GAME5M4Q8ZwkiFaW3iyLD9DqLVBxDiaEBeF8y9GRqNq6Q3gYTdgDERWF6fFmieDm3Wxz83fM9i6KFwjvPnLkkJeJFh3AkBPLiMmVPXwYCxFfQp2DefU72sb7nQu2GesYtYazB" + MINIS_TR_PSBT_B64 = "cHNidP8BALQCAAAAAV1wmxLC1I2zhBvb/xz9D47r6Lj1KCL0xr1jqS3yTIX/AAAAAAD9////A0BCDwAAAAAAIgAgGtm2ZA4YoTas6xCsjIQq247clo23ri8+wy0TRq/dMwcgoQcAAAAAACJRIECbDgsZc6aw71iJhjnT+24fhkwwaQcBeQzAKLfYAyKH3YgHAAAAAAAiUSBb9VQQXYabMAChLSgfclyBxe/EyI9CbdVU55TeBJFyUgAAAAAAAQEr73MeAAAAAAAiUSCKMs95fYJ22tHaq0FOis38SPVg352JZymPeaMp80ThwyIVwLGhNdUqlR4BAOLGTMrY2ZJYTQNRudp7fU7i8crRJqgEJSBxH7ZhlZP7yYLfGJ/WUNIbYrz3I9PdvXk35AmuapvqBK1WssAhFnEftmGVk/vJgt8Yn9ZQ0htivPcj0929eTfkCa5qm+oEPQH3BXv7e9eze4jSTr+PgvfiuCLkXs7bSOlAeCPQqaBnWALov/IwAACAAQAAgAAAAIACAACAAAAAAAMAAAAhFrGhNdUqlR4BAOLGTMrY2ZJYTQNRudp7fU7i8crRJqgEHQBzxdoKMAAAgAEAAIAAAACAAgAAgAAAAAADAAAAARcgsaE11SqVHgEA4sZMytjZklhNA1G52nt9TuLxytEmqAQBGCD3BXv7e9eze4jSTr+PgvfiuCLkXs7bSOlAeCPQqaBnWAAAAQUgCltZwSHuuC9ETlRoJyrNYv9NHUH8NLxR5mwpUS+4ousBBicAwCQgNCGNnpQXkylYa9f6nYj1HGwVeZN1xflZXOoQv2wj1IOtVrIhBwpbWcEh7rgvRE5UaCcqzWL/TR1B/DS8UeZsKVEvuKLrHQBzxdoKMAAAgAEAAIAAAACAAgAAgAAAAAAFAAAAIQc0IY2elBeTKVhr1/qdiPUcbBV5k3XF+Vlc6hC/bCPUgz0B+HPJ7BJ6Q0eyE8RsiTNNmOTI053J2hH40RD+MpDWZP0C6L/yMAAAgAEAAIAAAACAAgAAgAAAAAAFAAAAAAEFIPZsMKB9vP1h58W2+s9ObCf5R7r6y7P8DqKB4BZ+bpSNAQYnAMAkIHxuOjHDK8ueogVkcyIpbFujaTY/E2747lrLIcj51R5QrVayIQd8bjoxwyvLnqIFZHMiKWxbo2k2PxNu+O5ayyHI+dUeUD0Bv3zqft7M8nIO0b3NAPbE7MAFN+VV/xm4ztwJx+a3OlQC6L/yMAAAgAEAAIAAAACAAgAAgAEAAAABAAAAIQf2bDCgfbz9YefFtvrPTmwn+Ue6+suz/A6igeAWfm6UjR0Ac8XaCjAAAIABAACAAAAAgAIAAIABAAAAAQAAAAA=" + MINIS_TR_PSBT_UR_PSBT = UR("crypto-psbt", PSBT(MINIS_TR_PSBT).to_cbor()) + + IN_KEY_SIGNED_MINIS_TR_PSBT = b'psbt\xff\x01\x00\xb4\x02\x00\x00\x00\x01]p\x9b\x12\xc2\xd4\x8d\xb3\x84\x1b\xdb\xff\x1c\xfd\x0f\x8e\xeb\xe8\xb8\xf5("\xf4\xc6\xbdc\xa9-\xf2L\x85\xff\x00\x00\x00\x00\x00\xfd\xff\xff\xff\x03@B\x0f\x00\x00\x00\x00\x00"\x00 \x1a\xd9\xb6d\x0e\x18\xa16\xac\xeb\x10\xac\x8c\x84*\xdb\x8e\xdc\x96\x8d\xb7\xae/>\xc3-\x13F\xaf\xdd3\x07 \xa1\x07\x00\x00\x00\x00\x00"Q @\x9b\x0e\x0b\x19s\xa6\xb0\xefX\x89\x869\xd3\xfbn\x1f\x86L0i\x07\x01y\x0c\xc0(\xb7\xd8\x03"\x87\xdd\x88\x07\x00\x00\x00\x00\x00"Q [\xf5T\x10]\x86\x9b0\x00\xa1-(\x1fr\\\x81\xc5\xef\xc4\xc8\x8fBm\xd5T\xe7\x94\xde\x04\x91rR\x00\x00\x00\x00\x00\x01\x01+\xefs\x1e\x00\x00\x00\x00\x00"Q \x8a2\xcfy}\x82v\xda\xd1\xda\xabAN\x8a\xcd\xfcH\xf5`\xdf\x9d\x89g)\x8fy\xa3)\xf3D\xe1\xc3\x01\x08B\x01@Q3\x98\xe4[\xd0}HM\x86\x00H\xb4\xa0\x16\xb19\xc3-\x13F\xaf\xdd3\x07 \xa1\x07\x00\x00\x00\x00\x00"Q @\x9b\x0e\x0b\x19s\xa6\xb0\xefX\x89\x869\xd3\xfbn\x1f\x86L0i\x07\x01y\x0c\xc0(\xb7\xd8\x03"\x87\xdd\x88\x07\x00\x00\x00\x00\x00"Q [\xf5T\x10]\x86\x9b0\x00\xa1-(\x1fr\\\x81\xc5\xef\xc4\xc8\x8fBm\xd5T\xe7\x94\xde\x04\x91rR\x00\x00\x00\x00\x00\x01\x01+\xefs\x1e\x00\x00\x00\x00\x00"Q \x8a2\xcfy}\x82v\xda\xd1\xda\xabAN\x8a\xcd\xfcH\xf5`\xdf\x9d\x89g)\x8fy\xa3)\xf3D\xe1\xc3\x01\x08B\x01@Q3\x98\xe4[\xd0}HM\x86\x00H\xb4\xa0\x16\xb19\xc3-\x13F\xaf\xdd3\x07 \xa1\x07\x00\x00\x00\x00\x00"Q @\x9b\x0e\x0b\x19s\xa6\xb0\xefX\x89\x869\xd3\xfbn\x1f\x86L0i\x07\x01y\x0c\xc0(\xb7\xd8\x03"\x87\xdd\x88\x07\x00\x00\x00\x00\x00"Q [\xf5T\x10]\x86\x9b0\x00\xa1-(\x1fr\\\x81\xc5\xef\xc4\xc8\x8fBm\xd5T\xe7\x94\xde\x04\x91rR\x00\x00\x00\x00\x00\x01\x01+\xefs\x1e\x00\x00\x00\x00\x00"Q \x8a2\xcfy}\x82v\xda\xd1\xda\xabAN\x8a\xcd\xfcH\xf5`\xdf\x9d\x89g)\x8fy\xa3)\xf3D\xe1\xc3A\x14q\x1f\xb6a\x95\x93\xfb\xc9\x82\xdf\x18\x9f\xd6P\xd2\x1bb\xbc\xf7#\xd3\xdd\xbdy7\xe4\t\xaej\x9b\xea\x04\xf7\x05{\xfb{\xd7\xb3{\x88\xd2N\xbf\x8f\x82\xf7\xe2\xb8"\xe4^\xce\xdbH\xe9@x#\xd0\xa9\xa0gX@\xe0\xf3\n\xf0\x1b;\xf6\xde\x1d@X\xc0o\xe1s`\xea\xe2\x0bJ(\xbc4\xc1H\xa9%\xd7`q\xddC\x8b\xd3\xcd\xeb\x1d\xcc"w\x93\xcbj\x02[hW\xf0\xcd\xbc3>\xd3S\xf0a\xce\xfd\x92\x14\\\x9e\x1a\x17\x00\x00\x00\x00' + TAP_TREE_SIGNED_RECOV_PSBT_B43 = "2ZMQXUW070H/C$O912IUP38PY-B:Y$-RMWPWCEVH4INJ0O18-TBJ48CZOD$TWI4I/NE8VN6KM12$5ZSVRTAG309P5CUJ2C2//V/10X86GODPB2R6A.-1F03RUUBF1+7YE8I/JYD4EB9C+U2E*ECO6IS/FAIE8CSCDLXC3$O$N*WQ9M1R*V*AKQ0$1T9RPNVBEVDGRPOJH8W.+6WV5M8.F3BOON/VSH39A5Z17$:50YW8.QS+AO7ZR.:RO*PD-DZPBFCDLJY*9QV/+BT1W9PT7JQ0XX81B::I9YOOIUKCJ$V*39Q$GYUKV887917VA-S7U1*4RHVHP64V$L6+Y0Y-S-8MTY99L*VA*$C*IA1YRVZ.A85M07KQF:CUR1RYODG6IXQHI*J/LWU$I46Y*6OIR*B40O-0+7U6.B:..O*YZZHA-*UAKA3J.DJBUP9ZYOV34+9K4-5R074W7NP$ZSXH*YL1B0YFUBQSLB:LE*K7UM.$L-8L759$8R/A8NZQLYMHBAAK/Y8MDL.17RIOJE4$:QD+N4HTN3BOL+" + TAP_TREE_SIGNED_RECOV_PSBT_B58 = "2SudmehyNb1LkDuASe1chZCVpPJRP43zF2Ub1pehVv1WnSr4xckuEfFbCXCeSVuL2r43s6voxZbf4k8JeqvVwsVseK557UvqnpJ45Nts2vs274zxo1Pw1kEpZcKsMpsnaxp1rscFNrorxYGBQFBa3phYf39obkfjBcdTFuH724TJp95dPy3m66uegpNzCbWd3UnGbdXmALngVxMtZf26T4jEJrthxXZCDgQMM3ucGuYoVU1qdsGVCKvYo9NawBmRHsQGxxbEAdVkjDShJMMcpH9TpyVo8QUq28MNAMQYiBGFTTNGtm1iuQ118Tu37YAzCMtsEFL18hWNGjVGLPzednoie9iwxUrmm1fvHuxoZnjeop6h5aEJkksqZZESo8AYJXwVnv2NGvUn3KDMrRpEwWNp5m3Uj61UEeLWxav5kbf86x1rKukjpqA2GJBf3GqDvXHDf8EVoGTcNW7aAc3Edccd5dA4sy2QwfX2Ht1RHsfgVSWVRgfjZNUJGo" + TAP_TREE_SIGNED_RECOV_PSBT_B64 = "cHNidP8BALQCAAAAAV1wmxLC1I2zhBvb/xz9D47r6Lj1KCL0xr1jqS3yTIX/AAAAAAD9////A0BCDwAAAAAAIgAgGtm2ZA4YoTas6xCsjIQq247clo23ri8+wy0TRq/dMwcgoQcAAAAAACJRIECbDgsZc6aw71iJhjnT+24fhkwwaQcBeQzAKLfYAyKH3YgHAAAAAAAiUSBb9VQQXYabMAChLSgfclyBxe/EyI9CbdVU55TeBJFyUgAAAAAAAQEr73MeAAAAAAAiUSCKMs95fYJ22tHaq0FOis38SPVg352JZymPeaMp80Thw0EUcR+2YZWT+8mC3xif1lDSG2K89yPT3b15N+QJrmqb6gT3BXv7e9eze4jSTr+PgvfiuCLkXs7bSOlAeCPQqaBnWEDg8wrwGzv23h1AWMBv4XNg6uILSii8NMFIqSXXYHHdQ4vTzesdzCJ3k8tqAltoV/DNvDM+01PwYc79khRcnhoXAAAAAA==" + TAP_TREE_SIGNED_RECOV_PSBT_UR_PSBT = UR( + "crypto-psbt", PSBT(TAP_TREE_SIGNED_RECOV_PSBT).to_cbor() + ) + TAP_TREE_SIGNED_RECOV_PSBT_SD = b'psbt\xff\x01\x00\xb4\x02\x00\x00\x00\x01]p\x9b\x12\xc2\xd4\x8d\xb3\x84\x1b\xdb\xff\x1c\xfd\x0f\x8e\xeb\xe8\xb8\xf5("\xf4\xc6\xbdc\xa9-\xf2L\x85\xff\x00\x00\x00\x00\x00\xfd\xff\xff\xff\x03@B\x0f\x00\x00\x00\x00\x00"\x00 \x1a\xd9\xb6d\x0e\x18\xa16\xac\xeb\x10\xac\x8c\x84*\xdb\x8e\xdc\x96\x8d\xb7\xae/>\xc3-\x13F\xaf\xdd3\x07 \xa1\x07\x00\x00\x00\x00\x00"Q @\x9b\x0e\x0b\x19s\xa6\xb0\xefX\x89\x869\xd3\xfbn\x1f\x86L0i\x07\x01y\x0c\xc0(\xb7\xd8\x03"\x87\xdd\x88\x07\x00\x00\x00\x00\x00"Q [\xf5T\x10]\x86\x9b0\x00\xa1-(\x1fr\\\x81\xc5\xef\xc4\xc8\x8fBm\xd5T\xe7\x94\xde\x04\x91rR\x00\x00\x00\x00\x00\x01\x01+\xefs\x1e\x00\x00\x00\x00\x00"Q \x8a2\xcfy}\x82v\xda\xd1\xda\xabAN\x8a\xcd\xfcH\xf5`\xdf\x9d\x89g)\x8fy\xa3)\xf3D\xe1\xc3A\x14q\x1f\xb6a\x95\x93\xfb\xc9\x82\xdf\x18\x9f\xd6P\xd2\x1bb\xbc\xf7#\xd3\xdd\xbdy7\xe4\t\xaej\x9b\xea\x04\xf7\x05{\xfb{\xd7\xb3{\x88\xd2N\xbf\x8f\x82\xf7\xe2\xb8"\xe4^\xce\xdbH\xe9@x#\xd0\xa9\xa0gX@\xe0\xf3\n\xf0\x1b;\xf6\xde\x1d@X\xc0o\xe1s`\xea\xe2\x0bJ(\xbc4\xc1H\xa9%\xd7`q\xddC\x8b\xd3\xcd\xeb\x1d\xcc"w\x93\xcbj\x02[hW\xf0\xcd\xbc3>\xd3S\xf0a\xce\xfd\x92\x14\\\x9e\x1a\x17"\x15\xc0\xb1\xa15\xd5*\x95\x1e\x01\x00\xe2\xc6L\xca\xd8\xd9\x92XM\x03Q\xb9\xda{}N\xe2\xf1\xca\xd1&\xa8\x04% q\x1f\xb6a\x95\x93\xfb\xc9\x82\xdf\x18\x9f\xd6P\xd2\x1bb\xbc\xf7#\xd3\xdd\xbdy7\xe4\t\xaej\x9b\xea\x04\xadV\xb2\xc0!\x16q\x1f\xb6a\x95\x93\xfb\xc9\x82\xdf\x18\x9f\xd6P\xd2\x1bb\xbc\xf7#\xd3\xdd\xbdy7\xe4\t\xaej\x9b\xea\x04=\x01\xf7\x05{\xfb{\xd7\xb3{\x88\xd2N\xbf\x8f\x82\xf7\xe2\xb8"\xe4^\xce\xdbH\xe9@x#\xd0\xa9\xa0gX\x02\xe8\xbf\xf20\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x03\x00\x00\x00!\x16\xb1\xa15\xd5*\x95\x1e\x01\x00\xe2\xc6L\xca\xd8\xd9\x92XM\x03Q\xb9\xda{}N\xe2\xf1\xca\xd1&\xa8\x04\x1d\x00s\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x03\x00\x00\x00\x01\x17 \xb1\xa15\xd5*\x95\x1e\x01\x00\xe2\xc6L\xca\xd8\xd9\x92XM\x03Q\xb9\xda{}N\xe2\xf1\xca\xd1&\xa8\x04\x01\x18 \xf7\x05{\xfb{\xd7\xb3{\x88\xd2N\xbf\x8f\x82\xf7\xe2\xb8"\xe4^\xce\xdbH\xe9@x#\xd0\xa9\xa0gX\x00\x00\x01\x05 \n[Y\xc1!\xee\xb8/DNTh\'*\xcdb\xffM\x1dA\xfc4\xbcQ\xe6l)Q/\xb8\xa2\xeb!\x07\n[Y\xc1!\xee\xb8/DNTh\'*\xcdb\xffM\x1dA\xfc4\xbcQ\xe6l)Q/\xb8\xa2\xeb\x1d\x00s\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x05\x00\x00\x00!\x074!\x8d\x9e\x94\x17\x93)Xk\xd7\xfa\x9d\x88\xf5\x1cl\x15y\x93u\xc5\xf9Y\\\xea\x10\xbfl#\xd4\x83=\x01\xf8s\xc9\xec\x12zCG\xb2\x13\xc4l\x893M\x98\xe4\xc8\xd3\x9d\xc9\xda\x11\xf8\xd1\x10\xfe2\x90\xd6d\xfd\x02\xe8\xbf\xf20\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x00\x00\x00\x00\x05\x00\x00\x00\x01\x06\'\x00\xc0$ 4!\x8d\x9e\x94\x17\x93)Xk\xd7\xfa\x9d\x88\xf5\x1cl\x15y\x93u\xc5\xf9Y\\\xea\x10\xbfl#\xd4\x83\xadV\xb2\x00\x01\x05 \xf6l0\xa0}\xbc\xfda\xe7\xc5\xb6\xfa\xcfNl\'\xf9G\xba\xfa\xcb\xb3\xfc\x0e\xa2\x81\xe0\x16~n\x94\x8d!\x07|n:1\xc3+\xcb\x9e\xa2\x05ds")l[\xa3i6?\x13n\xf8\xeeZ\xcb!\xc8\xf9\xd5\x1eP=\x01\xbf|\xea~\xde\xcc\xf2r\x0e\xd1\xbd\xcd\x00\xf6\xc4\xec\xc0\x057\xe5U\xff\x19\xb8\xce\xdc\t\xc7\xe6\xb7:T\x02\xe8\xbf\xf20\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x01\x00\x00\x00\x01\x00\x00\x00!\x07\xf6l0\xa0}\xbc\xfda\xe7\xc5\xb6\xfa\xcfNl\'\xf9G\xba\xfa\xcb\xb3\xfc\x0e\xa2\x81\xe0\x16~n\x94\x8d\x1d\x00s\xc5\xda\n0\x00\x00\x80\x01\x00\x00\x80\x00\x00\x00\x80\x02\x00\x00\x80\x01\x00\x00\x00\x01\x00\x00\x00\x01\x06\'\x00\xc0$ |n:1\xc3+\xcb\x9e\xa2\x05ds")l[\xa3i6?\x13n\xf8\xeeZ\xcb!\xc8\xf9\xd5\x1eP\xadV\xb2\x00' + TAP_TREE_SIGNED_RECOV_PSBT_B64_SD = "cHNidP8BALQCAAAAAV1wmxLC1I2zhBvb/xz9D47r6Lj1KCL0xr1jqS3yTIX/AAAAAAD9////A0BCDwAAAAAAIgAgGtm2ZA4YoTas6xCsjIQq247clo23ri8+wy0TRq/dMwcgoQcAAAAAACJRIECbDgsZc6aw71iJhjnT+24fhkwwaQcBeQzAKLfYAyKH3YgHAAAAAAAiUSBb9VQQXYabMAChLSgfclyBxe/EyI9CbdVU55TeBJFyUgAAAAAAAQEr73MeAAAAAAAiUSCKMs95fYJ22tHaq0FOis38SPVg352JZymPeaMp80Thw0EUcR+2YZWT+8mC3xif1lDSG2K89yPT3b15N+QJrmqb6gT3BXv7e9eze4jSTr+PgvfiuCLkXs7bSOlAeCPQqaBnWEDg8wrwGzv23h1AWMBv4XNg6uILSii8NMFIqSXXYHHdQ4vTzesdzCJ3k8tqAltoV/DNvDM+01PwYc79khRcnhoXIhXAsaE11SqVHgEA4sZMytjZklhNA1G52nt9TuLxytEmqAQlIHEftmGVk/vJgt8Yn9ZQ0htivPcj0929eTfkCa5qm+oErVaywCEWcR+2YZWT+8mC3xif1lDSG2K89yPT3b15N+QJrmqb6gQ9AfcFe/t717N7iNJOv4+C9+K4IuRezttI6UB4I9CpoGdYAui/8jAAAIABAACAAAAAgAIAAIAAAAAAAwAAACEWsaE11SqVHgEA4sZMytjZklhNA1G52nt9TuLxytEmqAQdAHPF2gowAACAAQAAgAAAAIACAACAAAAAAAMAAAABFyCxoTXVKpUeAQDixkzK2NmSWE0DUbnae31O4vHK0SaoBAEYIPcFe/t717N7iNJOv4+C9+K4IuRezttI6UB4I9CpoGdYAAABBSAKW1nBIe64L0ROVGgnKs1i/00dQfw0vFHmbClRL7ii6yEHCltZwSHuuC9ETlRoJyrNYv9NHUH8NLxR5mwpUS+4ousdAHPF2gowAACAAQAAgAAAAIACAACAAAAAAAUAAAAhBzQhjZ6UF5MpWGvX+p2I9RxsFXmTdcX5WVzqEL9sI9SDPQH4c8nsEnpDR7ITxGyJM02Y5MjTncnaEfjREP4ykNZk/QLov/IwAACAAQAAgAAAAIACAACAAAAAAAUAAAABBicAwCQgNCGNnpQXkylYa9f6nYj1HGwVeZN1xflZXOoQv2wj1IOtVrIAAQUg9mwwoH28/WHnxbb6z05sJ/lHuvrLs/wOooHgFn5ulI0hB3xuOjHDK8ueogVkcyIpbFujaTY/E2747lrLIcj51R5QPQG/fOp+3szycg7Rvc0A9sTswAU35VX/GbjO3AnH5rc6VALov/IwAACAAQAAgAAAAIACAACAAQAAAAEAAAAhB/ZsMKB9vP1h58W2+s9ObCf5R7r6y7P8DqKB4BZ+bpSNHQBzxdoKMAAAgAEAAIAAAACAAgAAgAEAAAABAAAAAQYnAMAkIHxuOjHDK8ueogVkcyIpbFujaTY/E2747lrLIcj51R5QrVayAA==" + + # TR expanding multisig - tr(tpubD6NzVbkrYhZ4YYQjuKXFp85eRGCk4oA94MXbbHJW6zNAibMfkwZh7JQNVHEhpcQYaRCUs3b5PhvKPKESGoAJptiLvF8Rm1jhoFsryyFuR1P/<0;1>/*,{and_v(v:multi_a(2,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/<2;3>/*,[02e8bff2/48h/1h/0h/2h]tpubDESmyzX6RMHRHvzxgLVXymd193CSYYT6C9M9wa4XK649tbAy2WeEvkPwBgoC7i76MypQxpuruUazQjqibbCogTZuENJX6YiuZ5Fy8sf7GNi/<2;3>/*,[8cb36b38/48h/1h/0h/2h]tpubDESTVwqbbaSoN2mPq7tcWkPBpRBkaEADrUzUhRTVnNef6oVn6w2PHL4zoUjUAJSPLJQRBetkgX4sDRcoaCyFHxqHGyWyaiV8REKDkh7zQac/<0;1>/*),older(6)),multi_a(2,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/<0;1>/*,[02e8bff2/48h/1h/0h/2h]tpubDESmyzX6RMHRHvzxgLVXymd193CSYYT6C9M9wa4XK649tbAy2WeEvkPwBgoC7i76MypQxpuruUazQjqibbCogTZuENJX6YiuZ5Fy8sf7GNi/<0;1>/*)})#6ga85u82 + # tb1p2v4cyx9503f69uc4k9fy35xrrh0znvlz6exsm6jsm082uym87hasr4urrx + + EXP_TR_MINIS_PSBT = b'psbt\xff\x01\x00\xb4\x02\x00\x00\x00\x01\x7f\xca\x84\x9d\xd9\x85\xd8s\xa9Lz\xbd\x95\x05b\x1f<\xb1\xf3\x01:\x05j\xcda;\xc1\x8d\x88G\\\xf1\x00\x00\x00\x00\x00\xfd\xff\xff\xff\x03 \xa1\x07\x00\x00\x00\x00\x00"Q \x87\\?;;\x80\x10\xc0\xfe\x06\xe3\xdc\xcbj\xed\x00\xc4\x9a\r\x91?\x8d\xc3\x1a\xb1\xda%;\ne\x8a\x82@B\x0f\x00\x00\x00\x00\x00"Q \x8a2\xcfy}\x82v\xda\xd1\xda\xabAN\x8a\xcd\xfcH\xf5`\xdf\x9d\x89g)\x8fy\xa3)\xf3D\xe1\xc3\xda\x81\x07\x00\x00\x00\x00\x00"Q \x12Z\xa5\xd2\xc5\x8bR\xc8~\xbd\x8d\xbb\x1dK\r,\xea\x10\xf2\x16G\x98\x90g<\x88\xa4\x0cP\'\xff:\x00\x00\x00\x00\x00\x01\x01+\xe5n\x1e\x00\x00\x00\x00\x00"Q S+\x82\x18\xb4|S\xa2\xf3\x15\xb1RH\xd0\xc3\x1d\xde)\xb3\xe2\xd6M\r\xeaP\xdb\xce\xae\x13g\xf5\xfbB\x15\xc1\x12\x97\x80=D@o\x05S\xab\xcbu\x93h\xb7\xb05\r\xa3\xa6!\x04[G\x8dtVX\x87\xaan\x99W\xbbd\x83\x10\xd8\xec\xd4H\x13\xda\x12\xc0!jXcWp\x08o\\/^j?\x182\x85U\xc4\xa9k R\xb1/\x9c\xc6\xcd5\x8b\x97\xfa\xa0\xf1\xd0\x92F\xe3&\xe1\x1d\x07o`\x0e\xa7pns\xb7\xc8\xf3\xde@f\x8b\x87\x96I\x17\xe7\xaa\xc8y\xeb\x97\xf9GSX:\xf5+\x11eJ\x9bx\x18x\xaa\x1ax\xb8\x1d\xe9R\x0e\xe2jK\x8e\xadr\xcd\xc5VV\x1f\t\x11\xad\xcf\x93\x00\x00\x00\x00' + SIGNED_EXP_TR_MINIS_PSBT_B43 = "4TT$EW2BC750K0.EQ*OESR*TGYF.-DD/LNKXNKCH-6XECST3IQQE+:F6.Y/0JLF7F+GJN5RT*NFF:IV5+NZPT63T2:7-N/5Z67XO/QU*-Y5A/*5HX89U7DJWQ0I-6YIYT15-50EHV:4HD0T5J+M:FBO80S+V8R.NKPC86Y:EM/J8WSHFX$S$IQD.Q0/GQHLNY$R8$MRS6V*Y5VE6TMNMZU4$$AL/0P1*0VIWH:F5SI/UIRS46O64DL:VURGAY39L/FWKSPR9FWE6I43KBC27/1GSJN+FXOX*:Q3O-6BYTXWK5Z93Y3979VKYOP8VE/EQ6XUV14$J06O.NMPACBX/N.2-V+ODBBY-R8.6NPDSYJ:/XX0RWZ2:*P.BRF+9C*XQT.LBNN-DVA7.Q14LGOOENSM/I6$43M5271UL/Q+HM.-9-HV2:4Q6+WMP*IOWFIN.CA0ZR/414P7Y8$J41RYF11YMQ8.+8CFL:*W43S$DBC$JHKG8G*8UW*QTJ9R:JR42.W0DZ2RUW+MWIJ/YG6:XVR8V$594TU0QO0PDG-AAFEA+S/1ONG*05SNXP29T34$:-0OMFMUL:ZN330:GFKIT9RVMO$FT+Y8/*1C9JTU61R5661+-.WZ.U/8F9F:9ECFSHWS4$Z4-T*NK1LYPB1P7JJ8MJYD/6L+8.+/RZ6JGM$14Q$AA+*G29W$3.7OZMAH$HYVXUC9$NLMLYXG1352L..SF-+F-Q573B7*" + SIGNED_EXP_TR_MINIS_PSBT_B58 = "yBmUhxPER5LACMorWNH5G7qrTgLkwgd6sudwBgzFT78eurc9c2xwb7bhjyY7codASWJZsTU7tAAo51pgQMghHf64iBgA4V18jHKvEUSV9c47uTVoug7uZsx6DxcSSCnduUrh9L5oxoScPMkBR2dUjkEJYL9tJfZdtfztPy8m2MP587kj375NhNu3vHPb5mNrDaLtLC2rh5DZ6cASa4Vt1taRtpGLquwJE9NPQ7V6eEYvhpagxZvCvErNQZaSnhQQ3hgbHQgwQegmHehkXpVLTV3NqLh3uEWUosqNNhZesCRP9BTwp41dGETHi2Ksz8YRmjnyoFrL8g6xrX4j4YCMxkmf2tXY1XCPdMaLPJAmKu9f8o9MdJQrzHJCi6VuuvkfBKG6R9MAAPPkWE1Prj3GicD4aU7wWmo8A7eRZPpHbxwWxC3DMgveT4XPsGpe3MyMkzs6yG3Wfi3v5ae47oLhQuKexbwzDHtcNcNYsocS3d1fEAMW3SshGYpBzSY5YJiPX2JUZJ3Hr7QchnZmvAh37LAJRHHWELufq3xHpkGaWUExomR26eVFWyMwLjvpgUGxkuET9u4ByTwx1SWbygqmmtUsv15yLcnuoeuGVJr6KutdoWRShLdUDYQ8iHXgxSWFfMre75Ac6Ko9xzaarwKvGPcSxjtA18ZbyHyeP7xoYumZ" + SIGNED_EXP_TR_MINIS_PSBT_B64 = "cHNidP8BALQCAAAAAX/KhJ3ZhdhzqUx6vZUFYh88sfMBOgVqzWE7wY2IR1zxAAAAAAD9////AyChBwAAAAAAIlEgh1w/OzuAEMD+BuPcy2rtAMSaDZE/jcMasdolOwplioJAQg8AAAAAACJRIIoyz3l9gnba0dqrQU6KzfxI9WDfnYlnKY95oynzROHD2oEHAAAAAAAiUSASWqXSxYtSyH69jbsdSw0s6hDyFkeYkGc8iKQMUCf/OgAAAAAAAQEr5W4eAAAAAAAiUSBTK4IYtHxTovMVsVJI0MMd3imz4tZNDepQ286uE2f1+0EUUrEvnMbNNYuX+qDx0JJG4ybhHQdvYA6ncG5ztzxuEmXLToX/NmN8axrk+UvWU+drbTU4piY9m8SD4Zj49o39ekAEPMhGIog5xZmlR/87j90YTVZHO88f0V0WMzSkuEQaCJIKMU7q3c2GlpaAAl5XfyPGc7RMcLICj014xRR2efeGQRRolXLiiw/tqdaYHAI32eXe2/7BbecUP2gKAu1dFZ91h1e7ZIMQ2OzUSBPaEsAhalhjV3AIb1wvXmo/GDKFVcSpQPk6R2pV+yQ5qD7I895AZouHlkkX56rIeeuX+UdTWDr1KxFlSpt4GHiqGni4HelSDuJqS46tcs3FVlYfCRGtz5MAAAAA" + SIGNED_EXP_TR_MINIS_PSBT_UR_PSBT = UR( + "crypto-psbt", PSBT(SIGNED_EXP_TR_MINIS_PSBT).to_cbor() + ) + SIGNED_EXP_TR_MINIS_PSBT_SD = b'psbt\xff\x01\x00\xb4\x02\x00\x00\x00\x01\x7f\xca\x84\x9d\xd9\x85\xd8s\xa9Lz\xbd\x95\x05b\x1f<\xb1\xf3\x01:\x05j\xcda;\xc1\x8d\x88G\\\xf1\x00\x00\x00\x00\x00\xfd\xff\xff\xff\x03 \xa1\x07\x00\x00\x00\x00\x00"Q \x87\\?;;\x80\x10\xc0\xfe\x06\xe3\xdc\xcbj\xed\x00\xc4\x9a\r\x91?\x8d\xc3\x1a\xb1\xda%;\ne\x8a\x82@B\x0f\x00\x00\x00\x00\x00"Q \x8a2\xcfy}\x82v\xda\xd1\xda\xabAN\x8a\xcd\xfcH\xf5`\xdf\x9d\x89g)\x8fy\xa3)\xf3D\xe1\xc3\xda\x81\x07\x00\x00\x00\x00\x00"Q \x12Z\xa5\xd2\xc5\x8bR\xc8~\xbd\x8d\xbb\x1dK\r,\xea\x10\xf2\x16G\x98\x90g<\x88\xa4\x0cP\'\xff:\x00\x00\x00\x00\x00\x01\x01+\xe5n\x1e\x00\x00\x00\x00\x00"Q S+\x82\x18\xb4|S\xa2\xf3\x15\xb1RH\xd0\xc3\x1d\xde)\xb3\xe2\xd6M\r\xeaP\xdb\xce\xae\x13g\xf5\xfbA\x14R\xb1/\x9c\xc6\xcd5\x8b\x97\xfa\xa0\xf1\xd0\x92F\xe3&\xe1\x1d\x07o`\x0e\xa7pns\xb7\xc8\xf3\xde@f\x8b\x87\x96I\x17\xe7\xaa\xc8y\xeb\x97\xf9GSX:\xf5+\x11eJ\x9bx\x18x\xaa\x1ax\xb8\x1d\xe9R\x0e\xe2jK\x8e\xadr\xcd\xc5VV\x1f\t\x11\xad\xcf\x93B\x15\xc1\x12\x97\x80=D@o\x05S\xab\xcbu\x93h\xb7\xb05\r\xa3\xa6!\x04[G\x8dtVX\x87\xaan\x99W\xbbd\x83\x10\xd8\xec\xd4H\x13\xda\x12\xc0!jXcWp\x08o\\/^j?\x182\x85U\xc4\xa9k R\xb1/\x9c\xc6\xcd5\x8b\x97\xfa\xa0\xf1\xd0\x92F\xe3&\xe1\x1d\x07o`\x0e\xa7pns\xb7 3: wallet = Wallet( - Key(tdata.TEST_MNEMONIC, False, case[3], script_type=case[0]) + Key(tdata.TEST_MNEMONIC, TYPE_SINGLESIG, case[3], script_type=case[0]) ) else: wallet = Wallet( - Key(tdata.TEST_MNEMONIC, False, NETWORKS["test"], script_type=case[0]) + Key( + tdata.TEST_MNEMONIC, + TYPE_SINGLESIG, + NETWORKS["test"], + script_type=case[0], + ) ) signer = PSBTSigner(wallet, case[1], FORMAT_NONE) path_mismatch = signer.path_mismatch() @@ -747,12 +1723,12 @@ def test_path_mismatch(mocker, m5stickv, tdata): def test_sign_sats_vB(m5stickv): from embit.networks import NETWORKS from krux.psbt import PSBTSigner - from krux.key import Key + from krux.key import Key, TYPE_SINGLESIG, TYPE_MULTISIG from krux.wallet import Wallet from krux.qr import FORMAT_PMOFN MNEMONIC = "action action action action action action action action action action action action" - wallet = Wallet(Key(MNEMONIC, False, NETWORKS["test"])) + wallet = Wallet(Key(MNEMONIC, TYPE_SINGLESIG, NETWORKS["test"])) PSBT_satvB_1_31 = "cHNidP8BAPcCAAAABcfPlS2RvKvXxP/UxRmlAzMZcpLPKTOsBNbFM1JpT5Q7BgAAAAD9////x8+VLZG8q9fE/9TFGaUDMxlyks8pM6wE1sUzUmlPlDsAAAAAAP3////Hz5Utkbyr18T/1MUZpQMzGXKSzykzrATWxTNSaU+UOwMAAAAA/f///8fPlS2RvKvXxP/UxRmlAzMZcpLPKTOsBNbFM1JpT5Q7AgAAAAD9////x8+VLZG8q9fE/9TFGaUDMxlyks8pM6wE1sUzUmlPlDsBAAAAAP3///8B6AMAAAAAAAAXqRRiWIJrJ8MsDs5aLI2HPOHxoohj04e6+SoATwEENYfPA04BoMaAAAAADqwTbEcFGhxvEHabuwbcm8HLo8fY/7oVTxfPbpMs1/4DHz9uZifqAdnopjmilOHYCN/7ewoGlnjSzn1pi1wSdj4Q4MWVxVQAAIABAACAAAAAgAABAP19AQIAAAADBmHTgkZvesndEkYg1//mU0bdEWT0NDJ+pookrffo5xoBAAAAAP3///+lkTtN5yuncT07kkZIuZrgPlMIbk3e4Ph14s4MG+S3gQAAAAAA/f///6WRO03nK6dxPTuSRki5muA+UwhuTd7g+HXizgwb5LeBAQAAAAD9////CCwBAAAAAAAAFgAUBFtVYlpUm5iHZvf8cW1Te1D16gIsAQAAAAAAABYAFPiBJvmTv+DdmsRcvIcWU/8LXU0ULAEAAAAAAAAWABTTg2VJqeeWiJbBke8bW5/1ovQ0+iwBAAAAAAAAFgAU0MnlyP/ofvZQ1P3mcA4vKVen9LksAQAAAAAAABYAFOPwOp2QPepm6NFFGXgeWlZoANQHLAEAAAAAAAAWABSY8opy2FeyAGUjkBlsm10RSsV7qCwBAAAAAAAAFgAUXWUVxmbYT+CMAs0oR8G3MYyyuyqZGAAAAAAAABYAFGT8/EuuiDNH8x7K3O7S/vZEyAaHzvwkAAEBHywBAAAAAAAAFgAUXWUVxmbYT+CMAs0oR8G3MYyyuyoBAwQBAAAAIgYDz3BdBJxvxLD1uljlVV9xoAvqKB/2UpNWWX24J9399i8Y4MWVxVQAAIABAACAAAAAgAAAAABdAAAAAAEA/X0BAgAAAAMGYdOCRm96yd0SRiDX/+ZTRt0RZPQ0Mn6miiSt9+jnGgEAAAAA/f///6WRO03nK6dxPTuSRki5muA+UwhuTd7g+HXizgwb5LeBAAAAAAD9////pZE7Tecrp3E9O5JGSLma4D5TCG5N3uD4deLODBvkt4EBAAAAAP3///8ILAEAAAAAAAAWABQEW1ViWlSbmIdm9/xxbVN7UPXqAiwBAAAAAAAAFgAU+IEm+ZO/4N2axFy8hxZT/wtdTRQsAQAAAAAAABYAFNODZUmp55aIlsGR7xtbn/Wi9DT6LAEAAAAAAAAWABTQyeXI/+h+9lDU/eZwDi8pV6f0uSwBAAAAAAAAFgAU4/A6nZA96mbo0UUZeB5aVmgA1AcsAQAAAAAAABYAFJjyinLYV7IAZSOQGWybXRFKxXuoLAEAAAAAAAAWABRdZRXGZthP4IwCzShHwbcxjLK7KpkYAAAAAAAAFgAUZPz8S66IM0fzHsrc7tL+9kTIBofO/CQAAQEfLAEAAAAAAAAWABQEW1ViWlSbmIdm9/xxbVN7UPXqAgEDBAEAAAAiBgOJsnJY/31qHnpEdTEO2Vlnov5bpTUARCgRgnglWJAFXRjgxZXFVAAAgAEAAIAAAACAAAAAAF8AAAAAAQD9fQECAAAAAwZh04JGb3rJ3RJGINf/5lNG3RFk9DQyfqaKJK336OcaAQAAAAD9////pZE7Tecrp3E9O5JGSLma4D5TCG5N3uD4deLODBvkt4EAAAAAAP3///+lkTtN5yuncT07kkZIuZrgPlMIbk3e4Ph14s4MG+S3gQEAAAAA/f///wgsAQAAAAAAABYAFARbVWJaVJuYh2b3/HFtU3tQ9eoCLAEAAAAAAAAWABT4gSb5k7/g3ZrEXLyHFlP/C11NFCwBAAAAAAAAFgAU04NlSannloiWwZHvG1uf9aL0NPosAQAAAAAAABYAFNDJ5cj/6H72UNT95nAOLylXp/S5LAEAAAAAAAAWABTj8DqdkD3qZujRRRl4HlpWaADUBywBAAAAAAAAFgAUmPKKcthXsgBlI5AZbJtdEUrFe6gsAQAAAAAAABYAFF1lFcZm2E/gjALNKEfBtzGMsrsqmRgAAAAAAAAWABRk/PxLrogzR/Meytzu0v72RMgGh878JAABAR8sAQAAAAAAABYAFNDJ5cj/6H72UNT95nAOLylXp/S5AQMEAQAAACIGApFgNphi/Y+tOwzEH2UfKClwfJeJJJzSgzTqK01oIqC8GODFlcVUAACAAQAAgAAAAIAAAAAAYAAAAAABAP19AQIAAAADBmHTgkZvesndEkYg1//mU0bdEWT0NDJ+pookrffo5xoBAAAAAP3///+lkTtN5yuncT07kkZIuZrgPlMIbk3e4Ph14s4MG+S3gQAAAAAA/f///6WRO03nK6dxPTuSRki5muA+UwhuTd7g+HXizgwb5LeBAQAAAAD9////CCwBAAAAAAAAFgAUBFtVYlpUm5iHZvf8cW1Te1D16gIsAQAAAAAAABYAFPiBJvmTv+DdmsRcvIcWU/8LXU0ULAEAAAAAAAAWABTTg2VJqeeWiJbBke8bW5/1ovQ0+iwBAAAAAAAAFgAU0MnlyP/ofvZQ1P3mcA4vKVen9LksAQAAAAAAABYAFOPwOp2QPepm6NFFGXgeWlZoANQHLAEAAAAAAAAWABSY8opy2FeyAGUjkBlsm10RSsV7qCwBAAAAAAAAFgAUXWUVxmbYT+CMAs0oR8G3MYyyuyqZGAAAAAAAABYAFGT8/EuuiDNH8x7K3O7S/vZEyAaHzvwkAAEBHywBAAAAAAAAFgAU04NlSannloiWwZHvG1uf9aL0NPoBAwQBAAAAIgYC1sS/lSW4MscM8RNpfaFkTeTr3NEapRcqIRsX0yMSYk0Y4MWVxVQAAIABAACAAAAAgAAAAABeAAAAAAEA/X0BAgAAAAMGYdOCRm96yd0SRiDX/+ZTRt0RZPQ0Mn6miiSt9+jnGgEAAAAA/f///6WRO03nK6dxPTuSRki5muA+UwhuTd7g+HXizgwb5LeBAAAAAAD9////pZE7Tecrp3E9O5JGSLma4D5TCG5N3uD4deLODBvkt4EBAAAAAP3///8ILAEAAAAAAAAWABQEW1ViWlSbmIdm9/xxbVN7UPXqAiwBAAAAAAAAFgAU+IEm+ZO/4N2axFy8hxZT/wtdTRQsAQAAAAAAABYAFNODZUmp55aIlsGR7xtbn/Wi9DT6LAEAAAAAAAAWABTQyeXI/+h+9lDU/eZwDi8pV6f0uSwBAAAAAAAAFgAU4/A6nZA96mbo0UUZeB5aVmgA1AcsAQAAAAAAABYAFJjyinLYV7IAZSOQGWybXRFKxXuoLAEAAAAAAAAWABRdZRXGZthP4IwCzShHwbcxjLK7KpkYAAAAAAAAFgAUZPz8S66IM0fzHsrc7tL+9kTIBofO/CQAAQEfLAEAAAAAAAAWABT4gSb5k7/g3ZrEXLyHFlP/C11NFAEDBAEAAAAiBgKkYm7PbWC2qL7lSbbUdha2ITPTiLxViMSSGMvrhNJNhxjgxZXFVAAAgAEAAIAAAACAAAAAAFwAAAAAAA==" signer = PSBTSigner(wallet, PSBT_satvB_1_31, FORMAT_PMOFN) @@ -810,11 +1786,13 @@ def test_sign_sats_vB(m5stickv): == "Inputs (7): ₿ 0.00 002 100\n\nSpend (3): ₿ 0.00 000 997\n\nFee: ₿ 0.00 001 103 (110.7%) ~1.8 sat/vB" ) - wallet = Wallet(Key(MNEMONIC, True, NETWORKS["test"])) + wallet = Wallet(Key(MNEMONIC, TYPE_MULTISIG, NETWORKS["test"])) PSBT_satvB_164_83 = "cHNidP8BAP2rAQIAAAACxvxPDwl8OViT/AUxEgMo58M4h+6v+YQG6vWmKSP3qDsAAAAAAP3////hoVUsAQo/jjpZMWX4x2AksKK2VqWeAQrNMFDpJRhutQEAAAAA/f///wkmAgAAAAAAABepFPTiUaABy92SsOc6XwK0OmNoH1Lbh1gCAAAAAAAAIgAg7SlT+BVDPK6CszkbCElnUdk4PZXlLT7f3ewQK6ybKPVXBAAAAAAAABYAFOPwOp2QPepm6NFFGXgeWlZoANQHJwIAAAAAAAAZdqkUyltliXcHzzJx3FREv3ag6trFlYyIrAcJAAAAAAAAF6kUcqgjtuzhztFzNeRsoZyFKw2kEiCHJgIAAAAAAAAZdqkUiyEsghpJgfU7U7xyG8IhgkhfnLKIrI0MAAAAAAAAIlEgPKIogOE9kof2h9aNfUgVZrvgkD3bcv6ZgfUQQVjoaqzRBAAAAAAAACJRICXgX3C1Oe94STmmG+l0Bkf3jilUrwUfU1t/haMUUBFiMEcAAAAAAAAiACAtAwzag5dZxk2yX3unHhW2yDLgaRrQmXQjLwdS9SWtBL/5KgBPAQQ1h88Egfnuy4AAAAJawo0XlCalJkWVhdDk9Fodo/24Bk6o+YuRs/0CLKYO3AJ3mZ9qQXta3GcftjOQl2kCc8pn5ZH7EeYZ7lhbwLrPURTgxZXFMAAAgAEAAIAAAACAAgAAgE8BBDWHzwQ9FADogAAAAkYXR7HWVGIfNz4fqASjEfYHyTWBUw2PTIJyJVtefKIOApKe3r5nf3uVdD6BfzIM60MDCEBi0QB4iGRj5Ed3oTQ+FBkmx2MwAACAAQAAgAAAAIACAACATwEENYfPBGYTDceAAAACDO4xS6EEHIfyfcteiZSStchtI+zrJ1t2H5Q1mfIlJTsCI8ZPzBlGkmgIjIeIjHfX0ELxP0AT+Vg7Lhjv2lCxSHYUxfW+QDAAAIABAACAAAAAgAIAAIBPAQQ1h88EMYfv9YAAAAL/JKajJAfibVu85oZVYypXk0OV9/FtkjwS1jd6oZhjRwKF9l0WUsTwhpVeJS7jBd4WjUAtTjpz86d+jhUF6QmQPxSzALXuMAAAgAEAAIAAAACAAgAAgE8BBDWHzwRB1T2EgAAAAlQR4OXm+QDdeRxU7hB9Z1PWB4Qnw+18RTysLIsXRevCAsIE8N2Zw7y51fQ4iSAmhnFKBGqGKY5kDRS3rRtgVevPFPEoG+QwAACAAQAAgAAAAIACAACATwEENYfPBLVB3zGAAAACvaHPYepmiFMhK+Rv/e5iS6ZQwDFL451KZyNFEBY/HIEDe7pBxQTkTF+x4jxCcsbWXFtIvmCDzu32qDkNeVfpDFcUL1QD7DAAAIABAACAAAAAgAIAAIBPAQQ1h88EAEqjHIAAAAJUMQhLVe2B1QcyR1Lb7mfJCarIUywo4vzgfFvkZcR9RgJsqYjkSKUGpOJu2KY7UQLGNhemDpqmLky6xU8VlGkrHhTM39D0MAAAgAEAAIAAAACAAgAAgE8BBDWHzwShqicUgAAAAt6rtcow7E6u80Aj2mOIIZZXKPafV8a2X+Fg129sedNEA7BP3O9vCVWJe7nSEIwERlqVOQT74n9502Pod8jvLVB3FPpDCbswAACAAQAAgAAAAIACAACATwEENYfPBK4FICqAAAAC20g/x3iQJ35D5mq+7XWSGOmZwIITFdmRgYtfsvQLayUCPEWPgZTIbvqz0a8JQqI3xYD5hSjn05Adr3zDq0Z73r0UBig12TAAAIABAACAAAAAgAIAAIAAAQB9AgAAAAEugookeJZ2bsoQc1aT2mTarFkbS6KHZ0xS/PR5G00IkAAAAAAA/f///wLA1AEAAAAAACIAILgyrfO56tKA221zLVz01fcaSK7pvjt3uEAGzhBnnkUoEzhsAAAAAAAWABSOuyYZ2K1YUzqx4q6UShww3x6L9I7DJgABASvA1AEAAAAAACIAILgyrfO56tKA221zLVz01fcaSK7pvjt3uEAGzhBnnkUoAQMEAQAAAAEF/TUBUSECUT16jfx6OJwBCtunbTiymINeJ0mJFQ1OHNDunxAnSF4hAs3S1IXWxm8LAyt6qCcFpKi1QkLnCNbjvWQfCGU5o+w8IQL6JM3AbTCJqn//smKIK+HywN1hGtNNIsJt2D2EKwNrKCEC/7CpWhK09def6O+jpeDdqsgdmWwD87sY+LntPo5Vzi0hAxi7c5FKr3SXgIv7nq2u8NdftZu6CvFCvVQmecVAgRrhIQMagegznVzzv94BvpMDTLcd2i8arXvtDda2eqp5nZia/SEDOCPR/ei1x+po6gsWOhb7u/HPplV598QZG2DlCOVatoUhA5eIN5KuprIrKg8SDMjaAFAzuiSWErJDc9HbC40q0GfGIQObgorgQxCVJFOoYHIoaeUdYKzvQsXmxGF7ZEdKgK+2jlmuIgYDOCPR/ei1x+po6gsWOhb7u/HPplV598QZG2DlCOVatoUc4MWVxTAAAIABAACAAAAAgAIAAIAAAAAAAAAAACIGAlE9eo38ejicAQrbp204spiDXidJiRUNThzQ7p8QJ0heHBkmx2MwAACAAQAAgAAAAIACAACAAAAAAAAAAAAiBgL6JM3AbTCJqn//smKIK+HywN1hGtNNIsJt2D2EKwNrKBzF9b5AMAAAgAEAAIAAAACAAgAAgAAAAAAAAAAAIgYCzdLUhdbGbwsDK3qoJwWkqLVCQucI1uO9ZB8IZTmj7DwcswC17jAAAIABAACAAAAAgAIAAIAAAAAAAAAAACIGAxi7c5FKr3SXgIv7nq2u8NdftZu6CvFCvVQmecVAgRrhHPEoG+QwAACAAQAAgAAAAIACAACAAAAAAAAAAAAiBgL/sKlaErT115/o76Ol4N2qyB2ZbAPzuxj4ue0+jlXOLRwvVAPsMAAAgAEAAIAAAACAAgAAgAAAAAAAAAAAIgYDm4KK4EMQlSRTqGByKGnlHWCs70LF5sRhe2RHSoCvto4czN/Q9DAAAIABAACAAAAAgAIAAIAAAAAAAAAAACIGAxqB6DOdXPO/3gG+kwNMtx3aLxqte+0N1rZ6qnmdmJr9HPpDCbswAACAAQAAgAAAAIACAACAAAAAAAAAAAAiBgOXiDeSrqayKyoPEgzI2gBQM7oklhKyQ3PR2wuNKtBnxhwGKDXZMAAAgAEAAIAAAACAAgAAgAAAAAAAAAAAAAEAiQIAAAAB2r0h30illqWy9Otl7Q1XTlb9uLjrJhRhyP/h4EeuGPoAAAAAAP3///8CIrw5sgAAAAAiUSCR+4yczklxYxY7lyyOIWvzyiH7W4BdfpbD5UullxdmnH4pAAAAAAAAIgAgye2OxwLNIQqzm/EtFQLlNe0+cd3GhpDr7y+RKyLm5Lmk4yoAAQErfikAAAAAAAAiACDJ7Y7HAs0hCrOb8S0VAuU17T5x3caGkOvvL5ErIubkuQEDBAEAAAABBf01AVEhAiRLSJSvD7TWcv5EfGdo7NQtXs867sYx1+LvtN3JGwSgIQI/nN8aKAS8fLmkJAHddYEpJG8aI9pJChN+dxdrAm5LsCECU/y+ZtfRmymknEYfcBght6qHH5xwwawfNBgrzrqZWkUhAr9BWrWprasjR2fdngtFfmWq2c4AcMpz7agDflVh4WePIQL8sn3dhWJIIgUY8zEbBoG2qbpWlU4Zf/57/oJpyArRDCEDCjjKfpuT+7tXQDWs4LJJEN0AARKbRbxlp7IbSBhOJ9UhA4QIib/2AxF/nhevOdENoS3ju2CZOLM8fUvc+7gcaoNXIQOG3pMoAhywAeCMB3MjPEKGWjdJAzuKRwnqaqM6A50k+yEDlEZj185yGIl/WpuOHrR2q8RcpahQaTPWt1GNY/5jXiVZriIGAiRLSJSvD7TWcv5EfGdo7NQtXs867sYx1+LvtN3JGwSgHODFlcUwAACAAQAAgAAAAIACAACAAAAAAAMAAAAiBgJT/L5m19GbKaScRh9wGCG3qocfnHDBrB80GCvOuplaRRwZJsdjMAAAgAEAAIAAAACAAgAAgAAAAAADAAAAIgYDlEZj185yGIl/WpuOHrR2q8RcpahQaTPWt1GNY/5jXiUcxfW+QDAAAIABAACAAAAAgAIAAIAAAAAAAwAAACIGA4bekygCHLAB4IwHcyM8QoZaN0kDO4pHCepqozoDnST7HLMAte4wAACAAQAAgAAAAIACAACAAAAAAAMAAAAiBgK/QVq1qa2rI0dn3Z4LRX5lqtnOAHDKc+2oA35VYeFnjxzxKBvkMAAAgAEAAIAAAACAAgAAgAAAAAADAAAAIgYDhAiJv/YDEX+eF6850Q2hLeO7YJk4szx9S9z7uBxqg1ccL1QD7DAAAIABAACAAAAAgAIAAIAAAAAAAwAAACIGAj+c3xooBLx8uaQkAd11gSkkbxoj2kkKE353F2sCbkuwHMzf0PQwAACAAQAAgAAAAIACAACAAAAAAAMAAAAiBgMKOMp+m5P7u1dANazgskkQ3QABEptFvGWnshtIGE4n1Rz6Qwm7MAAAgAEAAIAAAACAAgAAgAAAAAADAAAAIgYC/LJ93YViSCIFGPMxGwaBtqm6VpVOGX/+e/6CacgK0QwcBig12TAAAIABAACAAAAAgAIAAIAAAAAAAwAAAAAAAQH9NQFRIQJv5fQ/lbwUT9wtw6/Eh2Oq16gicOpFq3BG6xcke9ubQiECe00vxV9fm/T+xbMIdQYOGuprslcW4e4E6+NQZS5pz7ghApX2A/BDiIbREOLnj3ijE7Kt632WymP5pMdKObvhnOVEIQLHSZr+ewkz9a4EmXVcdxAKVZnlNz4WW9xV+v0JTBXTaCEC/YMngcz+JMjycuGmtw0w0XqEK/Bn3/TgzQihBTitFWchAw5SiHyBWCloUClD8xJpTS3A9sepdstCOqdHeLfcb0IpIQMk7O/3Y0b6XibLKjDaJB04AJgfXDf3Lk9xp9wrt1eq9SEDWooQ0LGxQrxIWOW5lfW58GcMlVM7dh3vUqq1HB9cyWghA9kjjDpGlvtgO1S18k+gfCGfj+m85Eqy6JNrnwpaaQdtWa4iAgPZI4w6Rpb7YDtUtfJPoHwhn4/pvORKsuiTa58KWmkHbRzgxZXFMAAAgAEAAIAAAACAAgAAgAEAAAACAAAAIgICx0ma/nsJM/WuBJl1XHcQClWZ5Tc+FlvcVfr9CUwV02gcGSbHYzAAAIABAACAAAAAgAIAAIABAAAAAgAAACICAw5SiHyBWCloUClD8xJpTS3A9sepdstCOqdHeLfcb0IpHMX1vkAwAACAAQAAgAAAAIACAACAAQAAAAIAAAAiAgJ7TS/FX1+b9P7Fswh1Bg4a6muyVxbh7gTr41BlLmnPuByzALXuMAAAgAEAAIAAAACAAgAAgAEAAAACAAAAIgICb+X0P5W8FE/cLcOvxIdjqteoInDqRatwRusXJHvbm0Ic8Sgb5DAAAIABAACAAAAAgAIAAIABAAAAAgAAACICAyTs7/djRvpeJssqMNokHTgAmB9cN/cuT3Gn3Cu3V6r1HC9UA+wwAACAAQAAgAAAAIACAACAAQAAAAIAAAAiAgKV9gPwQ4iG0RDi5494oxOyret9lspj+aTHSjm74ZzlRBzM39D0MAAAgAEAAIAAAACAAgAAgAEAAAACAAAAIgIDWooQ0LGxQrxIWOW5lfW58GcMlVM7dh3vUqq1HB9cyWgc+kMJuzAAAIABAACAAAAAgAIAAIABAAAAAgAAACICAv2DJ4HM/iTI8nLhprcNMNF6hCvwZ9/04M0IoQU4rRVnHAYoNdkwAACAAQAAgAAAAIACAACAAQAAAAIAAAAAAAAAAAAAAQH9NQFRIQIVh1X3Xi0VmCSxsoxv8JqZrFaam6TxrGY9vuTnJ3mZDiECVgjUD97iD15YimuJOBa14mzNkqBCLxB4Wi3d3xLZOtchAnjet8DQhW4IgWooKGLhn5mNc48AI4lfcBbsaXcKCdcOIQK7b2u7eyw9XqBiH8NjIaPZvCz2ipRmkqgoiKfx66I1OiEC6szHg0gpJJk2olQnez8UIYHxoOs2hnBUHgzmbYMfikYhAwQ6rLGYVkedZQpFq82ARKhnMx98IgWdLOkD5CxRhB5eIQMS6yFOOszN1bymFkmfn5npNY1cVD/ARFO5GhYeNHdVsiEDVs0UK3uipbTk3CnY7WkbzyLLvXcEOU93FLEN0yQLnAUhA48yO/ywO9oBe+YXAY63aZUxJj+27skzrdSwUW+Cs0pDWa4iAgNWzRQre6KltOTcKdjtaRvPIsu9dwQ5T3cUsQ3TJAucBRzgxZXFMAAAgAEAAIAAAACAAgAAgAEAAAAAAAAAIgIC6szHg0gpJJk2olQnez8UIYHxoOs2hnBUHgzmbYMfikYcGSbHYzAAAIABAACAAAAAgAIAAIABAAAAAAAAACICAlYI1A/e4g9eWIpriTgWteJszZKgQi8QeFot3d8S2TrXHMX1vkAwAACAAQAAgAAAAIACAACAAQAAAAAAAAAiAgOPMjv8sDvaAXvmFwGOt2mVMSY/tu7JM63UsFFvgrNKQxyzALXuMAAAgAEAAIAAAACAAgAAgAEAAAAAAAAAIgIDEushTjrMzdW8phZJn5+Z6TWNXFQ/wERTuRoWHjR3VbIc8Sgb5DAAAIABAACAAAAAgAIAAIABAAAAAAAAACICArtva7t7LD1eoGIfw2Mho9m8LPaKlGaSqCiIp/HrojU6HC9UA+wwAACAAQAAgAAAAIACAACAAQAAAAAAAAAiAgMEOqyxmFZHnWUKRavNgESoZzMffCIFnSzpA+QsUYQeXhzM39D0MAAAgAEAAIAAAACAAgAAgAEAAAAAAAAAIgICeN63wNCFbgiBaigoYuGfmY1zjwAjiV9wFuxpdwoJ1w4c+kMJuzAAAIABAACAAAAAgAIAAIABAAAAAAAAACICAhWHVfdeLRWYJLGyjG/wmpmsVpqbpPGsZj2+5OcneZkOHAYoNdkwAACAAQAAgAAAAIACAACAAQAAAAAAAAAA" signer = PSBTSigner(wallet, PSBT_satvB_164_83, FORMAT_PMOFN) outputs, _ = signer.outputs() assert ( outputs[0] - == "Inputs (2): ₿ 0.00 130 622\n\nSpend (7): ₿ 0.00 009 519\n\nSelf-transfer or Change (2): ₿ 0.00 018 824\n\nFee: ₿ 0.00 102 279 (360.9%) ~165.2 sat/vB" + == "Inputs (2): ₿\u20090.00 130 622\n\nSpend (9): ₿\u20090.00 028 343\n\nFee: ₿\u20090.00 102 279 (360.9%) ~165.2 sat/vB" ) + + # TODO: Add a multisig with descriptor so change can be deteted diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 074b37794..efe9658af 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -8,7 +8,15 @@ def tdata(mocker): from collections import namedtuple from ur.ur import UR from embit.networks import NETWORKS - from krux.key import Key, P2PKH, P2SH_P2WPKH, P2TR + from krux.key import ( + Key, + P2PKH, + P2SH_P2WPKH, + P2TR, + TYPE_SINGLESIG, + TYPE_MULTISIG, + TYPE_MINISCRIPT, + ) TEST_MNEMONIC1 = ( "olympic term tissue route sense program under choose bean emerge velvet absurd" @@ -17,14 +25,20 @@ def tdata(mocker): TEST_MNEMONIC3 = "range fatigue into stadium endless kitchen royal present rally welcome scatter twice" SINGLESIG_KEY = Key( - TEST_MNEMONIC1, False, NETWORKS["main"] + TEST_MNEMONIC1, TYPE_SINGLESIG, NETWORKS["main"] ) # default account=0, script=P2WPKH - LEGACY1_KEY = Key(TEST_MNEMONIC1, False, NETWORKS["main"], "", 1, P2PKH) - NESTEDSW1_KEY = Key(TEST_MNEMONIC1, False, NETWORKS["main"], "", 1, P2SH_P2WPKH) - TAPROOT1_KEY = Key(TEST_MNEMONIC1, False, NETWORKS["main"], "", 1, P2TR) - MULTISIG_KEY1 = Key(TEST_MNEMONIC1, True, NETWORKS["main"]) - MULTISIG_KEY2 = Key(TEST_MNEMONIC2, True, NETWORKS["main"]) - MULTISIG_KEY3 = Key(TEST_MNEMONIC3, True, NETWORKS["main"]) + LEGACY1_KEY = Key(TEST_MNEMONIC1, TYPE_SINGLESIG, NETWORKS["main"], "", 1, P2PKH) + NESTEDSW1_KEY = Key( + TEST_MNEMONIC1, TYPE_SINGLESIG, NETWORKS["main"], "", 1, P2SH_P2WPKH + ) + TAPROOT1_KEY = Key(TEST_MNEMONIC1, TYPE_SINGLESIG, NETWORKS["main"], "", 1, P2TR) + MULTISIG_KEY1 = Key(TEST_MNEMONIC1, TYPE_MULTISIG, NETWORKS["main"]) + MULTISIG_KEY2 = Key(TEST_MNEMONIC2, TYPE_MULTISIG, NETWORKS["main"]) + MULTISIG_KEY3 = Key(TEST_MNEMONIC3, TYPE_MULTISIG, NETWORKS["main"]) + MINISCRIPT_KEY = Key(TEST_MNEMONIC1, TYPE_MINISCRIPT, NETWORKS["main"]) + TAP_MINISCRIPT_KEY = Key( + TEST_MNEMONIC1, TYPE_MINISCRIPT, NETWORKS["main"], script_type=P2TR + ) # SINGLESIG_KEY [55f8fc5d/84h/0h/0h]xpub6DPMTPxGMqdtzMwpqT1dDQaVdyaEppEm2qYSaJ7ANsuES7HkNzrXJst1Ed8D7NAnijUdgSDUFgph1oj5LKKAD5gyxWNhNP2AuDqaKYqzphA # MULTISIG_KEY1 [55f8fc5d/48h/0h/0h/2h]xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy @@ -154,9 +168,15 @@ def tdata(mocker): UNRELATED_SINGLESIG_DESCRIPTOR = "wpkh([55f8fc5d/84h/0h/0h]xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB)" UNRELATED_MULTISIG_DESCRIPTOR = "wsh(sortedmulti(2,[55f8fc5d/48h/0h/0h/2h]xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB,[3e15470d/48h/0h/0h/2h]xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu,[d3a80c8b/48h/0h/0h/2h]xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv))" + UNRELATED_MINISCRIPT_DESCRIPTOR = "wsh(or_d(multi(2,[1f280825/48'/1'/0'/2']tpubDEx7EkaqE8rG5NsCrijASmBjWiNv6teugndQCs4YN6JDS4hpJ3QtSC4ifPAcE7LQXtjRgB96trmEucoLbsiYYMuvLthymAhssZQpEPPb1pU/<0;1>/*,[e1efb2e7/48'/1'/0'/2']tpubDFhm1JYGdsR6Uv7SvXVd6JfjVkYimPDizEwwXRR9EhESpMhx3qL9nVjpfbtPRLzicWhYkMF4mn4AuZ4zYDjNMvuWSqugFBEJnYsMJurmbLM/<0;1>/*),and_v(v:thresh(2,pkh([1f280825/48'/1'/0'/2']tpubDEx7EkaqE8rG5NsCrijASmBjWiNv6teugndQCs4YN6JDS4hpJ3QtSC4ifPAcE7LQXtjRgB96trmEucoLbsiYYMuvLthymAhssZQpEPPb1pU/<2;3>/*),a:pkh([e1efb2e7/48'/1'/0'/2']tpubDFhm1JYGdsR6Uv7SvXVd6JfjVkYimPDizEwwXRR9EhESpMhx3qL9nVjpfbtPRLzicWhYkMF4mn4AuZ4zYDjNMvuWSqugFBEJnYsMJurmbLM/<2;3>/*),a:pkh([b32caab5/48'/1'/0'/2']tpubDEwY4xag4eQabW74PwS8BZb3aYy9mBzBffzBKqS74NjxzaDHodGGqfFLumwQGM5JYExNjs1mG3u8MaeEr94HNmxTaBPHERkoJXEcZ12aPdF/<0;1>/*)),older(144))))#tfk3syfj" + UNRELATED_TAP_MINISCRIPT_DESCRIPTOR = "tr(tpubD6NzVbkrYhZ4Y18xhod7E8V6Sy3YF36bge8HJb4ww1QgTrdkNvCEzcvUmFGQkTJA32gqr3j94iE8vsUzYpv8Pn29JezD9YiYnxgUREhN3QR/<0;1>/*,{and_v(v:multi_a(2,[1f280825/48'/1'/0'/2']tpubDEx7EkaqE8rG5NsCrijASmBjWiNv6teugndQCs4YN6JDS4hpJ3QtSC4ifPAcE7LQXtjRgB96trmEucoLbsiYYMuvLthymAhssZQpEPPb1pU/<2;3>/*,[e1efb2e7/48'/1'/0'/2']tpubDFhm1JYGdsR6Uv7SvXVd6JfjVkYimPDizEwwXRR9EhESpMhx3qL9nVjpfbtPRLzicWhYkMF4mn4AuZ4zYDjNMvuWSqugFBEJnYsMJurmbLM/<2;3>/*,[b32caab5/48'/1'/0'/2']tpubDEwY4xag4eQabW74PwS8BZb3aYy9mBzBffzBKqS74NjxzaDHodGGqfFLumwQGM5JYExNjs1mG3u8MaeEr94HNmxTaBPHERkoJXEcZ12aPdF/<0;1>/*),older(144)),multi_a(2,[1f280825/48'/1'/0'/2']tpubDEx7EkaqE8rG5NsCrijASmBjWiNv6teugndQCs4YN6JDS4hpJ3QtSC4ifPAcE7LQXtjRgB96trmEucoLbsiYYMuvLthymAhssZQpEPPb1pU/<0;1>/*,[e1efb2e7/48'/1'/0'/2']tpubDFhm1JYGdsR6Uv7SvXVd6JfjVkYimPDizEwwXRR9EhESpMhx3qL9nVjpfbtPRLzicWhYkMF4mn4AuZ4zYDjNMvuWSqugFBEJnYsMJurmbLM/<0;1>/*)})#u5clzmqy" UNSORTED_MULTISIG_DESCRIPTOR = "wsh(multi(2,[3e15470d/48h/0h/0h/2h]xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/<0;1>/*,[55f8fc5d/48h/0h/0h/2h]xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/<0;1>/*,[d3a80c8b/48h/0h/0h/2h]xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv/<0;1>/*))" + LIANA_MINISCRIPT_DESCRIPTOR = "wsh(or_d(pk([55f8fc5d/48'/0'/0'/2']xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/<0;1>/*),and_v(v:pkh([3e15470d/48'/0'/0'/2']xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/<0;1>/*),older(6))))#x09nw3rv" + LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR = "tr([55f8fc5d/48'/0'/0'/2']xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/<0;1>/*,and_v(v:pk([3e15470d/48'/0'/0'/2']xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/<0;1>/*),older(6)))#qjluv5ue" + LIANA_TAP_EXPANDING_MINISCRIPT_DESCRIPTOR = "tr(xpub661MyMwAqRbcFHMDceyRcHhEfeDBXneBmbTnqujM6EumzeNcd8wrs3SHGzkETt7dDwqSCmDJx2rz6uKEddXRcYUWuAu6rkaj4L2QuVxqNUS/<0;1>/*,{and_v(v:multi_a(2,[55f8fc5d/48'/0'/0'/2']xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/<2;3>/*,[3e15470d/48'/0'/0'/2']xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/<2;3>/*,[d3a80c8b/48'/0'/0'/2']xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv/<0;1>/*),older(65535)),multi_a(2,[55f8fc5d/48'/0'/0'/2']xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/<0;1>/*,[3e15470d/48'/0'/0'/2']xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/<0;1>/*)})#uyj29ygt" + return namedtuple( "TestData", [ @@ -170,6 +190,8 @@ def tdata(mocker): "MULTISIG_KEY1", "MULTISIG_KEY2", "MULTISIG_KEY3", + "MINISCRIPT_KEY", + "TAP_MINISCRIPT_KEY", "KRUX_LEGACY1_DESCRIPTOR", "KRUX_LEGACY1_XPUB", "KRUX_NESTEDSW1_DESCRIPTOR", @@ -207,7 +229,12 @@ def tdata(mocker): "AMBIGUOUS_MULTISIG_DESCRIPTOR", "UNRELATED_SINGLESIG_DESCRIPTOR", "UNRELATED_MULTISIG_DESCRIPTOR", + "UNRELATED_MINISCRIPT_DESCRIPTOR", + "UNRELATED_TAP_MINISCRIPT_DESCRIPTOR", "UNSORTED_MULTISIG_DESCRIPTOR", + "LIANA_MINISCRIPT_DESCRIPTOR", + "LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR", + "LIANA_TAP_EXPANDING_MINISCRIPT_DESCRIPTOR", ], )( TEST_MNEMONIC1, @@ -220,6 +247,8 @@ def tdata(mocker): MULTISIG_KEY1, MULTISIG_KEY2, MULTISIG_KEY3, + MINISCRIPT_KEY, + TAP_MINISCRIPT_KEY, KRUX_LEGACY1_DESCRIPTOR, KRUX_LEGACY1_XPUB, KRUX_NESTEDSW1_DESCRIPTOR, @@ -257,7 +286,12 @@ def tdata(mocker): AMBIGUOUS_MULTISIG_DESCRIPTOR, UNRELATED_SINGLESIG_DESCRIPTOR, UNRELATED_MULTISIG_DESCRIPTOR, + UNRELATED_MINISCRIPT_DESCRIPTOR, + UNRELATED_TAP_MINISCRIPT_DESCRIPTOR, UNSORTED_MULTISIG_DESCRIPTOR, + LIANA_MINISCRIPT_DESCRIPTOR, + LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, + LIANA_TAP_EXPANDING_MINISCRIPT_DESCRIPTOR, ) @@ -313,7 +347,6 @@ def test_init_singlesig(mocker, m5stickv, tdata): def test_init_multisig(mocker, m5stickv, tdata): from krux.wallet import Wallet - from krux.qr import FORMAT_NONE wallet = Wallet(tdata.MULTISIG_KEY1) assert isinstance(wallet, Wallet) @@ -328,35 +361,152 @@ def test_init_multisig(mocker, m5stickv, tdata): assert wallet.policy is None +def test_init_miniscript(mocker, m5stickv, tdata): + from krux.wallet import Wallet + + wallet = Wallet(tdata.MINISCRIPT_KEY) + assert isinstance(wallet, Wallet) + assert wallet.descriptor is None + assert wallet.label is None + assert wallet.policy is None + + def test_is_multisig(mocker, m5stickv, tdata): from krux.wallet import Wallet + from krux.qr import FORMAT_NONE + # Non multisig keys wallet = Wallet(tdata.SINGLESIG_KEY) assert not wallet.is_multisig() + wallet = Wallet(tdata.TAPROOT1_KEY) + assert not wallet.is_multisig() + + wallet = Wallet(tdata.MINISCRIPT_KEY) + assert not wallet.is_multisig() + + # Multisig key wallet = Wallet(tdata.MULTISIG_KEY1) assert wallet.is_multisig() + # Multisig key with loaded descriptor + wallet.load(tdata.SPECTER_MULTISIG_DESCRIPTOR, FORMAT_NONE) + assert wallet.is_multisig() + + # No key, no descriptor wallet = Wallet(None) assert not wallet.is_multisig() - from krux.qr import FORMAT_NONE + # Sans key: Descriptor only, no key loaded + # Multisig descriptor wallet.load(tdata.SPECTER_MULTISIG_DESCRIPTOR, FORMAT_NONE) assert wallet.is_multisig() + # Single-sig descriptor + wallet = Wallet(None) + wallet.load(tdata.UNAMBIGUOUS_SINGLESIG_DESCRIPTOR, FORMAT_NONE) + assert not wallet.is_multisig() + + # Taproot single-sig descriptor + wallet = Wallet(None) + wallet.load(tdata.KRUX_TAPROOT1_DESCRIPTOR, FORMAT_NONE) + assert not wallet.is_multisig() + + # Miniscript descriptor + wallet = Wallet(None) + wallet.load(tdata.LIANA_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + assert not wallet.is_multisig() + + # Taproot miniscript descriptor + wallet = Wallet(None) + wallet.load(tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + assert not wallet.is_multisig() + + +def test_is_miniscript(mocker, m5stickv, tdata): + from krux.wallet import Wallet + from krux.qr import FORMAT_NONE + + # Non multisig keys + wallet = Wallet(tdata.SINGLESIG_KEY) + assert not wallet.is_miniscript() + + wallet = Wallet(tdata.TAPROOT1_KEY) + assert not wallet.is_miniscript() + + wallet = Wallet(tdata.MULTISIG_KEY1) + assert not wallet.is_miniscript() + + # Miniscript key + wallet = Wallet(tdata.MINISCRIPT_KEY) + assert wallet.is_miniscript() + + # Miniscript key with loaded descriptor + wallet.load(tdata.LIANA_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + assert wallet.is_miniscript() + + # Taproot miniscript key with loaded descriptor + wallet = Wallet(tdata.TAP_MINISCRIPT_KEY) + wallet.load(tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + assert wallet.is_miniscript() + + # No key and no descriptor + wallet = Wallet(None) + assert not wallet.is_miniscript() + + # Non miniscript descriptors + wallet.load(tdata.SPECTER_MULTISIG_DESCRIPTOR, FORMAT_NONE) + assert not wallet.is_miniscript() + + wallet = Wallet(None) + wallet.load(tdata.UNAMBIGUOUS_SINGLESIG_DESCRIPTOR, FORMAT_NONE) + assert not wallet.is_miniscript() + + wallet = Wallet(None) + wallet.load(tdata.KRUX_TAPROOT1_DESCRIPTOR, FORMAT_NONE) + assert not wallet.is_miniscript() + + # Miniscript descriptor + wallet = Wallet(None) + wallet.load(tdata.LIANA_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + assert wallet.is_miniscript() + def test_is_loaded(mocker, m5stickv, tdata): from krux.wallet import Wallet + # Single-sig key wallet = Wallet(tdata.SINGLESIG_KEY) assert not wallet.is_loaded() + # Single-sig key with loaded descriptor + wallet.wallet_data = tdata.SPECTER_SINGLESIG_WALLET_DATA + assert wallet.is_loaded() + + # Multisig key wallet = Wallet(tdata.MULTISIG_KEY1) assert not wallet.is_loaded() + # Multisig key with loaded descriptor wallet.wallet_data = tdata.UR_OUTPUT_MULTISIG_WALLET_DATA assert wallet.is_loaded() + # Miniscript key + wallet = Wallet(tdata.MINISCRIPT_KEY) + assert not wallet.is_loaded() + + # Miniscript key with loaded descriptor + wallet.wallet_data = tdata.LIANA_MINISCRIPT_DESCRIPTOR + assert wallet.is_loaded() + + # Tap miniscript key + wallet = Wallet(tdata.TAP_MINISCRIPT_KEY) + assert not wallet.is_loaded() + + # Tap miniscript key with loaded descriptor + wallet.wallet_data = tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR + assert wallet.is_loaded() + wallet = Wallet(None) assert not wallet.is_loaded() @@ -378,9 +528,10 @@ def test_wallet_qr(mocker, m5stickv, tdata): def test_receive_addresses(mocker, m5stickv, tdata): from krux.wallet import Wallet from krux.qr import FORMAT_PMOFN + from krux.key import TYPE_SINGLESIG cases = [ - ( + ( # Native Segwit tdata.SINGLESIG_KEY, tdata.SPECTER_SINGLESIG_WALLET_DATA, FORMAT_PMOFN, @@ -397,7 +548,58 @@ def test_receive_addresses(mocker, m5stickv, tdata): "bc1q6mlxs236fmyeteummv9x5pmxteh86lkln4emag", ], ), - ( + ( # Legacy Single-sig + tdata.LEGACY1_KEY, + tdata.KRUX_LEGACY1_DESCRIPTOR, + FORMAT_PMOFN, + [ + "1GsaC1sVGGnN7KpYCHUMR4Z8c9F3BzfUAL", + "1CSSWcbgAPNsB5twS2TfACtCqC6PfahgnM", + "1E9U3icgCLbDqEhS4xqtopvXYtRU4UmSoU", + "17mTKjHWb1LNKjEdUUmVXsDFUcEmqWzSw6", + "16Sg1PLRfEoGr2YsH93v3xAfXSTf42vcHi", + "13rNEribNgeARh7p58FXKJxSRYcVg3i5VJ", + "1Ler4fyDfDq23dMbJ1WXRoYr9EcL6QX8KN", + "15dihUfEHdC21UaJhneQFUfPywonD9ZuJe", + "1MSN9AjP8GourmFEmtqTjukqV9Jp2vzgVS", + "1CoSqz4K5xEysnBoPPLukp4gmh2dnx11Wj", + ], + ), + ( # Nested Segwit Single-sig + tdata.NESTEDSW1_KEY, + tdata.KRUX_NESTEDSW1_DESCRIPTOR, + FORMAT_PMOFN, + [ + "3HN5XxXfK5kNNu83GSVc1tAu3CVpBjghcS", + "395RjbYLhWFWC2LarYkDk41BxxiBiJSWEb", + "3EuvWJNACMVswiczged3bHnWjywKA3n3WQ", + "3EZtai8s84WG8MvcUw3xnTHnWKiQUJBLr7", + "36eFaNxjFBTZUHFWHRcKacppmdFy4X1MKR", + "38p4zYbK1KC4KEkid1tVkHr2ruARTCKBJK", + "3P8PcnaFeHn8FCoFwoxPKFGCTzmxXnLJEx", + "33aw8rJwuxtexN8nZ6YiYvJSoNUAeaY8FM", + "35V6by4xd9owyHaZiBhueKCGMUecV9K4d8", + "33eCmbdSneaxYarxtX9mdJpvyoG1NWaKPj", + ], + ), + ( # Taproot Single-sig + tdata.TAPROOT1_KEY, + tdata.KRUX_TAPROOT1_DESCRIPTOR, + FORMAT_PMOFN, + [ + "bc1pkusdqe839xltdn5jk62rv0cx6c4nrrw3hr8rmf3478tnn88qs2ls4g77cl", + "bc1pzlnrsqze6lwcl9w9z5n9x8dfejf47mrdhv5t8pp4gjehe535xftqkwp635", + "bc1pxz7j9cakl53a72hg08g0ug8gtsy9cf86fmh5mtjucyp707jm7mjsx8lagh", + "bc1p2aqrr4wctz0mhcnn4tpcrg0r89pnn7yqqnv8xpeqr3sckkp3s3fqnnpkgh", + "bc1p3tdqgfjk2zn88jshtj698vfphzn8t3lr4l6qhpf38yyvcpevkfeql3p2fs", + "bc1paa4rtwsdf7z9p22wassvhj6zk5zs94xp0rpcrq2452xwzmgejxpquat7gs", + "bc1p93pdqw9v2237pn2xzyks0dd3mnpnf0kagx0hl44u2msz23425w0s68s658", + "bc1pmejm3jyp6d287dn4lx68j966y0783rvwfujus8s3h9dmedfdm98s7xglv7", + "bc1p9dvlkmfzl8qmax47lg22rfluztc0ev7lvs3rl838y0s96p65mwusz08lhv", + "bc1p8fue49l0amhqvsau72gmk3dau2w5ktxkh2njck95rej0v538wj4q40gksu", + ], + ), + ( # Multisig WSH tdata.MULTISIG_KEY1, tdata.SPECTER_MULTISIG_WALLET_DATA, FORMAT_PMOFN, @@ -414,10 +616,53 @@ def test_receive_addresses(mocker, m5stickv, tdata): "bc1qgzqffn9f4fal2ld04wdafakvgrl5gexp744gm8eg58j7a633lf9slzcjup", ], ), + ( # Miniscript WSH + tdata.MINISCRIPT_KEY, + tdata.LIANA_MINISCRIPT_DESCRIPTOR, + FORMAT_PMOFN, + [ + "bc1q3qp2kgjxkmh6dptyppvptjyg8wlvvddyec0cfgpwftn4nyjs8jpslyaynm", + "bc1qt2j45nvg7rup8h73t5syuaxuvzqr4wk3lqdpjw3ew2afdc7ggzlqspnqwj", + "bc1qvs22y63jdy0fdmgesht3yn3d7ruvuahf73yqmuhhx5w6trj6fy6syugd43", + "bc1qckd42m2mttn3gmz2my7c3flj9uujq0ekpkuxqjj2m2lnwgkrzwuqq8ke23", + "bc1qq4lhujwh4rhxmu82svy3cz97h7ays9j2wjmtw2ju8sqynx88pxys2fud4h", + "bc1q8jm9g8t96g8m75ce6uvq2lhp5snfnzmumdq3qh04v8jt5w7dlmvqfjs8f2", + "bc1qa8tc0pwljncccdauv37vka3kxfus9pssn62dwvvjjhye3h0egsjsj2eu27", + "bc1q5rehtcjd7xw9pfv72lnpv7rrtr4pjh93afr0y60hrluauzn2y83qc8p4fe", + "bc1qmz9v9z0acnd7qehmvqje2m7qmr9ytus87xyk8nprzkypg8gy4zksem4dfz", + "bc1qncuaqah98hypkxnpp465mqw7srjgm67s70gkzcs693kv9zp3dv8qa5w8fj", + ], + ), + ( # Miniscript TR + tdata.TAP_MINISCRIPT_KEY, + tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, + FORMAT_PMOFN, + [ + "bc1pq87pvf85eu3stcxgemm2gppt5w99uxgm75fv3l62lnmjaewuaw0qjpeda6", + "bc1purr3u8x4j0ean426yf09ap2xu5xvehyrlh9s3uqxsf3e2w9pxkjsaf9pum", + "bc1pw83dp6vq7tn7965jxxavkd5fa04y8vpz9njtg7ms8ng9l3u0d0uqnqssfa", + "bc1pef8a3pmsdee6vlup2g3mg5903zxq5lykvpwm4v5f4z0swefdrswsle9sy8", + "bc1pfxmm35spmp9s9jstmvwd0vjksura79ez5f68q6c5gewh6q77jwqs7geafk", + "bc1pwwntw7cplgueq5kulek7mqqqr5s6uatd6psgnclp2a8c3rukxgzqmhszr7", + "bc1pc0m0kvav5sddu5a05chzqk9de9z324teh7cdtem9kly6snw8fz4qpp5uwu", + "bc1pedvnetmy08ekk9j3qyw3zussmxy0lc3tlzmvs9qmnxm9yqq827dqhzjv69", + "bc1p40d9u4g7wgnggrykeghztla4l78k38nkc3ekrghsp6evp2awj72slfa8qc", + "bc1pvpwmch3gjcrer395wzswawnfuer4crz7x6vt9g845jqklvgvydkq0hm8gj", + ], + ), ] for case in cases: wallet = Wallet(case[0]) + # Check addresses before descriptor is loadaded in case it is single-sig + if wallet.key.policy_type == TYPE_SINGLESIG: + assert [addr for addr in wallet.obtain_addresses(0, limit=10)] == case[3] + else: + # Ensure an error is raised as the descriptor is not loaded + with pytest.raises(ValueError): + [addr for addr in wallet.obtain_addresses(0, limit=10)] + + # Check addresses after descriptor is loaded wallet.load(case[1], case[2]) assert [addr for addr in wallet.obtain_addresses(0, limit=10)] == case[3] @@ -463,23 +708,6 @@ def test_load_multisig(mocker, m5stickv, tdata): ], }, ), - # TODO: Fix AssertionError (see previous "TODO: Switch to 2-of-3 from keys defined above") - # ( - # tdata.UR_OUTPUT_MULTISIG_WALLET_DATA, - # FORMAT_UR, - # tdata.UR_OUTPUT_MULTISIG_DESCRIPTOR, - # '2 of 3', - # { - # 'type': 'p2wsh', - # 'm': 2, - # 'n': 3, - # 'cosigners': [ - # tdata.MULTISIG_KEY1.xpub(), - # tdata.MULTISIG_KEY2.xpub(), - # tdata.MULTISIG_KEY3.xpub() - # ], - # }, - # ), ( tdata.UR_BYTES_MULTISIG_WALLET_DATA, FORMAT_UR, @@ -570,6 +798,18 @@ def test_load_singlesig_fails_with_multisig_descriptor(mocker, m5stickv, tdata): wallet.load(case[0], case[1]) +def test_load_singlesig_fails_with_miniscript_descriptor(mocker, m5stickv, tdata): + from krux.wallet import Wallet + from krux.qr import FORMAT_NONE + + wallet = Wallet(tdata.SINGLESIG_KEY) + + with pytest.raises(ValueError): + wallet.load(tdata.LIANA_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + with pytest.raises(ValueError): + wallet.load(tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + + def test_load_multisig_fails_with_singlesig_descriptor(mocker, m5stickv, tdata): from krux.wallet import Wallet from krux.qr import FORMAT_NONE, FORMAT_PMOFN @@ -585,6 +825,83 @@ def test_load_multisig_fails_with_singlesig_descriptor(mocker, m5stickv, tdata): wallet.load(case[0], case[1]) +def test_load_multisig_fails_with_miniscript_descriptor(mocker, m5stickv, tdata): + from krux.wallet import Wallet + from krux.qr import FORMAT_NONE + + wallet = Wallet(tdata.MULTISIG_KEY1) + + with pytest.raises(ValueError): + wallet.load(tdata.LIANA_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + with pytest.raises(ValueError): + wallet.load(tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + + +def test_load_miniscript_fails_with_singlesig_descriptor(mocker, m5stickv, tdata): + from krux.wallet import Wallet + from krux.qr import FORMAT_NONE, FORMAT_PMOFN + + wallets = [ + Wallet(tdata.MINISCRIPT_KEY), + Wallet(tdata.TAP_MINISCRIPT_KEY), + ] + + cases = [ + (tdata.SPECTER_SINGLESIG_WALLET_DATA, FORMAT_PMOFN), + (tdata.UNAMBIGUOUS_SINGLESIG_DESCRIPTOR, FORMAT_NONE), + ] + for wallet in wallets: + for case in cases: + with pytest.raises(ValueError): + wallet.load(case[0], case[1]) + + +def test_load_miniscript_fails_with_multisig_descriptor(mocker, m5stickv, tdata): + from krux.wallet import Wallet + from krux.qr import FORMAT_NONE, FORMAT_PMOFN, FORMAT_UR + + wallets = [ + Wallet(tdata.MINISCRIPT_KEY), + Wallet(tdata.TAP_MINISCRIPT_KEY), + ] + + cases = [ + (tdata.SPECTER_MULTISIG_WALLET_DATA, FORMAT_PMOFN), + (tdata.BLUEWALLET_MULTISIG_WALLET_DATA, FORMAT_NONE), + (tdata.UR_OUTPUT_MULTISIG_WALLET_DATA, FORMAT_UR), + (tdata.UR_BYTES_MULTISIG_WALLET_DATA, FORMAT_UR), + ] + + for wallet in wallets: + for case in cases: + with pytest.raises(ValueError): + wallet.load(case[0], case[1]) + + +def test_load_wsh_miniscript_fails_with_tr_miniscript_descriptor( + mocker, m5stickv, tdata +): + from krux.wallet import Wallet + from krux.qr import FORMAT_NONE + + wallet = Wallet(tdata.MINISCRIPT_KEY) + + with pytest.raises(ValueError): + wallet.load(tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + + +def test_load_tr_miniscript_fails_with_wsh_miniscript_descriptor( + mocker, m5stickv, tdata +): + from krux.wallet import Wallet + from krux.qr import FORMAT_NONE + + wallet = Wallet(tdata.TAP_MINISCRIPT_KEY) + + with pytest.raises(ValueError): + wallet.load(tdata.LIANA_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + + def test_load_singlesig_fails_when_key_not_in_descriptor(mocker, m5stickv, tdata): from krux.wallet import Wallet from krux.qr import FORMAT_NONE @@ -605,6 +922,26 @@ def test_load_multisig_fails_when_key_not_in_descriptor(mocker, m5stickv, tdata) wallet.load(tdata.UNRELATED_MULTISIG_DESCRIPTOR, FORMAT_NONE) +def test_load_miniscript_fails_when_key_not_in_descriptor(mocker, m5stickv, tdata): + from krux.wallet import Wallet + from krux.qr import FORMAT_NONE + + wallet = Wallet(tdata.MINISCRIPT_KEY) + + with pytest.raises(ValueError): + wallet.load(tdata.UNRELATED_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + + +def test_load_tr_miniscript_fails_when_key_not_in_descriptor(mocker, m5stickv, tdata): + from krux.wallet import Wallet + from krux.qr import FORMAT_NONE + + wallet = Wallet(tdata.TAP_MINISCRIPT_KEY) + + with pytest.raises(ValueError): + wallet.load(tdata.UNRELATED_TAP_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + + def test_parse_wallet(mocker, m5stickv, tdata): from krux.wallet import parse_wallet, AssumptionWarning @@ -699,6 +1036,16 @@ def test_parse_wallet(mocker, m5stickv, tdata): tdata.UNAMBIGUOUS_SINGLESIG_DESCRIPTOR, None, ), + ( + tdata.LIANA_MINISCRIPT_DESCRIPTOR, + tdata.LIANA_MINISCRIPT_DESCRIPTOR, + None, + ), + ( + tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, + tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, + None, + ), ] case_n = 1 @@ -709,10 +1056,43 @@ def test_parse_wallet(mocker, m5stickv, tdata): descriptor, label = parse_wallet(case[0]) except AssumptionWarning as e: descriptor, label = parse_wallet(case[0], allow_assumption=e.args[1]) - assert descriptor.to_string() == case[1] + assert descriptor.to_string() == case[1].split("#")[0].replace("'", "h") assert label == case[2] +def test_provably_unspendable(mocker, m5stickv, tdata): + from krux.wallet import Wallet + from krux.qr import FORMAT_NONE + + wallet = Wallet(tdata.TAP_MINISCRIPT_KEY) + wallet.load(tdata.LIANA_TAP_EXPANDING_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + assert wallet.is_loaded() + + +def test_provably_unspendable_wrong_nums(mocker, m5stickv, tdata): + from krux.wallet import Wallet + from krux.qr import FORMAT_NONE + + WRONG_NUMS_DESCRIPTOR = "tr(xpub661MyMwAqRbcFHMDceyRcHhEfeDBXneBmbTnqujM6EumzeNcd8wrs3SHGzkETt7dEBzARS6NBwQaD311yasAjXYyDLCPvCKtRbeFre9vaLv/<0;1>/*,{and_v(v:multi_a(2,[55f8fc5d/48'/0'/0'/2']xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/<2;3>/*,[3e15470d/48'/0'/0'/2']xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/<2;3>/*,[d3a80c8b/48'/0'/0'/2']xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv/<0;1>/*),older(65535)),multi_a(2,[55f8fc5d/48'/0'/0'/2']xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/<0;1>/*,[3e15470d/48'/0'/0'/2']xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/<0;1>/*)})" + + wallet = Wallet(tdata.TAP_MINISCRIPT_KEY) + with pytest.raises(ValueError, match="Internal key not provably unspendable"): + wallet.load(WRONG_NUMS_DESCRIPTOR, FORMAT_NONE) + assert not wallet.is_loaded() + + +def test_provably_unspendable_non_deterministic_chain_code(mocker, m5stickv, tdata): + from krux.wallet import Wallet + from krux.qr import FORMAT_NONE + + NON_DETERMINISTIC_CHAIN_CODE = "tr(xpub661MyMwAqRbcFhaVQsdthkuGZQS3e9MENERDseTmnmhX2dFdgitmaFGLSPeXtcRzQ8jQaG3XCYPUknq7jX86V1qU6p981ripVVbvYnE5XpV/<0;1>/*,{and_v(v:multi_a(2,[55f8fc5d/48'/0'/0'/2']xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/<2;3>/*,[3e15470d/48'/0'/0'/2']xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/<2;3>/*,[d3a80c8b/48'/0'/0'/2']xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv/<0;1>/*),older(65535)),multi_a(2,[55f8fc5d/48'/0'/0'/2']xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/<0;1>/*,[3e15470d/48'/0'/0'/2']xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/<0;1>/*)})" + + wallet = Wallet(tdata.TAP_MINISCRIPT_KEY) + with pytest.raises(ValueError, match="Internal key not provably unspendable"): + wallet.load(NON_DETERMINISTIC_CHAIN_CODE, FORMAT_NONE) + assert not wallet.is_loaded() + + def test_parse_wallet_raises_errors(mocker, m5stickv, tdata): from krux.wallet import parse_wallet from ur.ur import UR @@ -779,6 +1159,18 @@ def test_parse_address(mocker, m5stickv, tdata): "bitcoin:bc1qx2zuday8d6j4ufh4df6e9ttd06lnfmn2cuz0vn?message=test", "bc1qx2zuday8d6j4ufh4df6e9ttd06lnfmn2cuz0vn", ), + ( + "BC1QX2ZUDAY8D6J4UFH4DF6E9TTD06LNFMN2CUZ0VN", + "bc1qx2zuday8d6j4ufh4df6e9ttd06lnfmn2cuz0vn", + ), + ( + "bitcoin:BC1QX2ZUDAY8D6J4UFH4DF6E9TTD06LNFMN2CUZ0VN?message=test", + "bc1qx2zuday8d6j4ufh4df6e9ttd06lnfmn2cuz0vn", + ), + ( + "BITCOIN:BC1QX2ZUDAY8D6J4UFH4DF6E9TTD06LNFMN2CUZ0VN?message=test", + "bc1qx2zuday8d6j4ufh4df6e9ttd06lnfmn2cuz0vn", + ), ( "bitcoin:32iCX1pY1iztdgM5qzurGLPMu5xhNfAUtg?message=test", "32iCX1pY1iztdgM5qzurGLPMu5xhNfAUtg", diff --git a/vendor/embit b/vendor/embit index 84cce66fb..ce7c19b5f 160000 --- a/vendor/embit +++ b/vendor/embit @@ -1 +1 @@ -Subproject commit 84cce66fb831fa6d625fb73f28e03605f3c04e28 +Subproject commit ce7c19b5f8b5c116dd78478cc97847a62c938270