diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..73637522 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,40 @@ +[run] +branch = True + +[report] +skip_empty = True +skip_covered = True + +# Omit; need a different approach to test modules with hardware dependencies +omit = + */__init__.py + */tests/* + */pyzbar/* + */gui/* + +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + def __str__ + if self\.debug + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: + + # Don't complain about abstract methods, they aren't run: + @(abc\.)?abstractmethod + + +[html] +directory = coverage_html_report +skip_empty = True +skip_covered = False diff --git a/.gitignore b/.gitignore index 0471f989..e165f9cc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,7 @@ src/seedsigner.egg-info/ .nova .vscode src/seedsigner/models/settings_definition.json -*.mo +.idea +.coverage +seedsigner-screenshots +.mo \ No newline at end of file diff --git a/README.md b/README.md index cb81cbaf..448bf69f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ +# jdlcdl branch: kdmukai/initial_multilanguage + +My self-directed exploration, from mid November through mid December 2022, to carry-on where kdmukai left-off this past summer, in similar style, regarding internationalization/localization of seedsigner codebase (w/ exploration focus in locale 'fr'). Note: requirements.txt has new dependencies; also, follow instructions in babel/ to build binary message catalogs. + +--------------- + # Build an offline, airgapped Bitcoin signing device for less than $50! ![Image of SeedSigners in Open Pill Enclosures](docs/img/Open_Pill_Star.JPG)![Image of SeedSigner in an Orange Pill enclosure](docs/img/Orange_Pill.JPG) @@ -17,45 +23,47 @@ # Project Summary -The goal of SeedSigner is to lower the cost and complexity of Bitcoin multi-signature wallet use. To accomplish this goal, SeedSigner offers anyone the opportunity to build a verifiably air-gapped, stateless Bitcoin signing device using inexpensive, publicly available hardware components (usually < $50). SeedSigner helps users save with Bitcoin by assisting with trustless private key generation and multi-signature wallet setup, and helps users transact with Bitcoin via a secure, air-gapped QR-exchange signing model. +The goal of SeedSigner is to lower the cost and complexity of Bitcoin multi-signature wallet use. To accomplish this goal, SeedSigner offers anyone the opportunity to build a verifiably air-gapped, stateless Bitcoin signing device using inexpensive, publicly available hardware components (usually < $50). SeedSigner helps users save with Bitcoin by assisting with trustless private key generation and multisignature (aka "multisig") wallet setup, and helps users transact with Bitcoin via a secure, air-gapped QR-exchange signing model. -Additional information about the project can be found at [seedsigner.com](https://seedsigner.com). +Additional information about the project can be found at [SeedSigner.com](https://seedsigner.com). You can follow [@SeedSigner](https://twitter.com/SeedSigner) on Twitter for the latest project news and developments. If you have specific questions about the project, our [Telegram Group](https://t.me/joinchat/GHNuc_nhNQjLPWsS) is a great place to ask them. ### Feature Highlights: -* Calculate word 12/24 of a BIP39 seed phrase -* Create a 24-word BIP39 seed phrase with 99 dice rolls -* Create a 24-word BIP39 seed phrase by taking a digital photo -* Temporarily store up to 3 seed phrases while device is powered -* Guided interface to manually create a SeedQR for instant input [(demo video here)](https://youtu.be/c1-PqTNx1vc) -* BIP39 passphrase / word 25 support -* Native Segwit Multisig XPUB generation w/ QR display -* Scan and parse transaction data from animated QR codes +* Calculate the final word (aka checksum) of a 12- or 24-word BIP39 seed phrase +* Create a 24-word BIP39 seed phrase with 99 dice rolls or a 12-word with 50 rolls [(Verifying dice seed generation)](docs/dice_verification.md) +* Create a 12- or 24-word BIP39 seed phrase via image entropy from the onboard camera +* Temporarily stores seeds in memory while the device is powered; all memory is wiped when power is removed +* SD card removable after boot to ensure no secret data can be written to it +* Guided interface to manually transcribe a seed to the SeedQR format for instant seed loading [(demo video here)](https://youtu.be/c1-PqTNx1vc) +* BIP39 passphrase (aka "word 25") support +* Native Segwit Multisig XPUB generation +* PSBT-compliant; scan and parse transaction data from animated QR codes * Sign transactions & transfer XPUB data using animated QR codes [(demo video here)](https://youtu.be/LPqvdQ2gSzs) -* Live preview during photo-to-seed and QR scanning UX +* Live preview during image entropy seed generation and QR scanning UX * Optimized seed word entry interface * Support for Bitcoin Mainnet & Testnet * Support for custom user-defined derivation paths * On-demand receive address verification +* Address Explorer for single sig and multisig wallets * User-configurable QR code display density * Responsive, event-driven user interface ### Considerations: * Built for compatibility with Specter Desktop, Sparrow, and BlueWallet Vaults * Device takes up to 60 seconds to boot before menu appears (be patient!) -* Always test your setup before transfering larger amounts of bitcoin (try testnet first!) +* Always test your setup before transferring larger amounts of bitcoin (try Testnet first!) * Taproot not quite yet supported * Slightly rotating the screen clockwise or counter-clockwise should resolve lighting/glare issues * If you think SeedSigner adds value to the Bitcoin ecosystem, please help us spread the word! (tweets, pics, videos, etc.) ### Planned Upcoming Improvements / Functionality: -* Single-sig and multi-sig change address verification -* Re-imagined, graphically-focused user interface * Multi-language support -* Customized Linux live-boot OS to allow MicroSD card removal +* Significantly faster boot time +* Reproducible builds +* Port to MicroPython to broaden the range of compatible hardware to include low-cost microcontrollers * Other optimizations based on user feedback! --------------- @@ -64,7 +72,7 @@ If you have specific questions about the project, our [Telegram Group](https://t To build a SeedSigner, you will need: -* Raspberry Pi Zero (preferably version 1.3 with no WiFi/Bluetooth capability, but any Raspberry Pi 2/3/4 or Zero model will work) +* Raspberry Pi Zero (preferably version 1.3 with no WiFi/Bluetooth capability, but any Raspberry Pi 2/3/4 or Zero model will work, Raspberry Pi 1 devices will require a hardware modification to the Waveshare LCD Hat, as per the [instructions here](./docs/legacy_hardware.md)) * Waveshare 1.3" 240x240 pxl LCD (correct pixel count is important, more info at https://www.waveshare.com/wiki/1.3inch_LCD_HAT) * Pi Zero-compatible camera (tested to work with the Aokin / AuviPal 5MP 1080p with OV5647 Sensor) @@ -77,60 +85,180 @@ Notes: # Software Installation -## Special Note on Minimizing Trust +## A Special Note On Minimizing Trust As is the nature of pre-packaged software downloads, downloading and using the prepared SeedSigner release images means implicitly placing trust in the individual preparing those images; in our project the release images are prepared and signed by the eponymous creator of the project, SeedSigner "the person". That individual is additionally the only person in possession of the PGP keys that are used to sign the release images. However, one of the many advantages of the open source software model is that the need for this kind of trust can be negated by our users' ability to (1) review the project's source code and (2) assemble the operating image necessary to use the software themselves. From our project's inception, instructions to build a SeedSigner operating image (using precisely the same process that is used to create the prepared release images) have been made availabile. We have put a lot of thought and work into making these instructions easy to understand and follow, even for less technical users. These instructions can be found [here](docs/manual_installation.md). ## Downloading the Software -The quickest and easiest way to install the software is to download the most recent "seedsigner_X_X_X.zip" file in the [software releases](https://github.com/SeedSigner/seedsigner/releases) section of this repository. + +Download the current Version (0.6.0) software image that is compatible with your Raspberry Pi Hardware. The Pi Zero 1.3 is the most common and recommended board. +| Board | Download Image Link/Name | +| --------------------- | --------------------------------- | +|**[Raspberry Pi Zero 1.3](https://www.raspberrypi.com/products/raspberry-pi-zero/)** |[`seedsigner_os.0.6.0.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi0.img) | +|[Raspberry Pi Zero W](https://www.raspberrypi.com/products/raspberry-pi-zero-w/) |[`seedsigner_os.0.6.0.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi0.img) | +|[Raspberry Pi Zero 2 W](https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/) |[`seedsigner_os.0.6.0.pi02w.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi02w.img) | +|[Raspberry Pi 2 Model B](https://www.raspberrypi.com/products/raspberry-pi-2-model-b/) |[`seedsigner_os.0.6.0.pi2.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi2.img) | +|[Raspberry Pi 3 Model B](https://www.raspberrypi.com/products/raspberry-pi-3-model-b/) |[`seedsigner_os.0.6.0.pi02w.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi02w.img) | +|[Raspberry Pi 4 Model B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/) |[`seedsigner_os.0.6.0.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi4.img) | +|[Raspberry Pi 400](https://www.raspberrypi.com/products/raspberry-pi-400-unit/) |[`seedsigner_os.0.6.0.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner_os.0.6.0.pi4.img) | -After downloading the .zip file, extract the seedsigner .img file, and write it to a MicroSD card (at least 4GB in size or larger). Then install the MicroSD in the assembled hardware and off you go. +Note: If you have physically removed the WiFi component from your board, you will still use the image file of the original(un-modified) hardware. (Our files are compiled/based on the *processor* architecture). Although it is better to spend a few minutes upfront to determine which specific Pi hardware/model you have, if you are still unsure which hardware you have, you can try using the pi0.img file. Making an incorrect choice here will not ruin your board, because this is software, not firmware. -## Verifying the Software -You can verify the data integrity and authenticity of the latest release with as little as three commands. This process assumes that you know [how to navigate on a terminal](https://terminalcheatsheet.com/guides/navigate-terminal) and have navigated to the folder where you have these four relevant files present: (This will most likely be your Downloads folder.) +**also download** these 2 signature verification files to the same folder +[The Plaintext manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner.0.6.0.sha256) +[The Signature of the manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.6.0/seedsigner.0.6.0.sha256.sig) -* seedsigner_pubkey.gpg (from the main folder of this repo) -* seedsigner_0_4_6.img.zip (from the software release) -* seedsigner_0_4_6.img.zip.sha256 (from the software release) -* seedsigner_0_4_6.img.zip.sha256.sig (from the software release) +Users familiar with older versions of the SeedSigner software might be surprised with how fast their software downloads now are, because since version 0.6.0 the software image files are now 100x smaller! Each image file is now under 42 Megabytes so your downloads and verifications will be very quick now (and might even seem *too* quick)! -**Note:** The specific version number of the files in your folder might not match the above exactly, but their overall format and amount should be the same. +Once the files have all finished downloading, follow the steps below to verify the download before continuing on to write the software onto a MicroSD card. Next, insert the MicroSD into your assembled hardware and connect the USB power. Allow about 45 seconds for our logo to appear, and then you can begin using your SeedSigner! + +[Our previous software versions are available here](https://github.com/SeedSigner/seedsigner/releases). Choose a specific version and then expand the *Assets* sub-heading to display the .img file binary and also the 2 associated signature files. **Note:** The prior version files will have lower numbers than the scripts and examples provided in this document, but the naming format will be the same, so you can edit them as required for signature verification etc. + + +## Verifying that the downloaded files are authentic (optional but highly recommended!) + +You can quickly verify that the software you just downloaded is both authentic and unaltered, by following these instructions. +We assume you are running the commands from a computer where both [GPG](https://gnupg.org/download/index.html) and [shasum](https://command-not-found.com/shasum) are already installed, and that you also know [how to navigate on a terminal](https://terminalcheatsheet.com/guides/navigate-terminal). + + +### Step 1. Verify that the signature (.sig) file is genuine: + +Run GPG's *fetch-keys* command to import the SeedSigner projects public key from the popular online keyserver called *Keybase.io*, into your computers *keychain*. -This process also assumes you are running the commands from a system where both [GPG](https://gnupg.org/download/index.html) and [shasum](https://command-not-found.com/shasum) are installed and working. -First make sure that the public key is present in your keychain: ``` -gpg --import seedsigner_pubkey.gpg +gpg --fetch-keys https://keybase.io/seedsigner/pgp_keys.asc ``` -This command will import the public key, or return: +The result should confirm that 1 key was *either* imported or updated. *Ignore* any key ID's or email addresses shown. + +![SS - Fetchkeys-Keybase PubKey import with Fingerprint shown (New import or update of the key)v3-100pct](https://user-images.githubusercontent.com/91296549/221334414-adc3616c-462e-490e-8492-3dfee367d13a.jpg) + +Next, you will run the *verify* command on the signature (.sig) file. (*Verify* must be run from inside the same folder that you downloaded the files into earlier. The `*`'s in this command will auto-fill the version from your current folder, so it should be copied and pasted as-is.) ``` -key <...> not changed +gpg --verify seedsigner.0.6.*.sha256.sig ``` -Now you can verify the authenticity of the small text file containing the release's SHA256 hash with the command: -``` -gpg --verify seedsigner_0_*_*.img.zip.sha256.sig -``` -**Note:** The `*`s in the command above allow the terminal to auto-populate the command with the version number you have in the folder you are in. It should be copied and pasted as is. +When the verify command completes successfully, it should display output like this: +
+![SS - Verify Command - GPG on Linux - Masked_v4-100pct](https://user-images.githubusercontent.com/91296549/221334135-8ad1f1af-26d2-429a-91ce-ad41703ed38c.jpg) +The result must display "**Good signature**". Ignore any email addresses - *only* matching Key fingerprints count here. Stop immediately if it displays "*Bad signature*"! +
-The reponse to this command should include the text: -``` -Good signature from "seedsigner " [unknown] -``` -The previous command validates that aforementioned small text file was signed using the private key that matches the published public key associated with the project (an early timestamped record of this public/private key's creation can be found in this [tweet](https://twitter.com/SeedSigner/status/1389617642286329856?s=20)). +On the *last* output line, look at your *rightmost* 16 characters (the 4 blocks of 4). +**Crucially, we must now check WHO that Primary key fingerprint /ID belongs to.** We will start by looking at Keybase.io to see if it is the *SeedSigner project* 's public key or not. + +
About the warning message: +

Since you are about to match the outputted fingerprint/ID against the proofs at Keybase.io/SeedSigner, and thereby confirm who the pubkey really belongs to-, you can safely ignore this warning message: -The last step is to make sure the .zip file that you've downloaded, and that contains the released software, is a perfect match to the software that was published by the holder of the private key in the last step. The command for this step is: ``` -shasum -a 256 -c seedsigner_0_*_*.img.zip.sha256 +> WARNING: This key is not certified with a trusted signature! +> There is no indication that the signature belongs to the owner. + ``` +

+
+
+ +
More about how the verify command works: +

+The verify command will attempt to decrypt the signature file (sha256.sig) by trying each public key already imported into your computer. If the public key we just imported (via fetch-keys), manages to: (a) successfully decrypt the .sig file , and (b), that result matches exactly to the clear-text equivalent (.sha256) of the .sig file, then its "a good signature"! + +Crucially, we must still manually check who *exactly* owns the Key ID which gave us that "Good signature". Thats what the warning message means- Who does the matching key really belong to? We will start by looking at keybase.io to see if it is "The SeedSigner project"'s public Key or not. +Note that it is the file hashes of .sig and .sha256 that *verify* compares, not their raw contents. + +

+
+
+ +Now to determine ***who*** the Public key ID belongs to: Goto [Keybase.io/SeedSigner](https://keybase.io/seedsigner) +
+![SS - Keybase Website PubKey visual matching1_Cropped-80pct](https://user-images.githubusercontent.com/91296549/215326193-97c84e35-5570-4e52-bf3f-e86d367c8908.jpg) + + + +**You must now *manually* compare: The 16 character fingerprint ID (as circled in red above) to, those *rightmost* 16 characters from your *verify* command.** + +**If they match exactly, then you have successfully confirmed that your .sig file is authentically from the SeedSigner Project!** +
+ +
Learn more about how keybase.io helps you check that someone (online) is who they say they are: +

+Keybase.io allows you to independently verify that the public key saved on Keybase.io, is both authentic and that it belongs to the organization it claims to represent. + Keybase has already checked the three pubkey file locations cryptographically when they were saved there. You can further verify the key publications if you would like: + + - *via Keybase*: By clicking on any of the three blue badges to see that the "proof" was published at that location. (The blue badge marked as tweet, is in the most human-readable form and it is also a bi-directional link on Twitter) +or, + - *without keybase (out-of-band)*: By using these 3 links directly: [Twitter](https://twitter.com/SeedSigner/status/1530555252373704707), [Github](https://gist.github.com/SeedSigner/5936fa1219b07e28a3672385b605b5d2) and [SeedSigner.com](https://seedsigner.com/keybase.txt). This method can be used if you would like to make an even deeper, independent inspection without relying on Keybase at all, or if the Keybase.io site is no longer valid or it is removed entirely. + +Once you have used one of these methods, you will know if the Public Key stored on Keybase, is genuinely from the SeedSinger Project or not. +

+
+
+ +If the two ID's do *not* match, then you must stop here immediately. Do not continue. Contact us for assistance in the Telegram group address above. + +
+ +### Step 2. Verifying that the *software images/binaries* are genuine + +Now that you have confirmed that you do have the real SeedSigner Project's Public Key (ie the 16 characters match) - you can return to your terminal window. Running the *shasum* command, is the final verification step and will confirm (via file hashing) that the software code/image files, were also not altered since publication, or even during your download process. +(Prior to version 0.6.0 , your verify command will check the .zip file which contains the binary files.) + + **On Linux or OSX:** Run this command ``` -The reponse to this command should include the text: -``` -seedsigner_0_4_6.img.zip: OK +shasum -a 256 --ignore-missing --check seedsigner.0.6.*.sha256 ``` -There are other steps you can take to verify the software, including examining the hash value in the .sha256 text file, but this one has been documented here because it seems the simplest for most people to follow. Please recognize that this process can only validate the software to the extent that the entity that first published the key is an honest actor, and assumes the private key has remained uncompromised and is not being used by a malicious actor. +**On Windows (inside Powershell):** Run this command +``` +CertUtil -hashfile seedsigner_os.0.6.0.Insert_Your_Pi_Models_binary_here_For_Example_pi02w.img SHA256 +``` +On Windows, you must then manually compare the resulting file hash value to the corresponding hash value shown inside the .SHA256 cleartext file. +
+ +Wait up to 30 seconds for the command to complete, and it should display: +``` +seedsigner_os.0.6.x.[Your_Pi_Model_For_Example:pi02w].img: OK +``` +**If you receive the "OK" message** for your **seedsigner_os.0.6.x.[Your_Pi_Model_For_Example:pi02w].img file**, as shown above, then your verification is fully complete! +**All of your downloaded files have now been confirmed as both authentic and unaltered!** You can proceed to create/write your MicroSD card😄😄 !! + +If your file result shows "FAILED", then you must stop here immediately. Do not continue. Contact us for assistance at the Telegram group address above. + +
+ +Please recognize that this process can only validate the software to the extent that the entity that first published the key is an honest actor, and their private key is not compromised or somehow being used by a malicious actor. +
+
+ + +## Writing the software onto your MicroSD card + +To write the SeedSigner software onto your MicroSD card, there are a few options available: +| Application | Description | Platform and official Source | +|--------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------| +| Balena Etcher | The application is called Etcher, and the company that wrote it is called Balena. Hence *Etcher by Balena* or *Balena Etcher* | [Available for Windows, Mac and Linux](https://www.balena.io/etcher#download-etcher) | +| Raspberry Pi Imager | Produced by the Raspberry Pi organization. | [Available for Windows, Mac and Linux](https://www.raspberrypi.com/software/) | +| DD Command Line Utility | Built-in to Linux and MacOS, the DD (Data Duplicator) is a tool for advanced users. If not used carefully it can accidentally format the incorrect disk! | Built-in to Linux and MacOS | + +Be sure to download the software from the genuine publisher. +Either of the Etcher or Pi Imager software is recommended. Some SeedSigner users have reported a better experience with one or the other. So, if the one application doesn’t work well for your particular machine, then please try the other one. +
+### **General Considerations:** +The writing and verify steps are very quick from version 0.6.0 upwards, so please pay close attention to your screen. +Make sure to set any write-protection physical slider on the MicroSD Card Adapter to UN-locked. +You also don’t need to pre-format the MicroSD beforehand. You *dont* need to unzip any .zip file beforehand. +Current Etcher and Pi Imager software will perform a verify action (by default) to make sure the card was written successfully! Watching for that verify step to complete successfully, can save you a lot of headaches if you later need to troubleshoot issues where your SeedSigner device doesn’t boot up at power on. +Writing the MicroSd card is also known as flashing. +It will overwrite everything on the MicroSD card. +If the one application fails for you, then please try again using our other recommended application. +Advanced users may want to try the Linux/MacOS *DD* command instead of using Etcher or Pi Imager, however, a reminder is given that DD can overwrite the wrong disk if you are not careful ! +#### **Specific considerations for Windows users:** +Use the Pi imager software as your first choice on Windows. Windows can sometimes flag the writing of a MicroSD as risky behaviour and hence it may prevent this activity. If this happens, your writing/flashing will fail, hang or wont even begin, in which case you should to try to run the Etcher/Pi-Imager app "As administrator", (right-click and choose that option). It can also be blocked by windows security in some cases, so If you have the (non-default) *Controlled Folder Access* option set to active, try turning that *off* temporarily. + + + --------------- @@ -154,7 +282,7 @@ The upper and lower portions of the enclosure can be printed using a standard FD * [Lil Pill](https://cults3d.com/en/3d-model/gadget/lil-pill-seedsigner-case) by @_CyberNomad * [OrangeSurf Case](https://github.com/orangesurf/orangesurf-seedsigner-case) by @OrangeSurfBTC -* [PS4 Seedsigner](https://www.thingiverse.com/thing:5363525) by @Silexperience +* [PS4 SeedSigner](https://www.thingiverse.com/thing:5363525) by @Silexperience * [OpenPill Faceplate](https://www.printables.com/en/model/179924-seedsigner-open-pill-cover-plates-digital-cross-jo) by @Revetuzo * [Waveshare CoverPlate](https://cults3d.com/en/3d-model/various/seedsigner-coverplate-for-waveshare-1-3-inch-lcd-hat-with-240x240-pixel-display) by @Adathome1 @@ -170,16 +298,35 @@ You can use SeedSigner to export your seed to a hand-transcribed SeedQR format t Standard SeedQR templates: -* [12-word SeedQR template (25x25)](docs/seed_qr/printable_templates/12words_seedqr_template.pdf) -* [24-word SeedQR template (29x29)](docs/seed_qr/printable_templates/24words_seedqr_template.pdf) -* [Baseball card template: 24-word SeedQR (29x29)](docs/seed_qr/printable_templates/Seed_QR_Card.pdf) +* [12-word SeedQR template dots (25x25)](docs/seed_qr/printable_templates/dots_25x25.pdf) +* [24-word SeedQR template dots (29x29)](docs/seed_qr/printable_templates/dots_29x29.pdf) +* [12-word SeedQR template grid (25x25)](docs/seed_qr/printable_templates/grid_25x25.pdf) +* [24-word SeedQR template grid (29x29)](docs/seed_qr/printable_templates/grid_29x29.pdf) +* [Baseball card template: 24-word SeedQR (29x29)](docs/seed_qr/printable_templates/trading_card_29x29_w24words.pdf) CompactSeedQR templates: -* [12-word CompactSeedQR template (21x21)](docs/seed_qr/printable_templates/compact_seedqr/12words_compactseedqr_template.pdf) -* [24-word CompactSeedQR template (25x25)](docs/seed_qr/printable_templates/compact_seedqr/24words_compactseedqr_template.pdf) +* [12-word CompactSeedQR template dots (21x21)](docs/seed_qr/printable_templates/dots_21x21.pdf) +* [24-word CompactSeedQR template dots (25x25)](docs/seed_qr/printable_templates/dots_25x25.pdf) +* [12-word CompactSeedQR template grid (21x21)](docs/seed_qr/printable_templates/grid_21x21.pdf) +* [24-word CompactSeedQR template grid (25x25)](docs/seed_qr/printable_templates/grid_25x25.pdf) +* [Baseball card template: 12-word Compact SeedQR (21x21)](docs/seed_qr/printable_templates/trading_card_21x21_w12words.pdf) +* [Baseball card template: 24-word Compact SeedQR (25x25)](docs/seed_qr/printable_templates/trading_card_25x25_w24words.pdf) -_note: CompactSeedQR is an advanced feature that can be enabled in Settings_ + +2-sided SeedQR templates - 8 per sheet +Printing settings - (2-sided)("flip on long edge")("Actual Size") +If printing on cardstock, adjust your printer settings via its control panel + +A4 templates(210mm * 297mm): +* [21x21 - stores 12-word seeds ONLY in CompactSeedQR format ONLY](docs/seed_qr/printable_templates/21x21_A4_trading_card_2sided.pdf) +* [25x25 - stores 12-word or 24 word seeds depending on SeedQR format](docs/seed_qr/printable_templates/25x25_A4_trading_card_2sided.pdf) +* [29x29 - stores 24-word seeds ONLY as plaintext SeedQR format ONLY](docs/seed_qr/printable_templates/29x29_A4_trading_card_2sided.pdf) + +Letter templates(8.5in * 11in): +* [21x21 - stores 12-word seeds ONLY in CompactSeedQR format ONLY](docs/seed_qr/printable_templates/21x21_letter_trading_card_2sided.pdf) +* [25x25 - stores 12-word or 24 word seeds depending on format](docs/seed_qr/printable_templates/25x25_letter_trading_card_2sided.pdf) +* [29x29 - stores 24-word seeds ONLY as plaintext SeedQR format ONLY](docs/seed_qr/printable_templates/29x29_letter_trading_card_2sided.pdf) --------------- # Manual Installation Instructions diff --git a/babel/README.md b/babel/README.md index 96645b45..419b19f2 100644 --- a/babel/README.md +++ b/babel/README.md @@ -21,8 +21,9 @@ Re-generate the `messages.pot` file: # -c TRANSLATOR_NOTE: will extract translator hints identified as comments starting with "# NOTE" # -s will strip the "NOTE:" part of the translator hint out # -F specifies the config file +# --add-location=file will include filename but not line-number of msgid # -o is our target output file -pybabel extract -c TRANSLATOR_NOTE: -s -F babel/babel.cfg -o babel/messages.pot . +pybabel extract -c TRANSLATOR_NOTE: -s -F babel/babel.cfg --add-location=file -o babel/messages.pot . ``` This will rescan all wrapped text, picking up new strings as well as updating existings strings that have been edited. @@ -77,4 +78,4 @@ In order to make that as transparent as possible, that procedure has been integr python3 setup.py install ``` -Unfortunately, it won't be executed by `pip3 install -e .` even though that has been propagated very long to be the developement-env installation procedure. --> \ No newline at end of file +Unfortunately, it won't be executed by `pip3 install -e .` even though that has been propagated very long to be the developement-env installation procedure. --> diff --git a/babel/messages.pot b/babel/messages.pot index e9d2fc69..dcb5e624 100644 --- a/babel/messages.pot +++ b/babel/messages.pot @@ -1,14 +1,14 @@ # Translations template for PROJECT. -# Copyright (C) 2022 ORGANIZATION +# Copyright (C) 2024 ORGANIZATION # This file is distributed under the same license as the PROJECT project. -# FIRST AUTHOR , 2022. +# FIRST AUTHOR , 2024. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-05-29 11:20-0500\n" +"POT-Creation-Date: 2024-02-23 18:47-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,396 +17,414 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.10.1\n" -#: src/seedsigner/gui/screens/main_menu_screens.py:12 -#: src/seedsigner/gui/screens/scan_screens.py:140 -msgid "Home" +#. The abbreviated label for the special key on a standard keyboard. +#: src/seedsigner/gui/keyboard.py +msgid "del" msgstr "" -#: src/seedsigner/gui/screens/main_menu_screens.py:24 -msgid "Restarting" +#. The abbreviated label for the special key on a standard keyboard. +#: src/seedsigner/gui/keyboard.py +msgid "space" msgstr "" -#: src/seedsigner/gui/screens/main_menu_screens.py:29 +#: src/seedsigner/gui/toast.py msgid "" -"SeedSigner is restarting.\n" -"\n" -"All in-memory data will be wiped." +"You can remove\n" +"the SD card now" msgstr "" -#: src/seedsigner/gui/screens/main_menu_screens.py:39 -msgid "Powering Off" +#: src/seedsigner/gui/toast.py +msgid "SD card removed" msgstr "" -#: src/seedsigner/gui/screens/main_menu_screens.py:44 -msgid "Please wait about 30 seconds before disconnecting power." +#: src/seedsigner/gui/toast.py +msgid "SD card inserted" msgstr "" -#: src/seedsigner/gui/screens/psbt_screens.py:29 -#: src/seedsigner/views/seed_views.py:385 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "Review PSBT" msgstr "" -#: src/seedsigner/gui/screens/psbt_screens.py:31 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "Review Details" msgstr "" -#: src/seedsigner/gui/screens/psbt_screens.py:88 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "1 input" msgstr "" -#: src/seedsigner/gui/screens/psbt_screens.py:90 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "input 1" msgstr "" -#: src/seedsigner/gui/screens/psbt_screens.py:91 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "input 2" msgstr "" #. Indicates that items have been omitted from a series: e.g. "1, 2, 3, [...], #. 8" -#: src/seedsigner/gui/screens/psbt_screens.py:93 -#: src/seedsigner/gui/screens/psbt_screens.py:141 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "[ ... ]" msgstr "" #. Input number will be inserted (e.g. "input 3") -#: src/seedsigner/gui/screens/psbt_screens.py:95 -#: src/seedsigner/gui/screens/psbt_screens.py:96 -#: src/seedsigner/gui/screens/psbt_screens.py:99 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "input {}" msgstr "" -#. Display a truncated address (e.g. "bc1qabc...") -#: src/seedsigner/gui/screens/psbt_screens.py:127 -msgid "{}..." +#. Ellipsis ("...") characters used to truncate an address (e.g. "bc1qabc...") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "..." msgstr "" -#: src/seedsigner/gui/screens/psbt_screens.py:136 -#: src/seedsigner/views/psbt_views.py:437 -#: src/seedsigner/views/psbt_views.py:441 +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/views/psbt_views.py msgid "self-transfer" msgstr "" -#: src/seedsigner/gui/screens/psbt_screens.py:139 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "recipient 1" msgstr "" #. Inserts the recipient number (e.g. the fifth one is: "recipient 5") -#: src/seedsigner/gui/screens/psbt_screens.py:143 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "recipient {}" msgstr "" -#: src/seedsigner/gui/screens/psbt_screens.py:145 -#: src/seedsigner/gui/screens/psbt_screens.py:555 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "fee" msgstr "" #. Label for a change output in the PSBT Overview flow diagram -#: src/seedsigner/gui/screens/psbt_screens.py:150 -#: src/seedsigner/views/psbt_views.py:437 -#: src/seedsigner/views/psbt_views.py:441 +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/views/psbt_views.py msgid "change" msgstr "" -#: src/seedsigner/gui/screens/psbt_screens.py:461 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "PSBT Math" msgstr "" -#: src/seedsigner/gui/screens/psbt_screens.py:462 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "Review Recipients" msgstr "" -#: src/seedsigner/gui/screens/psbt_screens.py:468 -#: src/seedsigner/gui/screens/psbt_screens.py:521 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "btc" msgstr "" -#: src/seedsigner/gui/screens/psbt_screens.py:480 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "sats" msgstr "" -#: src/seedsigner/gui/screens/psbt_screens.py:538 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "input" msgid_plural "inputs" msgstr[0] "" msgstr[1] "" -#: src/seedsigner/gui/screens/psbt_screens.py:548 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "recipient" msgid_plural "recipients" msgstr[0] "" msgstr[1] "" #. Denonination is inserted (e.g. your "btc change" or "sats change") -#: src/seedsigner/gui/screens/psbt_screens.py:566 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "{} change" msgstr "" #. First variable is either "Multisig" or the fingerprint for single sig; #. Second is "Change" or "Addr" (change vs receive addr); Third is the address #. index number (e.g. your #4 receive addr). -#: src/seedsigner/gui/screens/psbt_screens.py:662 -#: src/seedsigner/models/settings_definition.py:125 -#: src/seedsigner/views/seed_views.py:499 +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/models/settings_definition.py +#: src/seedsigner/views/seed_views.py msgid "Multisig" msgstr "" -#: src/seedsigner/gui/screens/psbt_screens.py:662 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "Change" msgstr "" -#: src/seedsigner/gui/screens/psbt_screens.py:662 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "Addr" msgstr "" -#: src/seedsigner/gui/screens/psbt_screens.py:672 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "Address verified!" msgstr "" -#: src/seedsigner/gui/screens/psbt_screens.py:684 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "Sign PSBT" msgstr "" -#: src/seedsigner/gui/screens/psbt_screens.py:698 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "Click to approve this transaction" msgstr "" -#: src/seedsigner/gui/screens/psbt_screens.py:708 -msgid "Signed PSBT" +#: src/seedsigner/gui/screens/scan_screens.py +msgid "< back | " msgstr "" -#. Prompt to specify which coordinator software to use (e.g. Specter, Sparrow, -#. etc) -#: src/seedsigner/gui/screens/psbt_screens.py:714 -msgid "Export as a QR code for:" -msgstr "" - -#: src/seedsigner/gui/screens/scan_screens.py:22 -msgid "Scan a QR code" -msgstr "" - -#: src/seedsigner/gui/screens/scan_screens.py:138 -msgid "Settings QR" -msgstr "" - -#: src/seedsigner/gui/screens/scan_screens.py:156 -msgid "Settings imported successfully!" -msgstr "" - -#: src/seedsigner/gui/screens/screen.py:735 -#: src/seedsigner/views/seed_views.py:995 -#: src/seedsigner/views/seed_views.py:1244 +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py msgid "Success!" msgstr "" -#: src/seedsigner/gui/screens/screen.py:739 -#: src/seedsigner/views/seed_views.py:997 -#: src/seedsigner/views/seed_views.py:1247 -#: src/seedsigner/views/seed_views.py:1690 src/seedsigner/views/view.py:145 +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py msgid "OK" msgstr "" -#: src/seedsigner/gui/screens/screen.py:854 -#: src/seedsigner/views/psbt_views.py:435 +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/psbt_views.py msgid "Caution" msgstr "" -#: src/seedsigner/gui/screens/screen.py:857 +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "Privacy Leak!" +msgstr "" + +#: src/seedsigner/gui/screens/screen.py msgid "I Understand" msgstr "" -#: src/seedsigner/gui/screens/screen.py:869 +#: src/seedsigner/gui/screens/screen.py msgid "Classified Info!" msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:411 +#: src/seedsigner/gui/screens/screen.py +msgid "Restarting" +msgstr "" + +#: src/seedsigner/gui/screens/screen.py +msgid "" +"SeedSigner is restarting.\n" +"\n" +"All in-memory data will be wiped." +msgstr "" + +#: src/seedsigner/gui/screens/screen.py +msgid "Powering Off" +msgstr "" + +#: src/seedsigner/gui/screens/screen.py +msgid "Please wait about 30 seconds before disconnecting power." +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py msgid "Finalize Seed" msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:424 -#: src/seedsigner/gui/screens/tools_screens.py:180 +#. a label for the shortened Key-id of a BIP-32 master HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "fingerprint" msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:455 -#: src/seedsigner/views/seed_views.py:388 +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py msgid "Backup Seed" msgstr "" #. Additional explainer for the two seed backup options (mnemonic phrase and #. SeedQR). -#: src/seedsigner/gui/screens/seed_screens.py:462 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Backups do not include your passphrase." msgstr "" #. Displays the page number and total: (e.g. page 1 of 6) -#: src/seedsigner/gui/screens/seed_screens.py:479 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Seed Words: {}/{}" msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:554 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "BIP-85 Index" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py msgid "Verify Backup?" msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:560 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Optionally verify that your mnemonic backup is correct." msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:570 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Derivation Path" msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:586 -msgid "Xpub Details" -msgstr "" - -#: src/seedsigner/gui/screens/seed_screens.py:592 -#: src/seedsigner/gui/screens/seed_screens.py:596 -#: src/seedsigner/views/seed_views.py:387 -#: src/seedsigner/views/seed_views.py:503 -#: src/seedsigner/views/seed_views.py:536 -#: src/seedsigner/views/seed_views.py:610 +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py msgid "Export Xpub" msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:605 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Xpub Details" +msgstr "" + +#. Short for "BIP32 Master Fingerprint" +#. a label for the shortened Key-id of a BIP-32 master HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Fingerprint" msgstr "" #. Short for "Derivation Path" -#: src/seedsigner/gui/screens/seed_screens.py:615 +#. a label for the derivation-path into a BIP-32 HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Derivation" msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:624 +#. Short for "BIP32 Extended Public Key" +#: src/seedsigner/gui/screens/seed_screens.py msgid "Xpub" msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:637 -#: src/seedsigner/views/seed_views.py:230 -msgid "Add Passphrase" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "BIP-39 Passphrase" msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:989 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Verify Passphrase" msgstr "" #. Describes the effect of applying a BIP-39 passphrase; it changes the seed's #. fingerprint -#: src/seedsigner/gui/screens/seed_screens.py:996 -msgid "changes seed:" +#: src/seedsigner/gui/screens/seed_screens.py +msgid "changes fingerprint" msgstr "" #. Refers to the SeedQR type: Standard or Compact -#: src/seedsigner/gui/screens/seed_screens.py:1083 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Standard" msgstr "" #. Briefly explains the Standard SeedQR data format -#: src/seedsigner/gui/screens/seed_screens.py:1085 +#: src/seedsigner/gui/screens/seed_screens.py msgid "BIP-39 wordlist indices" msgstr "" #. Refers to the SeedQR type: Standard or Compact -#: src/seedsigner/gui/screens/seed_screens.py:1093 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Compact" msgstr "" #. Briefly explains the Compact SeedQR data format -#: src/seedsigner/gui/screens/seed_screens.py:1095 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Raw entropy bits" msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:1109 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Transcribe SeedQR" msgstr "" #. Refers to the QR code size: 21x21, 25x25, or 29x29 -#: src/seedsigner/gui/screens/seed_screens.py:1111 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Begin {}x{}" msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:1188 +#: src/seedsigner/gui/screens/seed_screens.py msgid "click to exit" msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:1324 +#: src/seedsigner/gui/screens/seed_screens.py msgid "" "Optionally scan your transcribed SeedQR to confirm that it reads back " "correctly." msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:1381 -#: src/seedsigner/views/seed_views.py:1327 -#: src/seedsigner/views/seed_views.py:1385 +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py msgid "Verify Address" msgstr "" #. Inserts the nth address number (e.g. "Checking address 7") -#: src/seedsigner/gui/screens/seed_screens.py:1440 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Checking address {}" msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:1457 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Multisig Verification" msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:1462 +#: src/seedsigner/gui/screens/seed_screens.py msgid "" "Load your multisig wallet descriptor to verify your receive/self-transfer" " or change address." msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:1475 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Descriptor Loaded" msgstr "" #. Label for the multisig wallet's signing policy (e.g. 2-of-3) -#: src/seedsigner/gui/screens/seed_screens.py:1481 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Policy" msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:1489 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Signing Keys" msgstr "" -#: src/seedsigner/gui/screens/settings_screens.py:26 -#: src/seedsigner/views/main_menu_views.py:21 -#: src/seedsigner/views/settings_views.py:36 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Review Message" +msgstr "" + +#. Short for "Next step" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +#: src/seedsigner/views/tools_views.py +msgid "Next" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Confirm Address" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Sign Message" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "derivation path" +msgstr "" + +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py msgid "Settings" msgstr "" #. Short for "Input/Output"; screen to make sure the buttons and camera are #. working properly -#: src/seedsigner/gui/screens/settings_screens.py:59 +#: src/seedsigner/gui/screens/settings_screens.py msgid "I/O Test" msgstr "" #. Blank the screen -#: src/seedsigner/gui/screens/settings_screens.py:130 -#: src/seedsigner/gui/screens/settings_screens.py:141 -#: src/seedsigner/gui/screens/settings_screens.py:222 +#: src/seedsigner/gui/screens/settings_screens.py msgid "Clear" msgstr "" -#: src/seedsigner/gui/screens/settings_screens.py:162 +#: src/seedsigner/gui/screens/settings_screens.py msgid "Exit" msgstr "" -#: src/seedsigner/gui/screens/settings_screens.py:176 -#: src/seedsigner/gui/screens/tools_screens.py:71 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Capturing image..." msgstr "" -#: src/seedsigner/gui/screens/settings_screens.py:289 -#: src/seedsigner/views/settings_views.py:21 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py msgid "Donate" msgstr "" #. If your language uses the percent sign ("%"), your translation must also use #. two percent signs ("%%") due to python formatting oddities. "100%%" will be #. rendered as "100%". -#: src/seedsigner/gui/screens/settings_screens.py:294 +#: src/seedsigner/gui/screens/settings_screens.py #, python-format msgid "" "SeedSigner is 100%% free & open source, funded solely by the Bitcoin " @@ -415,256 +433,344 @@ msgid "" "Donate onchain or LN at:" msgstr "" -#. Instructions when taking a picture; "back" should point left. -#: src/seedsigner/gui/screens/tools_screens.py:40 -msgid "< back | click joystick" +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py +msgid "Settings QR" +msgstr "" + +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py +msgid "Home" msgstr "" #. Instructions when reviewing an image; "reshoot" must point left, "accept" #. must point right -#: src/seedsigner/gui/screens/tools_screens.py:98 +#: src/seedsigner/gui/screens/tools_screens.py msgid " < reshoot | accept > " msgstr "" #. current roll number vs total rolls (e.g. roll 7 of 50) -#: src/seedsigner/gui/screens/tools_screens.py:118 +#: src/seedsigner/gui/screens/tools_screens.py msgid "Dice Roll {}/{}" msgstr "" -#: src/seedsigner/gui/screens/tools_screens.py:163 +#. Build the last word in a 12 or 24 word BIP-39 mnemonic seed phrase. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Build Final Word" +msgstr "" + +#. Number of BIP-39 seed words, and the entropy -- in bits, contained within. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "" +"The {}th word is built from {} more entropy bits plus auto-calculated " +"checksum." +msgstr "" + +#. current coin-flip number vs total flips (e.g. flip 3 of 4) +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Coin Flip {}/{}" +msgstr "" + +#. How we call the "front" side result during a coin toss. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Heads = 1" +msgstr "" + +#. How we call the "back" side result during a coin toss. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Tails = 0" +msgstr "" + +#. A labeled presentation of a user-supplied value +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Your input: \"{}\"" +msgstr "" + +#. A function of "x" to be used for detecting errors in "x" +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Checksum" +msgstr "" + +#. labeled presentation of the last word in a BIP-39 mnemonic seed phrase. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Final Word: \"{}\"" +msgstr "" + +#. a label for the last word of a 12-word BIP-39 mnemonic seed phrase +#: src/seedsigner/gui/screens/tools_screens.py msgid "12th Word" msgstr "" -#: src/seedsigner/gui/screens/tools_screens.py:165 +#. a label for the last word of a 24-word BIP-39 mnemonic seed phrase +#: src/seedsigner/gui/screens/tools_screens.py msgid "24th Word" msgstr "" -#: src/seedsigner/models/settings_definition.py:14 +#. a label for the tool to explore public addresses for this seed. +#: src/seedsigner/gui/screens/tools_screens.py +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py +msgid "Address Explorer" +msgstr "" + +#. a label for a BIP-380-ish Output Descriptor +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Wallet descriptor" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Enabled" msgstr "" -#: src/seedsigner/models/settings_definition.py:15 -#: src/seedsigner/models/settings_definition.py:20 +#: src/seedsigner/models/settings_definition.py msgid "Disabled" msgstr "" -#: src/seedsigner/models/settings_definition.py:18 -#: src/seedsigner/models/settings_definition.py:26 +#: src/seedsigner/models/settings_definition.py msgid "Prompt" msgstr "" -#: src/seedsigner/models/settings_definition.py:19 -#: src/seedsigner/models/settings_definition.py:23 -#: src/seedsigner/models/settings_definition.py:29 +#: src/seedsigner/models/settings_definition.py msgid "Required" msgstr "" -#: src/seedsigner/models/settings_definition.py:74 -msgid "Btc-only" +#. Translator is expected ONLY to change their own locale below +#: src/seedsigner/models/settings_definition.py +msgid "Arabic" msgstr "" -#: src/seedsigner/models/settings_definition.py:75 -msgid "Sats-only" +#: src/seedsigner/models/settings_definition.py +msgid "Czech" msgstr "" -#: src/seedsigner/models/settings_definition.py:76 -msgid "Threshold at 0.01" +#: src/seedsigner/models/settings_definition.py +msgid "French" msgstr "" -#: src/seedsigner/models/settings_definition.py:77 -msgid "Btc | Sats hybrid" +#: src/seedsigner/models/settings_definition.py +msgid "German" msgstr "" -#: src/seedsigner/models/settings_definition.py:85 +#: src/seedsigner/models/settings_definition.py +msgid "Hebrew" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Japanese" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Korean" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Portuguese" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Russian" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Spanish" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "0°" msgstr "" -#: src/seedsigner/models/settings_definition.py:86 +#: src/seedsigner/models/settings_definition.py msgid "90°" msgstr "" -#: src/seedsigner/models/settings_definition.py:87 +#: src/seedsigner/models/settings_definition.py msgid "180°" msgstr "" -#: src/seedsigner/models/settings_definition.py:88 +#: src/seedsigner/models/settings_definition.py msgid "270°" msgstr "" -#: src/seedsigner/models/settings_definition.py:96 +#: src/seedsigner/models/settings_definition.py msgid "Low" msgstr "" -#: src/seedsigner/models/settings_definition.py:97 +#: src/seedsigner/models/settings_definition.py msgid "Medium" msgstr "" -#: src/seedsigner/models/settings_definition.py:98 +#: src/seedsigner/models/settings_definition.py msgid "High" msgstr "" -#: src/seedsigner/models/settings_definition.py:106 +#: src/seedsigner/models/settings_definition.py msgid "Mainnet" msgstr "" -#: src/seedsigner/models/settings_definition.py:107 +#: src/seedsigner/models/settings_definition.py msgid "Testnet" msgstr "" -#: src/seedsigner/models/settings_definition.py:108 +#: src/seedsigner/models/settings_definition.py msgid "Regtest" msgstr "" -#: src/seedsigner/models/settings_definition.py:124 -#: src/seedsigner/views/seed_views.py:498 +#: src/seedsigner/models/settings_definition.py +msgid "Store Settings on SD card" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Insert SD card to enable" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +#: src/seedsigner/views/seed_views.py msgid "Single Sig" msgstr "" -#: src/seedsigner/models/settings_definition.py:357 +#: src/seedsigner/models/settings_definition.py +msgid "Native Segwit" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Nested Segwit (legacy)" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Taproot" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Custom Derivation" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Language" msgstr "" -#: src/seedsigner/models/settings_definition.py:365 +#: src/seedsigner/models/settings_definition.py msgid "Mnemonic language" msgstr "" -#: src/seedsigner/models/settings_definition.py:373 +#: src/seedsigner/models/settings_definition.py msgid "Persistent settings" msgstr "" -#: src/seedsigner/models/settings_definition.py:374 -msgid "Store Settings on SD card." -msgstr "" - -#: src/seedsigner/models/settings_definition.py:379 +#: src/seedsigner/models/settings_definition.py msgid "Coordinator software" msgstr "" -#: src/seedsigner/models/settings_definition.py:386 +#: src/seedsigner/models/settings_definition.py msgid "Denomination display" msgstr "" -#: src/seedsigner/models/settings_definition.py:395 +#: src/seedsigner/models/settings_definition.py msgid "Bitcoin network" msgstr "" -#: src/seedsigner/models/settings_definition.py:403 +#: src/seedsigner/models/settings_definition.py msgid "QR code density" msgstr "" -#: src/seedsigner/models/settings_definition.py:411 +#: src/seedsigner/models/settings_definition.py msgid "Xpub export" msgstr "" -#: src/seedsigner/models/settings_definition.py:417 +#: src/seedsigner/models/settings_definition.py msgid "Sig types" msgstr "" -#: src/seedsigner/models/settings_definition.py:425 +#: src/seedsigner/models/settings_definition.py msgid "Script types" msgstr "" -#: src/seedsigner/models/settings_definition.py:433 +#: src/seedsigner/models/settings_definition.py msgid "Show xpub details" msgstr "" -#: src/seedsigner/models/settings_definition.py:439 +#: src/seedsigner/models/settings_definition.py msgid "BIP-39 passphrase" msgstr "" -#: src/seedsigner/models/settings_definition.py:447 +#: src/seedsigner/models/settings_definition.py msgid "Camera rotation" msgstr "" -#: src/seedsigner/models/settings_definition.py:455 +#: src/seedsigner/models/settings_definition.py msgid "Compact SeedQR" msgstr "" -#: src/seedsigner/models/settings_definition.py:461 +#: src/seedsigner/models/settings_definition.py +msgid "BIP-85 child seeds" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Message signing" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Show privacy warnings" msgstr "" -#: src/seedsigner/models/settings_definition.py:467 +#: src/seedsigner/models/settings_definition.py msgid "Show dire warnings" msgstr "" -#: src/seedsigner/models/settings_definition.py:473 -msgid "Show passphrase warning" +#: src/seedsigner/models/settings_definition.py +msgid "Show QR brightness tips" msgstr "" -#: src/seedsigner/models/settings_definition.py:479 +#: src/seedsigner/models/settings_definition.py msgid "Show partner logos" msgstr "" -#: src/seedsigner/models/settings_definition.py:494 +#: src/seedsigner/models/settings_definition.py msgid "QR background color" msgstr "" -#: src/seedsigner/views/main_menu_views.py:18 -msgid "Scan" -msgstr "" - -#: src/seedsigner/views/main_menu_views.py:19 -msgid "Seeds" -msgstr "" - -#: src/seedsigner/views/main_menu_views.py:20 -#: src/seedsigner/views/tools_views.py:29 -msgid "Tools" -msgstr "" - -#: src/seedsigner/views/main_menu_views.py:38 -msgid "Restart" -msgstr "" - -#: src/seedsigner/views/main_menu_views.py:39 -msgid "Power Off" -msgstr "" - -#: src/seedsigner/views/main_menu_views.py:42 -msgid "Reset / Power" -msgstr "" - -#: src/seedsigner/views/psbt_views.py:36 -#: src/seedsigner/views/seed_views.py:1365 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +#: src/seedsigner/views/tools_views.py msgid "Scan a seed" msgstr "" -#: src/seedsigner/views/psbt_views.py:37 -#: src/seedsigner/views/seed_views.py:1366 -msgid "Enter 12/24 words" +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +#: src/seedsigner/views/tools_views.py +msgid "Enter 12-word seed" msgstr "" -#: src/seedsigner/views/psbt_views.py:53 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +#: src/seedsigner/views/tools_views.py +msgid "Enter 24-word seed" +msgstr "" + +#: src/seedsigner/views/psbt_views.py msgid "Select Signer" msgstr "" -#: src/seedsigner/views/psbt_views.py:88 +#: src/seedsigner/views/psbt_views.py msgid "Parsing PSBT..." msgstr "" -#: src/seedsigner/views/psbt_views.py:160 +#: src/seedsigner/views/psbt_views.py msgid "Unsupported Script Type!" msgstr "" -#: src/seedsigner/views/psbt_views.py:161 +#: src/seedsigner/views/psbt_views.py msgid "" "PSBT has unsupported input script type, please verify your change " "addresses." msgstr "" -#: src/seedsigner/views/psbt_views.py:162 -#: src/seedsigner/views/psbt_views.py:181 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/view.py msgid "Continue" msgstr "" #. User will receive no change back; the inputs to this transaction are fully #. spent -#: src/seedsigner/views/psbt_views.py:179 +#: src/seedsigner/views/psbt_views.py msgid "Full Spend!" msgstr "" -#: src/seedsigner/views/psbt_views.py:180 +#: src/seedsigner/views/psbt_views.py msgid "" "This PSBT spends its entire input value. No change is coming back to your" " wallet." @@ -673,450 +779,549 @@ msgstr "" #. Future-tense used to indicate that this transaction will send this amount, #. as opposed to "Send" on its own which could be misread as an instant command #. (e.g. "Send Now"). -#: src/seedsigner/views/psbt_views.py:247 +#: src/seedsigner/views/psbt_views.py msgid "Will Send" msgstr "" -#: src/seedsigner/views/psbt_views.py:253 +#: src/seedsigner/views/psbt_views.py msgid "Next Recipient" msgstr "" -#. Short for "Next step" -#: src/seedsigner/views/psbt_views.py:256 -#: src/seedsigner/views/psbt_views.py:325 -#: src/seedsigner/views/seed_views.py:830 -msgid "Next" -msgstr "" - #. The amount you're receiving back from the transaction -#: src/seedsigner/views/psbt_views.py:329 +#: src/seedsigner/views/psbt_views.py msgid "Your Change" msgstr "" -#: src/seedsigner/views/psbt_views.py:330 +#: src/seedsigner/views/psbt_views.py msgid "Verify Multisig Change" msgstr "" -#: src/seedsigner/views/psbt_views.py:332 +#: src/seedsigner/views/psbt_views.py msgid "Self-Transfer" msgstr "" -#: src/seedsigner/views/psbt_views.py:333 +#: src/seedsigner/views/psbt_views.py msgid "Verify Multisig Addr" msgstr "" -#: src/seedsigner/views/psbt_views.py:352 +#: src/seedsigner/views/psbt_views.py msgid "Verifying Change..." msgstr "" -#: src/seedsigner/views/psbt_views.py:354 +#: src/seedsigner/views/psbt_views.py msgid "Verifying Self-Transfer..." msgstr "" #. Variable is either "change" or "self-transfer". -#: src/seedsigner/views/psbt_views.py:437 +#: src/seedsigner/views/psbt_views.py msgid "" "PSBT's {} address could not be verified with your multisig wallet " "descriptor." msgstr "" -#: src/seedsigner/views/psbt_views.py:439 +#: src/seedsigner/views/psbt_views.py msgid "Suspicious PSBT" msgstr "" #. Variable is either "change" or "self-transfer". -#: src/seedsigner/views/psbt_views.py:441 +#: src/seedsigner/views/psbt_views.py msgid "PSBT's {} address could not be generated from your seed." msgstr "" -#: src/seedsigner/views/psbt_views.py:445 +#: src/seedsigner/views/psbt_views.py msgid "Address Verification Failed" msgstr "" -#: src/seedsigner/views/psbt_views.py:447 +#: src/seedsigner/views/psbt_views.py msgid "Discard PSBT" msgstr "" -#: src/seedsigner/views/psbt_views.py:472 +#: src/seedsigner/views/psbt_views.py msgid "Approve PSBT" msgstr "" -#: src/seedsigner/views/psbt_views.py:477 -msgid "Signing PSBT..." -msgstr "" - -#: src/seedsigner/views/psbt_views.py:556 -msgid "PSBT Error" -msgstr "" - -#: src/seedsigner/views/psbt_views.py:558 -msgid "Signing Failed" -msgstr "" - -#: src/seedsigner/views/psbt_views.py:559 -msgid "Signing with this seed did not add a valid signature." -msgstr "" - -#: src/seedsigner/views/psbt_views.py:560 +#: src/seedsigner/views/psbt_views.py msgid "Select Diff Seed" msgstr "" +#: src/seedsigner/views/psbt_views.py +msgid "PSBT Error" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Signing Failed" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Signing with this seed did not add a valid signature." +msgstr "" + #. This is on the opening splash screen, displayed above the HRF logo -#: src/seedsigner/views/screensaver.py:74 +#: src/seedsigner/views/screensaver.py msgid "With support from:" msgstr "" -#: src/seedsigner/views/seed_views.py:53 +#: src/seedsigner/views/seed_views.py msgid "Load a seed" msgstr "" -#: src/seedsigner/views/seed_views.py:56 +#: src/seedsigner/views/seed_views.py msgid "In-Memory Seeds" msgstr "" -#: src/seedsigner/views/seed_views.py:77 +#: src/seedsigner/views/seed_views.py +msgid "Load the seed to verify" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Select seed to verify" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Load the seed to sign with" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Select seed to sign with" +msgstr "" + +#: src/seedsigner/views/seed_views.py msgid "Scan a SeedQR" msgstr "" -#: src/seedsigner/views/seed_views.py:78 -msgid "Enter 24-word seed" -msgstr "" - -#: src/seedsigner/views/seed_views.py:79 -msgid "Enter 12-word seed" -msgstr "" - -#: src/seedsigner/views/seed_views.py:80 +#: src/seedsigner/views/seed_views.py msgid "Create a seed" msgstr "" -#: src/seedsigner/views/seed_views.py:89 +#: src/seedsigner/views/seed_views.py msgid "Load A Seed" msgstr "" #. Inserts the word number (e.g. "Seed Word #6") -#: src/seedsigner/views/seed_views.py:126 +#: src/seedsigner/views/seed_views.py msgid "Seed Word #{}" msgstr "" -#: src/seedsigner/views/seed_views.py:187 +#: src/seedsigner/views/seed_views.py msgid "Review & Edit" msgstr "" -#: src/seedsigner/views/seed_views.py:188 -#: src/seedsigner/views/seed_views.py:343 -#: src/seedsigner/views/tools_views.py:259 +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py msgid "Discard" msgstr "" -#: src/seedsigner/views/seed_views.py:192 +#: src/seedsigner/views/seed_views.py msgid "Invalid Mnemonic!" msgstr "" -#: src/seedsigner/views/seed_views.py:194 +#: src/seedsigner/views/seed_views.py msgid "Checksum failure; not a valid seed phrase." msgstr "" -#: src/seedsigner/views/seed_views.py:229 -#: src/seedsigner/views/seed_views.py:301 -#: src/seedsigner/views/seed_views.py:831 -#: src/seedsigner/views/seed_views.py:1189 +#: src/seedsigner/views/seed_views.py src/seedsigner/views/view.py msgid "Done" msgstr "" -#: src/seedsigner/views/seed_views.py:260 -msgid "" -"BIP-39 passphrase changes your seed, resulting in new xpubs, wallets, " -"etc.\n" -"Be sure you know what you're doing!" -msgstr "" - -#: src/seedsigner/views/seed_views.py:300 +#: src/seedsigner/views/seed_views.py msgid "Edit passphrase" msgstr "" -#: src/seedsigner/views/seed_views.py:342 +#: src/seedsigner/views/seed_views.py msgid "Keep Seed" msgstr "" -#: src/seedsigner/views/seed_views.py:348 +#: src/seedsigner/views/seed_views.py msgid "Discard Seed?" msgstr "" -#: src/seedsigner/views/seed_views.py:350 +#: src/seedsigner/views/seed_views.py msgid "Wipe seed {} from the device?" msgstr "" -#: src/seedsigner/views/seed_views.py:384 +#: src/seedsigner/views/seed_views.py msgid "Scan PSBT" msgstr "" -#: src/seedsigner/views/seed_views.py:386 -#: src/seedsigner/views/seed_views.py:1689 +#: src/seedsigner/views/seed_views.py msgid "Verify Addr" msgstr "" -#: src/seedsigner/views/seed_views.py:389 +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Child Seed" +msgstr "" + +#: src/seedsigner/views/seed_views.py msgid "Discard Seed" msgstr "" -#: src/seedsigner/views/seed_views.py:464 +#: src/seedsigner/views/seed_views.py msgid "View Seed Words" msgstr "" -#: src/seedsigner/views/seed_views.py:465 +#: src/seedsigner/views/seed_views.py msgid "Export as SeedQR" msgstr "" -#: src/seedsigner/views/seed_views.py:652 -msgid "Privacy Leak!" -msgstr "" - -#: src/seedsigner/views/seed_views.py:653 +#: src/seedsigner/views/seed_views.py msgid "Xpub can be used to view all future transactions." msgstr "" -#: src/seedsigner/views/seed_views.py:683 +#: src/seedsigner/views/seed_views.py msgid "Generating xpub..." msgstr "" -#: src/seedsigner/views/seed_views.py:805 +#: src/seedsigner/views/seed_views.py msgid "You must keep your seed words private & away from all online devices." msgstr "" -#: src/seedsigner/views/seed_views.py:877 -msgid "Verify" +#: src/seedsigner/views/seed_views.py +msgid "Child #{}" msgstr "" -#: src/seedsigner/views/seed_views.py:878 -msgid "Skip" +#: src/seedsigner/views/seed_views.py +msgid "Seed Words" msgstr "" -#: src/seedsigner/views/seed_views.py:927 -msgid "Verify Word #{}" +#: src/seedsigner/views/seed_views.py +msgid "12 Words" msgstr "" -#: src/seedsigner/views/seed_views.py:967 -msgid "Review Seed Words" +#: src/seedsigner/views/seed_views.py +msgid "24 Words" msgstr "" -#: src/seedsigner/views/seed_views.py:968 +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Num Words" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Index Error" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Invalid Child Index" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Child Index must be between 0 and 2^31-1." +msgstr "" + +#: src/seedsigner/views/seed_views.py msgid "Try Again" msgstr "" -#: src/seedsigner/views/seed_views.py:972 +#: src/seedsigner/views/seed_views.py +msgid "Verify" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Skip" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Verify Word #{}" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Review Seed Words" +msgstr "" + +#: src/seedsigner/views/seed_views.py msgid "Verification Error" msgstr "" -#: src/seedsigner/views/seed_views.py:974 +#: src/seedsigner/views/seed_views.py msgid "Wrong Word!" msgstr "" -#: src/seedsigner/views/seed_views.py:975 +#: src/seedsigner/views/seed_views.py msgid "Word #{} is not \"{}\"!" msgstr "" -#: src/seedsigner/views/seed_views.py:993 +#: src/seedsigner/views/seed_views.py msgid "Backup Verified" msgstr "" -#: src/seedsigner/views/seed_views.py:996 +#: src/seedsigner/views/seed_views.py msgid "All mnemonic backup words were successfully verified!" msgstr "" -#: src/seedsigner/views/seed_views.py:1019 +#: src/seedsigner/views/seed_views.py msgid "Standard: 25x25" msgstr "" -#: src/seedsigner/views/seed_views.py:1020 +#: src/seedsigner/views/seed_views.py msgid "Compact: 21x21" msgstr "" -#: src/seedsigner/views/seed_views.py:1024 +#: src/seedsigner/views/seed_views.py msgid "Standard: 29x29" msgstr "" -#: src/seedsigner/views/seed_views.py:1025 +#: src/seedsigner/views/seed_views.py msgid "Compact: 25x25" msgstr "" -#: src/seedsigner/views/seed_views.py:1046 +#: src/seedsigner/views/seed_views.py msgid "SeedQR Format" msgstr "" -#: src/seedsigner/views/seed_views.py:1095 +#: src/seedsigner/views/seed_views.py msgid "SeedQR is your private key!" msgstr "" -#: src/seedsigner/views/seed_views.py:1096 -msgid "Never photograph it or scan it into an online device." +#: src/seedsigner/views/seed_views.py +msgid "Never photograph or scan it into a device that connects to the internet." msgstr "" -#: src/seedsigner/views/seed_views.py:1188 -#: src/seedsigner/views/seed_views.py:1232 -#: src/seedsigner/views/seed_views.py:1243 -#: src/seedsigner/views/seed_views.py:1255 +#: src/seedsigner/views/seed_views.py msgid "Confirm SeedQR" msgstr "" -#: src/seedsigner/views/seed_views.py:1193 +#: src/seedsigner/views/seed_views.py msgid "Confirm SeedQR?" msgstr "" -#: src/seedsigner/views/seed_views.py:1223 +#: src/seedsigner/views/seed_views.py msgid "Scan your SeedQR" msgstr "" -#: src/seedsigner/views/seed_views.py:1233 -#: src/seedsigner/views/seed_views.py:1256 +#: src/seedsigner/views/seed_views.py msgid "Error!" msgstr "" -#: src/seedsigner/views/seed_views.py:1234 +#: src/seedsigner/views/seed_views.py msgid "Your transcribed SeedQR does not match your original seed!" msgstr "" -#: src/seedsigner/views/seed_views.py:1236 -#: src/seedsigner/views/seed_views.py:1259 +#: src/seedsigner/views/seed_views.py msgid "Review SeedQR" msgstr "" -#: src/seedsigner/views/seed_views.py:1245 +#: src/seedsigner/views/seed_views.py msgid "Your transcribed SeedQR successfully scanned and yielded the same seed." msgstr "" -#: src/seedsigner/views/seed_views.py:1257 +#: src/seedsigner/views/seed_views.py msgid "Your transcribed SeedQR could not be read!" msgstr "" -#: src/seedsigner/views/seed_views.py:1328 +#: src/seedsigner/views/seed_views.py msgid "Sig type can't be auto-detected from this address. Please specify:" msgstr "" -#: src/seedsigner/views/seed_views.py:1369 -msgid "Load the seed to verify" -msgstr "" - -#: src/seedsigner/views/seed_views.py:1379 -msgid "Select seed to verify" -msgstr "" - -#: src/seedsigner/views/seed_views.py:1431 +#: src/seedsigner/views/seed_views.py msgid "Can't validate a single sig addr without specifying a seed" msgstr "" #. Option when scanning for a matching address; skips ten addresses ahead -#: src/seedsigner/views/seed_views.py:1477 +#: src/seedsigner/views/seed_views.py msgid "Skip 10" msgstr "" -#: src/seedsigner/views/seed_views.py:1478 -#: src/seedsigner/views/seed_views.py:1659 +#: src/seedsigner/views/seed_views.py msgid "Cancel" msgstr "" -#: src/seedsigner/views/seed_views.py:1606 -#: src/seedsigner/views/seed_views.py:1622 -msgid "Taproot verification not yet implemented!" -msgstr "" - -#: src/seedsigner/views/seed_views.py:1618 -msgid "Legacy P2PKH verification not implemented!" -msgstr "" - -#: src/seedsigner/views/seed_views.py:1643 +#: src/seedsigner/views/seed_views.py msgid "multisig" msgstr "" -#: src/seedsigner/views/seed_views.py:1645 +#: src/seedsigner/views/seed_views.py msgid "seed {}" msgstr "" -#: src/seedsigner/views/seed_views.py:1648 +#: src/seedsigner/views/seed_views.py msgid "Address Verified" msgstr "" -#: src/seedsigner/views/seed_views.py:1658 +#: src/seedsigner/views/seed_views.py msgid "Scan Descriptor" msgstr "" -#: src/seedsigner/views/seed_views.py:1688 +#: src/seedsigner/views/seed_views.py msgid "Return to PSBT" msgstr "" -#: src/seedsigner/views/settings_views.py:20 +#: src/seedsigner/views/settings_views.py msgid "I/O test" msgstr "" -#: src/seedsigner/views/settings_views.py:39 -#: src/seedsigner/views/settings_views.py:46 +#: src/seedsigner/views/settings_views.py msgid "Advanced" msgstr "" -#: src/seedsigner/views/settings_views.py:54 +#: src/seedsigner/views/settings_views.py msgid "Dev Options" msgstr "" -#: src/seedsigner/views/tools_views.py:24 -msgid " New seed" +#: src/seedsigner/views/settings_views.py +msgid "Persistent Settings enabled. Settings saved to SD card." msgstr "" -#: src/seedsigner/views/tools_views.py:25 +#: src/seedsigner/views/settings_views.py +msgid "Settings updated in temporary memory" +msgstr "" + +#: src/seedsigner/views/tools_views.py msgid "New seed" msgstr "" -#: src/seedsigner/views/tools_views.py:26 +#: src/seedsigner/views/tools_views.py msgid "Calc 12th/24th word" msgstr "" -#: src/seedsigner/views/tools_views.py:102 -#: src/seedsigner/views/tools_views.py:228 +#: src/seedsigner/views/tools_views.py src/seedsigner/views/view.py +msgid "Tools" +msgstr "" + +#: src/seedsigner/views/tools_views.py msgid "12 words" msgstr "" -#: src/seedsigner/views/tools_views.py:103 -#: src/seedsigner/views/tools_views.py:229 +#: src/seedsigner/views/tools_views.py msgid "24 words" msgstr "" -#: src/seedsigner/views/tools_views.py:107 +#: src/seedsigner/views/tools_views.py msgid "Mnemonic Length?" msgstr "" -#: src/seedsigner/views/tools_views.py:174 +#: src/seedsigner/views/tools_views.py msgid "12 words (50 rolls)" msgstr "" -#: src/seedsigner/views/tools_views.py:175 +#: src/seedsigner/views/tools_views.py msgid "24 words (99 rolls)" msgstr "" -#: src/seedsigner/views/tools_views.py:179 -#: src/seedsigner/views/tools_views.py:233 +#: src/seedsigner/views/tools_views.py msgid "Mnemonic Length" msgstr "" -#: src/seedsigner/views/tools_views.py:258 +#. Label to gather entropy through coin tosses +#: src/seedsigner/views/tools_views.py +msgid "Coin flip entropy" +msgstr "" + +#. Label to gather entropy through user specified BIP-39 word +#: src/seedsigner/views/tools_views.py +msgid "Word selection entropy" +msgstr "" + +#. Label to allow user to default entropy as all-zeros +#: src/seedsigner/views/tools_views.py +msgid "Finalize with zeros" +msgstr "" + +#. label to calculate the last word of a BIP-39 mnemonic seed phrase +#: src/seedsigner/views/tools_views.py +msgid "Final Word Calc" +msgstr "" + +#: src/seedsigner/views/tools_views.py msgid "Load seed" msgstr "" -#: src/seedsigner/views/view.py:124 +#: src/seedsigner/views/tools_views.py +msgid "Scan wallet descriptor" +msgstr "" + +#. label for addresses where others send us incoming payments +#: src/seedsigner/views/tools_views.py +msgid "Receive Addresses" +msgstr "" + +#. label for addresses that collect the change from our own outgoing payments +#: src/seedsigner/views/tools_views.py +msgid "Change Addresses" +msgstr "" + +#. a status message that our payment addresses are being calculated +#: src/seedsigner/views/tools_views.py +msgid "Calculating addrs..." +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Next {}" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Receive Addrs" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Change Addrs" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Scan" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Seeds" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Restart" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Power Off" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Reset / Power" +msgstr "" + +#: src/seedsigner/views/view.py msgid "Work In Progress" msgstr "" -#: src/seedsigner/views/view.py:125 +#: src/seedsigner/views/view.py msgid "Not Yet Implemented" msgstr "" -#: src/seedsigner/views/view.py:126 +#: src/seedsigner/views/view.py msgid "This is still on our to-do list!" msgstr "" -#: src/seedsigner/views/view.py:127 +#: src/seedsigner/views/view.py msgid "Back to Main Menu" msgstr "" -#: src/seedsigner/views/view.py:142 +#: src/seedsigner/views/view.py +msgid "Error" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Network Mismatch" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Change Setting" +msgstr "" + +#: src/seedsigner/views/view.py msgid "System Error" msgstr "" +#: src/seedsigner/views/view.py +msgid "Update Setting" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Option Disabled" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Security Tip" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "" +"For maximum security,\n" +"remove the MicroSD card\n" +"before continuing." +msgstr "" + diff --git a/docs/dice_verification.md b/docs/dice_verification.md index e4a5212c..361bf14d 100644 --- a/docs/dice_verification.md +++ b/docs/dice_verification.md @@ -1,40 +1,207 @@ -## Verifying dice seed generation +# Verifying dice seed generation -It is possible to do a 'dry run' to verify that seed generation has not been tempered. -This will ensure that the derivation algorithm has not been tempered and is still same as well known algorithm used bu coldcard and ian coleman bip39 webpage. -For example an 'evil maid' attack would be someone accesing your sdcard and change the code to only take in account 5 or 6 dice, so the 24 words would still feel random but the attacker will need to brute force only 5 or 6 dice (very easy). -This is part of the "do not trust, verify" crypto mottos. +The intention of this documentation is to describe how to verify the seed generation code used in SeedSigner against other independent tools, to prove that they all generate the same results, despite them using different programming languages and code libraries.

+As it is an important step to verify all software releases being used to ensure that the installation files downloaded have not been compromised, the same is true especially for the seed generation procedure which unknowingly might not work as expected due to bugs or even on purpose.

+This guide describes how this can be achieved.

+As usual: Don't Trust, Verify!
+

+**Note:**
+**Do NOT use this with any seed you want to use later with real funds. This exercise is only for checking that the independent codebases get to the same end result!**
+**However, if you do want to check your real seedphrases you should download the Iancoleman and/or Bitcoiner.Guide tools onto an airgapped, ephemeral computer (e.g. using tails-OS) and perform these tests on there. Destroy/abandon the TailsOS afterwards.**
+**Never input seed phrases that you intend to use to store real funds onto an internet-connected computer!!!** +


-### Verifying with Ian coleman webpage +## 99 Dice Rolls / 24 Seed Words Example -Go to https://iancoleman.io/bip39 and check show entropy detail: - +The following 99 dice roll results are used in the verification steps as an example for a 24 words seed:
+> 655152231316521321611331544441236164664431121534415633526456254462245546236542364246312613322234612 -And then make sure to check 'Hex' or 'base 10' (1) and 24 words as mnemonic length (2). -Do not use 'dice' format because dice 6 will be replaced by 0. -And then enter the 99 dices numbers in (3) : - +The corresponding 24 seed words are:
+> eyebrow obvious such suggest poet seven breeze blame virtual frown dynamic donor harsh pigeon express broccoli easy apology scatter force recipe shadow claim radio -When the 99 dice number has been entered in the seedsigner and ian coleman page, you will be able to verify that the 24 words are the same. +(Scroll down near the end to see result values for a 50 dice rolls / 12 seed words example) +


+## Creating seed via Dice rolls in SeedSigner (here v0.6.0) -#### Here in real life: +First we create a new seed based on dice rolls in SeedSigner:

+Power on your SeedSigner, go to the 'Tools' menu and select 'New Seed' (with the dice symbols):
+ -We start with first 3 roll, it is important to always read dice roll from left to right to avoid human bias: - +Select '24 words (99 rolls)' and on the next screen enter the dice numbers one after another:
+ -Then we arrive at the 99th dice: - +Go on until the end (99 dice roll numbers):
+ -Then same 24 words! : - +After that the 24 seed words are shown (in 6 screens of 4 words each):
+ +
**.....**
+ -Now we are sure that the dice derivation is correct and we can unplug everything and do a new dice roll only on the seedsigner. +The fingerprint for this seed is:
+ +


+Having now created a dice-based seed in the SeedSigner, we will go on to compare those details to what appears in the: +* Sparrow desktop wallet software +* Ian Coleman's Mnemonic Code Converter website +* Seed Tool website -### Verifying with Coldcard +We will create a wallet to have the complete zpub and receive/change addresses to check against the two web pages.
+SeedSigner currently supports BlueWallet, Nunchuk, Sparrow and Specter Desktop. Here we will use Sparrow wallet as an example.
-There is nothing specific, the algorithm are completely the same. Coldcard has a verification script in python and all explanations here: -https://coldcard.com/docs/verifying-dice-roll-math +Keep the SeedSigner open and the newly created seed still loaded as we will need it in the next step. +


+ +## Create new wallet from seed in Sparrow Wallet to see xpub/zpub and addresses + +Go to https://www.sparrowwallet.com/download/ and download the release version supported by your operating system.

+Open Sparrow Wallet, go to 'File' menu and select 'New Wallet'. Enter a name (e.g. test), and click 'Create Wallet'.

+Click 'Airgapped Hardware Wallet' (1) and click on the 'Scan' button in the SeedSigner entry (2) which will open the camera scan screen:
+ + +On SeedSigner go to the seed just created and click 'Export Xpub':
+ + +Follow these menu entries in SeedSigner:
+> Export Xpub --> Single Sig --> Native Segwit --> Sparrow
+ + + +Click 'Export Xpub' and SeedSigner will show an animated QR code to be scanned in Sparrow Wallet (where we are still in the wallet creation).
+Scan the QR code SeedSigner is showing in Sparrow Wallet.

+The wallet has now been created in Sparrow. Click 'Apply' button to finalize. The wallet's settings screen now looks like this:
+ + +We will later use this to verify: (1) fingerprint, (2) zpub (click this button to switch between xpub and zpub!) and (3) addresses on the 'Addresses' tab. +


+ +## Verifying with Ian Coleman BIP39 website + +Go to https://iancoleman.io/bip39 and check 'Show entropy details' (1):
+ + +
+Make sure to check (1) 'Hex' and (2) '24 Words' as 'Mnemonic Length'.
+(Do not use 'dice' format because dice 6 will be replaced by 0).
+Then enter the 99 dices numbers in (3). The corresponding seed words are shown in (4):
+ +

+The 24 seed words are the same in SeedSigner and the Ian Coleman tool. +

+ +### Verification of (1) fingerprint, (2) zpub and (3) generated addresses +**Fingerprint:**
+Fingerprint is not shown in the Ian Coleman tool (so cannot be verified here) +

+**Zpub:**
+Scroll down to the 'Derivation Path' section, click on the 'BIP84' tab (1) and find the zpub in (2): +

+Compare to zpub in Sparrow:
+ +
+ +Zpub is the same as shown in SeedSigner and Sparrow wallet. +

+**Addresses:**
+Scroll down to the 'Derived Addresses' section and compare the receive addresses to the ones generated in Sparrow ('Addresses' tab of the wallet): + +
+ +Check that the receive addresses all match.

+To verify the change addresses, change 'External / Internal' to 1 (1): +

+Compare the change addresses to the ones generated in Sparrow ('Addresses' tab of the wallet): + +
+ +Check that the change addresses all match. +


+ +## Verifying with Seed Tool website + +Go to https://bitcoiner.guide/seed/ and click on 'Seed Generation Input' (1):
+ + +Then click on the 'Show the Entropy Section' tab (1):
+ + +Enter the 99 dice numbers in (1), in (2) change back to 'Hex', check that (3) is still '24 Words' and the calculated seed words are shown in (4):
+ + +Seed words shown are the same as in SeedSigner and the Ian Coleman web tool seen before. +

+ +### Verification of (1) fingerprint, (2) zpub and (3) generated addresses + +**Fingerprint:**
+Fingerprint can be seen here (1):
+

+**Zpub:**
+Scroll down to the 'Derived Addresses' section (1), click on it, make sure that '84' is selected for 'Purpose' (2) and check the zpub at (3):
+

+Compare to zpub in Sparrow:
+ +
+ +Zpub is the same as shown in SeedSigner, Sparrow and Ian Colemand tool. +

+**Addresses:**
+Scroll down a little bit where the receive addresses are shown and compare to the ones generated in Sparrow ('Addresses' tab of the wallet):
+ +
+ +Check that the receive addresses all match.

+To verify the change addresses, change the 'Receive/Change' dropdown box to '1 (Change)' (1): +

+Compare the change addresses to the ones generated in Sparrow ('Addresses' tab of the wallet): + +
+ +Check that the change addresses all match. +


+ +## 50 Dice Rolls / 12 Seed Words Example + +SeedSigner supports the creation of mnenomic seeds both with 12 or 24 seed words corresponding to 50 or 99 dice rolls. Below are some example result values for using 50 dice rolls only.

+All the steps shown can be executed the same way, just select the '12 words (50 rolls)' option in SeedSigner and change the 'Mnenomic Length' dropdown boxes in both web tools to '12 Words'.

+ +50 dice roll results as an example for a 12 words seed:
+> 65515223131652132161133154444123616466443112153441 + +The corresponding 12 seed words are:
+> hole luggage safe present express tragic orbit shed switch metal identify path + +Fingerprint:
+> 8d9cced8 + +Zpub:
+> zpub6qf9ziL759pzyhKMWaPfNSiCETkoA6oq3fbCDvXqcURiMtPnkEg3nH93W5mrSkvGPoJC9xTYZheYDsYoiYc5AkSk9iY3DkCJHkFgHMdijW6 + +Addresses:
+Receive:
+> bc1q00lln3r4mt4uwvg7mxv96xgpewauwmggkex2ff
+> bc1q0jj2cv965f3642mv4lgq5za80jtfpkd0jhjefr
+> bc1qpecssejm2678v0rknk9tsxd5fsshezfr0vr5m5
+> bc1qcsl37xn5rkfz8qhcfwq5u52acxecyyfz0kv4gh
+> bc1q8ehx53re0wck4m9tzek8mlnctp2ztm7jq94zm4
+> ...
+ +Change:
+> bc1qz0ckhg3m349qpmweyn5v6r6tvx2wfw4nv8h75q
+> bc1qme5tu2t424ws3z69u445q0yw88vpc7fwygra4r
+> bc1qznuyuc087ky7fhv4nvlmll4p586ks4ggcyt36d
+> bc1qjcqxv22j0g00pehruwwh34sw5znu6vp3myaspy
+> bc1q6dpfl7czd22wt0max6p09vr6lvvpag7xw9u8lc
+> ... + +

+## Conclusion + +What did we achieve now?
+ +We created a dice-based seed in SeedSigner and set up a wallet using this seed in Sparrow wallet (as this is what a seed is used for).

+We double-checked in two different web tools implementing different methods for seed creation that what SeedSigner generates perfectly matches up with what the other tools calculate based on the same dice entropy used.
+ +So congratulations if the fingerprints, zpubs and addresses all match up in your example so you can be much more confident that nothing is wrong with your generated seed. -### Epilogue -You can use these methods to do dry run time to time to verify that no one has changed the micro sdcard. But do not use the generated 24 words as a valid wallet, they need to be generated alone, only on the seedsigner! diff --git a/docs/img/Open_Pill_Mini_Models.JPG b/docs/img/Open_Pill_Mini_Models.JPG new file mode 100644 index 00000000..47f69a84 Binary files /dev/null and b/docs/img/Open_Pill_Mini_Models.JPG differ diff --git a/docs/img/Open_Pill_Mini_w_Faceplate.JPG b/docs/img/Open_Pill_Mini_w_Faceplate.JPG new file mode 100644 index 00000000..68a9ceff Binary files /dev/null and b/docs/img/Open_Pill_Mini_w_Faceplate.JPG differ diff --git a/docs/img/Open_Pill_Models.JPG b/docs/img/Open_Pill_Models.JPG new file mode 100644 index 00000000..f687faa0 Binary files /dev/null and b/docs/img/Open_Pill_Models.JPG differ diff --git a/docs/img/Open_Pill_Star.JPG b/docs/img/Open_Pill_Star.JPG index eb312aa9..d42d2b9a 100644 Binary files a/docs/img/Open_Pill_Star.JPG and b/docs/img/Open_Pill_Star.JPG differ diff --git a/docs/img/Open_Pill_w_Comfort_Joystick.png b/docs/img/Open_Pill_w_Comfort_Joystick.png new file mode 100644 index 00000000..c4d21f76 Binary files /dev/null and b/docs/img/Open_Pill_w_Comfort_Joystick.png differ diff --git a/docs/img/Orange_Pill_Models.JPG b/docs/img/Orange_Pill_Models.JPG new file mode 100644 index 00000000..40a5015f Binary files /dev/null and b/docs/img/Orange_Pill_Models.JPG differ diff --git a/docs/img/Rugged_Pill_Thumb.jpg b/docs/img/Rugged_Pill_Thumb.jpg new file mode 100644 index 00000000..d40b878f Binary files /dev/null and b/docs/img/Rugged_Pill_Thumb.jpg differ diff --git a/docs/img/Simple_Pill_Models.JPG b/docs/img/Simple_Pill_Models.JPG new file mode 100644 index 00000000..5a4a36fd Binary files /dev/null and b/docs/img/Simple_Pill_Models.JPG differ diff --git a/docs/img/dicedoc/coleman_addresses.png b/docs/img/dicedoc/coleman_addresses.png new file mode 100644 index 00000000..e9200e9f Binary files /dev/null and b/docs/img/dicedoc/coleman_addresses.png differ diff --git a/docs/img/dicedoc/coleman_change_addresses_1.png b/docs/img/dicedoc/coleman_change_addresses_1.png new file mode 100644 index 00000000..d1c62cda Binary files /dev/null and b/docs/img/dicedoc/coleman_change_addresses_1.png differ diff --git a/docs/img/dicedoc/coleman_change_addresses_2.png b/docs/img/dicedoc/coleman_change_addresses_2.png new file mode 100644 index 00000000..f809cb8f Binary files /dev/null and b/docs/img/dicedoc/coleman_change_addresses_2.png differ diff --git a/docs/img/dicedoc/coleman_entropy.png b/docs/img/dicedoc/coleman_entropy.png new file mode 100644 index 00000000..8f9ad7f8 Binary files /dev/null and b/docs/img/dicedoc/coleman_entropy.png differ diff --git a/docs/img/dicedoc/coleman_verify.png b/docs/img/dicedoc/coleman_verify.png new file mode 100644 index 00000000..c976fe38 Binary files /dev/null and b/docs/img/dicedoc/coleman_verify.png differ diff --git a/docs/img/dicedoc/coleman_zpub.png b/docs/img/dicedoc/coleman_zpub.png new file mode 100644 index 00000000..f69e3e90 Binary files /dev/null and b/docs/img/dicedoc/coleman_zpub.png differ diff --git a/docs/img/dicedoc/seedtool_1.png b/docs/img/dicedoc/seedtool_1.png new file mode 100644 index 00000000..2b4ed01c Binary files /dev/null and b/docs/img/dicedoc/seedtool_1.png differ diff --git a/docs/img/dicedoc/seedtool_2.png b/docs/img/dicedoc/seedtool_2.png new file mode 100644 index 00000000..6c4f252d Binary files /dev/null and b/docs/img/dicedoc/seedtool_2.png differ diff --git a/docs/img/dicedoc/seedtool_3.png b/docs/img/dicedoc/seedtool_3.png new file mode 100644 index 00000000..cc52e853 Binary files /dev/null and b/docs/img/dicedoc/seedtool_3.png differ diff --git a/docs/img/dicedoc/seedtool_addresses.png b/docs/img/dicedoc/seedtool_addresses.png new file mode 100644 index 00000000..8c6cfc36 Binary files /dev/null and b/docs/img/dicedoc/seedtool_addresses.png differ diff --git a/docs/img/dicedoc/seedtool_change_addresses_1.png b/docs/img/dicedoc/seedtool_change_addresses_1.png new file mode 100644 index 00000000..9f2e7eb3 Binary files /dev/null and b/docs/img/dicedoc/seedtool_change_addresses_1.png differ diff --git a/docs/img/dicedoc/seedtool_change_addresses_2.png b/docs/img/dicedoc/seedtool_change_addresses_2.png new file mode 100644 index 00000000..99c41186 Binary files /dev/null and b/docs/img/dicedoc/seedtool_change_addresses_2.png differ diff --git a/docs/img/dicedoc/seedtool_fingerprint.png b/docs/img/dicedoc/seedtool_fingerprint.png new file mode 100644 index 00000000..c1ef3b04 Binary files /dev/null and b/docs/img/dicedoc/seedtool_fingerprint.png differ diff --git a/docs/img/dicedoc/seedtool_zpub.png b/docs/img/dicedoc/seedtool_zpub.png new file mode 100644 index 00000000..e43a38a0 Binary files /dev/null and b/docs/img/dicedoc/seedtool_zpub.png differ diff --git a/docs/img/dicedoc/sesi_dice_1.png b/docs/img/dicedoc/sesi_dice_1.png new file mode 100644 index 00000000..b85f7a6c Binary files /dev/null and b/docs/img/dicedoc/sesi_dice_1.png differ diff --git a/docs/img/dicedoc/sesi_dice_2.png b/docs/img/dicedoc/sesi_dice_2.png new file mode 100644 index 00000000..4c73d96b Binary files /dev/null and b/docs/img/dicedoc/sesi_dice_2.png differ diff --git a/docs/img/dicedoc/sesi_export_xpub_1.png b/docs/img/dicedoc/sesi_export_xpub_1.png new file mode 100644 index 00000000..601b7aac Binary files /dev/null and b/docs/img/dicedoc/sesi_export_xpub_1.png differ diff --git a/docs/img/dicedoc/sesi_export_xpub_2.png b/docs/img/dicedoc/sesi_export_xpub_2.png new file mode 100644 index 00000000..f43eea5e Binary files /dev/null and b/docs/img/dicedoc/sesi_export_xpub_2.png differ diff --git a/docs/img/dicedoc/sesi_finger_print.png b/docs/img/dicedoc/sesi_finger_print.png new file mode 100644 index 00000000..de15f023 Binary files /dev/null and b/docs/img/dicedoc/sesi_finger_print.png differ diff --git a/docs/img/dicedoc/sesi_seed_1.png b/docs/img/dicedoc/sesi_seed_1.png new file mode 100644 index 00000000..7693d674 Binary files /dev/null and b/docs/img/dicedoc/sesi_seed_1.png differ diff --git a/docs/img/dicedoc/sesi_seed_2.png b/docs/img/dicedoc/sesi_seed_2.png new file mode 100644 index 00000000..87b2f139 Binary files /dev/null and b/docs/img/dicedoc/sesi_seed_2.png differ diff --git a/docs/img/dicedoc/sesi_tools_dice_seed.png b/docs/img/dicedoc/sesi_tools_dice_seed.png new file mode 100644 index 00000000..1eeec191 Binary files /dev/null and b/docs/img/dicedoc/sesi_tools_dice_seed.png differ diff --git a/docs/img/dicedoc/sparrow_wallet_1.png b/docs/img/dicedoc/sparrow_wallet_1.png new file mode 100644 index 00000000..02d24af6 Binary files /dev/null and b/docs/img/dicedoc/sparrow_wallet_1.png differ diff --git a/docs/img/dicedoc/sparrow_wallet_2.png b/docs/img/dicedoc/sparrow_wallet_2.png new file mode 100644 index 00000000..cf459a4a Binary files /dev/null and b/docs/img/dicedoc/sparrow_wallet_2.png differ diff --git a/docs/img/dicedoc/sparrow_zpub.png b/docs/img/dicedoc/sparrow_zpub.png new file mode 100644 index 00000000..ccd22ab8 Binary files /dev/null and b/docs/img/dicedoc/sparrow_zpub.png differ diff --git a/docs/img/legacy_hardware_remapped_pins.jpg b/docs/img/legacy_hardware_remapped_pins.jpg new file mode 100644 index 00000000..f6af8b47 Binary files /dev/null and b/docs/img/legacy_hardware_remapped_pins.jpg differ diff --git a/docs/img/legacy_hardware_remapped_pins_photo.jpg b/docs/img/legacy_hardware_remapped_pins_photo.jpg new file mode 100644 index 00000000..a7abcccc Binary files /dev/null and b/docs/img/legacy_hardware_remapped_pins_photo.jpg differ diff --git a/docs/legacy_hardware.md b/docs/legacy_hardware.md new file mode 100644 index 00000000..09daaaab --- /dev/null +++ b/docs/legacy_hardware.md @@ -0,0 +1,22 @@ +# Legacy Hardware Modifications + +Older Raspberry Pi devices have a smaller GPIO header, 26 pin as opposed to the 40 pin header found on more recent models. The Waveshare LCD hat uses some of the pins between 26 and 40 for the buttons, meaning that these will need to be reassigned (and remapped in software) when used with an older device. + +## Hardware Changes + +A suggested remapping can be found here: +![Remapped Pins Shematic](./img/legacy_hardware_remapped_pins.jpg) + +This remapping can be done by soldering wires on to the Waveshare hat as below: +![Remapped Pins Photo](./img/legacy_hardware_remapped_pins_photo.jpg) + +Alternatively, you could do this by connecting the LCD hat on with individual breadboard jumper wires. + +Once you have re-mapped the pins, it is advised to do an IO Test to ensure that everything works. + +**Warning: Some of the GPIO pins on contain 5 volt output. Raspberry Pi GPIO pins are NOT 5V tolerant, meaning that if you accidentally connect a 5V supply pin to a GPIO input pin, you risk permanent damage.** + +## Software Changes +The Seedsigner software will automatically detect which hardware revision you are using and if the older hardware is detected, will remap the software to match the above modifications to the Waveshare hat. + +If you are using a pre-built Seedsigner image that hasn't yet has this incorporated, you can simply take the file "buttons.py" (/src/seedsigner/hardware/ int his repository) and overwrite same file on your Seedsigner SD card. (The easiest way to do this is to copy it on to the /boot/ partitition of the SD card then copy it over via the command line while connected to your Pi via monitor+keyboard) O \ No newline at end of file diff --git a/docs/manual_installation.md b/docs/manual_installation.md index 236d77c4..a8c63da5 100644 --- a/docs/manual_installation.md +++ b/docs/manual_installation.md @@ -6,20 +6,25 @@ https://downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2 Best practice is to verify the downloaded .zip file containing the Raspberry Pi Lite OS matches the published SHA256 hash of the file; for additional reference that hash is: c5dad159a2775c687e9281b1a0e586f7471690ae28f2f2282c90e7d59f64273c. After verifying the file's data integrity, you can decompress the .zip file to obtain the operating system image that it contains. You can then use Balena's Etcher tool (https://www.balena.io/etcher/) to write the Raspberry Pi Lite software image to a memory card (4 GB or larger). It's important to note that an image authoring tool must be used (the operating system image cannot be simply copied into a file storage partition on the memory card). -The manual SeedSigner installation and configuration process requires an internet connection on the device to download the necessary libraries and code. But because the Pi Zero 1.3 does not have onboard wifi, you have two options: +The manual SeedSigner installation and configuration process requires an internet connection on the Pi to download the necessary libraries and code. +If your Pi does not have onboard wifi, you have two options: -1. Run these steps on a separate Raspberry Pi 2/3/4 or Zero W which can connect to the internet and then transfer the SD card to the Pi Zero 1.3 when complete. -2. OR configure the Pi Zero 1.3 directly by relaying through your computer's internet connection over USB. See instructions [here](usb_relay.md). +1. Run these steps on a separate Raspberry Pi 2/3/4 or Zero W which does have onboard Wi-Fi to connect to the internet, and then move the SD card over to the non Wi-Fi enabled Pi when complete. +2. OR configure the non Wi-Fi enabled Pi directly by relaying through your computer's internet connection over USB. See instructions [here](usb_relay.md). +If your Pi does have onboard Wi-Fi, then using the Rasberry Pi Imager software will allow you to easily configure your Pi's Wi-Fi connection, as well as simultaneously write the image file. That will make your initial SSH into the Pi much easier. +Use the Pi's onboard Wi-Fi only if you are setting up a local development environment, never for real funds or binary image creation. + For the following steps you'll need to either connect a keyboard & monitor to the network-connected Raspberry Pi you are working with, or SSH into the Pi if you're familiar with that process. ### Configure the Pi First things first, verify that you are using the correct version of the Raspberry Pi Lite operating system by typing the command: -``` +```bash cat /etc/os-release ``` + The output of this command should match the following text: -``` +```bash PRETTY_NAME="Raspbian GNU/Linux 10 (buster)" NAME="Raspbian GNU/Linux" VERSION_ID="10" @@ -33,7 +38,7 @@ BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs" ``` Now launch the Raspberry Pi's System Configuration tool using the command: -``` +```bash sudo raspi-config ``` @@ -45,25 +50,65 @@ Set the following: * `Locale`: arrow up and down through the list and select or deselect languages with the spacebar. * Deselect the default language option that is selected * Select `en_US.UTF-8 UTF-8` for US English + * Use the `TAB` button to select `Ok` and press `ENTER` + * On the next screen select `en_US.UTF-8` for the default locale * You will also need to configure the WiFi settings if you are using the #1 option above to connect to the internet When you exit the System Configuration tool, you will be prompted to reboot the system; allow the system to reboot and continue with these instructions. +Each command should be run individually,unless its specified as a multi-line command. ### Change the default password Change the system's default password from the default "raspberry". Run the command: -``` +```bash passwd ``` You will be prompted to enter the current password ("raspberry") and then to enter a new password twice. In our prepared release image, the password used is `AirG@pped!`. -### Install dependencies -Copy this entire box and run it as one command (will take 15-20min to complete): +### Install python3.10 +```bash +# install compiler dependencies; takes ~1 minute on a Pi Zero 1.3 +# * openssl, libssl-dev: ssl support when pip fetches packages +# * libsqlite3-dev: required by `coverage` +sudo apt update && sudo apt install -y build-essential zlib1g-dev \ + libncurses5-dev libgdbm-dev libnss3-dev openssl libssl-dev \ + libreadline-dev libffi-dev wget libsqlite3-dev + +# Grab the python3.10 source +wget https://www.python.org/ftp/python/3.10.10/Python-3.10.10.tgz +tar -xzvf Python-3.10.10.tgz +cd Python-3.10.10 + +# Takes ~6 minutes on a Pi Zero 1.3 to check what is available +./configure --enable-optimizations + +# compiling takes ~80 minutes(!!) on a Pi Zero 1.3 +sudo make altinstall + +# cleanup +cd .. +sudo rm -rf Python-3.10.10* + +# Make python3.10 the default version +sudo update-alternatives --install /usr/bin/python python /usr/local/bin/python3.10 1 +sudo update-alternatives --install /usr/bin/python3 python3 /usr/local/bin/python3.10 1 ``` -sudo apt-get update && sudo apt-get install -y wiringpi python3-pip \ - python3-numpy python-pil libopenjp2-7 git python3-opencv \ - python3-picamera libatlas-base-dev qrencode + +Manually re-install `python3-apt` to avoid error messages in later steps (though, ironically, you will see the "ModuleNotFoundError: No module named 'apt_pkg'" error message during the `apt remove` step): +```bash +sudo apt remove --purge python3-apt -y +sudo apt autoremove -y +sudo apt install python3-apt -y +``` + + +### Install dependencies +Copy this entire box and run it as one command (will take a while to complete): +```bash +sudo apt update && sudo apt install -y wiringpi python3-pip \ + python3-numpy python-pil libjpeg-dev zlib1g-dev libopenjp2-7 \ + git python3-opencv python3-picamera libatlas-base-dev qrencode ``` ### Install `zbar` @@ -72,17 +117,17 @@ sudo apt-get update && sudo apt-get install -y wiringpi python3-pip \ SeedSigner requires `zbar` at 0.23.x or higher. Download the binary: -``` +```bash curl -L http://raspbian.raspberrypi.org/raspbian/pool/main/z/zbar/libzbar0_0.23.90-1_armhf.deb --output libzbar0_0.23.90-1_armhf.deb ``` And then install it: -``` +```bash sudo apt install ./libzbar0_0.23.90-1_armhf.deb ``` Cleanup: -``` +```bash rm libzbar0_0.23.90-1_armhf.deb ``` @@ -90,7 +135,7 @@ rm libzbar0_0.23.90-1_armhf.deb This library "provides functions for reading digital inputs and setting digital outputs, using SPI and I2C, and for accessing the system timers." Run each of the following individual steps: -``` +```bash wget http://www.airspayce.com/mikem/bcm2835/bcm2835-1.60.tar.gz tar zxvf bcm2835-1.60.tar.gz cd bcm2835-1.60/ @@ -102,88 +147,91 @@ sudo rm -rf bcm2835-1.60 ``` ### Set up `virtualenv` -``` -pip3 install virtualenvwrapper +```bash +python -m pip install virtualenvwrapper ``` Edit your bash profile with the command `nano ~/.profile` and add the following to the end: -``` +```bash export WORKON_HOME=$HOME/.envs export VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3 -source /home/pi/.local/bin/virtualenvwrapper.sh +source $HOME/.local/bin/virtualenvwrapper.sh ``` Then `CTRL-X` and `y` to exit and save changes. -Now create the python virtualenv for SeedSigner with these two commands: -``` +Now create the virtualenv for SeedSigner: +```bash source ~/.profile -mkvirtualenv --python=python3 seedsigner-env +mkvirtualenv seedsigner-env ``` For convenience you can configure your `.profile` to auto-activate the SeedSigner virtualenv when you ssh in. Once again `nano ~/.profile` and add at the end: -``` +```bash workon seedsigner-env ``` -Optional: If you're going to be testing new code on the SeedSigner, you'll find yourself often needing to kill the SeedSigner code that automatically runs at startup (we'll be configuring this further down). As an extra convenience you can list the process id so that you can then kill it from the terminal: -``` -ps aux | grep main.py -``` - -Save your changes with `CTRL-X` and `y`. - -Now when you `ssh` in you'll see something like: -``` -pi 297 65.4 9.7 74096 36736 ? Rsl 09:26 10:29 /home/pi/.envs/seedsigner-env/bin/python main.py -pi 857 0.0 0.4 7332 1876 pts/0 S+ 09:42 0:00 grep --color=auto main.py -``` - -The top line is our SeedSigner code running. To stop it, run: -``` -kill 297 -``` -Where `297` is the process id listed in the output above (it'll be different each time). - ### Download the SeedSigner code: -``` +```bash git clone https://github.com/SeedSigner/seedsigner cd seedsigner ``` -If you want to run a specific branch within the main SeedSigner repo, switch to it with: -``` -git checkout yourtargetbranch -``` - -And if you want to test a pull request (PR), for example PR #123: -``` -git fetch origin pull/123/head:pr_123 -git checkout pr_123 -``` - -where `pr_123` is any name you want to give to the new branch in your local repo that will hold the PR. - ### Install Python `pip` dependencies: -``` -pip3 install -r requirements.txt +```bash +# Takes 1hr 45min on a Pi Zero 1.3 +pip install -r requirements.txt +pip install -r requirements-raspi.txt ``` #### `pyzbar` -Note: The `requirements.txt` installs a fork of the python `pyzbar` repo (for now pointing to the fork in Keith's `kdmukai` github account [https://github.com/kdmukai/pyzbar](https://github.com/kdmukai/pyzbar)). +Note: The `requirements.txt` installs a fork of the python `pyzbar` repo. The fork is required because the main `pyzbar` repo has been abandoned. This [github issue](https://github.com/NaturalHistoryMuseum/pyzbar/issues/124#issuecomment-971967091) discusses the changes needed in order to support reading binary data from `zbar`, which is required for our `CompactSeedQR` format which writes byte data instead of strings. The changes specifically reference the following PRs which have already been merged into Keith's fork: * [PR 76](https://github.com/NaturalHistoryMuseum/pyzbar/pull/76/files): enables scanning to continue even when a null byte (`x\00`) is found. -* [PR 82](https://github.com/NaturalHistoryMuseum/pyzbar/pull/82): enable `zbar`'s new binary mode. Note that this PR has a trivial bug that was fixed in Keith's fork. +* [PR 82](https://github.com/NaturalHistoryMuseum/pyzbar/pull/82): enable `zbar`'s new binary mode. Note that this PR has a trivial bug that was fixed in our fork. + + +### Finish configuring the virtualenv +Set the SeedSigner `src/` directory as the project directory for the virtualenv (this is where the virtualenv will take you when you activate it): +```bash +cd src +setvirtualenvproject +``` + +Test it out: +```bash +# exit the virtualenv +deactivate + +# change dirs to somewhere else +cd ~ + +# activate the virtualenv +workon seedsigner-env + +# you should now be back in the SeedSigner src/ directory +pwd +``` + +### Optional: increase spidev buffer size +This allows `ST7789.py` to update the LCD without performing multiple write operations because the default buffer size is 4096 bytes. The default can be changed via the `/boot/cmdline.txt` file. You will need to add `spidev.bufsiz=131072` to the end of this single lined file command. + +Example `cmdline.txt` contents: +``` +console=serial0,115200 console=tty1 root=PARTUUID=2fa4ba7e-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait modules-load=dwc2,g_ether spidev.bufsiz=131072 +``` ### Configure `systemd` to run SeedSigner at boot: -``` + +```bash sudo nano /etc/systemd/system/seedsigner.service ``` -Add the following contents to the text file that was created: -``` +Add the following contents to the text file that was created: +If you are not using the username pi, then replace `pi` in the service section below with your username. There are 3 lines to change. +```ini [Unit] Description=Seedsigner @@ -199,46 +247,98 @@ WantedBy=multi-user.target _Note: For local dev you'll want to edit the `Restart=always` line to `Restart=no`. This way when your dev code crashes it won't keep trying to restart itself. Note that the UI "Reset" will no longer work when auto-restarts are disabled._ -_Note: Debugging output is completely wiped via routing the output to `/dev/null 2>&1`. When working in local dev, you're better off disabling the `systemd` SeedSigner service and just directly running the app so you can see all the debugging output live._ +_Note: Debugging output is completely wiped via routing the output to `/dev/null 2>&1`. When working in local dev, you'll `kill` the `systemd` SeedSigner service and just directly run the code on demand so you can see all the debugging output live._ Use `CTRL-X` and `y` to exit and save changes. Configure the service to start running (this will restart the seedsigner code automatically at startup and if it crashes): -``` +```bash sudo systemctl enable seedsigner.service ``` Now reboot the Raspberry Pi: -``` +```bash sudo reboot ``` After the Raspberry Pi reboots, you should see the SeedSigner splash screen and the SeedSigner menu subsequently appear on the LCD screen (note that it can take up to 60 seconds for the menu to appear). + +#### Optional: kill `systemd` SeedSigner process on login +If you're going to be testing new code on the device, you'll find yourself often needing to kill the SeedSigner instance that `systemd` automatically runs at startup. + +You can configure your `~/.profile` to find and kill the SeedSigner process when you ssh in. + +`nano ~/.profile` and add at the end: +```bash +# Find the SeedSigner process and kill it +kill $(ps aux | grep '[m]ain.py' | awk '{print $2}') +``` + + ### Further OS modifications Disable and remove the system's virtual memory / swap file with the commands: -``` + +```bash sudo apt remove dphys-swapfile -y sudo apt autoremove -y sudo rm /var/swap ``` -## Local testing and development -For those who will use the SeedSigner installation for testing/development, it can be helpful to change the system's host name so it doesn't potentially conflict with other Raspberry Pis that may already be present on your network. (For those who don't plan to use the installation for testing or development, you can skip this portion of the process.) To change the host name first edit the "hostname" with the command: +## Manually start the SeedSigner code +```bash +# activate the virtualenv if you haven't already +workon seedsigner-env + +# You should now be in the SeedSigner src/ directory. List its contents: +ls + +# You should see the main.py file. Run it: +python main.py + +# To kill the process, use CTRL-C ``` + + +## Local testing and development + +### Run specific branches or PRs +The default branch is `dev`. If you want to run a specific release tag or a specific branch: +```bash +# release tag for v0.6.0: +git checkout 0.6.0 +``` + +And if you want to test a pull request (PR), for example PR #123: +```bash +git fetch origin pull/123/head:pr_123 +git checkout pr_123 +``` + +where `pr_123` is any name you want to give to the new branch in your local repo that will hold the PR. + + +### Change the host name +For those who will use the SeedSigner installation for testing/development, it can be helpful to change the system's host name so it doesn't potentially conflict with other Raspberry Pis that may already be present on your network. (For those who don't plan to use the installation for testing or development, you can skip this portion of the process.) To change the host name first edit the "hostname" with the command: + +```bash sudo nano /etc/hostname ``` -and change "raspberrypi" to "seedsigner" (or another name). Use `CTRL-X` and `y` to exit and save changes. You'll also need to edit the "hosts" file with the command: -``` + +and change "raspberrypi" to "seedsigner" (or another name). Use `CTRL-X` and `y` to exit and save changes. + +You'll also need to edit the "hosts" file with the command: +```bash sudo nano /etc/hosts ``` + and change "raspberrypi" to "seedsigner" (or the other name you previously chose). Use `CTRL-X` and `y` to exit and save changes. ### Set a static IP Your local machine that `ssh`s into the SeedSigner can sometimes get confused if you're connecting to different SeedSigners that are all identified as `pi@seedsigner.local`. In this case it helps to set a static ip and just `ssh` directly to that instead. First find your current `nameserver`: -``` +```bash sudo cat /etc/resolv.conf ``` @@ -260,21 +360,26 @@ static domain_name_servers=192.168.1.254 `CTRL-X` and `y` to save changes. After your next reboot, access this SeedSigner using its new static ip: -``` +```bash +# Use the static ip you set above: ssh pi@192.168.1.200 + +# But the hostname will still work, too: +ssh pi@seedsigner.local ``` ### More convenient `ssh` access: Power SeedSigner devs will find themselves connecting to a lot of different SeedSigners. This can cause headaches with `ssh`'s built-in protections; a different device that uses the same `ssh` credentials is normally a potential spoofing attack. But we're doing this to ourselves on purpose and so we can carve out exceptions. On your local machine, run `nano ~/.ssh/config` and add to the end: -``` +```conf host seedsigner.local StrictHostKeyChecking no UserKnownHostsFile /dev/null User pi LogLevel QUIET +# Set this to the static ip you set above: host 192.168.1.200 StrictHostKeyChecking no UserKnownHostsFile /dev/null @@ -293,7 +398,7 @@ The second entry does the same for a specific static ip; you'll want this if you You can also configure the SeedSigner so that you don't have to enter the `pi` password when you `ssh` in. run `ssh-copy-id` with the same values that you connect via `ssh`: -``` +```bash ssh-copy-id pi@seedsigner.local # or if you're connecting over static ip, something like: @@ -307,7 +412,7 @@ _Note: If you don't have any ssh keys on your local machine, you'll need to crea ## Disable wifi/Bluetooth when using other Raspi boards If you plan to use your installation on a Raspberry Pi that is not a Zero version 1.3, but rather on a Raspberry Pi that has WiFi and Bluetooth capabilities, it is a good idea to disable the following WiFi & Bluetooth, as well as other relevant services (assuming you are not creating this installation for testing/development purposes). Enter the followiing commands to disable WiFi, Bluetooth, & other relevant services: -``` +```bash sudo systemctl disable bluetooth.service sudo systemctl disable wpa_supplicant.service sudo systemctl disable dhcpcd.service @@ -316,12 +421,13 @@ sudo systemctl disable networking.service sudo systemctl disable dphys-swapfile.service sudo ifconfig wlan0 down ``` + Please note that if you are using WiFi to connect/interact with your Raspberry Pi, the last command will sever that connection. You can now safely power the Raspberry Pi off from the SeedSigner main menu. If you do not plan to use your installation for testing/development, it is also a good idea to disable WiFi and Bluetooth by editing the config.txt file found in the installation's "boot" partition. You can add the following text to the end of that file with any simple text editor (Windows: Notepad, Mac: TextEdit, Linux: nano): -``` +```ini dtoverlay=disable-bt dtoverlay=pi3-disable-wifi ``` diff --git a/docs/recovery.md b/docs/recovery.md index 5725dc75..7e8569ff 100644 --- a/docs/recovery.md +++ b/docs/recovery.md @@ -23,7 +23,7 @@ Derivation paths for standard script types for mainnet: - Script Type: P2WSH in P2SH - Public Key Encoding: 0x0295b43f - Ypub -Custom derivation paths are also optional when generating an xpub from SeedSigner. The Public Key Encodings is detected based on the derivation path configured. Embit bitcoin library does the detection and is documented [here](https://github.com/diybitcoinhardware/embit/blob/master/docs/api/bip32.md#detect_version). For a video explination of these standards see a presentation by Stepan of Embit/Specter on this topic: https://youtube.com/watch?v=JCaC5DG2HTM +Custom derivation paths are also optional when generating an xpub from SeedSigner. The Public Key Encodings are detected based on the derivation path configured. The `embit` bitcoin library does this detection and is documented [here](https://github.com/diybitcoinhardware/embit/blob/master/docs/api/bip32.md#detect_version). For a video explanation of these standards see a presentation by Stepan of `embit` on this topic: https://youtube.com/watch?v=JCaC5DG2HTM Changing the network settings from main to test in SeedSigner will change the public key encoding and derivation path following [slip-0132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) standards. @@ -32,4 +32,4 @@ Related Standards: - [bip-0044](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) - [bip-0048](https://github.com/bitcoin/bips/blob/master/bip-0048.mediawiki) - [bip-0049](https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki) -- [bip-0084](https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki) \ No newline at end of file +- [bip-0084](https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki) diff --git a/docs/seed_qr/README.md b/docs/seed_qr/README.md index f22905a3..1d2a1a10 100644 --- a/docs/seed_qr/README.md +++ b/docs/seed_qr/README.md @@ -70,7 +70,7 @@ It's important to note here that QR codes can encode data in a number of differe QR codes are typically used to encode a website url, in which case the "Alphanumeric" format has to be used (the encoded data can consist of upper- and lowercase letters, numbers, and certain allowed symbols). -If you have a long url like `https://ohnoihavealongurl.com` (29 characters), the chart shows that it would not fit in a 21x21 QR code; its max capacity is 25 alphanumeric chars. But it's within the 47-char capacity of the 29x29 size. +If you have a long url like `https://ohnoihavealongurl.com` (29 characters), the chart shows that it would not fit in a 21x21 QR code; its max capacity is 25 alphanumeric chars. But it's within the 47-char capacity of the 25x25 size. ### Bit efficiency matters Notice that the "Numeric" column has greater capacity. This is because when you have fewer possible characters to encode, it takes less data to specify each one. @@ -298,16 +298,13 @@ It's just the above process in reverse: 12. nuclear 1210 ``` -It would be much more difficult to manually recreate your seed from a CompactSeedQR. Tools like [zxing.org](https://zxing.org/w/decode.jspx) can help you get the binary data out as a hexidecimal string: +It would be much more difficult to manually recreate your seed from a CompactSeedQR. Tools like [zxing.org](https://zxing.org/w/decode.jspx) or [ZBar](https://zbar.sourceforge.net/) can help you get the binary data out as a hexidecimal string: -All 12-word CompactSeedQRs will start with `41 0` (that specifies the binary data format and says the length of the data is 16 bytes) and end with `0 ec` (unused byte and a half). - -All 24-word CompactSeedQRs will start with `42 0` (binary, data length is 32 bytes) and end with `0` (unused half a byte). - -The remaining hexidecimal data is just an alternate representation of the 128-bit entropy for your 12-word mnemonic (or 256 bits for a 24-word mnemonic). Various programming tools exist to convert from hex back into mnemonic form. +Note that for some QR decoders, zxing in particular, may return more data than the compact seed itself: the data type, the data length, and padding. For example, the zxing result from 12-word CompactSeedQRs in low error correction mode will start with `41 0` (that specifies the binary data format and says the length of the data is 16 bytes) and end with `0 ec` (unused byte and a half). Similarly, 24-word CompactSeedQRs will start with `42 0` (binary, data length is 32 bytes) and end with `0` (unused half a byte). +The ZBar library just returns the encoded data, without metadata or padding. ## Obfuscation Conversely, having limited support for reading binary QR codes and the complications described above are seen by some as an added security feature. Should someone steal your CompactSeedQR or take a photo of it, they'll have to be fairly savvy to know how to decode it. diff --git a/docs/seed_qr/printable_templates/12words_seedqr_template.pdf b/docs/seed_qr/printable_templates/12words_seedqr_template.pdf deleted file mode 100644 index 4c4cf14f..00000000 Binary files a/docs/seed_qr/printable_templates/12words_seedqr_template.pdf and /dev/null differ diff --git a/docs/seed_qr/printable_templates/21x21_A4_trading_card_2sided.pdf b/docs/seed_qr/printable_templates/21x21_A4_trading_card_2sided.pdf new file mode 100644 index 00000000..01946eea Binary files /dev/null and b/docs/seed_qr/printable_templates/21x21_A4_trading_card_2sided.pdf differ diff --git a/docs/seed_qr/printable_templates/21x21_letter_trading_card_2sided.pdf b/docs/seed_qr/printable_templates/21x21_letter_trading_card_2sided.pdf new file mode 100644 index 00000000..40e1d647 Binary files /dev/null and b/docs/seed_qr/printable_templates/21x21_letter_trading_card_2sided.pdf differ diff --git a/docs/seed_qr/printable_templates/24words_seedqr_template.pdf b/docs/seed_qr/printable_templates/24words_seedqr_template.pdf deleted file mode 100644 index e5ea1cc5..00000000 Binary files a/docs/seed_qr/printable_templates/24words_seedqr_template.pdf and /dev/null differ diff --git a/docs/seed_qr/printable_templates/25x25_A4_trading_card_2sided.pdf b/docs/seed_qr/printable_templates/25x25_A4_trading_card_2sided.pdf new file mode 100644 index 00000000..4340c517 Binary files /dev/null and b/docs/seed_qr/printable_templates/25x25_A4_trading_card_2sided.pdf differ diff --git a/docs/seed_qr/printable_templates/25x25_letter_trading_card_2sided.pdf b/docs/seed_qr/printable_templates/25x25_letter_trading_card_2sided.pdf new file mode 100644 index 00000000..60691b58 Binary files /dev/null and b/docs/seed_qr/printable_templates/25x25_letter_trading_card_2sided.pdf differ diff --git a/docs/seed_qr/printable_templates/29x29_A4_trading_card_2sided.pdf b/docs/seed_qr/printable_templates/29x29_A4_trading_card_2sided.pdf new file mode 100644 index 00000000..c84ffbdd Binary files /dev/null and b/docs/seed_qr/printable_templates/29x29_A4_trading_card_2sided.pdf differ diff --git a/docs/seed_qr/printable_templates/29x29_letter_trading_card_2sided.pdf b/docs/seed_qr/printable_templates/29x29_letter_trading_card_2sided.pdf new file mode 100644 index 00000000..7731f967 Binary files /dev/null and b/docs/seed_qr/printable_templates/29x29_letter_trading_card_2sided.pdf differ diff --git a/docs/seed_qr/printable_templates/Seed_QR_Card.pdf b/docs/seed_qr/printable_templates/Seed_QR_Card.pdf deleted file mode 100644 index c7df3fbd..00000000 Binary files a/docs/seed_qr/printable_templates/Seed_QR_Card.pdf and /dev/null differ diff --git a/docs/seed_qr/printable_templates/compact_seedqr/12words_compactseedqr_template.pdf b/docs/seed_qr/printable_templates/compact_seedqr/12words_compactseedqr_template.pdf deleted file mode 100644 index d5924541..00000000 Binary files a/docs/seed_qr/printable_templates/compact_seedqr/12words_compactseedqr_template.pdf and /dev/null differ diff --git a/docs/seed_qr/printable_templates/compact_seedqr/24words_compactseedqr_template.pdf b/docs/seed_qr/printable_templates/compact_seedqr/24words_compactseedqr_template.pdf deleted file mode 100644 index 0bf49c79..00000000 Binary files a/docs/seed_qr/printable_templates/compact_seedqr/24words_compactseedqr_template.pdf and /dev/null differ diff --git a/docs/seed_qr/printable_templates/dots_21x21.pdf b/docs/seed_qr/printable_templates/dots_21x21.pdf new file mode 100644 index 00000000..d5a38c59 Binary files /dev/null and b/docs/seed_qr/printable_templates/dots_21x21.pdf differ diff --git a/docs/seed_qr/printable_templates/dots_25x25.pdf b/docs/seed_qr/printable_templates/dots_25x25.pdf new file mode 100644 index 00000000..625f5fc7 Binary files /dev/null and b/docs/seed_qr/printable_templates/dots_25x25.pdf differ diff --git a/docs/seed_qr/printable_templates/dots_29x29.pdf b/docs/seed_qr/printable_templates/dots_29x29.pdf new file mode 100644 index 00000000..a43fa0c8 Binary files /dev/null and b/docs/seed_qr/printable_templates/dots_29x29.pdf differ diff --git a/docs/seed_qr/printable_templates/grid_21x21.pdf b/docs/seed_qr/printable_templates/grid_21x21.pdf new file mode 100644 index 00000000..ebef81b4 Binary files /dev/null and b/docs/seed_qr/printable_templates/grid_21x21.pdf differ diff --git a/docs/seed_qr/printable_templates/grid_25x25.pdf b/docs/seed_qr/printable_templates/grid_25x25.pdf new file mode 100644 index 00000000..bfdaf9ef Binary files /dev/null and b/docs/seed_qr/printable_templates/grid_25x25.pdf differ diff --git a/docs/seed_qr/printable_templates/grid_29x29.pdf b/docs/seed_qr/printable_templates/grid_29x29.pdf new file mode 100644 index 00000000..59177397 Binary files /dev/null and b/docs/seed_qr/printable_templates/grid_29x29.pdf differ diff --git a/docs/seed_qr/printable_templates/grid_wfingerprint_21x21.pdf b/docs/seed_qr/printable_templates/grid_wfingerprint_21x21.pdf new file mode 100644 index 00000000..062dc80e Binary files /dev/null and b/docs/seed_qr/printable_templates/grid_wfingerprint_21x21.pdf differ diff --git a/docs/seed_qr/printable_templates/grid_wfingerprint_21x21_4-up.pdf b/docs/seed_qr/printable_templates/grid_wfingerprint_21x21_4-up.pdf new file mode 100644 index 00000000..0036b76e Binary files /dev/null and b/docs/seed_qr/printable_templates/grid_wfingerprint_21x21_4-up.pdf differ diff --git a/docs/seed_qr/printable_templates/mnemonic_12words_wfingerprint.pdf b/docs/seed_qr/printable_templates/mnemonic_12words_wfingerprint.pdf new file mode 100644 index 00000000..4cb9f2b8 Binary files /dev/null and b/docs/seed_qr/printable_templates/mnemonic_12words_wfingerprint.pdf differ diff --git a/docs/seed_qr/printable_templates/mnemonic_12words_wfingerprint_4up.pdf b/docs/seed_qr/printable_templates/mnemonic_12words_wfingerprint_4up.pdf new file mode 100644 index 00000000..4bc041ab Binary files /dev/null and b/docs/seed_qr/printable_templates/mnemonic_12words_wfingerprint_4up.pdf differ diff --git a/docs/seed_qr/printable_templates/mnemonic_24word_wfingerprint.pdf b/docs/seed_qr/printable_templates/mnemonic_24word_wfingerprint.pdf new file mode 100644 index 00000000..bc6ad3d8 Binary files /dev/null and b/docs/seed_qr/printable_templates/mnemonic_24word_wfingerprint.pdf differ diff --git a/docs/seed_qr/printable_templates/mnemonic_24word_wfingerprint_4-up.pdf b/docs/seed_qr/printable_templates/mnemonic_24word_wfingerprint_4-up.pdf new file mode 100644 index 00000000..c8f9d5ec Binary files /dev/null and b/docs/seed_qr/printable_templates/mnemonic_24word_wfingerprint_4-up.pdf differ diff --git a/docs/seed_qr/printable_templates/template_generator.py b/docs/seed_qr/printable_templates/template_generator.py deleted file mode 100644 index f0ed2f27..00000000 --- a/docs/seed_qr/printable_templates/template_generator.py +++ /dev/null @@ -1,294 +0,0 @@ -import math - -def generate_qr_template(qr_size, num_words=24, block_size=5, show_timing_marks=False): - if num_words == 24: - word_cols = 2 - else: - word_cols = 1 - - html = """ - - - - - - - - """ - - html += """ - - - -
- """ - for i in range(1, num_words + 1): - if i == 13: - html += """ - - """ - html += f"""
- {i}: ____________________________ -
- """ - html += "
" - - y_names = "A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,X,Y,Z" - - html += """""" - html += f"""""" - for j in range(0, math.ceil(qr_size/block_size)): - html += f"""""" - html += "" - - for i in range(0, qr_size): - html += """\n""" - if i % block_size == 0: - html += f"""""" - for j in range(0, qr_size): - html += f"""""" - html += """\n""" - html += "
{j + 1}
{y_names.split(",")[int(i / block_size)]}·
" - - html += " - - """ - - return html - - - -if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser( - description=""" - Generates a blank QR code template at 21x21, 25x25, or 29x29. - - ex: python3 qr_code_template.py 25 12 - ex: python3 qr_code_template.py 29 24 - - Optionally change the block size divider guides: - - python3 qr_code_template.py 29 24 --block_size 6 - """, - formatter_class=argparse.RawTextHelpFormatter - ) - - # Required positional arguments - parser.add_argument('qr_size', - type=int, - choices=[21, 25, 29], - help="QR code size. 21 for 21x21, 25 for 25x25, 29 for 29x29") - parser.add_argument('num_words', - type=int, - choices=[12, 24], - help="Number of words in the mnemonic seed. 12 or 24") - - # Add optional arguments - parser.add_argument('-b', '--block_size', - type=int, - default=5, - help="Size of the manual entry zoom blocks") - parser.add_argument('-t', '--timing_marks', - action='store_true', - default=False, - help="Show timing marks (the dashed blocks linking the three large registration boxes") - args = parser.parse_args() - - html = generate_qr_template(qr_size=args.qr_size, num_words=args.num_words, block_size=args.block_size, show_timing_marks=args.timing_marks) - file = open(f"{args.num_words}words_seedqr_template-{args.qr_size}x{args.qr_size}.html", "w") - file.write(html) - file.close() - diff --git a/docs/seed_qr/printable_templates/21x21_trading_card.pdf b/docs/seed_qr/printable_templates/trading_card_21x21_w12words.pdf similarity index 100% rename from docs/seed_qr/printable_templates/21x21_trading_card.pdf rename to docs/seed_qr/printable_templates/trading_card_21x21_w12words.pdf diff --git a/docs/seed_qr/printable_templates/25x25_trading_card.pdf b/docs/seed_qr/printable_templates/trading_card_25x25_w24words.pdf similarity index 100% rename from docs/seed_qr/printable_templates/25x25_trading_card.pdf rename to docs/seed_qr/printable_templates/trading_card_25x25_w24words.pdf diff --git a/docs/seed_qr/printable_templates/29x29_trading_card.pdf b/docs/seed_qr/printable_templates/trading_card_29x29_w24words.pdf similarity index 100% rename from docs/seed_qr/printable_templates/29x29_trading_card.pdf rename to docs/seed_qr/printable_templates/trading_card_29x29_w24words.pdf diff --git a/docs/seed_qr/printable_templates/xpub_wfingerprint.pdf b/docs/seed_qr/printable_templates/xpub_wfingerprint.pdf new file mode 100644 index 00000000..28688c97 Binary files /dev/null and b/docs/seed_qr/printable_templates/xpub_wfingerprint.pdf differ diff --git a/enclosures/open_pill/README.md b/enclosures/open_pill/README.md new file mode 100644 index 00000000..a7245ca3 --- /dev/null +++ b/enclosures/open_pill/README.md @@ -0,0 +1,27 @@ +## The Open Pill Enclosure + +The "Open Pill" was the second officially released SeedSigner enclosure. Given some of the challenges associated with the "Orange Pill", the Open Pill was designed for quick, inexpensive, and simple deployment. No secondary hardware components are required for assembly and the design consists of a single printed part that can be produced with even basic 3D printers. This simpler design was intended to put the focus back on the project's software, which was improving by leaps and bounds when the enclosure was released. + + + +### Characterisics: +- Supported Camera? Legacy RPi Camera (w/ gold Pi Zero Cable) +- Supported HAT? Waveshare 240x240 pixel LCD display + controls +- Recommended printing process? FDM +- Recommended printing materials? PLA +- Secondary Hardware Required? No +- Data-enabled USB Port accessible? Yes +- Mini-HDMI port accessible? Yes +- Removeable Memory Card? Yes +- Comfortable controls? No (see note below) + +### Assembly Demonstration: +https://www.youtube.com/watch?v=gXPFJygZobE + + +### Comfortable Controls Optional Upgrade: +The bare joystick on an open pill or open pill mini design can be uncomfortable for some thumbs. Twitter user @Vulcan21com developed a DIY solution to add comfort to the exposed joystick using a M2.5 knurled nut standardized under DIN 466, widely available at many hardware stores. The joystick itself has no threading, but it is possible to screw the M2.5 nut onto the joystick with a bit of precision and patience. While screwing it on for the first time it will cut a light thread into the plastic. Be gentle as to not transfer too much torque into the joystick. The result will look like this: + + + + diff --git a/enclosures/open_pill_mini/Main_Chassis.stl b/enclosures/open_pill_mini/Main_Chassis.stl new file mode 100644 index 00000000..805dbaea Binary files /dev/null and b/enclosures/open_pill_mini/Main_Chassis.stl differ diff --git a/enclosures/open_pill_mini/README.md b/enclosures/open_pill_mini/README.md new file mode 100644 index 00000000..f399d496 --- /dev/null +++ b/enclosures/open_pill_mini/README.md @@ -0,0 +1,26 @@ +## The Open Pill Mini Enclosure + +The "Open Pill Mini" is simply a smaller version of the original "Open Pill" that is designed to incorporate a more compact camera specifically designed for the Raspberry Pi Zero -- this enclosure is just about as small as a SeedSigner can conceivably get. It is pictured below, alongside the original Open Pill enclosure. + + + +### Characterisics: +- Supported Camera? "ZeroCam" designed specifically for RPi Zero +- Supported HAT? Waveshare 240x240 pixel LCD display + controls +- Recommended printing process? FDM +- Recommended printing materials? PLA +- Secondary Hardware Required? No +- Data-enabled USB Port accessible? Yes +- Mini-HDMI port accessible? Yes +- Removeable Memory Card? Yes +- Comfortable controls? No (see note below) + +### Assembly Demonstration: +(assembly is very similar to the original Open Pill assembly, depicted below) +https://www.youtube.com/watch?v=aIIc2DiZYcI + +### Comfortable Controls Optional Upgrade: +The bare joystick on an open pill or open pill mini design can be uncomfortable for some thumbs. Twitter user @Vulcan21com developed a DIY solution to add comfort to the exposed joystick using a M2.5 knurled nut standardized under DIN 466, widely available at many hardware stores. The joystick itself has no threading, but it is possible to screw the M2.5 nut onto the joystick with a bit of precision and patience. While screwing it on for the first time it will cut a light thread into the plastic. Be gentle as to not transfer too much torque into the joystick. The result will look like this: + + +(image displayed is Open Pill, not Open Pill Mini) diff --git a/enclosures/open_pill_mini_w_coverplate/Buttons.stl b/enclosures/open_pill_mini_w_coverplate/Buttons.stl new file mode 100644 index 00000000..f6e89c44 Binary files /dev/null and b/enclosures/open_pill_mini_w_coverplate/Buttons.stl differ diff --git a/enclosures/open_pill_mini_w_coverplate/Lid.stl b/enclosures/open_pill_mini_w_coverplate/Lid.stl new file mode 100644 index 00000000..356ef499 Binary files /dev/null and b/enclosures/open_pill_mini_w_coverplate/Lid.stl differ diff --git a/enclosures/open_pill_mini_w_coverplate/Main_Chassis.stl b/enclosures/open_pill_mini_w_coverplate/Main_Chassis.stl new file mode 100644 index 00000000..7f31bfd6 Binary files /dev/null and b/enclosures/open_pill_mini_w_coverplate/Main_Chassis.stl differ diff --git a/enclosures/open_pill_mini_w_coverplate/README.md b/enclosures/open_pill_mini_w_coverplate/README.md new file mode 100644 index 00000000..5bd9d62f --- /dev/null +++ b/enclosures/open_pill_mini_w_coverplate/README.md @@ -0,0 +1,19 @@ +## The Open Pill Mini w/ Coverplate Enclosure + +The "Open Pill Mini w/ Coverplate" was designed as an attempt to incorporate the best of the "Orange Pill" and the "Open Pill" enclosures into a single design. It is fully enclosed for improved aesthetics and protection of electronic components, yet is compact and can be manufactured with a simple FDM printer, also not requiring any additional hardware components. A first for this enclosure was preventing access to the SeedSigner's data-enabled USB port as an additional security assurance for users. + + + +### Characterisics: +- Supported Camera? "ZeroCam" designed specifically for RPi Zero +- Supported HAT? Waveshare 240x240 pixel LCD display + controls +- Recommended printing process? FDM for enclosure & controls +- Recommended printing materials? PLA for enclosure, TPU for controls +- Secondary Hardware Required? No +- Data-enabled USB Port accessible? No +- Mini-HDMI port accessible? No +- Removeable Memory Card? Yes +- Comfortable controls? Yes + +### Assembly Demonstration: +https://www.youtube.com/watch?v=6-5cDneXoWs diff --git a/enclosures/open_pill_mini_w_coverplate/Thumbstick_PLA.stl b/enclosures/open_pill_mini_w_coverplate/Thumbstick_PLA.stl new file mode 100644 index 00000000..c22c02c4 Binary files /dev/null and b/enclosures/open_pill_mini_w_coverplate/Thumbstick_PLA.stl differ diff --git a/enclosures/open_pill_mini_w_coverplate/Thumbstick_TPU.stl b/enclosures/open_pill_mini_w_coverplate/Thumbstick_TPU.stl new file mode 100644 index 00000000..0de57906 Binary files /dev/null and b/enclosures/open_pill_mini_w_coverplate/Thumbstick_TPU.stl differ diff --git a/enclosures/orange_pill/README.md b/enclosures/orange_pill/README.md new file mode 100644 index 00000000..a3348aae --- /dev/null +++ b/enclosures/orange_pill/README.md @@ -0,0 +1,24 @@ +## The Orange Pill Enclosure + +The "Orange Pill" was the first SeedSigner enclosure. It's eye-catching design is the enclosure most commonly associated with the SeedSigner project for many. While aesthetically appealing and fun to showcase to others, the thumbstick topper's sub-optimal design, the need for secondary hardware components, and other drawbacks make this enclosure a less desirable choice for every day SeedSigner use. + + + +### Characterisics: +- Supported Camera? Legacy RPi Camera (w/ gold Pi Zero Cable) +- Supported HAT? Waveshare 240x240 pixel LCD display + controls +- Recommended printing process? FDM for enclosure, SLA for controls +- Recommended printing materials? PLA for enclosure, resin for controls +- Secondary Hardware Required? Yes +- Data-enabled USB Port accessible? Yes +- Mini-HDMI port accessible? Yes +- Removeable Memory Card? Requires partial disassembly +- Comfortable controls? Yes (but thumbstick can be clumsy) + +### Secondary Hardware Required: +- Four (4): 10mm M2.5 F-F spacers +- Four (4): 6mm M2.5 screws +- Four (4): 12mm M2.5 screws + +### Assembly Demonstration: +https://www.youtube.com/watch?v=aIIc2DiZYcI diff --git a/enclosures/rugged_pill/Multimaterial/RuggedPIll_Bottom_MMU.3mf b/enclosures/rugged_pill/Multimaterial/RuggedPIll_Bottom_MMU.3mf new file mode 100644 index 00000000..ca393c01 Binary files /dev/null and b/enclosures/rugged_pill/Multimaterial/RuggedPIll_Bottom_MMU.3mf differ diff --git a/enclosures/rugged_pill/Multimaterial/RuggedPill_Top_MMU.3mf b/enclosures/rugged_pill/Multimaterial/RuggedPill_Top_MMU.3mf new file mode 100644 index 00000000..95cb6b1a Binary files /dev/null and b/enclosures/rugged_pill/Multimaterial/RuggedPill_Top_MMU.3mf differ diff --git a/enclosures/rugged_pill/README.md b/enclosures/rugged_pill/README.md new file mode 100644 index 00000000..3e6caad0 --- /dev/null +++ b/enclosures/rugged_pill/README.md @@ -0,0 +1,34 @@ +## The Rugged Pill Enclosure + + + +- The Rugged Pill is an enclosure designed for building a SeedSigner without the need for screws or spacers. +- This enclosure requires use of the so called "zerocam", which allows for a more compact form factor. +- The enclosure offers a very comfortable handling experience. +- The included controls can be printed from PLA, but TPU is recommended. +- Recommended shore hardness is 95A, but any TPU will do. +- Separate files are available for single and multicolor printing. +- Top and bottom part snap fit together. +- SD-card removable +- Data port is closed. (Use the separate file labeled 4_SeedHammer) + + +### File overwiev + +#### Single color: +- RuggedPill_Top.stl[^1] +- RuggedPill_Bottom.stl[^1] +- RuggedPillBottom_4SeedHammer.stl[^1] + +- RuggedPill_Buttons.stl[^2] +- RuggedPill_Thumbstick.stl[^2] +- Rugged_Pill_Trackball.stl[^2] + +#### Multimaterial: +- RuggedPill_Top_MMU.3mf +- RuggedPill_Bottom_MMU.3mf + +You only need one set of buttons and a Thumbstick or Trackball, print both since they use minmal material. Test which one you like best and use that. + +[^1]: (Recommended Materials: PLA, PETG) +[^2]: (Recommended Materials: TPU) diff --git a/enclosures/rugged_pill/RuggedPillBottom_4SeedHammer.stl b/enclosures/rugged_pill/RuggedPillBottom_4SeedHammer.stl new file mode 100644 index 00000000..04ac0813 Binary files /dev/null and b/enclosures/rugged_pill/RuggedPillBottom_4SeedHammer.stl differ diff --git a/enclosures/rugged_pill/RuggedPill_Bottom.stl b/enclosures/rugged_pill/RuggedPill_Bottom.stl new file mode 100644 index 00000000..c8f0ba78 Binary files /dev/null and b/enclosures/rugged_pill/RuggedPill_Bottom.stl differ diff --git a/enclosures/rugged_pill/RuggedPill_Buttons.stl b/enclosures/rugged_pill/RuggedPill_Buttons.stl new file mode 100644 index 00000000..d8928bf2 Binary files /dev/null and b/enclosures/rugged_pill/RuggedPill_Buttons.stl differ diff --git a/enclosures/rugged_pill/RuggedPill_Thumbstick.stl b/enclosures/rugged_pill/RuggedPill_Thumbstick.stl new file mode 100644 index 00000000..20367d54 Binary files /dev/null and b/enclosures/rugged_pill/RuggedPill_Thumbstick.stl differ diff --git a/enclosures/rugged_pill/RuggedPill_Top.stl b/enclosures/rugged_pill/RuggedPill_Top.stl new file mode 100644 index 00000000..0a871291 Binary files /dev/null and b/enclosures/rugged_pill/RuggedPill_Top.stl differ diff --git a/enclosures/rugged_pill/RuggedPill_Trackball.stl b/enclosures/rugged_pill/RuggedPill_Trackball.stl new file mode 100644 index 00000000..ccfbc1be Binary files /dev/null and b/enclosures/rugged_pill/RuggedPill_Trackball.stl differ diff --git a/enclosures/simple_pill/README.md b/enclosures/simple_pill/README.md new file mode 100644 index 00000000..8bbc83af --- /dev/null +++ b/enclosures/simple_pill/README.md @@ -0,0 +1,25 @@ +## The Simple Pill Enclosure + +The "Simple Pill" is a community-contributed design by @blackcoffeebtc. The most significant innovation of this design was the adaptation of a DPAD-stype thumbstick topper for a SeedSigner enclosure, offering a much more comfortable and responsive control experience. The Simple Pill was also the first enclosure to allow for easy access to the device's memory card. + + + +### Characterisics: +- Supported Camera? Legacy RPi Camera (w/ gold Pi Zero Cable) +- Supported HAT? Waveshare 240x240 pixel LCD display + controls +- Recommended printing process? FDM for enclosure, SLA for controls +- Recommended printing materials? PLA for enclosure, resin for controls +- Secondary Hardware Required? Yes +- Data-enabled USB Port accessible? Yes +- Mini-HDMI port accessible? Yes +- Removeable Memory Card? Yes +- Comfortable controls? Yes + +### Secondary Hardware Required: +- Four (4): 10mm M2.5 F-F spacers +- Four (4): 6mm M2.5 screws +- Four (4): 12mm M2.5 screws + +### Assembly Demonstration: +(assembly is similar to that of the original Orange Pill) +https://www.youtube.com/watch?v=aIIc2DiZYcI diff --git a/enclosures/simple_pill/protective_case/sp_protective_case_lower.stl b/enclosures/simple_pill/protective_case/sp_protective_case_lower.stl new file mode 100644 index 00000000..1fc2e277 Binary files /dev/null and b/enclosures/simple_pill/protective_case/sp_protective_case_lower.stl differ diff --git a/enclosures/simple_pill/protective_case/sp_protective_case_upper.stl b/enclosures/simple_pill/protective_case/sp_protective_case_upper.stl new file mode 100644 index 00000000..8d3e7f02 Binary files /dev/null and b/enclosures/simple_pill/protective_case/sp_protective_case_upper.stl differ diff --git a/i18n_notes.txt b/i18n_notes.txt new file mode 100644 index 00000000..0b6a2db0 --- /dev/null +++ b/i18n_notes.txt @@ -0,0 +1,72 @@ +# i18n notes +# +# Format is "Pattern filename space-separated-list-of-line-numbers-with-pattern-prefixes" as of 20 November 2022 +# +# kdmukai/intial_multilanguage branch already has plenty of work +# * copied kdmukai's branch +# * merged dev in into his branch. +# * following his lead to change text literals and f-string substitutions. +# * NOTE: TODO: add TranslatorNotes once I understand when they are needed!!! +# +# Common coding patterns noted: (source code lines below are prefixed w/[LFDECP) to identify pattern) +# +# * L) Literal text strings. +# +# * F) F-strings with placeholders. +# +# * V) variables with values set to a previously translated constant, ie _(display_name) +# +# * D) Default variables in baseclasses like "Button Label": is it necessary? user should never see these, else complain? +# +# * E) Exception messages: is it really necessary? only if frequent, else let user complain? +# +# * P) Proper names, partner logos, Coordinators... is it really of any concern at all? +# +# * print() debug messages to the C)onsole or /dev/null do not need translations? Not even included here. +# +# +# While reading the notes below: +# Each file and line-number are preceded by the type of i18n change needed, if truly 'needed' +# A preceding # indicates there is nothing left to do, it's already i18n-ized. +# A preceding ? indicates that I should give this another look, or a deeper look. +# A preceding [DEP] might indicate nothing to do... if we really don't want to i18n these patterns. +# + + +E seedsigner/controller.py E117 E161 E168 + +ED seedsigner/gui/components.py E285 E324 E457 E498 E591 D1096 D1311 +E seedsigner/gui/keyboard.py E204 E207 E485 E587 +#L seedsigner/gui/renderer.py #L144 + +DE#LF seedsigner/gui/screens/screen.py D193 E524 #L756 #L771 #L885 #L888 #L900 #F1112 +#LF seedsigner/gui/screens/psbt_screens.py #L526 #L543? #L553 #L30 #L32 #L89 #L91 #L92 #L94 #F96 #F97 #F100 #L128 #L132 #L141 #L144 #L146 #F148 #L150 #L155 #L466 #L467 #L473 #L485 #L560 #F571 #FL667 #L677 #L689 #L703 +#L seedsigner/gui/screens/scan_screens.py #L22 #L138 #L139 #L140 #L156 +?#L seedsigner/gui/screens/settings_screens.py (_().replace() hmmm ?#L294) #L26 #L59 #L130 #L141 #L162 #L176 #L222 #L289 +#LF seedsigner/gui/screens/tools_screens.py #L41 #L72 #L99 #L119 #F150 #L161 #F167 #F177 #F189 #F193 #F200 #F241 #L274 #F311 #L352 #L354 #L369 #L385 #L393 #F402 #F410 #F418 +#L seedsigner/gui/screens/main_menu_screens.py #L12 #L24 #L29 #L39 #L44 +?#L seedsigner/gui/screens/seed_screens.py (why colon ?#L1033) #L411 #L424 #L555 #L561 #L571 #L587 #L593 #L597 #L606 #L616 #L625 #L638 #L1026 #L1121 #L1123 #L1131 #L1133 #L1147 #L1149 #L1226 #L1362 #L1419 #L1483 #L1500 #L1505 #L1518 #L1524 #L1532 + +E seedsigner/hardware/camera.py E36 E65 +E seedsigner/hardware/ST7789.py E154 + +E seedsigner/helpers/embit_utils.py E25 E35 E43 E45 E47 E91 E95 +E seedsigner/helpers/mnemonic_generation.py E22 +? seedsigner/helpers/ur2/DID-NOT-LOOK + +E#LF seedsigner/views/psbt_views.py E30 #L36 #L37 #L38 #L55 #L94 #L166 #L167 #L168 #L185 #L186 #L187 #L253 #259 #L262 #L331 #L335 #L336 #L338 #L339 #L358 #L360 #L441 #F443 #L445 #FL447 #L451 #L453 #L478 #L530 #L532 #L533 #L534 +#L seedsigner/views/screensaver.py #L74 +E#LF seedsigner/views/seed_views.py (Why this one ?#E1464) #L1637 #L54 #L57 #L78 #L79 #L80 #L81 #L90 #F127 #L187 #L188 #L192 #L194 #L229 #L230 #L260 #L300 #L301 #L342 #L343 #L348 #F350 #L384 #L385 #L386 #L387 #L388 #L389 #L390 #L420 #L476 #L477 #L510 #L511 #L515 #L554 #L556 #L641 #L683 #L684 #L693 #L830 #L855 #L856 #F836 #L902 #L903 #F952 #L992 #L993 #L997 #L999 #F1000 #L1018 #L1020 #L1021 #L1022 #L1044 #L1045 #L1049 #L1050 #L1071 #L1120 #L1121 #L1213 #L1214 #L1218 #L1248 #L1257 #L1258 #L1259 #L1221 #L1268 #L1269 #L1270 #L1272 #L1280 #L1281 #L1282 #L1284 #L1353 #L1354 #L1391 #L1392 #L1393 #L1396 #L1406 #L1413 #L1510 #L1511 #F1639 #L1642 #L1652 #L1653 #L1682 #L1683 #L1684 #L1685 +#V seedsigner/views/settings_views.py #V26 #L20 #L21 #L36 #L39 #L46 #L54 +E#LF seedsigner/views/tools_views.py E605 E616 #L29 #L30 #L31 #L32 #L35 #L112 #L113 #L117 #L184 #L185 #L189 #L238 #L239 #L243 #L275 #L276 #L277 #L376 #L379 #L401 #L402 #L430 #L431 #L432 #L433 #L451 #L538 #L539 #L589 #F628 #FL631 +E#L seedsigner/views/view.py E66 #L124 #L125 #L126 #L127 #L142 #L145 + +E seedsigner/models/encode_qr.py E46 E104 E143 E146 E150 E153 E273 E356 +E seedsigner/models/psbt_parser.py E96 E226 E229 E232 E241 E245 E255 E269 +E seedsigner/models/seed.py E26 E42 +E seedsigner/models/seed_storage.py E68 +P#L seedsigner/models/settings_definition.py P37 P38 P39 P40 #V288 #L14 #L15 #L18 #L19 #L20 #L23 #L26 #L29 #L74 #L55 #L76 #L77 #L96 #L97 #L98 #L106 #L107 #L108 #L124 #L125 #L134 #L135 #L136 #L137 #L357 #L365 #L373 #L374 #L379 #L386 #L395 #L403 #L411 #L417 #L425 #L433 #L439 #L447 #L455 #L461 #L467 #L473 +E seedsigner/models/settings.py E112 E134 E150 E153 E162 E165 +E seedsigner/models/singleton.py E6 E24 E31 +E seedsigner/models/threads.py E23 + diff --git a/requirements-raspi.txt b/requirements-raspi.txt new file mode 100644 index 00000000..8053eeab --- /dev/null +++ b/requirements-raspi.txt @@ -0,0 +1,3 @@ +picamera==1.13 +RPi.GPIO==0.7.0 +spidev==3.5 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index fe2d6366..337ea35b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,8 @@ Babel==2.10.1 -embit==0.6.1 -numpy==1.21.1 -picamera==1.13 -Pillow==9.1.0 -pytz==2022.1 --e git+https://github.com/seedsigner/pyzbar.git@c3c237821c6a20b17953efe59b90df0b514a1c03#egg=pyzbar +embit==0.7.0 +numpy==1.25.2 +Pillow==9.4.0 +pyzbar @ git+https://github.com/seedsigner/pyzbar.git@c3c237821c6a20b17953efe59b90df0b514a1c03 qrcode==7.3.1 -RPi.GPIO==0.7.0 six==1.16.0 -spidev==3.5 -urtypes @ git+https://github.com/jreesun/urtypes.git@e0d0db277ec2339650343eaf7b220fffb9233241 +urtypes @ git+https://github.com/selfcustody/urtypes.git@7fb280eab3b3563dfc57d2733b0bf5cbc0a96a6a diff --git a/setup.py b/setup.py index cc8d8fb3..fb6eff2c 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh: setuptools.setup( name="seedsigner", - version="0.5.0", + version="0.7.0", author="SeedSigner", author_email="author@example.com", description="Build an offline, airgapped Bitcoin signing device for less than $50!", @@ -23,4 +23,4 @@ setuptools.setup( package_dir={"": "src"}, packages=setuptools.find_packages(where="src"), python_requires=">=3.6", -) \ No newline at end of file +) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index b3169558..23903dbe 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -1,25 +1,23 @@ +import time import logging + import traceback import gettext +import os -from embit.descriptor import Descriptor -from embit.psbt import PSBT from PIL.Image import Image -from typing import List -from seedsigner.gui.renderer import Renderer -from seedsigner.hardware.buttons import HardwareButtons -from seedsigner.views.screensaver import ScreensaverScreen -from seedsigner.views.view import Destination, NotYetImplementedView, UnhandledExceptionView - -from .models import Seed, SeedStorage, Settings, Singleton, PSBTParser +from seedsigner.models.settings import Settings +from seedsigner.models.singleton import Singleton +from seedsigner.models.threads import BaseThread +from seedsigner.views.view import Destination logger = logging.getLogger(__name__) -class BackStack(List[Destination]): +class BackStack(list[Destination]): def __repr__(self): if len(self) == 0: return "[]" @@ -31,6 +29,54 @@ class BackStack(List[Destination]): +class StopFlowBasedTest(Exception): + """ + This is a special exception that is only raised by the test suite to stop the + Controller's main loop. It should not be raised by any other code. + """ + pass + + + +class FlowBasedTestException(Exception): + """ + This is a special exception that is only raised by the test suite. + It should not be raised by any other code. + """ + pass + + + +class BackgroundImportThread(BaseThread): + def run(self): + from importlib import import_module + + # import seedsigner.hardware.buttons # slowly imports GPIO along the way + + def time_import(module_name): + last = time.time() + import_module(module_name) + # print(time.time() - last, module_name) + + time_import('embit') + time_import('seedsigner.helpers.embit_utils') + + # Do costly initializations + time_import('seedsigner.models.seed_storage') + from seedsigner.models.seed_storage import SeedStorage + Controller.get_instance()._storage = SeedStorage() + + # Get MainMenuView ready to respond quickly + time_import('seedsigner.views.scan_views') + + time_import('seedsigner.views.seed_views') + + time_import('seedsigner.views.tools_views') + + time_import('seedsigner.views.settings_views') + + + class Controller(Singleton): """ The Controller is a globally available singleton that maintains SeedSigner state. @@ -49,29 +95,29 @@ class Controller(Singleton): rather than at the top in order avoid circular imports. """ - VERSION = "0.5.1" + VERSION = "0.7.0" # Declare class member vars with type hints to enable richer IDE support throughout # the code. - buttons: HardwareButtons = None - storage: SeedStorage = None + _storage: 'SeedStorage' = None # TODO: Rename "storage" to something more indicative of its temp, in-memory state settings: Settings = None - renderer: Renderer = None # TODO: Refactor these flow-related attrs that survive across multiple Screens. # TODO: Should all in-memory flow-related attrs get wiped on MainMenuView? - psbt: PSBT = None - psbt_seed: Seed = None - psbt_parser: PSBTParser = None + psbt: 'embit.psbt.PSBT' = None + psbt_seed: 'Seed' = None + psbt_parser: 'PSBTParser' = None unverified_address = None - multisig_wallet_descriptor: Descriptor = None + multisig_wallet_descriptor: 'embit.descriptor.Descriptor' = None - image_entropy_preview_frames: List[Image] = None + image_entropy_preview_frames: list[Image] = None image_entropy_final_image: Image = None address_explorer_data: dict = None + + sign_message_data: dict = None # TODO: end refactor section # Destination placeholder for when we need to jump out to a side flow but intend to @@ -81,10 +127,12 @@ class Controller(Singleton): FLOW__VERIFY_MULTISIG_ADDR = "multisig_addr" FLOW__VERIFY_SINGLESIG_ADDR = "singlesig_addr" FLOW__ADDRESS_EXPLORER = "address_explorer" + FLOW__SIGN_MESSAGE = "sign_message" resume_main_flow: str = None back_stack: BackStack = None - screensaver: ScreensaverScreen = None + screensaver: 'ScreensaverScreen' = None + toast_notification_thread: 'BaseToastOverlayManagerThread' = None # Babel initialization gettext.install('messages', localedir='seedsigner/resources/babel') @@ -112,6 +160,9 @@ class Controller(Singleton): each time you try to re-initialize a Controller. """ + from seedsigner.gui.renderer import Renderer + from seedsigner.hardware.microsd import MicroSD + # Must be called before the first get_instance() call if cls._instance: raise Exception("Instance already configured") @@ -120,16 +171,11 @@ class Controller(Singleton): controller = cls.__new__(cls) cls._instance = controller - # Input Buttons - if disable_hardware: - controller.buttons = None - else: - controller.buttons = HardwareButtons.get_instance() - # models - # TODO: Rename "storage" to something more indicative of its temp, in-memory state - controller.storage = SeedStorage() controller.settings = Settings.get_instance() + + controller.microsd = MicroSD.get_instance() + controller.microsd.start_detection() # Store one working psbt in memory controller.psbt = None @@ -138,13 +184,14 @@ class Controller(Singleton): # Configure the Renderer Renderer.configure_instance() - controller.screensaver = ScreensaverScreen(controller.buttons) - controller.back_stack = BackStack() # Other behavior constants - controller.screensaver_activation_ms = 120 * 1000 + controller.screensaver_activation_ms = 2 * 60 * 1000 # two minutes + background_import_thread = BackgroundImportThread() + background_import_thread.start() + return cls._instance @@ -152,9 +199,18 @@ class Controller(Singleton): def camera(self): from .hardware.camera import Camera return Camera.get_instance() + + + @property + def storage(self): + while not self._storage: + # Wait for the BackgroundImportThread to finish initializing the storage. + # This is a rare timing issue that likely only occurs in the test suite. + time.sleep(0.001) + return self._storage - def get_seed(self, seed_num: int) -> Seed: + def get_seed(self, seed_num: int) -> 'Seed': if seed_num < len(self.storage.seeds): return self.storage.seeds[seed_num] else: @@ -169,7 +225,6 @@ class Controller(Singleton): def pop_prev_from_back_stack(self): - from .views import Destination if len(self.back_stack) > 0: # Pop the top View (which is the current View_cls) self.back_stack.pop() @@ -184,13 +239,18 @@ class Controller(Singleton): self.back_stack = BackStack() - def start(self) -> None: - from .views import BackStackView - from .views.main_menu_views import MainMenuView - from .views.screensaver import OpeningSplashScreen + def start(self, initial_destination: Destination = None) -> None: + """ + The main loop of the application. - opening_splash = OpeningSplashScreen() - opening_splash.start() + * initial_destination: The first View to run. If None, the MainMenuView is + used. Only used by the test suite. + """ + from seedsigner.views import MainMenuView, BackStackView + from seedsigner.views.screensaver import OpeningSplashScreen + from seedsigner.gui.toast import RemoveSDCardToastManagerThread + + OpeningSplashScreen().start() """ Class references can be stored as variables in python! @@ -218,7 +278,14 @@ class Controller(Singleton): View_cls(**init_args).run() """ try: - next_destination = Destination(MainMenuView) + if initial_destination: + next_destination = initial_destination + else: + next_destination = Destination(MainMenuView) + + # Set up our one-time toast notification tip to remove the SD card + self.activate_toast(RemoveSDCardToastManagerThread()) + while True: # Destination(None) is a special case; render the Home screen if next_destination.View_cls is None: @@ -228,23 +295,41 @@ class Controller(Singleton): # Home always wipes the back_stack self.clear_back_stack() - # Clear other temp vars + # Home always wipes the back_stack/state of temp vars self.resume_main_flow = None self.multisig_wallet_descriptor = None self.unverified_address = None self.address_explorer_data = None - + self.psbt = None + self.psbt_parser = None + self.psbt_seed = None + print(f"back_stack: {self.back_stack}") try: + # Instantiate the View class and run it print(f"Executing {next_destination}") next_destination = next_destination.run() + + except StopFlowBasedTest: + # This is a special exception that is only raised by the test suite + # to stop the Controller loop and exit the test. + return + + except FlowBasedTestException as e: + # This is a special exception that is only raised by the test suite. + # Re-raise so the test suite can handle it. + raise e + except Exception as e: # Display user-friendly error screen w/debugging info + import traceback + traceback.print_exc() next_destination = self.handle_exception(e) if not next_destination: # Should only happen during dev when you hit an unimplemented option + from seedsigner.views.view import NotYetImplementedView next_destination = Destination(NotYetImplementedView) if next_destination.skip_current_view: @@ -277,16 +362,61 @@ class Controller(Singleton): print("-" * 30) finally: - if self.screensaver.is_running: + from seedsigner.gui.renderer import Renderer + if self.is_screensaver_running: self.screensaver.stop() + + if self.toast_notification_thread and self.toast_notification_thread.is_alive(): + self.toast_notification_thread.stop() # Clear the screen when exiting print("Clearing screen, exiting") Renderer.get_instance().display_blank_screen() + @property + def is_screensaver_running(self): + return self.screensaver is not None and self.screensaver.is_running + + def start_screensaver(self): + # If a toast is running, tell it to give up the Renderer.lock; it will then + # block until the screensaver is done, at which point the toast can re-acquire + # the Renderer.lock and resume where it left off. + if self.toast_notification_thread and self.toast_notification_thread.is_alive(): + print(f"Controller: settings toggle_render_lock for {self.toast_notification_thread.__class__.__name__}") + self.toast_notification_thread.toggle_renderer_lock() + + print("Controller: Starting screensaver") + if not self.screensaver: + # Do a lazy/late import and instantiation to reduce Controller initial startup time + from seedsigner.views.screensaver import ScreensaverScreen + from seedsigner.hardware.buttons import HardwareButtons + self.screensaver = ScreensaverScreen(HardwareButtons.get_instance()) + + # Start the screensaver, but it will block until it can acquire the Renderer.lock. self.screensaver.start() + print("Controller: Screensaver started") + + + def activate_toast(self, toast_manager_thread: 'BaseToastOverlayManagerThread'): + """ + Ensures that the Controller has explicit control over which processes get to + claim the Renderer.lock and which need to (potentially) release it. + """ + if self.is_screensaver_running: + # New toast notifications break out of the Screensaver + print("Controller: stopping screensaver") + self.screensaver.stop() + + if self.toast_notification_thread and self.toast_notification_thread.is_alive(): + # Can only run one toast at a time + print(f"Controller: stopping {self.toast_notification_thread.__class__.__name__}") + self.toast_notification_thread.stop() + + self.toast_notification_thread = toast_manager_thread + print(f"Controller: starting {self.toast_notification_thread.__class__.__name__}") + self.toast_notification_thread.start() def handle_exception(self, e) -> Destination: @@ -299,6 +429,7 @@ class Controller(Singleton): * python file, line num, method name * Exception message """ + from seedsigner.views.view import UnhandledExceptionView logger.exception(e) # The final exception output line is: @@ -306,7 +437,12 @@ class Controller(Singleton): # So we extract the Exception type and trim off any "foo.bar." namespacing: last_line = traceback.format_exc().splitlines()[-1] exception_type = last_line.split(":")[0].split(".")[-1] - exception_msg = last_line.split(":")[1] + + # Extract the error message, if there is one + if ":" in last_line: + exception_msg = last_line.split(":")[1] + else: + exception_msg = "" # Scan for the last debugging line that includes a line number reference line_info = None diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index 01511fc4..e6f41f9f 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -1,15 +1,17 @@ import math import os import pathlib +import re +from time import time from dataclasses import dataclass from decimal import Decimal from PIL import Image, ImageDraw, ImageFont, ImageFilter from typing import Any, List, Tuple -from seedsigner.models import Singleton from seedsigner.models.settings import Settings from seedsigner.models.settings_definition import SettingsConstants +from seedsigner.models.singleton import Singleton @@ -21,28 +23,29 @@ class GUIConstants: BACKGROUND_COLOR = "black" WARNING_COLOR = "#FFD60A" - DIRE_WARNING_COLOR = "red" - SUCCESS_COLOR = "#00dd00" - BITCOIN_ORANGE = "#ff9416" - ACCENT_COLOR = "orange" - TESTNET_COLOR = "#00f100" - REGTEST_COLOR = "#00caf1" - LIGHT_BLUE = "#0084ff" + DIRE_WARNING_COLOR = "#FF453A" + SUCCESS_COLOR = "#30D158" + INFO_COLOR = "#0084FF" + ACCENT_COLOR = "#FF9F0A" + BITCOIN_ORANGE = "#FF9416" + TESTNET_COLOR = "#00F100" + REGTEST_COLOR = "#00CAF1" - ICON_FONT_NAME__FONT_AWESOME = "Font_Awesome_6_Free-Solid-900.otf" - ICON_FONT_NAME__SEEDSIGNER = "seedsigner-glyphs.otf" + ICON_FONT_NAME__FONT_AWESOME = "Font_Awesome_6_Free-Solid-900" + ICON_FONT_NAME__SEEDSIGNER = "seedsigner-icons" ICON_FONT_SIZE = 22 ICON_INLINE_FONT_SIZE = 24 - ICON_LARGE_BUTTON_SIZE = 36 + ICON_LARGE_BUTTON_SIZE = 48 + ICON_TOAST_FONT_SIZE = 30 ICON_PRIMARY_SCREEN_SIZE = 50 TOP_NAV_TITLE_FONT_NAME = { - "default": "OpenSans-SemiBold.ttf", - "ar": "multilanguage/NotoSansAR-Regular.ttf", - "he": "multilanguage/NotoSansHE-Regular.ttf", - "ja": "multilanguage/NotoSansJP-Regular.otf", - "kr": "multilanguage/NotoSansKR-Regular.otf", - "ru": "multilanguage/NotoSans-Regular.ttf", + "default": "OpenSans-SemiBold", + "ar": "multilanguage/NotoSansAR-Regular", + "he": "multilanguage/NotoSansHE-Regular", + "ja": "multilanguage/NotoSansJP-Regular", + "kr": "multilanguage/NotoSansKR-Regular", + "ru": "multilanguage/NotoSans-Regular", } TOP_NAV_TITLE_FONT_SIZE = { "default": 20, @@ -51,12 +54,12 @@ class GUIConstants: TOP_NAV_BUTTON_SIZE = 32 BODY_FONT_NAME = { - "default": "OpenSans-Regular.ttf", - "ar": "multilanguage/NotoSansAR-Regular.ttf", - "he": "multilanguage/NotoSansHE-Regular.ttf", - "ja": "multilanguage/NotoSansJP-Regular.otf", - "kr": "multilanguage/NotoSansKR-Regular.otf", - "ru": "multilanguage/NotoSans-Regular.ttf", + "default": "OpenSans-Regular", + "ar": "multilanguage/NotoSansAR-Regular", + "he": "multilanguage/NotoSansHE-Regular", + "ja": "multilanguage/NotoSansJP-Regular", + "kr": "multilanguage/NotoSansKR-Regular", + "ru": "multilanguage/NotoSans-Regular", } BODY_FONT_SIZE = { "default": 17, @@ -64,32 +67,34 @@ class GUIConstants: } BODY_FONT_MAX_SIZE = TOP_NAV_TITLE_FONT_SIZE["default"] BODY_FONT_MIN_SIZE = 15 - BODY_FONT_COLOR = "#f8f8f8" + BODY_FONT_COLOR = "#FCFCFC" BODY_LINE_SPACING = COMPONENT_PADDING - FIXED_WIDTH_FONT_NAME = "Inconsolata-Regular.ttf" - FIXED_WIDTH_EMPHASIS_FONT_NAME = "Inconsolata-SemiBold.ttf" + FIXED_WIDTH_FONT_NAME = "Inconsolata-Regular" + FIXED_WIDTH_EMPHASIS_FONT_NAME = "Inconsolata-SemiBold" LABEL_FONT_SIZE = BODY_FONT_MIN_SIZE - LABEL_FONT_COLOR = "#777" + LABEL_FONT_COLOR = "#777777" BUTTON_FONT_NAME = { - "default": "OpenSans-SemiBold.ttf", - "ar": "multilanguage/NotoSansAR-Regular.ttf", - "he": "multilanguage/NotoSansHE-Regular.ttf", - "ja": "multilanguage/NotoSansJP-Regular.otf", - "kr": "multilanguage/NotoSansKR-Regular.otf", - "ru": "multilanguage/NotoSans-Regular.ttf", + "default": "OpenSans-SemiBold", + "ar": "multilanguage/NotoSansAR-Regular", + "he": "multilanguage/NotoSansHE-Regular", + "ja": "multilanguage/NotoSansJP-Regular", + "kr": "multilanguage/NotoSansKR-Regular", + "ru": "multilanguage/NotoSans-Regular", } BUTTON_FONT_SIZE = { "default": 18, "ar": 16, "ja": 16, } - BUTTON_FONT_COLOR = "#e8e8e8" - BUTTON_BACKGROUND_COLOR = "#2c2c2c" + BUTTON_FONT_COLOR = "#FCFCFC" + BUTTON_BACKGROUND_COLOR = "#2C2C2C" BUTTON_HEIGHT = 32 - BUTTON_SELECTED_FONT_COLOR = "black" + BUTTON_SELECTED_FONT_COLOR = BACKGROUND_COLOR + + NOTIFICATION_COLOR = "#00F100" @staticmethod @@ -153,11 +158,8 @@ class FontAwesomeIconConstants: ANGLE_RIGHT = "\uf105" ANGLE_UP = "\uf106" CAMERA = "\uf030" - CARET_DOWN = "\uf0d7" - CARET_LEFT = "\uf0d9" - CARET_RIGHT = "\uf0da" - CARET_UP = "\uf0d8" - SOLID_CIRCLE_CHECK = "\uf058" + CHEVRON_UP = "\uf077" + CHEVRON_DOWN = "\uf078" CIRCLE = "\uf111" CIRCLE_CHEVRON_RIGHT = "\uf138" DICE = "\uf522" @@ -167,47 +169,64 @@ class FontAwesomeIconConstants: DICE_FOUR = "\uf524" DICE_FIVE = "\uf523" DICE_SIX = "\uf526" - FILE_SIGNATURE = "\uf573" - GEAR = "\uf013" - KEY = "\uf084" KEYBOARD = "\uf11c" LOCK = "\uf023" MAP = "\uf279" PAPER_PLANE = "\uf1d8" PEN = "\uf304" - PLUS = "+" - POWER_OFF = "\uf011" - ROTATE_RIGHT = "\uf2f9" - SCREWDRIVER_WRENCH = "\uf7d9" - SQUARE = "\uf0c8" SQUARE_CARET_DOWN = "\uf150" SQUARE_CARET_LEFT = "\uf191" SQUARE_CARET_RIGHT = "\uf152" SQUARE_CARET_UP = "\uf151" - SQUARE_CHECK = "\uf14a" - TRIANGLE_EXCLAMATION = "\uf071" UNLOCK = "\uf09c" - QRCODE = "\uf029" X = "\u0058" +class SeedSignerIconConstants: + # Menu icons + SCAN = "\ue900" + SEEDS = "\ue901" + SETTINGS = "\ue902" + TOOLS = "\ue903" -class SeedSignerCustomIconConstants: - LARGE_CHEVRON_LEFT = "\ue900" - SMALL_CHEVRON_RIGHT = "\ue901" - PAGE_UP = "\ue903" - PAGE_DOWN = "\ue902" - PLUS = "\ue904" - CIRCLE_CHECK = "\ue907" - CIRCLE_EXCLAMATION = "\ue908" - CIRCLE_X = "\ue909" - FINGERPRINT = "\ue90a" - PATH = "\ue90b" - BITCOIN_LOGO_STRAIGHT = "\ue90c" - BITCOIN_LOGO_TILTED = "\ue90d" + # Utility icons + BACK = "\ue904" + CHECK = "\ue905" + CHECKBOX = "\ue906" + CHECKBOX_SELECTED = "\ue907" + CHEVRON_DOWN = "\ue908" + CHEVRON_LEFT = "\ue909" + CHEVRON_RIGHT = "\ue90a" + CHEVRON_UP = "\ue90b" + CLOSE = "\ue90c" + PAGE_DOWN = "\ue90d" + PAGE_UP = "\ue90e" + PLUS = "\ue90f" + POWER = "\ue910" + RESTART = "\ue911" - MIN_VALUE = LARGE_CHEVRON_LEFT - MAX_VALUE = BITCOIN_LOGO_TILTED + # Messaging icons + ERROR = "\ue912" + SUCCESS = "\ue913" + WARNING = "\ue914" + + # Informational icons + ADDRESS = "\ue915" + CHANGE = "\ue916" + DERIVATION = "\ue917" + FEE = "\ue918" + FINGERPRINT = "\ue919" + PASSPHRASE = "\ue91a" + + # Misc icons + BITCOIN = "\ue91b" + BITCOIN_ALT = "\ue91c" + BRIGHTNESS = "\ue91d" + MICROSD = "\ue91e" + QRCODE = "\ue91f" + + MIN_VALUE = SCAN + MAX_VALUE = QRCODE @@ -272,17 +291,20 @@ class Fonts(Singleton): fonts = {} @classmethod - def get_font(cls, font_name, size) -> ImageFont.FreeTypeFont: + def get_font(cls, font_name, size, file_extension: str = "ttf") -> ImageFont.FreeTypeFont: # Cache already-loaded fonts if font_name not in cls.fonts: cls.fonts[font_name] = {} + if font_name in [GUIConstants.ICON_FONT_NAME__FONT_AWESOME, GUIConstants.ICON_FONT_NAME__SEEDSIGNER]: + file_extension = "otf" + if size not in cls.fonts[font_name]: try: - cls.fonts[font_name][size] = ImageFont.truetype(os.path.join(cls.font_path, font_name), size) + cls.fonts[font_name][size] = ImageFont.truetype(os.path.join(cls.font_path, f"{font_name}.{file_extension}"), size) except OSError as e: if "cannot open resource" in str(e): - raise Exception(f"Font {font_name} not found: {repr(e)}") + raise Exception(f"Font {font_name + '.' + file_extension + ':' + str(size)} not found: {repr(e)}") else: raise e @@ -344,7 +366,7 @@ class TextArea(BaseComponent): height: int = None # None = special case: autosize to min height screen_x: int = 0 screen_y: int = 0 - min_text_x: int = None + min_text_x: int = 0 # Text can not start at x any less than this background_color: str = GUIConstants.BACKGROUND_COLOR font_name: str = None font_size: int = None @@ -353,7 +375,8 @@ class TextArea(BaseComponent): is_text_centered: bool = True supersampling_factor: int = 1 auto_line_break: bool = True - allow_text_overflow: bool = True + allow_text_overflow: bool = False + height_ignores_below_baseline: bool = False # If True, characters that render below the baseline (e.g. "pqgy") will not affect the final height calculation def __post_init__(self): @@ -366,166 +389,133 @@ class TextArea(BaseComponent): if not self.width: self.width = self.canvas_width + + if self.screen_x + self.width > self.canvas_width: + self.width = self.canvas_width - self.screen_x - if self.font_size < 18 and (not self.supersampling_factor or self.supersampling_factor == 1): - self.supersampling_factor = 2 - - self.font = Fonts.get_font(self.font_name, int(self.supersampling_factor * self.font_size)) - self.supersampled_width = self.supersampling_factor * self.width - if self.height is None: - self.supersampled_height = None - else: - self.supersampled_height = self.supersampling_factor * self.height self.line_spacing = GUIConstants.BODY_LINE_SPACING # We have to figure out if and where to make line breaks in the text so that it # fits in its bounding rect (plus accounting for edge padding) using its given # font. - # Measure from left baseline ("ls") - # TODO: getbbox() seems to ignore "\n" so isn't properly factored into height - # calcs and yields incorrect full_text_width. For now must specify self.height to - # render properly. Centering will be wrong. - (left, top, full_text_width, bottom) = self.font.getbbox(self.text, anchor="ls") - self.text_font_height = -1 * top - self.bbox_height = self.text_font_height + bottom + # Do initial calcs without worrying about supersampling. + self.text_lines = reflow_text_for_width( + text=self.text, + width=self.width - 2*self.edge_padding, + font_name=self.font_name, + font_size=self.font_size, + allow_text_overflow=self.allow_text_overflow, + ) - # Stores each line of text and its rendering starting x-coord - self.text_lines = [] - self.text_width = 0 - def _add_text_line(text, width): - if self.is_text_centered: - text_x = int((self.supersampled_width - width) / 2) - else: - text_x = self.supersampling_factor * self.edge_padding + # Calculate the actual font height from the "baseline" anchor ("_s") + font = Fonts.get_font(self.font_name, self.font_size) - if self.min_text_x is not None and text_x < self.supersampling_factor * self.min_text_x: - text_x = self.supersampling_factor * self.min_text_x - self.text_lines.append({"text": text, "text_x": text_x}) + # Note: from the baseline anchor, `top` is a negative number while `bottom` + # conveys the height of the pixels that rendered below the baseline, if any + # (e.g. "py" in "python"). + (left, top, right, bottom) = font.getbbox(self.text, anchor="ls") + self.text_height_above_baseline = -1 * top + self.text_height_below_baseline = bottom - if width > self.text_width: - self.text_width = width + # Initialize the text rendering relative to the baseline + self.text_y = self.text_height_above_baseline - if not self.auto_line_break or full_text_width < self.supersampled_width - (2 * self.edge_padding * self.supersampling_factor): - # The whole text fits on one line - _add_text_line(self.text, full_text_width) + # Other components, like IconTextLine will need to know how wide the actual + # rendered text will be, separate from the TextArea's defined overall `width`. + self.text_width = max(line["text_width"] for line in self.text_lines) - if self.height is None: - self.text_y = self.text_font_height - self.supersampled_height = self.bbox_height - self.height = int(self.bbox_height / self.supersampling_factor) - else: - # Vertical starting point calc is easy in this case - self.text_y = self.text_font_height + int((self.supersampled_height - self.text_font_height)/2) - - self.text_width = full_text_width + # Calculate the actual height + if len(self.text_lines) == 1: + total_text_height = self.text_height_above_baseline + if not self.height_ignores_below_baseline: + total_text_height += self.text_height_below_baseline + else: + # Multiply for the number of lines plus the spacer + total_text_height = self.text_height_above_baseline * len(self.text_lines) + self.line_spacing * (len(self.text_lines) - 1) + + if not self.height_ignores_below_baseline and re.findall(f"[gjpqy]", self.text_lines[-1]["text"]): + # Last line has at least one char that dips below baseline + total_text_height += self.text_height_below_baseline + + if self.height is None: + # Autoscale height to text lines + self.height = total_text_height else: - # Have to calc how to break text into multiple lines - def _binary_len_search(min_index, max_index, word_spacer=" "): - # Try the middle of the range - index = math.ceil((max_index + min_index) / 2) - if index == 0: - # Handle edge case where there's only one word in the last line - index = 1 - - tw, th = self.font.getsize(word_spacer.join(words[0:index])) - # print(f"tw: {tw} vs {self.supersampled_width} | {word_spacer.join(words[0:index])}") - - if tw > self.supersampled_width - (2 * self.edge_padding * self.supersampling_factor): - # Candidate line is still too long. Restrict search range down. - if min_index + 1 == index: - if index == 1: - # It's just one long, unbreakable word. There's no good - # solution here. Just accept it as is and let it render off - # the edges. - return (index, tw) - else: - # There's still room to back down the min_index in the next - # round. - index -= 1 - return _binary_len_search(min_index=min_index, max_index=index, word_spacer=word_spacer) - elif index == max_index: - # We have converged - return (index, tw) + if total_text_height > self.height: + if not self.allow_text_overflow: + pass #raise TextDoesNotFitException(f"Text cannot fit in target rect with this font/size\n\ttotal_text_height: {total_text_height} | self.height: {self.height}") else: - # Candidate line is possibly shorter than necessary. - return _binary_len_search(min_index=index, max_index=max_index, word_spacer=word_spacer) - - if Settings.get_instance().get_value(SettingsConstants.SETTING__LOCALE) not in [SettingsConstants.LOCALE__JAPANESE]: - if len(self.text.split()) == 1 and not self.allow_text_overflow: - # No whitespace chars to split on! - raise TextDoesNotFitException("Text cannot fit in target rect with this font/size") - - for line in self.text.split("\n"): - if Settings.get_instance().get_value(SettingsConstants.SETTING__LOCALE) in [SettingsConstants.LOCALE__JAPANESE]: - # Treat each character as a word that can be split - # TODO: Needs refinement; only split on kanji, not English or Katakana chars - words = line - - # No spaces between chars - word_spacer = "" - - else: - # Separate words by any whitespace (spaces, line breaks, etc) - words = line.split() - - # When joining words, separate with a space char - word_spacer = " " - - if not words: - # It's a blank line - _add_text_line("", 0) - - else: - while words: - (index, tw) = _binary_len_search(0, len(words), word_spacer=word_spacer) - - _add_text_line(word_spacer.join(words[0:index]), tw) - words = words[index:] - - # TODO: Don't render blank lines as full height - total_text_height = self.bbox_height * len(self.text_lines) + self.line_spacing * (len(self.text_lines) - 1) - if self.height is None: - # Autoscale height to text lines - self.supersampled_height = total_text_height - self.height = int(self.supersampled_height / self.supersampling_factor) - self.text_y = self.text_font_height + # Just let it render off the edge, but preserve the top portion + pass else: - self.supersampled_height = self.height * self.supersampling_factor - if total_text_height > self.height * self.supersampling_factor + 2*GUIConstants.COMPONENT_PADDING * self.supersampling_factor: - if not self.allow_text_overflow: - raise TextDoesNotFitException("Text cannot fit in target rect with this font/size") - else: - # Just let it render off the edge, but preserve the top portion - self.text_y = self.text_font_height - - else: - # Vertically center the multiline text's starting point - self.text_y = self.text_font_height + int((self.supersampled_height - total_text_height)/2) - - # Make sure the width/height that get referenced outside this obj are - # specified and restored to their normal scaling factor. - self.width = int(self.text_width / self.supersampling_factor) - self.text_font_height = int(self.text_font_height / self.supersampling_factor) + # Vertically center the text's starting point + self.text_y += int(self.height - total_text_height)/2 def render(self): # Render to a temp img scaled up by self.supersampling_factor, then resize down # with bicubic resampling. - # TODO: Store resulting super-sampled image as a member var in __post_init__ and - # just re-paste it here. - img = Image.new("RGB", (self.supersampled_width, self.supersampled_height), self.background_color) + # Add a `resample_padding` above and below when supersampling to avoid edge + # effects (resized text that's right up against the top/bottom gets slightly + # dimmer at the edge otherwise). + if self.font_size < 20 and (not self.supersampling_factor or self.supersampling_factor == 1): + self.supersampling_factor = 2 + + actual_text_height = self.height + if self.height_ignores_below_baseline: + # Even though we're ignoring the pixels below the baseline for spacing + # purposes, we have to make sure we don't crop those pixels out during the + # supersampling operations here. + actual_text_height += self.text_height_below_baseline + + resample_padding = 10 if self.supersampling_factor > 1.0 else 0 + img = Image.new( + "RGBA", + ( + self.width * self.supersampling_factor, + (actual_text_height + 2*resample_padding) * self.supersampling_factor + ), + self.background_color + ) draw = ImageDraw.Draw(img) - cur_y = self.text_y + + cur_y = (self.text_y + resample_padding) * self.supersampling_factor + supersampled_font = Fonts.get_font(self.font_name, int(self.supersampling_factor * self.font_size)) + + if self.is_text_centered: + anchor = "ms" + else: + anchor = "ls" + + # Position where we'll render each line of text + text_x = self.edge_padding for line in self.text_lines: - draw.text((line["text_x"], cur_y), line["text"], fill=self.font_color, font=self.font, anchor="ls") - cur_y += self.bbox_height + self.line_spacing + if self.is_text_centered: + # We'll render with a centered anchor so we just need the midpoint + text_x = int(self.width/2) + if text_x - int(line["text_width"]/2) < self.min_text_x: + # The left edge of the centered text will protrude too far; nudge it right + text_x = self.min_text_x + int(line["text_width"]/2) - resized = img.resize((int(self.supersampled_width / self.supersampling_factor), self.height), Image.Resampling.LANCZOS) - resized = resized.filter(ImageFilter.SHARPEN) - self.canvas.paste(resized, (self.screen_x, self.screen_y)) + draw.text((text_x * self.supersampling_factor, cur_y), line["text"], fill=self.font_color, font=supersampled_font, anchor=anchor) + + # Debugging: show the exact vertical extents of each line of text + # draw.line((0, cur_y - self.text_height_above_baseline * self.supersampling_factor, self.width * self.supersampling_factor, cur_y - self.text_height_above_baseline * self.supersampling_factor), fill="red", width=int(self.supersampling_factor)) + # draw.line((0, cur_y, self.width * self.supersampling_factor, cur_y), fill="red", width=int(self.supersampling_factor)) + + cur_y += (self.text_height_above_baseline + self.line_spacing) * self.supersampling_factor + + # Crop off the top_padding and resize the result down to onscreen size + if self.supersampling_factor > 1.0: + resized = img.resize((self.width, actual_text_height + 2*resample_padding), Image.LANCZOS) + sharpened = resized.filter(ImageFilter.SHARPEN) + + # Crop args are actually (left, top, WIDTH, HEIGHT) + img = sharpened.crop((0, resample_padding, self.width, actual_text_height + resample_padding)) + self.canvas.paste(img, (self.screen_x, self.screen_y)) @@ -533,15 +523,15 @@ class TextArea(BaseComponent): class Icon(BaseComponent): screen_x: int = 0 screen_y: int = 0 - icon_name: str = SeedSignerCustomIconConstants.BITCOIN_LOGO_TILTED + icon_name: str = SeedSignerIconConstants.BITCOIN_ALT icon_size: int = GUIConstants.ICON_FONT_SIZE icon_color: str = GUIConstants.BODY_FONT_COLOR def __post_init__(self): super().__post_init__() - if SeedSignerCustomIconConstants.MIN_VALUE <= self.icon_name and self.icon_name <= SeedSignerCustomIconConstants.MAX_VALUE: - self.icon_font = Fonts.get_font(GUIConstants.ICON_FONT_NAME__SEEDSIGNER, self.icon_size) + if SeedSignerIconConstants.MIN_VALUE <= self.icon_name and self.icon_name <= SeedSignerIconConstants.MAX_VALUE: + self.icon_font = Fonts.get_font(GUIConstants.ICON_FONT_NAME__SEEDSIGNER, self.icon_size, file_extension="otf") else: self.icon_font = Fonts.get_font(GUIConstants.ICON_FONT_NAME__FONT_AWESOME, self.icon_size) @@ -613,20 +603,21 @@ class IconTextLine(BaseComponent): canvas=self.canvas, text=self.label_text, font_size=GUIConstants.get_body_font_size() - 2, - font_color="#666", + font_color=GUIConstants.LABEL_FONT_COLOR, edge_padding=0, is_text_centered=self.is_text_centered if not self.icon_name else False, auto_line_break=False, screen_x=text_screen_x, - screen_y=self.screen_y + screen_y=self.screen_y, + allow_text_overflow=False, ) else: self.label_textarea = None value_textarea_screen_y = self.screen_y if self.label_text: - label_padding_y = GUIConstants.COMPONENT_PADDING - value_textarea_screen_y += self.label_textarea.text_font_height + label_padding_y + label_padding_y = int(GUIConstants.COMPONENT_PADDING / 2) + value_textarea_screen_y += self.label_textarea.height + label_padding_y self.value_textarea = TextArea( image_draw=self.image_draw, @@ -646,11 +637,11 @@ class IconTextLine(BaseComponent): if self.label_text: if not self.height: self.height = self.label_textarea.height + label_padding_y + self.value_textarea.height - max_textarea_width = max(self.label_textarea.width, self.value_textarea.width) + max_textarea_width = max(self.label_textarea.text_width, self.value_textarea.text_width) else: if not self.height: self.height = self.value_textarea.height - max_textarea_width = self.value_textarea.width + max_textarea_width = self.value_textarea.text_width # Now we can update the icon's y position if self.icon_name: @@ -659,17 +650,12 @@ class IconTextLine(BaseComponent): self.height = max(self.icon.height, self.height) - if self.is_text_centered: - if self.icon_name: - total_width = max_textarea_width + self.icon.width + self.icon_horizontal_spacer - self.icon.screen_x = self.screen_x + int((self.canvas_width - self.screen_x - total_width) / 2) - if self.label_text: - self.label_textarea.screen_x = self.icon.screen_x + self.icon.width + self.icon_horizontal_spacer - self.value_textarea.screen_x = self.icon.screen_x + self.icon.width + self.icon_horizontal_spacer - # else: - # if self.label_text: - # self.label_textarea.screen_x = self.screen_x + int((self.canvas_width - self.screen_x - max_textarea_width + (max_textarea_width - self.label_textarea.width))/2) - # self.value_textarea.screen_x = self.screen_x + int((self.canvas_width - self.screen_x - max_textarea_width + (max_textarea_width - self.value_textarea.width))/2) + if self.is_text_centered and self.icon_name: + total_width = max_textarea_width + self.icon.width + self.icon_horizontal_spacer + self.icon.screen_x = self.screen_x + int((self.canvas_width - self.screen_x - total_width) / 2) + if self.label_text: + self.label_textarea.screen_x = self.icon.screen_x + self.icon.width + self.icon_horizontal_spacer + self.value_textarea.screen_x = self.icon.screen_x + self.icon.width + self.icon_horizontal_spacer self.width = self.canvas_width @@ -905,7 +891,7 @@ class BtcAmount(BaseComponent): btc_icon = Icon( image_draw=draw, canvas=self.paste_image, - icon_name=SeedSignerCustomIconConstants.BITCOIN_LOGO_TILTED, + icon_name=SeedSignerIconConstants.BITCOIN_ALT, icon_color=btc_color, icon_size=self.icon_size, screen_x=0, @@ -939,7 +925,7 @@ class BtcAmount(BaseComponent): # font = smaller_digit_font (left, top, text_width, bottom) = font.getbbox(btc_text, anchor="ls") - text_height = -1 * top + text_height = -1 * top + bottom text_y = self.paste_image.height - int((self.paste_image.height - text_height)/2) draw.text( @@ -966,7 +952,7 @@ class BtcAmount(BaseComponent): if self.total_sats > 1e9: font = smaller_digit_font (left, top, text_width, bottom) = font.getbbox(sats_text, anchor="ls") - text_height = -1 * top + text_height = -1 * top + bottom text_y = self.paste_image.height - int((self.paste_image.height - text_height)/2) draw.text( xy=( @@ -993,7 +979,7 @@ class BtcAmount(BaseComponent): btc_icon = Icon( image_draw=draw, canvas=self.paste_image, - icon_name=SeedSignerCustomIconConstants.BITCOIN_LOGO_TILTED, + icon_name=SeedSignerIconConstants.BITCOIN_ALT, icon_color=btc_color, icon_size=self.icon_size, screen_x=0, @@ -1003,7 +989,7 @@ class BtcAmount(BaseComponent): cur_x = btc_icon.width + int(GUIConstants.COMPONENT_PADDING/4) (left, top, text_width, bottom) = smaller_digit_font.getbbox(btc_text, anchor="ls") - text_height = -1 * top + text_height = -1 * top + bottom text_y = self.paste_image.height - int((self.paste_image.height - text_height)/2) draw.text( @@ -1069,7 +1055,7 @@ class BtcAmount(BaseComponent): ) unit_textarea.render() - final_x = cur_x + GUIConstants.COMPONENT_PADDING + unit_textarea.width + final_x = cur_x + GUIConstants.COMPONENT_PADDING + unit_text_width self.paste_image = self.paste_image.crop((0, 0, final_x, self.paste_image.height)) self.paste_coords = ( @@ -1153,7 +1139,9 @@ class Button(BaseComponent): (left, top, self.text_width, bottom) = self.font.getbbox(self.text, anchor="ls") # print(f"left: {left} | top: {top} | right: {self.text_width} | bottom: {bottom}") - # Note: "top" is negative when measured from a "baseline" anchor + # Note: "top" is negative when measured from a "baseline" anchor. Intentionally + # ignore any chars below the baseline for consistent vertical positioning + # regardless of the Button text. self.text_height = -1 * top # TODO: Only apply screen_y at render @@ -1255,8 +1243,8 @@ class CheckedSelectionButton(Button): def __post_init__(self): self.is_text_centered = False - self.icon_name = FontAwesomeIconConstants.SOLID_CIRCLE_CHECK - self.icon_color = "#00dd00" + self.icon_name = SeedSignerIconConstants.CHECK + self.icon_color = GUIConstants.SUCCESS_COLOR super().__post_init__() if not self.is_checked: @@ -1274,10 +1262,10 @@ class CheckboxButton(Button): def __post_init__(self): self.is_text_centered = False if self.is_checked: - self.icon_name = FontAwesomeIconConstants.SQUARE_CHECK - self.icon_color = "#00dd00" + self.icon_name = SeedSignerIconConstants.CHECKBOX_SELECTED + self.icon_color = GUIConstants.SUCCESS_COLOR else: - self.icon_name = FontAwesomeIconConstants.SQUARE + self.icon_name = SeedSignerIconConstants.CHECKBOX self.icon_color = GUIConstants.BODY_FONT_COLOR super().__post_init__() @@ -1314,9 +1302,9 @@ class TopNav(BaseComponent): background_color: str = GUIConstants.BACKGROUND_COLOR icon_name: str = None icon_color: str = GUIConstants.BODY_FONT_COLOR - font_name: str = None - font_size: int = None - font_color: str = "#fcfcfc" + font_name: str = GUIConstants.get_top_nav_title_font_name() + font_size: int = GUIConstants.get_top_nav_title_font_size() + font_color: str = GUIConstants.BODY_FONT_COLOR show_back_button: bool = True show_power_button: bool = False is_selected: bool = False @@ -1336,17 +1324,17 @@ class TopNav(BaseComponent): if self.show_back_button: self.left_button = IconButton( - icon_name=SeedSignerCustomIconConstants.LARGE_CHEVRON_LEFT, + icon_name=SeedSignerIconConstants.BACK, icon_size=GUIConstants.ICON_INLINE_FONT_SIZE, screen_x=GUIConstants.EDGE_PADDING, - screen_y=GUIConstants.EDGE_PADDING, + screen_y=GUIConstants.EDGE_PADDING - 1, # Text can't perfectly vertically center relative to the button; shifting it down 1px looks better. width=GUIConstants.TOP_NAV_BUTTON_SIZE, height=GUIConstants.TOP_NAV_BUTTON_SIZE, ) if self.show_power_button: self.right_button = IconButton( - icon_name=FontAwesomeIconConstants.POWER_OFF, + icon_name=SeedSignerIconConstants.POWER, icon_size=GUIConstants.ICON_INLINE_FONT_SIZE, screen_x=self.width - GUIConstants.TOP_NAV_BUTTON_SIZE - GUIConstants.EDGE_PADDING, screen_y=GUIConstants.EDGE_PADDING, @@ -1354,39 +1342,10 @@ class TopNav(BaseComponent): height=GUIConstants.TOP_NAV_BUTTON_SIZE, ) - min_x = 0 + min_text_x = 0 if self.show_back_button: # Don't let the title intrude on the BACK button - min_x = self.left_button.screen_x + self.left_button.width + GUIConstants.COMPONENT_PADDING - - # TODO: Account for the icon's width if there is one - max_width = self.width - min_x - if self.show_power_button: - max_width -= (self.width - self.right_button.screen_x) + GUIConstants.COMPONENT_PADDING - else: - max_width -= GUIConstants.EDGE_PADDING - - # print(f"width: {self.width}") - # print(f"min_x: {min_x}") - # print(f"max_width: {max_width}") - # print(f"self.font_size: {self.font_size}") - - # Dynamically size the title font size as needed - title_font_size = self.font_size - min_title_font_size = GUIConstants.get_body_font_size() - while title_font_size >= min_title_font_size: - font = Fonts.get_font(self.font_name, title_font_size) - (left, top, text_width, bottom) = font.getbbox(self.text, anchor="ls") - # print(f"text_width: {text_width}") - if text_width > max_width: - title_font_size -= 1 - # print(f"title_font_size: {title_font_size}") - else: - # It fits! - break - - if title_font_size < min_title_font_size: - title_font_size = min_title_font_size + min_text_x = self.left_button.screen_x + self.left_button.width + GUIConstants.COMPONENT_PADDING if self.icon_name: self.title = IconTextLine( @@ -1399,18 +1358,20 @@ class TopNav(BaseComponent): value_text=self.text, is_text_centered=True, font_name=self.font_name, - font_size=title_font_size, + font_size=self.font_size, ) else: self.title = TextArea( screen_x=0, screen_y=0, - min_text_x=min_x, + min_text_x=min_text_x, + width=self.width, height=self.height, text=self.text, is_text_centered=True, font_name=self.font_name, - font_size=title_font_size, + font_size=self.font_size, + height_ignores_below_baseline=True, # Consistently vertically center text, ignoring chars that render below baseline (e.g. "pqyj") ) @@ -1438,15 +1399,6 @@ class TopNav(BaseComponent): self.right_button.is_selected = self.is_selected self.right_button.render() - # self.image_draw.text( - # (self.text_x, self.text_y), - # self.text, - # font=self.font, - # fill=self.font_color, - # stroke_width=1, - # stroke_fill=GUIConstants.BACKGROUND_COLOR, - # ) - def linear_interp(a, b, t): @@ -1484,3 +1436,134 @@ def calc_bezier_curve(p1: Tuple[int,int], p2: Tuple[int,int], p3: Tuple[int,int] points.append(q1) return points + + + +def reflow_text_for_width(text: str, + width: int, + font_name=GUIConstants.get_body_font_name(), + font_size=GUIConstants.get_body_font_size(), + allow_text_overflow: bool=False) -> list[dict]: + """ + Reflows text to fit within `width` by breaking long lines up. + + Returns a List with each reflowed line of text as its own entry. + + Note: It is up to the calling code to handle any height considerations for the + resulting lines of text. + """ + # We have to figure out if and where to make line breaks in the text so that it + # fits in its bounding rect (plus accounting for edge padding) using its given + # font. + start = time() + font = Fonts.get_font(font_name=font_name, size=font_size) + # Measure from left baseline ("ls") + (left, top, full_text_width, bottom) = font.getbbox(text, anchor="ls") + + # Stores each line of text and its rendering starting x-coord + text_lines = [] + def _add_text_line(text, text_width): + text_lines.append({"text": text, "text_width": text_width}) + + if "\n" not in text and full_text_width < width: + # The whole text fits on one line + _add_text_line(text, full_text_width) + + else: + # Have to calc how to break text into multiple lines + def _binary_len_search(min_index, max_index): + # Try the middle of the range + index = math.ceil((max_index + min_index) / 2) + if index == 0: + # Handle edge case where there's only one word in the last line + index = 1 + + # Measure rendered width from "left" anchor (anchor="l_") + (left, top, right, bottom) = font.getbbox(" ".join(words[0:index]), anchor="ls") + line_width = right - left + + if line_width >= width: + # Candidate line is still too long. Restrict search range down. + if min_index + 1 == index: + if index == 1: + # It's just one long, unbreakable word. There's no good + # solution here. Just accept it as is and let it render off + # the edges. + return (index, line_width) + else: + # There's still room to back down the min_index in the next + # round. + index -= 1 + return _binary_len_search(min_index=min_index, max_index=index) + elif index == max_index: + # We have converged + return (index, line_width) + else: + # Candidate line is possibly shorter than necessary. + return _binary_len_search(min_index=index, max_index=max_index) + + if len(text.split()) == 1 and not allow_text_overflow: + # No whitespace chars to split on! + raise TextDoesNotFitException("Text cannot fit in target rect with this font+size") + + # Now we're ready to go line-by-line into our line break binary search! + for line in text.split("\n"): + words = line.split() + if not words: + # It's a blank line + _add_text_line("", 0) + else: + while words: + (index, tw) = _binary_len_search(0, len(words)) + _add_text_line(" ".join(words[0:index]), tw) + words = words[index:] + + return text_lines + + + +def reflow_text_into_pages(text: str, + width: int, + height: int, + font_name=GUIConstants.get_body_font_name(), + font_size=GUIConstants.get_body_font_size(), + line_spacer: int = GUIConstants.BODY_LINE_SPACING, + allow_text_overflow: bool=False) -> list[str]: + """ + Invokes `reflow_text_for_width` above to convert long text into width-limited + individual text lines and then calculates how many lines will fit on a "page" and + groups the output accordingly. + + Returns a list of strings where each string is a page's worth of line-breaked text. + """ + reflowed_lines_dicts = reflow_text_for_width(text=text, + width=width, + font_name=font_name, + font_size=font_size, + allow_text_overflow=allow_text_overflow) + + lines = [] + for line_dict in reflowed_lines_dicts: + lines.append(line_dict["text"]) + print(f"""{line_dict["text_width"]:3}: {line_dict["text"]}""") + + font = Fonts.get_font(font_name=font_name, size=font_size) + # Measure the font's height above baseline and how for below it certain characters + # (e.g. lowercase "g") can render. + (left, top, right, bottom) = font.getbbox("Agjpqy", anchor="ls") + font_height_above_baseline = -1 * top + font_height_below_baseline = bottom + + # I'm sure there's a smarter way to do this... + lines_per_page = 0 + for i in range(1, height): + if height > font_height_above_baseline * i + line_spacer * (i-1) + font_height_below_baseline: + lines_per_page = i + else: + break + + pages = [] + for i in range(0, len(lines), lines_per_page): + pages.append("\n".join(lines[i:i+lines_per_page])) + + return pages diff --git a/src/seedsigner/gui/keyboard.py b/src/seedsigner/gui/keyboard.py index 9f9ab2a8..7521ae31 100644 --- a/src/seedsigner/gui/keyboard.py +++ b/src/seedsigner/gui/keyboard.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from PIL import Image, ImageDraw, ImageFont from typing import Tuple +from gettext import gettext as _ from seedsigner.gui.components import Fonts, GUIConstants from seedsigner.hardware.buttons import HardwareButtonsConstants @@ -27,18 +28,28 @@ class Keyboard: REGULAR_KEY_FONT = "regular" COMPACT_KEY_FONT = "compact" + # TRANSLATOR_NOTE: The abbreviated label for the special key on a standard keyboard. + babel_extract_food = _("del") KEY_BACKSPACE = { "code": "DEL", "letter": "del", "font": COMPACT_KEY_FONT, "size": 2, } + # TRANSLATOR_NOTE: The abbreviated label for the special key on a standard keyboard. + babel_extract_food = _("space") KEY_SPACE = { "code": "SPACE", "letter": "space", "font": COMPACT_KEY_FONT, "size": 1, } + KEY_SPACE_2 = { + "code": "SPACE", + "letter": "space", + "font": COMPACT_KEY_FONT, + "size": 2, + } KEY_SPACE_3 = { "code": "SPACE", "letter": "space", @@ -117,6 +128,7 @@ class Keyboard: rect_color = self.keyboard.deactivated_background_color font_color = "#333" # Show the letter but render as gray outline_color = self.keyboard.deactivated_background_color + if self.is_selected: # Inactive, selected just gets highlighted outline outline_color = self.keyboard.highlight_color @@ -125,11 +137,11 @@ class Keyboard: font_color = "black" else: if self.is_additional_key: - # rect_color = "#111" - rect_color = self.keyboard.background_color + rect_color = "#000" + font_color = "#999" else: rect_color = self.keyboard.background_color - font_color = "#e8e8e8" + font_color = "#e8e8e8" self.keyboard.draw.rounded_rectangle( ( @@ -151,7 +163,7 @@ class Keyboard: self.screen_x + int(self.keyboard.key_width * self.size / 2), self.screen_y + self.keyboard.key_height - int((self.keyboard.key_height - text_height)/2) ), - self.letter, + _(self.letter), fill=font_color, font=font, anchor="ms" @@ -186,6 +198,7 @@ class Keyboard: self.auto_wrap = auto_wrap self.background_color = GUIConstants.BUTTON_BACKGROUND_COLOR self.deactivated_background_color = GUIConstants.BACKGROUND_COLOR + self.additional_key_deactivated_background_color = GUIConstants.BACKGROUND_COLOR self.highlight_color = highlight_color # Does the specified layout work? @@ -201,8 +214,7 @@ class Keyboard: # Set up the rendering and state params self.active_keys = list(self.charset) - # TODO: Remove RobotoCondensed font dependency - self.additonal_key_compact_font = Fonts.get_font("RobotoCondensed-Bold.ttf", 18) + self.additonal_key_compact_font = Fonts.get_font("RobotoCondensed-Bold", 18) self.x_start = rect[0] self.y_start = rect[1] self.x_gap = 2 diff --git a/src/seedsigner/gui/renderer.py b/src/seedsigner/gui/renderer.py index 8aa78134..03b3c103 100644 --- a/src/seedsigner/gui/renderer.py +++ b/src/seedsigner/gui/renderer.py @@ -1,9 +1,11 @@ import os +from gettext import gettext as _ from PIL import Image, ImageDraw from threading import Lock from seedsigner.gui.components import Fonts, GUIConstants -from seedsigner.models import ConfigurableSingleton +from seedsigner.hardware.ST7789 import ST7789 +from seedsigner.models.singleton import ConfigurableSingleton @@ -19,8 +21,6 @@ class Renderer(ConfigurableSingleton): @classmethod def configure_instance(cls): - from seedsigner.hardware.ST7789 import ST7789 - # Instantiate the one and only Renderer instance renderer = cls.__new__(cls) cls._instance = renderer @@ -34,10 +34,12 @@ class Renderer(ConfigurableSingleton): renderer.draw = ImageDraw.Draw(renderer.canvas) - def show_image(self, image=None, alpha_overlay=None, is_background_thread: bool = False): - """ - is_background_thread: Only used by the ScreenshotRenderer. - """ + def show_image(self, image=None, alpha_overlay=None, show_direct=False): + if show_direct: + # Use the incoming image as the canvas and immediately render + self.disp.ShowImage(image, 0, 0) + return + if alpha_overlay: if image == None: image = self.canvas @@ -84,123 +86,7 @@ class Renderer(ConfigurableSingleton): self.disp.ShowImage(crop, 0, 0) - # TODO: Remove all references - def show_image_with_text(self, image, text, font=None, text_color="GREY", text_background=None): - image_copy = image.copy().convert("RGBA") - - text_overlay = Image.new("RGBA", (self.canvas_width, self.canvas_height), (255,255,255,0)) - text_overlay_draw = ImageDraw.Draw(text_overlay) - if not font: - font = Fonts.get_font(GUIConstants.get_body_font_name(), GUIConstants.get_body_font_size()) - tw, th = text_overlay_draw.textsize(text, font=font) - if text_background: - text_overlay_draw.rectangle(((240 - tw) / 2 - 3, 240 - th, (240 - tw) / 2 + tw + 3, 240), fill=text_background) - text_overlay_draw.text(((240 - tw) / 2, 240 - th - 1), text, fill=text_color, font=font) - - self.show_image(image_copy, alpha_overlay=text_overlay) - - - # TODO: Should probably move this to screens.py - def draw_modal(self, lines = [], title = "", bottom = "") -> None: - self.draw.rectangle((0, 0, self.canvas_width, self.canvas_height), outline=0, fill=0) - - if len(title) > 0: - tw, th = self.draw.textsize(title, font=Fonts.get_font("Assistant-Medium", 22)) - self.draw.text(((240 - tw) / 2, 2), title, fill=self.color, font=Fonts.get_font("Assistant-Medium", 22)) - - if len(bottom) > 0: - tw, th = self.draw.textsize(bottom, font=Fonts.get_font("Assistant-Medium", 18)) - self.draw.text(((240 - tw) / 2, 210), bottom, fill=self.color, font=Fonts.get_font("Assistant-Medium", 18)) - - if len(lines) == 1: - tw, th = self.draw.textsize(lines[0], font=Fonts.get_font("Assistant-Medium", 26)) - self.draw.text(((240 - tw) / 2, 90), lines[0], fill=self.color, font=Fonts.get_font("Assistant-Medium", 26)) - elif len(lines) == 2: - tw, th = self.draw.textsize(lines[0], font=Fonts.get_font("Assistant-Medium", 22)) - self.draw.text(((240 - tw) / 2, 90), lines[0], fill=self.color, font=Fonts.get_font("Assistant-Medium", 22)) - tw, th = self.draw.textsize(lines[1], font=Fonts.get_font("Assistant-Medium", 22)) - self.draw.text(((240 - tw) / 2, 125), lines[1], fill=self.color, font=Fonts.get_font("Assistant-Medium", 22)) - elif len(lines) == 3: - tw, th = self.draw.textsize(lines[0], font=Fonts.get_font("Assistant-Medium", 26)) - self.draw.text(((240 - tw) / 2, 55), lines[0], fill=self.color, font=Fonts.get_font("Assistant-Medium", 26)) - tw, th = self.draw.textsize(lines[1], font=Fonts.get_font("Assistant-Medium", 22)) - self.draw.text(((240 - tw) / 2, 90), lines[1], fill=self.color, font=Fonts.get_font("Assistant-Medium", 22)) - tw, th = self.draw.textsize(lines[2], font=Fonts.get_font("Assistant-Medium", 22)) - self.draw.text(((240 - tw) / 2, 125), lines[2], fill=self.color, font=Fonts.get_font("Assistant-Medium", 22)) - elif len(lines) == 4: - tw, th = self.draw.textsize(lines[0], font=Fonts.get_font("Assistant-Medium", 22)) - self.draw.text(((240 - tw) / 2, 55), lines[0], fill=self.color, font=Fonts.get_font("Assistant-Medium", 22)) - tw, th = self.draw.textsize(lines[1], font=Fonts.get_font("Assistant-Medium", 22)) - self.draw.text(((240 - tw) / 2, 90), lines[1], fill=self.color, font=Fonts.get_font("Assistant-Medium", 22)) - tw, th = self.draw.textsize(lines[2], font=Fonts.get_font("Assistant-Medium", 22)) - self.draw.text(((240 - tw) / 2, 125), lines[2], fill=self.color, font=Fonts.get_font("Assistant-Medium", 22)) - tw, th = self.draw.textsize(lines[3], font=Fonts.get_font("Assistant-Medium", 22)) - self.draw.text(((240 - tw) / 2, 160), lines[3], fill=self.color, font=Fonts.get_font("Assistant-Medium", 22)) - - self.show_image() - - return - - - # TODO: Should probably move this to templates.py - def draw_prompt_yes_no(self, lines = [], title = "", bottom = "") -> None: - self.draw_prompt_custom("", "Yes ", "No ", lines, title, bottom) - return - - - # TODO: Should probably move this to templates.py - def draw_prompt_custom(self, a_txt, b_txt, c_txt, lines = [], title = "", bottom = "") -> None: - self.draw.rectangle((0, 0, self.canvas_width, self.canvas_height), outline=0, fill=0) - - if len(title) > 0: - tw, th = self.draw.textsize(title, font=Fonts.get_font("Assistant-Medium", 22)) - self.draw.text(((240 - tw) / 2, 2), title, fill=self.color, font=Fonts.get_font("Assistant-Medium", 22)) - - if len(bottom) > 0: - tw, th = self.draw.textsize(bottom, font=Fonts.get_font("Assistant-Medium", 18)) - self.draw.text(((240 - tw) / 2, 210), bottom, fill=self.color, font=Fonts.get_font("Assistant-Medium", 18)) - - if len(lines) == 1: - tw, th = self.draw.textsize(lines[0], font=Fonts.get_font("Assistant-Medium", 26)) - self.draw.text(((240 - tw) / 2, 90), lines[0], fill=self.color, font=Fonts.get_font("Assistant-Medium", 26)) - elif len(lines) == 2: - tw, th = self.draw.textsize(lines[0], font=Fonts.get_font("Assistant-Medium", 22)) - self.draw.text(((240 - tw) / 2, 90), lines[0], fill=self.color, font=Fonts.get_font("Assistant-Medium", 22)) - tw, th = self.draw.textsize(lines[1], font=Fonts.get_font("Assistant-Medium", 22)) - self.draw.text(((240 - tw) / 2, 125), lines[1], fill=self.color, font=Fonts.get_font("Assistant-Medium", 22)) - elif len(lines) == 3: - tw, th = self.draw.textsize(lines[0], font=Fonts.get_font("Assistant-Medium", 26)) - self.draw.text(((240 - tw) / 2, 20), lines[0], fill=self.color, font=Fonts.get_font("Assistant-Medium", 26)) - tw, th = self.draw.textsize(lines[1], font=Fonts.get_font("Assistant-Medium", 22)) - self.draw.text(((240 - tw) / 2, 90), lines[1], fill=self.color, font=Fonts.get_font("Assistant-Medium", 22)) - tw, th = self.draw.textsize(lines[2], font=Fonts.get_font("Assistant-Medium", 22)) - self.draw.text(((240 - tw) / 2, 125), lines[2], fill=self.color, font=Fonts.get_font("Assistant-Medium", 22)) - elif len(lines) == 4: - tw, th = self.draw.textsize(lines[0], font=Fonts.get_font("Assistant-Medium", 22)) - self.draw.text(((240 - tw) / 2, 20), lines[0], fill=self.color, font=Fonts.get_font("Assistant-Medium", 22)) - tw, th = self.draw.textsize(lines[1], font=Fonts.get_font("Assistant-Medium", 22)) - self.draw.text(((240 - tw) / 2, 90), lines[1], fill=self.color, font=Fonts.get_font("Assistant-Medium", 22)) - tw, th = self.draw.textsize(lines[2], font=Fonts.get_font("Assistant-Medium", 22)) - self.draw.text(((240 - tw) / 2, 125), lines[2], fill=self.color, font=Fonts.get_font("Assistant-Medium", 22)) - tw, th = self.draw.textsize(lines[3], font=Fonts.get_font("Assistant-Medium", 22)) - self.draw.text(((240 - tw) / 2, 160), lines[3], fill=self.color, font=Fonts.get_font("Assistant-Medium", 22)) - - a_x_offset = 240 - Fonts.get_font("Assistant-Medium", 25).getsize(a_txt)[0] - self.draw.text((a_x_offset, 39 + 0), a_txt, fill=self.color, font=Fonts.get_font("Assistant-Medium", 25)) - - b_x_offset = 240 - Fonts.get_font("Assistant-Medium", 25).getsize(b_txt)[0] - self.draw.text((b_x_offset , 39 + 60), b_txt, fill=self.color, font=Fonts.get_font("Assistant-Medium", 25)) - - c_x_offset = 240 - Fonts.get_font("Assistant-Medium", 25).getsize(c_txt)[0] - self.draw.text((c_x_offset , 39 + 120), c_txt, fill=self.color, font=Fonts.get_font("Assistant-Medium", 25)) - - self.show_image() - - return - def display_blank_screen(self): self.draw.rectangle((0, 0, self.canvas_width, self.canvas_height), outline=0, fill=0) self.show_image() - - diff --git a/src/seedsigner/gui/screens/main_menu_screens.py b/src/seedsigner/gui/screens/main_menu_screens.py deleted file mode 100644 index 8ae9aa23..00000000 --- a/src/seedsigner/gui/screens/main_menu_screens.py +++ /dev/null @@ -1,47 +0,0 @@ - -from dataclasses import dataclass -from gettext import gettext as _ - -from seedsigner.gui.components import GUIConstants, TextArea -from seedsigner.gui.screens.screen import BaseTopNavScreen, LargeButtonScreen - - -@dataclass -class MainMenuScreen(LargeButtonScreen): - def __post_init__(self): - self.title = _("Home") - self.title_font_size = GUIConstants.get_top_nav_title_font_size() + 6 - self.show_back_button=False - self.show_power_button=True - - super().__post_init__() - - - -@dataclass -class ResetScreen(BaseTopNavScreen): - def __post_init__(self): - self.title = _("Restarting") - self.show_back_button = False - super().__post_init__() - - self.components.append(TextArea( - text=_("SeedSigner is restarting.\n\nAll in-memory data will be wiped."), - screen_y=self.top_nav.height, - height=self.canvas_height - self.top_nav.height, - )) - - - -@dataclass -class PowerOffScreen(BaseTopNavScreen): - def __post_init__(self): - self.title = _("Powering Off") - self.show_back_button = False - super().__post_init__() - - self.components.append(TextArea( - text=_("Please wait about 30 seconds before disconnecting power."), - screen_y=self.top_nav.height, - height=self.canvas_height - self.top_nav.height, - )) diff --git a/src/seedsigner/gui/screens/psbt_screens.py b/src/seedsigner/gui/screens/psbt_screens.py index a89ad9eb..3a249c69 100644 --- a/src/seedsigner/gui/screens/psbt_screens.py +++ b/src/seedsigner/gui/screens/psbt_screens.py @@ -3,12 +3,13 @@ from gettext import gettext as _ from gettext import ngettext from PIL import Image, ImageDraw, ImageFilter from typing import List +import time from seedsigner.gui.renderer import Renderer from seedsigner.models.threads import BaseThread from .screen import ButtonListScreen, WarningScreen -from ..components import (BtcAmount, Button, Icon, FontAwesomeIconConstants, IconTextLine, FormattedAddress, GUIConstants, Fonts, SeedSignerCustomIconConstants, TextArea, +from ..components import (BtcAmount, Button, Icon, FontAwesomeIconConstants, IconTextLine, FormattedAddress, GUIConstants, Fonts, SeedSignerIconConstants, TextArea, calc_bezier_curve, linear_interp) @@ -446,7 +447,7 @@ class PSBTOverviewScreen(ButtonListScreen): self.renderer.show_image() # No need to CPU limit when running in its own thread? - # time.sleep(0.02) + time.sleep(0.02) @@ -660,10 +661,10 @@ class PSBTChangeDetailsScreen(ButtonListScreen): # Adjust the vertical spacing screen_y -= GUIConstants.COMPONENT_PADDING self.components.append(IconTextLine( - icon_name=SeedSignerCustomIconConstants.FINGERPRINT, - icon_color="blue", # TRANSLATOR_NOTE: First variable is either "Multisig" or the fingerprint for single sig; Second is "Change" or "Addr" (change vs receive addr); Third is the address index number (e.g. your #4 receive addr). value_text="""{}: {} #{}""".format(_("Multisig") if self.is_multisig else self.fingerprint, _("Change") if self.is_change_derivation_path else _("Addr"), self.derivation_path_addr_index), + icon_name=SeedSignerIconConstants.FINGERPRINT, + icon_color=GUIConstants.INFO_COLOR, is_text_centered=False, screen_x=GUIConstants.EDGE_PADDING, screen_y=screen_y, @@ -671,8 +672,8 @@ class PSBTChangeDetailsScreen(ButtonListScreen): if self.is_change_addr_verified: self.components.append(IconTextLine( - icon_name=SeedSignerCustomIconConstants.CIRCLE_CHECK, - icon_color="#00dd00", + icon_name=SeedSignerIconConstants.SUCCESS, + icon_color=GUIConstants.SUCCESS_COLOR, value_text=_("Address verified!"), is_text_centered=False, screen_x=GUIConstants.EDGE_PADDING, @@ -690,8 +691,8 @@ class PSBTFinalizeScreen(ButtonListScreen): super().__post_init__() icon = Icon( - icon_name=FontAwesomeIconConstants.FILE_SIGNATURE, - icon_color=GUIConstants.LIGHT_BLUE, + icon_name=FontAwesomeIconConstants.PAPER_PLANE, + icon_color=GUIConstants.INFO_COLOR, icon_size=GUIConstants.ICON_LARGE_BUTTON_SIZE, screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING ) @@ -702,20 +703,3 @@ class PSBTFinalizeScreen(ButtonListScreen): text=_("Click to approve this transaction"), screen_y=icon.screen_y + icon.height + 2*GUIConstants.COMPONENT_PADDING )) - - - -@dataclass -class PSBTSelectCoordinatorScreen(ButtonListScreen): - def __post_init__(self): - # Customize defaults - self.title = _("Signed PSBT") - self.is_bottom_list = True - super().__post_init__() - - self.components.append(TextArea( - # TRANSLATOR_NOTE: Prompt to specify which coordinator software to use (e.g. Specter, Sparrow, etc) - text=_("Export as a QR code for:"), - is_text_centered=True, - screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING, - )) diff --git a/src/seedsigner/gui/screens/scan_screens.py b/src/seedsigner/gui/screens/scan_screens.py index edf482da..d2613004 100644 --- a/src/seedsigner/gui/screens/scan_screens.py +++ b/src/seedsigner/gui/screens/scan_screens.py @@ -2,27 +2,51 @@ import time from dataclasses import dataclass from gettext import gettext as _ -from typing import List, Tuple +from PIL import Image, ImageDraw from seedsigner.gui import renderer from seedsigner.hardware.buttons import HardwareButtonsConstants from seedsigner.hardware.camera import Camera -from seedsigner.models import DecodeQR, DecodeQRStatus +from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus from seedsigner.models.threads import BaseThread -from .screen import BaseScreen, BaseTopNavScreen, ButtonListScreen -from ..components import BaseComponent, Button, GUIConstants, Fonts, IconButton, TextArea, calc_text_centering +from .screen import BaseScreen, ButtonListScreen +from ..components import GUIConstants, Fonts, TextArea @dataclass class ScanScreen(BaseScreen): + """ + Live preview has to balance three competing threads: + * Camera capturing frames and making them available to read. + * Decoder analyzing frames for QR codes. + * Live preview display writing frames to the screen. + + All of this would ideally be rewritten as in C/C++/Rust with python bindings for + vastly improved performance. + + Until then, we have to balance the resources the Pi Zero has to work with. Thus, we + set a modest fps target for the camera: 5fps. At this pace, the decoder and the live + display can more or less keep up with the flow of frames without much wasted effort + in any of the threads. + + Note: performance tuning was targeted for the Pi Zero. + + The resolution (480x480) has not been tweaked in order to guarantee that our + decoding abilities remain as-is. It's possible that more optimizations could be made + here (e.g. higher res w/no performance impact? Lower res w/same decoding but faster + performance? etc). + + Note: This is quite a lot of important tasks for a Screen to be managing; much of + this should probably be refactored into the Controller. + """ decoder: DecodeQR = None - instructions_text: str = _("Scan a QR code") - resolution: Tuple[int,int] = (480, 480) - framerate: int = 12 - render_rect: Tuple[int,int,int,int] = None + instructions_text: str = None + resolution: tuple[int,int] = (480, 480) + framerate: int = 6 # TODO: alternate optimization for Pi Zero 2W? + render_rect: tuple[int,int,int,int] = None def __post_init__(self): @@ -30,6 +54,8 @@ class ScanScreen(BaseScreen): # Initialize the base class super().__post_init__() + self.instructions_text = _("< back | ") + self.instructions_text + self.camera = Camera.get_instance() self.camera.start_video_stream_mode(resolution=self.resolution, framerate=self.framerate, format="rgb") @@ -43,7 +69,7 @@ class ScanScreen(BaseScreen): class LivePreviewThread(BaseThread): - def __init__(self, camera: Camera, decoder: DecodeQR, renderer: renderer.Renderer, instructions_text: str, render_rect: Tuple[int,int,int,int]): + def __init__(self, camera: Camera, decoder: DecodeQR, renderer: renderer.Renderer, instructions_text: str, render_rect: tuple[int,int,int,int]): self.camera = camera self.decoder = decoder self.renderer = renderer @@ -54,9 +80,7 @@ class ScanScreen(BaseScreen): self.render_rect = (0, 0, self.renderer.canvas_width, self.renderer.canvas_height) self.render_width = self.render_rect[2] - self.render_rect[0] self.render_height = self.render_rect[3] - self.render_rect[1] - - print(f"render_width: {self.render_width}") - print(f"render_height: {self.render_height}") + self.decoder_fps = "0.0" super().__init__() @@ -65,44 +89,62 @@ class ScanScreen(BaseScreen): from timeit import default_timer as timer instructions_font = Fonts.get_font(GUIConstants.get_body_font_name(), GUIConstants.get_button_font_size()) + start_time = time.time() + num_frames = 0 + show_framerate = False # enable for debugging / testing while self.keep_running: - start = timer() frame = self.camera.read_video_stream(as_image=True) if frame is not None: - scan_text = self.instructions_text + num_frames += 1 + cur_time = time.time() + cur_fps = num_frames / (cur_time - start_time) if self.decoder and self.decoder.get_percent_complete() > 0 and self.decoder.is_psbt: scan_text = str(self.decoder.get_percent_complete()) + "% Complete" + if show_framerate: + scan_text += f" {cur_fps:0.2f} | {self.decoder_fps}" + else: + if show_framerate: + scan_text = f"{cur_fps:0.2f} | {self.decoder_fps}" + else: + scan_text = self.instructions_text with self.renderer.lock: if frame.width > self.render_width or frame.height > self.render_height: frame = frame.resize( - (self.render_width, self.render_height) + (self.render_width, self.render_height), + resample=Image.NEAREST # Use nearest neighbor for max speed ) - self.renderer.canvas.paste( - frame, - (self.render_rect[0], self.render_rect[1]) - ) + + draw = ImageDraw.Draw(frame) if scan_text: - self.renderer.draw.text( - xy=( - int(self.renderer.canvas_width/2), - self.renderer.canvas_height - GUIConstants.EDGE_PADDING - ), - text=scan_text, - fill=GUIConstants.BODY_FONT_COLOR, - font=instructions_font, - stroke_width=4, - stroke_fill=GUIConstants.BACKGROUND_COLOR, - anchor="ms" - ) + # Note: shadowed text (adding a 'stroke' outline) can + # significantly slow down the rendering. + # Temp solution: render a slight 1px shadow behind the text + # TODO: Replace the instructions_text with a disappearing + # toast/popup (see: QR Brightness UI)? + draw.text(xy=( + int(self.renderer.canvas_width/2 + 2), + self.renderer.canvas_height - GUIConstants.EDGE_PADDING + 2 + ), + text=scan_text, + fill="black", + font=instructions_font, + anchor="ms") - self.renderer.show_image() + # Render the onscreen instructions + draw.text(xy=( + int(self.renderer.canvas_width/2), + self.renderer.canvas_height - GUIConstants.EDGE_PADDING + ), + text=scan_text, + fill=GUIConstants.BODY_FONT_COLOR, + font=instructions_font, + anchor="ms") - end = timer() - # print(f"{1.0/(end - start)} fps") # Time in seconds, e.g. 5.38091952400282 + self.renderer.show_image(frame, show_direct=True) + # print(f" {cur_fps:0.2f} | {self.decoder_fps}") - time.sleep(0.05) # turn this up or down to tune performance while decoding psbt if self.camera._video_stream is None: break @@ -113,49 +155,22 @@ class ScanScreen(BaseScreen): Screen. Once interaction starts, the display updates have to be managed in _run(). The live preview is an extra-complex case. """ + num_frames = 0 + start_time = time.time() while True: frame = self.camera.read_video_stream() if frame is not None: status = self.decoder.add_image(frame) + num_frames += 1 + decoder_fps = f"{num_frames / (time.time() - start_time):0.2f}" + self.threads[0].decoder_fps = decoder_fps + if status in (DecodeQRStatus.COMPLETE, DecodeQRStatus.INVALID): self.camera.stop_video_stream_mode() break - # TODO: KEY_UP gives control to NavBar; use its back arrow to cancel if self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_RIGHT) or self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_LEFT): self.camera.stop_video_stream_mode() break - - -@dataclass -class SettingsUpdatedScreen(ButtonListScreen): - config_name: str = None - - def __post_init__(self): - # Customize defaults - self.title = _("Settings QR") - self.is_bottom_list = True - self.button_data = [_("Home")] - - super().__post_init__() - - start_y = self.top_nav.height + 20 - if self.config_name: - self.config_name_textarea = TextArea( - text=f'"{self.config_name}"', - is_text_centered=True, - auto_line_break=True, - screen_y=start_y - ) - self.components.append(self.config_name_textarea) - start_y = self.config_name_textarea.screen_y + 50 - - self.components.append(TextArea( - text=_("Settings imported successfully!"), - is_text_centered=True, - auto_line_break=True, - screen_y=start_y - )) - diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index 5e09c544..f39e90f6 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -4,17 +4,15 @@ from dataclasses import dataclass from gettext import gettext as _ from PIL import Image, ImageDraw, ImageColor from typing import Any, List, Tuple + +from seedsigner.gui.components import (GUIConstants, + BaseComponent, Button, Icon, IconButton, LargeIconButton, + SeedSignerIconConstants, TopNav, TextArea, load_image) from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay from seedsigner.gui.renderer import Renderer -from seedsigner.hardware.buttons import HardwareButtons, HardwareButtonsConstants - -from seedsigner.models.threads import BaseThread, ThreadsafeCounter -from seedsigner.models.encode_qr import EncodeQR +from seedsigner.hardware.buttons import HardwareButtonsConstants, HardwareButtons from seedsigner.models.settings import SettingsConstants - -from ..components import (FontAwesomeIconConstants, GUIConstants, BaseComponent, Button, Icon, IconButton, LargeIconButton, SeedSignerCustomIconConstants, TopNav, - TextArea, load_image) - +from seedsigner.models.threads import BaseThread, ThreadsafeCounter # Must be huge numbers to avoid conflicting with the selected_button returned by the @@ -273,6 +271,9 @@ class ButtonListScreen(BaseTopNavScreen): Button_cls = Button checked_buttons: List[int] = None + # Enables returning w/buttons rendered at the same place + scroll_y_initial_offset: int = None + def __post_init__(self): if not self.button_font_name: @@ -328,6 +329,7 @@ class ButtonListScreen(BaseTopNavScreen): right_icon_name=right_icon_name, screen_x=GUIConstants.EDGE_PADDING, screen_y=button_list_y + i * (button_height + GUIConstants.LIST_ITEM_PADDING), + scroll_y=self.scroll_y_initial_offset if self.scroll_y_initial_offset is not None else 0, width=self.canvas_width - (2 * GUIConstants.EDGE_PADDING), height=button_height, is_text_centered=self.is_button_text_centered, @@ -343,7 +345,7 @@ class ButtonListScreen(BaseTopNavScreen): if self.has_scroll_arrows: self.arrow_half_width = 10 - self.cur_scroll_y = 0 + self.cur_scroll_y = self.scroll_y_initial_offset if self.scroll_y_initial_offset is not None else 0 self.up_arrow_img = Image.new("RGBA", size=(2 * self.arrow_half_width, 8), color="black") self.up_arrow_img_y = self.top_nav.height - 12 arrow_draw = ImageDraw.Draw(self.up_arrow_img) @@ -359,10 +361,6 @@ class ButtonListScreen(BaseTopNavScreen): cur_selected_button = self.buttons[self.selected_button] cur_selected_button.is_selected = True - if self.has_scroll_arrows: - frame_scroll = self.buttons[0].screen_y - cur_selected_button.screen_y - for button in self.buttons: - button.scroll_y -= frame_scroll def _render(self): @@ -679,28 +677,112 @@ class LargeButtonScreen(BaseTopNavScreen): @dataclass class QRDisplayScreen(BaseScreen): - qr_encoder: EncodeQR = None + qr_encoder: 'EncodeQR' = None class QRDisplayThread(BaseThread): - def __init__(self, qr_encoder: EncodeQR, qr_brightness: ThreadsafeCounter, renderer: Renderer): + def __init__(self, qr_encoder: 'EncodeQR', qr_brightness: ThreadsafeCounter, renderer: Renderer, + tips_start_time: ThreadsafeCounter): super().__init__() self.qr_encoder = qr_encoder self.qr_brightness = qr_brightness self.renderer = renderer + self.tips_start_time = tips_start_time + + + def add_brightness_tips(self, image: Image.Image) -> None: + # TODO: Refactor ToastOverlay to support two lines of icon + text and use + # that instead of this more manual approach. + + # Instantiate a temp Image and ImageDraw object to draw on + rectangle_width = image.width + rectangle_height = GUIConstants.COMPONENT_PADDING * 2 + GUIConstants.get_body_font_size() * 2 + GUIConstants.BODY_LINE_SPACING + rectangle = Image.new('RGBA', (rectangle_width, rectangle_height), (0, 0, 0, 0)) + img_draw = ImageDraw.Draw(rectangle) + + overlay_opacity = 224 + + # Create a semi-transparent background for the overlay, rounded edges, w/a 1-pixel gap from the edges + img_draw.rounded_rectangle((1, 0, rectangle_width - 2, rectangle_height - 1), radius=8, fill=(0, 0, 0, overlay_opacity)) + + chevron_up_icon = Icon( + image_draw=img_draw, + canvas=rectangle, + screen_x=GUIConstants.EDGE_PADDING*2 + 1, + screen_y=GUIConstants.COMPONENT_PADDING + 4, # +4 fudge factor to account for where the chevron is drawn relative to baseline + icon_name=SeedSignerIconConstants.CHEVRON_UP, + icon_size=GUIConstants.get_body_font_size(), + ) + chevron_up_icon.render() + + chevron_down_icon = Icon( + image_draw=img_draw, + canvas=rectangle, + screen_x=chevron_up_icon.screen_x, + screen_y=chevron_up_icon.screen_y + chevron_up_icon.icon_size + GUIConstants.BODY_LINE_SPACING, + icon_name=SeedSignerIconConstants.CHEVRON_DOWN, + icon_size=chevron_up_icon.icon_size, + ) + chevron_down_icon.render() + + TextArea( + image_draw=img_draw, + canvas=rectangle, + text="Brighter", + font_size=GUIConstants.get_body_font_size(), + font_name=GUIConstants.get_button_font_name(), + background_color=(0, 0, 0, overlay_opacity), + edge_padding=0, + is_text_centered=False, + auto_line_break=False, + width=int(rectangle_width/2), + screen_x=chevron_up_icon.screen_x + GUIConstants.ICON_INLINE_FONT_SIZE, + screen_y=chevron_up_icon.screen_y - 2, # -2 to account for Icon's positioning + allow_text_overflow=False + ).render() + + TextArea( + image_draw=img_draw, + canvas=rectangle, + text="Darker", + font_size=GUIConstants.get_body_font_size(), + font_name=GUIConstants.get_button_font_name(), + background_color=(0, 0, 0, overlay_opacity), + edge_padding=0, + is_text_centered=False, + auto_line_break=False, + width=int(rectangle_width/2), + screen_x=chevron_down_icon.screen_x + GUIConstants.ICON_INLINE_FONT_SIZE, + screen_y=chevron_down_icon.screen_y - 2, # -2 to account for Icon's positioning + allow_text_overflow=False + ).render() + + # Write our temp Image onto the main image + image.paste(rectangle, (0, image.height - rectangle_height - 1), rectangle) def run(self): + from seedsigner.models.settings import Settings + settings = Settings.get_instance() + cur_brightness_setting = settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS_TIPS) + show_brightness_tips = cur_brightness_setting == SettingsConstants.OPTION__ENABLED + # Loop whether the QR is a single frame or animated; each loop might adjust # brightness setting. while self.keep_running: # convert the self.qr_brightness integer (31-255) into hex triplets hex_color = (hex(self.qr_brightness.cur_count).split('x')[1]) * 3 - image = self.qr_encoder.next_part_image(240,240, border=2, background_color=hex_color) + image = self.qr_encoder.next_part_image(240, 240, border=2, background_color=hex_color) + + # Display the brightness tips toast + duration = 10 ** 9 * 1.2 # 1.2 seconds + if show_brightness_tips and time.time_ns() - self.tips_start_time.cur_count < duration: + self.add_brightness_tips(image) + with self.renderer.lock: self.renderer.show_image(image) # Target n held frames per second before rendering next QR image - time.sleep(5/30.0) + time.sleep(5 / 30.0) def __post_init__(self): @@ -709,12 +791,15 @@ class QRDisplayScreen(BaseScreen): # Shared coordination var so the display thread can detect success settings = Settings.get_instance() - self.qr_brightness = ThreadsafeCounter(initial_value=settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS)) + self.qr_brightness = ThreadsafeCounter( + initial_value=settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS)) + self.tips_start_time = ThreadsafeCounter(initial_value=time.time_ns()) self.threads.append(QRDisplayScreen.QRDisplayThread( qr_encoder=self.qr_encoder, qr_brightness=self.qr_brightness, renderer=self.renderer, + tips_start_time=self.tips_start_time )) @@ -735,10 +820,12 @@ class QRDisplayScreen(BaseScreen): if user_input == HardwareButtonsConstants.KEY_DOWN: # Reduce QR code background brightness self.qr_brightness.set_value(max(31, self.qr_brightness.cur_count - 31)) + self.tips_start_time.set_value(time.time_ns()) elif user_input == HardwareButtonsConstants.KEY_UP: # Incrase QR code background brightness self.qr_brightness.set_value(min(self.qr_brightness.cur_count + 31, 255)) + self.tips_start_time.set_value(time.time_ns()) else: # Any other input exits the screen @@ -754,12 +841,14 @@ class QRDisplayScreen(BaseScreen): @dataclass class LargeIconStatusScreen(ButtonListScreen): title: str = None - status_icon_name: str = SeedSignerCustomIconConstants.CIRCLE_CHECK + status_icon_name: str = SeedSignerIconConstants.SUCCESS status_icon_size: int = GUIConstants.ICON_PRIMARY_SCREEN_SIZE status_color: str = GUIConstants.SUCCESS_COLOR status_headline: str = None text: str = "" # The body text of the screen + text_edge_padding: int = GUIConstants.EDGE_PADDING button_data: list = None + allow_text_overflow: bool = False def __post_init__(self): @@ -780,7 +869,7 @@ class LargeIconStatusScreen(ButtonListScreen): self.status_icon.screen_x = int((self.canvas_width - self.status_icon.width) / 2) self.components.append(self.status_icon) - next_y = self.status_icon.screen_y + self.status_icon.height + 4 + next_y = self.status_icon.screen_y + self.status_icon.height + int(GUIConstants.COMPONENT_PADDING/2) if self.status_headline: self.warning_headline_textarea = TextArea( text=self.status_headline, @@ -795,6 +884,7 @@ class LargeIconStatusScreen(ButtonListScreen): height=self.buttons[0].screen_y - next_y, text=self.text, width=self.canvas_width, + edge_padding=self.text_edge_padding, # Don't render all the way up to the far left/right edges screen_y=next_y, )) @@ -867,6 +957,7 @@ class WarningEdgesThread(BaseThread): @dataclass class WarningEdgesMixin: status_color: str = GUIConstants.WARNING_COLOR + text_edge_padding: int = 2 * GUIConstants.EDGE_PADDING def __post_init__(self): super().__post_init__() @@ -877,12 +968,17 @@ class WarningEdgesMixin: @dataclass class WarningScreen(WarningEdgesMixin, LargeIconStatusScreen): - status_icon_name: str = SeedSignerCustomIconConstants.CIRCLE_EXCLAMATION + title: str = None + status_icon_name: str = SeedSignerIconConstants.WARNING status_color: str = "yellow" + status_headline: str = "Privacy Leak!" # The colored text under the alert icon def __post_init__(self): if not self.title: self.title = _("Caution") + + if not self.status_headline: + self.status_headline = _("Privacy Leak!") if not self.button_data: self.button_data = [_("I Understand")] @@ -893,6 +989,7 @@ class WarningScreen(WarningEdgesMixin, LargeIconStatusScreen): @dataclass class DireWarningScreen(WarningScreen): + status_headline: str = "Classified Info!" # The colored text under the alert icon status_color: str = GUIConstants.DIRE_WARNING_COLOR def __post_init__(self): @@ -903,6 +1000,51 @@ class DireWarningScreen(WarningScreen): +@dataclass +class ResetScreen(BaseTopNavScreen): + def __post_init__(self): + self.title = _("Restarting") + self.show_back_button = False + super().__post_init__() + + self.components.append(TextArea( + text=_("SeedSigner is restarting.\n\nAll in-memory data will be wiped."), + screen_y=self.top_nav.height, + height=self.canvas_height - self.top_nav.height, + )) + + + +@dataclass +class PowerOffScreen(BaseTopNavScreen): + def __post_init__(self): + self.title = _("Powering Off") + self.show_back_button = False + super().__post_init__() + + self.components.append(TextArea( + text=_("Please wait about 30 seconds before disconnecting power."), + screen_y=self.top_nav.height, + height=self.canvas_height - self.top_nav.height, + )) + + + +@dataclass +class PowerOffNotRequiredScreen(BaseTopNavScreen): + def __post_init__(self): + self.title = "Just Unplug It" + self.show_back_button = True + super().__post_init__() + + self.components.append(TextArea( + text="It is safe to disconnect power at any time.", + screen_y=self.top_nav.height, + height=self.canvas_height - self.top_nav.height, + )) + + + @dataclass class KeyboardScreen(BaseTopNavScreen): """ @@ -953,7 +1095,7 @@ class KeyboardScreen(BaseTopNavScreen): # Render the right button panel (only has a Key3 "Save" button) self.save_button = IconButton( - icon_name=FontAwesomeIconConstants.SOLID_CIRCLE_CHECK, + icon_name=SeedSignerIconConstants.CHECK, icon_color=GUIConstants.SUCCESS_COLOR, width=right_panel_buttons_width, screen_x=hw_button_x, @@ -1034,14 +1176,17 @@ class KeyboardScreen(BaseTopNavScreen): elif self.show_save_button and input == HardwareButtonsConstants.KEY3: # Save! + if len(self.user_input) == 0: + # Don't try to submit zero input + continue + # First show the save button reacting to the click self.save_button.is_selected = True self.save_button.render() self.renderer.show_image() # Then return the input to the View - if len(self.user_input) > 0: - return self.user_input.strip() + return self.user_input.strip() # Process normal input if input in [HardwareButtonsConstants.KEY_UP, HardwareButtonsConstants.KEY_DOWN] and self.top_nav.is_selected: @@ -1086,8 +1231,8 @@ class KeyboardScreen(BaseTopNavScreen): if self.update_title(): TextArea( text=self.title, - font_name=GUIConstants.TOP_NAV_TITLE_FONT_NAME, - font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE, + font_name=GUIConstants.get_top_nav_title_font_name(), + font_size=GUIConstants.get_top_nav_title_font_size(), height=self.top_nav.height, ).render() self.top_nav.render_buttons() @@ -1112,3 +1257,12 @@ class KeyboardScreen(BaseTopNavScreen): self.title = _("Roll {}".format(self.cursor_position + 1)) """ return False + + + +@dataclass +class MainMenuScreen(LargeButtonScreen): + # Override LargeButtonScreen defaults + title_font_size: int = 26 + show_back_button: bool = False + show_power_button: bool = True diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index cafcfe72..a6f07825 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -10,11 +10,9 @@ from seedsigner.gui.renderer import Renderer from seedsigner.helpers.qr import QR from seedsigner.models.threads import BaseThread, ThreadsafeCounter -from seedsigner.models.seed import Seed - from .screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, KeyboardScreen, WarningEdgesMixin -from ..components import (Button, FontAwesomeIconConstants, Fonts, FormattedAddress, Icon, IconButton, - IconTextLine, SeedSignerCustomIconConstants, TextArea, GUIConstants) +from ..components import (Button, FontAwesomeIconConstants, Fonts, FormattedAddress, IconButton, + IconTextLine, SeedSignerIconConstants, TextArea, GUIConstants, reflow_text_into_pages) from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay from seedsigner.hardware.buttons import HardwareButtons, HardwareButtonsConstants @@ -377,7 +375,8 @@ class SeedMnemonicEntryScreen(BaseTopNavScreen): self.keyboard.render_keys() - elif input in HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN: + elif input in HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN \ + or input in (Keyboard.ENTER_TOP, Keyboard.ENTER_BOTTOM): if ret_val in self.possible_alphabet: # Live joystick movement; haven't locked this new letter in yet. # Replace the last letter w/the currently selected one. But don't @@ -408,18 +407,17 @@ class SeedMnemonicEntryScreen(BaseTopNavScreen): @dataclass class SeedFinalizeScreen(ButtonListScreen): fingerprint: str = None - title: str = _("Finalize Seed") is_bottom_list: bool = True button_data: list = None def __post_init__(self): self.show_back_button = False - + self.title = _("Finalize Seed") super().__post_init__() self.fingerprint_icontl = IconTextLine( - icon_name=SeedSignerCustomIconConstants.FINGERPRINT, - icon_color="blue", + icon_name=SeedSignerIconConstants.FINGERPRINT, + icon_color=GUIConstants.INFO_COLOR, icon_size=GUIConstants.ICON_FONT_SIZE + 12, label_text=_("fingerprint"), value_text=self.fingerprint, @@ -438,8 +436,8 @@ class SeedOptionsScreen(ButtonListScreen): has_passphrase: bool = False def __post_init__(self): - self.top_nav_icon_name = SeedSignerCustomIconConstants.FINGERPRINT - self.top_nav_icon_color = "blue" + self.top_nav_icon_name = SeedSignerIconConstants.FINGERPRINT + self.top_nav_icon_color = GUIConstants.INFO_COLOR self.title = self.fingerprint self.is_button_text_centered = False self.is_bottom_list = True @@ -519,7 +517,7 @@ class SeedWordsScreen(WarningEdgesMixin, ButtonListScreen): for index, word in enumerate(self.words): draw.rounded_rectangle( (number_box_x, number_box_y, number_box_x + number_box_width, number_box_y + number_box_height), - fill="#202020", + fill=GUIConstants.BUTTON_BACKGROUND_COLOR, radius=5 * supersampling_factor ) baseline_y = number_box_y + number_box_height - int((number_box_height - number_height)/2) @@ -527,7 +525,7 @@ class SeedWordsScreen(WarningEdgesMixin, ButtonListScreen): (number_box_x + int(number_box_width/2), baseline_y), font=number_font, text=str(self.page_index * words_per_page + index + 1), - fill="#0084ff", + fill=GUIConstants.INFO_COLOR, anchor="ms" # Middle (centered), baSeline ) @@ -549,6 +547,22 @@ class SeedWordsScreen(WarningEdgesMixin, ButtonListScreen): +@dataclass +class SeedBIP85SelectChildIndexScreen(KeyboardScreen): + def __post_init__(self): + self.title = _("BIP-85 Index") + self.user_input = "" + + # Specify the keys in the keyboard + self.rows = 3 + self.cols = 5 + self.keys_charset = "0123456789" + self.show_save_button = True + + super().__post_init__() + + + @dataclass class SeedWordsBackupTestPromptScreen(ButtonListScreen): def __post_init__(self): @@ -584,25 +598,25 @@ class SeedExportXpubCustomDerivationScreen(KeyboardScreen): @dataclass class SeedExportXpubDetailsScreen(WarningEdgesMixin, ButtonListScreen): # Customize defaults - title: str = _("Xpub Details") is_bottom_list: bool = True fingerprint: str = None has_passphrase: bool = False derivation_path: str = "m/84'/0'/0'" xpub: str = "zpub6r..." - button_data=[_("Export Xpub")] def __post_init__(self): # Programmatically set up other args self.button_data = [_("Export Xpub")] + self.title = _("Xpub Details") # Initialize the base class super().__post_init__() # Set up the fingerprint and passphrase displays self.fingerprint_line = IconTextLine( - icon_name=SeedSignerCustomIconConstants.FINGERPRINT, - icon_color="blue", + icon_name=SeedSignerIconConstants.FINGERPRINT, + icon_color=GUIConstants.INFO_COLOR, + # TRANSLATOR_NOTE: Short for "BIP32 Master Fingerprint" label_text=_("Fingerprint"), value_text=self.fingerprint, screen_x=GUIConstants.COMPONENT_PADDING, @@ -611,7 +625,8 @@ class SeedExportXpubDetailsScreen(WarningEdgesMixin, ButtonListScreen): self.components.append(self.fingerprint_line) self.derivation_line = IconTextLine( - icon_name=SeedSignerCustomIconConstants.PATH, + icon_name=SeedSignerIconConstants.DERIVATION, + icon_color=GUIConstants.INFO_COLOR, # TRANSLATOR_NOTE: Short for "Derivation Path" label_text=_("Derivation"), value_text=self.derivation_path, @@ -622,6 +637,8 @@ class SeedExportXpubDetailsScreen(WarningEdgesMixin, ButtonListScreen): self.xpub_line = IconTextLine( icon_name=FontAwesomeIconConstants.X, + icon_color=GUIConstants.INFO_COLOR, + # TRANSLATOR_NOTE: Short for "BIP32 Extended Public Key" label_text=_("Xpub"), value_text=f"{self.xpub[:18]}...", font_name=GUIConstants.FIXED_WIDTH_FONT_NAME, @@ -635,22 +652,29 @@ class SeedExportXpubDetailsScreen(WarningEdgesMixin, ButtonListScreen): @dataclass class SeedAddPassphraseScreen(BaseTopNavScreen): - title: str = _("Add Passphrase") passphrase: str = "" - KEYBOARD__LOWERCASE_BUTTON_TEXT = "abc" KEYBOARD__UPPERCASE_BUTTON_TEXT = "ABC" KEYBOARD__DIGITS_BUTTON_TEXT = "123" - KEYBOARD__SYMBOLS_BUTTON_TEXT = "!@#" + KEYBOARD__SYMBOLS_1_BUTTON_TEXT = "!@#" + KEYBOARD__SYMBOLS_2_BUTTON_TEXT = "*[]" def __post_init__(self): + self.title = _("BIP-39 Passphrase") super().__post_init__() keys_lower = "abcdefghijklmnopqrstuvwxyz" keys_upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" keys_number = "0123456789" - keys_symbol = "!\"#$%&'()*+,=./;:<>?@[]|-_`~" + + # Present the most common/puncutation-related symbols & the most human-friendly + # symbols first (limited to 18 chars). + keys_symbol_1 = """!@#$%&();:,.-+='"?""" + + # Isolate the more math-oriented or just uncommon symbols + keys_symbol_2 = """^*[]{}_\\|<>/`~""" + # Set up the keyboard params self.right_panel_buttons_width = 56 @@ -721,11 +745,11 @@ class SeedAddPassphraseScreen(BaseTopNavScreen): render_now=False ) - self.keyboard_symbols = Keyboard( + self.keyboard_symbols_1 = Keyboard( draw=self.renderer.draw, - charset=keys_symbol, + charset=keys_symbol_1, rows=4, - cols=max_cols, + cols=6, rect=( GUIConstants.COMPONENT_PADDING, keyboard_start_y, @@ -733,7 +757,28 @@ class SeedAddPassphraseScreen(BaseTopNavScreen): self.canvas_height - GUIConstants.EDGE_PADDING ), additional_keys=[ - Keyboard.KEY_SPACE_4, + Keyboard.KEY_SPACE_2, + Keyboard.KEY_CURSOR_LEFT, + Keyboard.KEY_CURSOR_RIGHT, + Keyboard.KEY_BACKSPACE + ], + auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT], + render_now=False + ) + + self.keyboard_symbols_2 = Keyboard( + draw=self.renderer.draw, + charset=keys_symbol_2, + rows=4, + cols=6, + rect=( + GUIConstants.COMPONENT_PADDING, + keyboard_start_y, + self.canvas_width - GUIConstants.COMPONENT_PADDING - self.right_panel_buttons_width, + self.canvas_height - GUIConstants.EDGE_PADDING + ), + additional_keys=[ + Keyboard.KEY_SPACE_2, Keyboard.KEY_CURSOR_LEFT, Keyboard.KEY_CURSOR_RIGHT, Keyboard.KEY_BACKSPACE @@ -782,7 +827,7 @@ class SeedAddPassphraseScreen(BaseTopNavScreen): ) self.hw_button3 = IconButton( - icon_name=FontAwesomeIconConstants.SOLID_CIRCLE_CHECK, + icon_name=SeedSignerIconConstants.CHECK, icon_color=GUIConstants.SUCCESS_COLOR, width=self.right_panel_buttons_width, screen_x=hw_button_x, @@ -826,9 +871,7 @@ class SeedAddPassphraseScreen(BaseTopNavScreen): self.hw_button3.is_selected = True self.hw_button3.render() self.renderer.show_image() - - if len(self.passphrase) > 0: - return self.passphrase.strip() + return self.passphrase elif input == HardwareButtonsConstants.KEY_PRESS and self.top_nav.is_selected: # Back button clicked @@ -843,8 +886,10 @@ class SeedAddPassphraseScreen(BaseTopNavScreen): # Return to the same button2 keyboard, if applicable if cur_keyboard == self.keyboard_digits: cur_button2_text = self.KEYBOARD__DIGITS_BUTTON_TEXT - elif cur_keyboard == self.keyboard_symbols: - cur_button2_text = self.KEYBOARD__SYMBOLS_BUTTON_TEXT + elif cur_keyboard == self.keyboard_symbols_1: + cur_button2_text = self.KEYBOARD__SYMBOLS_1_BUTTON_TEXT + elif cur_keyboard == self.keyboard_symbols_2: + cur_button2_text = self.KEYBOARD__SYMBOLS_2_BUTTON_TEXT if cur_button1_text == self.KEYBOARD__LOWERCASE_BUTTON_TEXT: self.keyboard_abc.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"]) @@ -881,10 +926,15 @@ class SeedAddPassphraseScreen(BaseTopNavScreen): self.keyboard_digits.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"]) cur_keyboard = self.keyboard_digits cur_keyboard.render_keys() - cur_button2_text = self.KEYBOARD__SYMBOLS_BUTTON_TEXT - else: - self.keyboard_symbols.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"]) - cur_keyboard = self.keyboard_symbols + cur_button2_text = self.KEYBOARD__SYMBOLS_1_BUTTON_TEXT + elif cur_button2_text == self.KEYBOARD__SYMBOLS_1_BUTTON_TEXT: + self.keyboard_symbols_1.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"]) + cur_keyboard = self.keyboard_symbols_1 + cur_keyboard.render_keys() + cur_button2_text = self.KEYBOARD__SYMBOLS_2_BUTTON_TEXT + elif cur_button2_text == self.KEYBOARD__SYMBOLS_2_BUTTON_TEXT: + self.keyboard_symbols_2.set_selected_key_indices(x=cur_keyboard.selected_key["x"], y=cur_keyboard.selected_key["y"]) + cur_keyboard = self.keyboard_symbols_2 cur_keyboard.render_keys() cur_button2_text = self.KEYBOARD__DIGITS_BUTTON_TEXT cur_keyboard.render_keys() @@ -992,46 +1042,21 @@ class SeedReviewPassphraseScreen(ButtonListScreen): super().__post_init__() - fingerprint_change_label = TextArea( + self.components.append(IconTextLine( + icon_name=SeedSignerIconConstants.FINGERPRINT, + icon_color=GUIConstants.INFO_COLOR, # TRANSLATOR_NOTE: Describes the effect of applying a BIP-39 passphrase; it changes the seed's fingerprint - text=_("changes seed:"), - font_size=GUIConstants.LABEL_FONT_SIZE, - font_color=GUIConstants.LABEL_FONT_COLOR, - is_text_centered=False, - screen_x=int(1.25*GUIConstants.EDGE_PADDING), - screen_y=self.buttons[0].screen_y - GUIConstants.COMPONENT_PADDING - int(GUIConstants.get_body_font_size()*2.25) - ) - self.components.append(fingerprint_change_label) - - screen_y = fingerprint_change_label.screen_y + fingerprint_change_label.height + int(GUIConstants.COMPONENT_PADDING/2) - self.components.append(TextArea( - text=self.fingerprint_without, - font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, - font_size=GUIConstants.get_body_font_size() + 4, - font_color=GUIConstants.LABEL_FONT_COLOR, - is_text_centered=False, - screen_x=int(1.25*GUIConstants.EDGE_PADDING), - screen_y=screen_y, - )) - self.components.append(Icon( - icon_name=FontAwesomeIconConstants.CARET_RIGHT, - icon_size=GUIConstants.get_body_font_size() + 4, - icon_color=GUIConstants.LIGHT_BLUE, - screen_x=self.components[-1].screen_x + self.components[-1].width + 2*GUIConstants.COMPONENT_PADDING, - screen_y=screen_y, - )) - self.components.append(TextArea( - text=self.fingerprint_with, - font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, - font_size=GUIConstants.get_body_font_size() + 4, - is_text_centered=False, - screen_x=self.components[-1].screen_x + self.components[-1].width, - screen_y=screen_y, + label_text=_("changes fingerprint"), + value_text=f"{self.fingerprint_without} >> {self.fingerprint_with}", + is_text_centered=True, + screen_y = self.buttons[0].screen_y - GUIConstants.COMPONENT_PADDING - int(GUIConstants.get_body_font_size()*2.5) )) - available_height = fingerprint_change_label.screen_y - self.top_nav.height - max_font_size = 28 - min_font_size = 16 + if self.passphrase != self.passphrase.strip() or " " in self.passphrase: + self.passphrase = self.passphrase.replace(" ", "\u2589") + available_height = self.components[-1].screen_y - self.top_nav.height + GUIConstants.COMPONENT_PADDING + max_font_size = GUIConstants.get_top_nav_title_font_size() + 8 + min_font_size = GUIConstants.get_top_nav_title_font_size() - 4 font_size = max_font_size max_lines = 3 passphrase = [self.passphrase] @@ -1066,6 +1091,7 @@ class SeedReviewPassphraseScreen(ButtonListScreen): font_color="orange", is_text_centered=True, screen_y=screen_y, + allow_text_overflow=True )) screen_y += char_height + 2 @@ -1345,7 +1371,7 @@ class AddressVerificationSigTypeScreen(ButtonListScreen): @dataclass -class SeedSingleSigAddressVerificationSelectSeedScreen(ButtonListScreen): +class SeedSelectSeedScreen(ButtonListScreen): text: str = "" def __post_init__(self): @@ -1500,3 +1526,76 @@ class MultisigWalletDescriptorScreen(ButtonListScreen): is_text_centered=True, auto_line_break=True, )) + + + +@dataclass +class SeedSignMessageConfirmMessageScreen(ButtonListScreen): + page_num: int = None + + def __post_init__(self): + from seedsigner.controller import Controller + renderer = Renderer.get_instance() + start_y = GUIConstants.TOP_NAV_HEIGHT + GUIConstants.COMPONENT_PADDING + end_y = renderer.canvas_height - GUIConstants.EDGE_PADDING - GUIConstants.BUTTON_HEIGHT - GUIConstants.COMPONENT_PADDING + message_height = end_y - start_y + + self.sign_message_data = Controller.get_instance().sign_message_data + if "paged_message" not in self.sign_message_data: + paged = reflow_text_into_pages( + text=self.sign_message_data["message"], + width=renderer.canvas_width - 2*GUIConstants.EDGE_PADDING, + height=message_height, + ) + self.sign_message_data["paged_message"] = paged + + if self.page_num >= len(self.sign_message_data["paged_message"]): + raise Exception("Bug in paged_message calculation") + + if len(self.sign_message_data["paged_message"]) == 1: + self.title = _("Review Message") + else: + self.title = f"""Message (pt {self.page_num + 1}/{len(self.sign_message_data["paged_message"])})""" + self.is_bottom_list = True + self.is_button_text_centered = True + self.button_data = [_("Next")] + super().__post_init__() + + message_display = TextArea( + text=self.sign_message_data["paged_message"][self.page_num], + is_text_centered=False, + allow_text_overflow=True, + screen_y=start_y, + ) + self.components.append(message_display) + + + +@dataclass +class SeedSignMessageConfirmAddressScreen(ButtonListScreen): + derivation_path: str = None + address: str = None + + def __post_init__(self): + self.title = _("Confirm Address") + self.is_bottom_list = True + self.is_button_text_centered = True + self.button_data = [_("Sign Message")] + super().__post_init__() + + derivation_path_display = IconTextLine( + icon_name=SeedSignerIconConstants.DERIVATION, + icon_color=GUIConstants.INFO_COLOR, + label_text=_("derivation path"), + value_text=self.derivation_path, + is_text_centered=True, + screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING, + ) + self.components.append(derivation_path_display) + + address_display = FormattedAddress( + address=self.address, + max_lines=3, + screen_y=derivation_path_display.screen_y + derivation_path_display.height + 2*GUIConstants.COMPONENT_PADDING, + ) + self.components.append(address_display) diff --git a/src/seedsigner/gui/screens/settings_screens.py b/src/seedsigner/gui/screens/settings_screens.py index e9a661dd..a7e3fa23 100644 --- a/src/seedsigner/gui/screens/settings_screens.py +++ b/src/seedsigner/gui/screens/settings_screens.py @@ -297,9 +297,43 @@ class DonateScreen(BaseTopNavScreen): self.components.append(TextArea( text="seedsigner.com", - font_name=GUIConstants.BODY_FONT_NAME["default"], + font_name=GUIConstants.get_body_font_name(), font_size=28, font_color=GUIConstants.ACCENT_COLOR, supersampling_factor=1, screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING )) + + + +@dataclass +class SettingsQRConfirmationScreen(ButtonListScreen): + config_name: str = None + title: str = "Settings QR" + status_message: str = "Settings updated..." + is_bottom_list: bool = True + + def __post_init__(self): + self.title = _("Settings QR") + # Customize defaults + self.button_data = [_("Home")] + self.show_back_button = False + super().__post_init__() + + start_y = self.top_nav.height + 20 + if self.config_name: + self.config_name_textarea = TextArea( + text=f'"{self.config_name}"', + is_text_centered=True, + auto_line_break=True, + screen_y=start_y + ) + self.components.append(self.config_name_textarea) + start_y = self.config_name_textarea.screen_y + 50 + + self.components.append(TextArea( + text=self.status_message, + is_text_centered=True, + auto_line_break=True, + screen_y=start_y + )) diff --git a/src/seedsigner/gui/screens/tools_screens.py b/src/seedsigner/gui/screens/tools_screens.py index 26f06256..f71c117a 100644 --- a/src/seedsigner/gui/screens/tools_screens.py +++ b/src/seedsigner/gui/screens/tools_screens.py @@ -1,10 +1,11 @@ +import time + from dataclasses import dataclass from gettext import gettext as _ from typing import Any from PIL.Image import Image - from seedsigner.hardware.camera import Camera -from seedsigner.gui.components import FontAwesomeIconConstants, Fonts, GUIConstants, IconTextLine, SeedSignerCustomIconConstants, TextArea +from seedsigner.gui.components import FontAwesomeIconConstants, Fonts, GUIConstants, IconTextLine, SeedSignerIconConstants, TextArea from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, BaseScreen, ButtonListScreen, KeyboardScreen from seedsigner.hardware.buttons import HardwareButtonsConstants @@ -18,7 +19,7 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen): super().__post_init__() self.camera = Camera.get_instance() - self.camera.start_video_stream_mode(resolution=(240, 240), framerate=24, format="rgb") + self.camera.start_video_stream_mode(resolution=(self.canvas_width, self.canvas_height), framerate=24, format="rgb") def _run(self): @@ -28,28 +29,6 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen): instructions_font = Fonts.get_font(GUIConstants.get_body_font_name(), GUIConstants.get_button_font_size()) while True: - frame = self.camera.read_video_stream(as_image=True) - if frame is not None: - self.renderer.canvas.paste(frame) - - self.renderer.draw.text( - xy=( - int(self.renderer.canvas_width/2), - self.renderer.canvas_height - GUIConstants.EDGE_PADDING - ), - # TRANSLATOR_NOTE: Instructions when taking a picture; "back" should point left. - text=_("< back | click joystick"), - fill=GUIConstants.BODY_FONT_COLOR, - font=instructions_font, - stroke_width=4, - stroke_fill=GUIConstants.BACKGROUND_COLOR, - anchor="ms" - ) - self.renderer.show_image() - - if len(preview_images) < max_entropy_frames: - preview_images.append(frame) - if self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_LEFT): # Have to manually update last input time since we're not in a wait_for loop self.hw_inputs.update_last_input_time() @@ -57,7 +36,15 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen): self.camera.stop_video_stream_mode() return RET_CODE__BACK_BUTTON - elif self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_PRESS): + frame = self.camera.read_video_stream(as_image=True) + + if frame is None: + # Camera probably isn't ready yet + time.sleep(0.01) + continue + + # Check for joystick click to take final entropy image + if self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_PRESS): # Have to manually update last input time since we're not in a wait_for loop self.hw_inputs.update_last_input_time() self.camera.stop_video_stream_mode() @@ -80,6 +67,29 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen): return preview_images + # If we're still here, it's just another preview frame loop + self.renderer.canvas.paste(frame) + + self.renderer.draw.text( + xy=( + int(self.renderer.canvas_width/2), + self.renderer.canvas_height - GUIConstants.EDGE_PADDING + ), + text="< back | click joystick", + fill=GUIConstants.BODY_FONT_COLOR, + font=instructions_font, + stroke_width=4, + stroke_fill=GUIConstants.BACKGROUND_COLOR, + anchor="ms" + ) + self.renderer.show_image() + + if len(preview_images) == max_entropy_frames: + # Keep a moving window of the last n preview frames; pop the oldest + # before we add the currest frame. + preview_images.pop(0) + preview_images.append(frame) + @dataclass @@ -147,7 +157,7 @@ class ToolsDiceEntropyEntryScreen(KeyboardScreen): def update_title(self) -> bool: - self.title = f"Dice Roll {self.cursor_position + 1}/{self.return_after_n_chars}" + self.title = _("Dice Roll {}/{}").format(self.cursor_position + 1, self.return_after_n_chars) return True @@ -158,14 +168,16 @@ class ToolsCalcFinalWordFinalizePromptScreen(ButtonListScreen): num_entropy_bits: int = None def __post_init__(self): - self.title = "Build Final Word" + # TRANSLATOR_NOTE: Build the last word in a 12 or 24 word BIP-39 mnemonic seed phrase. + self.title = _("Build Final Word") self.is_bottom_list = True self.is_button_text_centered = True super().__post_init__() self.components.append(TextArea( - text=f"The {self.mnemonic_length}th word is built from {self.num_entropy_bits} more entropy bits plus auto-calculated checksum.", - screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING, + # TRANSLATOR_NOTE: Number of BIP-39 seed words, and the entropy -- in bits, contained within. + text=_("The {}th word is built from {} more entropy bits plus auto-calculated checksum.").format(self.mnemonic_length, self.num_entropy_bits), + screen_y=self.top_nav.height + int(GUIConstants.COMPONENT_PADDING/2), )) @@ -174,29 +186,33 @@ class ToolsCalcFinalWordFinalizePromptScreen(ButtonListScreen): class ToolsCoinFlipEntryScreen(KeyboardScreen): def __post_init__(self): # Override values set by the parent class - self.title = f"Coin Flip 1/{self.return_after_n_chars}" + # TRANSLATOR_NOTE: current coin-flip number vs total flips (e.g. flip 3 of 4) + self.title = _("Coin Flip {}/{}").format(1, self.return_after_n_chars) # Specify the keys in the keyboard self.rows = 1 self.cols = 4 - self.key_height = GUIConstants.TOP_NAV_TITLE_FONT_SIZE + 2 + 2*GUIConstants.EDGE_PADDING + self.key_height = GUIConstants.get_top_nav_title_font_size() + 2 + 2*GUIConstants.EDGE_PADDING self.keys_charset = "10" # Now initialize the parent class super().__post_init__() self.components.append(TextArea( - text="Heads = 1", + # TRANSLATOR_NOTE: How we call the "front" side result during a coin toss. + text=_("Heads = 1"), screen_y = self.keyboard.rect[3] + 4*GUIConstants.COMPONENT_PADDING, )) self.components.append(TextArea( - text="Tails = 0", + # TRANSLATOR_NOTE: How we call the "back" side result during a coin toss. + text=_("Tails = 0"), screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING, )) def update_title(self) -> bool: - self.title = f"Coin Flip {self.cursor_position + 1}/{self.return_after_n_chars}" + # l10n_note already done. + self.title = _("Coin Flip {}/{}").format(self.cursor_position + 1, self.return_after_n_chars) return True @@ -213,7 +229,7 @@ class ToolsCalcFinalWordScreen(ButtonListScreen): super().__post_init__() # First what's the total bit display width and where do the checksum bits start? - bit_font_size = GUIConstants.BUTTON_FONT_SIZE + 2 + bit_font_size = GUIConstants.get_button_font_size() + 2 font = Fonts.get_font(GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, bit_font_size) (left, top, bit_display_width, bit_font_height) = font.getbbox("0" * 11, anchor="lt") (left, top, checksum_x, bottom) = font.getbbox("0" * (11 - len(self.checksum_bits)), anchor="lt") @@ -238,25 +254,29 @@ class ToolsCalcFinalWordScreen(ButtonListScreen): discard_selected_bits = "_" * (len(self.checksum_bits)) self.components.append(TextArea( - text=f"""Your input: \"{selection_text}\"""", - screen_y=self.top_nav.height, + # TRANSLATOR_NOTE: A labeled presentation of a user-supplied value + text=_('Your input: "{}"').format(selection_text), + screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING - 2, # Nudge to last line doesn't get too close to "Next" button + height_ignores_below_baseline=True, # Keep the next line (bits display) snugged up, regardless of text rendering below the baseline )) # ...and that entropy's associated 11 bits - screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING - self.components.append(TextArea( + screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING + first_bits_line = TextArea( text=keeper_selected_bits, font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, font_size=bit_font_size, edge_padding=0, screen_x=bit_display_x, screen_y=screen_y, - height=bit_font_height, is_text_centered=False, - )) + ) + self.components.append(first_bits_line) # Render the least significant bits that will be replaced by the checksum in a # de-emphasized font color. + if "_" in discard_selected_bits: + screen_y += int(first_bits_line.height/2) # center the underscores vertically like hypens self.components.append(TextArea( text=discard_selected_bits, font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, @@ -265,15 +285,15 @@ class ToolsCalcFinalWordScreen(ButtonListScreen): edge_padding=0, screen_x=checksum_x, screen_y=screen_y, - height=bit_font_height, is_text_centered=False, )) # Show the checksum.. self.components.append(TextArea( - text="Checksum", + # TRANSLATOR_NOTE: A function of "x" to be used for detecting errors in "x" + text=_("Checksum"), edge_padding=0, - screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING, + screen_y=first_bits_line.screen_y + first_bits_line.height + 2*GUIConstants.COMPONENT_PADDING, )) # ...and its actual bits. Prepend spacers to keep vertical alignment @@ -289,8 +309,7 @@ class ToolsCalcFinalWordScreen(ButtonListScreen): font_size=bit_font_size, edge_padding=0, screen_x=bit_display_x, - screen_y=screen_y, - height=bit_font_height, + screen_y=screen_y + int(first_bits_line.height/2), # center the underscores vertically like hypens is_text_centered=False, )) @@ -308,8 +327,10 @@ class ToolsCalcFinalWordScreen(ButtonListScreen): # And now the *actual* final word after merging the bit data self.components.append(TextArea( - text=f"""Final Word: \"{self.actual_final_word}\"""", + # TRANSLATOR_NOTE: labeled presentation of the last word in a BIP-39 mnemonic seed phrase. + text=_('Final Word: "{}"').format(self.actual_final_word), screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING, + height_ignores_below_baseline=True, # Keep the next line (bits display) snugged up, regardless of text rendering below the baseline )) # Once again show the bits that came from the user's entropy... @@ -349,8 +370,10 @@ class ToolsCalcFinalWordDoneScreen(ButtonListScreen): def __post_init__(self): # Manually specify 12 vs 24 case for easier ordinal translation if self.mnemonic_word_length == 12: + # TRANSLATOR_NOTE: a label for the last word of a 12-word BIP-39 mnemonic seed phrase self.title = _("12th Word") else: + # TRANSLATOR_NOTE: a label for the last word of a 24-word BIP-39 mnemonic seed phrase self.title = _("24th Word") self.is_bottom_list = True @@ -364,8 +387,9 @@ class ToolsCalcFinalWordDoneScreen(ButtonListScreen): )) self.components.append(IconTextLine( - icon_name=SeedSignerCustomIconConstants.FINGERPRINT, - icon_color="blue", + icon_name=SeedSignerIconConstants.FINGERPRINT, + icon_color=GUIConstants.INFO_COLOR, + # TRANSLATOR_NOTE: a label for the shortened Key-id of a BIP-32 master HD wallet label_text=_("fingerprint"), value_text=self.fingerprint, is_text_centered=True, @@ -382,15 +406,17 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen): custom_derivation_path: str = None def __post_init__(self): - self.title = "Address Explorer" + # TRANSLATOR_NOTE: a label for the tool to explore public addresses for this seed. + self.title = _("Address Explorer") self.is_bottom_list = True super().__post_init__() if self.fingerprint: self.components.append(IconTextLine( - icon_name=SeedSignerCustomIconConstants.FINGERPRINT, - icon_color="blue", - label_text="Fingerprint", + icon_name=SeedSignerIconConstants.FINGERPRINT, + icon_color=GUIConstants.INFO_COLOR, + # TRANSLATOR_NOTE: a label for the shortened Key-id of a BIP-32 master HD wallet + label_text=_("Fingerprint"), value_text=self.fingerprint, screen_x=GUIConstants.EDGE_PADDING, screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING, @@ -398,26 +424,29 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen): if self.script_type != SettingsConstants.CUSTOM_DERIVATION: self.components.append(IconTextLine( - icon_name=SeedSignerCustomIconConstants.PATH, - label_text="Derivation", + icon_name=SeedSignerIconConstants.DERIVATION, + # TRANSLATOR_NOTE: a label for the derivation-path into a BIP-32 HD wallet + label_text=_("Derivation"), value_text=SettingsDefinition.get_settings_entry(attr_name=SettingsConstants.SETTING__SCRIPT_TYPES).get_selection_option_display_name_by_value(value=self.script_type), screen_x=GUIConstants.EDGE_PADDING, - screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING, + screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING, )) else: self.components.append(IconTextLine( - icon_name=SeedSignerCustomIconConstants.PATH, - label_text="Derivation", + icon_name=SeedSignerIconConstants.DERIVATION, + # l10n_note already exists. + label_text=_("Derivation"), value_text=self.custom_derivation_path, screen_x=GUIConstants.EDGE_PADDING, - screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING, + screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING, )) else: self.components.append(IconTextLine( - label_text="Wallet descriptor", + # TRANSLATOR_NOTE: a label for a BIP-380-ish Output Descriptor + label_text=_("Wallet descriptor"), value_text=self.wallet_descriptor_display_name, - is_text_centered=False, + is_text_centered=True, screen_x=GUIConstants.EDGE_PADDING, screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING, )) diff --git a/src/seedsigner/gui/toast.py b/src/seedsigner/gui/toast.py new file mode 100644 index 00000000..2f4ff229 --- /dev/null +++ b/src/seedsigner/gui/toast.py @@ -0,0 +1,232 @@ +from gettext import gettext as _ +import time +from dataclasses import dataclass +from seedsigner.gui.components import BaseComponent, GUIConstants, Icon, SeedSignerIconConstants, TextArea +from seedsigner.models.threads import BaseThread + + + +@dataclass +class ToastOverlay(BaseComponent): + icon_name: str = None + color: str = GUIConstants.NOTIFICATION_COLOR + label_text: str = None + height: int = GUIConstants.ICON_TOAST_FONT_SIZE + 2*GUIConstants.EDGE_PADDING + font_size: int = 19 + outline_thickness: int = 2 # pixels + + def __post_init__(self): + super().__post_init__() + + self.icon = Icon( + image_draw=self.image_draw, + canvas=self.canvas, + screen_x=self.outline_thickness + 2*GUIConstants.EDGE_PADDING, # Push the icon further from the left edge than strictly necessary + icon_name=self.icon_name, + icon_size=GUIConstants.ICON_TOAST_FONT_SIZE, + icon_color=self.color + ) + self.icon.screen_y = self.canvas_height - self.height + int((self.height - self.icon.height)/2) + + self.label = TextArea( + image_draw=self.image_draw, + canvas=self.canvas, + text=self.label_text, + font_size=self.font_size, + font_color=self.color, + edge_padding=0, + is_text_centered=False, + auto_line_break=True, + width=self.canvas_width - self.icon.screen_x - self.icon.width - GUIConstants.COMPONENT_PADDING - self.outline_thickness, + screen_x=self.icon.screen_x + self.icon.width + GUIConstants.COMPONENT_PADDING, + allow_text_overflow=False, + ) + + # Vertically center the message within the toast (for single- or multi-line + # messages). + self.label.screen_y = self.canvas_height - self.height + self.outline_thickness + int((self.height - 2*self.outline_thickness - self.label.height)/2) + + + def render(self): + # Render the toast's solid background + self.image_draw.rounded_rectangle( + (0, self.canvas_height - self.height, self.canvas_width, self.canvas_height), + fill=GUIConstants.BACKGROUND_COLOR, + radius=8, + outline=self.color, + width=self.outline_thickness, + ) + + # Draw the toast visual elements + self.icon.render() + self.label.render() + + self.renderer.show_image() + + + +class BaseToastOverlayManagerThread(BaseThread): + """ + The toast notification popup consists of a gui component (`ToastOverlay`) and this + manager thread that the Controller will use to coordinate handing off resources + between competing toasts, the screensaver, and the current underlying Screen. + + Controller should set BaseThread.keep_running = False to terminate the toast when it + needs to be removed or replaced. + + Controller should set toggle_renderer_lock = True to make the toast temporarily + release the Renderer.lock so another process (e.g. screensaver) can use it. The toast + thread will immediately try to reacquire the lock, but will have to block and wait + until it's available again. Note that this thread will be unresponsive while it + waits to reacquire the lock! + + Note: any process can call lock.release() but it simplifies the logic to try to keep + each process aware of whether it is currently holding the lock or not (i.e. it's + better for the "owner" thread to release the lock itself). + """ + def __init__(self, + activation_delay: int = 0, # seconds before toast is displayed + duration: int = 3, # seconds toast is displayed + ): + from seedsigner.controller import Controller + from seedsigner.gui.renderer import Renderer + from seedsigner.hardware.buttons import HardwareButtons + super().__init__() + self.activation_delay: int = activation_delay + self.duration: int = duration + self._toggle_renderer_lock: bool = False + + self.renderer = Renderer.get_instance() + self.controller = Controller.get_instance() + self.hw_inputs = HardwareButtons.get_instance() + + # Special case when screensaver is running + self.hw_inputs.override_ind = True + + self.toast = self.instantiate_toast() + + + def instantiate_toast(self) -> ToastOverlay: + raise Exception("Must be implemented by subclass") + + + def should_keep_running(self) -> bool: + """ Placeholder for custom exit conditions """ + return True + + + def toggle_renderer_lock(self): + self._toggle_renderer_lock = True + + + def run(self): + print(f"{self.__class__.__name__}: started") + start = time.time() + while time.time() - start < self.activation_delay: + if self.hw_inputs.has_any_input(): + # User has pressed a button, cancel the toast + print(f"{self.__class__.__name__}: Canceling toast due to user input") + return + time.sleep(0.1) + + try: + # Hold onto the Renderer lock so we're guaranteed to restore the original + # screen before any other listener can get a screen write in. + print(f"{self.__class__.__name__}: Acquiring lock") + self.renderer.lock.acquire() + print(f"{self.__class__.__name__}: Lock acquired") + + has_rendered = False + previous_screen_state = None + while self.keep_running and self.should_keep_running(): + if self.hw_inputs.has_any_input(): + # User has pressed a button, hide the toast + print(f"{self.__class__.__name__}: Exiting due to user input") + break + + if self._toggle_renderer_lock: + # Controller has notified us that another process needs the lock + print(f"{self.__class__.__name__}: Releasing lock") + self._toggle_renderer_lock = False + self.renderer.lock.release() + + # pause to avoid race conditions reacquiring the lock + while not self.renderer.lock.locked(): + # Wait for a different process to grab the lock + time.sleep(0.1) + + # Block while waiting to reaquire the lock + print(f"{self.__class__.__name__}: Blocking to re-acquire lock") + self.renderer.lock.acquire() + print(f"{self.__class__.__name__}: Lock re-acquired") + + if not has_rendered: + previous_screen_state = self.renderer.canvas.copy() + print(f"{self.__class__.__name__}: Showing toast") + self.toast.render() + has_rendered = True + + if time.time() - start > self.activation_delay + self.duration and has_rendered: + print(f"{self.__class__.__name__}: Hiding toast") + break + + # Free up cpu resources for main thread + time.sleep(0.1) + + finally: + print(f"{self.__class__.__name__}: exiting") + if has_rendered and self.renderer.lock.locked(): + # As far as we know, we currently hold the Renderer.lock + self.renderer.show_image(previous_screen_state) + print(f"{self.__class__.__name__}: restored previous screen state") + + # We're done, release the lock + self.renderer.lock.release() + + + +class RemoveSDCardToastManagerThread(BaseToastOverlayManagerThread): + def __init__(self, activation_delay=3): + # Note: activation_delay is configurable so the screenshot generator can get the + # toast to immediately render. + super().__init__( + activation_delay=activation_delay, # seconds + duration=1e6, # seconds ("forever") + ) + + + def instantiate_toast(self) -> ToastOverlay: + body_font_size = GUIConstants.get_body_font_size() + return ToastOverlay( + icon_name=SeedSignerIconConstants.MICROSD, + label_text=_("You can remove\nthe SD card now"), + font_size=body_font_size, + height=body_font_size * 2 + GUIConstants.BODY_LINE_SPACING + GUIConstants.EDGE_PADDING, + ) + + + def should_keep_running(self) -> bool: + """ Custom exit condition: keep running until the SD card is removed """ + from seedsigner.hardware.microsd import MicroSD + return MicroSD.get_instance().is_inserted + + + +class SDCardStateChangeToastManagerThread(BaseToastOverlayManagerThread): + def __init__(self, action: str, *args, **kwargs): + # Note: we could just directly detect the MicroSD status here, but passing it in + # via `action` lets us simulate the state we want in the screenshot generator. + from seedsigner.hardware.microsd import MicroSD + if action not in [MicroSD.ACTION__INSERTED, MicroSD.ACTION__REMOVED]: + raise Exception(f"Invalid MicroSD action: {action}") + self.message = _("SD card removed") if action == MicroSD.ACTION__REMOVED else _("SD card inserted") + + super().__init__(*args, **kwargs) + + + def instantiate_toast(self) -> ToastOverlay: + print("instantiating toast!") + return ToastOverlay( + icon_name=SeedSignerIconConstants.MICROSD, + label_text=self.message, + ) diff --git a/src/seedsigner/hardware/ST7789.py b/src/seedsigner/hardware/ST7789.py index 09cebd20..ebd4952b 100644 --- a/src/seedsigner/hardware/ST7789.py +++ b/src/seedsigner/hardware/ST7789.py @@ -1,7 +1,7 @@ import spidev import RPi.GPIO as GPIO import time -import numpy as np +import array @@ -13,11 +13,11 @@ class ST7789(object): self.height = 240 #Initialize DC RST pin - self._dc = 25 - self._rst = 27 - self._bl = 24 + self._dc = 22 + self._rst = 13 + self._bl = 18 - GPIO.setmode(GPIO.BCM) + GPIO.setmode(GPIO.BOARD) GPIO.setwarnings(False) GPIO.setup(self._dc,GPIO.OUT) GPIO.setup(self._rst,GPIO.OUT) @@ -153,20 +153,17 @@ class ST7789(object): if imwidth != self.width or imheight != self.height: raise ValueError('Image must be same dimensions as display \ ({0}x{1}).' .format(self.width, self.height)) - img = np.asarray(Image) - pix = np.zeros((self.width,self.height,2), dtype = np.uint8) - pix[...,[0]] = np.add(np.bitwise_and(img[...,[0]],0xF8),np.right_shift(img[...,[1]],5)) - pix[...,[1]] = np.add(np.bitwise_and(np.left_shift(img[...,[1]],3),0xE0),np.right_shift(img[...,[2]],3)) - pix = pix.flatten().tolist() + # convert 24-bit RGB-8:8:8 to gBRG-3:5:5:3; then per-pixel byteswap to 16-bit RGB-5:6:5 + arr = array.array("H", Image.convert("BGR;16").tobytes()) + arr.byteswap() + pix = arr.tobytes() self.SetWindows ( 0, 0, self.width, self.height) GPIO.output(self._dc,GPIO.HIGH) - for i in range(0,len(pix),4096): - self._spi.writebytes(pix[i:i+4096]) + self._spi.writebytes2(pix) def clear(self): """Clear contents of image buffer""" _buffer = [0xff]*(self.width * self.height * 2) self.SetWindows ( 0, 0, self.width, self.height) GPIO.output(self._dc,GPIO.HIGH) - for i in range(0,len(_buffer),4096): - self._spi.writebytes(_buffer[i:i+4096]) + self._spi.writebytes2(_buffer) diff --git a/src/seedsigner/hardware/buttons.py b/src/seedsigner/hardware/buttons.py index 014f1895..eec0174e 100644 --- a/src/seedsigner/hardware/buttons.py +++ b/src/seedsigner/hardware/buttons.py @@ -4,18 +4,30 @@ import time from seedsigner.models.singleton import Singleton - class HardwareButtons(Singleton): - KEY_UP_PIN = 6 - KEY_DOWN_PIN = 19 - KEY_LEFT_PIN = 5 - KEY_RIGHT_PIN = 26 - KEY_PRESS_PIN = 13 + if GPIO.RPI_INFO['P1_REVISION'] == 3: #This indicates that we have revision 3 GPIO + print("Detected 40pin GPIO (Rasbperry Pi 2 and above)") + KEY_UP_PIN = 31 + KEY_DOWN_PIN = 35 + KEY_LEFT_PIN = 29 + KEY_RIGHT_PIN = 37 + KEY_PRESS_PIN = 33 - KEY1_PIN = 21 - KEY2_PIN = 20 - KEY3_PIN = 16 + KEY1_PIN = 40 + KEY2_PIN = 38 + KEY3_PIN = 36 + else: + print("Assuming 26 Pin GPIO (Raspberry P1 1)") + KEY_UP_PIN = 5 + KEY_DOWN_PIN = 11 + KEY_LEFT_PIN = 3 + KEY_RIGHT_PIN = 15 + KEY_PRESS_PIN = 7 + + KEY1_PIN = 16 + KEY2_PIN = 12 + KEY3_PIN = 8 @classmethod def get_instance(cls): @@ -24,7 +36,7 @@ class HardwareButtons(Singleton): cls._instance = cls.__new__(cls) #init GPIO - GPIO.setmode(GPIO.BCM) + GPIO.setmode(GPIO.BOARD) GPIO.setup(HardwareButtons.KEY_UP_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up GPIO.setup(HardwareButtons.KEY_DOWN_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up GPIO.setup(HardwareButtons.KEY_LEFT_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up @@ -69,7 +81,7 @@ class HardwareButtons(Singleton): while True: cur_time = int(time.time() * 1000) - if cur_time - self.last_input_time > controller.screensaver_activation_ms and not controller.screensaver.is_running: + if cur_time - self.last_input_time > controller.screensaver_activation_ms and not controller.is_screensaver_running: # Start the screensaver. Will block execution until input detected. controller.start_screensaver() @@ -171,21 +183,31 @@ class HardwareButtons(Singleton): return True return False - - - # class used as short hand for static button/channel lookup values # TODO: Implement `release_lock` functionality as a global somewhere. Mixes up design # patterns to have a static constants class plus a settable global value. class HardwareButtonsConstants: - KEY_UP = 6 - KEY_DOWN = 19 - KEY_LEFT = 5 - KEY_RIGHT = 26 - KEY_PRESS = 13 - KEY1 = 21 - KEY2 = 20 - KEY3 = 16 + if GPIO.RPI_INFO['P1_REVISION'] == 3: #This indicates that we have revision 3 GPIO + KEY_UP = 31 + KEY_DOWN = 35 + KEY_LEFT = 29 + KEY_RIGHT = 37 + KEY_PRESS = 33 + + KEY1 = 40 + KEY2 = 38 + KEY3 = 36 + else: + KEY_UP = 5 + KEY_DOWN = 11 + KEY_LEFT = 3 + KEY_RIGHT = 15 + KEY_PRESS = 7 + + KEY1 = 16 + KEY2 = 12 + KEY3 = 8 + OVERRIDE = 1000 ALL_KEYS = [ diff --git a/src/seedsigner/hardware/camera.py b/src/seedsigner/hardware/camera.py index 47b5437a..be80a6f3 100644 --- a/src/seedsigner/hardware/camera.py +++ b/src/seedsigner/hardware/camera.py @@ -1,9 +1,9 @@ import io -import numpy from PIL import Image -from seedsigner.models import Singleton -from seedsigner.models.settings import SettingsConstants +from seedsigner.hardware.pivideostream import PiVideoStream +from seedsigner.models.settings import Settings, SettingsConstants +from seedsigner.models.singleton import Singleton @@ -15,7 +15,6 @@ class Camera(Singleton): @classmethod def get_instance(cls): # This is the only way to access the one and only Controller - from seedsigner.models import Settings if cls._instance is None: cls._instance = cls.__new__(cls) cls._instance._camera_rotation = int(Settings.get_instance().get_value(SettingsConstants.SETTING__CAMERA_ROTATION)) diff --git a/src/seedsigner/hardware/microsd.py b/src/seedsigner/hardware/microsd.py new file mode 100644 index 00000000..26065c03 --- /dev/null +++ b/src/seedsigner/hardware/microsd.py @@ -0,0 +1,71 @@ +import os +import time + +from seedsigner.models.singleton import Singleton +from seedsigner.models.threads import BaseThread +from seedsigner.models.settings import Settings + + + +class MicroSD(Singleton, BaseThread): + MOUNT_POINT = "/mnt/microsd" + FIFO_PATH = "/tmp/mdev_fifo" + FIFO_MODE = 0o600 + ACTION__INSERTED = "add" + ACTION__REMOVED = "remove" + + + @classmethod + def get_instance(cls): + # This is the only way to access the one and only instance + if cls._instance is None: + # Instantiate the one and only instance + microsd = cls.__new__(cls) + cls._instance = microsd + + # explicitly call BaseThread __init__ since multiple class inheritance + BaseThread.__init__(microsd) + + return cls._instance + + + @property + def is_inserted(self): + if Settings.HOSTNAME == Settings.SEEDSIGNER_OS: + return os.path.exists(MicroSD.MOUNT_POINT) + else: + # Always True for Raspi OS + return True + + + def start_detection(self): + self.start() + + + def run(self): + from seedsigner.controller import Controller + from seedsigner.gui.toast import SDCardStateChangeToastManagerThread + action = "" + + # explicitly only microsd add/remove detection in seedsigner-os + if Settings.HOSTNAME == Settings.SEEDSIGNER_OS: + + # at start-up, get current status and inform Settings + Settings.handle_microsd_state_change( + action=MicroSD.ACTION__INSERTED if self.is_inserted else MicroSD.ACTION__REMOVED + ) + + if os.path.exists(self.FIFO_PATH): + os.remove(self.FIFO_PATH) + + os.mkfifo(self.FIFO_PATH, self.FIFO_MODE) + + while self.keep_running: + with open(self.FIFO_PATH) as fifo: + action = fifo.read() + print(f"fifo message: {action}") + + Settings.handle_microsd_state_change(action=action) + Controller.get_instance().activate_toast(SDCardStateChangeToastManagerThread(action=action)) + + time.sleep(0.1) diff --git a/src/seedsigner/helpers/embit_utils.py b/src/seedsigner/helpers/embit_utils.py index bdd95b7f..da8f827a 100644 --- a/src/seedsigner/helpers/embit_utils.py +++ b/src/seedsigner/helpers/embit_utils.py @@ -1,9 +1,14 @@ import embit -from embit import bip32 + +from binascii import b2a_base64 +from hashlib import sha256 + +from embit import bip32, compact, ec from embit.bip32 import HDKey from embit.descriptor import Descriptor from embit.networks import NETWORKS -from embit.networks import NETWORKS +from embit.util import secp256k1 + from seedsigner.models.settings_definition import SettingsConstants @@ -93,3 +98,101 @@ def get_multisig_address(descriptor: Descriptor, index: int = 0, is_change: bool elif descriptor.is_taproot: # TODO: Not yet implemented! raise Exception("Taproot verification not yet implemented!") + + + +def get_embit_network_name(settings_name): + """ Convert SeedSigner SettingsConstants for `network` to embit's NETWORK key """ + lookup = { + SettingsConstants.MAINNET: "main", + SettingsConstants.TESTNET: "test", + SettingsConstants.REGTEST: "regtest", + } + return lookup.get(settings_name) + + + +def parse_derivation_path(derivation_path: str) -> dict: + """ + Parses a derivation path into its related SettingsConstants equivalents. + + Primarily only supports single sig derivation paths. + + May return None for fields it cannot parse. + """ + # Support either m/44'/... or m/44h/... style + derivation_path = derivation_path.replace("'", "h") + + sections = derivation_path.split("/") + + if sections[1] == "48h": + # So far this helper is only meant for single sig message signing + raise Exception("Not implemented") + + lookups = { + "script_types": { + "84h": SettingsConstants.NATIVE_SEGWIT, + "49h": SettingsConstants.NESTED_SEGWIT, + "86h": SettingsConstants.TAPROOT, + }, + "networks": { + "0h": SettingsConstants.MAINNET, + "1h": [SettingsConstants.TESTNET, SettingsConstants.REGTEST], + } + } + + details = dict() + details["script_type"] = lookups["script_types"].get(sections[1]) + if not details["script_type"]: + details["script_type"] = SettingsConstants.CUSTOM_DERIVATION + details["network"] = lookups["networks"].get(sections[2]) + + # Check if there's a standard change path + if sections[-2] in ["0", "1"]: + details["is_change"] = sections[-2] == "1" + else: + details["is_change"] = None + + # Check if there's a standard address index + if sections[-1].isdigit(): + details["index"] = int(sections[-1]) + else: + details["index"] = None + + if details["is_change"] is not None and details["index"] is not None: + # standard change and addr index; safe to truncate to the wallet level + details["wallet_derivation_path"] = "/".join(sections[:-2]) + else: + details["wallet_derivation_path"] = None + + details["clean_match"] = True + for k, v in details.items(): + if v is None: + # At least one field couldn't be parsed + details["clean_match"] = False + break + + return details + + + +def sign_message(seed_bytes: bytes, derivation: str, msg: bytes, compressed: bool = True, embit_network: str = "main") -> bytes: + """ + from: https://github.com/cryptoadvance/specter-diy/blob/b58a819ef09b2bca880a82c7e122618944355118/src/apps/signmessage/signmessage.py + """ + """Sign message with private key""" + msghash = sha256( + sha256( + b"\x18Bitcoin Signed Message:\n" + compact.to_bytes(len(msg)) + msg + ).digest() + ).digest() + + root = bip32.HDKey.from_seed(seed_bytes, version=NETWORKS[embit_network]["xprv"]) + prv = root.derive(derivation).key + sig = secp256k1.ecdsa_sign_recoverable(msghash, prv._secret) + flag = sig[64] + sig = ec.Signature(sig[:64]) + c = 4 if compressed else 0 + flag = bytes([27 + flag + c]) + ser = flag + secp256k1.ecdsa_signature_serialize_compact(sig._sig) + return b2a_base64(ser).strip().decode() diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index dab63a48..a021a834 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -5,7 +5,6 @@ from embit import bip39 from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes from typing import List -from seedsigner.models.seed import Seed def calculate_checksum(mnemonic: list, wordlist_language_code: str) -> List[str]: @@ -15,6 +14,7 @@ def calculate_checksum(mnemonic: list, wordlist_language_code: str) -> List[str] If 11- or 23-words are provided, append word `0000` to end of list as temp final word. """ + from seedsigner.models.seed import Seed if len(mnemonic) in [11, 23]: mnemonic.append(Seed.get_wordlist(wordlist_language_code)[0]) diff --git a/src/seedsigner/helpers/qr.py b/src/seedsigner/helpers/qr.py index 27f10ac6..333910b0 100644 --- a/src/seedsigner/helpers/qr.py +++ b/src/seedsigner/helpers/qr.py @@ -42,12 +42,12 @@ class QR: else: border_str = "3" - cmd = f"""qrencode -m {border_str} -s 3 -l L --foreground=000000 --background={background_color} -t PNG -o "/dev/shm/qrcode.png" "{str(data)}" """ + cmd = f"""qrencode -m {border_str} -s 3 -l L --foreground=000000 --background={background_color} -t PNG -o "/tmp/qrcode.png" "{str(data)}" """ rv = subprocess.call(cmd, shell=True) # if qrencode fails, fall back to only encoder if rv != 0: return self.qrimage(data,width,height,border) - img = Image.open("/dev/shm/qrcode.png").resize((width,height), Image.NEAREST).convert("RGBA") + img = Image.open("/tmp/qrcode.png").resize((width,height), Image.Resampling.NEAREST).convert("RGBA") return img diff --git a/src/seedsigner/helpers/ur2/bytewords.py b/src/seedsigner/helpers/ur2/bytewords.py index b7e8432a..a1204ed2 100644 --- a/src/seedsigner/helpers/ur2/bytewords.py +++ b/src/seedsigner/helpers/ur2/bytewords.py @@ -126,7 +126,7 @@ class Bytewords: elif style == Bytewords_Style_minimal: return encode_minimal(bytes) else: - assert(False) + assert False @staticmethod def decode(style, str): @@ -137,4 +137,4 @@ class Bytewords: elif style == Bytewords_Style_minimal: return decode(str, 0, 2) else: - assert(False) + assert False diff --git a/src/seedsigner/helpers/ur2/fountain_decoder.py b/src/seedsigner/helpers/ur2/fountain_decoder.py index c0b3f873..69eb64e3 100644 --- a/src/seedsigner/helpers/ur2/fountain_decoder.py +++ b/src/seedsigner/helpers/ur2/fountain_decoder.py @@ -255,7 +255,7 @@ class FountainDecoder: elif self.is_failure(): return 'Exception: {}'.format(self.result) else: - assert(False) + assert False def print_part(self, p): print('part indexes: {}'.format(self.indexes_to_string(p.indexes))) diff --git a/src/seedsigner/helpers/ur2/fountain_encoder.py b/src/seedsigner/helpers/ur2/fountain_encoder.py index d539e460..ac43bb62 100644 --- a/src/seedsigner/helpers/ur2/fountain_encoder.py +++ b/src/seedsigner/helpers/ur2/fountain_encoder.py @@ -84,7 +84,7 @@ class Part: class FountainEncoder: def __init__(self, message, max_fragment_len, first_seq_num = 0, min_fragment_len = 10): - assert(len(message) <= MAX_UINT32) + assert len(message) <= MAX_UINT32 self.message_len = len(message) self.checksum = crc32_int(message) self.fragment_len = FountainEncoder.find_nominal_fragment_length(self.message_len, min_fragment_len, max_fragment_len) @@ -93,9 +93,9 @@ class FountainEncoder: @staticmethod def find_nominal_fragment_length(message_len, min_fragment_len, max_fragment_len): - assert(message_len > 0) - assert(min_fragment_len > 0) - assert(max_fragment_len >= min_fragment_len) + assert message_len > 0 + assert min_fragment_len > 0 + assert max_fragment_len >= min_fragment_len max_fragment_count = message_len // min_fragment_len fragment_len = None @@ -104,7 +104,7 @@ class FountainEncoder: if fragment_len <= max_fragment_len: break - assert(fragment_len != None) + assert fragment_len != None return fragment_len diff --git a/src/seedsigner/helpers/ur2/random_sampler.py b/src/seedsigner/helpers/ur2/random_sampler.py index 087789e8..d1b80411 100644 --- a/src/seedsigner/helpers/ur2/random_sampler.py +++ b/src/seedsigner/helpers/ur2/random_sampler.py @@ -9,11 +9,11 @@ class RandomSampler: def __init__(self, probs): for p in probs: - assert(p > 0) + assert p > 0 # Normalize given probabilities total = sum(probs) - assert(total > 0) + assert total > 0 n = len(probs) diff --git a/src/seedsigner/helpers/ur2/utils.py b/src/seedsigner/helpers/ur2/utils.py index 4e88c363..31a0dfe1 100644 --- a/src/seedsigner/helpers/ur2/utils.py +++ b/src/seedsigner/helpers/ur2/utils.py @@ -57,7 +57,7 @@ def join_bytes(list_of_ba): def xor_into(target, source): count = len(target) - assert(count == len(source)) # Must be the same length + assert count == len(source) # Must be the same length for i in range(count): target[i] ^= source[i] diff --git a/src/seedsigner/models/__init__.py b/src/seedsigner/models/__init__.py index c21cf9cb..e69de29b 100644 --- a/src/seedsigner/models/__init__.py +++ b/src/seedsigner/models/__init__.py @@ -1,10 +0,0 @@ -# Must import crucial base class first! -from .singleton import Singleton, ConfigurableSingleton - -from .seed import * -from .qr_type import * -from .decode_qr import * -from .encode_qr import * -from .psbt_parser import * -from .seed_storage import * -from .settings import * diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index 3928ff8e..a020c312 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -9,14 +9,13 @@ from embit import psbt, bip39 from pyzbar import pyzbar from pyzbar.pyzbar import ZBarSymbol from urtypes.crypto import PSBT as UR_PSBT -from urtypes.crypto import Account, HDKey, Output, Keypath, PathComponent, SCRIPT_EXPRESSION_TAG_MAP +from urtypes.crypto import Account, Output from urtypes.bytes import Bytes from seedsigner.helpers.ur2.ur_decoder import URDecoder -from seedsigner.models.psbt_parser import PSBTParser - -from . import QRType, Seed -from .settings import SettingsConstants +from seedsigner.models.qr_type import QRType +from seedsigner.models.seed import Seed +from seedsigner.models.settings import SettingsConstants logger = logging.getLogger(__name__) @@ -84,6 +83,9 @@ class DecodeQR: elif self.qr_type == QRType.BITCOIN_ADDRESS: self.decoder = BitcoinAddressQrDecoder() # Single Segment bitcoin address + elif self.qr_type == QRType.SIGN_MESSAGE: + self.decoder = SignMessageQrDecoder() # Single Segment sign message request + elif self.qr_type == QRType.WALLET__SPECTER: self.decoder = SpecterWalletQrDecoder() # Specter Desktop Wallet Export decoder @@ -132,6 +134,9 @@ class DecodeQR: return rt + # TODO: Refactor all of these specific `get_` to just something generic like + # `get_data` and let each QRDecoder class return whatever it needs to as a + # str, tuple, dict, etc? def get_psbt(self): if self.complete: data = self.get_data_psbt() @@ -175,12 +180,7 @@ class DecodeQR: def get_settings_data(self): if self.is_settings: - return self.decoder.settings - - - def get_settings_config_name(self): - if self.is_settings: - return self.decoder.config_name + return self.decoder.data def get_address(self): @@ -193,6 +193,15 @@ class DecodeQR: return self.decoder.get_address_type() + def get_qr_data(self) -> dict: + """ + This provides a single access point for external code to retrieve the QR data, + regardless of which decoder is actually instantiated. + """ + # TODO: Implement this approach across all decoders + return self.decoder.get_qr_data() + + def get_wallet_descriptor(self): if self.is_wallet_descriptor: if self.qr_type in [QRType.OUTPUT__UR, QRType.ACCOUNT__UR, QRType.BYTES__UR]: @@ -273,6 +282,11 @@ class DecodeQR: return self.qr_type == QRType.BITCOIN_ADDRESS + @property + def is_sign_message(self): + return self.qr_type == QRType.SIGN_MESSAGE + + @property def is_wallet_descriptor(self): check = self.qr_type in [QRType.WALLET__SPECTER, QRType.WALLET__UR, QRType.WALLET__CONFIGFILE, QRType.WALLET__GENERIC, QRType.OUTPUT__UR] @@ -363,8 +377,12 @@ class DecodeQR: elif DecodeQR.is_bitcoin_address(s): return QRType.BITCOIN_ADDRESS + # message signing + elif DecodeQR.is_sign_message(s): + return QRType.SIGN_MESSAGE + # config data - if "type=settings" in s: + if s.startswith("settings::"): return QRType.SETTINGS # Seed @@ -482,7 +500,13 @@ class DecodeQR: return True else: return False - + + + @staticmethod + def is_sign_message(s): + return type(s) == str and s.startswith("signmessage") + + @staticmethod def multisig_setup_file_to_descriptor(text) -> str: # sample text file, parse the contents and create descriptor @@ -578,6 +602,8 @@ class DecodeQR: return descriptor + + class BaseQrDecoder: def __init__(self): self.total_segments = None @@ -590,6 +616,10 @@ class BaseQrDecoder: def add(self, segment, qr_type): raise Exception("Not implemented in child class") + + def get_qr_data(self) -> dict: + # TODO: standardize this approach across all decoders (example: SignMessageQrDecoder) + raise Exception("get_qr_data must be implemented in decoder child class") @@ -673,6 +703,8 @@ class SpecterPsbtQrDecoder(BaseAnimatedQrDecoder): def parse_segment(self, segment) -> str: return segment.split(" ")[-1].strip() + + class Base64PsbtQrDecoder(BaseSingleFrameQrDecoder): """ Decodes single frame base64 encoded qr image. @@ -826,107 +858,68 @@ class SeedQrDecoder(BaseSingleFrameQrDecoder): -# TODO: Refactor this to work with the new SettingsDefinition class SettingsQrDecoder(BaseSingleFrameQrDecoder): + """ + Decodes settings data from the SettingsQR Generator. + """ def __init__(self): super().__init__() - self.settings = {} - self.config_name = None + self.data = None def add(self, segment, qr_type=QRType.SETTINGS): - # print(f"SettingsQR:\n{segment}") - try: - self.settings = {} + """ + * Ignores unrecognized settings options. + * Raises an Exception if a settings value is invalid. - # QR Settings format is space-separated key/value pairs, but should also - # parse \n-separated keys. - for entry in segment.split(): - key = entry.split("=")[0].strip() - value = entry.split("=")[1].strip() - self.settings[key] = value + See `Settings.update()` for info on settings validation, especially for + missing settings. + """ + if not segment.startswith("settings::"): + raise Exception("Invalid SettingsQR data") + + # Leave any other parsing or validation up to the Settings class itself. + # SettingsQR are just ascii data to hand it over as-is. + self.data = segment - # Remove values only needed for import - self.settings.pop("type", None) - version = self.settings.pop("version", None) - if not version or int(version) != 1: - raise Exception(f"Settings QR version {version} not supported") + self.complete = True + self.collected_segments = 1 + return DecodeQRStatus.COMPLETE - self.config_name = self.settings.pop("name", None) - if self.config_name: - self.config_name = self.config_name.replace("_", " ") - - # Have to translate the abbreviated settings into the human-readable values - # used in the normal Settings. - map_abbreviated_enable = { - "0": SettingsConstants.OPTION__DISABLED, - "1": SettingsConstants.OPTION__ENABLED, - "2": SettingsConstants.OPTION__PROMPT, - } - map_abbreviated_sig_types = { - "s": SettingsConstants.SINGLE_SIG, - "m": SettingsConstants.MULTISIG, - } - map_abbreviated_scripts = { - "na": SettingsConstants.NATIVE_SEGWIT, - "ne": SettingsConstants.NESTED_SEGWIT, - "tr": SettingsConstants.TAPROOT, - "cu": SettingsConstants.CUSTOM_DERIVATION, - } - map_abbreviated_coordinators = { - "bw": SettingsConstants.COORDINATOR__BLUE_WALLET, - "sw": SettingsConstants.COORDINATOR__SPARROW, - "sd": SettingsConstants.COORDINATOR__SPECTER_DESKTOP, - } - def convert_abbreviated_value(category, key, abbreviation_map, is_list=False, new_key_name=None): - try: - if key not in self.settings: - print(f"'{key}' not found in settings") - return - value = self.settings[key] - if not is_list: - new_value = abbreviation_map.get(value) - if not new_value: - logger.error(f"No abbreviation map value for \"{value}\" for setting {key}") - return - else: - # `value` is a comma-separated list; yields list of map matches - values = value.split(",") - new_value = [] - for v in values: - mapped_value = abbreviation_map.get(v) - if not mapped_value: - logger.error(f"No abbreviation map value for \"{v}\" for setting {key}") - return - new_value.append(mapped_value) - del self.settings[key] - if new_key_name: - key = new_key_name - if category not in self.settings: - self.settings[category] = {} - self.settings[category][key] = new_value - except Exception as e: - logger.exception(e) - return +class SignMessageQrDecoder(BaseSingleFrameQrDecoder): + def __init__(self): + super().__init__() + self.message = None + self.derivation_path = None - convert_abbreviated_value("wallet", "coord", map_abbreviated_coordinators, is_list=True, new_key_name="coordinators") - convert_abbreviated_value("features", "xpub", map_abbreviated_enable, new_key_name="xpub_export") - convert_abbreviated_value("features", "sigs", map_abbreviated_sig_types, is_list=True, new_key_name="sig_types") - convert_abbreviated_value("features", "scripts", map_abbreviated_scripts, is_list=True, new_key_name="script_types") - convert_abbreviated_value("features", "xp_det", map_abbreviated_enable, new_key_name="show_xpub_details") - convert_abbreviated_value("features", "passphrase", map_abbreviated_enable) - convert_abbreviated_value("features", "priv_warn", map_abbreviated_enable, new_key_name="show_privacy_warnings") - convert_abbreviated_value("features", "dire_warn", map_abbreviated_enable, new_key_name="show_dire_warnings") - self.complete = True - self.collected_segments = 1 - return DecodeQRStatus.COMPLETE - except Exception as e: - logger.exception(e) + def add(self, segment, qr_type=QRType.SIGN_MESSAGE): + """ + Expected QR data format: + + signmessage {derivation_path} ascii:{message} + """ + parts = segment.split() + self.derivation_path = parts[1].replace("h", "'") + fmt = parts[2].split(":")[0] + self.message = segment.split(f"{fmt}:")[1] + + # TODO: support formats other than ascii? + if fmt != "ascii": + print(f"Sign message: Unsupported format: {fmt}") return DecodeQRStatus.INVALID + self.complete = True + self.collected_segments = 1 + + return DecodeQRStatus.COMPLETE + + + def get_qr_data(self) -> dict: + return dict(derivation_path=self.derivation_path, message=self.message) + class BitcoinAddressQrDecoder(BaseSingleFrameQrDecoder): @@ -1091,11 +1084,10 @@ class GenericWalletQrDecoder(BaseSingleFrameQrDecoder): def get_wallet_descriptor(self): return self.descriptor - -class MultiSigConfigFileQRDecoder(GenericWalletQrDecoder): - + + + +class MultiSigConfigFileQRDecoder(GenericWalletQrDecoder): def add(self, segment, qr_type=QRType.WALLET__CONFIGFILE): descriptor = DecodeQR.multisig_setup_file_to_descriptor(segment) return super().add(descriptor,qr_type=QRType.WALLET__CONFIGFILE) - - diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index 37e41848..6efa715d 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -11,12 +11,13 @@ from embit.psbt import PSBT from seedsigner.helpers.ur2.ur_encoder import UREncoder from seedsigner.helpers.ur2.ur import UR from seedsigner.helpers.qr import QR -from seedsigner.models import Seed, QRType +from seedsigner.models.qr_type import QRType +from seedsigner.models.seed import Seed +from seedsigner.models.settings import SettingsConstants from urtypes.crypto import PSBT as UR_PSBT from urtypes.crypto import Account, HDKey, Output, Keypath, PathComponent, SCRIPT_EXPRESSION_TAG_MAP -from seedsigner.models.settings import SettingsConstants @@ -38,6 +39,7 @@ class EncodeQR: qr_density: str = SettingsConstants.DENSITY__MEDIUM wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH bitcoin_address: str = None + signed_message: str = None def __post_init__(self): self.qr = QR() @@ -97,9 +99,13 @@ class EncodeQR: self.encoder = CompactSeedQrEncoder(seed_phrase=self.seed_phrase, wordlist_language_code=self.wordlist_language_code) + # Misc formats elif self.qr_type == QRType.BITCOIN_ADDRESS: self.encoder = BitcoinAddressEncoder(address=self.bitcoin_address) + elif self.qr_type == QRType.SIGN_MESSAGE: + self.encoder = SignedMessageEncoder(signed_message=self.signed_message) + else: raise Exception('QR Type not supported') @@ -154,6 +160,17 @@ class BaseQrEncoder: +class BaseStaticQrEncoder(BaseQrEncoder): + def seq_len(self): + return 1 + + + @property + def is_complete(self): + return True + + + class BasePsbtQrEncoder(BaseQrEncoder): def __init__(self, psbt: PSBT): self.psbt = psbt @@ -263,7 +280,7 @@ class SpecterPsbtQrEncoder(BasePsbtQrEncoder): -class SeedQrEncoder(BaseQrEncoder): +class SeedQrEncoder(BaseStaticQrEncoder): def __init__(self, seed_phrase: List[str], wordlist_language_code: str): super().__init__() self.seed_phrase = seed_phrase @@ -273,10 +290,6 @@ class SeedQrEncoder(BaseQrEncoder): raise Exception('Wordlist Required') - def seq_len(self): - return 1 - - def next_part(self): data = "" # Output as Numeric data format @@ -286,11 +299,6 @@ class SeedQrEncoder(BaseQrEncoder): return data - @property - def is_complete(self): - return True - - class CompactSeedQrEncoder(SeedQrEncoder): def next_part(self): @@ -322,23 +330,29 @@ class CompactSeedQrEncoder(SeedQrEncoder): -class BitcoinAddressEncoder(BaseQrEncoder): +class BitcoinAddressEncoder(BaseStaticQrEncoder): def __init__(self, address: str): super().__init__() self.address = address - def seq_len(self): - return 1 - - def next_part(self): return self.address - @property - def is_complete(self): - return True + +class SignedMessageEncoder(BaseStaticQrEncoder): + """ + Assumes that a signed message will fit in a single-frame QR + """ + def __init__(self, signed_message: str): + super().__init__() + self.signed_message = signed_message + + + def next_part(self): + return self.signed_message + class XpubQrEncoder(BaseQrEncoder): @@ -498,6 +512,8 @@ class UrXpubQrEncoder(XpubQrEncoder): ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey)) elif origin.components[3].index == 1: # Nested Multisig ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey)) + elif origin.components[0].index == 86: # P2TR + ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[409]],self.ur_hdkey)) # If empty, add all script types if len(ur_outputs) == 0: diff --git a/src/seedsigner/models/psbt_parser.py b/src/seedsigner/models/psbt_parser.py index 03444fd8..f431a3c6 100644 --- a/src/seedsigner/models/psbt_parser.py +++ b/src/seedsigner/models/psbt_parser.py @@ -1,12 +1,12 @@ from binascii import hexlify -from embit import psbt, script, ec, bip32, bip39 +from embit import psbt, script, ec, bip32 from embit.descriptor import Descriptor from embit.networks import NETWORKS from embit.psbt import PSBT from io import BytesIO from typing import List -from seedsigner.models import Seed +from seedsigner.models.seed import Seed from seedsigner.models.settings import SettingsConstants @@ -139,18 +139,40 @@ class PSBTParser(): if sc.data == self.psbt.tx.vout[i].script_pubkey.data: is_change = True + elif "p2tr" in self.policy["type"]: + print("TAPROOT output!") + my_pubkey = None + # should have one or zero derivations for single-key addresses + if len(out.taproot_bip32_derivations.values()) > 0: + # TODO: Support keys in taptree leaves + leaf_hashes, derivation = list(out.taproot_bip32_derivations.values())[0] + der = derivation.derivation + my_pubkey = self.root.derive(der) + sc = script.p2tr(my_pubkey) + + if sc.data == self.psbt.tx.vout[i].script_pubkey.data: + is_change = True + if sc.data == self.psbt.tx.vout[i].script_pubkey.data: is_change = True + if is_change: addr = self.psbt.tx.vout[i].script_pubkey.address(NETWORKS[SettingsConstants.map_network_to_embit(self.network)]) - fingerprints = None - derivation_paths = None + fingerprints = [] + derivation_paths = [] + + # extract info from non-taproot outputs if len(self.psbt.outputs[i].bip32_derivations) > 0: - fingerprints = [] - derivation_paths = [] for d, derivation_path in self.psbt.outputs[i].bip32_derivations.items(): fingerprints.append(hexlify(derivation_path.fingerprint).decode()) derivation_paths.append(bip32.path_to_str(derivation_path.derivation)) + + # extract info from taproot outputs + if len(self.psbt.outputs[i].taproot_bip32_derivations) > 0: + for d, (leaf_hashes, derivation) in self.psbt.outputs[i].taproot_bip32_derivations.items(): + fingerprints.append(hexlify(derivation.fingerprint).decode()) + derivation_paths.append(bip32.path_to_str(derivation.derivation)) + self.change_data.append({ "output_index": i, "address": addr, @@ -159,6 +181,7 @@ class PSBTParser(): "derivation_path": derivation_paths, }) self.change_amount += self.psbt.tx.vout[i].value + else: addr = self.psbt.tx.vout[i].script_pubkey.address(NETWORKS[SettingsConstants.map_network_to_embit(self.network)]) self.destination_addresses.append(addr) @@ -172,10 +195,14 @@ class PSBTParser(): @staticmethod def trim(tx): trimmed_psbt = psbt.PSBT(tx.tx) - sigsEnd = 0 for i, inp in enumerate(tx.inputs): - sigsEnd += len(list(inp.partial_sigs.keys())) - trimmed_psbt.inputs[i].partial_sigs = inp.partial_sigs + if inp.final_scriptwitness: + # Taproot sign; trim to only final_scriptwitness + # From BIP-371 and BIP-174, once final script witness is populated + # it contains all necessary signatures + trimmed_psbt.inputs[i].final_scriptwitness = inp.final_scriptwitness + else: + trimmed_psbt.inputs[i].partial_sigs = inp.partial_sigs return trimmed_psbt @@ -184,7 +211,11 @@ class PSBTParser(): def sig_count(tx): cnt = 0 for i, inp in enumerate(tx.inputs): - cnt += len(list(inp.partial_sigs.keys())) + if inp.final_scriptwitness is not None: + # Taproot sign + cnt += 1 + else: + cnt += len(list(inp.partial_sigs.keys())) return cnt @@ -283,6 +314,12 @@ class PSBTParser(): for input in psbt.inputs: for pub, derivation_path in input.bip32_derivations.items(): fingerprints.add(hexlify(derivation_path.fingerprint).decode()) + + for pub, (leaf_hashes, derivation_path) in input.taproot_bip32_derivations.items(): + # TODO: Support spends from leaves; depends on support in embit + if len(leaf_hashes) > 0: + raise Exception("Signing keyspends from within a taptree not yet implemented") + fingerprints.add(hexlify(derivation_path.fingerprint).decode()) return list(fingerprints) @@ -297,6 +334,10 @@ class PSBTParser(): for pub, derivation_path in input.bip32_derivations.items(): if seed_fingerprint == hexlify(derivation_path.fingerprint).decode(): return True + + for pub, (leaf_hashes, derivation_path) in input.taproot_bip32_derivations.items(): + if seed_fingerprint == hexlify(derivation_path.fingerprint).decode(): + return True return False diff --git a/src/seedsigner/models/qr_type.py b/src/seedsigner/models/qr_type.py index 7ef0ca67..6ec1f5ec 100644 --- a/src/seedsigner/models/qr_type.py +++ b/src/seedsigner/models/qr_type.py @@ -21,6 +21,8 @@ class QRType: BITCOIN_ADDRESS = "bitcoin_address" + SIGN_MESSAGE = "sign_message" + WALLET__SPECTER = "wallet__specter" WALLET__UR = "wallet__ur" WALLET__CONFIGFILE = "wallet__configfile" diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py index 654159ae..64f0500f 100644 --- a/src/seedsigner/models/seed.py +++ b/src/seedsigner/models/seed.py @@ -1,13 +1,11 @@ import unicodedata from binascii import hexlify -from embit import bip39, bip32 +from embit import bip39, bip32, bip85 from embit.networks import NETWORKS from typing import List from seedsigner.models.settings import SettingsConstants -from seedsigner.helpers import embit_utils - class InvalidSeedException(Exception): @@ -35,7 +33,7 @@ class Seed: @staticmethod def get_wordlist(wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> List[str]: - # TODO: Support other bip-39 wordlist languages! + # TODO: Support other BIP-39 wordlist languages! if wordlist_language_code == SettingsConstants.WORDLIST_LANGUAGE__ENGLISH: return bip39.WORDLIST else: @@ -104,19 +102,29 @@ class Seed: def set_wordlist_language_code(self, language_code: str): - # TODO: Support other bip-39 wordlist languages! + # TODO: Support other BIP-39 wordlist languages! raise Exception("Not yet implemented!") def get_fingerprint(self, network: str = SettingsConstants.MAINNET) -> str: root = bip32.HDKey.from_seed(self.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(network)]["xprv"]) return hexlify(root.child(0).fingerprint).decode('utf-8') - + def get_xpub(self, wallet_path: str = '/', network: str = SettingsConstants.MAINNET): + # Import here to avoid slow startup times; takes 1.35s to import the first time + from seedsigner.helpers import embit_utils return embit_utils.get_xpub(seed_bytes=self.seed_bytes, derivation_path=wallet_path, embit_network=SettingsConstants.map_network_to_embit(network)) + def get_bip85_child_mnemonic(self, bip85_index: int, bip85_num_words: int, network: str = SettingsConstants.MAINNET): + """Derives the seed's nth BIP-85 child mnemonic""" + root = bip32.HDKey.from_seed(self.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(network)]["xprv"]) + + # TODO: Support other BIP-39 wordlist languages! + return bip85.derive_mnemonic(root, bip85_num_words, bip85_index) + + ### override operators def __eq__(self, other): if isinstance(other, Seed): diff --git a/src/seedsigner/models/seed_storage.py b/src/seedsigner/models/seed_storage.py index d2881e32..5b6c3049 100644 --- a/src/seedsigner/models/seed_storage.py +++ b/src/seedsigner/models/seed_storage.py @@ -1,6 +1,5 @@ from typing import List -from seedsigner.models import Seed -from seedsigner.models.seed import InvalidSeedException +from seedsigner.models.seed import Seed, InvalidSeedException from seedsigner.models.settings_definition import SettingsConstants @@ -64,6 +63,11 @@ class SeedStorage: def update_pending_mnemonic(self, word: str, index: int): + """ + Replaces the nth word in the pending mnemonic. + + * may specify a negative `index` (e.g. -1 is the last word). + """ if index >= len(self._pending_mnemonic): raise Exception(f"index {index} is too high") self._pending_mnemonic[index] = word diff --git a/src/seedsigner/models/settings.py b/src/seedsigner/models/settings.py index 6a9e3841..d8b0fb2b 100644 --- a/src/seedsigner/models/settings.py +++ b/src/seedsigner/models/settings.py @@ -2,19 +2,24 @@ import gettext import json import os import pathlib +import platform -from typing import Any, List +from typing import List from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition +from seedsigner.models.singleton import Singleton -from .singleton import Singleton +class InvalidSettingsQRData(Exception): + pass class Settings(Singleton): - SETTINGS_FILENAME = "settings.json" - + HOSTNAME = platform.uname()[1] + SEEDSIGNER_OS = "seedsigner-os" + SETTINGS_FILENAME = "/mnt/microsd/settings.json" if HOSTNAME == SEEDSIGNER_OS else "settings.json" + @classmethod def get_instance(cls): # This is the only way to access the one and only instance @@ -28,7 +33,7 @@ class Settings(Singleton): # Read persistent settings file, if it exists if os.path.exists(Settings.SETTINGS_FILENAME): with open(Settings.SETTINGS_FILENAME) as settings_file: - settings.update(json.load(settings_file), disable_missing_entries=False) + settings.update(json.load(settings_file)) # Setup Babel path = os.path.join( @@ -45,6 +50,70 @@ class Settings(Singleton): return cls._instance + @classmethod + def parse_settingsqr(cls, data: str) -> tuple[str, dict]: + """ + Parses SettingsQR data and returns a tuple of (config_name, settings_dict). + + The resulting settings config can be applied by calling `Settings.update(settings_dict)`. + """ + if not data.startswith("settings::"): + raise InvalidSettingsQRData() + + version = data.split()[0].split("::")[1] + if version != "v1": + raise InvalidSettingsQRData(f"Unsupported SettingsQR version: {version}") + + # Start parsing key/value settings at the nth split() index + split_index = 1 + + # handle optional "name" attr + config_name = None + if "name=" in data.split()[1]: + config_name = data.split("name=")[1].split()[0].replace("_", " ") + split_index += 1 + + updated_settings = {} + for entry in data.split()[split_index:]: + abbreviated_name, value = entry.split("=") + + # Parse multi-value settings; integer-ize where needed + if "," in value: + values_updated = [] + for v in value.split(","): + if v.isdigit(): + v = int(v) + values_updated.append(v) + value = values_updated + elif value.isdigit(): + value = int(value) + + # Replace abbreviated name with full attr_name + settings_entry = SettingsDefinition.get_settings_entry_by_abbreviated_name(abbreviated_name) + if not settings_entry: + print(f"Ignoring unrecognized attribute: {abbreviated_name}") + continue + + # Validate value(s) against SettingsDefinition's valid options + if type(value) is not list: + values = [value] + else: + values = value + for v in values: + if v not in [opt[0] for opt in settings_entry.selection_options]: + if settings_entry.attr_name == SettingsConstants.SETTING__PERSISTENT_SETTINGS and v == SettingsConstants.OPTION__ENABLED: + # Special case: trying to enable Persistent Settings when + # DISABLED is the only option allowed (because the SD card is not + # inserted. Explicitly set to DISABLED. + value = SettingsConstants.OPTION__DISABLED + break + raise InvalidSettingsQRData(f"""{abbreviated_name} = '{v}' is not valid""") + + updated_settings[settings_entry.attr_name] = value + + return (config_name, updated_settings) + + def __str__(self): return json.dumps(self._data, indent=4) @@ -53,34 +122,28 @@ class Settings(Singleton): if self._data[SettingsConstants.SETTING__PERSISTENT_SETTINGS] == SettingsConstants.OPTION__ENABLED: with open(Settings.SETTINGS_FILENAME, 'w') as settings_file: json.dump(self._data, settings_file, indent=4) + # SeedSignerOS makes removing the microsd possible, flush and then fsync forces persistent settings to disk + # without this, recent settings changes could be missing after the microsd card was removed + settings_file.flush() + os.fsync(settings_file.fileno()) - def update(self, new_settings: dict, disable_missing_entries: bool = True): + def update(self, new_settings: dict): """ - * disable_missing_entries: The SettingsQR Generator omits any multiselect - fields with zero selections or disabled Enabled/Disabled toggles. So if a - field is missing, interpret it as such. But if this is set to False, keep - the existing value for the field; most likely this is a new setting that - the user may not have a value for when loading their persistent settings, - in which case this would preserve the new field's default value. + Replaces the current settings with the incoming dict. + + If a setting is missing from `new_settings`: + * Hidden settings that have a value remain as-is. + * All other missing settings are set to their default value. """ for entry in SettingsDefinition.settings_entries: if entry.attr_name not in new_settings: - if not disable_missing_entries: + if entry.visibility == SettingsConstants.VISIBILITY__HIDDEN and entry.attr_name in self._data: + # Preserve existing hidden values + new_settings[entry.attr_name] = self._data[entry.attr_name] + else: # Setting is missing; insert default new_settings[entry.attr_name] = entry.default_value - - elif entry.visibility == SettingsConstants.VISIBILITY__HIDDEN: - # Missing hidden values always get their default - new_settings[entry.attr_name] = entry.default_value - - elif entry.type == SettingsConstants.TYPE__MULTISELECT: - # Clear out the multiselect - new_settings[entry.attr_name] = [] - - elif entry.type in SettingsConstants.ALL_ENABLED_DISABLED_TYPES: - # Set DISABLED for this missing setting - new_settings[entry.attr_name] = SettingsConstants.OPTION__DISABLED else: # Clean the incoming data, if necessary @@ -88,9 +151,6 @@ class Settings(Singleton): if type(new_settings[entry.attr_name]) == str: # Break comma-separated SettingsQR input into List new_settings[entry.attr_name] = new_settings[entry.attr_name].split(",") - - # TODO: If value is not in entry.selection_options... - for key, value in new_settings.items(): self.set_value(key, value) @@ -113,9 +173,12 @@ class Settings(Singleton): # Special handling for toggling persistence if attr_name == SettingsConstants.SETTING__PERSISTENT_SETTINGS and value == SettingsConstants.OPTION__DISABLED: - os.remove(self.SETTINGS_FILENAME) - print(f"Removed {self.SETTINGS_FILENAME}") - + try: + os.remove(self.SETTINGS_FILENAME) + print(f"Removed {self.SETTINGS_FILENAME}") + except: + print(f"{self.SETTINGS_FILENAME} not found to be removed") + self._data[attr_name] = value self.save() @@ -165,8 +228,11 @@ class Settings(Singleton): raise Exception(f"Unsupported SettingsEntry.type: {settings_entry.type}") display_names = [] - for value in self._data[attr_name]: - display_names.append(settings_entry.get_selection_option_display_name_by_value(value)) + # Iterate through the selection_options list in order to preserve intended sort + # order when adding which options are selected. + for value, display_name in settings_entry.selection_options: + if value in self._data[attr_name]: + display_names.append(display_name) return display_names @@ -192,3 +258,39 @@ class Settings(Singleton): return self._data[SettingsConstants.SETTING__DEBUG] == SettingsConstants.OPTION__ENABLED + def handle_microsd_state_change(action: str): + """ + Enables/Disables the Persistent Settings option based on the MicroSD card state. + """ + from seedsigner.hardware.microsd import MicroSD + + if Settings.HOSTNAME == Settings.SEEDSIGNER_OS: + if action == MicroSD.ACTION__INSERTED: + # SD card was just inserted. + # Restore persistent settings back to defaults + entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__PERSISTENT_SETTINGS) + entry.selection_options = SettingsConstants.OPTIONS__ENABLED_DISABLED + entry.help_text = SettingsConstants.PERSISTENT_SETTINGS__SD_INSERTED__HELP_TEXT + + # TODO: Perhaps prompt the user if the current settings (not including persistent + # settings) should overwrite the settings on disk, if they differ: + # - Overwrite settings on the SD? + # - Load settings from SD? + # if Settings file exists (meaning persistent settings was previously enabled), write out current settings to disk + if os.path.exists(Settings.SETTINGS_FILENAME): + # enable persistent settings first, then save + Settings.get_instance()._data[SettingsConstants.SETTING__PERSISTENT_SETTINGS] = SettingsConstants.OPTION__ENABLED + Settings.get_instance().save() + + elif action == MicroSD.ACTION__REMOVED: + # SD card was just removed. + # Set persistent settings to disabled value directly + Settings.get_instance()._data[SettingsConstants.SETTING__PERSISTENT_SETTINGS] = SettingsConstants.OPTION__DISABLED + + # set persistent settings to only have disabled as an option, adding additional help text that microSD is removed + entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__PERSISTENT_SETTINGS) + entry.selection_options = SettingsConstants.OPTIONS__ONLY_DISABLED + entry.help_text = SettingsConstants.PERSISTENT_SETTINGS__SD_REMOVED__HELP_TEXT + + else: + raise Exception(f"Invalid MicroSD action: {action}") diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index a0be4472..df90695d 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -14,6 +14,9 @@ class SettingsConstants: (OPTION__ENABLED, _("Enabled")), (OPTION__DISABLED, _("Disabled")), ] + OPTIONS__ONLY_DISABLED = [ + (OPTION__DISABLED, _("Disabled")), + ] OPTIONS__PROMPT_REQUIRED_DISABLED = [ (OPTION__PROMPT, _("Prompt")), (OPTION__REQUIRED, _("Required")), @@ -34,11 +37,13 @@ class SettingsConstants: COORDINATOR__NUNCHUK = "nun" COORDINATOR__SPARROW = "spa" COORDINATOR__SPECTER_DESKTOP = "spd" + COORDINATOR__KEEPER = "kpr" ALL_COORDINATORS = [ (COORDINATOR__BLUE_WALLET, "BlueWallet"), (COORDINATOR__NUNCHUK, "Nunchuk"), (COORDINATOR__SPARROW, "Sparrow"), (COORDINATOR__SPECTER_DESKTOP, "Specter Desktop"), + (COORDINATOR__KEEPER, "Keeper"), ] LOCALE__ARABIC = "ar" @@ -53,17 +58,17 @@ class SettingsConstants: LOCALE__RUSSIAN = "ru" LOCALE__SPANISH = "es" ALL_LOCALES = [ - (LOCALE__ARABIC, "Arabic"), - (LOCALE__CZECH, "Czech"), + # (LOCALE__ARABIC, "Arabic"), + (LOCALE__CZECH, "čeština"), (LOCALE__ENGLISH, "English"), - (LOCALE__FRENCH, "French"), - (LOCALE__GERMAN, "German"), - (LOCALE__HEBREW, "Hebrew"), - (LOCALE__JAPANESE, "Japanese"), - (LOCALE__KOREAN, "Korean"), - (LOCALE__PORTUGUESE, "Portuguese"), - (LOCALE__RUSSIAN, "Russian"), - (LOCALE__SPANISH, "Spanish"), + (LOCALE__FRENCH, "Français"), + (LOCALE__GERMAN, "Deutsch"), + # (LOCALE__HEBREW, "Hebrew"), + # (LOCALE__JAPANESE, "Japanese"), + # (LOCALE__KOREAN, "Korean"), + (LOCALE__PORTUGUESE, "Português"), + (LOCALE__RUSSIAN, "русский"), + (LOCALE__SPANISH, "Español"), ] BTC_DENOMINATION__BTC = "btc" @@ -71,10 +76,10 @@ class SettingsConstants: BTC_DENOMINATION__THRESHOLD = "thr" BTC_DENOMINATION__BTCSATSHYBRID = "hyb" ALL_BTC_DENOMINATIONS = [ - (BTC_DENOMINATION__BTC, _("Btc-only")), - (BTC_DENOMINATION__SATS, _("Sats-only")), - (BTC_DENOMINATION__THRESHOLD, _("Threshold at 0.01")), - (BTC_DENOMINATION__BTCSATSHYBRID, _("Btc | Sats hybrid")), + (BTC_DENOMINATION__BTC, "BTC"), + (BTC_DENOMINATION__SATS, "sats"), + (BTC_DENOMINATION__THRESHOLD, "Threshold at 0.01"), + (BTC_DENOMINATION__BTCSATSHYBRID, "BTC | sats hybrid"), ] CAMERA_ROTATION__0 = 0 @@ -117,6 +122,8 @@ class SettingsConstants: if network == SettingsConstants.REGTEST: return "regtest" + PERSISTENT_SETTINGS__SD_INSERTED__HELP_TEXT = _("Store Settings on SD card") + PERSISTENT_SETTINGS__SD_REMOVED__HELP_TEXT = _("Insert SD card to enable") SINGLE_SIG = "ss" MULTISIG = "ms" @@ -131,10 +138,10 @@ class SettingsConstants: TAPROOT = "tr" CUSTOM_DERIVATION = "cus" ALL_SCRIPT_TYPES = [ - (NATIVE_SEGWIT, "Native Segwit"), - (NESTED_SEGWIT, "Nested Segwit (legacy)"), - (TAPROOT, "Taproot"), - (CUSTOM_DERIVATION, "Custom Derivation"), + (NATIVE_SEGWIT, _("Native Segwit")), + (NESTED_SEGWIT, _("Nested Segwit (legacy)")), + (TAPROOT, _("Taproot")), + (CUSTOM_DERIVATION, _("Custom Derivation")), ] WORDLIST_LANGUAGE__ENGLISH = "en" @@ -173,9 +180,11 @@ class SettingsConstants: SETTING__PASSPHRASE = "passphrase" SETTING__CAMERA_ROTATION = "camera_rotation" SETTING__COMPACT_SEEDQR = "compact_seedqr" + SETTING__BIP85_CHILD_SEEDS = "bip85_child_seeds" + SETTING__MESSAGE_SIGNING = "message_signing" SETTING__PRIVACY_WARNINGS = "privacy_warnings" SETTING__DIRE_WARNINGS = "dire_warnings" - SETTING__PASSPHRASE_WARNING = "passphrase_warning" + SETTING__QR_BRIGHTNESS_TIPS = "qr_brightness_tips" SETTING__PARTNER_LOGOS = "partner_logos" SETTING__DEBUG = "debug" @@ -228,12 +237,11 @@ class SettingsEntry: category: str attr_name: str display_name: str - verbose_name: str = None abbreviated_name: str = None visibility: str = SettingsConstants.VISIBILITY__GENERAL type: str = SettingsConstants.TYPE__ENABLED_DISABLED help_text: str = None - selection_options: List[str] = None + selection_options: list[tuple[str | int], str] = None default_value: Any = None def __post_init__(self): @@ -244,7 +252,7 @@ class SettingsEntry: self.selection_options = SettingsConstants.OPTIONS__ENABLED_DISABLED_PROMPT elif self.type == SettingsConstants.TYPE__ENABLED_DISABLED_PROMPT_REQUIRED: - self.selection_options = [SettingsConstants.ALL_OPTIONS] + self.selection_options = SettingsConstants.ALL_OPTIONS # Account for List[tuple] and tuple formats as default_value if type(self.default_value) == list and type(self.default_value[0]) == tuple: @@ -263,18 +271,12 @@ class SettingsEntry: def get_selection_option_value(self, i: int): + """ Returns the value of the selection option at index `i` """ value = self.selection_options[i] if type(value) == tuple: value = value[0] return value - - def get_selection_option_display_name(self, i: int) -> str: - value = self.selection_options[i] - if type(value) == tuple: - value = value[1] - return value - def get_selection_option_display_name_by_value(self, value) -> str: for option in self.selection_options: @@ -320,9 +322,8 @@ class SettingsEntry: return { "category": self.category, "attr_name": self.attr_name, - "display_name": self.display_name, - "verbose_name": self.verbose_name, "abbreviated_name": self.abbreviated_name, + "display_name": self.display_name, "visibility": self.visibility, "type": self.type, "help_text": self.help_text, @@ -355,6 +356,7 @@ class SettingsDefinition: SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM, attr_name=SettingsConstants.SETTING__LOCALE, display_name=_("Language"), + abbreviated_name="lang", type=SettingsConstants.TYPE__SELECT_1, selection_options=SettingsConstants.ALL_LOCALES, default_value=SettingsConstants.LOCALE__ENGLISH), @@ -362,6 +364,7 @@ class SettingsDefinition: # TODO: Support other bip-39 wordlist languages! Until then, type == HIDDEN SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM, attr_name=SettingsConstants.SETTING__WORDLIST_LANGUAGE, + abbreviated_name="wordlist_lang", display_name=_("Mnemonic language"), type=SettingsConstants.TYPE__SELECT_1, visibility=SettingsConstants.VISIBILITY__HIDDEN, @@ -370,19 +373,27 @@ class SettingsDefinition: SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM, attr_name=SettingsConstants.SETTING__PERSISTENT_SETTINGS, + abbreviated_name="persistent", display_name=_("Persistent settings"), - help_text=_("Store Settings on SD card."), + help_text=SettingsConstants.PERSISTENT_SETTINGS__SD_INSERTED__HELP_TEXT, default_value=SettingsConstants.OPTION__DISABLED), SettingsEntry(category=SettingsConstants.CATEGORY__WALLET, attr_name=SettingsConstants.SETTING__COORDINATORS, + abbreviated_name="coords", display_name=_("Coordinator software"), type=SettingsConstants.TYPE__MULTISELECT, selection_options=SettingsConstants.ALL_COORDINATORS, - default_value=SettingsConstants.ALL_COORDINATORS), + default_value=[ + SettingsConstants.COORDINATOR__BLUE_WALLET, + SettingsConstants.COORDINATOR__NUNCHUK, + SettingsConstants.COORDINATOR__SPARROW, + SettingsConstants.COORDINATOR__SPECTER_DESKTOP, + ]), SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM, attr_name=SettingsConstants.SETTING__BTC_DENOMINATION, + abbreviated_name="denom", display_name=_("Denomination display"), type=SettingsConstants.TYPE__SELECT_1, selection_options=SettingsConstants.ALL_BTC_DENOMINATIONS, @@ -414,6 +425,7 @@ class SettingsDefinition: SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, attr_name=SettingsConstants.SETTING__SIG_TYPES, + abbreviated_name="sigs", display_name=_("Sig types"), type=SettingsConstants.TYPE__MULTISELECT, visibility=SettingsConstants.VISIBILITY__ADVANCED, @@ -422,6 +434,7 @@ class SettingsDefinition: SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, attr_name=SettingsConstants.SETTING__SCRIPT_TYPES, + abbreviated_name="scripts", display_name=_("Script types"), type=SettingsConstants.TYPE__MULTISELECT, visibility=SettingsConstants.VISIBILITY__ADVANCED, @@ -444,38 +457,55 @@ class SettingsDefinition: SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, attr_name=SettingsConstants.SETTING__CAMERA_ROTATION, + abbreviated_name="camera", display_name=_("Camera rotation"), type=SettingsConstants.TYPE__SELECT_1, visibility=SettingsConstants.VISIBILITY__ADVANCED, selection_options=SettingsConstants.ALL_CAMERA_ROTATIONS, - default_value=SettingsConstants.CAMERA_ROTATION__0), + default_value=SettingsConstants.CAMERA_ROTATION__180), SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, attr_name=SettingsConstants.SETTING__COMPACT_SEEDQR, display_name=_("Compact SeedQR"), visibility=SettingsConstants.VISIBILITY__ADVANCED, + default_value=SettingsConstants.OPTION__ENABLED), + + SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, + attr_name=SettingsConstants.SETTING__BIP85_CHILD_SEEDS, + abbreviated_name="bip85", + display_name=_("BIP-85 child seeds"), + visibility=SettingsConstants.VISIBILITY__ADVANCED, + default_value=SettingsConstants.OPTION__DISABLED), + + SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, + attr_name=SettingsConstants.SETTING__MESSAGE_SIGNING, + display_name=_("Message signing"), + visibility=SettingsConstants.VISIBILITY__ADVANCED, default_value=SettingsConstants.OPTION__DISABLED), SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, attr_name=SettingsConstants.SETTING__PRIVACY_WARNINGS, + abbreviated_name="priv_warn", display_name=_("Show privacy warnings"), visibility=SettingsConstants.VISIBILITY__ADVANCED, default_value=SettingsConstants.OPTION__ENABLED), SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, attr_name=SettingsConstants.SETTING__DIRE_WARNINGS, + abbreviated_name="dire_warn", display_name=_("Show dire warnings"), visibility=SettingsConstants.VISIBILITY__ADVANCED, default_value=SettingsConstants.OPTION__ENABLED), SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, - attr_name=SettingsConstants.SETTING__PASSPHRASE_WARNING, - display_name=_("Show passphrase warning"), + attr_name=SettingsConstants.SETTING__QR_BRIGHTNESS_TIPS, + display_name=_("Show QR brightness tips"), visibility=SettingsConstants.VISIBILITY__ADVANCED, default_value=SettingsConstants.OPTION__ENABLED), SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, attr_name=SettingsConstants.SETTING__PARTNER_LOGOS, + abbreviated_name="partners", display_name=_("Show partner logos"), visibility=SettingsConstants.VISIBILITY__ADVANCED, default_value=SettingsConstants.OPTION__ENABLED), @@ -491,18 +521,19 @@ class SettingsDefinition: # "Hidden" settings with no UI interaction SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM, attr_name=SettingsConstants.SETTING__QR_BRIGHTNESS, + abbreviated_name="qr_brightness", display_name=_("QR background color"), type=SettingsConstants.TYPE__FREE_ENTRY, visibility=SettingsConstants.VISIBILITY__HIDDEN, - default_value=189), + default_value=62), ] @classmethod - def get_settings_entries(cls, visibiilty: str = SettingsConstants.VISIBILITY__GENERAL) -> List[SettingsEntry]: + def get_settings_entries(cls, visibility: str = SettingsConstants.VISIBILITY__GENERAL) -> List[SettingsEntry]: entries = [] for entry in cls.settings_entries: - if entry.visibility == visibiilty: + if entry.visibility == visibility: entries.append(entry) return entries @@ -514,6 +545,13 @@ class SettingsDefinition: return entry + @classmethod + def get_settings_entry_by_abbreviated_name(cls, abbreviated_name: str) -> SettingsEntry: + for entry in cls.settings_entries: + if abbreviated_name in [entry.abbreviated_name, entry.attr_name]: + return entry + + @classmethod def get_defaults(cls) -> dict: as_dict = {} @@ -541,7 +579,14 @@ class SettingsDefinition: if __name__ == "__main__": import json + import os - output_file = "settings_definition.json" + hostname = os.uname()[1] + + if hostname == "seedsigner-os": + output_file = "/mnt/microsd/settings_definition.json" + else: + output_file = "settings_definition.json" + with open(output_file, 'w') as json_file: json.dump(SettingsDefinition.to_dict(), json_file, indent=4) diff --git a/src/seedsigner/resources/babel/ar/LC_MESSAGES/messages.po b/src/seedsigner/resources/babel/ar/LC_MESSAGES/messages.po index b1082056..f725d07c 100644 --- a/src/seedsigner/resources/babel/ar/LC_MESSAGES/messages.po +++ b/src/seedsigner/resources/babel/ar/LC_MESSAGES/messages.po @@ -1,31 +1,213 @@ -# Translations template for PROJECT. +# Arabic translations for PROJECT. # Copyright (C) 2022 ORGANIZATION # This file is distributed under the same license as the PROJECT project. # FIRST AUTHOR , 2022. -# +# # Translators: # Best Money, 2022 -# +# jdlcdl, 2022 msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-05-04 08:13-0500\n" +"POT-Creation-Date: 2023-10-09 11:36-0400\n" "PO-Revision-Date: 2022-04-29 13:50+0000\n" "Last-Translator: Best Money, 2022\n" -"Language-Team: Arabic (https://www.transifex.com/seedsigner/teams/135832/ar/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.10.1\n" "Language: ar\n" +"Language-Team: Arabic (https://www.transifex.com/seedsigner/teams/135832/ar/)\n" "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.10.3\n" -#: src/seedsigner/gui/screens/main_menu_screens.py:17 +#. The abbreviated label for the special key on a standard keyboard. +#: src/seedsigner/gui/keyboard.py +msgid "del" +msgstr "" + +#. The abbreviated label for the special key on a standard keyboard. +#: src/seedsigner/gui/keyboard.py +msgid "space" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "" +"You can remove\n" +"the SD card now" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "SD card removed" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "SD card inserted" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review PSBT" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review Details" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "1 input" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input 1" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input 2" +msgstr "" + +#. Indicates that items have been omitted from a series: e.g. "1, 2, 3, [...], +#. 8" +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "[ ... ]" +msgstr "" + +#. Input number will be inserted (e.g. "input 3") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input {}" +msgstr "" + +#. Ellipsis ("...") characters used to truncate an address (e.g. "bc1qabc...") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "..." +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/views/psbt_views.py +msgid "self-transfer" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient 1" +msgstr "" + +#. Inserts the recipient number (e.g. the fifth one is: "recipient 5") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient {}" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "fee" +msgstr "" + +#. Label for a change output in the PSBT Overview flow diagram +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/views/psbt_views.py +msgid "change" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "PSBT Math" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review Recipients" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "btc" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "sats" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input" +msgid_plural "inputs" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" +msgstr[4] "" +msgstr[5] "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient" +msgid_plural "recipients" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" +msgstr[4] "" +msgstr[5] "" + +#. Denonination is inserted (e.g. your "btc change" or "sats change") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "{} change" +msgstr "" + +#. First variable is either "Multisig" or the fingerprint for single sig; +#. Second is "Change" or "Addr" (change vs receive addr); Third is the address +#. index number (e.g. your #4 receive addr). +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/models/settings_definition.py +#: src/seedsigner/views/seed_views.py +msgid "Multisig" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Change" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Addr" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Address verified!" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Sign PSBT" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Click to approve this transaction" +msgstr "" + +#: src/seedsigner/gui/screens/scan_screens.py +msgid "< back | " +msgstr "" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "Success!" +msgstr "" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "OK" +msgstr "" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/psbt_views.py +msgid "Caution" +msgstr "" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "Privacy Leak!" +msgstr "" + +#: src/seedsigner/gui/screens/screen.py +msgid "I Understand" +msgstr "" + +#: src/seedsigner/gui/screens/screen.py +msgid "Classified Info!" +msgstr "" + +#: src/seedsigner/gui/screens/screen.py msgid "Restarting" msgstr "يتم إعادة التشغيل" -#: src/seedsigner/gui/screens/main_menu_screens.py:22 +#: src/seedsigner/gui/screens/screen.py msgid "" "SeedSigner is restarting.\n" "\n" @@ -35,115 +217,1200 @@ msgstr "" "\n" "سيتم مسح جميع البيانات من الذاكرة" -#: src/seedsigner/gui/screens/main_menu_screens.py:32 +#: src/seedsigner/gui/screens/screen.py msgid "Powering Off" msgstr "يتم إيقاف التشغيل" -#: src/seedsigner/gui/screens/main_menu_screens.py:37 +#: src/seedsigner/gui/screens/screen.py msgid "Please wait about 30 seconds before disconnecting power." msgstr "يرجى الانتظار 30 ثانية تقريبا قبل فصل الطاقة." -#: src/seedsigner/gui/screens/settings_screens.py:26 -#: src/seedsigner/views/main_menu_views.py:21 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Finalize Seed" +msgstr "" + +#. a label for the shortened Key-id of a BIP-32 master HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py +msgid "fingerprint" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Backup Seed" +msgstr "" + +#. Additional explainer for the two seed backup options (mnemonic phrase and +#. SeedQR). +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Backups do not include your passphrase." +msgstr "" + +#. Displays the page number and total: (e.g. page 1 of 6) +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Seed Words: {}/{}" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "BIP-85 Index" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Verify Backup?" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Optionally verify that your mnemonic backup is correct." +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Derivation Path" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Export Xpub" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Xpub Details" +msgstr "" + +#. Short for "BIP32 Master Fingerprint" +#. a label for the shortened Key-id of a BIP-32 master HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Fingerprint" +msgstr "" + +#. Short for "Derivation Path" +#. a label for the derivation-path into a BIP-32 HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Derivation" +msgstr "" + +#. Short for "BIP32 Extended Public Key" +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Xpub" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "BIP-39 Passphrase" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Verify Passphrase" +msgstr "" + +#. Describes the effect of applying a BIP-39 passphrase; it changes the seed's +#. fingerprint +#: src/seedsigner/gui/screens/seed_screens.py +msgid "changes fingerprint" +msgstr "" + +#. Refers to the SeedQR type: Standard or Compact +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Standard" +msgstr "" + +#. Briefly explains the Standard SeedQR data format +#: src/seedsigner/gui/screens/seed_screens.py +msgid "BIP-39 wordlist indices" +msgstr "" + +#. Refers to the SeedQR type: Standard or Compact +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Compact" +msgstr "" + +#. Briefly explains the Compact SeedQR data format +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Raw entropy bits" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Transcribe SeedQR" +msgstr "" + +#. Refers to the QR code size: 21x21, 25x25, or 29x29 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Begin {}x{}" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "click to exit" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Optionally scan your transcribed SeedQR to confirm that it reads back correctly." +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Verify Address" +msgstr "" + +#. Inserts the nth address number (e.g. "Checking address 7") +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Checking address {}" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Multisig Verification" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Load your multisig wallet descriptor to verify your receive/self-transfer or change address." +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Descriptor Loaded" +msgstr "" + +#. Label for the multisig wallet's signing policy (e.g. 2-of-3) +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Policy" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Signing Keys" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Review Message" +msgstr "" + +#. Short for "Next step" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +#: src/seedsigner/views/tools_views.py +msgid "Next" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Confirm Address" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Sign Message" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "derivation path" +msgstr "" + +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py msgid "Settings" msgstr "إعدادات" -#: src/seedsigner/gui/screens/settings_screens.py:57 +#. Short for "Input/Output"; screen to make sure the buttons and camera are +#. working properly +#: src/seedsigner/gui/screens/settings_screens.py msgid "I/O Test" msgstr "فحص المدخل/المخرج" -#: src/seedsigner/gui/screens/settings_screens.py:138 -#: src/seedsigner/gui/screens/settings_screens.py:219 +#. Blank the screen +#: src/seedsigner/gui/screens/settings_screens.py msgid "Clear" msgstr "مسح" -#: src/seedsigner/gui/screens/settings_screens.py:159 +#: src/seedsigner/gui/screens/settings_screens.py msgid "Exit" msgstr "خروج" -#: src/seedsigner/gui/screens/settings_screens.py:173 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Capturing image..." msgstr "يتم التصوير..." -#: src/seedsigner/gui/screens/settings_screens.py:286 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py msgid "Donate" msgstr "للتبرع" -#: src/seedsigner/gui/screens/settings_screens.py:290 +#. If your language uses the percent sign ("%"), your translation must also use +#. two percent signs ("%%") due to python formatting oddities. "100%%" will be +#. rendered as "100%". +#: src/seedsigner/gui/screens/settings_screens.py #, python-format msgid "" -"SeedSigner is free & open source, funded solely by the Bitcoin community.\n" +"SeedSigner is 100%% free & open source, funded solely by the Bitcoin community.\n" "\n" "Donate onchain or LN at:" msgstr "" -"SeedSigner هو مجاني 100% ومفتوح المصدر، مُمول فقط من مجتمع البيتكوين.\n" -"\n" -"للتبرع عن طريق البيتكوين وشبكة البرق:" -#: src/seedsigner/models/settings_definition.py:14 -msgid "Enabled" -msgstr "مُفعل" +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py +msgid "Settings QR" +msgstr "" -#: src/seedsigner/models/settings_definition.py:15 -#: src/seedsigner/models/settings_definition.py:20 -msgid "Disabled" -msgstr "مُعطل" - -#: src/seedsigner/models/settings_definition.py:18 -#: src/seedsigner/models/settings_definition.py:26 -msgid "Prompt" -msgstr "تنبيه" - -#: src/seedsigner/models/settings_definition.py:19 -#: src/seedsigner/models/settings_definition.py:23 -#: src/seedsigner/models/settings_definition.py:29 -msgid "Required" -msgstr "إلزامي" - -#: src/seedsigner/models/settings_definition.py:346 -msgid "Language" -msgstr "اللغة" - -#: src/seedsigner/models/settings_definition.py:354 -msgid "Mnemonic language" -msgstr "لغة الكلمات التذكيرية" - -#: src/seedsigner/models/settings_definition.py:362 -msgid "Persistent settings" -msgstr "تثبيت الإعدادات" - -#: src/seedsigner/models/settings_definition.py:368 -msgid "Coordinator software" -msgstr "برنامج التنسيق" - -#: src/seedsigner/models/settings_definition.py:375 -msgid "Denomination display" -msgstr "طريقة عرض فئة النقد" - -#: src/seedsigner/views/main_menu_views.py:18 -msgid "Scan" -msgstr "للمسح ضوئيًا" - -#: src/seedsigner/views/main_menu_views.py:19 -msgid "Seeds" -msgstr "المفاتيح" - -#: src/seedsigner/views/main_menu_views.py:20 -msgid "Tools" -msgstr "أدوات" - -#: src/seedsigner/views/main_menu_views.py:25 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py msgid "Home" msgstr "الرئيسية" -#: src/seedsigner/views/main_menu_views.py:42 +#. Instructions when reviewing an image; "reshoot" must point left, "accept" +#. must point right +#: src/seedsigner/gui/screens/tools_screens.py +msgid " < reshoot | accept > " +msgstr "" + +#. current roll number vs total rolls (e.g. roll 7 of 50) +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Dice Roll {}/{}" +msgstr "" + +#. Build the last word in a 12 or 24 word BIP-39 mnemonic seed phrase. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Build Final Word" +msgstr "" + +#. Number of BIP-39 seed words, and the entropy -- in bits, contained within. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "The {}th word is built from {} more entropy bits plus auto-calculated checksum." +msgstr "" + +#. current coin-flip number vs total flips (e.g. flip 3 of 4) +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Coin Flip {}/{}" +msgstr "" + +#. How we call the "front" side result during a coin toss. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Heads = 1" +msgstr "" + +#. How we call the "back" side result during a coin toss. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Tails = 0" +msgstr "" + +#. A labeled presentation of a user-supplied value +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Your input: \"{}\"" +msgstr "" + +#. A function of "x" to be used for detecting errors in "x" +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Checksum" +msgstr "" + +#. labeled presentation of the last word in a BIP-39 mnemonic seed phrase. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Final Word: \"{}\"" +msgstr "" + +#. a label for the last word of a 12-word BIP-39 mnemonic seed phrase +#: src/seedsigner/gui/screens/tools_screens.py +msgid "12th Word" +msgstr "" + +#. a label for the last word of a 24-word BIP-39 mnemonic seed phrase +#: src/seedsigner/gui/screens/tools_screens.py +msgid "24th Word" +msgstr "" + +#. a label for the tool to explore public addresses for this seed. +#: src/seedsigner/gui/screens/tools_screens.py +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py +msgid "Address Explorer" +msgstr "" + +#. a label for a BIP-380-ish Output Descriptor +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Wallet descriptor" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Enabled" +msgstr "مُفعل" + +#: src/seedsigner/models/settings_definition.py +msgid "Disabled" +msgstr "مُعطل" + +#: src/seedsigner/models/settings_definition.py +msgid "Prompt" +msgstr "تنبيه" + +#: src/seedsigner/models/settings_definition.py +msgid "Required" +msgstr "إلزامي" + +#. Translator is expected ONLY to change their own locale below +#: src/seedsigner/models/settings_definition.py +msgid "Arabic" +msgstr "عربي" + +#: src/seedsigner/models/settings_definition.py +msgid "Czech" +msgstr "čeština" + +#: src/seedsigner/models/settings_definition.py +msgid "French" +msgstr "Français" + +#: src/seedsigner/models/settings_definition.py +msgid "German" +msgstr "Deutsch" + +#: src/seedsigner/models/settings_definition.py +msgid "Hebrew" +msgstr "עִברִית" + +#: src/seedsigner/models/settings_definition.py +msgid "Japanese" +msgstr "日本" + +#: src/seedsigner/models/settings_definition.py +msgid "Korean" +msgstr "한국인" + +#: src/seedsigner/models/settings_definition.py +msgid "Portuguese" +msgstr "Português" + +#: src/seedsigner/models/settings_definition.py +msgid "Russian" +msgstr "Русский" + +#: src/seedsigner/models/settings_definition.py +msgid "Spanish" +msgstr "Español" + +#: src/seedsigner/models/settings_definition.py +msgid "0°" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "90°" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "180°" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "270°" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Low" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Medium" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "High" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Mainnet" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Testnet" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Regtest" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Store Settings on SD card" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Insert SD card to enable" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +#: src/seedsigner/views/seed_views.py +msgid "Single Sig" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Native Segwit" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Nested Segwit (legacy)" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Taproot" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Custom Derivation" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Language" +msgstr "اللغة" + +#: src/seedsigner/models/settings_definition.py +msgid "Mnemonic language" +msgstr "لغة الكلمات التذكيرية" + +#: src/seedsigner/models/settings_definition.py +msgid "Persistent settings" +msgstr "تثبيت الإعدادات" + +#: src/seedsigner/models/settings_definition.py +msgid "Coordinator software" +msgstr "برنامج التنسيق" + +#: src/seedsigner/models/settings_definition.py +msgid "Denomination display" +msgstr "طريقة عرض فئة النقد" + +#: src/seedsigner/models/settings_definition.py +msgid "Bitcoin network" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "QR code density" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Xpub export" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Sig types" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Script types" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Show xpub details" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "BIP-39 passphrase" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Camera rotation" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Compact SeedQR" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "BIP-85 child seeds" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Message signing" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Show privacy warnings" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Show dire warnings" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Show QR brightness tips" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Show partner logos" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "QR background color" +msgstr "" + +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +msgid "Scan a seed" +msgstr "" + +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +msgid "Enter 12-word seed" +msgstr "" + +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +msgid "Enter 24-word seed" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Select Signer" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Parsing PSBT..." +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Unsupported Script Type!" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "PSBT has unsupported input script type, please verify your change addresses." +msgstr "" + +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/view.py +msgid "Continue" +msgstr "" + +#. User will receive no change back; the inputs to this transaction are fully +#. spent +#: src/seedsigner/views/psbt_views.py +msgid "Full Spend!" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "This PSBT spends its entire input value. No change is coming back to your wallet." +msgstr "" + +#. Future-tense used to indicate that this transaction will send this amount, +#. as opposed to "Send" on its own which could be misread as an instant command +#. (e.g. "Send Now"). +#: src/seedsigner/views/psbt_views.py +msgid "Will Send" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Next Recipient" +msgstr "" + +#. The amount you're receiving back from the transaction +#: src/seedsigner/views/psbt_views.py +msgid "Your Change" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Verify Multisig Change" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Self-Transfer" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Verify Multisig Addr" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Verifying Change..." +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Verifying Self-Transfer..." +msgstr "" + +#. Variable is either "change" or "self-transfer". +#: src/seedsigner/views/psbt_views.py +msgid "PSBT's {} address could not be verified with your multisig wallet descriptor." +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Suspicious PSBT" +msgstr "" + +#. Variable is either "change" or "self-transfer". +#: src/seedsigner/views/psbt_views.py +msgid "PSBT's {} address could not be generated from your seed." +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Address Verification Failed" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Discard PSBT" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Approve PSBT" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Select Diff Seed" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "PSBT Error" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Signing Failed" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Signing with this seed did not add a valid signature." +msgstr "" + +#. This is on the opening splash screen, displayed above the HRF logo +#: src/seedsigner/views/screensaver.py +msgid "With support from:" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Load a seed" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "In-Memory Seeds" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Load the seed to verify" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Select seed to verify" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Load the seed to sign with" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Select seed to sign with" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Scan a SeedQR" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Create a seed" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Load A Seed" +msgstr "" + +#. Inserts the word number (e.g. "Seed Word #6") +#: src/seedsigner/views/seed_views.py +msgid "Seed Word #{}" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Review & Edit" +msgstr "" + +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py +msgid "Discard" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Invalid Mnemonic!" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Checksum failure; not a valid seed phrase." +msgstr "" + +#: src/seedsigner/views/seed_views.py src/seedsigner/views/view.py +msgid "Done" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Edit passphrase" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Keep Seed" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Discard Seed?" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Wipe seed {} from the device?" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Scan PSBT" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Verify Addr" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Child Seed" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Discard Seed" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "View Seed Words" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Export as SeedQR" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Xpub can be used to view all future transactions." +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Generating xpub..." +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "You must keep your seed words private & away from all online devices." +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Child #{}" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Seed Words" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "12 Words" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "24 Words" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Num Words" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Index Error" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Invalid Child Index" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Child Index must be between 0 and 2^31-1." +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Try Again" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Verify" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Skip" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Verify Word #{}" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Review Seed Words" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Verification Error" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Wrong Word!" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Word #{} is not \"{}\"!" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Backup Verified" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "All mnemonic backup words were successfully verified!" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Standard: 25x25" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Compact: 21x21" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Standard: 29x29" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Compact: 25x25" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "SeedQR Format" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Never photograph or scan it into a device that connects to the internet." +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Confirm SeedQR" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Confirm SeedQR?" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Scan your SeedQR" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Error!" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Your transcribed SeedQR does not match your original seed!" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Review SeedQR" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Your transcribed SeedQR successfully scanned and yielded the same seed." +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Your transcribed SeedQR could not be read!" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Sig type can't be auto-detected from this address. Please specify:" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Can't validate a single sig addr without specifying a seed" +msgstr "" + +#. Option when scanning for a matching address; skips ten addresses ahead +#: src/seedsigner/views/seed_views.py +msgid "Skip 10" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Cancel" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "multisig" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "seed {}" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Address Verified" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Scan Descriptor" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Return to PSBT" +msgstr "" + +#: src/seedsigner/views/settings_views.py +msgid "I/O test" +msgstr "" + +#: src/seedsigner/views/settings_views.py +msgid "Advanced" +msgstr "" + +#: src/seedsigner/views/settings_views.py +msgid "Dev Options" +msgstr "" + +#: src/seedsigner/views/settings_views.py +msgid "Persistent Settings enabled. Settings saved to SD card." +msgstr "" + +#: src/seedsigner/views/settings_views.py +msgid "Settings updated in temporary memory" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "New seed" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Calc 12th/24th word" +msgstr "" + +#: src/seedsigner/views/tools_views.py src/seedsigner/views/view.py +msgid "Tools" +msgstr "أدوات" + +#: src/seedsigner/views/tools_views.py +msgid "12 words" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "24 words" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Mnemonic Length?" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "12 words (50 rolls)" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "24 words (99 rolls)" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Mnemonic Length" +msgstr "" + +#. Label to gather entropy through coin tosses +#: src/seedsigner/views/tools_views.py +msgid "Coin flip entropy" +msgstr "" + +#. Label to gather entropy through user specified BIP-39 word +#: src/seedsigner/views/tools_views.py +msgid "Word selection entropy" +msgstr "" + +#. Label to allow user to default entropy as all-zeros +#: src/seedsigner/views/tools_views.py +msgid "Finalize with zeros" +msgstr "" + +#. label to calculate the last word of a BIP-39 mnemonic seed phrase +#: src/seedsigner/views/tools_views.py +msgid "Final Word Calc" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Load seed" +msgstr "" + +#. label for addresses where others send us incoming payments +#: src/seedsigner/views/tools_views.py +msgid "Receive Addresses" +msgstr "" + +#. label for addresses that collect the change from our own outgoing payments +#: src/seedsigner/views/tools_views.py +msgid "Change Addresses" +msgstr "" + +#. a status message that our payment addresses are being calculated +#: src/seedsigner/views/tools_views.py +msgid "Calculating addrs..." +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Next {}" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Receive Addrs" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Change Addrs" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Scan" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Seeds" +msgstr "" + +#: src/seedsigner/views/view.py msgid "Restart" -msgstr "إعادة تشغيل" +msgstr "" -#: src/seedsigner/views/main_menu_views.py:43 +#: src/seedsigner/views/view.py msgid "Power Off" -msgstr "إغلاق" +msgstr "" -#: src/seedsigner/views/main_menu_views.py:46 +#: src/seedsigner/views/view.py msgid "Reset / Power" -msgstr "إعادة/تشغيل" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Work In Progress" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Not Yet Implemented" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "This is still on our to-do list!" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Back to Main Menu" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Error" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Network Mismatch" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Change Setting" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "System Error" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Update Setting" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Option Disabled" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Security Tip" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "" +"For maximum security,\n" +"remove the MicroSD card\n" +"before continuing." +msgstr "" + +#~ msgid "" +#~ "SeedSigner is free & open source, funded solely by the Bitcoin community.\n" +#~ "\n" +#~ "Donate onchain or LN at:" +#~ msgstr "" +#~ "SeedSigner هو مجاني 100% ومفتوح المصدر، مُمول فقط من مجتمع البيتكوين.\n" +#~ "\n" +#~ "للتبرع عن طريق البيتكوين وشبكة البرق:" + +#~ msgid "Add Passphrase" +#~ msgstr "" + +#~ msgid "Btc-only" +#~ msgstr "" + +#~ msgid "Sats-only" +#~ msgstr "" + +#~ msgid "Threshold at 0.01" +#~ msgstr "" + +#~ msgid "Btc | Sats hybrid" +#~ msgstr "" + +#~ msgid "Scan" +#~ msgstr "" + +#~ msgid "Seeds" +#~ msgstr "" + +#~ msgid "Restart" +#~ msgstr "" + +#~ msgid "Power Off" +#~ msgstr "" + +#~ msgid "Reset / Power" +#~ msgstr "إعادة/تشغيل" + +#~ msgid " (?)" +#~ msgstr "" + +#~ msgid "Scan a QR code" +#~ msgstr "" + +#~ msgid "Yes " +#~ msgstr "" + +#~ msgid "No " +#~ msgstr "" + +#~ msgid "< back | Scan a QR code" +#~ msgstr "" + +#~ msgid "Settings QR" +#~ msgstr "" + +#~ msgid "Settings imported successfully!" +#~ msgstr "" + +#~ msgid "MicroSD removed" +#~ msgstr "" + +#~ msgid "MicroSD inserted" +#~ msgstr "" + +#~ msgid "changes seed:" +#~ msgstr "" + +#~ msgid "< back | click joystick" +#~ msgstr "" + +#~ msgid "Store Settings on SD card." +#~ msgstr "" + +#~ msgid "MicroSD card is removed" +#~ msgstr "" + +#~ msgid "Show passphrase warning" +#~ msgstr "" + +#~ msgid "" +#~ "BIP-39 passphrase changes your seed, resulting in new xpubs, wallets, etc.\n" +#~ "Be sure you know what you're doing!" +#~ msgstr "" + +#~ msgid "SeedQR is your private key!" +#~ msgstr "" + +#~ msgid "Never photograph it or scan it into an online device." +#~ msgstr "" + +#~ msgid " New seed" +#~ msgstr "" + +#~ msgid "Scan wallet descriptor" +#~ msgstr "" + diff --git a/src/seedsigner/resources/babel/cs/LC_MESSAGES/messages.po b/src/seedsigner/resources/babel/cs/LC_MESSAGES/messages.po index 68cd2f08..da87508e 100644 --- a/src/seedsigner/resources/babel/cs/LC_MESSAGES/messages.po +++ b/src/seedsigner/resources/babel/cs/LC_MESSAGES/messages.po @@ -1,38 +1,211 @@ -# Translations template for PROJECT. +# Czech translations for PROJECT. # Copyright (C) 2022 ORGANIZATION # This file is distributed under the same license as the PROJECT project. # FIRST AUTHOR , 2022. -# +# # Translators: # Keith Mukai, 2022 # Plasmonic Resonance, 2022 -# +# jdlcdl, 2022 #, fuzzy msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-05-29 11:20-0500\n" +"POT-Creation-Date: 2023-10-09 11:36-0400\n" "PO-Revision-Date: 2022-04-29 13:50+0000\n" "Last-Translator: Plasmonic Resonance, 2022\n" -"Language-Team: Czech (https://www.transifex.com/seedsigner/teams/135832/cs/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.10.1\n" "Language: cs\n" +"Language-Team: Czech (https://www.transifex.com/seedsigner/teams/135832/cs/)\n" "Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.10.3\n" -#: src/seedsigner/gui/screens/main_menu_screens.py:12 -#: src/seedsigner/gui/screens/scan_screens.py:140 -msgid "Home" -msgstr "Nabídka" +#. The abbreviated label for the special key on a standard keyboard. +#: src/seedsigner/gui/keyboard.py +msgid "del" +msgstr "" -#: src/seedsigner/gui/screens/main_menu_screens.py:24 +#. The abbreviated label for the special key on a standard keyboard. +#: src/seedsigner/gui/keyboard.py +msgid "space" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "" +"You can remove\n" +"the SD card now" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "SD card removed" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "SD card inserted" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review PSBT" +msgstr "Kontrola PSBT" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review Details" +msgstr "Kontrola detailů" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "1 input" +msgstr "1 vstup" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input 1" +msgstr "vstup 1" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input 2" +msgstr "vstup 2" + +#. Indicates that items have been omitted from a series: e.g. "1, 2, 3, [...], +#. 8" +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "[ ... ]" +msgstr "[ ... ]" + +#. Input number will be inserted (e.g. "input 3") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input {}" +msgstr "vstup {}" + +#. Ellipsis ("...") characters used to truncate an address (e.g. "bc1qabc...") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "..." +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/views/psbt_views.py +msgid "self-transfer" +msgstr "převod sobě" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient 1" +msgstr "příjemce 1" + +#. Inserts the recipient number (e.g. the fifth one is: "recipient 5") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient {}" +msgstr "příjemce {}" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "fee" +msgstr "poplatek" + +#. Label for a change output in the PSBT Overview flow diagram +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/views/psbt_views.py +msgid "change" +msgstr "nazpět" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "PSBT Math" +msgstr "PSBT rozpis" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review Recipients" +msgstr "Kontrola příjemců" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "btc" +msgstr "btc" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "sats" +msgstr "sats" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input" +msgid_plural "inputs" +msgstr[0] "vstup" +msgstr[1] "vstupy" +msgstr[2] "vstupy" +msgstr[3] "vstupy" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient" +msgid_plural "recipients" +msgstr[0] "příjemce" +msgstr[1] "příjemci" +msgstr[2] "příjemci" +msgstr[3] "příjemci" + +#. Denonination is inserted (e.g. your "btc change" or "sats change") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "{} change" +msgstr "{} nazpět" + +#. First variable is either "Multisig" or the fingerprint for single sig; +#. Second is "Change" or "Addr" (change vs receive addr); Third is the address +#. index number (e.g. your #4 receive addr). +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/models/settings_definition.py +#: src/seedsigner/views/seed_views.py +msgid "Multisig" +msgstr "Multisig" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Change" +msgstr "Nazpět" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Addr" +msgstr "Adr" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Address verified!" +msgstr "Adresa ověřena!" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Sign PSBT" +msgstr "Podepsat PSBT" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Click to approve this transaction" +msgstr "Klikněte pro potvrzení" + +#: src/seedsigner/gui/screens/scan_screens.py +msgid "< back | " +msgstr "" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "Success!" +msgstr "Povedlo se!" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "OK" +msgstr "OK" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/psbt_views.py +msgid "Caution" +msgstr "Pozor" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "Privacy Leak!" +msgstr "Únik soukromých informací!" + +#: src/seedsigner/gui/screens/screen.py +msgid "I Understand" +msgstr "Rozumím" + +#: src/seedsigner/gui/screens/screen.py +msgid "Classified Info!" +msgstr "Tajná informace!" + +#: src/seedsigner/gui/screens/screen.py msgid "Restarting" msgstr "Restartuji" -#: src/seedsigner/gui/screens/main_menu_screens.py:29 +#: src/seedsigner/gui/screens/screen.py msgid "" "SeedSigner is restarting.\n" "\n" @@ -42,387 +215,225 @@ msgstr "" "\n" "Veškerá uložená data budou vymazána." -#: src/seedsigner/gui/screens/main_menu_screens.py:39 +#: src/seedsigner/gui/screens/screen.py msgid "Powering Off" msgstr "Vypínám" -#: src/seedsigner/gui/screens/main_menu_screens.py:44 +#: src/seedsigner/gui/screens/screen.py msgid "Please wait about 30 seconds before disconnecting power." msgstr "Vyčkejte minimálně 30 vteřin než odpojíte napájení." -#: src/seedsigner/gui/screens/psbt_screens.py:29 -#: src/seedsigner/views/seed_views.py:385 -msgid "Review PSBT" -msgstr "Kontrola PSBT" - -#: src/seedsigner/gui/screens/psbt_screens.py:31 -msgid "Review Details" -msgstr "Kontrola detailů" - -#: src/seedsigner/gui/screens/psbt_screens.py:88 -msgid "1 input" -msgstr "1 vstup" - -#: src/seedsigner/gui/screens/psbt_screens.py:90 -msgid "input 1" -msgstr "vstup 1" - -#: src/seedsigner/gui/screens/psbt_screens.py:91 -msgid "input 2" -msgstr "vstup 2" - -#. Indicates that items have been omitted from a series: e.g. "1, 2, 3, [...], -#. 8" -#: src/seedsigner/gui/screens/psbt_screens.py:93 -#: src/seedsigner/gui/screens/psbt_screens.py:141 -msgid "[ ... ]" -msgstr "[ ... ]" - -#. Input number will be inserted (e.g. "input 3") -#: src/seedsigner/gui/screens/psbt_screens.py:95 -#: src/seedsigner/gui/screens/psbt_screens.py:96 -#: src/seedsigner/gui/screens/psbt_screens.py:99 -msgid "input {}" -msgstr "vstup {}" - -#. Display a truncated address (e.g. "bc1qabc...") -#: src/seedsigner/gui/screens/psbt_screens.py:127 -msgid "{}..." -msgstr "{}..." - -#: src/seedsigner/gui/screens/psbt_screens.py:136 -#: src/seedsigner/views/psbt_views.py:437 -#: src/seedsigner/views/psbt_views.py:441 -msgid "self-transfer" -msgstr "převod sobě" - -#: src/seedsigner/gui/screens/psbt_screens.py:139 -msgid "recipient 1" -msgstr "příjemce 1" - -#. Inserts the recipient number (e.g. the fifth one is: "recipient 5") -#: src/seedsigner/gui/screens/psbt_screens.py:143 -msgid "recipient {}" -msgstr "příjemce {}" - -#: src/seedsigner/gui/screens/psbt_screens.py:145 -#: src/seedsigner/gui/screens/psbt_screens.py:555 -msgid "fee" -msgstr "poplatek" - -#. Label for a change output in the PSBT Overview flow diagram -#: src/seedsigner/gui/screens/psbt_screens.py:150 -#: src/seedsigner/views/psbt_views.py:437 -#: src/seedsigner/views/psbt_views.py:441 -msgid "change" -msgstr "nazpět" - -#: src/seedsigner/gui/screens/psbt_screens.py:461 -msgid "PSBT Math" -msgstr "PSBT rozpis" - -#: src/seedsigner/gui/screens/psbt_screens.py:462 -msgid "Review Recipients" -msgstr "Kontrola příjemců" - -#: src/seedsigner/gui/screens/psbt_screens.py:468 -#: src/seedsigner/gui/screens/psbt_screens.py:521 -msgid "btc" -msgstr "btc" - -#: src/seedsigner/gui/screens/psbt_screens.py:480 -msgid "sats" -msgstr "sats" - -#: src/seedsigner/gui/screens/psbt_screens.py:538 -msgid "input" -msgid_plural "inputs" -msgstr[0] "vstup" -msgstr[1] "vstupy" -msgstr[2] "vstupy" -msgstr[3] "vstupy" - -#: src/seedsigner/gui/screens/psbt_screens.py:548 -msgid "recipient" -msgid_plural "recipients" -msgstr[0] "příjemce" -msgstr[1] "příjemci" -msgstr[2] "příjemci" -msgstr[3] "příjemci" - -#. Denonination is inserted (e.g. your "btc change" or "sats change") -#: src/seedsigner/gui/screens/psbt_screens.py:566 -msgid "{} change" -msgstr "{} nazpět" - -#. First variable is either "Multisig" or the fingerprint for single sig; -#. Second is "Change" or "Addr" (change vs receive addr); Third is the address -#. index number (e.g. your #4 receive addr). -#: src/seedsigner/gui/screens/psbt_screens.py:662 -#: src/seedsigner/models/settings_definition.py:125 -#: src/seedsigner/views/seed_views.py:499 -msgid "Multisig" -msgstr "Multisig" - -#: src/seedsigner/gui/screens/psbt_screens.py:662 -msgid "Change" -msgstr "Nazpět" - -#: src/seedsigner/gui/screens/psbt_screens.py:662 -msgid "Addr" -msgstr "Adr" - -#: src/seedsigner/gui/screens/psbt_screens.py:672 -msgid "Address verified!" -msgstr "Adresa ověřena!" - -#: src/seedsigner/gui/screens/psbt_screens.py:684 -msgid "Sign PSBT" -msgstr "Podepsat PSBT" - -#: src/seedsigner/gui/screens/psbt_screens.py:698 -msgid "Click to approve this transaction" -msgstr "Klikněte pro potvrzení" - -#: src/seedsigner/gui/screens/psbt_screens.py:708 -msgid "Signed PSBT" -msgstr "Podepsaná PSBT" - -#. Prompt to specify which coordinator software to use (e.g. Specter, Sparrow, -#. etc) -#: src/seedsigner/gui/screens/psbt_screens.py:714 -msgid "Export as a QR code for:" -msgstr "Exportovat QR kod pro:" - -#: src/seedsigner/gui/screens/scan_screens.py:22 -msgid "Scan a QR code" -msgstr "Skenovat QR kód" - -#: src/seedsigner/gui/screens/scan_screens.py:138 -msgid "Settings QR" -msgstr "Nastavení QR" - -#: src/seedsigner/gui/screens/scan_screens.py:156 -msgid "Settings imported successfully!" -msgstr "Nastavení úspěšně načtena!" - -#: src/seedsigner/gui/screens/screen.py:735 -#: src/seedsigner/views/seed_views.py:995 -#: src/seedsigner/views/seed_views.py:1244 -msgid "Success!" -msgstr "Povedlo se!" - -#: src/seedsigner/gui/screens/screen.py:739 -#: src/seedsigner/views/seed_views.py:997 -#: src/seedsigner/views/seed_views.py:1247 -#: src/seedsigner/views/seed_views.py:1690 src/seedsigner/views/view.py:145 -msgid "OK" -msgstr "OK" - -#: src/seedsigner/gui/screens/screen.py:854 -#: src/seedsigner/views/psbt_views.py:435 -msgid "Caution" -msgstr "Pozor" - -#: src/seedsigner/gui/screens/screen.py:857 -msgid "I Understand" -msgstr "Rozumím" - -#: src/seedsigner/gui/screens/screen.py:869 -msgid "Classified Info!" -msgstr "Tajná informace!" - -#: src/seedsigner/gui/screens/seed_screens.py:411 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Finalize Seed" msgstr "Dokonči Seed" -#: src/seedsigner/gui/screens/seed_screens.py:424 -#: src/seedsigner/gui/screens/tools_screens.py:180 +#. a label for the shortened Key-id of a BIP-32 master HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "fingerprint" msgstr "otisk" -#: src/seedsigner/gui/screens/seed_screens.py:455 -#: src/seedsigner/views/seed_views.py:388 +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py msgid "Backup Seed" msgstr "Zálohovat seed" #. Additional explainer for the two seed backup options (mnemonic phrase and #. SeedQR). -#: src/seedsigner/gui/screens/seed_screens.py:462 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Backups do not include your passphrase." msgstr "Zálohy neobsahují bezpečnostní fráze" #. Displays the page number and total: (e.g. page 1 of 6) -#: src/seedsigner/gui/screens/seed_screens.py:479 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Seed Words: {}/{}" msgstr "Slova seedu: {}/{}" -#: src/seedsigner/gui/screens/seed_screens.py:554 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "BIP-85 Index" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py msgid "Verify Backup?" msgstr "Ověřit zálohu?" -#: src/seedsigner/gui/screens/seed_screens.py:560 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Optionally verify that your mnemonic backup is correct." msgstr "Volitelně ověřte, že je Vaše záloha správná. " -#: src/seedsigner/gui/screens/seed_screens.py:570 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Derivation Path" msgstr "Odvozovací cesta" -#: src/seedsigner/gui/screens/seed_screens.py:586 -msgid "Xpub Details" -msgstr "Podrobnosti Xpubu" - -#: src/seedsigner/gui/screens/seed_screens.py:592 -#: src/seedsigner/gui/screens/seed_screens.py:596 -#: src/seedsigner/views/seed_views.py:387 -#: src/seedsigner/views/seed_views.py:503 -#: src/seedsigner/views/seed_views.py:536 -#: src/seedsigner/views/seed_views.py:610 +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py msgid "Export Xpub" msgstr "Exportuj Xpub" -#: src/seedsigner/gui/screens/seed_screens.py:605 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Xpub Details" +msgstr "Podrobnosti Xpubu" + +#. Short for "BIP32 Master Fingerprint" +#. a label for the shortened Key-id of a BIP-32 master HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Fingerprint" msgstr "Otisk" #. Short for "Derivation Path" -#: src/seedsigner/gui/screens/seed_screens.py:615 +#. a label for the derivation-path into a BIP-32 HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Derivation" msgstr "Odvození" -#: src/seedsigner/gui/screens/seed_screens.py:624 +#. Short for "BIP32 Extended Public Key" +#: src/seedsigner/gui/screens/seed_screens.py msgid "Xpub" msgstr "Xpub" -#: src/seedsigner/gui/screens/seed_screens.py:637 -#: src/seedsigner/views/seed_views.py:230 -msgid "Add Passphrase" -msgstr "Přidat frázi" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "BIP-39 Passphrase" +msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:989 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Verify Passphrase" msgstr "Ověřit frázi" #. Describes the effect of applying a BIP-39 passphrase; it changes the seed's #. fingerprint -#: src/seedsigner/gui/screens/seed_screens.py:996 -msgid "changes seed:" -msgstr "změní seed:" +#: src/seedsigner/gui/screens/seed_screens.py +msgid "changes fingerprint" +msgstr "" #. Refers to the SeedQR type: Standard or Compact -#: src/seedsigner/gui/screens/seed_screens.py:1083 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Standard" msgstr "Standart" #. Briefly explains the Standard SeedQR data format -#: src/seedsigner/gui/screens/seed_screens.py:1085 +#: src/seedsigner/gui/screens/seed_screens.py msgid "BIP-39 wordlist indices" msgstr "Indexace seznamu slov BIP-39" #. Refers to the SeedQR type: Standard or Compact -#: src/seedsigner/gui/screens/seed_screens.py:1093 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Compact" msgstr "Zhutnit" #. Briefly explains the Compact SeedQR data format -#: src/seedsigner/gui/screens/seed_screens.py:1095 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Raw entropy bits" msgstr "kusy prvotní entropie" -#: src/seedsigner/gui/screens/seed_screens.py:1109 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Transcribe SeedQR" msgstr "Přepis SeedQR" #. Refers to the QR code size: 21x21, 25x25, or 29x29 -#: src/seedsigner/gui/screens/seed_screens.py:1111 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Begin {}x{}" msgstr "Začít přepis {}x{}" -#: src/seedsigner/gui/screens/seed_screens.py:1188 +#: src/seedsigner/gui/screens/seed_screens.py msgid "click to exit" msgstr "stiskem odejít" -#: src/seedsigner/gui/screens/seed_screens.py:1324 -msgid "" -"Optionally scan your transcribed SeedQR to confirm that it reads back " -"correctly." +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Optionally scan your transcribed SeedQR to confirm that it reads back correctly." msgstr "Volitelně naskenujte SeedQR pro potvrzení, že je načítán správně." -#: src/seedsigner/gui/screens/seed_screens.py:1381 -#: src/seedsigner/views/seed_views.py:1327 -#: src/seedsigner/views/seed_views.py:1385 +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py msgid "Verify Address" msgstr "Ověřit adresu" #. Inserts the nth address number (e.g. "Checking address 7") -#: src/seedsigner/gui/screens/seed_screens.py:1440 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Checking address {}" msgstr "Kontrola adresy {}" -#: src/seedsigner/gui/screens/seed_screens.py:1457 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Multisig Verification" msgstr "Ověření multisigu" -#: src/seedsigner/gui/screens/seed_screens.py:1462 -msgid "" -"Load your multisig wallet descriptor to verify your receive/self-transfer or" -" change address." -msgstr "" -"Pro ověření přijímací adresy či adresy pro drobné nahrajte deskriptor vaší " -"multisig peněženky." +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Load your multisig wallet descriptor to verify your receive/self-transfer or change address." +msgstr "Pro ověření přijímací adresy či adresy pro drobné nahrajte deskriptor vaší multisig peněženky." -#: src/seedsigner/gui/screens/seed_screens.py:1475 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Descriptor Loaded" msgstr "Deskriptor načten" #. Label for the multisig wallet's signing policy (e.g. 2-of-3) -#: src/seedsigner/gui/screens/seed_screens.py:1481 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Policy" msgstr "kvorum" -#: src/seedsigner/gui/screens/seed_screens.py:1489 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Signing Keys" msgstr "Podpisové klíče" -#: src/seedsigner/gui/screens/settings_screens.py:26 -#: src/seedsigner/views/main_menu_views.py:21 -#: src/seedsigner/views/settings_views.py:36 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Review Message" +msgstr "" + +#. Short for "Next step" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +#: src/seedsigner/views/tools_views.py +msgid "Next" +msgstr "Další" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Confirm Address" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Sign Message" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "derivation path" +msgstr "" + +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py msgid "Settings" msgstr "Nastavení" #. Short for "Input/Output"; screen to make sure the buttons and camera are #. working properly -#: src/seedsigner/gui/screens/settings_screens.py:59 +#: src/seedsigner/gui/screens/settings_screens.py msgid "I/O Test" msgstr "Test ovladačů" #. Blank the screen -#: src/seedsigner/gui/screens/settings_screens.py:130 -#: src/seedsigner/gui/screens/settings_screens.py:141 -#: src/seedsigner/gui/screens/settings_screens.py:222 +#: src/seedsigner/gui/screens/settings_screens.py msgid "Clear" msgstr "Vyčistit" -#: src/seedsigner/gui/screens/settings_screens.py:162 +#: src/seedsigner/gui/screens/settings_screens.py msgid "Exit" msgstr "Odejít" -#: src/seedsigner/gui/screens/settings_screens.py:176 -#: src/seedsigner/gui/screens/tools_screens.py:71 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Capturing image..." msgstr "Zachycuji snímek..." -#: src/seedsigner/gui/screens/settings_screens.py:289 -#: src/seedsigner/views/settings_views.py:21 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py msgid "Donate" msgstr "Podpořit" -#. If your language uses the percent sign ("%"), your translation must also -#. use +#. If your language uses the percent sign ("%"), your translation must also use #. two percent signs ("%%") due to python formatting oddities. "100%%" will be #. rendered as "100%". -#: src/seedsigner/gui/screens/settings_screens.py:294 +#: src/seedsigner/gui/screens/settings_screens.py #, python-format msgid "" "SeedSigner is 100%% free & open source, funded solely by the Bitcoin community.\n" @@ -433,712 +444,988 @@ msgstr "" "\n" "Podpořit jej lze on-chain či pomocí LN na:" -#. Instructions when taking a picture; "back" should point left. -#: src/seedsigner/gui/screens/tools_screens.py:40 -msgid "< back | click joystick" -msgstr "< zpět | stiskni joystick" +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py +msgid "Settings QR" +msgstr "" + +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py +msgid "Home" +msgstr "Nabídka" #. Instructions when reviewing an image; "reshoot" must point left, "accept" #. must point right -#: src/seedsigner/gui/screens/tools_screens.py:98 +#: src/seedsigner/gui/screens/tools_screens.py msgid " < reshoot | accept > " msgstr "< přefotit | přijmout > " #. current roll number vs total rolls (e.g. roll 7 of 50) -#: src/seedsigner/gui/screens/tools_screens.py:118 +#: src/seedsigner/gui/screens/tools_screens.py msgid "Dice Roll {}/{}" msgstr "Hod kostkou {}/{}" -#: src/seedsigner/gui/screens/tools_screens.py:163 +#. Build the last word in a 12 or 24 word BIP-39 mnemonic seed phrase. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Build Final Word" +msgstr "" + +#. Number of BIP-39 seed words, and the entropy -- in bits, contained within. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "The {}th word is built from {} more entropy bits plus auto-calculated checksum." +msgstr "" + +#. current coin-flip number vs total flips (e.g. flip 3 of 4) +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Coin Flip {}/{}" +msgstr "" + +#. How we call the "front" side result during a coin toss. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Heads = 1" +msgstr "" + +#. How we call the "back" side result during a coin toss. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Tails = 0" +msgstr "" + +#. A labeled presentation of a user-supplied value +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Your input: \"{}\"" +msgstr "" + +#. A function of "x" to be used for detecting errors in "x" +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Checksum" +msgstr "" + +#. labeled presentation of the last word in a BIP-39 mnemonic seed phrase. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Final Word: \"{}\"" +msgstr "" + +#. a label for the last word of a 12-word BIP-39 mnemonic seed phrase +#: src/seedsigner/gui/screens/tools_screens.py msgid "12th Word" msgstr "12té slovo" -#: src/seedsigner/gui/screens/tools_screens.py:165 +#. a label for the last word of a 24-word BIP-39 mnemonic seed phrase +#: src/seedsigner/gui/screens/tools_screens.py msgid "24th Word" msgstr "24té slovo" -#: src/seedsigner/models/settings_definition.py:14 +#. a label for the tool to explore public addresses for this seed. +#: src/seedsigner/gui/screens/tools_screens.py +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py +msgid "Address Explorer" +msgstr "" + +#. a label for a BIP-380-ish Output Descriptor +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Wallet descriptor" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Enabled" msgstr "Povoleno" -#: src/seedsigner/models/settings_definition.py:15 -#: src/seedsigner/models/settings_definition.py:20 +#: src/seedsigner/models/settings_definition.py msgid "Disabled" msgstr "Zakázáno" -#: src/seedsigner/models/settings_definition.py:18 -#: src/seedsigner/models/settings_definition.py:26 +#: src/seedsigner/models/settings_definition.py msgid "Prompt" msgstr "Výzva k akci" -#: src/seedsigner/models/settings_definition.py:19 -#: src/seedsigner/models/settings_definition.py:23 -#: src/seedsigner/models/settings_definition.py:29 +#: src/seedsigner/models/settings_definition.py msgid "Required" msgstr "Vyžadován" -#: src/seedsigner/models/settings_definition.py:74 -msgid "Btc-only" -msgstr "pouze BTC" +#. Translator is expected ONLY to change their own locale below +#: src/seedsigner/models/settings_definition.py +msgid "Arabic" +msgstr "عربي" -#: src/seedsigner/models/settings_definition.py:75 -msgid "Sats-only" -msgstr "pouze Sats" +#: src/seedsigner/models/settings_definition.py +msgid "Czech" +msgstr "čeština" -#: src/seedsigner/models/settings_definition.py:76 -msgid "Threshold at 0.01" -msgstr "Přechod na 0.01" +#: src/seedsigner/models/settings_definition.py +msgid "French" +msgstr "Français" -#: src/seedsigner/models/settings_definition.py:77 -msgid "Btc | Sats hybrid" -msgstr "Btc | Sats hybrid" +#: src/seedsigner/models/settings_definition.py +msgid "German" +msgstr "Deutsch" -#: src/seedsigner/models/settings_definition.py:85 +#: src/seedsigner/models/settings_definition.py +msgid "Hebrew" +msgstr "עִברִית" + +#: src/seedsigner/models/settings_definition.py +msgid "Japanese" +msgstr "日本" + +#: src/seedsigner/models/settings_definition.py +msgid "Korean" +msgstr "한국인" + +#: src/seedsigner/models/settings_definition.py +msgid "Portuguese" +msgstr "Português" + +#: src/seedsigner/models/settings_definition.py +msgid "Russian" +msgstr "Русский" + +#: src/seedsigner/models/settings_definition.py +msgid "Spanish" +msgstr "Español" + +#: src/seedsigner/models/settings_definition.py msgid "0°" msgstr "0°" -#: src/seedsigner/models/settings_definition.py:86 +#: src/seedsigner/models/settings_definition.py msgid "90°" msgstr "90°" -#: src/seedsigner/models/settings_definition.py:87 +#: src/seedsigner/models/settings_definition.py msgid "180°" msgstr "180°" -#: src/seedsigner/models/settings_definition.py:88 +#: src/seedsigner/models/settings_definition.py msgid "270°" msgstr "270°" -#: src/seedsigner/models/settings_definition.py:96 +#: src/seedsigner/models/settings_definition.py msgid "Low" msgstr "Nízká" -#: src/seedsigner/models/settings_definition.py:97 +#: src/seedsigner/models/settings_definition.py msgid "Medium" msgstr "Střední" -#: src/seedsigner/models/settings_definition.py:98 +#: src/seedsigner/models/settings_definition.py msgid "High" msgstr "Vysoká" -#: src/seedsigner/models/settings_definition.py:106 +#: src/seedsigner/models/settings_definition.py msgid "Mainnet" msgstr "Mainnet" -#: src/seedsigner/models/settings_definition.py:107 +#: src/seedsigner/models/settings_definition.py msgid "Testnet" msgstr "Testnet" -#: src/seedsigner/models/settings_definition.py:108 +#: src/seedsigner/models/settings_definition.py msgid "Regtest" msgstr "Regtest" -#: src/seedsigner/models/settings_definition.py:124 -#: src/seedsigner/views/seed_views.py:498 +#: src/seedsigner/models/settings_definition.py +msgid "Store Settings on SD card" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Insert SD card to enable" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +#: src/seedsigner/views/seed_views.py msgid "Single Sig" msgstr "Single Sig" -#: src/seedsigner/models/settings_definition.py:357 +#: src/seedsigner/models/settings_definition.py +msgid "Native Segwit" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Nested Segwit (legacy)" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Taproot" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Custom Derivation" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Language" msgstr "Jazyk" -#: src/seedsigner/models/settings_definition.py:365 +#: src/seedsigner/models/settings_definition.py msgid "Mnemonic language" msgstr "Jazyk zálohy" -#: src/seedsigner/models/settings_definition.py:373 +#: src/seedsigner/models/settings_definition.py msgid "Persistent settings" msgstr "Zapamatovat nastavení" -#: src/seedsigner/models/settings_definition.py:374 -msgid "Store Settings on SD card." -msgstr "Uložit nastavení na SD kartu." - -#: src/seedsigner/models/settings_definition.py:379 +#: src/seedsigner/models/settings_definition.py msgid "Coordinator software" msgstr "Výběr koordinátora" -#: src/seedsigner/models/settings_definition.py:386 +#: src/seedsigner/models/settings_definition.py msgid "Denomination display" msgstr "Nastavení jednotek" -#: src/seedsigner/models/settings_definition.py:395 +#: src/seedsigner/models/settings_definition.py msgid "Bitcoin network" msgstr "Bitcoinová síť" -#: src/seedsigner/models/settings_definition.py:403 +#: src/seedsigner/models/settings_definition.py msgid "QR code density" msgstr "Hustota QR kodu" -#: src/seedsigner/models/settings_definition.py:411 +#: src/seedsigner/models/settings_definition.py msgid "Xpub export" msgstr "Export Xpubu" -#: src/seedsigner/models/settings_definition.py:417 +#: src/seedsigner/models/settings_definition.py msgid "Sig types" msgstr "Podpisový typ" -#: src/seedsigner/models/settings_definition.py:425 +#: src/seedsigner/models/settings_definition.py msgid "Script types" msgstr "Typy skriptů" -#: src/seedsigner/models/settings_definition.py:433 +#: src/seedsigner/models/settings_definition.py msgid "Show xpub details" msgstr "Ukázat podrobnosti Xpubu" -#: src/seedsigner/models/settings_definition.py:439 +#: src/seedsigner/models/settings_definition.py msgid "BIP-39 passphrase" msgstr "BIP-39 bezpečnostní fráze" -#: src/seedsigner/models/settings_definition.py:447 +#: src/seedsigner/models/settings_definition.py msgid "Camera rotation" msgstr "Rotace kamery" -#: src/seedsigner/models/settings_definition.py:455 +#: src/seedsigner/models/settings_definition.py msgid "Compact SeedQR" msgstr "Zhutnit SeedQR" -#: src/seedsigner/models/settings_definition.py:461 +#: src/seedsigner/models/settings_definition.py +msgid "BIP-85 child seeds" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Message signing" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Show privacy warnings" msgstr "Zobrzovat bezpečnostní varování" -#: src/seedsigner/models/settings_definition.py:467 +#: src/seedsigner/models/settings_definition.py msgid "Show dire warnings" msgstr "Zobrazovat kritická varování" -#: src/seedsigner/models/settings_definition.py:473 -msgid "Show passphrase warning" -msgstr "Ukázovat varování při použití bezpečnostní fráze" +#: src/seedsigner/models/settings_definition.py +msgid "Show QR brightness tips" +msgstr "" -#: src/seedsigner/models/settings_definition.py:479 +#: src/seedsigner/models/settings_definition.py msgid "Show partner logos" msgstr "Ukazovat loga partnerů" -#: src/seedsigner/models/settings_definition.py:494 +#: src/seedsigner/models/settings_definition.py msgid "QR background color" msgstr "barva pozadí QR" -#: src/seedsigner/views/main_menu_views.py:18 -msgid "Scan" -msgstr "Naskenovat" - -#: src/seedsigner/views/main_menu_views.py:19 -msgid "Seeds" -msgstr "Seedy" - -#: src/seedsigner/views/main_menu_views.py:20 -#: src/seedsigner/views/tools_views.py:29 -msgid "Tools" -msgstr "Nástroje" - -#: src/seedsigner/views/main_menu_views.py:38 -msgid "Restart" -msgstr "Restartovat" - -#: src/seedsigner/views/main_menu_views.py:39 -msgid "Power Off" -msgstr "Vypnout" - -#: src/seedsigner/views/main_menu_views.py:42 -msgid "Reset / Power" -msgstr "Možnosti napájení" - -#: src/seedsigner/views/psbt_views.py:36 -#: src/seedsigner/views/seed_views.py:1365 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py msgid "Scan a seed" msgstr "Naskenovat seed" -#: src/seedsigner/views/psbt_views.py:37 -#: src/seedsigner/views/seed_views.py:1366 -msgid "Enter 12/24 words" -msgstr "Vložit 12/24 slov" +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +msgid "Enter 12-word seed" +msgstr "Vložit seed o 12 slovech" -#: src/seedsigner/views/psbt_views.py:53 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +msgid "Enter 24-word seed" +msgstr "Vložit seed o 24 slovech" + +#: src/seedsigner/views/psbt_views.py msgid "Select Signer" msgstr "Vybrat klíč" -#: src/seedsigner/views/psbt_views.py:88 +#: src/seedsigner/views/psbt_views.py msgid "Parsing PSBT..." msgstr "Rozebírám PSBT" -#: src/seedsigner/views/psbt_views.py:160 +#: src/seedsigner/views/psbt_views.py msgid "Unsupported Script Type!" msgstr "Nepodporovaný typ skriptu" -#: src/seedsigner/views/psbt_views.py:161 -msgid "" -"PSBT has unsupported input script type, please verify your change addresses." -msgstr "" -"PSBT využívá nepodporovaný typ scriptu na vstupu, ověřte zpáteční adresu " +#: src/seedsigner/views/psbt_views.py +msgid "PSBT has unsupported input script type, please verify your change addresses." +msgstr "PSBT využívá nepodporovaný typ scriptu na vstupu, ověřte zpáteční adresu " -#: src/seedsigner/views/psbt_views.py:162 -#: src/seedsigner/views/psbt_views.py:181 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/view.py msgid "Continue" msgstr "Pokračovat" #. User will receive no change back; the inputs to this transaction are fully #. spent -#: src/seedsigner/views/psbt_views.py:179 +#: src/seedsigner/views/psbt_views.py msgid "Full Spend!" msgstr "Utratit!" -#: src/seedsigner/views/psbt_views.py:180 -msgid "" -"This PSBT spends its entire input value. No change is coming back to your " -"wallet." -msgstr "" -"Tato PSBT utrácí úplnou hodnotu vstupu. Nepříjdou žádné drobné nazpět." +#: src/seedsigner/views/psbt_views.py +msgid "This PSBT spends its entire input value. No change is coming back to your wallet." +msgstr "Tato PSBT utrácí úplnou hodnotu vstupu. Nepříjdou žádné drobné nazpět." #. Future-tense used to indicate that this transaction will send this amount, -#. as opposed to "Send" on its own which could be misread as an instant -#. command +#. as opposed to "Send" on its own which could be misread as an instant command #. (e.g. "Send Now"). -#: src/seedsigner/views/psbt_views.py:247 +#: src/seedsigner/views/psbt_views.py msgid "Will Send" msgstr "Bude odesláno" -#: src/seedsigner/views/psbt_views.py:253 +#: src/seedsigner/views/psbt_views.py msgid "Next Recipient" msgstr "Další příjemce" -#. Short for "Next step" -#: src/seedsigner/views/psbt_views.py:256 -#: src/seedsigner/views/psbt_views.py:325 -#: src/seedsigner/views/seed_views.py:830 -msgid "Next" -msgstr "Další" - #. The amount you're receiving back from the transaction -#: src/seedsigner/views/psbt_views.py:329 +#: src/seedsigner/views/psbt_views.py msgid "Your Change" msgstr "Vaše drobné" -#: src/seedsigner/views/psbt_views.py:330 +#: src/seedsigner/views/psbt_views.py msgid "Verify Multisig Change" msgstr "Ověřit multisig drobné" -#: src/seedsigner/views/psbt_views.py:332 +#: src/seedsigner/views/psbt_views.py msgid "Self-Transfer" msgstr "Převod Sobě" -#: src/seedsigner/views/psbt_views.py:333 +#: src/seedsigner/views/psbt_views.py msgid "Verify Multisig Addr" msgstr "Oveřit multisig adres" -#: src/seedsigner/views/psbt_views.py:352 +#: src/seedsigner/views/psbt_views.py msgid "Verifying Change..." msgstr "Ověřuji drobné..." -#: src/seedsigner/views/psbt_views.py:354 +#: src/seedsigner/views/psbt_views.py msgid "Verifying Self-Transfer..." msgstr "Ověřuji vlastní převod..." #. Variable is either "change" or "self-transfer". -#: src/seedsigner/views/psbt_views.py:437 -msgid "" -"PSBT's {} address could not be verified with your multisig wallet " -"descriptor." -msgstr "" -"PSBT {} adresa nemohla být ověřena s poskytnutým multisig deskriptorem." +#: src/seedsigner/views/psbt_views.py +msgid "PSBT's {} address could not be verified with your multisig wallet descriptor." +msgstr "PSBT {} adresa nemohla být ověřena s poskytnutým multisig deskriptorem." -#: src/seedsigner/views/psbt_views.py:439 +#: src/seedsigner/views/psbt_views.py msgid "Suspicious PSBT" msgstr "Podezřelá PSBT" #. Variable is either "change" or "self-transfer". -#: src/seedsigner/views/psbt_views.py:441 +#: src/seedsigner/views/psbt_views.py msgid "PSBT's {} address could not be generated from your seed." msgstr "PSBT {} adresa nemohla být vygenerována z vašeho seedu." -#: src/seedsigner/views/psbt_views.py:445 +#: src/seedsigner/views/psbt_views.py msgid "Address Verification Failed" msgstr "Selhalo ověření adresy" -#: src/seedsigner/views/psbt_views.py:447 +#: src/seedsigner/views/psbt_views.py msgid "Discard PSBT" msgstr "Zahodit PSBT" -#: src/seedsigner/views/psbt_views.py:472 +#: src/seedsigner/views/psbt_views.py msgid "Approve PSBT" msgstr "Schválit PSBT" -#: src/seedsigner/views/psbt_views.py:477 -msgid "Signing PSBT..." -msgstr "Podepisuji PSBT..." - -#: src/seedsigner/views/psbt_views.py:556 -msgid "PSBT Error" -msgstr "Chyba PSBT" - -#: src/seedsigner/views/psbt_views.py:558 -msgid "Signing Failed" -msgstr "Podepisování se nezdařilo" - -#: src/seedsigner/views/psbt_views.py:559 -msgid "Signing with this seed did not add a valid signature." -msgstr "Podepisování s daným klíčem nepřidalo platný podpis." - -#: src/seedsigner/views/psbt_views.py:560 +#: src/seedsigner/views/psbt_views.py msgid "Select Diff Seed" msgstr "Vyberte jiný seed" +#: src/seedsigner/views/psbt_views.py +msgid "PSBT Error" +msgstr "Chyba PSBT" + +#: src/seedsigner/views/psbt_views.py +msgid "Signing Failed" +msgstr "Podepisování se nezdařilo" + +#: src/seedsigner/views/psbt_views.py +msgid "Signing with this seed did not add a valid signature." +msgstr "Podepisování s daným klíčem nepřidalo platný podpis." + #. This is on the opening splash screen, displayed above the HRF logo -#: src/seedsigner/views/screensaver.py:74 +#: src/seedsigner/views/screensaver.py msgid "With support from:" msgstr "Za podpory:" -#: src/seedsigner/views/seed_views.py:53 +#: src/seedsigner/views/seed_views.py msgid "Load a seed" msgstr "Nahrát seed" -#: src/seedsigner/views/seed_views.py:56 +#: src/seedsigner/views/seed_views.py msgid "In-Memory Seeds" msgstr "Uložené Seedy" -#: src/seedsigner/views/seed_views.py:77 +#: src/seedsigner/views/seed_views.py +msgid "Load the seed to verify" +msgstr "Načtěte seed pro ověření" + +#: src/seedsigner/views/seed_views.py +msgid "Select seed to verify" +msgstr "Vyberte seed pro ověření" + +#: src/seedsigner/views/seed_views.py +msgid "Load the seed to sign with" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Select seed to sign with" +msgstr "" + +#: src/seedsigner/views/seed_views.py msgid "Scan a SeedQR" msgstr "Skenovat SeedQR" -#: src/seedsigner/views/seed_views.py:78 -msgid "Enter 24-word seed" -msgstr "Vložit seed o 24 slovech" - -#: src/seedsigner/views/seed_views.py:79 -msgid "Enter 12-word seed" -msgstr "Vložit seed o 12 slovech" - -#: src/seedsigner/views/seed_views.py:80 +#: src/seedsigner/views/seed_views.py msgid "Create a seed" msgstr "Vytvořit Seed" -#: src/seedsigner/views/seed_views.py:89 +#: src/seedsigner/views/seed_views.py msgid "Load A Seed" msgstr "Nahrát Seed" #. Inserts the word number (e.g. "Seed Word #6") -#: src/seedsigner/views/seed_views.py:126 +#: src/seedsigner/views/seed_views.py msgid "Seed Word #{}" msgstr "Slovo seedu #{}" -#: src/seedsigner/views/seed_views.py:187 +#: src/seedsigner/views/seed_views.py msgid "Review & Edit" msgstr "Zkontrolovat & Upravit" -#: src/seedsigner/views/seed_views.py:188 -#: src/seedsigner/views/seed_views.py:343 -#: src/seedsigner/views/tools_views.py:259 +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py msgid "Discard" msgstr "Zahodit" -#: src/seedsigner/views/seed_views.py:192 +#: src/seedsigner/views/seed_views.py msgid "Invalid Mnemonic!" msgstr "Neplatná záloha!" -#: src/seedsigner/views/seed_views.py:194 +#: src/seedsigner/views/seed_views.py msgid "Checksum failure; not a valid seed phrase." msgstr "Kontrolní součet selhal; neplatný seed" -#: src/seedsigner/views/seed_views.py:229 -#: src/seedsigner/views/seed_views.py:301 -#: src/seedsigner/views/seed_views.py:831 -#: src/seedsigner/views/seed_views.py:1189 +#: src/seedsigner/views/seed_views.py src/seedsigner/views/view.py msgid "Done" msgstr "Dokončit" -#: src/seedsigner/views/seed_views.py:260 -msgid "" -"BIP-39 passphrase changes your seed, resulting in new xpubs, wallets, etc.\n" -"Be sure you know what you're doing!" -msgstr "" -"BIP-39. bezpečnostní fráze změní seed, dojde ke změně veřejných klíčů, peněženek etc.\n" -"Ujistěte se, že víte co děláte!" - -#: src/seedsigner/views/seed_views.py:300 +#: src/seedsigner/views/seed_views.py msgid "Edit passphrase" msgstr "Upravit frázi" -#: src/seedsigner/views/seed_views.py:342 +#: src/seedsigner/views/seed_views.py msgid "Keep Seed" msgstr "Udržet Seed" -#: src/seedsigner/views/seed_views.py:348 +#: src/seedsigner/views/seed_views.py msgid "Discard Seed?" msgstr "Zahodit Seed?" -#: src/seedsigner/views/seed_views.py:350 +#: src/seedsigner/views/seed_views.py msgid "Wipe seed {} from the device?" msgstr "Odstranit seed {} ze zařízení?" -#: src/seedsigner/views/seed_views.py:384 +#: src/seedsigner/views/seed_views.py msgid "Scan PSBT" msgstr "Naskenovat PSBT" -#: src/seedsigner/views/seed_views.py:386 -#: src/seedsigner/views/seed_views.py:1689 +#: src/seedsigner/views/seed_views.py msgid "Verify Addr" msgstr "Ověřit adresu" -#: src/seedsigner/views/seed_views.py:389 +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Child Seed" +msgstr "" + +#: src/seedsigner/views/seed_views.py msgid "Discard Seed" msgstr "Zahodit Seed" -#: src/seedsigner/views/seed_views.py:464 +#: src/seedsigner/views/seed_views.py msgid "View Seed Words" msgstr "Zobrazit slova seedu" -#: src/seedsigner/views/seed_views.py:465 +#: src/seedsigner/views/seed_views.py msgid "Export as SeedQR" msgstr "Exportovat jako SeedQR" -#: src/seedsigner/views/seed_views.py:652 -msgid "Privacy Leak!" -msgstr "Únik soukromých informací!" - -#: src/seedsigner/views/seed_views.py:653 +#: src/seedsigner/views/seed_views.py msgid "Xpub can be used to view all future transactions." msgstr "Xpub lze využít k prohlížení všech budoucích transakcí." -#: src/seedsigner/views/seed_views.py:683 +#: src/seedsigner/views/seed_views.py msgid "Generating xpub..." msgstr "Generuji xpub..." -#: src/seedsigner/views/seed_views.py:805 +#: src/seedsigner/views/seed_views.py msgid "You must keep your seed words private & away from all online devices." msgstr "Držte svůj seed vždy soukromý & nenahrávejte jej na online zařízení." -#: src/seedsigner/views/seed_views.py:877 -msgid "Verify" -msgstr "Oveřit" +#: src/seedsigner/views/seed_views.py +msgid "Child #{}" +msgstr "" -#: src/seedsigner/views/seed_views.py:878 -msgid "Skip" -msgstr "Přeskočit" +#: src/seedsigner/views/seed_views.py +msgid "Seed Words" +msgstr "" -#: src/seedsigner/views/seed_views.py:927 -msgid "Verify Word #{}" -msgstr "Ověřit slovo #{}" +#: src/seedsigner/views/seed_views.py +msgid "12 Words" +msgstr "" -#: src/seedsigner/views/seed_views.py:967 -msgid "Review Seed Words" -msgstr "Zkontrolovat slova Seedu" +#: src/seedsigner/views/seed_views.py +msgid "24 Words" +msgstr "" -#: src/seedsigner/views/seed_views.py:968 +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Num Words" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Index Error" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Invalid Child Index" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Child Index must be between 0 and 2^31-1." +msgstr "" + +#: src/seedsigner/views/seed_views.py msgid "Try Again" msgstr "Zkusit znovu" -#: src/seedsigner/views/seed_views.py:972 +#: src/seedsigner/views/seed_views.py +msgid "Verify" +msgstr "Oveřit" + +#: src/seedsigner/views/seed_views.py +msgid "Skip" +msgstr "Přeskočit" + +#: src/seedsigner/views/seed_views.py +msgid "Verify Word #{}" +msgstr "Ověřit slovo #{}" + +#: src/seedsigner/views/seed_views.py +msgid "Review Seed Words" +msgstr "Zkontrolovat slova Seedu" + +#: src/seedsigner/views/seed_views.py msgid "Verification Error" msgstr "Chyba ověření" -#: src/seedsigner/views/seed_views.py:974 +#: src/seedsigner/views/seed_views.py msgid "Wrong Word!" msgstr "Špatné slovo!" -#: src/seedsigner/views/seed_views.py:975 +#: src/seedsigner/views/seed_views.py msgid "Word #{} is not \"{}\"!" msgstr "Slovo #{} není \"{}\"!" -#: src/seedsigner/views/seed_views.py:993 +#: src/seedsigner/views/seed_views.py msgid "Backup Verified" msgstr "Záloha ověřena" -#: src/seedsigner/views/seed_views.py:996 +#: src/seedsigner/views/seed_views.py msgid "All mnemonic backup words were successfully verified!" msgstr "Všechna slova zálohy byla úspěšně ověřena!" -#: src/seedsigner/views/seed_views.py:1019 +#: src/seedsigner/views/seed_views.py msgid "Standard: 25x25" msgstr "Standart: 25x25" -#: src/seedsigner/views/seed_views.py:1020 +#: src/seedsigner/views/seed_views.py msgid "Compact: 21x21" msgstr "Zhutněný: 21x21" -#: src/seedsigner/views/seed_views.py:1024 +#: src/seedsigner/views/seed_views.py msgid "Standard: 29x29" msgstr "Standart: 29x29" -#: src/seedsigner/views/seed_views.py:1025 +#: src/seedsigner/views/seed_views.py msgid "Compact: 25x25" msgstr "Zhutněný: 25x25" -#: src/seedsigner/views/seed_views.py:1046 +#: src/seedsigner/views/seed_views.py msgid "SeedQR Format" msgstr "Formát SeedQR" -#: src/seedsigner/views/seed_views.py:1095 -msgid "SeedQR is your private key!" -msgstr "SeedQR je Váš privátní klíč!" +#: src/seedsigner/views/seed_views.py +msgid "Never photograph or scan it into a device that connects to the internet." +msgstr "" -#: src/seedsigner/views/seed_views.py:1096 -msgid "Never photograph it or scan it into an online device." -msgstr "Nikdy jej nefoťte či neskenujte do online zařízení." - -#: src/seedsigner/views/seed_views.py:1188 -#: src/seedsigner/views/seed_views.py:1232 -#: src/seedsigner/views/seed_views.py:1243 -#: src/seedsigner/views/seed_views.py:1255 +#: src/seedsigner/views/seed_views.py msgid "Confirm SeedQR" msgstr "Potvrďte SeedQR" -#: src/seedsigner/views/seed_views.py:1193 +#: src/seedsigner/views/seed_views.py msgid "Confirm SeedQR?" msgstr "Potvrďte SeedQR?" -#: src/seedsigner/views/seed_views.py:1223 +#: src/seedsigner/views/seed_views.py msgid "Scan your SeedQR" msgstr "Naskenujte svůj SeedQR" -#: src/seedsigner/views/seed_views.py:1233 -#: src/seedsigner/views/seed_views.py:1256 +#: src/seedsigner/views/seed_views.py msgid "Error!" msgstr "Chyba!" -#: src/seedsigner/views/seed_views.py:1234 +#: src/seedsigner/views/seed_views.py msgid "Your transcribed SeedQR does not match your original seed!" msgstr "Váš přepsaný SeedQR nekoresponduje s původním seedem!" -#: src/seedsigner/views/seed_views.py:1236 -#: src/seedsigner/views/seed_views.py:1259 +#: src/seedsigner/views/seed_views.py msgid "Review SeedQR" msgstr "Ověřte SeedQR" -#: src/seedsigner/views/seed_views.py:1245 -msgid "" -"Your transcribed SeedQR successfully scanned and yielded the same seed." +#: src/seedsigner/views/seed_views.py +msgid "Your transcribed SeedQR successfully scanned and yielded the same seed." msgstr "Váš přepsaný SeedQR byl naskenován a vyprodukoval stejný seed." -#: src/seedsigner/views/seed_views.py:1257 +#: src/seedsigner/views/seed_views.py msgid "Your transcribed SeedQR could not be read!" msgstr "Váš přepsaný SeedQR nebyl načten!" -#: src/seedsigner/views/seed_views.py:1328 +#: src/seedsigner/views/seed_views.py msgid "Sig type can't be auto-detected from this address. Please specify:" msgstr "Podpisový typ nebyl načten pomocí této adresy. Prosím upřesněte:" -#: src/seedsigner/views/seed_views.py:1369 -msgid "Load the seed to verify" -msgstr "Načtěte seed pro ověření" - -#: src/seedsigner/views/seed_views.py:1379 -msgid "Select seed to verify" -msgstr "Vyberte seed pro ověření" - -#: src/seedsigner/views/seed_views.py:1431 +#: src/seedsigner/views/seed_views.py msgid "Can't validate a single sig addr without specifying a seed" msgstr "Nelze potvrdit adresu bez specifikace seedu" #. Option when scanning for a matching address; skips ten addresses ahead -#: src/seedsigner/views/seed_views.py:1477 +#: src/seedsigner/views/seed_views.py msgid "Skip 10" msgstr "Přeskočit 10" -#: src/seedsigner/views/seed_views.py:1478 -#: src/seedsigner/views/seed_views.py:1659 +#: src/seedsigner/views/seed_views.py msgid "Cancel" msgstr "Zrušit" -#: src/seedsigner/views/seed_views.py:1606 -#: src/seedsigner/views/seed_views.py:1622 -msgid "Taproot verification not yet implemented!" -msgstr "Funkcionalita `ověření taproot` nebyla naimplementována!" - -#: src/seedsigner/views/seed_views.py:1618 -msgid "Legacy P2PKH verification not implemented!" -msgstr "Funkcionalita `ověření P2PKH` nebyla naimplementována!" - -#: src/seedsigner/views/seed_views.py:1643 +#: src/seedsigner/views/seed_views.py msgid "multisig" msgstr "multisig" -#: src/seedsigner/views/seed_views.py:1645 +#: src/seedsigner/views/seed_views.py msgid "seed {}" msgstr "seed {}" -#: src/seedsigner/views/seed_views.py:1648 +#: src/seedsigner/views/seed_views.py msgid "Address Verified" msgstr "Adresa ověřena" -#: src/seedsigner/views/seed_views.py:1658 +#: src/seedsigner/views/seed_views.py msgid "Scan Descriptor" msgstr "Skenovat deskriptor" -#: src/seedsigner/views/seed_views.py:1688 +#: src/seedsigner/views/seed_views.py msgid "Return to PSBT" msgstr "Zpět k PSBT" -#: src/seedsigner/views/settings_views.py:20 +#: src/seedsigner/views/settings_views.py msgid "I/O test" msgstr "Test ovladačů" -#: src/seedsigner/views/settings_views.py:39 -#: src/seedsigner/views/settings_views.py:46 +#: src/seedsigner/views/settings_views.py msgid "Advanced" msgstr "Rozšířené" -#: src/seedsigner/views/settings_views.py:54 +#: src/seedsigner/views/settings_views.py msgid "Dev Options" msgstr "Možnosti vývojáře" -#: src/seedsigner/views/tools_views.py:24 -msgid " New seed" -msgstr " Nový seed" +#: src/seedsigner/views/settings_views.py +msgid "Persistent Settings enabled. Settings saved to SD card." +msgstr "" -#: src/seedsigner/views/tools_views.py:25 +#: src/seedsigner/views/settings_views.py +msgid "Settings updated in temporary memory" +msgstr "" + +#: src/seedsigner/views/tools_views.py msgid "New seed" msgstr "Nový seed" -#: src/seedsigner/views/tools_views.py:26 +#: src/seedsigner/views/tools_views.py msgid "Calc 12th/24th word" msgstr "Spočítat 12/24 slovo" -#: src/seedsigner/views/tools_views.py:102 -#: src/seedsigner/views/tools_views.py:228 +#: src/seedsigner/views/tools_views.py src/seedsigner/views/view.py +msgid "Tools" +msgstr "Nástroje" + +#: src/seedsigner/views/tools_views.py msgid "12 words" msgstr "12 slov" -#: src/seedsigner/views/tools_views.py:103 -#: src/seedsigner/views/tools_views.py:229 +#: src/seedsigner/views/tools_views.py msgid "24 words" msgstr "24 slov" -#: src/seedsigner/views/tools_views.py:107 +#: src/seedsigner/views/tools_views.py msgid "Mnemonic Length?" msgstr "Délka zálohy?" -#: src/seedsigner/views/tools_views.py:174 +#: src/seedsigner/views/tools_views.py msgid "12 words (50 rolls)" msgstr "12 slov (50 hodů)" -#: src/seedsigner/views/tools_views.py:175 +#: src/seedsigner/views/tools_views.py msgid "24 words (99 rolls)" msgstr "24 slov (99 hodů)" -#: src/seedsigner/views/tools_views.py:179 -#: src/seedsigner/views/tools_views.py:233 +#: src/seedsigner/views/tools_views.py msgid "Mnemonic Length" msgstr "Délka zálohy" -#: src/seedsigner/views/tools_views.py:258 +#. Label to gather entropy through coin tosses +#: src/seedsigner/views/tools_views.py +msgid "Coin flip entropy" +msgstr "" + +#. Label to gather entropy through user specified BIP-39 word +#: src/seedsigner/views/tools_views.py +msgid "Word selection entropy" +msgstr "" + +#. Label to allow user to default entropy as all-zeros +#: src/seedsigner/views/tools_views.py +msgid "Finalize with zeros" +msgstr "" + +#. label to calculate the last word of a BIP-39 mnemonic seed phrase +#: src/seedsigner/views/tools_views.py +msgid "Final Word Calc" +msgstr "" + +#: src/seedsigner/views/tools_views.py msgid "Load seed" msgstr "Nahrát seed" -#: src/seedsigner/views/view.py:124 +#. label for addresses where others send us incoming payments +#: src/seedsigner/views/tools_views.py +msgid "Receive Addresses" +msgstr "" + +#. label for addresses that collect the change from our own outgoing payments +#: src/seedsigner/views/tools_views.py +msgid "Change Addresses" +msgstr "" + +#. a status message that our payment addresses are being calculated +#: src/seedsigner/views/tools_views.py +msgid "Calculating addrs..." +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Next {}" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Receive Addrs" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Change Addrs" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Scan" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Seeds" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Restart" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Power Off" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Reset / Power" +msgstr "" + +#: src/seedsigner/views/view.py msgid "Work In Progress" msgstr "Probíhající práce" -#: src/seedsigner/views/view.py:125 +#: src/seedsigner/views/view.py msgid "Not Yet Implemented" msgstr "Zatím nenaimplementováno" -#: src/seedsigner/views/view.py:126 +#: src/seedsigner/views/view.py msgid "This is still on our to-do list!" msgstr "Tohle máme na seznamů úkolů!" -#: src/seedsigner/views/view.py:127 +#: src/seedsigner/views/view.py msgid "Back to Main Menu" msgstr "Návrat do hlavního menu" -#: src/seedsigner/views/view.py:142 +#: src/seedsigner/views/view.py +msgid "Error" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Network Mismatch" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Change Setting" +msgstr "" + +#: src/seedsigner/views/view.py msgid "System Error" msgstr "Systémová chyba" + +#: src/seedsigner/views/view.py +msgid "Update Setting" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Option Disabled" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Security Tip" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "" +"For maximum security,\n" +"remove the MicroSD card\n" +"before continuing." +msgstr "" + +#~ msgid "{}..." +#~ msgstr "{}..." + +#~ msgid "Signed PSBT" +#~ msgstr "Podepsaná PSBT" + +#~ msgid "Export as a QR code for:" +#~ msgstr "Exportovat QR kod pro:" + +#~ msgid "Enter 12/24 words" +#~ msgstr "Vložit 12/24 slov" + +#~ msgid "Signing PSBT..." +#~ msgstr "Podepisuji PSBT..." + +#~ msgid "Taproot verification not yet implemented!" +#~ msgstr "Funkcionalita `ověření taproot` nebyla naimplementována!" + +#~ msgid "Legacy P2PKH verification not implemented!" +#~ msgstr "Funkcionalita `ověření P2PKH` nebyla naimplementována!" + +#~ msgid "Add Passphrase" +#~ msgstr "Přidat frázi" + +#~ msgid "Btc-only" +#~ msgstr "pouze BTC" + +#~ msgid "Sats-only" +#~ msgstr "pouze Sats" + +#~ msgid "Threshold at 0.01" +#~ msgstr "Přechod na 0.01" + +#~ msgid "Btc | Sats hybrid" +#~ msgstr "Btc | Sats hybrid" + +#~ msgid "Scan" +#~ msgstr "" + +#~ msgid "Seeds" +#~ msgstr "" + +#~ msgid "Restart" +#~ msgstr "" + +#~ msgid "Power Off" +#~ msgstr "" + +#~ msgid "Reset / Power" +#~ msgstr "Možnosti napájení" + +#~ msgid " (?)" +#~ msgstr "" + +#~ msgid "Scan a QR code" +#~ msgstr "Skenovat QR kód" + +#~ msgid "Yes " +#~ msgstr "" + +#~ msgid "No " +#~ msgstr "" + +#~ msgid "< back | Scan a QR code" +#~ msgstr "" + +#~ msgid "Settings QR" +#~ msgstr "Nastavení QR" + +#~ msgid "Settings imported successfully!" +#~ msgstr "Nastavení úspěšně načtena!" + +#~ msgid "MicroSD removed" +#~ msgstr "" + +#~ msgid "MicroSD inserted" +#~ msgstr "" + +#~ msgid "changes seed:" +#~ msgstr "změní seed:" + +#~ msgid "< back | click joystick" +#~ msgstr "< zpět | stiskni joystick" + +#~ msgid "Store Settings on SD card." +#~ msgstr "Uložit nastavení na SD kartu." + +#~ msgid "MicroSD card is removed" +#~ msgstr "" + +#~ msgid "Show passphrase warning" +#~ msgstr "Ukázovat varování při použití bezpečnostní fráze" + +#~ msgid "" +#~ "BIP-39 passphrase changes your seed, resulting in new xpubs, wallets, etc.\n" +#~ "Be sure you know what you're doing!" +#~ msgstr "" +#~ "BIP-39. bezpečnostní fráze změní seed, dojde ke změně veřejných klíčů, peněženek etc.\n" +#~ "Ujistěte se, že víte co děláte!" + +#~ msgid "SeedQR is your private key!" +#~ msgstr "SeedQR je Váš privátní klíč!" + +#~ msgid "Never photograph it or scan it into an online device." +#~ msgstr "Nikdy jej nefoťte či neskenujte do online zařízení." + +#~ msgid " New seed" +#~ msgstr " Nový seed" + +#~ msgid "Scan wallet descriptor" +#~ msgstr "" + diff --git a/src/seedsigner/resources/babel/de/LC_MESSAGES/messages.po b/src/seedsigner/resources/babel/de/LC_MESSAGES/messages.po index abf34f66..01546ac5 100644 --- a/src/seedsigner/resources/babel/de/LC_MESSAGES/messages.po +++ b/src/seedsigner/resources/babel/de/LC_MESSAGES/messages.po @@ -1,38 +1,207 @@ -# Translations template for PROJECT. +# German translations for PROJECT. # Copyright (C) 2022 ORGANIZATION # This file is distributed under the same license as the PROJECT project. # FIRST AUTHOR , 2022. -# +# # Translators: # Keith Mukai, 2022 # SeedMint, 2022 -# +# jdlcdl, 2022 #, fuzzy msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-05-29 11:20-0500\n" +"POT-Creation-Date: 2023-10-09 11:36-0400\n" "PO-Revision-Date: 2022-04-29 13:50+0000\n" "Last-Translator: SeedMint, 2022\n" -"Language-Team: German (https://www.transifex.com/seedsigner/teams/135832/de/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.10.1\n" "Language: de\n" +"Language-Team: German (https://www.transifex.com/seedsigner/teams/135832/de/)\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.10.3\n" -#: src/seedsigner/gui/screens/main_menu_screens.py:12 -#: src/seedsigner/gui/screens/scan_screens.py:140 -msgid "Home" -msgstr "Start" +#. The abbreviated label for the special key on a standard keyboard. +#: src/seedsigner/gui/keyboard.py +msgid "del" +msgstr "" -#: src/seedsigner/gui/screens/main_menu_screens.py:24 +#. The abbreviated label for the special key on a standard keyboard. +#: src/seedsigner/gui/keyboard.py +msgid "space" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "" +"You can remove\n" +"the SD card now" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "SD card removed" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "SD card inserted" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review PSBT" +msgstr "Kontrolle PSBT" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review Details" +msgstr "Kontrolle Details" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "1 input" +msgstr "1 Input" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input 1" +msgstr "Input 1" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input 2" +msgstr "Input 2" + +#. Indicates that items have been omitted from a series: e.g. "1, 2, 3, [...], +#. 8" +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "[ ... ]" +msgstr "[ ... ]" + +#. Input number will be inserted (e.g. "input 3") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input {}" +msgstr "input {}" + +#. Ellipsis ("...") characters used to truncate an address (e.g. "bc1qabc...") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "..." +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/views/psbt_views.py +msgid "self-transfer" +msgstr "selbst zahlungs" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient 1" +msgstr "Empfänger 1" + +#. Inserts the recipient number (e.g. the fifth one is: "recipient 5") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient {}" +msgstr "Empfänger {}" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "fee" +msgstr "Gebühr" + +#. Label for a change output in the PSBT Overview flow diagram +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/views/psbt_views.py +msgid "change" +msgstr "Change" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "PSBT Math" +msgstr "PSBT Math" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review Recipients" +msgstr "Kontrolle Empfänger" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "btc" +msgstr "btc" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "sats" +msgstr "sats" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input" +msgid_plural "inputs" +msgstr[0] "input" +msgstr[1] "inputs" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient" +msgid_plural "recipients" +msgstr[0] "recipient" +msgstr[1] "Empfänger" + +#. Denonination is inserted (e.g. your "btc change" or "sats change") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "{} change" +msgstr "{} change" + +#. First variable is either "Multisig" or the fingerprint for single sig; +#. Second is "Change" or "Addr" (change vs receive addr); Third is the address +#. index number (e.g. your #4 receive addr). +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/models/settings_definition.py +#: src/seedsigner/views/seed_views.py +msgid "Multisig" +msgstr "Multisig" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Change" +msgstr "Change" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Addr" +msgstr "Addr" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Address verified!" +msgstr "Adresse verifiziert!" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Sign PSBT" +msgstr "PSBT signieren" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Click to approve this transaction" +msgstr "Drücken um diese Transaktion zu bestätigen" + +#: src/seedsigner/gui/screens/scan_screens.py +msgid "< back | " +msgstr "" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "Success!" +msgstr "Erfolg!" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "OK" +msgstr "OK" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/psbt_views.py +msgid "Caution" +msgstr "Achtung" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "Privacy Leak!" +msgstr "Privatsphäreleck!" + +#: src/seedsigner/gui/screens/screen.py +msgid "I Understand" +msgstr "Verstanden" + +#: src/seedsigner/gui/screens/screen.py +msgid "Classified Info!" +msgstr "Geheime Info!" + +#: src/seedsigner/gui/screens/screen.py msgid "Restarting" msgstr "Neustart" -#: src/seedsigner/gui/screens/main_menu_screens.py:29 +#: src/seedsigner/gui/screens/screen.py msgid "" "SeedSigner is restarting.\n" "\n" @@ -41,385 +210,225 @@ msgstr "" "SeedSigner startet neu.\n" "Alle Daten aus dem internen Speicher werden gelöscht." -#: src/seedsigner/gui/screens/main_menu_screens.py:39 +#: src/seedsigner/gui/screens/screen.py msgid "Powering Off" msgstr "Ausschalten.." -#: src/seedsigner/gui/screens/main_menu_screens.py:44 +#: src/seedsigner/gui/screens/screen.py msgid "Please wait about 30 seconds before disconnecting power." msgstr "Vor dem Entfernen des Stromkabels, bitte ca. 30 Sekunden warten." -#: src/seedsigner/gui/screens/psbt_screens.py:29 -#: src/seedsigner/views/seed_views.py:385 -msgid "Review PSBT" -msgstr "Kontrolle PSBT" - -#: src/seedsigner/gui/screens/psbt_screens.py:31 -msgid "Review Details" -msgstr "Kontrolle Details" - -#: src/seedsigner/gui/screens/psbt_screens.py:88 -msgid "1 input" -msgstr "1 Input" - -#: src/seedsigner/gui/screens/psbt_screens.py:90 -msgid "input 1" -msgstr "Input 1" - -#: src/seedsigner/gui/screens/psbt_screens.py:91 -msgid "input 2" -msgstr "Input 2" - -#. Indicates that items have been omitted from a series: e.g. "1, 2, 3, [...], -#. 8" -#: src/seedsigner/gui/screens/psbt_screens.py:93 -#: src/seedsigner/gui/screens/psbt_screens.py:141 -msgid "[ ... ]" -msgstr "[ ... ]" - -#. Input number will be inserted (e.g. "input 3") -#: src/seedsigner/gui/screens/psbt_screens.py:95 -#: src/seedsigner/gui/screens/psbt_screens.py:96 -#: src/seedsigner/gui/screens/psbt_screens.py:99 -msgid "input {}" -msgstr "input {}" - -#. Display a truncated address (e.g. "bc1qabc...") -#: src/seedsigner/gui/screens/psbt_screens.py:127 -msgid "{}..." -msgstr "{}..." - -#: src/seedsigner/gui/screens/psbt_screens.py:136 -#: src/seedsigner/views/psbt_views.py:437 -#: src/seedsigner/views/psbt_views.py:441 -msgid "self-transfer" -msgstr "selbst zahlungs" - -#: src/seedsigner/gui/screens/psbt_screens.py:139 -msgid "recipient 1" -msgstr "Empfänger 1" - -#. Inserts the recipient number (e.g. the fifth one is: "recipient 5") -#: src/seedsigner/gui/screens/psbt_screens.py:143 -msgid "recipient {}" -msgstr "Empfänger {}" - -#: src/seedsigner/gui/screens/psbt_screens.py:145 -#: src/seedsigner/gui/screens/psbt_screens.py:555 -msgid "fee" -msgstr "Gebühr" - -#. Label for a change output in the PSBT Overview flow diagram -#: src/seedsigner/gui/screens/psbt_screens.py:150 -#: src/seedsigner/views/psbt_views.py:437 -#: src/seedsigner/views/psbt_views.py:441 -msgid "change" -msgstr "Change" - -#: src/seedsigner/gui/screens/psbt_screens.py:461 -msgid "PSBT Math" -msgstr "PSBT Math" - -#: src/seedsigner/gui/screens/psbt_screens.py:462 -msgid "Review Recipients" -msgstr "Kontrolle Empfänger" - -#: src/seedsigner/gui/screens/psbt_screens.py:468 -#: src/seedsigner/gui/screens/psbt_screens.py:521 -msgid "btc" -msgstr "btc" - -#: src/seedsigner/gui/screens/psbt_screens.py:480 -msgid "sats" -msgstr "sats" - -#: src/seedsigner/gui/screens/psbt_screens.py:538 -msgid "input" -msgid_plural "inputs" -msgstr[0] "input" -msgstr[1] "inputs" - -#: src/seedsigner/gui/screens/psbt_screens.py:548 -msgid "recipient" -msgid_plural "recipients" -msgstr[0] "recipient" -msgstr[1] "Empfänger" - -#. Denonination is inserted (e.g. your "btc change" or "sats change") -#: src/seedsigner/gui/screens/psbt_screens.py:566 -msgid "{} change" -msgstr "{} change" - -#. First variable is either "Multisig" or the fingerprint for single sig; -#. Second is "Change" or "Addr" (change vs receive addr); Third is the address -#. index number (e.g. your #4 receive addr). -#: src/seedsigner/gui/screens/psbt_screens.py:662 -#: src/seedsigner/models/settings_definition.py:125 -#: src/seedsigner/views/seed_views.py:499 -msgid "Multisig" -msgstr "Multisig" - -#: src/seedsigner/gui/screens/psbt_screens.py:662 -msgid "Change" -msgstr "Change" - -#: src/seedsigner/gui/screens/psbt_screens.py:662 -msgid "Addr" -msgstr "Addr" - -#: src/seedsigner/gui/screens/psbt_screens.py:672 -msgid "Address verified!" -msgstr "Adresse verifiziert!" - -#: src/seedsigner/gui/screens/psbt_screens.py:684 -msgid "Sign PSBT" -msgstr "PSBT signieren" - -#: src/seedsigner/gui/screens/psbt_screens.py:698 -msgid "Click to approve this transaction" -msgstr "Drücken um diese Transaktion zu bestätigen" - -#: src/seedsigner/gui/screens/psbt_screens.py:708 -msgid "Signed PSBT" -msgstr "Signierte PSBT" - -#. Prompt to specify which coordinator software to use (e.g. Specter, Sparrow, -#. etc) -#: src/seedsigner/gui/screens/psbt_screens.py:714 -msgid "Export as a QR code for:" -msgstr "QR Code exportieren für:" - -#: src/seedsigner/gui/screens/scan_screens.py:22 -msgid "Scan a QR code" -msgstr "Scanne einen QR Code" - -#: src/seedsigner/gui/screens/scan_screens.py:138 -msgid "Settings QR" -msgstr "QR Konfig." - -#: src/seedsigner/gui/screens/scan_screens.py:156 -msgid "Settings imported successfully!" -msgstr "Einstellungen erfolgreich importiert!" - -#: src/seedsigner/gui/screens/screen.py:735 -#: src/seedsigner/views/seed_views.py:995 -#: src/seedsigner/views/seed_views.py:1244 -msgid "Success!" -msgstr "Erfolg!" - -#: src/seedsigner/gui/screens/screen.py:739 -#: src/seedsigner/views/seed_views.py:997 -#: src/seedsigner/views/seed_views.py:1247 -#: src/seedsigner/views/seed_views.py:1690 src/seedsigner/views/view.py:145 -msgid "OK" -msgstr "OK" - -#: src/seedsigner/gui/screens/screen.py:854 -#: src/seedsigner/views/psbt_views.py:435 -msgid "Caution" -msgstr "Achtung" - -#: src/seedsigner/gui/screens/screen.py:857 -msgid "I Understand" -msgstr "Verstanden" - -#: src/seedsigner/gui/screens/screen.py:869 -msgid "Classified Info!" -msgstr "Geheime Info!" - -#: src/seedsigner/gui/screens/seed_screens.py:411 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Finalize Seed" msgstr "Seed finalisieren" -#: src/seedsigner/gui/screens/seed_screens.py:424 -#: src/seedsigner/gui/screens/tools_screens.py:180 +#. a label for the shortened Key-id of a BIP-32 master HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "fingerprint" msgstr "Fingerabruck" -#: src/seedsigner/gui/screens/seed_screens.py:455 -#: src/seedsigner/views/seed_views.py:388 +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py msgid "Backup Seed" msgstr "Seed Backup" #. Additional explainer for the two seed backup options (mnemonic phrase and #. SeedQR). -#: src/seedsigner/gui/screens/seed_screens.py:462 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Backups do not include your passphrase." msgstr "Backups enthalten deine Passphrase NICHT" #. Displays the page number and total: (e.g. page 1 of 6) -#: src/seedsigner/gui/screens/seed_screens.py:479 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Seed Words: {}/{}" msgstr "Seed Wörter: {}/{}" -#: src/seedsigner/gui/screens/seed_screens.py:554 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "BIP-85 Index" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py msgid "Verify Backup?" msgstr "Backup verifizieren?" -#: src/seedsigner/gui/screens/seed_screens.py:560 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Optionally verify that your mnemonic backup is correct." msgstr "Optional können Sie verifizieren das Ihr Seed Backup korrekt ist." -#: src/seedsigner/gui/screens/seed_screens.py:570 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Derivation Path" msgstr "Ableitungspfad" -#: src/seedsigner/gui/screens/seed_screens.py:586 -msgid "Xpub Details" -msgstr "Xpub Details" - -#: src/seedsigner/gui/screens/seed_screens.py:592 -#: src/seedsigner/gui/screens/seed_screens.py:596 -#: src/seedsigner/views/seed_views.py:387 -#: src/seedsigner/views/seed_views.py:503 -#: src/seedsigner/views/seed_views.py:536 -#: src/seedsigner/views/seed_views.py:610 +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py msgid "Export Xpub" msgstr "Export Xpub" -#: src/seedsigner/gui/screens/seed_screens.py:605 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Xpub Details" +msgstr "Xpub Details" + +#. Short for "BIP32 Master Fingerprint" +#. a label for the shortened Key-id of a BIP-32 master HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Fingerprint" msgstr "Fingerabdruck" #. Short for "Derivation Path" -#: src/seedsigner/gui/screens/seed_screens.py:615 +#. a label for the derivation-path into a BIP-32 HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Derivation" msgstr "Ableitung" -#: src/seedsigner/gui/screens/seed_screens.py:624 +#. Short for "BIP32 Extended Public Key" +#: src/seedsigner/gui/screens/seed_screens.py msgid "Xpub" msgstr "Xpub" -#: src/seedsigner/gui/screens/seed_screens.py:637 -#: src/seedsigner/views/seed_views.py:230 -msgid "Add Passphrase" -msgstr "Passphrase hinzufüg." +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "BIP-39 Passphrase" +msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:989 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Verify Passphrase" msgstr "Passphrase verifizieren" #. Describes the effect of applying a BIP-39 passphrase; it changes the seed's #. fingerprint -#: src/seedsigner/gui/screens/seed_screens.py:996 -msgid "changes seed:" -msgstr "ändert Seed:" +#: src/seedsigner/gui/screens/seed_screens.py +msgid "changes fingerprint" +msgstr "" #. Refers to the SeedQR type: Standard or Compact -#: src/seedsigner/gui/screens/seed_screens.py:1083 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Standard" msgstr "Standart" #. Briefly explains the Standard SeedQR data format -#: src/seedsigner/gui/screens/seed_screens.py:1085 +#: src/seedsigner/gui/screens/seed_screens.py msgid "BIP-39 wordlist indices" msgstr "BIP-39 Wortlisten Indexe" #. Refers to the SeedQR type: Standard or Compact -#: src/seedsigner/gui/screens/seed_screens.py:1093 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Compact" msgstr "Kompakt" #. Briefly explains the Compact SeedQR data format -#: src/seedsigner/gui/screens/seed_screens.py:1095 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Raw entropy bits" msgstr "Rohe Entropie Bits" -#: src/seedsigner/gui/screens/seed_screens.py:1109 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Transcribe SeedQR" msgstr "SeedQR übersetz." #. Refers to the QR code size: 21x21, 25x25, or 29x29 -#: src/seedsigner/gui/screens/seed_screens.py:1111 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Begin {}x{}" msgstr "Start {}x{}" -#: src/seedsigner/gui/screens/seed_screens.py:1188 +#: src/seedsigner/gui/screens/seed_screens.py msgid "click to exit" msgstr "zum verlassen, klicken" -#: src/seedsigner/gui/screens/seed_screens.py:1324 -msgid "" -"Optionally scan your transcribed SeedQR to confirm that it reads back " -"correctly." -msgstr "" -"Optional, übersetzten SeedQR scannen um zu verifizieren, dass er korrekt " -"gelesen wird." +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Optionally scan your transcribed SeedQR to confirm that it reads back correctly." +msgstr "Optional, übersetzten SeedQR scannen um zu verifizieren, dass er korrekt gelesen wird." -#: src/seedsigner/gui/screens/seed_screens.py:1381 -#: src/seedsigner/views/seed_views.py:1327 -#: src/seedsigner/views/seed_views.py:1385 +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py msgid "Verify Address" msgstr "Adresse verifizieren" #. Inserts the nth address number (e.g. "Checking address 7") -#: src/seedsigner/gui/screens/seed_screens.py:1440 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Checking address {}" msgstr "überprüfe Adresse {}" -#: src/seedsigner/gui/screens/seed_screens.py:1457 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Multisig Verification" msgstr "Multisig Verifikation" -#: src/seedsigner/gui/screens/seed_screens.py:1462 -msgid "" -"Load your multisig wallet descriptor to verify your receive/self-transfer or" -" change address." -msgstr "" -"Laden Sie Ihren Multisig Deskriptor um Ihre Empfangs-/ change- und " -"Selbstzahlungsadressen zu verifizieren." +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Load your multisig wallet descriptor to verify your receive/self-transfer or change address." +msgstr "Laden Sie Ihren Multisig Deskriptor um Ihre Empfangs-/ change- und Selbstzahlungsadressen zu verifizieren." -#: src/seedsigner/gui/screens/seed_screens.py:1475 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Descriptor Loaded" msgstr "Deskriptor geladen" #. Label for the multisig wallet's signing policy (e.g. 2-of-3) -#: src/seedsigner/gui/screens/seed_screens.py:1481 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Policy" msgstr "Policy" -#: src/seedsigner/gui/screens/seed_screens.py:1489 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Signing Keys" msgstr "Singnier Schlüssel" -#: src/seedsigner/gui/screens/settings_screens.py:26 -#: src/seedsigner/views/main_menu_views.py:21 -#: src/seedsigner/views/settings_views.py:36 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Review Message" +msgstr "" + +#. Short for "Next step" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +#: src/seedsigner/views/tools_views.py +msgid "Next" +msgstr "weiter" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Confirm Address" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Sign Message" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "derivation path" +msgstr "" + +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py msgid "Settings" msgstr "Setup" #. Short for "Input/Output"; screen to make sure the buttons and camera are #. working properly -#: src/seedsigner/gui/screens/settings_screens.py:59 +#: src/seedsigner/gui/screens/settings_screens.py msgid "I/O Test" msgstr "E/A Test" #. Blank the screen -#: src/seedsigner/gui/screens/settings_screens.py:130 -#: src/seedsigner/gui/screens/settings_screens.py:141 -#: src/seedsigner/gui/screens/settings_screens.py:222 +#: src/seedsigner/gui/screens/settings_screens.py msgid "Clear" msgstr "löschen" -#: src/seedsigner/gui/screens/settings_screens.py:162 +#: src/seedsigner/gui/screens/settings_screens.py msgid "Exit" msgstr "zurück" -#: src/seedsigner/gui/screens/settings_screens.py:176 -#: src/seedsigner/gui/screens/tools_screens.py:71 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Capturing image..." msgstr "Bild aufnehmen..." -#: src/seedsigner/gui/screens/settings_screens.py:289 -#: src/seedsigner/views/settings_views.py:21 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py msgid "Donate" msgstr "Spenden" -#. If your language uses the percent sign ("%"), your translation must also -#. use +#. If your language uses the percent sign ("%"), your translation must also use #. two percent signs ("%%") due to python formatting oddities. "100%%" will be #. rendered as "100%". -#: src/seedsigner/gui/screens/settings_screens.py:294 +#: src/seedsigner/gui/screens/settings_screens.py #, python-format msgid "" "SeedSigner is 100%% free & open source, funded solely by the Bitcoin community.\n" @@ -429,719 +438,988 @@ msgstr "" "SeedSigner ist 100%% frei & quelloffen, sowie einzig durch die Bitcoin community finanziert.\n" "Spende onchain bzw. via LN:" -#. Instructions when taking a picture; "back" should point left. -#: src/seedsigner/gui/screens/tools_screens.py:40 -msgid "< back | click joystick" -msgstr "< zurück | klick Joystick" +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py +msgid "Settings QR" +msgstr "" + +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py +msgid "Home" +msgstr "Start" #. Instructions when reviewing an image; "reshoot" must point left, "accept" #. must point right -#: src/seedsigner/gui/screens/tools_screens.py:98 +#: src/seedsigner/gui/screens/tools_screens.py msgid " < reshoot | accept > " msgstr " < wiederholen | akzeptieren > " #. current roll number vs total rolls (e.g. roll 7 of 50) -#: src/seedsigner/gui/screens/tools_screens.py:118 +#: src/seedsigner/gui/screens/tools_screens.py msgid "Dice Roll {}/{}" msgstr "Wurf {}/{}" -#: src/seedsigner/gui/screens/tools_screens.py:163 +#. Build the last word in a 12 or 24 word BIP-39 mnemonic seed phrase. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Build Final Word" +msgstr "" + +#. Number of BIP-39 seed words, and the entropy -- in bits, contained within. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "The {}th word is built from {} more entropy bits plus auto-calculated checksum." +msgstr "" + +#. current coin-flip number vs total flips (e.g. flip 3 of 4) +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Coin Flip {}/{}" +msgstr "" + +#. How we call the "front" side result during a coin toss. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Heads = 1" +msgstr "" + +#. How we call the "back" side result during a coin toss. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Tails = 0" +msgstr "" + +#. A labeled presentation of a user-supplied value +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Your input: \"{}\"" +msgstr "" + +#. A function of "x" to be used for detecting errors in "x" +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Checksum" +msgstr "" + +#. labeled presentation of the last word in a BIP-39 mnemonic seed phrase. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Final Word: \"{}\"" +msgstr "" + +#. a label for the last word of a 12-word BIP-39 mnemonic seed phrase +#: src/seedsigner/gui/screens/tools_screens.py msgid "12th Word" msgstr "12. Wort" -#: src/seedsigner/gui/screens/tools_screens.py:165 +#. a label for the last word of a 24-word BIP-39 mnemonic seed phrase +#: src/seedsigner/gui/screens/tools_screens.py msgid "24th Word" msgstr "24. Wort" -#: src/seedsigner/models/settings_definition.py:14 +#. a label for the tool to explore public addresses for this seed. +#: src/seedsigner/gui/screens/tools_screens.py +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py +msgid "Address Explorer" +msgstr "" + +#. a label for a BIP-380-ish Output Descriptor +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Wallet descriptor" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Enabled" msgstr "Aktiviert" -#: src/seedsigner/models/settings_definition.py:15 -#: src/seedsigner/models/settings_definition.py:20 +#: src/seedsigner/models/settings_definition.py msgid "Disabled" msgstr "Deaktiviert" -#: src/seedsigner/models/settings_definition.py:18 -#: src/seedsigner/models/settings_definition.py:26 +#: src/seedsigner/models/settings_definition.py msgid "Prompt" msgstr "Aufforderung" -#: src/seedsigner/models/settings_definition.py:19 -#: src/seedsigner/models/settings_definition.py:23 -#: src/seedsigner/models/settings_definition.py:29 +#: src/seedsigner/models/settings_definition.py msgid "Required" msgstr "Erforderlich" -#: src/seedsigner/models/settings_definition.py:74 -msgid "Btc-only" -msgstr "Nur in Btc" +#. Translator is expected ONLY to change their own locale below +#: src/seedsigner/models/settings_definition.py +msgid "Arabic" +msgstr "عربي" -#: src/seedsigner/models/settings_definition.py:75 -msgid "Sats-only" -msgstr "Nur in Sats" +#: src/seedsigner/models/settings_definition.py +msgid "Czech" +msgstr "čeština" -#: src/seedsigner/models/settings_definition.py:76 -msgid "Threshold at 0.01" -msgstr "Grenzwert bei 0.01" +#: src/seedsigner/models/settings_definition.py +msgid "French" +msgstr "Français" -#: src/seedsigner/models/settings_definition.py:77 -msgid "Btc | Sats hybrid" -msgstr "Btc | Sats Hybrid" +#: src/seedsigner/models/settings_definition.py +msgid "German" +msgstr "Deutsch" -#: src/seedsigner/models/settings_definition.py:85 +#: src/seedsigner/models/settings_definition.py +msgid "Hebrew" +msgstr "עִברִית" + +#: src/seedsigner/models/settings_definition.py +msgid "Japanese" +msgstr "日本" + +#: src/seedsigner/models/settings_definition.py +msgid "Korean" +msgstr "한국인" + +#: src/seedsigner/models/settings_definition.py +msgid "Portuguese" +msgstr "Português" + +#: src/seedsigner/models/settings_definition.py +msgid "Russian" +msgstr "Русский" + +#: src/seedsigner/models/settings_definition.py +msgid "Spanish" +msgstr "Español" + +#: src/seedsigner/models/settings_definition.py msgid "0°" msgstr "0°" -#: src/seedsigner/models/settings_definition.py:86 +#: src/seedsigner/models/settings_definition.py msgid "90°" msgstr "90°" -#: src/seedsigner/models/settings_definition.py:87 +#: src/seedsigner/models/settings_definition.py msgid "180°" msgstr "180°" -#: src/seedsigner/models/settings_definition.py:88 +#: src/seedsigner/models/settings_definition.py msgid "270°" msgstr "270°" -#: src/seedsigner/models/settings_definition.py:96 +#: src/seedsigner/models/settings_definition.py msgid "Low" msgstr "Gering" -#: src/seedsigner/models/settings_definition.py:97 +#: src/seedsigner/models/settings_definition.py msgid "Medium" msgstr "Mittel" -#: src/seedsigner/models/settings_definition.py:98 +#: src/seedsigner/models/settings_definition.py msgid "High" msgstr "Hoch" -#: src/seedsigner/models/settings_definition.py:106 +#: src/seedsigner/models/settings_definition.py msgid "Mainnet" msgstr "Mainnet" -#: src/seedsigner/models/settings_definition.py:107 +#: src/seedsigner/models/settings_definition.py msgid "Testnet" msgstr "Testnet" -#: src/seedsigner/models/settings_definition.py:108 +#: src/seedsigner/models/settings_definition.py msgid "Regtest" msgstr "Regtest" -#: src/seedsigner/models/settings_definition.py:124 -#: src/seedsigner/views/seed_views.py:498 +#: src/seedsigner/models/settings_definition.py +msgid "Store Settings on SD card" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Insert SD card to enable" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +#: src/seedsigner/views/seed_views.py msgid "Single Sig" msgstr "Single Sig" -#: src/seedsigner/models/settings_definition.py:357 +#: src/seedsigner/models/settings_definition.py +msgid "Native Segwit" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Nested Segwit (legacy)" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Taproot" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Custom Derivation" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Language" msgstr "Sprache" -#: src/seedsigner/models/settings_definition.py:365 +#: src/seedsigner/models/settings_definition.py msgid "Mnemonic language" msgstr "Seedphrasen Sprache" -#: src/seedsigner/models/settings_definition.py:373 +#: src/seedsigner/models/settings_definition.py msgid "Persistent settings" msgstr "dauerhafte Einstellungen" -#: src/seedsigner/models/settings_definition.py:374 -msgid "Store Settings on SD card." -msgstr "Einstellungen auf SD Karte speichern" - -#: src/seedsigner/models/settings_definition.py:379 +#: src/seedsigner/models/settings_definition.py msgid "Coordinator software" msgstr "Koordinator Software" -#: src/seedsigner/models/settings_definition.py:386 +#: src/seedsigner/models/settings_definition.py msgid "Denomination display" msgstr "Sats/Btc Anzeige" -#: src/seedsigner/models/settings_definition.py:395 +#: src/seedsigner/models/settings_definition.py msgid "Bitcoin network" msgstr "Bitcoin Netzwerk" -#: src/seedsigner/models/settings_definition.py:403 +#: src/seedsigner/models/settings_definition.py msgid "QR code density" msgstr "Auflösung QR Code" -#: src/seedsigner/models/settings_definition.py:411 +#: src/seedsigner/models/settings_definition.py msgid "Xpub export" msgstr "Xpub Export" -#: src/seedsigner/models/settings_definition.py:417 +#: src/seedsigner/models/settings_definition.py msgid "Sig types" msgstr "Signatur Arten" -#: src/seedsigner/models/settings_definition.py:425 +#: src/seedsigner/models/settings_definition.py msgid "Script types" msgstr "Skript Typen" -#: src/seedsigner/models/settings_definition.py:433 +#: src/seedsigner/models/settings_definition.py msgid "Show xpub details" msgstr "Zeige xpub Details" -#: src/seedsigner/models/settings_definition.py:439 +#: src/seedsigner/models/settings_definition.py msgid "BIP-39 passphrase" msgstr "BIP-39 Passphrase" -#: src/seedsigner/models/settings_definition.py:447 +#: src/seedsigner/models/settings_definition.py msgid "Camera rotation" msgstr "Kamera Rotation" -#: src/seedsigner/models/settings_definition.py:455 +#: src/seedsigner/models/settings_definition.py msgid "Compact SeedQR" msgstr "Kompakt SeedQR" -#: src/seedsigner/models/settings_definition.py:461 +#: src/seedsigner/models/settings_definition.py +msgid "BIP-85 child seeds" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Message signing" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Show privacy warnings" msgstr "Privatsphäre Warnungen" -#: src/seedsigner/models/settings_definition.py:467 +#: src/seedsigner/models/settings_definition.py msgid "Show dire warnings" msgstr "dringliche Warnungen" -#: src/seedsigner/models/settings_definition.py:473 -msgid "Show passphrase warning" -msgstr "Passphrasen Warnmeldung anzeigen" +#: src/seedsigner/models/settings_definition.py +msgid "Show QR brightness tips" +msgstr "" -#: src/seedsigner/models/settings_definition.py:479 +#: src/seedsigner/models/settings_definition.py msgid "Show partner logos" msgstr "zeige Partnerlogos" -#: src/seedsigner/models/settings_definition.py:494 +#: src/seedsigner/models/settings_definition.py msgid "QR background color" msgstr "QR Hintergrundfarbe" -#: src/seedsigner/views/main_menu_views.py:18 -msgid "Scan" -msgstr "Scan" - -#: src/seedsigner/views/main_menu_views.py:19 -msgid "Seeds" -msgstr "Seeds" - -#: src/seedsigner/views/main_menu_views.py:20 -#: src/seedsigner/views/tools_views.py:29 -msgid "Tools" -msgstr "Tools" - -#: src/seedsigner/views/main_menu_views.py:38 -msgid "Restart" -msgstr "Neustart" - -#: src/seedsigner/views/main_menu_views.py:39 -msgid "Power Off" -msgstr "Ausschalten" - -#: src/seedsigner/views/main_menu_views.py:42 -msgid "Reset / Power" -msgstr "Reset / Power" - -#: src/seedsigner/views/psbt_views.py:36 -#: src/seedsigner/views/seed_views.py:1365 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py msgid "Scan a seed" msgstr "Scanne einen Seed" -#: src/seedsigner/views/psbt_views.py:37 -#: src/seedsigner/views/seed_views.py:1366 -msgid "Enter 12/24 words" -msgstr "12/24 Wort Eingabe" +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +msgid "Enter 12-word seed" +msgstr "12Wort Seed Eingabe" -#: src/seedsigner/views/psbt_views.py:53 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +msgid "Enter 24-word seed" +msgstr "24Wort Seed Eingabe" + +#: src/seedsigner/views/psbt_views.py msgid "Select Signer" msgstr "Auswahl Signer" -#: src/seedsigner/views/psbt_views.py:88 +#: src/seedsigner/views/psbt_views.py msgid "Parsing PSBT..." msgstr "Analysiert PSBT... " -#: src/seedsigner/views/psbt_views.py:160 +#: src/seedsigner/views/psbt_views.py msgid "Unsupported Script Type!" msgstr "Skript nicht unterstützt!" -#: src/seedsigner/views/psbt_views.py:161 -msgid "" -"PSBT has unsupported input script type, please verify your change addresses." -msgstr "" -"PSBT enthält nicht unterstützte Input-Skript Typen, bitte Change Adressen " -"verifizieren" +#: src/seedsigner/views/psbt_views.py +msgid "PSBT has unsupported input script type, please verify your change addresses." +msgstr "PSBT enthält nicht unterstützte Input-Skript Typen, bitte Change Adressen verifizieren" -#: src/seedsigner/views/psbt_views.py:162 -#: src/seedsigner/views/psbt_views.py:181 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/view.py msgid "Continue" msgstr "Weiter" #. User will receive no change back; the inputs to this transaction are fully #. spent -#: src/seedsigner/views/psbt_views.py:179 +#: src/seedsigner/views/psbt_views.py msgid "Full Spend!" msgstr "komplett ausgegeben!" -#: src/seedsigner/views/psbt_views.py:180 -msgid "" -"This PSBT spends its entire input value. No change is coming back to your " -"wallet." -msgstr "" -"Diese PSBT zahlt den kompletten Eingabebetrag. Es wird kein Wechselgeld an " -"Ihre Wallet zurück gezahlt." +#: src/seedsigner/views/psbt_views.py +msgid "This PSBT spends its entire input value. No change is coming back to your wallet." +msgstr "Diese PSBT zahlt den kompletten Eingabebetrag. Es wird kein Wechselgeld an Ihre Wallet zurück gezahlt." #. Future-tense used to indicate that this transaction will send this amount, -#. as opposed to "Send" on its own which could be misread as an instant -#. command +#. as opposed to "Send" on its own which could be misread as an instant command #. (e.g. "Send Now"). -#: src/seedsigner/views/psbt_views.py:247 +#: src/seedsigner/views/psbt_views.py msgid "Will Send" msgstr "zu senden" -#: src/seedsigner/views/psbt_views.py:253 +#: src/seedsigner/views/psbt_views.py msgid "Next Recipient" msgstr "nächster Empfänger" -#. Short for "Next step" -#: src/seedsigner/views/psbt_views.py:256 -#: src/seedsigner/views/psbt_views.py:325 -#: src/seedsigner/views/seed_views.py:830 -msgid "Next" -msgstr "weiter" - #. The amount you're receiving back from the transaction -#: src/seedsigner/views/psbt_views.py:329 +#: src/seedsigner/views/psbt_views.py msgid "Your Change" msgstr "Ihr Change" -#: src/seedsigner/views/psbt_views.py:330 +#: src/seedsigner/views/psbt_views.py msgid "Verify Multisig Change" msgstr "Verifiziere Multisig Change" -#: src/seedsigner/views/psbt_views.py:332 +#: src/seedsigner/views/psbt_views.py msgid "Self-Transfer" msgstr "Selbst-Transfer" -#: src/seedsigner/views/psbt_views.py:333 +#: src/seedsigner/views/psbt_views.py msgid "Verify Multisig Addr" msgstr "Verifiziere Multisig Addr" -#: src/seedsigner/views/psbt_views.py:352 +#: src/seedsigner/views/psbt_views.py msgid "Verifying Change..." msgstr "Verifiziere Change..." -#: src/seedsigner/views/psbt_views.py:354 +#: src/seedsigner/views/psbt_views.py msgid "Verifying Self-Transfer..." msgstr "Verifiziere Selbstzahlung..." #. Variable is either "change" or "self-transfer". -#: src/seedsigner/views/psbt_views.py:437 -msgid "" -"PSBT's {} address could not be verified with your multisig wallet " -"descriptor." +#: src/seedsigner/views/psbt_views.py +msgid "PSBT's {} address could not be verified with your multisig wallet descriptor." msgstr "PSBT: {} Adresse konnte nicht verifiziert werden." -#: src/seedsigner/views/psbt_views.py:439 +#: src/seedsigner/views/psbt_views.py msgid "Suspicious PSBT" msgstr "Verdächtige PSBT" #. Variable is either "change" or "self-transfer". -#: src/seedsigner/views/psbt_views.py:441 +#: src/seedsigner/views/psbt_views.py msgid "PSBT's {} address could not be generated from your seed." msgstr "PSBT's {} Adresse konnte vom Seed nicht generiert werden." -#: src/seedsigner/views/psbt_views.py:445 +#: src/seedsigner/views/psbt_views.py msgid "Address Verification Failed" msgstr "Adressen Verifikation gescheitert" -#: src/seedsigner/views/psbt_views.py:447 +#: src/seedsigner/views/psbt_views.py msgid "Discard PSBT" msgstr "PSBT verwerfen" -#: src/seedsigner/views/psbt_views.py:472 +#: src/seedsigner/views/psbt_views.py msgid "Approve PSBT" msgstr "PSBT bestätigen" -#: src/seedsigner/views/psbt_views.py:477 -msgid "Signing PSBT..." -msgstr "Signiere PSBT..." - -#: src/seedsigner/views/psbt_views.py:556 -msgid "PSBT Error" -msgstr "PSBT Fehler" - -#: src/seedsigner/views/psbt_views.py:558 -msgid "Signing Failed" -msgstr "Fehler beim signieren" - -#: src/seedsigner/views/psbt_views.py:559 -msgid "Signing with this seed did not add a valid signature." -msgstr "Die Signatur mit diesem Seed ergab keine gültige Signatur" - -#: src/seedsigner/views/psbt_views.py:560 +#: src/seedsigner/views/psbt_views.py msgid "Select Diff Seed" msgstr "Wähle anderen Seed" +#: src/seedsigner/views/psbt_views.py +msgid "PSBT Error" +msgstr "PSBT Fehler" + +#: src/seedsigner/views/psbt_views.py +msgid "Signing Failed" +msgstr "Fehler beim signieren" + +#: src/seedsigner/views/psbt_views.py +msgid "Signing with this seed did not add a valid signature." +msgstr "Die Signatur mit diesem Seed ergab keine gültige Signatur" + #. This is on the opening splash screen, displayed above the HRF logo -#: src/seedsigner/views/screensaver.py:74 +#: src/seedsigner/views/screensaver.py msgid "With support from:" msgstr "Mit Unterstützung von:" -#: src/seedsigner/views/seed_views.py:53 +#: src/seedsigner/views/seed_views.py msgid "Load a seed" msgstr "Seed Eingabe" -#: src/seedsigner/views/seed_views.py:56 +#: src/seedsigner/views/seed_views.py msgid "In-Memory Seeds" msgstr "gespeicherte Seeds" -#: src/seedsigner/views/seed_views.py:77 +#: src/seedsigner/views/seed_views.py +msgid "Load the seed to verify" +msgstr "Laden Sie den Seed zum verifizieren" + +#: src/seedsigner/views/seed_views.py +msgid "Select seed to verify" +msgstr "Zu verifizierenden Seed wählen" + +#: src/seedsigner/views/seed_views.py +msgid "Load the seed to sign with" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Select seed to sign with" +msgstr "" + +#: src/seedsigner/views/seed_views.py msgid "Scan a SeedQR" msgstr "Scanne einen SeedQR" -#: src/seedsigner/views/seed_views.py:78 -msgid "Enter 24-word seed" -msgstr "24Wort Seed Eingabe" - -#: src/seedsigner/views/seed_views.py:79 -msgid "Enter 12-word seed" -msgstr "12Wort Seed Eingabe" - -#: src/seedsigner/views/seed_views.py:80 +#: src/seedsigner/views/seed_views.py msgid "Create a seed" msgstr "Seed erzeugen" -#: src/seedsigner/views/seed_views.py:89 +#: src/seedsigner/views/seed_views.py msgid "Load A Seed" msgstr "Seed Eingabe" #. Inserts the word number (e.g. "Seed Word #6") -#: src/seedsigner/views/seed_views.py:126 +#: src/seedsigner/views/seed_views.py msgid "Seed Word #{}" msgstr "Seed Wort #{}" -#: src/seedsigner/views/seed_views.py:187 +#: src/seedsigner/views/seed_views.py msgid "Review & Edit" msgstr "Prüfen & bearbeiten" -#: src/seedsigner/views/seed_views.py:188 -#: src/seedsigner/views/seed_views.py:343 -#: src/seedsigner/views/tools_views.py:259 +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py msgid "Discard" msgstr "Verwerfen" -#: src/seedsigner/views/seed_views.py:192 +#: src/seedsigner/views/seed_views.py msgid "Invalid Mnemonic!" msgstr "Ungültiger Seed!" -#: src/seedsigner/views/seed_views.py:194 +#: src/seedsigner/views/seed_views.py msgid "Checksum failure; not a valid seed phrase." msgstr "Prüfsumme fehlerhaft, kein valider Seed." -#: src/seedsigner/views/seed_views.py:229 -#: src/seedsigner/views/seed_views.py:301 -#: src/seedsigner/views/seed_views.py:831 -#: src/seedsigner/views/seed_views.py:1189 +#: src/seedsigner/views/seed_views.py src/seedsigner/views/view.py msgid "Done" msgstr "Fertig" -#: src/seedsigner/views/seed_views.py:260 -msgid "" -"BIP-39 passphrase changes your seed, resulting in new xpubs, wallets, etc.\n" -"Be sure you know what you're doing!" -msgstr "" -"Eine BIP-39 Passphrase verändert deinen Seed --> neue xpubs, wallets, etc.\n" -"Bitte vorher informieren!" - -#: src/seedsigner/views/seed_views.py:300 +#: src/seedsigner/views/seed_views.py msgid "Edit passphrase" msgstr "Passphrase bearbeiten" -#: src/seedsigner/views/seed_views.py:342 +#: src/seedsigner/views/seed_views.py msgid "Keep Seed" msgstr "Seed behalten" -#: src/seedsigner/views/seed_views.py:348 +#: src/seedsigner/views/seed_views.py msgid "Discard Seed?" msgstr "Seed verwerfen?" -#: src/seedsigner/views/seed_views.py:350 +#: src/seedsigner/views/seed_views.py msgid "Wipe seed {} from the device?" msgstr "Seed {} vom Gerät löschen?" -#: src/seedsigner/views/seed_views.py:384 +#: src/seedsigner/views/seed_views.py msgid "Scan PSBT" msgstr "PSBT scannen" -#: src/seedsigner/views/seed_views.py:386 -#: src/seedsigner/views/seed_views.py:1689 +#: src/seedsigner/views/seed_views.py msgid "Verify Addr" msgstr "Verifiziere Addr" -#: src/seedsigner/views/seed_views.py:389 +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Child Seed" +msgstr "" + +#: src/seedsigner/views/seed_views.py msgid "Discard Seed" msgstr "Seed verwerfen" -#: src/seedsigner/views/seed_views.py:464 +#: src/seedsigner/views/seed_views.py msgid "View Seed Words" msgstr "Seed-Phrase zeigen" -#: src/seedsigner/views/seed_views.py:465 +#: src/seedsigner/views/seed_views.py msgid "Export as SeedQR" msgstr "Als SeedQR exportieren" -#: src/seedsigner/views/seed_views.py:652 -msgid "Privacy Leak!" -msgstr "Privatsphäreleck!" - -#: src/seedsigner/views/seed_views.py:653 +#: src/seedsigner/views/seed_views.py msgid "Xpub can be used to view all future transactions." -msgstr "" -"Der Xpub kann verwendet werden um alle künftigen Transaktionen einzusehen." +msgstr "Der Xpub kann verwendet werden um alle künftigen Transaktionen einzusehen." -#: src/seedsigner/views/seed_views.py:683 +#: src/seedsigner/views/seed_views.py msgid "Generating xpub..." msgstr "Generiere xpub..." -#: src/seedsigner/views/seed_views.py:805 +#: src/seedsigner/views/seed_views.py msgid "You must keep your seed words private & away from all online devices." +msgstr "Halte deinen Seed privat & gib Ihn NIEMALS in ein Gerät mit Internetzugang ein." + +#: src/seedsigner/views/seed_views.py +msgid "Child #{}" msgstr "" -"Halte deinen Seed privat & gib Ihn NIEMALS in ein Gerät mit Internetzugang " -"ein." -#: src/seedsigner/views/seed_views.py:877 -msgid "Verify" -msgstr "Verifizieren" +#: src/seedsigner/views/seed_views.py +msgid "Seed Words" +msgstr "" -#: src/seedsigner/views/seed_views.py:878 -msgid "Skip" -msgstr "Überspringen" +#: src/seedsigner/views/seed_views.py +msgid "12 Words" +msgstr "" -#: src/seedsigner/views/seed_views.py:927 -msgid "Verify Word #{}" -msgstr "Verifiziere Wort #{}" +#: src/seedsigner/views/seed_views.py +msgid "24 Words" +msgstr "" -#: src/seedsigner/views/seed_views.py:967 -msgid "Review Seed Words" -msgstr "Seed Phrase überprüfen" +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Num Words" +msgstr "" -#: src/seedsigner/views/seed_views.py:968 +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Index Error" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Invalid Child Index" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Child Index must be between 0 and 2^31-1." +msgstr "" + +#: src/seedsigner/views/seed_views.py msgid "Try Again" msgstr "Erneut versuchen" -#: src/seedsigner/views/seed_views.py:972 +#: src/seedsigner/views/seed_views.py +msgid "Verify" +msgstr "Verifizieren" + +#: src/seedsigner/views/seed_views.py +msgid "Skip" +msgstr "Überspringen" + +#: src/seedsigner/views/seed_views.py +msgid "Verify Word #{}" +msgstr "Verifiziere Wort #{}" + +#: src/seedsigner/views/seed_views.py +msgid "Review Seed Words" +msgstr "Seed Phrase überprüfen" + +#: src/seedsigner/views/seed_views.py msgid "Verification Error" msgstr "Verifikationsfehler" -#: src/seedsigner/views/seed_views.py:974 +#: src/seedsigner/views/seed_views.py msgid "Wrong Word!" msgstr "Falsches Wort!" -#: src/seedsigner/views/seed_views.py:975 +#: src/seedsigner/views/seed_views.py msgid "Word #{} is not \"{}\"!" msgstr "Wort #{} ist nicht \"{}\"!" -#: src/seedsigner/views/seed_views.py:993 +#: src/seedsigner/views/seed_views.py msgid "Backup Verified" msgstr "Backup überprüft" -#: src/seedsigner/views/seed_views.py:996 +#: src/seedsigner/views/seed_views.py msgid "All mnemonic backup words were successfully verified!" msgstr "Alle Seed Wörter wurden erfolgreich überprüft" -#: src/seedsigner/views/seed_views.py:1019 +#: src/seedsigner/views/seed_views.py msgid "Standard: 25x25" msgstr "Standart: 25x25" -#: src/seedsigner/views/seed_views.py:1020 +#: src/seedsigner/views/seed_views.py msgid "Compact: 21x21" msgstr "Kompakt: 21x21" -#: src/seedsigner/views/seed_views.py:1024 +#: src/seedsigner/views/seed_views.py msgid "Standard: 29x29" msgstr "Standart: 29x29" -#: src/seedsigner/views/seed_views.py:1025 +#: src/seedsigner/views/seed_views.py msgid "Compact: 25x25" msgstr "Kompakt: 25x25" -#: src/seedsigner/views/seed_views.py:1046 +#: src/seedsigner/views/seed_views.py msgid "SeedQR Format" msgstr "SeedQR Format" -#: src/seedsigner/views/seed_views.py:1095 -msgid "SeedQR is your private key!" -msgstr "Der SeedQR ist dein privater Schlüssel!" +#: src/seedsigner/views/seed_views.py +msgid "Never photograph or scan it into a device that connects to the internet." +msgstr "" -#: src/seedsigner/views/seed_views.py:1096 -msgid "Never photograph it or scan it into an online device." -msgstr "Niemals mit einem online Gerät scannen bzw. fotografieren!" - -#: src/seedsigner/views/seed_views.py:1188 -#: src/seedsigner/views/seed_views.py:1232 -#: src/seedsigner/views/seed_views.py:1243 -#: src/seedsigner/views/seed_views.py:1255 +#: src/seedsigner/views/seed_views.py msgid "Confirm SeedQR" msgstr "SeedQR verifizieren" -#: src/seedsigner/views/seed_views.py:1193 +#: src/seedsigner/views/seed_views.py msgid "Confirm SeedQR?" msgstr "SeedQR verifizieren?" -#: src/seedsigner/views/seed_views.py:1223 +#: src/seedsigner/views/seed_views.py msgid "Scan your SeedQR" msgstr "Scannen Sie Ihren SeedQR" -#: src/seedsigner/views/seed_views.py:1233 -#: src/seedsigner/views/seed_views.py:1256 +#: src/seedsigner/views/seed_views.py msgid "Error!" msgstr "Fehler!" -#: src/seedsigner/views/seed_views.py:1234 +#: src/seedsigner/views/seed_views.py msgid "Your transcribed SeedQR does not match your original seed!" msgstr "Ihr übersetzer SeedQR stimmt nicht mit dem originalen Seed überein!" -#: src/seedsigner/views/seed_views.py:1236 -#: src/seedsigner/views/seed_views.py:1259 +#: src/seedsigner/views/seed_views.py msgid "Review SeedQR" msgstr "SeedQR überprüfen" -#: src/seedsigner/views/seed_views.py:1245 -msgid "" -"Your transcribed SeedQR successfully scanned and yielded the same seed." -msgstr "" -"Ihr übersetzter SeedQR wurde erfolgreich gescannt und ergab den selben Seed." +#: src/seedsigner/views/seed_views.py +msgid "Your transcribed SeedQR successfully scanned and yielded the same seed." +msgstr "Ihr übersetzter SeedQR wurde erfolgreich gescannt und ergab den selben Seed." -#: src/seedsigner/views/seed_views.py:1257 +#: src/seedsigner/views/seed_views.py msgid "Your transcribed SeedQR could not be read!" msgstr "Ihr übersetzter SeedQR konnte nicht gelesen werden!" -#: src/seedsigner/views/seed_views.py:1328 +#: src/seedsigner/views/seed_views.py msgid "Sig type can't be auto-detected from this address. Please specify:" msgstr "Sig Typ konnte nicht automatisch erkannt werden. Bitte spezifizieren:" -#: src/seedsigner/views/seed_views.py:1369 -msgid "Load the seed to verify" -msgstr "Laden Sie den Seed zum verifizieren" - -#: src/seedsigner/views/seed_views.py:1379 -msgid "Select seed to verify" -msgstr "Zu verifizierenden Seed wählen" - -#: src/seedsigner/views/seed_views.py:1431 +#: src/seedsigner/views/seed_views.py msgid "Can't validate a single sig addr without specifying a seed" -msgstr "" -"Sie müssen einen Seed spezifizieren um eine \"single sig\" Adresse zu " -"verifizieren" +msgstr "Sie müssen einen Seed spezifizieren um eine \"single sig\" Adresse zu verifizieren" #. Option when scanning for a matching address; skips ten addresses ahead -#: src/seedsigner/views/seed_views.py:1477 +#: src/seedsigner/views/seed_views.py msgid "Skip 10" msgstr "Springe 10" -#: src/seedsigner/views/seed_views.py:1478 -#: src/seedsigner/views/seed_views.py:1659 +#: src/seedsigner/views/seed_views.py msgid "Cancel" msgstr "Abbruch" -#: src/seedsigner/views/seed_views.py:1606 -#: src/seedsigner/views/seed_views.py:1622 -msgid "Taproot verification not yet implemented!" -msgstr "Taproot Überprüfung noch nicht implementiert!" - -#: src/seedsigner/views/seed_views.py:1618 -msgid "Legacy P2PKH verification not implemented!" -msgstr "Legacy P2PKH Überprüfung nicht implementiert!" - -#: src/seedsigner/views/seed_views.py:1643 +#: src/seedsigner/views/seed_views.py msgid "multisig" msgstr "Multisig" -#: src/seedsigner/views/seed_views.py:1645 +#: src/seedsigner/views/seed_views.py msgid "seed {}" msgstr "Seed {}" -#: src/seedsigner/views/seed_views.py:1648 +#: src/seedsigner/views/seed_views.py msgid "Address Verified" msgstr "Addresse verifiziert" -#: src/seedsigner/views/seed_views.py:1658 +#: src/seedsigner/views/seed_views.py msgid "Scan Descriptor" msgstr "Deskriptor scannen" -#: src/seedsigner/views/seed_views.py:1688 +#: src/seedsigner/views/seed_views.py msgid "Return to PSBT" msgstr "Zurück zur PSBT" -#: src/seedsigner/views/settings_views.py:20 +#: src/seedsigner/views/settings_views.py msgid "I/O test" msgstr "E/A Test" -#: src/seedsigner/views/settings_views.py:39 -#: src/seedsigner/views/settings_views.py:46 +#: src/seedsigner/views/settings_views.py msgid "Advanced" msgstr "Fortgeschritten" -#: src/seedsigner/views/settings_views.py:54 +#: src/seedsigner/views/settings_views.py msgid "Dev Options" msgstr "Entwickler Optionen" -#: src/seedsigner/views/tools_views.py:24 -msgid " New seed" -msgstr "Generiere Seed" +#: src/seedsigner/views/settings_views.py +msgid "Persistent Settings enabled. Settings saved to SD card." +msgstr "" -#: src/seedsigner/views/tools_views.py:25 +#: src/seedsigner/views/settings_views.py +msgid "Settings updated in temporary memory" +msgstr "" + +#: src/seedsigner/views/tools_views.py msgid "New seed" msgstr "Würfle Seed" -#: src/seedsigner/views/tools_views.py:26 +#: src/seedsigner/views/tools_views.py msgid "Calc 12th/24th word" msgstr "Ber. 12./24. Wort" -#: src/seedsigner/views/tools_views.py:102 -#: src/seedsigner/views/tools_views.py:228 +#: src/seedsigner/views/tools_views.py src/seedsigner/views/view.py +msgid "Tools" +msgstr "Tools" + +#: src/seedsigner/views/tools_views.py msgid "12 words" msgstr "12 Wörter" -#: src/seedsigner/views/tools_views.py:103 -#: src/seedsigner/views/tools_views.py:229 +#: src/seedsigner/views/tools_views.py msgid "24 words" msgstr "24 Wörter" -#: src/seedsigner/views/tools_views.py:107 +#: src/seedsigner/views/tools_views.py msgid "Mnemonic Length?" msgstr "Seedphrasen Länge?" -#: src/seedsigner/views/tools_views.py:174 +#: src/seedsigner/views/tools_views.py msgid "12 words (50 rolls)" msgstr "12 Wörter (50 Würfe)" -#: src/seedsigner/views/tools_views.py:175 +#: src/seedsigner/views/tools_views.py msgid "24 words (99 rolls)" msgstr "24 Wörter (99 Würfe)" -#: src/seedsigner/views/tools_views.py:179 -#: src/seedsigner/views/tools_views.py:233 +#: src/seedsigner/views/tools_views.py msgid "Mnemonic Length" msgstr "Seedphrasen Länge" -#: src/seedsigner/views/tools_views.py:258 +#. Label to gather entropy through coin tosses +#: src/seedsigner/views/tools_views.py +msgid "Coin flip entropy" +msgstr "" + +#. Label to gather entropy through user specified BIP-39 word +#: src/seedsigner/views/tools_views.py +msgid "Word selection entropy" +msgstr "" + +#. Label to allow user to default entropy as all-zeros +#: src/seedsigner/views/tools_views.py +msgid "Finalize with zeros" +msgstr "" + +#. label to calculate the last word of a BIP-39 mnemonic seed phrase +#: src/seedsigner/views/tools_views.py +msgid "Final Word Calc" +msgstr "" + +#: src/seedsigner/views/tools_views.py msgid "Load seed" msgstr "Lade Seed" -#: src/seedsigner/views/view.py:124 +#. label for addresses where others send us incoming payments +#: src/seedsigner/views/tools_views.py +msgid "Receive Addresses" +msgstr "" + +#. label for addresses that collect the change from our own outgoing payments +#: src/seedsigner/views/tools_views.py +msgid "Change Addresses" +msgstr "" + +#. a status message that our payment addresses are being calculated +#: src/seedsigner/views/tools_views.py +msgid "Calculating addrs..." +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Next {}" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Receive Addrs" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Change Addrs" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Scan" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Seeds" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Restart" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Power Off" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Reset / Power" +msgstr "" + +#: src/seedsigner/views/view.py msgid "Work In Progress" msgstr "in Arbeit" -#: src/seedsigner/views/view.py:125 +#: src/seedsigner/views/view.py msgid "Not Yet Implemented" msgstr "Noch nicht implementiert" -#: src/seedsigner/views/view.py:126 +#: src/seedsigner/views/view.py msgid "This is still on our to-do list!" msgstr "Dies ist noch auf unserer To-Do Liste!" -#: src/seedsigner/views/view.py:127 +#: src/seedsigner/views/view.py msgid "Back to Main Menu" msgstr "Zurück zum Hauptmenü" -#: src/seedsigner/views/view.py:142 +#: src/seedsigner/views/view.py +msgid "Error" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Network Mismatch" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Change Setting" +msgstr "" + +#: src/seedsigner/views/view.py msgid "System Error" msgstr "Systemfehler" + +#: src/seedsigner/views/view.py +msgid "Update Setting" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Option Disabled" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Security Tip" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "" +"For maximum security,\n" +"remove the MicroSD card\n" +"before continuing." +msgstr "" + +#~ msgid "{}..." +#~ msgstr "{}..." + +#~ msgid "Signed PSBT" +#~ msgstr "Signierte PSBT" + +#~ msgid "Export as a QR code for:" +#~ msgstr "QR Code exportieren für:" + +#~ msgid "Enter 12/24 words" +#~ msgstr "12/24 Wort Eingabe" + +#~ msgid "Signing PSBT..." +#~ msgstr "Signiere PSBT..." + +#~ msgid "Taproot verification not yet implemented!" +#~ msgstr "Taproot Überprüfung noch nicht implementiert!" + +#~ msgid "Legacy P2PKH verification not implemented!" +#~ msgstr "Legacy P2PKH Überprüfung nicht implementiert!" + +#~ msgid "Add Passphrase" +#~ msgstr "Passphrase hinzufüg." + +#~ msgid "Btc-only" +#~ msgstr "Nur in Btc" + +#~ msgid "Sats-only" +#~ msgstr "Nur in Sats" + +#~ msgid "Threshold at 0.01" +#~ msgstr "Grenzwert bei 0.01" + +#~ msgid "Btc | Sats hybrid" +#~ msgstr "Btc | Sats Hybrid" + +#~ msgid "Scan" +#~ msgstr "" + +#~ msgid "Seeds" +#~ msgstr "" + +#~ msgid "Restart" +#~ msgstr "" + +#~ msgid "Power Off" +#~ msgstr "" + +#~ msgid "Reset / Power" +#~ msgstr "Reset / Power" + +#~ msgid " (?)" +#~ msgstr "" + +#~ msgid "Scan a QR code" +#~ msgstr "Scanne einen QR Code" + +#~ msgid "Yes " +#~ msgstr "" + +#~ msgid "No " +#~ msgstr "" + +#~ msgid "< back | Scan a QR code" +#~ msgstr "" + +#~ msgid "Settings QR" +#~ msgstr "QR Konfig." + +#~ msgid "Settings imported successfully!" +#~ msgstr "Einstellungen erfolgreich importiert!" + +#~ msgid "MicroSD removed" +#~ msgstr "" + +#~ msgid "MicroSD inserted" +#~ msgstr "" + +#~ msgid "changes seed:" +#~ msgstr "ändert Seed:" + +#~ msgid "< back | click joystick" +#~ msgstr "< zurück | klick Joystick" + +#~ msgid "Store Settings on SD card." +#~ msgstr "Einstellungen auf SD Karte speichern" + +#~ msgid "MicroSD card is removed" +#~ msgstr "" + +#~ msgid "Show passphrase warning" +#~ msgstr "Passphrasen Warnmeldung anzeigen" + +#~ msgid "" +#~ "BIP-39 passphrase changes your seed, resulting in new xpubs, wallets, etc.\n" +#~ "Be sure you know what you're doing!" +#~ msgstr "" +#~ "Eine BIP-39 Passphrase verändert deinen Seed --> neue xpubs, wallets, etc.\n" +#~ "Bitte vorher informieren!" + +#~ msgid "SeedQR is your private key!" +#~ msgstr "Der SeedQR ist dein privater Schlüssel!" + +#~ msgid "Never photograph it or scan it into an online device." +#~ msgstr "Niemals mit einem online Gerät scannen bzw. fotografieren!" + +#~ msgid " New seed" +#~ msgstr "Generiere Seed" + +#~ msgid "Scan wallet descriptor" +#~ msgstr "" + diff --git a/src/seedsigner/resources/babel/es/LC_MESSAGES/messages.po b/src/seedsigner/resources/babel/es/LC_MESSAGES/messages.po index 282f54ac..80cd39df 100644 --- a/src/seedsigner/resources/babel/es/LC_MESSAGES/messages.po +++ b/src/seedsigner/resources/babel/es/LC_MESSAGES/messages.po @@ -1,150 +1,140 @@ -# Translations template for PROJECT. +# Spanish translations for PROJECT. # Copyright (C) 2022 ORGANIZATION # This file is distributed under the same license as the PROJECT project. # FIRST AUTHOR , 2022. -# +# # Translators: # Cabildo BTC, 2022 # Keith Mukai, 2022 # Bitcoin Hornet, 2022 -# +# jdlcdl, 2022 +# enteropositivo, 2024 #, fuzzy msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-05-29 11:20-0500\n" +"POT-Creation-Date: 2024-02-23 18:47-0400\n" "PO-Revision-Date: 2022-04-29 13:50+0000\n" "Last-Translator: Bitcoin Hornet, 2022\n" +"Language: es\n" "Language-Team: Spanish (https://www.transifex.com/seedsigner/teams/135832/es/)\n" +"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" "MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" +"Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.10.1\n" -"Language: es\n" -"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" -#: src/seedsigner/gui/screens/main_menu_screens.py:12 -#: src/seedsigner/gui/screens/scan_screens.py:140 -msgid "Home" -msgstr "Principal" +#. The abbreviated label for the special key on a standard keyboard. +#: src/seedsigner/gui/keyboard.py +msgid "del" +msgstr "supr" -#: src/seedsigner/gui/screens/main_menu_screens.py:24 -msgid "Restarting" -msgstr "Reiniciando" +#. The abbreviated label for the special key on a standard keyboard. +#: src/seedsigner/gui/keyboard.py +msgid "space" +msgstr "espacio" -#: src/seedsigner/gui/screens/main_menu_screens.py:29 +#: src/seedsigner/gui/toast.py msgid "" -"SeedSigner is restarting.\n" -"\n" -"All in-memory data will be wiped." +"You can remove\n" +"the SD card now" msgstr "" -"SeedSigner esta reiniciando.\n" -"\n" -"Todos los datos en la memoria se borraran." +"Ya puedes reirar\n" +"la tarjeta SD" -#: src/seedsigner/gui/screens/main_menu_screens.py:39 -msgid "Powering Off" -msgstr "Apagándose" +#: src/seedsigner/gui/toast.py +msgid "SD card removed" +msgstr "Tarjeta SD retirada" -#: src/seedsigner/gui/screens/main_menu_screens.py:44 -msgid "Please wait about 30 seconds before disconnecting power." -msgstr "" -"Por favor espere aproximadamente 30 segundos antes de desconectar la fuente " -"de alimentación." +#: src/seedsigner/gui/toast.py +msgid "SD card inserted" +msgstr "Tarjeta SD dentro" -#: src/seedsigner/gui/screens/psbt_screens.py:29 -#: src/seedsigner/views/seed_views.py:385 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "Review PSBT" msgstr "Revisar PSBT" -#: src/seedsigner/gui/screens/psbt_screens.py:31 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "Review Details" msgstr "Revisar Detalles" -#: src/seedsigner/gui/screens/psbt_screens.py:88 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "1 input" -msgstr "1 Entrada" +msgstr "1 entrada" -#: src/seedsigner/gui/screens/psbt_screens.py:90 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "input 1" -msgstr "Entrada 1" +msgstr "entrada 1" -#: src/seedsigner/gui/screens/psbt_screens.py:91 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "input 2" -msgstr "Entrada 2" +msgstr "entrada 2" #. Indicates that items have been omitted from a series: e.g. "1, 2, 3, [...], #. 8" -#: src/seedsigner/gui/screens/psbt_screens.py:93 -#: src/seedsigner/gui/screens/psbt_screens.py:141 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "[ ... ]" -msgstr "[ ... ]" +msgstr "" #. Input number will be inserted (e.g. "input 3") -#: src/seedsigner/gui/screens/psbt_screens.py:95 -#: src/seedsigner/gui/screens/psbt_screens.py:96 -#: src/seedsigner/gui/screens/psbt_screens.py:99 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "input {}" -msgstr "Entrada {}" +msgstr "entrada {}" -#. Display a truncated address (e.g. "bc1qabc...") -#: src/seedsigner/gui/screens/psbt_screens.py:127 -msgid "{}..." -msgstr "{}..." +#. Ellipsis ("...") characters used to truncate an address (e.g. "bc1qabc...") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "..." +msgstr "" -#: src/seedsigner/gui/screens/psbt_screens.py:136 -#: src/seedsigner/views/psbt_views.py:437 -#: src/seedsigner/views/psbt_views.py:441 +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/views/psbt_views.py msgid "self-transfer" -msgstr "autotransferencia" +msgstr "auto-transferencia" -#: src/seedsigner/gui/screens/psbt_screens.py:139 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "recipient 1" msgstr "destinatario 1" #. Inserts the recipient number (e.g. the fifth one is: "recipient 5") -#: src/seedsigner/gui/screens/psbt_screens.py:143 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "recipient {}" msgstr "destinatario {}" -#: src/seedsigner/gui/screens/psbt_screens.py:145 -#: src/seedsigner/gui/screens/psbt_screens.py:555 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "fee" msgstr "tasa" #. Label for a change output in the PSBT Overview flow diagram -#: src/seedsigner/gui/screens/psbt_screens.py:150 -#: src/seedsigner/views/psbt_views.py:437 -#: src/seedsigner/views/psbt_views.py:441 +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/views/psbt_views.py msgid "change" msgstr "cambio" -#: src/seedsigner/gui/screens/psbt_screens.py:461 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "PSBT Math" msgstr "Matemática del PSBT" -#: src/seedsigner/gui/screens/psbt_screens.py:462 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "Review Recipients" msgstr "Revisar Destinatarios" -#: src/seedsigner/gui/screens/psbt_screens.py:468 -#: src/seedsigner/gui/screens/psbt_screens.py:521 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "btc" msgstr "btc" -#: src/seedsigner/gui/screens/psbt_screens.py:480 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "sats" msgstr "sats" -#: src/seedsigner/gui/screens/psbt_screens.py:538 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "input" msgid_plural "inputs" msgstr[0] "Entrada" msgstr[1] "entradas" msgstr[2] "entradas" -#: src/seedsigner/gui/screens/psbt_screens.py:548 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "recipient" msgid_plural "recipients" msgstr[0] "destinatario" @@ -152,281 +142,300 @@ msgstr[1] "destinatarios" msgstr[2] "destinatarios" #. Denonination is inserted (e.g. your "btc change" or "sats change") -#: src/seedsigner/gui/screens/psbt_screens.py:566 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "{} change" msgstr "{} cambio" #. First variable is either "Multisig" or the fingerprint for single sig; #. Second is "Change" or "Addr" (change vs receive addr); Third is the address #. index number (e.g. your #4 receive addr). -#: src/seedsigner/gui/screens/psbt_screens.py:662 -#: src/seedsigner/models/settings_definition.py:125 -#: src/seedsigner/views/seed_views.py:499 +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/models/settings_definition.py +#: src/seedsigner/views/seed_views.py msgid "Multisig" msgstr "Multifirma" -#: src/seedsigner/gui/screens/psbt_screens.py:662 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "Change" msgstr "Cambio" -#: src/seedsigner/gui/screens/psbt_screens.py:662 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "Addr" msgstr "Dirección" -#: src/seedsigner/gui/screens/psbt_screens.py:672 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "Address verified!" msgstr "¡Dirección verificada!" -#: src/seedsigner/gui/screens/psbt_screens.py:684 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "Sign PSBT" msgstr "Firmar PSBT" -#: src/seedsigner/gui/screens/psbt_screens.py:698 +#: src/seedsigner/gui/screens/psbt_screens.py msgid "Click to approve this transaction" msgstr "Clic para aprobar esta transacción" -#: src/seedsigner/gui/screens/psbt_screens.py:708 -msgid "Signed PSBT" -msgstr "PSBT Firmado" +#: src/seedsigner/gui/screens/scan_screens.py +msgid "< back | " +msgstr "< volver | " -#. Prompt to specify which coordinator software to use (e.g. Specter, Sparrow, -#. etc) -#: src/seedsigner/gui/screens/psbt_screens.py:714 -msgid "Export as a QR code for:" -msgstr "Exportar como código QR para:" - -#: src/seedsigner/gui/screens/scan_screens.py:22 -msgid "Scan a QR code" -msgstr "Escanear un código QR" - -#: src/seedsigner/gui/screens/scan_screens.py:138 -msgid "Settings QR" -msgstr "Ajustes QR" - -#: src/seedsigner/gui/screens/scan_screens.py:156 -msgid "Settings imported successfully!" -msgstr "¡Ajustes importados exitosamente!" - -#: src/seedsigner/gui/screens/screen.py:735 -#: src/seedsigner/views/seed_views.py:995 -#: src/seedsigner/views/seed_views.py:1244 +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py msgid "Success!" msgstr "¡Éxito!" -#: src/seedsigner/gui/screens/screen.py:739 -#: src/seedsigner/views/seed_views.py:997 -#: src/seedsigner/views/seed_views.py:1247 -#: src/seedsigner/views/seed_views.py:1690 src/seedsigner/views/view.py:145 +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py msgid "OK" msgstr "OK" -#: src/seedsigner/gui/screens/screen.py:854 -#: src/seedsigner/views/psbt_views.py:435 +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/psbt_views.py msgid "Caution" msgstr "Precaución" -#: src/seedsigner/gui/screens/screen.py:857 +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "Privacy Leak!" +msgstr "¡Privacidad comprometida!" + +#: src/seedsigner/gui/screens/screen.py msgid "I Understand" -msgstr "Entiendo" +msgstr "Entendido" -#: src/seedsigner/gui/screens/screen.py:869 +#: src/seedsigner/gui/screens/screen.py msgid "Classified Info!" -msgstr "¡Información Clasificada!" +msgstr "¡Información Confidencial!" -#: src/seedsigner/gui/screens/seed_screens.py:411 +#: src/seedsigner/gui/screens/screen.py +msgid "Restarting" +msgstr "Reiniciando" + +#: src/seedsigner/gui/screens/screen.py +msgid "" +"SeedSigner is restarting.\n" +"\n" +"All in-memory data will be wiped." +msgstr "" +"SeedSigner esta reiniciando.\n" +"\n" +"Todos los datos en la memoria se borrarán." + +#: src/seedsigner/gui/screens/screen.py +msgid "Powering Off" +msgstr "Apagando" + +#: src/seedsigner/gui/screens/screen.py +msgid "Please wait about 30 seconds before disconnecting power." +msgstr "Por favor espere unos 30 segundos antes de quitar la alimentación." + +#: src/seedsigner/gui/screens/seed_screens.py msgid "Finalize Seed" msgstr "Completar Semilla" -#: src/seedsigner/gui/screens/seed_screens.py:424 -#: src/seedsigner/gui/screens/tools_screens.py:180 +#. a label for the shortened Key-id of a BIP-32 master HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "fingerprint" -msgstr "huellas" +msgstr "huella" -#: src/seedsigner/gui/screens/seed_screens.py:455 -#: src/seedsigner/views/seed_views.py:388 +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py msgid "Backup Seed" msgstr "Respaldar Semilla" #. Additional explainer for the two seed backup options (mnemonic phrase and #. SeedQR). -#: src/seedsigner/gui/screens/seed_screens.py:462 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Backups do not include your passphrase." -msgstr "Copias de seguridad no incluyen tu frase de contraseña." +msgstr "Los respaldos no incluyen tu contraseña." #. Displays the page number and total: (e.g. page 1 of 6) -#: src/seedsigner/gui/screens/seed_screens.py:479 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Seed Words: {}/{}" -msgstr "Palabras Claves: {}/{}" +msgstr "Palabras : {}/{}" -#: src/seedsigner/gui/screens/seed_screens.py:554 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "BIP-85 Index" +msgstr "Índice BIP-85" + +#: src/seedsigner/gui/screens/seed_screens.py msgid "Verify Backup?" -msgstr "¿Verificar Copia de Seguridad?" +msgstr "¿Verificar copia de seguridad?" -#: src/seedsigner/gui/screens/seed_screens.py:560 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Optionally verify that your mnemonic backup is correct." -msgstr "" -"Opcionalmente verifique que su copia de seguridad mnemónica esta correcta." +msgstr "Opcionalmente puedes verificar que has respaldado correctamente semilla." -#: src/seedsigner/gui/screens/seed_screens.py:570 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Derivation Path" -msgstr "Ruta de Derivación" +msgstr "Derivation Path" -#: src/seedsigner/gui/screens/seed_screens.py:586 -msgid "Xpub Details" -msgstr "Detalles de la Xpub" - -#: src/seedsigner/gui/screens/seed_screens.py:592 -#: src/seedsigner/gui/screens/seed_screens.py:596 -#: src/seedsigner/views/seed_views.py:387 -#: src/seedsigner/views/seed_views.py:503 -#: src/seedsigner/views/seed_views.py:536 -#: src/seedsigner/views/seed_views.py:610 +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py msgid "Export Xpub" msgstr "Exportar Xpub" -#: src/seedsigner/gui/screens/seed_screens.py:605 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Xpub Details" +msgstr "Detalles de la Xpub" + +#. Short for "BIP32 Master Fingerprint" +#. a label for the shortened Key-id of a BIP-32 master HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Fingerprint" -msgstr "Huella digital" +msgstr "Huella" #. Short for "Derivation Path" -#: src/seedsigner/gui/screens/seed_screens.py:615 +#. a label for the derivation-path into a BIP-32 HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Derivation" msgstr "Derivación" -#: src/seedsigner/gui/screens/seed_screens.py:624 +#. Short for "BIP32 Extended Public Key" +#: src/seedsigner/gui/screens/seed_screens.py msgid "Xpub" msgstr "Xpub" -#: src/seedsigner/gui/screens/seed_screens.py:637 -#: src/seedsigner/views/seed_views.py:230 -msgid "Add Passphrase" -msgstr "Agregar Frase de Contraseña" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "BIP-39 Passphrase" +msgstr "Constraseña BIP-39" -#: src/seedsigner/gui/screens/seed_screens.py:989 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Verify Passphrase" -msgstr "Verificar Frase de Contraseña" +msgstr "Verificar Contraseña" #. Describes the effect of applying a BIP-39 passphrase; it changes the seed's #. fingerprint -#: src/seedsigner/gui/screens/seed_screens.py:996 -msgid "changes seed:" -msgstr "cambia la semilla:" +#: src/seedsigner/gui/screens/seed_screens.py +msgid "changes fingerprint" +msgstr "cambia la huella" #. Refers to the SeedQR type: Standard or Compact -#: src/seedsigner/gui/screens/seed_screens.py:1083 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Standard" msgstr "Estándar" #. Briefly explains the Standard SeedQR data format -#: src/seedsigner/gui/screens/seed_screens.py:1085 +#: src/seedsigner/gui/screens/seed_screens.py msgid "BIP-39 wordlist indices" -msgstr "Índices de lista de palabras BIP-39" +msgstr "Índices de la lista BIP-39" #. Refers to the SeedQR type: Standard or Compact -#: src/seedsigner/gui/screens/seed_screens.py:1093 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Compact" msgstr "Compacto" #. Briefly explains the Compact SeedQR data format -#: src/seedsigner/gui/screens/seed_screens.py:1095 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Raw entropy bits" -msgstr "Entropía cruda de bits" +msgstr "Bits de entropía" -#: src/seedsigner/gui/screens/seed_screens.py:1109 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Transcribe SeedQR" msgstr "Transcribir SeedQR" #. Refers to the QR code size: 21x21, 25x25, or 29x29 -#: src/seedsigner/gui/screens/seed_screens.py:1111 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Begin {}x{}" msgstr "Empezar {}x{}" -#: src/seedsigner/gui/screens/seed_screens.py:1188 +#: src/seedsigner/gui/screens/seed_screens.py msgid "click to exit" msgstr "clic para salir" -#: src/seedsigner/gui/screens/seed_screens.py:1324 -msgid "" -"Optionally scan your transcribed SeedQR to confirm that it reads back " -"correctly." -msgstr "" -"Opcionalmente escanea tu SeedQR transcrito para confirmar que se relee " -"correctamente." +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Optionally scan your transcribed SeedQR to confirm that it reads back correctly." +msgstr "Opcionalmente puedes escanear tu SeedQR y confirmar que se relee correctamente." -#: src/seedsigner/gui/screens/seed_screens.py:1381 -#: src/seedsigner/views/seed_views.py:1327 -#: src/seedsigner/views/seed_views.py:1385 +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py msgid "Verify Address" -msgstr "Verificar Dirección" +msgstr "Comprobar Dirección" #. Inserts the nth address number (e.g. "Checking address 7") -#: src/seedsigner/gui/screens/seed_screens.py:1440 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Checking address {}" msgstr "Verificando dirección {}" -#: src/seedsigner/gui/screens/seed_screens.py:1457 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Multisig Verification" -msgstr "Verificación de la multifirma" +msgstr "Verificar Multifirma" -#: src/seedsigner/gui/screens/seed_screens.py:1462 -msgid "" -"Load your multisig wallet descriptor to verify your receive/self-transfer or" -" change address." -msgstr "" -"Carga tu descriptor de billetera multifirma para verificar tu dirección de " -"recibo/autotransferencia o de cambio." +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Load your multisig wallet descriptor to verify your receive/self-transfer or change address." +msgstr "Carga el descriptor de tu billetera multifirma para verificar tu dirección de recepción/autotransferencia o cambio." -#: src/seedsigner/gui/screens/seed_screens.py:1475 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Descriptor Loaded" msgstr "Descriptor Cargado" #. Label for the multisig wallet's signing policy (e.g. 2-of-3) -#: src/seedsigner/gui/screens/seed_screens.py:1481 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Policy" msgstr "Normas" -#: src/seedsigner/gui/screens/seed_screens.py:1489 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Signing Keys" -msgstr "Llaves de firmado" +msgstr "Claves de firma" -#: src/seedsigner/gui/screens/settings_screens.py:26 -#: src/seedsigner/views/main_menu_views.py:21 -#: src/seedsigner/views/settings_views.py:36 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Review Message" +msgstr "Revisar Mensaje" + +#. Short for "Next step" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +#: src/seedsigner/views/tools_views.py +msgid "Next" +msgstr "Próximo" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Confirm Address" +msgstr "Confirmar Dirección" + +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Sign Message" +msgstr "Firmar Mensaje" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "derivation path" +msgstr "path derivación" + +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py msgid "Settings" msgstr "Ajustes" #. Short for "Input/Output"; screen to make sure the buttons and camera are #. working properly -#: src/seedsigner/gui/screens/settings_screens.py:59 +#: src/seedsigner/gui/screens/settings_screens.py msgid "I/O Test" -msgstr "Examen de Hardware" +msgstr "Test de Hardware" #. Blank the screen -#: src/seedsigner/gui/screens/settings_screens.py:130 -#: src/seedsigner/gui/screens/settings_screens.py:141 -#: src/seedsigner/gui/screens/settings_screens.py:222 +#: src/seedsigner/gui/screens/settings_screens.py msgid "Clear" msgstr "Borrar" -#: src/seedsigner/gui/screens/settings_screens.py:162 +#: src/seedsigner/gui/screens/settings_screens.py msgid "Exit" msgstr "Salir" -#: src/seedsigner/gui/screens/settings_screens.py:176 -#: src/seedsigner/gui/screens/tools_screens.py:71 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Capturing image..." msgstr "Capturando imagen..." -#: src/seedsigner/gui/screens/settings_screens.py:289 -#: src/seedsigner/views/settings_views.py:21 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py msgid "Donate" msgstr "Donar" -#. If your language uses the percent sign ("%"), your translation must also -#. use +#. If your language uses the percent sign ("%"), your translation must also use #. two percent signs ("%%") due to python formatting oddities. "100%%" will be #. rendered as "100%". -#: src/seedsigner/gui/screens/settings_screens.py:294 +#: src/seedsigner/gui/screens/settings_screens.py #, python-format msgid "" "SeedSigner is 100%% free & open source, funded solely by the Bitcoin community.\n" @@ -435,725 +444,997 @@ msgid "" msgstr "" "SeedSigner es 100%% gratis y de código abierto, financiado únicamente por la comunidad de Bitcoin.\n" "\n" -"Donar en cadena o por billetera Lightning:" +"Donar BTC o por Lightning:" -#. Instructions when taking a picture; "back" should point left. -#: src/seedsigner/gui/screens/tools_screens.py:40 -msgid "< back | click joystick" -msgstr "retroceder | clic joystick" +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py +msgid "Settings QR" +msgstr "Configurar QR" + +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py +msgid "Home" +msgstr "Principal" #. Instructions when reviewing an image; "reshoot" must point left, "accept" #. must point right -#: src/seedsigner/gui/screens/tools_screens.py:98 +#: src/seedsigner/gui/screens/tools_screens.py msgid " < reshoot | accept > " msgstr " < recapturar | aceptar > " #. current roll number vs total rolls (e.g. roll 7 of 50) -#: src/seedsigner/gui/screens/tools_screens.py:118 +#: src/seedsigner/gui/screens/tools_screens.py msgid "Dice Roll {}/{}" msgstr "Tirar Dado {}/{}" -#: src/seedsigner/gui/screens/tools_screens.py:163 +#. Build the last word in a 12 or 24 word BIP-39 mnemonic seed phrase. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Build Final Word" +msgstr "Última Palabra" + +#. Number of BIP-39 seed words, and the entropy -- in bits, contained within. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "The {}th word is built from {} more entropy bits plus auto-calculated checksum." +msgstr "La palabra {} se construye a partir de {} bits de entropía y suma de comprobación." + +#. current coin-flip number vs total flips (e.g. flip 3 of 4) +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Coin Flip {}/{}" +msgstr "Cara o Cruz {}/{}" + +#. How we call the "front" side result during a coin toss. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Heads = 1" +msgstr "Cara = 1" + +#. How we call the "back" side result during a coin toss. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Tails = 0" +msgstr "Cruz = 0" + +#. A labeled presentation of a user-supplied value +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Your input: \"{}\"" +msgstr "Tu entrada: \"{}\"" + +#. A function of "x" to be used for detecting errors in "x" +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Checksum" +msgstr "Comprobación" + +#. labeled presentation of the last word in a BIP-39 mnemonic seed phrase. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Final Word: \"{}\"" +msgstr "Última Palabra: \"{}\"" + +#. a label for the last word of a 12-word BIP-39 mnemonic seed phrase +#: src/seedsigner/gui/screens/tools_screens.py msgid "12th Word" msgstr "Palabra 12º" -#: src/seedsigner/gui/screens/tools_screens.py:165 +#. a label for the last word of a 24-word BIP-39 mnemonic seed phrase +#: src/seedsigner/gui/screens/tools_screens.py msgid "24th Word" msgstr "Palabra 24º" -#: src/seedsigner/models/settings_definition.py:14 +#. a label for the tool to explore public addresses for this seed. +#: src/seedsigner/gui/screens/tools_screens.py +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py +msgid "Address Explorer" +msgstr "Ver direcciones" + +#. a label for a BIP-380-ish Output Descriptor +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Wallet descriptor" +msgstr "Descriptor de cartera" + +#: src/seedsigner/models/settings_definition.py msgid "Enabled" msgstr "Activada" -#: src/seedsigner/models/settings_definition.py:15 -#: src/seedsigner/models/settings_definition.py:20 +#: src/seedsigner/models/settings_definition.py msgid "Disabled" msgstr "Desactivada" -#: src/seedsigner/models/settings_definition.py:18 -#: src/seedsigner/models/settings_definition.py:26 +#: src/seedsigner/models/settings_definition.py msgid "Prompt" msgstr "Preguntar" -#: src/seedsigner/models/settings_definition.py:19 -#: src/seedsigner/models/settings_definition.py:23 -#: src/seedsigner/models/settings_definition.py:29 +#: src/seedsigner/models/settings_definition.py msgid "Required" msgstr "Requerido" -#: src/seedsigner/models/settings_definition.py:74 -msgid "Btc-only" -msgstr "Btc-Solamente" +#. Translator is expected ONLY to change their own locale below +#: src/seedsigner/models/settings_definition.py +msgid "Arabic" +msgstr "عربي" -#: src/seedsigner/models/settings_definition.py:75 -msgid "Sats-only" -msgstr "Sats-Solamente" +#: src/seedsigner/models/settings_definition.py +msgid "Czech" +msgstr "čeština" -#: src/seedsigner/models/settings_definition.py:76 -msgid "Threshold at 0.01" -msgstr "Límite a 0.01" +#: src/seedsigner/models/settings_definition.py +msgid "French" +msgstr "Français" -#: src/seedsigner/models/settings_definition.py:77 -msgid "Btc | Sats hybrid" -msgstr "Btc | Sats Híbrido" +#: src/seedsigner/models/settings_definition.py +msgid "German" +msgstr "Deutsch" -#: src/seedsigner/models/settings_definition.py:85 +#: src/seedsigner/models/settings_definition.py +msgid "Hebrew" +msgstr "עִברִית" + +#: src/seedsigner/models/settings_definition.py +msgid "Japanese" +msgstr "日本" + +#: src/seedsigner/models/settings_definition.py +msgid "Korean" +msgstr "한국인" + +#: src/seedsigner/models/settings_definition.py +msgid "Portuguese" +msgstr "Português" + +#: src/seedsigner/models/settings_definition.py +msgid "Russian" +msgstr "Русский" + +#: src/seedsigner/models/settings_definition.py +msgid "Spanish" +msgstr "Español" + +#: src/seedsigner/models/settings_definition.py msgid "0°" msgstr "0°" -#: src/seedsigner/models/settings_definition.py:86 +#: src/seedsigner/models/settings_definition.py msgid "90°" msgstr "90°" -#: src/seedsigner/models/settings_definition.py:87 +#: src/seedsigner/models/settings_definition.py msgid "180°" msgstr "180°" -#: src/seedsigner/models/settings_definition.py:88 +#: src/seedsigner/models/settings_definition.py msgid "270°" msgstr "270°" -#: src/seedsigner/models/settings_definition.py:96 +#: src/seedsigner/models/settings_definition.py msgid "Low" msgstr "Bajo" -#: src/seedsigner/models/settings_definition.py:97 +#: src/seedsigner/models/settings_definition.py msgid "Medium" msgstr "Medio" -#: src/seedsigner/models/settings_definition.py:98 +#: src/seedsigner/models/settings_definition.py msgid "High" msgstr "Alto" -#: src/seedsigner/models/settings_definition.py:106 +#: src/seedsigner/models/settings_definition.py msgid "Mainnet" msgstr "Mainnet" -#: src/seedsigner/models/settings_definition.py:107 +#: src/seedsigner/models/settings_definition.py msgid "Testnet" msgstr "Testnet" -#: src/seedsigner/models/settings_definition.py:108 +#: src/seedsigner/models/settings_definition.py msgid "Regtest" msgstr "Regtest" -#: src/seedsigner/models/settings_definition.py:124 -#: src/seedsigner/views/seed_views.py:498 -msgid "Single Sig" -msgstr "Firma simple" +#: src/seedsigner/models/settings_definition.py +msgid "Store Settings on SD card" +msgstr "Guardar config. en la SD" -#: src/seedsigner/models/settings_definition.py:357 +#: src/seedsigner/models/settings_definition.py +msgid "Insert SD card to enable" +msgstr "Inserta la SD para habilitar" + +#: src/seedsigner/models/settings_definition.py +#: src/seedsigner/views/seed_views.py +msgid "Single Sig" +msgstr "Firma Simple" + +#: src/seedsigner/models/settings_definition.py +msgid "Native Segwit" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Nested Segwit (legacy)" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Taproot" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Custom Derivation" +msgstr "Derivación Personalizada" + +#: src/seedsigner/models/settings_definition.py msgid "Language" msgstr "Idioma" -#: src/seedsigner/models/settings_definition.py:365 +#: src/seedsigner/models/settings_definition.py msgid "Mnemonic language" msgstr "Idioma de frase mnemónica" -#: src/seedsigner/models/settings_definition.py:373 +#: src/seedsigner/models/settings_definition.py msgid "Persistent settings" msgstr "Ajustes persistentes" -#: src/seedsigner/models/settings_definition.py:374 -msgid "Store Settings on SD card." -msgstr "Guardar configuraciones en la tarjeta SD." - -#: src/seedsigner/models/settings_definition.py:379 +#: src/seedsigner/models/settings_definition.py msgid "Coordinator software" msgstr "Software coordinador" -#: src/seedsigner/models/settings_definition.py:386 +#: src/seedsigner/models/settings_definition.py msgid "Denomination display" -msgstr "Pantalla de denominaciones" +msgstr "Mostrar unidades en..." -#: src/seedsigner/models/settings_definition.py:395 +#: src/seedsigner/models/settings_definition.py msgid "Bitcoin network" msgstr "Red Bitcoin" -#: src/seedsigner/models/settings_definition.py:403 +#: src/seedsigner/models/settings_definition.py msgid "QR code density" msgstr "Densidad del QR" -#: src/seedsigner/models/settings_definition.py:411 +#: src/seedsigner/models/settings_definition.py msgid "Xpub export" msgstr "Exportar Xpub" -#: src/seedsigner/models/settings_definition.py:417 +#: src/seedsigner/models/settings_definition.py msgid "Sig types" msgstr "Tipos de firma" -#: src/seedsigner/models/settings_definition.py:425 +#: src/seedsigner/models/settings_definition.py msgid "Script types" msgstr "Tipos de script" -#: src/seedsigner/models/settings_definition.py:433 +#: src/seedsigner/models/settings_definition.py msgid "Show xpub details" -msgstr "Mostrar detalles de la xpub" +msgstr "Mostrar detalles de xpub" -#: src/seedsigner/models/settings_definition.py:439 +#: src/seedsigner/models/settings_definition.py msgid "BIP-39 passphrase" -msgstr "Frase de contraseña BIP-39" +msgstr "Usar clave BIP-39" -#: src/seedsigner/models/settings_definition.py:447 +#: src/seedsigner/models/settings_definition.py msgid "Camera rotation" msgstr "Rotación de cámara" -#: src/seedsigner/models/settings_definition.py:455 +#: src/seedsigner/models/settings_definition.py msgid "Compact SeedQR" msgstr "SeedQR Compacto" -#: src/seedsigner/models/settings_definition.py:461 +#: src/seedsigner/models/settings_definition.py +msgid "BIP-85 child seeds" +msgstr "Semillas hijas BIP-85" + +#: src/seedsigner/models/settings_definition.py +msgid "Message signing" +msgstr "Firmar mensaje" + +#: src/seedsigner/models/settings_definition.py msgid "Show privacy warnings" -msgstr "Mostrar alertas de privacidad" +msgstr "Alertas de privacidad" -#: src/seedsigner/models/settings_definition.py:467 +#: src/seedsigner/models/settings_definition.py msgid "Show dire warnings" -msgstr "Mostrar advertencias graves" +msgstr "Advertencias graves" -#: src/seedsigner/models/settings_definition.py:473 -msgid "Show passphrase warning" -msgstr "Mostrar advertencia de frase de contraseña" +#: src/seedsigner/models/settings_definition.py +msgid "Show QR brightness tips" +msgstr "Ajustar brillo en QR" -#: src/seedsigner/models/settings_definition.py:479 +#: src/seedsigner/models/settings_definition.py msgid "Show partner logos" -msgstr "Mostrar logos de nuestros asociados" +msgstr "Logos de patrocinadores" -#: src/seedsigner/models/settings_definition.py:494 +#: src/seedsigner/models/settings_definition.py msgid "QR background color" msgstr "Color de fondo del QR" -#: src/seedsigner/views/main_menu_views.py:18 -msgid "Scan" -msgstr "Escanear" - -#: src/seedsigner/views/main_menu_views.py:19 -msgid "Seeds" -msgstr "Semillas" - -#: src/seedsigner/views/main_menu_views.py:20 -#: src/seedsigner/views/tools_views.py:29 -msgid "Tools" -msgstr "Útiles" - -#: src/seedsigner/views/main_menu_views.py:38 -msgid "Restart" -msgstr "Reiniciar" - -#: src/seedsigner/views/main_menu_views.py:39 -msgid "Power Off" -msgstr "Apagar" - -#: src/seedsigner/views/main_menu_views.py:42 -msgid "Reset / Power" -msgstr "Reiniciar / Encender" - -#: src/seedsigner/views/psbt_views.py:36 -#: src/seedsigner/views/seed_views.py:1365 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +#: src/seedsigner/views/tools_views.py msgid "Scan a seed" -msgstr "Escanear una semilla" +msgstr "Escanear semilla" -#: src/seedsigner/views/psbt_views.py:37 -#: src/seedsigner/views/seed_views.py:1366 -msgid "Enter 12/24 words" -msgstr "Entrar 12/24 palabras" +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +#: src/seedsigner/views/tools_views.py +msgid "Enter 12-word seed" +msgstr "Teclea 12 palabras" -#: src/seedsigner/views/psbt_views.py:53 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +#: src/seedsigner/views/tools_views.py +msgid "Enter 24-word seed" +msgstr "Teclea 24 palabras" + +#: src/seedsigner/views/psbt_views.py msgid "Select Signer" -msgstr "Seleccionar firmante" +msgstr "Seleccionar Firmante" -#: src/seedsigner/views/psbt_views.py:88 +#: src/seedsigner/views/psbt_views.py msgid "Parsing PSBT..." msgstr "Analizando PSBT..." -#: src/seedsigner/views/psbt_views.py:160 +#: src/seedsigner/views/psbt_views.py msgid "Unsupported Script Type!" -msgstr "¡Tipo de Script no soportado!" +msgstr "¡Script no soportado!" -#: src/seedsigner/views/psbt_views.py:161 -msgid "" -"PSBT has unsupported input script type, please verify your change addresses." +#: src/seedsigner/views/psbt_views.py +msgid "PSBT has unsupported input script type, please verify your change addresses." msgstr "" "Tipo de script PSBT no soportado.\n" -"Por favor verificar su dirección de cambio." +"Por favor verifica tus direcciones de cambio." -#: src/seedsigner/views/psbt_views.py:162 -#: src/seedsigner/views/psbt_views.py:181 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/view.py msgid "Continue" msgstr "Continuar" #. User will receive no change back; the inputs to this transaction are fully #. spent -#: src/seedsigner/views/psbt_views.py:179 +#: src/seedsigner/views/psbt_views.py msgid "Full Spend!" msgstr "¡Gasto Completo!" -#: src/seedsigner/views/psbt_views.py:180 -msgid "" -"This PSBT spends its entire input value. No change is coming back to your " -"wallet." -msgstr "" -"Este PSBT gasta todo su valor de entrada. No hay cambio de vuelta a tu " -"billetera." +#: src/seedsigner/views/psbt_views.py +msgid "This PSBT spends its entire input value. No change is coming back to your wallet." +msgstr "Este PSBT gasta todo su valor de entrada. No hay cambio de vuelta a tu billetera." #. Future-tense used to indicate that this transaction will send this amount, -#. as opposed to "Send" on its own which could be misread as an instant -#. command +#. as opposed to "Send" on its own which could be misread as an instant command #. (e.g. "Send Now"). -#: src/seedsigner/views/psbt_views.py:247 +#: src/seedsigner/views/psbt_views.py msgid "Will Send" msgstr "Se Enviará" -#: src/seedsigner/views/psbt_views.py:253 +#: src/seedsigner/views/psbt_views.py msgid "Next Recipient" msgstr "Próximo Destinatario" -#. Short for "Next step" -#: src/seedsigner/views/psbt_views.py:256 -#: src/seedsigner/views/psbt_views.py:325 -#: src/seedsigner/views/seed_views.py:830 -msgid "Next" -msgstr "Próximo" - #. The amount you're receiving back from the transaction -#: src/seedsigner/views/psbt_views.py:329 +#: src/seedsigner/views/psbt_views.py msgid "Your Change" msgstr "Tu Cambio" -#: src/seedsigner/views/psbt_views.py:330 +#: src/seedsigner/views/psbt_views.py msgid "Verify Multisig Change" msgstr "Verificar Camio de Multifirma" -#: src/seedsigner/views/psbt_views.py:332 +#: src/seedsigner/views/psbt_views.py msgid "Self-Transfer" msgstr "Autotransferencia" -#: src/seedsigner/views/psbt_views.py:333 +#: src/seedsigner/views/psbt_views.py msgid "Verify Multisig Addr" -msgstr "Verificar Dirección de Multifirma" +msgstr "Verificar dir. de Multifirma" -#: src/seedsigner/views/psbt_views.py:352 +#: src/seedsigner/views/psbt_views.py msgid "Verifying Change..." msgstr "Verificando Cambio..." -#: src/seedsigner/views/psbt_views.py:354 +#: src/seedsigner/views/psbt_views.py msgid "Verifying Self-Transfer..." msgstr "Verificando Autotransferencia" #. Variable is either "change" or "self-transfer". -#: src/seedsigner/views/psbt_views.py:437 -msgid "" -"PSBT's {} address could not be verified with your multisig wallet " -"descriptor." -msgstr "" -"Dirección de PSBT {} no se pudo verificar con tu descriptor de billetera " -"multifirma." +#: src/seedsigner/views/psbt_views.py +msgid "PSBT's {} address could not be verified with your multisig wallet descriptor." +msgstr "Dirección de PSBT {} no se pudo verificar con tu descriptor de billetera multifirma." -#: src/seedsigner/views/psbt_views.py:439 +#: src/seedsigner/views/psbt_views.py msgid "Suspicious PSBT" msgstr "PSBT sospechoso" #. Variable is either "change" or "self-transfer". -#: src/seedsigner/views/psbt_views.py:441 +#: src/seedsigner/views/psbt_views.py msgid "PSBT's {} address could not be generated from your seed." msgstr "Dirección de PSBT {} no se pudo generar de tu semilla." -#: src/seedsigner/views/psbt_views.py:445 +#: src/seedsigner/views/psbt_views.py msgid "Address Verification Failed" -msgstr "Fracaso Verificando Dirección" +msgstr "Falló la Verificando de Dirección" -#: src/seedsigner/views/psbt_views.py:447 +#: src/seedsigner/views/psbt_views.py msgid "Discard PSBT" msgstr "Descartar PSBT" -#: src/seedsigner/views/psbt_views.py:472 +#: src/seedsigner/views/psbt_views.py msgid "Approve PSBT" msgstr "Aprobar PSBT" -#: src/seedsigner/views/psbt_views.py:477 -msgid "Signing PSBT..." -msgstr "Firmando PSBT..." +#: src/seedsigner/views/psbt_views.py +msgid "Select Diff Seed" +msgstr "Seleccionar Otra Semilla" -#: src/seedsigner/views/psbt_views.py:556 +#: src/seedsigner/views/psbt_views.py msgid "PSBT Error" msgstr "Error en el PSBT" -#: src/seedsigner/views/psbt_views.py:558 +#: src/seedsigner/views/psbt_views.py msgid "Signing Failed" -msgstr "Fracaso Firmando" +msgstr "Falló La Firma" -#: src/seedsigner/views/psbt_views.py:559 +#: src/seedsigner/views/psbt_views.py msgid "Signing with this seed did not add a valid signature." msgstr "Firmando con esta semilla no produjo una firma válida." -#: src/seedsigner/views/psbt_views.py:560 -msgid "Select Diff Seed" -msgstr "Seleccionar Diferente Semilla" - #. This is on the opening splash screen, displayed above the HRF logo -#: src/seedsigner/views/screensaver.py:74 +#: src/seedsigner/views/screensaver.py msgid "With support from:" msgstr "Con apoyo de:" -#: src/seedsigner/views/seed_views.py:53 +#: src/seedsigner/views/seed_views.py msgid "Load a seed" -msgstr "Carga una semilla" +msgstr "Cargar semilla" -#: src/seedsigner/views/seed_views.py:56 +#: src/seedsigner/views/seed_views.py msgid "In-Memory Seeds" -msgstr "Semillas en Memoria" +msgstr "Semillas cargadas" -#: src/seedsigner/views/seed_views.py:77 +#: src/seedsigner/views/seed_views.py +msgid "Load the seed to verify" +msgstr "Cargar semilla para verificar" + +#: src/seedsigner/views/seed_views.py +msgid "Select seed to verify" +msgstr "Selecciona semilla para verificar" + +#: src/seedsigner/views/seed_views.py +msgid "Load the seed to sign with" +msgstr "Carga la semilla para firmar" + +#: src/seedsigner/views/seed_views.py +msgid "Select seed to sign with" +msgstr "Elige la semilla para firmar" + +#: src/seedsigner/views/seed_views.py msgid "Scan a SeedQR" msgstr "Escanear SeedQR" -#: src/seedsigner/views/seed_views.py:78 -msgid "Enter 24-word seed" -msgstr "Entrar semilla de 24 palabras" - -#: src/seedsigner/views/seed_views.py:79 -msgid "Enter 12-word seed" -msgstr "Entrar semilla de 12 palabras" - -#: src/seedsigner/views/seed_views.py:80 +#: src/seedsigner/views/seed_views.py msgid "Create a seed" msgstr "Crear una semilla" -#: src/seedsigner/views/seed_views.py:89 +#: src/seedsigner/views/seed_views.py msgid "Load A Seed" -msgstr "Carga una Semilla" +msgstr "Carga Semilla" #. Inserts the word number (e.g. "Seed Word #6") -#: src/seedsigner/views/seed_views.py:126 +#: src/seedsigner/views/seed_views.py msgid "Seed Word #{}" msgstr "Palabra Clave #{}" -#: src/seedsigner/views/seed_views.py:187 +#: src/seedsigner/views/seed_views.py msgid "Review & Edit" msgstr "Revisar y Editar" -#: src/seedsigner/views/seed_views.py:188 -#: src/seedsigner/views/seed_views.py:343 -#: src/seedsigner/views/tools_views.py:259 +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py msgid "Discard" msgstr "Descartar" -#: src/seedsigner/views/seed_views.py:192 +#: src/seedsigner/views/seed_views.py msgid "Invalid Mnemonic!" msgstr "¡Frase Mnemónica inválida!" -#: src/seedsigner/views/seed_views.py:194 +#: src/seedsigner/views/seed_views.py msgid "Checksum failure; not a valid seed phrase." -msgstr "Fallo de suma de comprobación; la semilla no es válida." +msgstr "Fallo de comprobación; la semilla no es válida." -#: src/seedsigner/views/seed_views.py:229 -#: src/seedsigner/views/seed_views.py:301 -#: src/seedsigner/views/seed_views.py:831 -#: src/seedsigner/views/seed_views.py:1189 +#: src/seedsigner/views/seed_views.py src/seedsigner/views/view.py msgid "Done" msgstr "Finalizado" -#: src/seedsigner/views/seed_views.py:260 -msgid "" -"BIP-39 passphrase changes your seed, resulting in new xpubs, wallets, etc.\n" -"Be sure you know what you're doing!" -msgstr "" -"Frases de contraseña BIP-39 cambian la semilla, resultando en nuevos xpubs, billeteras, etc.\n" -"¡Asegúrate de saber lo que estás haciendo!" - -#: src/seedsigner/views/seed_views.py:300 +#: src/seedsigner/views/seed_views.py msgid "Edit passphrase" -msgstr "Editar frase de contraseña" +msgstr "Modificar contraseña" -#: src/seedsigner/views/seed_views.py:342 +#: src/seedsigner/views/seed_views.py msgid "Keep Seed" -msgstr "Guardar Semilla" +msgstr "Mantener Semilla" -#: src/seedsigner/views/seed_views.py:348 +#: src/seedsigner/views/seed_views.py msgid "Discard Seed?" msgstr "¿Descartar Semilla?" -#: src/seedsigner/views/seed_views.py:350 +#: src/seedsigner/views/seed_views.py msgid "Wipe seed {} from the device?" msgstr "¿Borrar semilla {} del dispositivo?" -#: src/seedsigner/views/seed_views.py:384 +#: src/seedsigner/views/seed_views.py msgid "Scan PSBT" msgstr "Escanear PSBT" -#: src/seedsigner/views/seed_views.py:386 -#: src/seedsigner/views/seed_views.py:1689 +#: src/seedsigner/views/seed_views.py msgid "Verify Addr" msgstr "Verificar Dirección" -#: src/seedsigner/views/seed_views.py:389 +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Child Seed" +msgstr "Semilla Hija BIP-85" + +#: src/seedsigner/views/seed_views.py msgid "Discard Seed" msgstr "Descartar Semilla" -#: src/seedsigner/views/seed_views.py:464 +#: src/seedsigner/views/seed_views.py msgid "View Seed Words" msgstr "Ver Palabras Clave" -#: src/seedsigner/views/seed_views.py:465 +#: src/seedsigner/views/seed_views.py msgid "Export as SeedQR" msgstr "Exportar como SeedQR" -#: src/seedsigner/views/seed_views.py:652 -msgid "Privacy Leak!" -msgstr "¡Privacidad comprometida!" - -#: src/seedsigner/views/seed_views.py:653 +#: src/seedsigner/views/seed_views.py msgid "Xpub can be used to view all future transactions." msgstr "El Xpub puede ser usado para ver todas las transacciones futuras." -#: src/seedsigner/views/seed_views.py:683 +#: src/seedsigner/views/seed_views.py msgid "Generating xpub..." msgstr "Generando xpub..." -#: src/seedsigner/views/seed_views.py:805 +#: src/seedsigner/views/seed_views.py msgid "You must keep your seed words private & away from all online devices." +msgstr "Debes mantener tus palabras semilla en privado y lejos de dispositivos en línea." + +#: src/seedsigner/views/seed_views.py +msgid "Child #{}" +msgstr "Hija #{}" + +#: src/seedsigner/views/seed_views.py +msgid "Seed Words" +msgstr "Palabras Claves" + +#: src/seedsigner/views/seed_views.py +msgid "12 Words" +msgstr "12 Palabras" + +#: src/seedsigner/views/seed_views.py +msgid "24 Words" +msgstr "24 Palabras" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Num Words" +msgstr "Palabras BIP-85" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Index Error" msgstr "" -"Debes mantener tus palabras claves en privado y lejos de dispositivos en " -"línea." -#: src/seedsigner/views/seed_views.py:877 -msgid "Verify" -msgstr "Verificar" +#: src/seedsigner/views/seed_views.py +msgid "Invalid Child Index" +msgstr "" -#: src/seedsigner/views/seed_views.py:878 -msgid "Skip" -msgstr "Saltar" +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Child Index must be between 0 and 2^31-1." +msgstr "" -#: src/seedsigner/views/seed_views.py:927 -msgid "Verify Word #{}" -msgstr "Verificar Palabra #{}" - -#: src/seedsigner/views/seed_views.py:967 -msgid "Review Seed Words" -msgstr "Revisar Palabras Clave" - -#: src/seedsigner/views/seed_views.py:968 +#: src/seedsigner/views/seed_views.py msgid "Try Again" msgstr "Volver a Intentarlo" -#: src/seedsigner/views/seed_views.py:972 +#: src/seedsigner/views/seed_views.py +msgid "Verify" +msgstr "Verificar" + +#: src/seedsigner/views/seed_views.py +msgid "Skip" +msgstr "Saltar" + +#: src/seedsigner/views/seed_views.py +msgid "Verify Word #{}" +msgstr "Verificar Palabra #{}" + +#: src/seedsigner/views/seed_views.py +msgid "Review Seed Words" +msgstr "Revisar Palabras Clave" + +#: src/seedsigner/views/seed_views.py msgid "Verification Error" msgstr "Error en Verificación" -#: src/seedsigner/views/seed_views.py:974 +#: src/seedsigner/views/seed_views.py msgid "Wrong Word!" msgstr "¡Palabra Incorrecta!" -#: src/seedsigner/views/seed_views.py:975 +#: src/seedsigner/views/seed_views.py msgid "Word #{} is not \"{}\"!" msgstr "¡Palabra #{} no es \"{}\"!" -#: src/seedsigner/views/seed_views.py:993 +#: src/seedsigner/views/seed_views.py msgid "Backup Verified" -msgstr "Copias de deguridad verificada" +msgstr "Respaldo Verificado" -#: src/seedsigner/views/seed_views.py:996 +#: src/seedsigner/views/seed_views.py msgid "All mnemonic backup words were successfully verified!" -msgstr "" -"¡Se verificó exitosamente el backup de todas las palabras de la frase " -"mnemónica!" +msgstr "¡Todas las palabras del respaldo han sido verificadas correctamente!" -#: src/seedsigner/views/seed_views.py:1019 +#: src/seedsigner/views/seed_views.py msgid "Standard: 25x25" -msgstr "25x25 estándar" +msgstr "Estándar: 25x25" -#: src/seedsigner/views/seed_views.py:1020 +#: src/seedsigner/views/seed_views.py msgid "Compact: 21x21" -msgstr "21x21 compacto" +msgstr "Compacto: 21x21" -#: src/seedsigner/views/seed_views.py:1024 +#: src/seedsigner/views/seed_views.py msgid "Standard: 29x29" -msgstr "29x29 estándar" +msgstr "Standard: 29x29" -#: src/seedsigner/views/seed_views.py:1025 +#: src/seedsigner/views/seed_views.py msgid "Compact: 25x25" -msgstr "25x25 compacto" +msgstr "Compacto: 25x25" -#: src/seedsigner/views/seed_views.py:1046 +#: src/seedsigner/views/seed_views.py msgid "SeedQR Format" msgstr "Formato SeedQR" -#: src/seedsigner/views/seed_views.py:1095 +#: src/seedsigner/views/seed_views.py msgid "SeedQR is your private key!" -msgstr "¡SeedQR es tu clave privada/semilla!" +msgstr "SeedQR es tu clave privada!" -#: src/seedsigner/views/seed_views.py:1096 -msgid "Never photograph it or scan it into an online device." -msgstr "Nunca fotografíar o escanear en un dispositivo en línea." +#: src/seedsigner/views/seed_views.py +msgid "Never photograph or scan it into a device that connects to the internet." +msgstr "Nunca lo fotografíes o escanees en un dispositivo conectado a internet." -#: src/seedsigner/views/seed_views.py:1188 -#: src/seedsigner/views/seed_views.py:1232 -#: src/seedsigner/views/seed_views.py:1243 -#: src/seedsigner/views/seed_views.py:1255 +#: src/seedsigner/views/seed_views.py msgid "Confirm SeedQR" msgstr "Confirmar SeedQR" -#: src/seedsigner/views/seed_views.py:1193 +#: src/seedsigner/views/seed_views.py msgid "Confirm SeedQR?" -msgstr "¿Confirmar SeedQR?" +msgstr "Confirmar SeedQR?" -#: src/seedsigner/views/seed_views.py:1223 +#: src/seedsigner/views/seed_views.py msgid "Scan your SeedQR" -msgstr "Escanear tu SeedQR" +msgstr "Escanea tu SeedQR" -#: src/seedsigner/views/seed_views.py:1233 -#: src/seedsigner/views/seed_views.py:1256 +#: src/seedsigner/views/seed_views.py msgid "Error!" msgstr "¡Error!" -#: src/seedsigner/views/seed_views.py:1234 +#: src/seedsigner/views/seed_views.py msgid "Your transcribed SeedQR does not match your original seed!" -msgstr "¡El transcrito del SeedQR no coincide con tu semilla original!" +msgstr "¡Tu SeedQR transcrito no coincide con tu semilla original!" -#: src/seedsigner/views/seed_views.py:1236 -#: src/seedsigner/views/seed_views.py:1259 +#: src/seedsigner/views/seed_views.py msgid "Review SeedQR" msgstr "Revisar SeedQR" -#: src/seedsigner/views/seed_views.py:1245 -msgid "" -"Your transcribed SeedQR successfully scanned and yielded the same seed." -msgstr "El transcrito del SeedQR escaneo y produjo la misma semilla." +#: src/seedsigner/views/seed_views.py +msgid "Your transcribed SeedQR successfully scanned and yielded the same seed." +msgstr "Tu SeedQR transcrito y escaneado produjo la misma semilla correctamente." -#: src/seedsigner/views/seed_views.py:1257 +#: src/seedsigner/views/seed_views.py msgid "Your transcribed SeedQR could not be read!" -msgstr "¡El transcrito del SeedQR no pudo ser leído!" +msgstr "¡Tu SeedQR transcrito no se pudo leer!" -#: src/seedsigner/views/seed_views.py:1328 +#: src/seedsigner/views/seed_views.py msgid "Sig type can't be auto-detected from this address. Please specify:" -msgstr "" -"El tipo de firma no puede ser autodetectado para esta dirección. Por favor " -"especificar:" +msgstr "El tipo de firma se puede autodetectado para esta dirección. Por favor especifica:" -#: src/seedsigner/views/seed_views.py:1369 -msgid "Load the seed to verify" -msgstr "Cargar la semilla para verificar" - -#: src/seedsigner/views/seed_views.py:1379 -msgid "Select seed to verify" -msgstr "Seleccione semilla para verificar" - -#: src/seedsigner/views/seed_views.py:1431 +#: src/seedsigner/views/seed_views.py msgid "Can't validate a single sig addr without specifying a seed" -msgstr "" -"No se puede validar una dirección de firma individual sin especificar una " -"semilla" +msgstr "No se puede validar una dirección de firma individual sin seleccionar una semilla" #. Option when scanning for a matching address; skips ten addresses ahead -#: src/seedsigner/views/seed_views.py:1477 +#: src/seedsigner/views/seed_views.py msgid "Skip 10" msgstr "Saltar 10" -#: src/seedsigner/views/seed_views.py:1478 -#: src/seedsigner/views/seed_views.py:1659 +#: src/seedsigner/views/seed_views.py msgid "Cancel" msgstr "Cancelar" -#: src/seedsigner/views/seed_views.py:1606 -#: src/seedsigner/views/seed_views.py:1622 -msgid "Taproot verification not yet implemented!" -msgstr "¡La verificación de Taproot todavía no está implementada!" - -#: src/seedsigner/views/seed_views.py:1618 -msgid "Legacy P2PKH verification not implemented!" -msgstr "¡La verificación P2PKH heredadas no está todavía implementada!" - -#: src/seedsigner/views/seed_views.py:1643 +#: src/seedsigner/views/seed_views.py msgid "multisig" msgstr "multifirma" -#: src/seedsigner/views/seed_views.py:1645 +#: src/seedsigner/views/seed_views.py msgid "seed {}" msgstr "semilla {}" -#: src/seedsigner/views/seed_views.py:1648 +#: src/seedsigner/views/seed_views.py msgid "Address Verified" msgstr "Dirección Verificada" -#: src/seedsigner/views/seed_views.py:1658 +#: src/seedsigner/views/seed_views.py msgid "Scan Descriptor" msgstr "Escanear Descriptor" -#: src/seedsigner/views/seed_views.py:1688 +#: src/seedsigner/views/seed_views.py msgid "Return to PSBT" msgstr "Regresar al PSBT" -#: src/seedsigner/views/settings_views.py:20 +#: src/seedsigner/views/settings_views.py msgid "I/O test" -msgstr "Examen de hardware" +msgstr "Test de hardware" -#: src/seedsigner/views/settings_views.py:39 -#: src/seedsigner/views/settings_views.py:46 +#: src/seedsigner/views/settings_views.py msgid "Advanced" msgstr "Avanzado" -#: src/seedsigner/views/settings_views.py:54 +#: src/seedsigner/views/settings_views.py msgid "Dev Options" -msgstr "Opciones Desarrollador" +msgstr "Opciones Desarrollo" -#: src/seedsigner/views/tools_views.py:24 -msgid " New seed" -msgstr "Nueva semilla" +#: src/seedsigner/views/settings_views.py +msgid "Persistent Settings enabled. Settings saved to SD card." +msgstr "Confirguración persistente activada y guardada en la SD" -#: src/seedsigner/views/tools_views.py:25 +#: src/seedsigner/views/settings_views.py +msgid "Settings updated in temporary memory" +msgstr "Configuración actualizada en memoria temporalmente" + +#: src/seedsigner/views/tools_views.py msgid "New seed" msgstr "Nueva semilla" -#: src/seedsigner/views/tools_views.py:26 +#: src/seedsigner/views/tools_views.py msgid "Calc 12th/24th word" -msgstr "Calcular palabra 12º/24º" +msgstr "Calc. última palabra" -#: src/seedsigner/views/tools_views.py:102 -#: src/seedsigner/views/tools_views.py:228 +#: src/seedsigner/views/tools_views.py src/seedsigner/views/view.py +msgid "Tools" +msgstr "Útiles" + +#: src/seedsigner/views/tools_views.py msgid "12 words" msgstr "12 palabras" -#: src/seedsigner/views/tools_views.py:103 -#: src/seedsigner/views/tools_views.py:229 +#: src/seedsigner/views/tools_views.py msgid "24 words" msgstr "24 palabras" -#: src/seedsigner/views/tools_views.py:107 +#: src/seedsigner/views/tools_views.py msgid "Mnemonic Length?" -msgstr "¿Longitud de la frase Mnemónica?" +msgstr "Generar mnemonic de:" -#: src/seedsigner/views/tools_views.py:174 +#: src/seedsigner/views/tools_views.py msgid "12 words (50 rolls)" msgstr "12 palabras (50 tiradas)" -#: src/seedsigner/views/tools_views.py:175 +#: src/seedsigner/views/tools_views.py msgid "24 words (99 rolls)" msgstr "24 palabras (99 tiradas)" -#: src/seedsigner/views/tools_views.py:179 -#: src/seedsigner/views/tools_views.py:233 +#: src/seedsigner/views/tools_views.py msgid "Mnemonic Length" msgstr "Longitud de la frase Mnemónica" -#: src/seedsigner/views/tools_views.py:258 +#. Label to gather entropy through coin tosses +#: src/seedsigner/views/tools_views.py +msgid "Coin flip entropy" +msgstr "Entropía Cara o cruz" + +#. Label to gather entropy through user specified BIP-39 word +#: src/seedsigner/views/tools_views.py +msgid "Word selection entropy" +msgstr "Selección de palabras" + +#. Label to allow user to default entropy as all-zeros +#: src/seedsigner/views/tools_views.py +msgid "Finalize with zeros" +msgstr "Finalizar con ceros" + +#. label to calculate the last word of a BIP-39 mnemonic seed phrase +#: src/seedsigner/views/tools_views.py +msgid "Final Word Calc" +msgstr "Calc. Última Palabra" + +#: src/seedsigner/views/tools_views.py msgid "Load seed" msgstr "Cargar semilla" -#: src/seedsigner/views/view.py:124 +#: src/seedsigner/views/tools_views.py +msgid "Scan wallet descriptor" +msgstr "Escanear descriptor" + +#. label for addresses where others send us incoming payments +#: src/seedsigner/views/tools_views.py +msgid "Receive Addresses" +msgstr "De Recepción" + +#. label for addresses that collect the change from our own outgoing payments +#: src/seedsigner/views/tools_views.py +msgid "Change Addresses" +msgstr "De Cambio" + +#. a status message that our payment addresses are being calculated +#: src/seedsigner/views/tools_views.py +msgid "Calculating addrs..." +msgstr "Calculando direcciones..." + +#: src/seedsigner/views/tools_views.py +msgid "Next {}" +msgstr "Próxima {}" + +#: src/seedsigner/views/tools_views.py +msgid "Receive Addrs" +msgstr "Dirs. Recepción" + +#: src/seedsigner/views/tools_views.py +msgid "Change Addrs" +msgstr "Dirs. Cambio" + +#: src/seedsigner/views/view.py +msgid "Scan" +msgstr "Escanear" + +#: src/seedsigner/views/view.py +msgid "Seeds" +msgstr "Semillas" + +#: src/seedsigner/views/view.py +msgid "Restart" +msgstr "Reiniciar" + +#: src/seedsigner/views/view.py +msgid "Power Off" +msgstr "Apagar" + +#: src/seedsigner/views/view.py +msgid "Reset / Power" +msgstr "Reiniciar / Apagar" + +#: src/seedsigner/views/view.py msgid "Work In Progress" -msgstr "Trabajo en Proceso" +msgstr "Trabajo en curso" -#: src/seedsigner/views/view.py:125 +#: src/seedsigner/views/view.py msgid "Not Yet Implemented" -msgstr "Aún no se ha implementado" +msgstr "Todavía sin implementar" -#: src/seedsigner/views/view.py:126 +#: src/seedsigner/views/view.py msgid "This is still on our to-do list!" msgstr "¡Todavía en la lista de cosas para hacer!" -#: src/seedsigner/views/view.py:127 +#: src/seedsigner/views/view.py msgid "Back to Main Menu" -msgstr "Volver al menú principal" +msgstr "Volver al Inicio" -#: src/seedsigner/views/view.py:142 +#: src/seedsigner/views/view.py +msgid "Error" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Network Mismatch" +msgstr "Discrepancia de red" + +#: src/seedsigner/views/view.py +msgid "Change Setting" +msgstr "Cambiar Configuración" + +#: src/seedsigner/views/view.py msgid "System Error" msgstr "Error del Sistema" + +#: src/seedsigner/views/view.py +msgid "Update Setting" +msgstr "Actualizar Configuración" + +#: src/seedsigner/views/view.py +msgid "Option Disabled" +msgstr "Opción Deshabilitada" + +#: src/seedsigner/views/view.py +msgid "Security Tip" +msgstr "Consejo de Seguridad" + +#: src/seedsigner/views/view.py +msgid "" +"For maximum security,\n" +"remove the MicroSD card\n" +"before continuing." +msgstr "" +"Para mayor seguridad,\n" +"retira la MicroSD\n" +"antes de continuar." + +#~ msgid "{}..." +#~ msgstr "{}..." + +#~ msgid "Signed PSBT" +#~ msgstr "PSBT Firmado" + +#~ msgid "Export as a QR code for:" +#~ msgstr "Exportar como código QR para:" + +#~ msgid "Enter 12/24 words" +#~ msgstr "Entrar 12/24 palabras" + +#~ msgid "Signing PSBT..." +#~ msgstr "Firmando PSBT..." + +#~ msgid "Taproot verification not yet implemented!" +#~ msgstr "¡La verificación de Taproot todavía no está implementada!" + +#~ msgid "Legacy P2PKH verification not implemented!" +#~ msgstr "¡La verificación P2PKH heredadas no está todavía implementada!" + +#~ msgid "More entropy" +#~ msgstr "" + +#~ msgid "English" +#~ msgstr "" + +#~ msgid "Add Passphrase" +#~ msgstr "Agregar Frase de Contraseña" + +#~ msgid "Btc-only" +#~ msgstr "Btc-Solamente" + +#~ msgid "Sats-only" +#~ msgstr "Sats-Solamente" + +#~ msgid "Scan" +#~ msgstr "" + +#~ msgid "Seeds" +#~ msgstr "" + +#~ msgid "Restart" +#~ msgstr "" + +#~ msgid "Power Off" +#~ msgstr "" + +#~ msgid "Reset / Power" +#~ msgstr "Reiniciar / Encender" + +#~ msgid " (?)" +#~ msgstr "" + +#~ msgid "Scan a QR code" +#~ msgstr "Escanear un código QR" + +#~ msgid "Yes " +#~ msgstr "Si" + +#~ msgid "No " +#~ msgstr "No" + +#~ msgid "< back | Scan a QR code" +#~ msgstr "" + +#~ msgid "Settings QR" +#~ msgstr "Ajustes QR" + +#~ msgid "Settings imported successfully!" +#~ msgstr "¡Ajustes importados exitosamente!" + +#~ msgid "MicroSD removed" +#~ msgstr "MicroSD sacada" + +#~ msgid "MicroSD inserted" +#~ msgstr "MicroSD insertada" + +#~ msgid "changes seed:" +#~ msgstr "cambia la semilla:" + +#~ msgid "MicroSD card is removed" +#~ msgstr "" + +#~ msgid "Show passphrase warning" +#~ msgstr "Mostrar advertencia de frase de contraseña" + +#~ msgid "" +#~ "BIP-39 passphrase changes your seed, resulting in new xpubs, wallets, etc.\n" +#~ "Be sure you know what you're doing!" +#~ msgstr "" +#~ "Frases de contraseña BIP-39 cambian la semilla, resultando en nuevos xpubs, billeteras, etc.\n" +#~ "¡Asegúrate de saber lo que estás haciendo!" + +#~ msgid "< back | click joystick" +#~ msgstr "< volver | clic joystick" + +#~ msgid "Threshold at 0.01" +#~ msgstr "Límite a 0.01" + +#~ msgid "BTC | sats hybrid" +#~ msgstr "Híbrido: BTC | Sats " + diff --git a/src/seedsigner/resources/babel/fr/LC_MESSAGES/messages.po b/src/seedsigner/resources/babel/fr/LC_MESSAGES/messages.po index 0b5d5ab4..34c02339 100644 --- a/src/seedsigner/resources/babel/fr/LC_MESSAGES/messages.po +++ b/src/seedsigner/resources/babel/fr/LC_MESSAGES/messages.po @@ -1,38 +1,209 @@ -# Translations template for PROJECT. +# French translations for PROJECT. # Copyright (C) 2022 ORGANIZATION # This file is distributed under the same license as the PROJECT project. # FIRST AUTHOR , 2022. -# +# # Translators: # Keith Mukai, 2022 # Jp Prudhomme , 2022 -# +# jdlcdl, 2022-2023 #, fuzzy msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-05-10 17:32-0500\n" +"POT-Creation-Date: 2024-02-23 18:47-0400\n" "PO-Revision-Date: 2022-04-29 13:50+0000\n" "Last-Translator: Jp Prudhomme , 2022\n" +"Language: fr\n" "Language-Team: French (https://www.transifex.com/seedsigner/teams/135832/fr/)\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" "MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" +"Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.10.1\n" -"Language: fr\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: src/seedsigner/gui/screens/main_menu_screens.py:12 -#: src/seedsigner/gui/screens/scan_screens.py:140 -msgid "Home" -msgstr "Accueil" +#. The abbreviated label for the special key on a standard keyboard. +#: src/seedsigner/gui/keyboard.py +msgid "del" +msgstr "sup" -#: src/seedsigner/gui/screens/main_menu_screens.py:24 +#. The abbreviated label for the special key on a standard keyboard. +#: src/seedsigner/gui/keyboard.py +msgid "space" +msgstr "espace" + +#: src/seedsigner/gui/toast.py +msgid "" +"You can remove\n" +"the SD card now" +msgstr "" +"Vous pouvez retirer\n" +"la carte SD" + +#: src/seedsigner/gui/toast.py +msgid "SD card removed" +msgstr "Carte SD retiré" + +#: src/seedsigner/gui/toast.py +msgid "SD card inserted" +msgstr "Carte SD inseré" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review PSBT" +msgstr "Examiner PSBT" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review Details" +msgstr "Examiner Détails" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "1 input" +msgstr "1 entrée" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input 1" +msgstr "entrée 1" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input 2" +msgstr "entrée 2" + +#. Indicates that items have been omitted from a series: e.g. "1, 2, 3, [...], +#. 8" +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "[ ... ]" +msgstr "[ ... ]" + +#. Input number will be inserted (e.g. "input 3") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input {}" +msgstr "entrée {}" + +#. Ellipsis ("...") characters used to truncate an address (e.g. "bc1qabc...") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "..." +msgstr "..." + +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/views/psbt_views.py +msgid "self-transfer" +msgstr "auto-transfert" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient 1" +msgstr "destinataire 1" + +#. Inserts the recipient number (e.g. the fifth one is: "recipient 5") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient {}" +msgstr "déstinataire {}" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "fee" +msgstr "frais" + +#. Label for a change output in the PSBT Overview flow diagram +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/views/psbt_views.py +msgid "change" +msgstr "monnaie" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "PSBT Math" +msgstr "Math PSBT" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review Recipients" +msgstr "Examiner Destinataires" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "btc" +msgstr "btc" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "sats" +msgstr "sats" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input" +msgid_plural "inputs" +msgstr[0] "entrée" +msgstr[1] "entrées" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient" +msgid_plural "recipients" +msgstr[0] "destinataire" +msgstr[1] "destinataires" + +#. Denonination is inserted (e.g. your "btc change" or "sats change") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "{} change" +msgstr "{} monnaie" + +#. First variable is either "Multisig" or the fingerprint for single sig; +#. Second is "Change" or "Addr" (change vs receive addr); Third is the address +#. index number (e.g. your #4 receive addr). +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/models/settings_definition.py +#: src/seedsigner/views/seed_views.py +msgid "Multisig" +msgstr "Multisig" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Change" +msgstr "Monnaie" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Addr" +msgstr "Adr" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Address verified!" +msgstr "Adresse vérifiée!" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Sign PSBT" +msgstr "Signer PSBT" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Click to approve this transaction" +msgstr "Cliquer pour approuver cette transaction" + +#: src/seedsigner/gui/screens/scan_screens.py +msgid "< back | " +msgstr "< arriere | " + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "Success!" +msgstr "Succès!" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "OK" +msgstr "OK" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/psbt_views.py +msgid "Caution" +msgstr "Attention" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "Privacy Leak!" +msgstr "Brèche Confidentialité!" + +#: src/seedsigner/gui/screens/screen.py +msgid "I Understand" +msgstr "Je comprends" + +#: src/seedsigner/gui/screens/screen.py +msgid "Classified Info!" +msgstr "Info Confidentielle!" + +#: src/seedsigner/gui/screens/screen.py msgid "Restarting" msgstr "Redémarrage" -#: src/seedsigner/gui/screens/main_menu_screens.py:29 +#: src/seedsigner/gui/screens/screen.py msgid "" "SeedSigner is restarting.\n" "\n" @@ -42,1071 +213,1216 @@ msgstr "" "\n" "Toutes les données en mémoire seront effacées." -#: src/seedsigner/gui/screens/main_menu_screens.py:39 +#: src/seedsigner/gui/screens/screen.py msgid "Powering Off" -msgstr "Extinction en cours" +msgstr "Arrêt en cours" -#: src/seedsigner/gui/screens/main_menu_screens.py:44 +#: src/seedsigner/gui/screens/screen.py msgid "Please wait about 30 seconds before disconnecting power." -msgstr "" -"Veuillez patienter à peu près 30 secondes avant d'enlever l'alimentation. " +msgstr "Veuillez patienter environ 30 secondes avant de débrancher l'alimentation." -#: src/seedsigner/gui/screens/psbt_screens.py:29 -#: src/seedsigner/views/seed_views.py:352 -msgid "Review PSBT" -msgstr "Passer en revue la PSBT" - -#: src/seedsigner/gui/screens/psbt_screens.py:31 -msgid "Review Details" -msgstr "Passer en revue les détails" - -#: src/seedsigner/gui/screens/psbt_screens.py:88 -msgid "1 input" -msgstr "1 entrée" - -#: src/seedsigner/gui/screens/psbt_screens.py:90 -msgid "input 1" -msgstr "Entrée 1" - -#: src/seedsigner/gui/screens/psbt_screens.py:91 -msgid "input 2" -msgstr "Entrée 2" - -#. Indicates that items have been omitted from a series: e.g. "1, 2, 3, [...], -#. 8" -#: src/seedsigner/gui/screens/psbt_screens.py:93 -#: src/seedsigner/gui/screens/psbt_screens.py:141 -msgid "[ ... ]" -msgstr "[ ... ]" - -#. Input number will be inserted (e.g. "input 3") -#: src/seedsigner/gui/screens/psbt_screens.py:95 -#: src/seedsigner/gui/screens/psbt_screens.py:96 -#: src/seedsigner/gui/screens/psbt_screens.py:99 -msgid "input {}" -msgstr "entrée {}" - -#. Display a truncated address (e.g. "bc1qabc...") -#: src/seedsigner/gui/screens/psbt_screens.py:127 -msgid "{}..." -msgstr "{}..." - -#: src/seedsigner/gui/screens/psbt_screens.py:136 -msgid "self-transfer" -msgstr "transfert à soi-même " - -#: src/seedsigner/gui/screens/psbt_screens.py:139 -msgid "recipient 1" -msgstr "destinataire 1" - -#. Inserts the recipient number (e.g. the fifth one is: "recipient 5") -#: src/seedsigner/gui/screens/psbt_screens.py:143 -msgid "recipient {}" -msgstr "déstinataire {}" - -#: src/seedsigner/gui/screens/psbt_screens.py:145 -#: src/seedsigner/gui/screens/psbt_screens.py:555 -msgid "fee" -msgstr "frais" - -#. Label for a change output in the PSBT Overview flow diagram -#: src/seedsigner/gui/screens/psbt_screens.py:150 -msgid "change" -msgstr "monnaie" - -#: src/seedsigner/gui/screens/psbt_screens.py:461 -msgid "PSBT Math" -msgstr "Math PSBT" - -#: src/seedsigner/gui/screens/psbt_screens.py:462 -msgid "Review Recipients" -msgstr "Passer en revue les destinataires" - -#: src/seedsigner/gui/screens/psbt_screens.py:468 -#: src/seedsigner/gui/screens/psbt_screens.py:521 -msgid "btc" -msgstr "btc" - -#: src/seedsigner/gui/screens/psbt_screens.py:480 -msgid "sats" -msgstr "sats" - -#: src/seedsigner/gui/screens/psbt_screens.py:538 -msgid "input" -msgid_plural "inputs" -msgstr[0] "entrée" -msgstr[1] "entrées" - -#: src/seedsigner/gui/screens/psbt_screens.py:548 -msgid "recipient" -msgid_plural "recipients" -msgstr[0] "destinataire" -msgstr[1] "destinataires" - -#. Denonination is inserted (e.g. your "btc change" or "sats change") -#: src/seedsigner/gui/screens/psbt_screens.py:566 -msgid "{} change" -msgstr "{} rendu de monnaie" - -#: src/seedsigner/gui/screens/psbt_screens.py:671 -msgid "Address verified!" -msgstr "Adresse vérifiée !" - -#: src/seedsigner/gui/screens/psbt_screens.py:683 -msgid "Sign PSBT" -msgstr "Signer la PSBT" - -#: src/seedsigner/gui/screens/psbt_screens.py:697 -msgid "Click to authorize this transaction" -msgstr "Cliquer pour autoriser cette transaction" - -#: src/seedsigner/gui/screens/psbt_screens.py:707 -msgid "Signed PSBT" -msgstr "PSBT signée" - -#. Prompt to specify which coordinator software to use (e.g. Specter, Sparrow, -#. etc) -#: src/seedsigner/gui/screens/psbt_screens.py:713 -msgid "Export as a QR code for:" -msgstr "Exporter en tant que QRcode pour:" - -#: src/seedsigner/gui/screens/scan_screens.py:22 -msgid "Scan a QR code" -msgstr "Scan d'un QRcode" - -#: src/seedsigner/gui/screens/scan_screens.py:138 -msgid "Settings QR" -msgstr "QRcode Paramètres" - -#: src/seedsigner/gui/screens/scan_screens.py:156 -msgid "Settings imported successfully!" -msgstr "Paramètres importés avec succès!" - -#: src/seedsigner/gui/screens/screen.py:731 -#: src/seedsigner/views/seed_views.py:954 -#: src/seedsigner/views/seed_views.py:1201 -msgid "Success!" -msgstr "Succès!" - -#: src/seedsigner/gui/screens/screen.py:735 -#: src/seedsigner/views/seed_views.py:956 -#: src/seedsigner/views/seed_views.py:1204 -#: src/seedsigner/views/seed_views.py:1647 src/seedsigner/views/view.py:145 -msgid "OK" -msgstr "OK" - -#: src/seedsigner/gui/screens/screen.py:852 -#: src/seedsigner/views/psbt_views.py:430 -msgid "Caution" -msgstr "Attention" - -#: src/seedsigner/gui/screens/screen.py:855 -msgid "I Understand" -msgstr "Je comprends" - -#: src/seedsigner/gui/screens/screen.py:867 -msgid "Classified Info!" -msgstr "Info classifiée!" - -#: src/seedsigner/gui/screens/seed_screens.py:411 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Finalize Seed" -msgstr "Finaliser la seed" +msgstr "Finaliser Seed" -#: src/seedsigner/gui/screens/seed_screens.py:424 -#: src/seedsigner/gui/screens/tools_screens.py:291 +#. a label for the shortened Key-id of a BIP-32 master HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "fingerprint" -msgstr "Empreinte" +msgstr "empreinte" + +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Backup Seed" +msgstr "Archiver Seed" + +#. Additional explainer for the two seed backup options (mnemonic phrase and +#. SeedQR). +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Backups do not include your passphrase." +msgstr "L'archive n'inclut pas votre phrase secrète." #. Displays the page number and total: (e.g. page 1 of 6) -#: src/seedsigner/gui/screens/seed_screens.py:461 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Seed Words: {}/{}" msgstr "Mots seed: {}/{}" -#: src/seedsigner/gui/screens/seed_screens.py:541 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "BIP-85 Index" +msgstr "Index BIP-85" + +#: src/seedsigner/gui/screens/seed_screens.py msgid "Verify Backup?" -msgstr "Verifier la sauvegarde?" +msgstr "Verifier l'Archive?" -#: src/seedsigner/gui/screens/seed_screens.py:547 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Optionally verify that your mnemonic backup is correct." -msgstr "" -"Optionnellement verifier que votre sauvegarde des mnémoniques est correcte." +msgstr "Vérifier également que votre archive mnémonique est correcte." -#: src/seedsigner/gui/screens/seed_screens.py:556 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Derivation Path" -msgstr "Chemin de dérivation" +msgstr "Chemin Dérivation" -#: src/seedsigner/gui/screens/seed_screens.py:703 +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Export Xpub" +msgstr "Exporter Xpub" + +#: src/seedsigner/gui/screens/seed_screens.py msgid "Xpub Details" msgstr "Détails Xpub" -#: src/seedsigner/gui/screens/seed_screens.py:709 -#: src/seedsigner/gui/screens/seed_screens.py:713 -#: src/seedsigner/views/seed_views.py:354 -#: src/seedsigner/views/seed_views.py:471 -#: src/seedsigner/views/seed_views.py:504 -#: src/seedsigner/views/seed_views.py:578 -msgid "Export Xpub" -msgstr "Exporter la Xpub" - -#: src/seedsigner/gui/screens/seed_screens.py:722 +#. Short for "BIP32 Master Fingerprint" +#. a label for the shortened Key-id of a BIP-32 master HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Fingerprint" -msgstr "Empreintes " +msgstr "Empreinte" #. Short for "Derivation Path" -#: src/seedsigner/gui/screens/seed_screens.py:732 +#. a label for the derivation-path into a BIP-32 HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Derivation" msgstr "Dérivation" -#: src/seedsigner/gui/screens/seed_screens.py:741 +#. Short for "BIP32 Extended Public Key" +#: src/seedsigner/gui/screens/seed_screens.py msgid "Xpub" msgstr "Xpub" -#: src/seedsigner/gui/screens/seed_screens.py:754 -#: src/seedsigner/views/seed_views.py:217 -msgid "Add Passphrase" -msgstr "Ajouter une phrase secrète" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "BIP-39 Passphrase" +msgstr "Phrase Secrète" -#: src/seedsigner/gui/screens/seed_screens.py:1106 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Verify Passphrase" -msgstr "Verifier la phrase secrète" +msgstr "Verifier Phrase Secrète" #. Describes the effect of applying a BIP-39 passphrase; it changes the seed's #. fingerprint -#: src/seedsigner/gui/screens/seed_screens.py:1115 +#: src/seedsigner/gui/screens/seed_screens.py msgid "changes fingerprint" msgstr "change l'empreinte" #. Refers to the SeedQR type: Standard or Compact -#: src/seedsigner/gui/screens/seed_screens.py:1170 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Standard" msgstr "Standard" #. Briefly explains the Standard SeedQR data format -#: src/seedsigner/gui/screens/seed_screens.py:1172 +#: src/seedsigner/gui/screens/seed_screens.py msgid "BIP-39 wordlist indices" -msgstr "indexes de la liste des mots BIP-39" +msgstr "Indexes des mots BIP-39" #. Refers to the SeedQR type: Standard or Compact -#: src/seedsigner/gui/screens/seed_screens.py:1180 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Compact" msgstr "Compacte" #. Briefly explains the Compact SeedQR data format -#: src/seedsigner/gui/screens/seed_screens.py:1182 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Raw entropy bits" -msgstr "bits entropie brut" +msgstr "Entropie brute bits" -#: src/seedsigner/gui/screens/seed_screens.py:1196 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Transcribe SeedQR" -msgstr "Transcrire la SeedQR" +msgstr "Transcrire SeedQR" #. Refers to the QR code size: 21x21, 25x25, or 29x29 -#: src/seedsigner/gui/screens/seed_screens.py:1198 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Begin {}x{}" -msgstr "Démarrer {}x{}" +msgstr "Commencer {}x{}" -#: src/seedsigner/gui/screens/seed_screens.py:1275 +#: src/seedsigner/gui/screens/seed_screens.py msgid "click to exit" msgstr "cliquer pour sortir" -#: src/seedsigner/gui/screens/seed_screens.py:1411 -msgid "" -"Optionally scan your transcribed SeedQR to confirm that it reads back " -"correctly." -msgstr "" -"Scanner optionnellement votre SeedQR transcrite pour confirmer une lecture " -"correcte." +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Optionally scan your transcribed SeedQR to confirm that it reads back correctly." +msgstr "Également scanner votre SeedQR transcrite pour confirmer une lecture correcte." -#: src/seedsigner/gui/screens/seed_screens.py:1468 -#: src/seedsigner/views/seed_views.py:1284 -#: src/seedsigner/views/seed_views.py:1342 +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py msgid "Verify Address" -msgstr "Verifier l'adresse" +msgstr "Verifier l'Adresse" #. Inserts the nth address number (e.g. "Checking address 7") -#: src/seedsigner/gui/screens/seed_screens.py:1527 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Checking address {}" msgstr "Vérification addresse {}" -#: src/seedsigner/gui/screens/seed_screens.py:1544 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Multisig Verification" msgstr "Verification Multisig" -#: src/seedsigner/gui/screens/seed_screens.py:1549 -msgid "" -"Load your multisig wallet descriptor to verify your receive/self-transfer or" -" change address." -msgstr "" -"Charger votre descripteur portefeuille multisig pour vérifier votre adresse " -"de réception/ transfert à soi-même ou monnaie" +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Load your multisig wallet descriptor to verify your receive/self-transfer or change address." +msgstr "Charger votre descripteur multisig pour vérifier l'adresse de réception/auto-transfert ou monnaie" -#: src/seedsigner/gui/screens/seed_screens.py:1562 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Descriptor Loaded" -msgstr "Descriptor chargé" +msgstr "Descripteur Chargé" #. Label for the multisig wallet's signing policy (e.g. 2-of-3) -#: src/seedsigner/gui/screens/seed_screens.py:1568 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Policy" msgstr "Politique" -#: src/seedsigner/gui/screens/seed_screens.py:1576 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Signing Keys" -msgstr "Clé de signature" +msgstr "Clés de Signature" -#: src/seedsigner/gui/screens/settings_screens.py:26 -#: src/seedsigner/views/main_menu_views.py:21 -#: src/seedsigner/views/settings_views.py:36 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Review Message" +msgstr "Lire Message" + +#. Short for "Next step" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +#: src/seedsigner/views/tools_views.py +msgid "Next" +msgstr "Suivant" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Confirm Address" +msgstr "Confirmer Adresse" + +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Sign Message" +msgstr "Signer Message" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "derivation path" +msgstr "chemin de dérivation" + +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py msgid "Settings" msgstr "Paramètres" #. Short for "Input/Output"; screen to make sure the buttons and camera are #. working properly -#: src/seedsigner/gui/screens/settings_screens.py:58 +#: src/seedsigner/gui/screens/settings_screens.py msgid "I/O Test" -msgstr "Test Entré/Sortie" +msgstr "Test Entrée/Sortie" #. Blank the screen -#: src/seedsigner/gui/screens/settings_screens.py:129 -#: src/seedsigner/gui/screens/settings_screens.py:140 -#: src/seedsigner/gui/screens/settings_screens.py:221 +#: src/seedsigner/gui/screens/settings_screens.py msgid "Clear" msgstr "Effacer" -#: src/seedsigner/gui/screens/settings_screens.py:161 +#: src/seedsigner/gui/screens/settings_screens.py msgid "Exit" msgstr "Sortie" -#: src/seedsigner/gui/screens/settings_screens.py:175 -#: src/seedsigner/gui/screens/tools_screens.py:71 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Capturing image..." -msgstr "Capture de l'image" +msgstr "Capture de l'image..." -#: src/seedsigner/gui/screens/settings_screens.py:288 -#: src/seedsigner/views/settings_views.py:21 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py msgid "Donate" -msgstr "Donation" +msgstr "Dons" -#. If your language uses the percent sign ("%"), your translation must also -#. use +#. If your language uses the percent sign ("%"), your translation must also use #. two percent signs ("%%") due to python formatting oddities. "100%%" will be #. rendered as "100%". -#: src/seedsigner/gui/screens/settings_screens.py:293 +#: src/seedsigner/gui/screens/settings_screens.py #, python-format msgid "" "SeedSigner is 100%% free & open source, funded solely by the Bitcoin community.\n" "\n" "Donate onchain or LN at:" -msgstr "" -"SeedSigner est 100%% gratuit et open source, financé par la communauté " -"Bitcoin. Faite une donation 'onchain' ou LN à:" +msgstr "SeedSigner est 100%% gratuit & open source, financé par la communauté Bitcoin. Faites un don 'onchain' ou LN à:" -#. Instructions when taking a picture; "back" should point left. -#: src/seedsigner/gui/screens/tools_screens.py:40 -msgid "< back | click joystick" -msgstr "< retour | clic joystick" +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py +msgid "Settings QR" +msgstr "Paramètres QR" + +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py +msgid "Home" +msgstr "Accueil" #. Instructions when reviewing an image; "reshoot" must point left, "accept" #. must point right -#: src/seedsigner/gui/screens/tools_screens.py:98 +#: src/seedsigner/gui/screens/tools_screens.py msgid " < reshoot | accept > " -msgstr "< recommencer | accepter >" +msgstr " < recapturer | accepter > " #. current roll number vs total rolls (e.g. roll 7 of 50) -#: src/seedsigner/gui/screens/tools_screens.py:120 -#: src/seedsigner/gui/screens/tools_screens.py:245 +#: src/seedsigner/gui/screens/tools_screens.py msgid "Dice Roll {}/{}" msgstr "Lancé de Dé {}/{}" -#: src/seedsigner/gui/screens/tools_screens.py:274 +#. Build the last word in a 12 or 24 word BIP-39 mnemonic seed phrase. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Build Final Word" +msgstr "Créer Dernier Mot" + +#. Number of BIP-39 seed words, and the entropy -- in bits, contained within. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "The {}th word is built from {} more entropy bits plus auto-calculated checksum." +msgstr "Le {}ème mot est obtenu par {} plus d'entropie bits plus la somme de contrôle." + +#. current coin-flip number vs total flips (e.g. flip 3 of 4) +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Coin Flip {}/{}" +msgstr "Pile ou Face {}/{}" + +#. How we call the "front" side result during a coin toss. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Heads = 1" +msgstr "Face = 1" + +#. How we call the "back" side result during a coin toss. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Tails = 0" +msgstr "Pile = 0" + +#. A labeled presentation of a user-supplied value +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Your input: \"{}\"" +msgstr "Votre entrée: \"{}\"" + +#. A function of "x" to be used for detecting errors in "x" +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Checksum" +msgstr "Somme de contrôle" + +#. labeled presentation of the last word in a BIP-39 mnemonic seed phrase. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Final Word: \"{}\"" +msgstr "Dernier Mot: \"{}\"" + +#. a label for the last word of a 12-word BIP-39 mnemonic seed phrase +#: src/seedsigner/gui/screens/tools_screens.py msgid "12th Word" -msgstr "12ème Mots" +msgstr "12ème Mot" -#: src/seedsigner/gui/screens/tools_screens.py:276 +#. a label for the last word of a 24-word BIP-39 mnemonic seed phrase +#: src/seedsigner/gui/screens/tools_screens.py msgid "24th Word" -msgstr "24ème Mots" +msgstr "24ème Mot" -#: src/seedsigner/models/settings_definition.py:14 +#. a label for the tool to explore public addresses for this seed. +#: src/seedsigner/gui/screens/tools_screens.py +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py +msgid "Address Explorer" +msgstr "Explor. d'Adresses" + +#. a label for a BIP-380-ish Output Descriptor +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Wallet descriptor" +msgstr "Descripteur portefeuille" + +#: src/seedsigner/models/settings_definition.py msgid "Enabled" msgstr "Activé" -#: src/seedsigner/models/settings_definition.py:15 -#: src/seedsigner/models/settings_definition.py:20 +#: src/seedsigner/models/settings_definition.py msgid "Disabled" msgstr "Désactivé" -#: src/seedsigner/models/settings_definition.py:18 -#: src/seedsigner/models/settings_definition.py:26 +#: src/seedsigner/models/settings_definition.py msgid "Prompt" msgstr "Demander" -#: src/seedsigner/models/settings_definition.py:19 -#: src/seedsigner/models/settings_definition.py:23 -#: src/seedsigner/models/settings_definition.py:29 +#: src/seedsigner/models/settings_definition.py msgid "Required" msgstr "Requis" -#: src/seedsigner/models/settings_definition.py:72 -msgid "Btc-only" -msgstr "BTC-seulement" +#. Translator is expected ONLY to change their own locale below +#: src/seedsigner/models/settings_definition.py +msgid "Arabic" +msgstr "عربي" -#: src/seedsigner/models/settings_definition.py:73 -msgid "Sats-only" -msgstr "Sats-seulement" +#: src/seedsigner/models/settings_definition.py +msgid "Czech" +msgstr "čeština" -#: src/seedsigner/models/settings_definition.py:74 -msgid "Threshold at 0.01" -msgstr "Seuil à 0.01" +#: src/seedsigner/models/settings_definition.py +msgid "French" +msgstr "Français" -#: src/seedsigner/models/settings_definition.py:75 -msgid "Btc | Sats hybrid" -msgstr "Btc | Sats hybride" +#: src/seedsigner/models/settings_definition.py +msgid "German" +msgstr "Deutsch" -#: src/seedsigner/models/settings_definition.py:83 +#: src/seedsigner/models/settings_definition.py +msgid "Hebrew" +msgstr "עִברִית" + +#: src/seedsigner/models/settings_definition.py +msgid "Japanese" +msgstr "日本" + +#: src/seedsigner/models/settings_definition.py +msgid "Korean" +msgstr "한국인" + +#: src/seedsigner/models/settings_definition.py +msgid "Portuguese" +msgstr "Português" + +#: src/seedsigner/models/settings_definition.py +msgid "Russian" +msgstr "Русский" + +#: src/seedsigner/models/settings_definition.py +msgid "Spanish" +msgstr "Español" + +#: src/seedsigner/models/settings_definition.py msgid "0°" msgstr "0°" -#: src/seedsigner/models/settings_definition.py:84 +#: src/seedsigner/models/settings_definition.py msgid "90°" msgstr "90°" -#: src/seedsigner/models/settings_definition.py:85 +#: src/seedsigner/models/settings_definition.py msgid "180°" msgstr "180°" -#: src/seedsigner/models/settings_definition.py:86 +#: src/seedsigner/models/settings_definition.py msgid "270°" msgstr "270°" -#: src/seedsigner/models/settings_definition.py:94 +#: src/seedsigner/models/settings_definition.py msgid "Low" msgstr "Bas" -#: src/seedsigner/models/settings_definition.py:95 +#: src/seedsigner/models/settings_definition.py msgid "Medium" -msgstr "Milieu" +msgstr "Moyen" -#: src/seedsigner/models/settings_definition.py:96 +#: src/seedsigner/models/settings_definition.py msgid "High" -msgstr "Haut" +msgstr "Élevé" -#: src/seedsigner/models/settings_definition.py:104 +#: src/seedsigner/models/settings_definition.py msgid "Mainnet" msgstr "Mainnet" -#: src/seedsigner/models/settings_definition.py:105 +#: src/seedsigner/models/settings_definition.py msgid "Testnet" msgstr "Testnet" -#: src/seedsigner/models/settings_definition.py:106 +#: src/seedsigner/models/settings_definition.py msgid "Regtest" msgstr "Regtest" -#: src/seedsigner/models/settings_definition.py:122 -#: src/seedsigner/views/seed_views.py:466 +#: src/seedsigner/models/settings_definition.py +msgid "Store Settings on SD card" +msgstr "Stocker Params sur carte SD" + +#: src/seedsigner/models/settings_definition.py +msgid "Insert SD card to enable" +msgstr "Insérer carte SD pour activer" + +#: src/seedsigner/models/settings_definition.py +#: src/seedsigner/views/seed_views.py msgid "Single Sig" -msgstr "Single Sig" +msgstr "Clé simple" -#: src/seedsigner/models/settings_definition.py:123 -#: src/seedsigner/views/seed_views.py:467 -msgid "Multisig" -msgstr "Multisig" +#: src/seedsigner/models/settings_definition.py +msgid "Native Segwit" +msgstr "Segwit Natif" -#: src/seedsigner/models/settings_definition.py:354 +#: src/seedsigner/models/settings_definition.py +msgid "Nested Segwit (legacy)" +msgstr "Segwit Nested" + +#: src/seedsigner/models/settings_definition.py +msgid "Taproot" +msgstr "Taproot" + +#: src/seedsigner/models/settings_definition.py +msgid "Custom Derivation" +msgstr "Dérivation sur mesure" + +#: src/seedsigner/models/settings_definition.py msgid "Language" -msgstr "Language" +msgstr "Langue" -#: src/seedsigner/models/settings_definition.py:362 +#: src/seedsigner/models/settings_definition.py msgid "Mnemonic language" msgstr "Langue Mnémoniques" -#: src/seedsigner/models/settings_definition.py:370 +#: src/seedsigner/models/settings_definition.py msgid "Persistent settings" -msgstr "Paramètres persistent" +msgstr "Paramètres persistants" -#: src/seedsigner/models/settings_definition.py:376 +#: src/seedsigner/models/settings_definition.py msgid "Coordinator software" msgstr "Logiciel coordinateur" -#: src/seedsigner/models/settings_definition.py:383 +#: src/seedsigner/models/settings_definition.py msgid "Denomination display" msgstr "Unité d'affichage" -#: src/seedsigner/models/settings_definition.py:392 +#: src/seedsigner/models/settings_definition.py msgid "Bitcoin network" msgstr "Réseau Bitcoin" -#: src/seedsigner/models/settings_definition.py:400 +#: src/seedsigner/models/settings_definition.py msgid "QR code density" -msgstr "Densité QRcode" +msgstr "Densité code QR" -#: src/seedsigner/models/settings_definition.py:408 +#: src/seedsigner/models/settings_definition.py msgid "Xpub export" msgstr "Export Xpub" -#: src/seedsigner/models/settings_definition.py:414 +#: src/seedsigner/models/settings_definition.py msgid "Sig types" -msgstr "Types signature" +msgstr "Types signatures" -#: src/seedsigner/models/settings_definition.py:422 +#: src/seedsigner/models/settings_definition.py msgid "Script types" msgstr "Types Scripts" -#: src/seedsigner/models/settings_definition.py:430 +#: src/seedsigner/models/settings_definition.py msgid "Show xpub details" -msgstr "Montrer les détails Xpub" +msgstr "Afficher détails xpub" -#: src/seedsigner/models/settings_definition.py:436 +#: src/seedsigner/models/settings_definition.py msgid "BIP-39 passphrase" msgstr "Phrase secrète BIP-39" -#: src/seedsigner/models/settings_definition.py:444 +#: src/seedsigner/models/settings_definition.py msgid "Camera rotation" msgstr "Rotation caméra" -#: src/seedsigner/models/settings_definition.py:452 +#: src/seedsigner/models/settings_definition.py msgid "Compact SeedQR" msgstr "SeedQR Compact" -#: src/seedsigner/models/settings_definition.py:458 +#: src/seedsigner/models/settings_definition.py +msgid "BIP-85 child seeds" +msgstr "BIP-85 seeds enfants" + +#: src/seedsigner/models/settings_definition.py +msgid "Message signing" +msgstr "Signer Messages" + +#: src/seedsigner/models/settings_definition.py msgid "Show privacy warnings" -msgstr "Montrer les avertissements sur la vie privée" +msgstr "Afficher avertissements de confidentialité" -#: src/seedsigner/models/settings_definition.py:464 +#: src/seedsigner/models/settings_definition.py msgid "Show dire warnings" -msgstr "Montrer les avertissements terribles" +msgstr "Afficher avertissements alarmants" -#: src/seedsigner/models/settings_definition.py:470 +#: src/seedsigner/models/settings_definition.py +msgid "Show QR brightness tips" +msgstr "Montrer indices lumineux" + +#: src/seedsigner/models/settings_definition.py msgid "Show partner logos" -msgstr "Montrer les logos partenaires" +msgstr "Afficher logos partenaires" -#: src/seedsigner/models/settings_definition.py:485 +#: src/seedsigner/models/settings_definition.py msgid "QR background color" -msgstr "Couleur arrière-plan du QRcode" +msgstr "Couleur arrière-plan code QR" -#: src/seedsigner/views/main_menu_views.py:18 -msgid "Scan" -msgstr "Scan" - -#: src/seedsigner/views/main_menu_views.py:19 -msgid "Seeds" -msgstr "Seeds" - -#: src/seedsigner/views/main_menu_views.py:20 -#: src/seedsigner/views/tools_views.py:29 -msgid "Tools" -msgstr "Outils" - -#: src/seedsigner/views/main_menu_views.py:38 -msgid "Restart" -msgstr "Redémarrer" - -#: src/seedsigner/views/main_menu_views.py:39 -msgid "Power Off" -msgstr "Éteindre" - -#: src/seedsigner/views/main_menu_views.py:42 -msgid "Reset / Power" -msgstr "Réinit/Alimentation" - -#: src/seedsigner/views/psbt_views.py:36 -#: src/seedsigner/views/seed_views.py:1322 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +#: src/seedsigner/views/tools_views.py msgid "Scan a seed" msgstr "Scanner une seed" -#: src/seedsigner/views/psbt_views.py:37 -#: src/seedsigner/views/seed_views.py:1323 -msgid "Enter 12/24 words" -msgstr "Entrer 12/24 mots" +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +#: src/seedsigner/views/tools_views.py +msgid "Enter 12-word seed" +msgstr "Entrer seed 12 mots" -#: src/seedsigner/views/psbt_views.py:53 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +#: src/seedsigner/views/tools_views.py +msgid "Enter 24-word seed" +msgstr "Entrer seed 24 mots" + +#: src/seedsigner/views/psbt_views.py msgid "Select Signer" -msgstr "Selectionner le Signer" +msgstr "Selectionner Signataire" -#: src/seedsigner/views/psbt_views.py:85 +#: src/seedsigner/views/psbt_views.py msgid "Parsing PSBT..." msgstr "Analyse du PSBT..." -#: src/seedsigner/views/psbt_views.py:155 +#: src/seedsigner/views/psbt_views.py msgid "Unsupported Script Type!" -msgstr "Type script non supporté!" +msgstr "Type de Script non supporté!" -#: src/seedsigner/views/psbt_views.py:156 -msgid "" -"PSBT has unsupported input script type, please verify your change addresses." -msgstr "" -"La PSBT a un script d'entré non supporté, veuillez vérfier votre adresse de " -"rendu de monnaie." +#: src/seedsigner/views/psbt_views.py +msgid "PSBT has unsupported input script type, please verify your change addresses." +msgstr "Script d'entrée PSBT non supporté, veuillez vérfier votre adresse de monnaie." -#: src/seedsigner/views/psbt_views.py:157 -#: src/seedsigner/views/psbt_views.py:176 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/view.py msgid "Continue" msgstr "Continuer" #. User will receive no change back; the inputs to this transaction are fully #. spent -#: src/seedsigner/views/psbt_views.py:174 +#: src/seedsigner/views/psbt_views.py msgid "Full Spend!" -msgstr "Dépensé complètement" +msgstr "Dépense Totale!" -#: src/seedsigner/views/psbt_views.py:175 -msgid "" -"This PSBT spends its entire input value. No change is coming back to your " -"wallet." -msgstr "" -"Cette PSBT dépense la totalité de l'entrée. Aucun rendu de monnaie n'est " -"renvoyé vers votre portefeuille." +#: src/seedsigner/views/psbt_views.py +msgid "This PSBT spends its entire input value. No change is coming back to your wallet." +msgstr "Cette PSBT fait l'appoint. Aucune monnaie ne sera rendue." #. Future-tense used to indicate that this transaction will send this amount, -#. as opposed to "Send" on its own which could be misread as an instant -#. command +#. as opposed to "Send" on its own which could be misread as an instant command #. (e.g. "Send Now"). -#: src/seedsigner/views/psbt_views.py:242 +#: src/seedsigner/views/psbt_views.py msgid "Will Send" -msgstr "Va envoyer" +msgstr "À envoyer" -#: src/seedsigner/views/psbt_views.py:248 +#: src/seedsigner/views/psbt_views.py msgid "Next Recipient" msgstr "Destinataire suivant" -#. Short for "Next step" -#: src/seedsigner/views/psbt_views.py:251 -#: src/seedsigner/views/psbt_views.py:320 -#: src/seedsigner/views/seed_views.py:796 -msgid "Next" -msgstr "Suivant" - #. The amount you're receiving back from the transaction -#: src/seedsigner/views/psbt_views.py:324 +#: src/seedsigner/views/psbt_views.py msgid "Your Change" -msgstr "Votre monnaie" +msgstr "Votre Monnaie" -#: src/seedsigner/views/psbt_views.py:325 +#: src/seedsigner/views/psbt_views.py msgid "Verify Multisig Change" -msgstr "Verifier la monnaie Multisig" +msgstr "Verifier Monnaie Multisig" -#: src/seedsigner/views/psbt_views.py:327 +#: src/seedsigner/views/psbt_views.py msgid "Self-Transfer" -msgstr "Transfert à soi-même" +msgstr "Auto-Transfert" -#: src/seedsigner/views/psbt_views.py:328 +#: src/seedsigner/views/psbt_views.py msgid "Verify Multisig Addr" msgstr "Verifier l'adresse Multisig" -#: src/seedsigner/views/psbt_views.py:347 +#: src/seedsigner/views/psbt_views.py msgid "Verifying Change..." -msgstr "Vérification Monnaie" +msgstr "Vérification Monnaie..." -#: src/seedsigner/views/psbt_views.py:349 +#: src/seedsigner/views/psbt_views.py msgid "Verifying Self-Transfer..." -msgstr "Verification du transfert à soi-même" +msgstr "Vérification de l' Auto-Transfert..." -#: src/seedsigner/views/psbt_views.py:431 -msgid "" -"PSBT's {} address could not be verified with your multisig wallet " -"descriptor." -msgstr "" -"L'adresse PSBT's {} n'a pas pu être vérifiée avec le descripteur multisig de" -" votre portefeuille." +#. Variable is either "change" or "self-transfer". +#: src/seedsigner/views/psbt_views.py +msgid "PSBT's {} address could not be verified with your multisig wallet descriptor." +msgstr "Votre descripteur multisig n'a pas pu vérifier l'adresse PSBT {}." -#: src/seedsigner/views/psbt_views.py:433 +#: src/seedsigner/views/psbt_views.py msgid "Suspicious PSBT" -msgstr "PSBT suspicieux" +msgstr "PSBT Suspect" -#: src/seedsigner/views/psbt_views.py:434 +#. Variable is either "change" or "self-transfer". +#: src/seedsigner/views/psbt_views.py msgid "PSBT's {} address could not be generated from your seed." -msgstr "L'adresse {} PSBT n'a pas pu être générée depuis votre seed." +msgstr "Votre seed n'a pas pu générer l'adresse PSBT {}." -#: src/seedsigner/views/psbt_views.py:438 +#: src/seedsigner/views/psbt_views.py msgid "Address Verification Failed" -msgstr "Echec vérification de l'adresse" +msgstr "Echec vérification de l'Adresse" -#: src/seedsigner/views/psbt_views.py:440 +#: src/seedsigner/views/psbt_views.py msgid "Discard PSBT" -msgstr "Rejeter la PSBT" +msgstr "Rejeter PSBT" -#: src/seedsigner/views/psbt_views.py:465 +#: src/seedsigner/views/psbt_views.py msgid "Approve PSBT" -msgstr "Approuver la PSBT" +msgstr "Approuver PSBT" -#: src/seedsigner/views/psbt_views.py:470 -msgid "Signing PSBT..." -msgstr "Signature de la PSBT" - -#: src/seedsigner/views/psbt_views.py:548 -msgid "PSBT Error" -msgstr "Erreur PSBT" - -#: src/seedsigner/views/psbt_views.py:550 -msgid "Signing Failed" -msgstr "Echec signature" - -#: src/seedsigner/views/psbt_views.py:551 -msgid "Signing with this seed did not add a valid signature." -msgstr "Signer avec cette seed n'a pas ajouté une signature valide" - -#: src/seedsigner/views/psbt_views.py:552 +#: src/seedsigner/views/psbt_views.py msgid "Select Diff Seed" msgstr "Sélectionner une seed différente" +#: src/seedsigner/views/psbt_views.py +msgid "PSBT Error" +msgstr "Erreur PSBT" + +#: src/seedsigner/views/psbt_views.py +msgid "Signing Failed" +msgstr "Echec signature" + +#: src/seedsigner/views/psbt_views.py +msgid "Signing with this seed did not add a valid signature." +msgstr "La signature avec cette seed n'a pas ajouté de signature valide." + #. This is on the opening splash screen, displayed above the HRF logo -#: src/seedsigner/views/screensaver.py:74 +#: src/seedsigner/views/screensaver.py msgid "With support from:" msgstr "Avec l'aide de:" -#: src/seedsigner/views/seed_views.py:53 +#: src/seedsigner/views/seed_views.py msgid "Load a seed" -msgstr "Charger une seed" +msgstr "Charger seed" -#: src/seedsigner/views/seed_views.py:56 +#: src/seedsigner/views/seed_views.py msgid "In-Memory Seeds" msgstr "Seeds En-Mémoire" -#: src/seedsigner/views/seed_views.py:77 +#: src/seedsigner/views/seed_views.py +msgid "Load the seed to verify" +msgstr "Charger seed à vérifier " + +#: src/seedsigner/views/seed_views.py +msgid "Select seed to verify" +msgstr "Sélectionner seed à vérifier" + +#: src/seedsigner/views/seed_views.py +msgid "Load the seed to sign with" +msgstr "Entrer le seed pour signer" + +#: src/seedsigner/views/seed_views.py +msgid "Select seed to sign with" +msgstr "Selectionner le seed pour signer" + +#: src/seedsigner/views/seed_views.py msgid "Scan a SeedQR" -msgstr "Scanner une SeedQR" +msgstr "Scanner SeedQR" -#: src/seedsigner/views/seed_views.py:78 -msgid "Enter 24-word seed" -msgstr "Entrer la seed 24 mots" - -#: src/seedsigner/views/seed_views.py:79 -msgid "Enter 12-word seed" -msgstr "Entrer la seed 12 mots" - -#: src/seedsigner/views/seed_views.py:80 +#: src/seedsigner/views/seed_views.py msgid "Create a seed" -msgstr "Creer une seed" +msgstr "Creer seed" -#: src/seedsigner/views/seed_views.py:89 +#: src/seedsigner/views/seed_views.py msgid "Load A Seed" -msgstr "Charger une seed" +msgstr "Charger seed" #. Inserts the word number (e.g. "Seed Word #6") -#: src/seedsigner/views/seed_views.py:126 +#: src/seedsigner/views/seed_views.py msgid "Seed Word #{}" msgstr "Mots seed #{}" -#: src/seedsigner/views/seed_views.py:187 +#: src/seedsigner/views/seed_views.py msgid "Review & Edit" -msgstr "Passer en revue et Editer" +msgstr "Relire & Editer" -#: src/seedsigner/views/seed_views.py:188 -#: src/seedsigner/views/seed_views.py:310 -#: src/seedsigner/views/tools_views.py:259 +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py msgid "Discard" msgstr "Rejeter" -#: src/seedsigner/views/seed_views.py:192 +#: src/seedsigner/views/seed_views.py msgid "Invalid Mnemonic!" -msgstr "Mnemonique invalide!" +msgstr "Mnemonique Invalide!" -#: src/seedsigner/views/seed_views.py:194 +#: src/seedsigner/views/seed_views.py msgid "Checksum failure; not a valid seed phrase." msgstr "Erreur checksum; ce n'est pas une phrase de seed valide" -#: src/seedsigner/views/seed_views.py:216 -#: src/seedsigner/views/seed_views.py:268 -#: src/seedsigner/views/seed_views.py:797 -#: src/seedsigner/views/seed_views.py:1146 +#: src/seedsigner/views/seed_views.py src/seedsigner/views/view.py msgid "Done" -msgstr "Fait" +msgstr "Terminé" -#: src/seedsigner/views/seed_views.py:267 +#: src/seedsigner/views/seed_views.py msgid "Edit passphrase" -msgstr "Editer la phrase secrète" +msgstr "Editer phrase secrète" -#: src/seedsigner/views/seed_views.py:309 +#: src/seedsigner/views/seed_views.py msgid "Keep Seed" -msgstr "Garder la seed" +msgstr "Garder Seed" -#: src/seedsigner/views/seed_views.py:315 +#: src/seedsigner/views/seed_views.py msgid "Discard Seed?" -msgstr "Rejeter la seed?" +msgstr "Rejeter Seed?" -#: src/seedsigner/views/seed_views.py:317 +#: src/seedsigner/views/seed_views.py msgid "Wipe seed {} from the device?" -msgstr "Effacer la seed {} de l'appareil?" +msgstr "Effacer seed {} de l'appareil?" -#: src/seedsigner/views/seed_views.py:351 +#: src/seedsigner/views/seed_views.py msgid "Scan PSBT" -msgstr "Scanner la PSBT" +msgstr "Scanner PSBT" -#: src/seedsigner/views/seed_views.py:353 -#: src/seedsigner/views/seed_views.py:1646 +#: src/seedsigner/views/seed_views.py msgid "Verify Addr" -msgstr "Vérifier adresse" +msgstr "Vérifier Adresse" -#: src/seedsigner/views/seed_views.py:355 -#: src/seedsigner/views/seed_views.py:436 -msgid "Backup Seed" -msgstr "Sauvegarde de la seed" +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Child Seed" +msgstr "BIP-85 Seed Enfant" -#: src/seedsigner/views/seed_views.py:356 +#: src/seedsigner/views/seed_views.py msgid "Discard Seed" -msgstr "Rejeter la seed" +msgstr "Rejeter Seed" -#: src/seedsigner/views/seed_views.py:431 +#: src/seedsigner/views/seed_views.py msgid "View Seed Words" -msgstr "Voir les mots de la seed" +msgstr "Voir Les Mots" -#: src/seedsigner/views/seed_views.py:432 +#: src/seedsigner/views/seed_views.py msgid "Export as SeedQR" -msgstr "Exporter en tant que SeedQR" +msgstr "Exporter en SeedQR" -#: src/seedsigner/views/seed_views.py:620 -msgid "Privacy Leak!" -msgstr "Fuite vie privée!" - -#: src/seedsigner/views/seed_views.py:621 +#: src/seedsigner/views/seed_views.py msgid "Xpub can be used to view all future transactions." msgstr "L'Xpub peut être utilisée pour voir toutes les transactions futures." -#: src/seedsigner/views/seed_views.py:651 +#: src/seedsigner/views/seed_views.py msgid "Generating xpub..." msgstr "Génération xpub..." -#: src/seedsigner/views/seed_views.py:771 +#: src/seedsigner/views/seed_views.py msgid "You must keep your seed words private & away from all online devices." -msgstr "" -"Vous devez garder vos mots de seed privé et loin de tout appareil connectés." +msgstr "Vous devez conserver vos mots de seed privé et loin de tout appareil connecté." -#: src/seedsigner/views/seed_views.py:836 +#: src/seedsigner/views/seed_views.py +msgid "Child #{}" +msgstr "Enfant #{}" + +#: src/seedsigner/views/seed_views.py +msgid "Seed Words" +msgstr "Mnémonique" + +#: src/seedsigner/views/seed_views.py +msgid "12 Words" +msgstr "12 Mots" + +#: src/seedsigner/views/seed_views.py +msgid "24 Words" +msgstr " 24 Mots" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Num Words" +msgstr "BIP-85 Nb Mots" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Index Error" +msgstr "Index Erreur BIP-85" + +#: src/seedsigner/views/seed_views.py +msgid "Invalid Child Index" +msgstr "Index Enfant Incorrect" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Child Index must be between 0 and 2^31-1." +msgstr "Index BIP-85 compris entre 0 et 2^31-1" + +#: src/seedsigner/views/seed_views.py +msgid "Try Again" +msgstr "Réessayer" + +#: src/seedsigner/views/seed_views.py msgid "Verify" msgstr "Vérifier" -#: src/seedsigner/views/seed_views.py:837 +#: src/seedsigner/views/seed_views.py msgid "Skip" msgstr "Sauter" -#: src/seedsigner/views/seed_views.py:886 +#: src/seedsigner/views/seed_views.py msgid "Verify Word #{}" msgstr "Vérifier Mots #{}" -#: src/seedsigner/views/seed_views.py:926 +#: src/seedsigner/views/seed_views.py msgid "Review Seed Words" -msgstr "Passer en revue les mots de seed" +msgstr "Éxaminer les mots de seed" -#: src/seedsigner/views/seed_views.py:927 -msgid "Try Again" -msgstr "Essaye encore" - -#: src/seedsigner/views/seed_views.py:931 +#: src/seedsigner/views/seed_views.py msgid "Verification Error" -msgstr "Erreur vérification" +msgstr "Erreur Vérification" -#: src/seedsigner/views/seed_views.py:933 +#: src/seedsigner/views/seed_views.py msgid "Wrong Word!" -msgstr "Mots faux!" +msgstr "Mots Erronés!" -#: src/seedsigner/views/seed_views.py:934 +#: src/seedsigner/views/seed_views.py msgid "Word #{} is not \"{}\"!" msgstr "Mots #{} n'est pas \"{}\"!" -#: src/seedsigner/views/seed_views.py:952 +#: src/seedsigner/views/seed_views.py msgid "Backup Verified" -msgstr "La sauvegarde échoué" +msgstr "Archive Vérifiée" -#: src/seedsigner/views/seed_views.py:955 +#: src/seedsigner/views/seed_views.py msgid "All mnemonic backup words were successfully verified!" -msgstr "" -"Toutes les sauvegardes de mots mnémonique ont été vérifiés avec succès!" +msgstr "L'archive des mots mnémoniques a été vérifiée avec succès!" -#: src/seedsigner/views/seed_views.py:978 +#: src/seedsigner/views/seed_views.py msgid "Standard: 25x25" msgstr "Standard: 25x25" -#: src/seedsigner/views/seed_views.py:979 +#: src/seedsigner/views/seed_views.py msgid "Compact: 21x21" msgstr "Compact: 21x21" -#: src/seedsigner/views/seed_views.py:983 +#: src/seedsigner/views/seed_views.py msgid "Standard: 29x29" msgstr "Standard: 29x29" -#: src/seedsigner/views/seed_views.py:984 +#: src/seedsigner/views/seed_views.py msgid "Compact: 25x25" msgstr "Compact: 25x25" -#: src/seedsigner/views/seed_views.py:1003 +#: src/seedsigner/views/seed_views.py msgid "SeedQR Format" msgstr "Format SeedQR" -#: src/seedsigner/views/seed_views.py:1052 +#: src/seedsigner/views/seed_views.py msgid "SeedQR is your private key!" -msgstr "SeedQR est votre clé privée" +msgstr "SeedQR est votre clé privée!" -#: src/seedsigner/views/seed_views.py:1053 -msgid "Never photograph it or scan it into an online device." -msgstr "Ne jamais le photographier ou scanner sur un appareil connecté" +#: src/seedsigner/views/seed_views.py +msgid "Never photograph or scan it into a device that connects to the internet." +msgstr "Ne jamais le photographier ou scanner sur un appareil connecté." -#: src/seedsigner/views/seed_views.py:1145 -#: src/seedsigner/views/seed_views.py:1189 -#: src/seedsigner/views/seed_views.py:1200 -#: src/seedsigner/views/seed_views.py:1212 +#: src/seedsigner/views/seed_views.py msgid "Confirm SeedQR" -msgstr "Confirmer la SeedQR" +msgstr "Confirmer SeedQR" -#: src/seedsigner/views/seed_views.py:1150 +#: src/seedsigner/views/seed_views.py msgid "Confirm SeedQR?" -msgstr "Confirmer la SeedQR?" +msgstr "Confirmer SeedQR?" -#: src/seedsigner/views/seed_views.py:1180 +#: src/seedsigner/views/seed_views.py msgid "Scan your SeedQR" msgstr "Scanner votre SeedQR" -#: src/seedsigner/views/seed_views.py:1190 -#: src/seedsigner/views/seed_views.py:1213 +#: src/seedsigner/views/seed_views.py msgid "Error!" msgstr "Erreur!" -#: src/seedsigner/views/seed_views.py:1191 +#: src/seedsigner/views/seed_views.py msgid "Your transcribed SeedQR does not match your original seed!" msgstr "Votre SeedQR transcrite ne correspond pas a la seed d'origine!" -#: src/seedsigner/views/seed_views.py:1193 -#: src/seedsigner/views/seed_views.py:1216 +#: src/seedsigner/views/seed_views.py msgid "Review SeedQR" -msgstr "Passer en revue la SeedQR" +msgstr "Éxaminer SeedQR" -#: src/seedsigner/views/seed_views.py:1202 -msgid "" -"Your transcribed SeedQR successfully scanned and yielded the same seed." -msgstr "" -"Votre SeedQR transcrite a été scannée avec succès et correspond à la même " -"seed." +#: src/seedsigner/views/seed_views.py +msgid "Your transcribed SeedQR successfully scanned and yielded the same seed." +msgstr "Votre SeedQR transcrite a été scannée avec succès et correspond à la même seed." -#: src/seedsigner/views/seed_views.py:1214 +#: src/seedsigner/views/seed_views.py msgid "Your transcribed SeedQR could not be read!" -msgstr "Votre SeedQR transcrite n'a pu être lue!" +msgstr "Votre SeedQR transcrite n'a pas pu être lue!" -#: src/seedsigner/views/seed_views.py:1285 +#: src/seedsigner/views/seed_views.py msgid "Sig type can't be auto-detected from this address. Please specify:" -msgstr "" -"Type Sig ne peut être auto-détecté depuis cette adresse. Veuillez spécifier:" +msgstr "Type Sig ne peut être auto-détecté depuis cette adresse. Veuillez spécifier:" -#: src/seedsigner/views/seed_views.py:1326 -msgid "Load the seed to verify" -msgstr "Charger la seed à vérifier " - -#: src/seedsigner/views/seed_views.py:1336 -msgid "Select seed to verify" -msgstr "Sélectionner la seed à vérifier" - -#: src/seedsigner/views/seed_views.py:1388 +#: src/seedsigner/views/seed_views.py msgid "Can't validate a single sig addr without specifying a seed" -msgstr "Validation d'une adresse single sig impossible sans spécifier la seed" +msgstr "Impossible de valider l'adresse d'une clé simple sans spécifier la seed" #. Option when scanning for a matching address; skips ten addresses ahead -#: src/seedsigner/views/seed_views.py:1434 +#: src/seedsigner/views/seed_views.py msgid "Skip 10" msgstr "Sauter 10" -#: src/seedsigner/views/seed_views.py:1435 -#: src/seedsigner/views/seed_views.py:1616 +#: src/seedsigner/views/seed_views.py msgid "Cancel" msgstr "Annuler" -#: src/seedsigner/views/seed_views.py:1563 -#: src/seedsigner/views/seed_views.py:1579 -msgid "Taproot verification not yet implemented!" -msgstr "Vérification Taproot pas encore implémentée" - -#: src/seedsigner/views/seed_views.py:1575 -msgid "Legacy P2PKH verification not implemented!" -msgstr "Verification ancien P2PKH n'est pas implémenté!" - -#: src/seedsigner/views/seed_views.py:1600 +#: src/seedsigner/views/seed_views.py msgid "multisig" msgstr "multisig" -#: src/seedsigner/views/seed_views.py:1602 +#: src/seedsigner/views/seed_views.py msgid "seed {}" msgstr "seed {}" -#: src/seedsigner/views/seed_views.py:1605 +#: src/seedsigner/views/seed_views.py msgid "Address Verified" -msgstr "Adresse vérifiée" +msgstr "Adresse Vérifiée" -#: src/seedsigner/views/seed_views.py:1615 +#: src/seedsigner/views/seed_views.py msgid "Scan Descriptor" -msgstr "Scanner le Descriptor" +msgstr "Scanner Descripteur" -#: src/seedsigner/views/seed_views.py:1645 +#: src/seedsigner/views/seed_views.py msgid "Return to PSBT" -msgstr "Retour vers PSBT" +msgstr "Retour au PSBT" -#: src/seedsigner/views/settings_views.py:20 +#: src/seedsigner/views/settings_views.py msgid "I/O test" msgstr "Test Entré/Sortie" -#: src/seedsigner/views/settings_views.py:39 -#: src/seedsigner/views/settings_views.py:46 +#: src/seedsigner/views/settings_views.py msgid "Advanced" msgstr "Avancé" -#: src/seedsigner/views/settings_views.py:54 +#: src/seedsigner/views/settings_views.py msgid "Dev Options" -msgstr "Options développeur" +msgstr "Options Dév" -#: src/seedsigner/views/tools_views.py:24 -msgid " New seed" -msgstr "Nouvelle seed" +#: src/seedsigner/views/settings_views.py +msgid "Persistent Settings enabled. Settings saved to SD card." +msgstr "Paramètres Persistant activé. Params enrestés sur carte SD." -#: src/seedsigner/views/tools_views.py:25 +#: src/seedsigner/views/settings_views.py +msgid "Settings updated in temporary memory" +msgstr "Révision provisoire paramètres" + +#: src/seedsigner/views/tools_views.py msgid "New seed" msgstr "Nouvelle seed" -#: src/seedsigner/views/tools_views.py:26 +#: src/seedsigner/views/tools_views.py msgid "Calc 12th/24th word" -msgstr "Calc. 12/24ème mots" +msgstr "Calc. 12ème/24ème mots" -#: src/seedsigner/views/tools_views.py:102 -#: src/seedsigner/views/tools_views.py:228 +#: src/seedsigner/views/tools_views.py src/seedsigner/views/view.py +msgid "Tools" +msgstr "Outils" + +#: src/seedsigner/views/tools_views.py msgid "12 words" msgstr "12 mots" -#: src/seedsigner/views/tools_views.py:103 -#: src/seedsigner/views/tools_views.py:229 +#: src/seedsigner/views/tools_views.py msgid "24 words" msgstr "24 mots" -#: src/seedsigner/views/tools_views.py:107 +#: src/seedsigner/views/tools_views.py msgid "Mnemonic Length?" -msgstr "Longueur de la mnémonique?" +msgstr "Longueur Phrase?" -#: src/seedsigner/views/tools_views.py:174 +#: src/seedsigner/views/tools_views.py msgid "12 words (50 rolls)" msgstr "12 mots (50 jets)" -#: src/seedsigner/views/tools_views.py:175 +#: src/seedsigner/views/tools_views.py msgid "24 words (99 rolls)" msgstr "24 mots (99 jets)" -#: src/seedsigner/views/tools_views.py:179 -#: src/seedsigner/views/tools_views.py:233 +#: src/seedsigner/views/tools_views.py msgid "Mnemonic Length" -msgstr "Longueur de la mnémonique" +msgstr "Longueur Phrase" -#: src/seedsigner/views/tools_views.py:258 +#. Label to gather entropy through coin tosses +#: src/seedsigner/views/tools_views.py +msgid "Coin flip entropy" +msgstr "Entropie pile ou face" + +#. Label to gather entropy through user specified BIP-39 word +#: src/seedsigner/views/tools_views.py +msgid "Word selection entropy" +msgstr "Entropie choix de mots" + +#. Label to allow user to default entropy as all-zeros +#: src/seedsigner/views/tools_views.py +msgid "Finalize with zeros" +msgstr "Finaliser avec zéros" + +#. label to calculate the last word of a BIP-39 mnemonic seed phrase +#: src/seedsigner/views/tools_views.py +msgid "Final Word Calc" +msgstr "Calc. Dernier Mot" + +#: src/seedsigner/views/tools_views.py msgid "Load seed" -msgstr "Charger la seed" +msgstr "Charger seed" -#: src/seedsigner/views/view.py:124 +#: src/seedsigner/views/tools_views.py +msgid "Scan wallet descriptor" +msgstr "Scanner descripteur portefeuille" + +#. label for addresses where others send us incoming payments +#: src/seedsigner/views/tools_views.py +msgid "Receive Addresses" +msgstr "Adresses de Réception" + +#. label for addresses that collect the change from our own outgoing payments +#: src/seedsigner/views/tools_views.py +msgid "Change Addresses" +msgstr "Adresses de Monnaie" + +#. a status message that our payment addresses are being calculated +#: src/seedsigner/views/tools_views.py +msgid "Calculating addrs..." +msgstr "Calcul adrs..." + +#: src/seedsigner/views/tools_views.py +msgid "Next {}" +msgstr "Prochain {}" + +#: src/seedsigner/views/tools_views.py +msgid "Receive Addrs" +msgstr "Adrs Réception" + +#: src/seedsigner/views/tools_views.py +msgid "Change Addrs" +msgstr "Adrs Monnaie" + +#: src/seedsigner/views/view.py +msgid "Scan" +msgstr "Scan" + +#: src/seedsigner/views/view.py +msgid "Seeds" +msgstr "Seeds" + +#: src/seedsigner/views/view.py +msgid "Restart" +msgstr "Redémarrer" + +#: src/seedsigner/views/view.py +msgid "Power Off" +msgstr "Éteindre" + +#: src/seedsigner/views/view.py +msgid "Reset / Power" +msgstr "Redémarrer / Arrêt" + +#: src/seedsigner/views/view.py msgid "Work In Progress" msgstr "Travaux en cours" -#: src/seedsigner/views/view.py:125 +#: src/seedsigner/views/view.py msgid "Not Yet Implemented" msgstr "Pas encore implémenté" -#: src/seedsigner/views/view.py:126 +#: src/seedsigner/views/view.py msgid "This is still on our to-do list!" -msgstr "Ceci est toujours dans notre liste 'a-faire' !" +msgstr "Ce qu'il nous reste 'à-faire'!" -#: src/seedsigner/views/view.py:127 +#: src/seedsigner/views/view.py msgid "Back to Main Menu" -msgstr "Retour vers le menu principal" +msgstr "Retour au menu principal" -#: src/seedsigner/views/view.py:142 +#: src/seedsigner/views/view.py +msgid "Error" +msgstr "Erreur" + +#: src/seedsigner/views/view.py +msgid "Network Mismatch" +msgstr "Réseau Incorrect" + +#: src/seedsigner/views/view.py +msgid "Change Setting" +msgstr "Changer Param" + +#: src/seedsigner/views/view.py msgid "System Error" msgstr "Erreur Système" + +#: src/seedsigner/views/view.py +msgid "Update Setting" +msgstr "Réviser Param" + +#: src/seedsigner/views/view.py +msgid "Option Disabled" +msgstr "Option Desactivé" + +#: src/seedsigner/views/view.py +msgid "Security Tip" +msgstr "Astuce Sécurité" + +#: src/seedsigner/views/view.py +msgid "" +"For maximum security,\n" +"remove the MicroSD card\n" +"before continuing." +msgstr "" + +#~ msgid "{}..." +#~ msgstr "{}..." + +#~ msgid "Click to authorize this transaction" +#~ msgstr "Cliquer pour autoriser cette transaction" + +#~ msgid "Signed PSBT" +#~ msgstr "PSBT signée" + +#~ msgid "Export as a QR code for:" +#~ msgstr "Exporter en tant que QRcode pour:" + +#~ msgid "changes fingerprint" +#~ msgstr "change l'empreinte" + +#~ msgid "Enter 12/24 words" +#~ msgstr "Entrer 12/24 mots" + +#~ msgid "Signing PSBT..." +#~ msgstr "Signature de la PSBT" + +#~ msgid "Taproot verification not yet implemented!" +#~ msgstr "Vérification Taproot pas encore implémentée" + +#~ msgid "Legacy P2PKH verification not implemented!" +#~ msgstr "Verification ancien P2PKH n'est pas implémenté!" + +#~ msgid "Add Passphrase" +#~ msgstr "Ajouter Phrase Secrète" + +#~ msgid "Btc-only" +#~ msgstr "BTC-seulement" + +#~ msgid "Sats-only" +#~ msgstr "Sats-seulement" + +#~ msgid "Threshold at 0.01" +#~ msgstr "Seuil à 0.01" + +#~ msgid "Btc | Sats hybrid" +#~ msgstr "Btc | Sats hybrides" + +#~ msgid " (?)" +#~ msgstr " (?)" + +#~ msgid "Scan a QR code" +#~ msgstr "Scanner un code QR" + +#~ msgid "Yes " +#~ msgstr "Oui " + +#~ msgid "No " +#~ msgstr "Non " + +#~ msgid "< back | Scan a QR code" +#~ msgstr "< retour | Scanner un code QR" + +#~ msgid "Settings QR" +#~ msgstr "Paramètres QR" + +#~ msgid "Settings imported successfully!" +#~ msgstr "Import Paramètres réussi!" + +#~ msgid "MicroSD removed" +#~ msgstr "MicroSD retirée" + +#~ msgid "MicroSD inserted" +#~ msgstr "MicroSD insérée" + +#~ msgid "changes seed:" +#~ msgstr "change l'empreinte:" + +#~ msgid "< back | click joystick" +#~ msgstr "< retour | clic joystick" + +#~ msgid "Store Settings on SD card." +#~ msgstr "Enregistrer Paramètres sur carte SD." + +#~ msgid "MicroSD card is removed" +#~ msgstr "MicroSD est retirée" + +#~ msgid "Show passphrase warning" +#~ msgstr "Afficher avertissement phrase secrète" + +#~ msgid "" +#~ "BIP-39 passphrase changes your seed, resulting in new xpubs, wallets, etc.\n" +#~ "Be sure you know what you're doing!" +#~ msgstr "" +#~ "La phrase secrète BIP-39 change votre seed, créant de nouveaux xpubs, portefeuilles, etc.\n" +#~ "Soyez certain de savoir ce que vous faites!" + +#~ msgid "SeedQR is your private key!" +#~ msgstr "SeedQR est votre clé privée!" + +#~ msgid "Never photograph it or scan it into an online device." +#~ msgstr "Ne jamais le photographier ou scanner sur un appareil connecté." + +#~ msgid "New seed" +#~ msgstr "Nouvelle seed" + +#~ msgid "Scan wallet descriptor" +#~ msgstr "Scanner descripteur portefeuille" + diff --git a/src/seedsigner/resources/babel/he/LC_MESSAGES/messages.po b/src/seedsigner/resources/babel/he/LC_MESSAGES/messages.po index dc9b5a7c..fd7e3132 100644 --- a/src/seedsigner/resources/babel/he/LC_MESSAGES/messages.po +++ b/src/seedsigner/resources/babel/he/LC_MESSAGES/messages.po @@ -1,31 +1,209 @@ -# Translations template for PROJECT. +# Hebrew translations for PROJECT. # Copyright (C) 2022 ORGANIZATION # This file is distributed under the same license as the PROJECT project. # FIRST AUTHOR , 2022. -# +# # Translators: # Daniel Babaev, 2022 -# +# jdlcdl, 2022 msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-05-04 08:13-0500\n" +"POT-Creation-Date: 2023-10-09 11:36-0400\n" "PO-Revision-Date: 2022-04-29 13:50+0000\n" "Last-Translator: Daniel Babaev, 2022\n" -"Language-Team: Hebrew (https://www.transifex.com/seedsigner/teams/135832/he/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.10.1\n" "Language: he\n" +"Language-Team: Hebrew (https://www.transifex.com/seedsigner/teams/135832/he/)\n" "Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.10.3\n" -#: src/seedsigner/gui/screens/main_menu_screens.py:17 +#. The abbreviated label for the special key on a standard keyboard. +#: src/seedsigner/gui/keyboard.py +msgid "del" +msgstr "" + +#. The abbreviated label for the special key on a standard keyboard. +#: src/seedsigner/gui/keyboard.py +msgid "space" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "" +"You can remove\n" +"the SD card now" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "SD card removed" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "SD card inserted" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review PSBT" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review Details" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "1 input" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input 1" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input 2" +msgstr "" + +#. Indicates that items have been omitted from a series: e.g. "1, 2, 3, [...], +#. 8" +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "[ ... ]" +msgstr "" + +#. Input number will be inserted (e.g. "input 3") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input {}" +msgstr "" + +#. Ellipsis ("...") characters used to truncate an address (e.g. "bc1qabc...") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "..." +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/views/psbt_views.py +msgid "self-transfer" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient 1" +msgstr "" + +#. Inserts the recipient number (e.g. the fifth one is: "recipient 5") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient {}" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "fee" +msgstr "" + +#. Label for a change output in the PSBT Overview flow diagram +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/views/psbt_views.py +msgid "change" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "PSBT Math" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review Recipients" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "btc" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "sats" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input" +msgid_plural "inputs" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient" +msgid_plural "recipients" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +#. Denonination is inserted (e.g. your "btc change" or "sats change") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "{} change" +msgstr "" + +#. First variable is either "Multisig" or the fingerprint for single sig; +#. Second is "Change" or "Addr" (change vs receive addr); Third is the address +#. index number (e.g. your #4 receive addr). +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/models/settings_definition.py +#: src/seedsigner/views/seed_views.py +msgid "Multisig" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Change" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Addr" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Address verified!" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Sign PSBT" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Click to approve this transaction" +msgstr "" + +#: src/seedsigner/gui/screens/scan_screens.py +msgid "< back | " +msgstr "" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "Success!" +msgstr "" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "OK" +msgstr "" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/psbt_views.py +msgid "Caution" +msgstr "" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "Privacy Leak!" +msgstr "" + +#: src/seedsigner/gui/screens/screen.py +msgid "I Understand" +msgstr "" + +#: src/seedsigner/gui/screens/screen.py +msgid "Classified Info!" +msgstr "" + +#: src/seedsigner/gui/screens/screen.py msgid "Restarting" msgstr "מפעיל מחדש" -#: src/seedsigner/gui/screens/main_menu_screens.py:22 +#: src/seedsigner/gui/screens/screen.py msgid "" "SeedSigner is restarting.\n" "\n" @@ -35,115 +213,1200 @@ msgstr "" "\n" "כל הנתונים בזיכרון הפנימי יימחקו." -#: src/seedsigner/gui/screens/main_menu_screens.py:32 +#: src/seedsigner/gui/screens/screen.py msgid "Powering Off" msgstr "מתכבה" -#: src/seedsigner/gui/screens/main_menu_screens.py:37 +#: src/seedsigner/gui/screens/screen.py msgid "Please wait about 30 seconds before disconnecting power." msgstr "אנא המתן כ-30 שניות לפני ניתוק החשמל." -#: src/seedsigner/gui/screens/settings_screens.py:26 -#: src/seedsigner/views/main_menu_views.py:21 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Finalize Seed" +msgstr "" + +#. a label for the shortened Key-id of a BIP-32 master HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py +msgid "fingerprint" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Backup Seed" +msgstr "" + +#. Additional explainer for the two seed backup options (mnemonic phrase and +#. SeedQR). +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Backups do not include your passphrase." +msgstr "" + +#. Displays the page number and total: (e.g. page 1 of 6) +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Seed Words: {}/{}" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "BIP-85 Index" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Verify Backup?" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Optionally verify that your mnemonic backup is correct." +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Derivation Path" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Export Xpub" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Xpub Details" +msgstr "" + +#. Short for "BIP32 Master Fingerprint" +#. a label for the shortened Key-id of a BIP-32 master HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Fingerprint" +msgstr "" + +#. Short for "Derivation Path" +#. a label for the derivation-path into a BIP-32 HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Derivation" +msgstr "" + +#. Short for "BIP32 Extended Public Key" +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Xpub" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "BIP-39 Passphrase" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Verify Passphrase" +msgstr "" + +#. Describes the effect of applying a BIP-39 passphrase; it changes the seed's +#. fingerprint +#: src/seedsigner/gui/screens/seed_screens.py +msgid "changes fingerprint" +msgstr "" + +#. Refers to the SeedQR type: Standard or Compact +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Standard" +msgstr "" + +#. Briefly explains the Standard SeedQR data format +#: src/seedsigner/gui/screens/seed_screens.py +msgid "BIP-39 wordlist indices" +msgstr "" + +#. Refers to the SeedQR type: Standard or Compact +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Compact" +msgstr "" + +#. Briefly explains the Compact SeedQR data format +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Raw entropy bits" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Transcribe SeedQR" +msgstr "" + +#. Refers to the QR code size: 21x21, 25x25, or 29x29 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Begin {}x{}" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "click to exit" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Optionally scan your transcribed SeedQR to confirm that it reads back correctly." +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Verify Address" +msgstr "" + +#. Inserts the nth address number (e.g. "Checking address 7") +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Checking address {}" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Multisig Verification" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Load your multisig wallet descriptor to verify your receive/self-transfer or change address." +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Descriptor Loaded" +msgstr "" + +#. Label for the multisig wallet's signing policy (e.g. 2-of-3) +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Policy" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Signing Keys" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Review Message" +msgstr "" + +#. Short for "Next step" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +#: src/seedsigner/views/tools_views.py +msgid "Next" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Confirm Address" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Sign Message" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "derivation path" +msgstr "" + +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py msgid "Settings" msgstr "הגדרות" -#: src/seedsigner/gui/screens/settings_screens.py:57 +#. Short for "Input/Output"; screen to make sure the buttons and camera are +#. working properly +#: src/seedsigner/gui/screens/settings_screens.py msgid "I/O Test" msgstr "בדיקת קלט/פלט" -#: src/seedsigner/gui/screens/settings_screens.py:138 -#: src/seedsigner/gui/screens/settings_screens.py:219 +#. Blank the screen +#: src/seedsigner/gui/screens/settings_screens.py msgid "Clear" msgstr "לנקות" -#: src/seedsigner/gui/screens/settings_screens.py:159 +#: src/seedsigner/gui/screens/settings_screens.py msgid "Exit" msgstr "לצאת" -#: src/seedsigner/gui/screens/settings_screens.py:173 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Capturing image..." msgstr "לכידת תמונה..." -#: src/seedsigner/gui/screens/settings_screens.py:286 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py msgid "Donate" msgstr "לתרום" -#: src/seedsigner/gui/screens/settings_screens.py:290 +#. If your language uses the percent sign ("%"), your translation must also use +#. two percent signs ("%%") due to python formatting oddities. "100%%" will be +#. rendered as "100%". +#: src/seedsigner/gui/screens/settings_screens.py #, python-format msgid "" -"SeedSigner is free & open source, funded solely by the Bitcoin community.\n" +"SeedSigner is 100%% free & open source, funded solely by the Bitcoin community.\n" "\n" "Donate onchain or LN at:" msgstr "" -"SeedSigner הוא 100% חינמי וקוד פתוח, הממומן אך ורק על ידי קהילת הביטקוין.\n" -"\n" -"תרמו בעזרת על-השרשרת או ברשת ברק בכתובת:" -#: src/seedsigner/models/settings_definition.py:14 -msgid "Enabled" -msgstr "מופעל" +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py +msgid "Settings QR" +msgstr "" -#: src/seedsigner/models/settings_definition.py:15 -#: src/seedsigner/models/settings_definition.py:20 -msgid "Disabled" -msgstr "מושבת" - -#: src/seedsigner/models/settings_definition.py:18 -#: src/seedsigner/models/settings_definition.py:26 -msgid "Prompt" -msgstr "הנחיה" - -#: src/seedsigner/models/settings_definition.py:19 -#: src/seedsigner/models/settings_definition.py:23 -#: src/seedsigner/models/settings_definition.py:29 -msgid "Required" -msgstr "נדרש" - -#: src/seedsigner/models/settings_definition.py:346 -msgid "Language" -msgstr "שפה" - -#: src/seedsigner/models/settings_definition.py:354 -msgid "Mnemonic language" -msgstr "שפת הזכרונות" - -#: src/seedsigner/models/settings_definition.py:362 -msgid "Persistent settings" -msgstr "הגדרות קבועות" - -#: src/seedsigner/models/settings_definition.py:368 -msgid "Coordinator software" -msgstr "תוכנת רכז" - -#: src/seedsigner/models/settings_definition.py:375 -msgid "Denomination display" -msgstr "תצוגת יחידה" - -#: src/seedsigner/views/main_menu_views.py:18 -msgid "Scan" -msgstr "לסרוק" - -#: src/seedsigner/views/main_menu_views.py:19 -msgid "Seeds" -msgstr "מפתחות" - -#: src/seedsigner/views/main_menu_views.py:20 -msgid "Tools" -msgstr "כלים" - -#: src/seedsigner/views/main_menu_views.py:25 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py msgid "Home" msgstr "מסך ראשי" -#: src/seedsigner/views/main_menu_views.py:42 +#. Instructions when reviewing an image; "reshoot" must point left, "accept" +#. must point right +#: src/seedsigner/gui/screens/tools_screens.py +msgid " < reshoot | accept > " +msgstr "" + +#. current roll number vs total rolls (e.g. roll 7 of 50) +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Dice Roll {}/{}" +msgstr "" + +#. Build the last word in a 12 or 24 word BIP-39 mnemonic seed phrase. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Build Final Word" +msgstr "" + +#. Number of BIP-39 seed words, and the entropy -- in bits, contained within. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "The {}th word is built from {} more entropy bits plus auto-calculated checksum." +msgstr "" + +#. current coin-flip number vs total flips (e.g. flip 3 of 4) +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Coin Flip {}/{}" +msgstr "" + +#. How we call the "front" side result during a coin toss. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Heads = 1" +msgstr "" + +#. How we call the "back" side result during a coin toss. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Tails = 0" +msgstr "" + +#. A labeled presentation of a user-supplied value +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Your input: \"{}\"" +msgstr "" + +#. A function of "x" to be used for detecting errors in "x" +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Checksum" +msgstr "" + +#. labeled presentation of the last word in a BIP-39 mnemonic seed phrase. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Final Word: \"{}\"" +msgstr "" + +#. a label for the last word of a 12-word BIP-39 mnemonic seed phrase +#: src/seedsigner/gui/screens/tools_screens.py +msgid "12th Word" +msgstr "" + +#. a label for the last word of a 24-word BIP-39 mnemonic seed phrase +#: src/seedsigner/gui/screens/tools_screens.py +msgid "24th Word" +msgstr "" + +#. a label for the tool to explore public addresses for this seed. +#: src/seedsigner/gui/screens/tools_screens.py +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py +msgid "Address Explorer" +msgstr "" + +#. a label for a BIP-380-ish Output Descriptor +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Wallet descriptor" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Enabled" +msgstr "מופעל" + +#: src/seedsigner/models/settings_definition.py +msgid "Disabled" +msgstr "מושבת" + +#: src/seedsigner/models/settings_definition.py +msgid "Prompt" +msgstr "הנחיה" + +#: src/seedsigner/models/settings_definition.py +msgid "Required" +msgstr "נדרש" + +#. Translator is expected ONLY to change their own locale below +#: src/seedsigner/models/settings_definition.py +msgid "Arabic" +msgstr "عربي" + +#: src/seedsigner/models/settings_definition.py +msgid "Czech" +msgstr "čeština" + +#: src/seedsigner/models/settings_definition.py +msgid "French" +msgstr "Français" + +#: src/seedsigner/models/settings_definition.py +msgid "German" +msgstr "Deutsch" + +#: src/seedsigner/models/settings_definition.py +msgid "Hebrew" +msgstr "עִברִית" + +#: src/seedsigner/models/settings_definition.py +msgid "Japanese" +msgstr "日本" + +#: src/seedsigner/models/settings_definition.py +msgid "Korean" +msgstr "한국인" + +#: src/seedsigner/models/settings_definition.py +msgid "Portuguese" +msgstr "Português" + +#: src/seedsigner/models/settings_definition.py +msgid "Russian" +msgstr "Русский" + +#: src/seedsigner/models/settings_definition.py +msgid "Spanish" +msgstr "Español" + +#: src/seedsigner/models/settings_definition.py +msgid "0°" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "90°" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "180°" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "270°" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Low" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Medium" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "High" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Mainnet" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Testnet" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Regtest" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Store Settings on SD card" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Insert SD card to enable" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +#: src/seedsigner/views/seed_views.py +msgid "Single Sig" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Native Segwit" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Nested Segwit (legacy)" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Taproot" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Custom Derivation" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Language" +msgstr "שפה" + +#: src/seedsigner/models/settings_definition.py +msgid "Mnemonic language" +msgstr "שפת הזכרונות" + +#: src/seedsigner/models/settings_definition.py +msgid "Persistent settings" +msgstr "הגדרות קבועות" + +#: src/seedsigner/models/settings_definition.py +msgid "Coordinator software" +msgstr "תוכנת רכז" + +#: src/seedsigner/models/settings_definition.py +msgid "Denomination display" +msgstr "תצוגת יחידה" + +#: src/seedsigner/models/settings_definition.py +msgid "Bitcoin network" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "QR code density" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Xpub export" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Sig types" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Script types" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Show xpub details" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "BIP-39 passphrase" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Camera rotation" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Compact SeedQR" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "BIP-85 child seeds" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Message signing" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Show privacy warnings" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Show dire warnings" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Show QR brightness tips" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Show partner logos" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "QR background color" +msgstr "" + +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +msgid "Scan a seed" +msgstr "" + +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +msgid "Enter 12-word seed" +msgstr "" + +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +msgid "Enter 24-word seed" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Select Signer" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Parsing PSBT..." +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Unsupported Script Type!" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "PSBT has unsupported input script type, please verify your change addresses." +msgstr "" + +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/view.py +msgid "Continue" +msgstr "" + +#. User will receive no change back; the inputs to this transaction are fully +#. spent +#: src/seedsigner/views/psbt_views.py +msgid "Full Spend!" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "This PSBT spends its entire input value. No change is coming back to your wallet." +msgstr "" + +#. Future-tense used to indicate that this transaction will send this amount, +#. as opposed to "Send" on its own which could be misread as an instant command +#. (e.g. "Send Now"). +#: src/seedsigner/views/psbt_views.py +msgid "Will Send" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Next Recipient" +msgstr "" + +#. The amount you're receiving back from the transaction +#: src/seedsigner/views/psbt_views.py +msgid "Your Change" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Verify Multisig Change" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Self-Transfer" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Verify Multisig Addr" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Verifying Change..." +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Verifying Self-Transfer..." +msgstr "" + +#. Variable is either "change" or "self-transfer". +#: src/seedsigner/views/psbt_views.py +msgid "PSBT's {} address could not be verified with your multisig wallet descriptor." +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Suspicious PSBT" +msgstr "" + +#. Variable is either "change" or "self-transfer". +#: src/seedsigner/views/psbt_views.py +msgid "PSBT's {} address could not be generated from your seed." +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Address Verification Failed" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Discard PSBT" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Approve PSBT" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Select Diff Seed" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "PSBT Error" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Signing Failed" +msgstr "" + +#: src/seedsigner/views/psbt_views.py +msgid "Signing with this seed did not add a valid signature." +msgstr "" + +#. This is on the opening splash screen, displayed above the HRF logo +#: src/seedsigner/views/screensaver.py +msgid "With support from:" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Load a seed" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "In-Memory Seeds" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Load the seed to verify" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Select seed to verify" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Load the seed to sign with" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Select seed to sign with" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Scan a SeedQR" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Create a seed" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Load A Seed" +msgstr "" + +#. Inserts the word number (e.g. "Seed Word #6") +#: src/seedsigner/views/seed_views.py +msgid "Seed Word #{}" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Review & Edit" +msgstr "" + +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py +msgid "Discard" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Invalid Mnemonic!" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Checksum failure; not a valid seed phrase." +msgstr "" + +#: src/seedsigner/views/seed_views.py src/seedsigner/views/view.py +msgid "Done" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Edit passphrase" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Keep Seed" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Discard Seed?" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Wipe seed {} from the device?" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Scan PSBT" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Verify Addr" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Child Seed" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Discard Seed" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "View Seed Words" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Export as SeedQR" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Xpub can be used to view all future transactions." +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Generating xpub..." +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "You must keep your seed words private & away from all online devices." +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Child #{}" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Seed Words" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "12 Words" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "24 Words" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Num Words" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Index Error" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Invalid Child Index" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Child Index must be between 0 and 2^31-1." +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Try Again" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Verify" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Skip" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Verify Word #{}" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Review Seed Words" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Verification Error" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Wrong Word!" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Word #{} is not \"{}\"!" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Backup Verified" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "All mnemonic backup words were successfully verified!" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Standard: 25x25" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Compact: 21x21" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Standard: 29x29" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Compact: 25x25" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "SeedQR Format" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Never photograph or scan it into a device that connects to the internet." +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Confirm SeedQR" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Confirm SeedQR?" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Scan your SeedQR" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Error!" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Your transcribed SeedQR does not match your original seed!" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Review SeedQR" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Your transcribed SeedQR successfully scanned and yielded the same seed." +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Your transcribed SeedQR could not be read!" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Sig type can't be auto-detected from this address. Please specify:" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Can't validate a single sig addr without specifying a seed" +msgstr "" + +#. Option when scanning for a matching address; skips ten addresses ahead +#: src/seedsigner/views/seed_views.py +msgid "Skip 10" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Cancel" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "multisig" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "seed {}" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Address Verified" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Scan Descriptor" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Return to PSBT" +msgstr "" + +#: src/seedsigner/views/settings_views.py +msgid "I/O test" +msgstr "" + +#: src/seedsigner/views/settings_views.py +msgid "Advanced" +msgstr "" + +#: src/seedsigner/views/settings_views.py +msgid "Dev Options" +msgstr "" + +#: src/seedsigner/views/settings_views.py +msgid "Persistent Settings enabled. Settings saved to SD card." +msgstr "" + +#: src/seedsigner/views/settings_views.py +msgid "Settings updated in temporary memory" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "New seed" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Calc 12th/24th word" +msgstr "" + +#: src/seedsigner/views/tools_views.py src/seedsigner/views/view.py +msgid "Tools" +msgstr "כלים" + +#: src/seedsigner/views/tools_views.py +msgid "12 words" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "24 words" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Mnemonic Length?" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "12 words (50 rolls)" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "24 words (99 rolls)" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Mnemonic Length" +msgstr "" + +#. Label to gather entropy through coin tosses +#: src/seedsigner/views/tools_views.py +msgid "Coin flip entropy" +msgstr "" + +#. Label to gather entropy through user specified BIP-39 word +#: src/seedsigner/views/tools_views.py +msgid "Word selection entropy" +msgstr "" + +#. Label to allow user to default entropy as all-zeros +#: src/seedsigner/views/tools_views.py +msgid "Finalize with zeros" +msgstr "" + +#. label to calculate the last word of a BIP-39 mnemonic seed phrase +#: src/seedsigner/views/tools_views.py +msgid "Final Word Calc" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Load seed" +msgstr "" + +#. label for addresses where others send us incoming payments +#: src/seedsigner/views/tools_views.py +msgid "Receive Addresses" +msgstr "" + +#. label for addresses that collect the change from our own outgoing payments +#: src/seedsigner/views/tools_views.py +msgid "Change Addresses" +msgstr "" + +#. a status message that our payment addresses are being calculated +#: src/seedsigner/views/tools_views.py +msgid "Calculating addrs..." +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Next {}" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Receive Addrs" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Change Addrs" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Scan" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Seeds" +msgstr "" + +#: src/seedsigner/views/view.py msgid "Restart" -msgstr "הפעלה מחדש" +msgstr "" -#: src/seedsigner/views/main_menu_views.py:43 +#: src/seedsigner/views/view.py msgid "Power Off" -msgstr "כיבוי" +msgstr "" -#: src/seedsigner/views/main_menu_views.py:46 +#: src/seedsigner/views/view.py msgid "Reset / Power" -msgstr "איפוס / הפעלה" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Work In Progress" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Not Yet Implemented" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "This is still on our to-do list!" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Back to Main Menu" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Error" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Network Mismatch" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Change Setting" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "System Error" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Update Setting" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Option Disabled" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Security Tip" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "" +"For maximum security,\n" +"remove the MicroSD card\n" +"before continuing." +msgstr "" + +#~ msgid "" +#~ "SeedSigner is free & open source, funded solely by the Bitcoin community.\n" +#~ "\n" +#~ "Donate onchain or LN at:" +#~ msgstr "" +#~ "SeedSigner הוא 100% חינמי וקוד פתוח, הממומן אך ורק על ידי קהילת הביטקוין.\n" +#~ "\n" +#~ "תרמו בעזרת על-השרשרת או ברשת ברק בכתובת:" + +#~ msgid "Add Passphrase" +#~ msgstr "" + +#~ msgid "Btc-only" +#~ msgstr "" + +#~ msgid "Sats-only" +#~ msgstr "" + +#~ msgid "Threshold at 0.01" +#~ msgstr "" + +#~ msgid "Btc | Sats hybrid" +#~ msgstr "" + +#~ msgid "Scan" +#~ msgstr "" + +#~ msgid "Seeds" +#~ msgstr "" + +#~ msgid "Restart" +#~ msgstr "" + +#~ msgid "Power Off" +#~ msgstr "" + +#~ msgid "Reset / Power" +#~ msgstr "איפוס / הפעלה" + +#~ msgid " (?)" +#~ msgstr "" + +#~ msgid "Scan a QR code" +#~ msgstr "" + +#~ msgid "Yes " +#~ msgstr "" + +#~ msgid "No " +#~ msgstr "" + +#~ msgid "< back | Scan a QR code" +#~ msgstr "" + +#~ msgid "Settings QR" +#~ msgstr "" + +#~ msgid "Settings imported successfully!" +#~ msgstr "" + +#~ msgid "MicroSD removed" +#~ msgstr "" + +#~ msgid "MicroSD inserted" +#~ msgstr "" + +#~ msgid "changes seed:" +#~ msgstr "" + +#~ msgid "< back | click joystick" +#~ msgstr "" + +#~ msgid "Store Settings on SD card." +#~ msgstr "" + +#~ msgid "MicroSD card is removed" +#~ msgstr "" + +#~ msgid "Show passphrase warning" +#~ msgstr "" + +#~ msgid "" +#~ "BIP-39 passphrase changes your seed, resulting in new xpubs, wallets, etc.\n" +#~ "Be sure you know what you're doing!" +#~ msgstr "" + +#~ msgid "SeedQR is your private key!" +#~ msgstr "" + +#~ msgid "Never photograph it or scan it into an online device." +#~ msgstr "" + +#~ msgid " New seed" +#~ msgstr "" + +#~ msgid "Scan wallet descriptor" +#~ msgstr "" + diff --git a/src/seedsigner/resources/babel/ja/LC_MESSAGES/messages.po b/src/seedsigner/resources/babel/ja/LC_MESSAGES/messages.po index 327444ad..29a08d55 100644 --- a/src/seedsigner/resources/babel/ja/LC_MESSAGES/messages.po +++ b/src/seedsigner/resources/babel/ja/LC_MESSAGES/messages.po @@ -1,39 +1,206 @@ -# Translations template for PROJECT. +# Japanese translations for PROJECT. # Copyright (C) 2022 ORGANIZATION # This file is distributed under the same license as the PROJECT project. # FIRST AUTHOR , 2022. -# +# # Translators: # mister wizard, 2022 # I M, 2022 # Keith Mukai, 2022 -# +# jdlcdl, 2022 #, fuzzy msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-05-10 17:32-0500\n" +"POT-Creation-Date: 2023-10-09 11:36-0400\n" "PO-Revision-Date: 2022-04-29 13:50+0000\n" "Last-Translator: Keith Mukai, 2022\n" -"Language-Team: Japanese (https://www.transifex.com/seedsigner/teams/135832/ja/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.10.1\n" "Language: ja\n" +"Language-Team: Japanese (https://www.transifex.com/seedsigner/teams/135832/ja/)\n" "Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.10.3\n" -#: src/seedsigner/gui/screens/main_menu_screens.py:12 -#: src/seedsigner/gui/screens/scan_screens.py:140 -msgid "Home" -msgstr "メニュー" +#. The abbreviated label for the special key on a standard keyboard. +#: src/seedsigner/gui/keyboard.py +msgid "del" +msgstr "" -#: src/seedsigner/gui/screens/main_menu_screens.py:24 +#. The abbreviated label for the special key on a standard keyboard. +#: src/seedsigner/gui/keyboard.py +msgid "space" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "" +"You can remove\n" +"the SD card now" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "SD card removed" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "SD card inserted" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review PSBT" +msgstr "PSBTの内容を確認する" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review Details" +msgstr "詳細を確認する" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "1 input" +msgstr "インプット数 = 1" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input 1" +msgstr "インプット1" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input 2" +msgstr "インプット2" + +#. Indicates that items have been omitted from a series: e.g. "1, 2, 3, [...], +#. 8" +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "[ ... ]" +msgstr "[ ... ]" + +#. Input number will be inserted (e.g. "input 3") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input {}" +msgstr "インプット {}" + +#. Ellipsis ("...") characters used to truncate an address (e.g. "bc1qabc...") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "..." +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/views/psbt_views.py +msgid "self-transfer" +msgstr "自己送付" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient 1" +msgstr "宛先1" + +#. Inserts the recipient number (e.g. the fifth one is: "recipient 5") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient {}" +msgstr "宛先{}" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "fee" +msgstr "手数料" + +#. Label for a change output in the PSBT Overview flow diagram +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/views/psbt_views.py +msgid "change" +msgstr "お釣り" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "PSBT Math" +msgstr "PSBT算出方式" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review Recipients" +msgstr "宛先を確認する" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "btc" +msgstr "btc" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "sats" +msgstr "sat" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input" +msgid_plural "inputs" +msgstr[0] "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient" +msgid_plural "recipients" +msgstr[0] "" + +#. Denonination is inserted (e.g. your "btc change" or "sats change") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "{} change" +msgstr "" + +#. First variable is either "Multisig" or the fingerprint for single sig; +#. Second is "Change" or "Addr" (change vs receive addr); Third is the address +#. index number (e.g. your #4 receive addr). +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/models/settings_definition.py +#: src/seedsigner/views/seed_views.py +msgid "Multisig" +msgstr "マルチシグ" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Change" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Addr" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Address verified!" +msgstr "アドレスを確認済み。" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Sign PSBT" +msgstr "PSBTに署名する" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Click to approve this transaction" +msgstr "" + +#: src/seedsigner/gui/screens/scan_screens.py +msgid "< back | " +msgstr "" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "Success!" +msgstr "成功しました。" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "OK" +msgstr "OK" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/psbt_views.py +msgid "Caution" +msgstr "注意" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "Privacy Leak!" +msgstr "個人情報漏洩注意!" + +#: src/seedsigner/gui/screens/screen.py +msgid "I Understand" +msgstr "承知しました。" + +#: src/seedsigner/gui/screens/screen.py +msgid "Classified Info!" +msgstr "秘密情報!" + +#: src/seedsigner/gui/screens/screen.py msgid "Restarting" msgstr "再起動中" -#: src/seedsigner/gui/screens/main_menu_screens.py:29 +#: src/seedsigner/gui/screens/screen.py msgid "" "SeedSigner is restarting.\n" "\n" @@ -43,347 +210,225 @@ msgstr "" "\n" "揮発性メモリーにある全てのデータが削除されます。" -#: src/seedsigner/gui/screens/main_menu_screens.py:39 +#: src/seedsigner/gui/screens/screen.py msgid "Powering Off" msgstr "シャットダウン中" -#: src/seedsigner/gui/screens/main_menu_screens.py:44 +#: src/seedsigner/gui/screens/screen.py msgid "Please wait about 30 seconds before disconnecting power." msgstr "電源を外す前に、約30秒待ってください。" -#: src/seedsigner/gui/screens/psbt_screens.py:29 -#: src/seedsigner/views/seed_views.py:352 -msgid "Review PSBT" -msgstr "PSBTの内容を確認する" - -#: src/seedsigner/gui/screens/psbt_screens.py:31 -msgid "Review Details" -msgstr "詳細を確認する" - -#: src/seedsigner/gui/screens/psbt_screens.py:88 -msgid "1 input" -msgstr "インプット数 = 1" - -#: src/seedsigner/gui/screens/psbt_screens.py:90 -msgid "input 1" -msgstr "インプット1" - -#: src/seedsigner/gui/screens/psbt_screens.py:91 -msgid "input 2" -msgstr "インプット2" - -#. Indicates that items have been omitted from a series: e.g. "1, 2, 3, [...], -#. 8" -#: src/seedsigner/gui/screens/psbt_screens.py:93 -#: src/seedsigner/gui/screens/psbt_screens.py:141 -msgid "[ ... ]" -msgstr "[ ... ]" - -#. Input number will be inserted (e.g. "input 3") -#: src/seedsigner/gui/screens/psbt_screens.py:95 -#: src/seedsigner/gui/screens/psbt_screens.py:96 -#: src/seedsigner/gui/screens/psbt_screens.py:99 -msgid "input {}" -msgstr "インプット {}" - -#. Display a truncated address (e.g. "bc1qabc...") -#: src/seedsigner/gui/screens/psbt_screens.py:127 -msgid "{}..." -msgstr "{}..." - -#: src/seedsigner/gui/screens/psbt_screens.py:136 -msgid "self-transfer" -msgstr "自己送付" - -#: src/seedsigner/gui/screens/psbt_screens.py:139 -msgid "recipient 1" -msgstr "宛先1" - -#. Inserts the recipient number (e.g. the fifth one is: "recipient 5") -#: src/seedsigner/gui/screens/psbt_screens.py:143 -msgid "recipient {}" -msgstr "宛先{}" - -#: src/seedsigner/gui/screens/psbt_screens.py:145 -#: src/seedsigner/gui/screens/psbt_screens.py:555 -msgid "fee" -msgstr "手数料" - -#. Label for a change output in the PSBT Overview flow diagram -#: src/seedsigner/gui/screens/psbt_screens.py:150 -msgid "change" -msgstr "お釣り" - -#: src/seedsigner/gui/screens/psbt_screens.py:461 -msgid "PSBT Math" -msgstr "PSBT算出方式" - -#: src/seedsigner/gui/screens/psbt_screens.py:462 -msgid "Review Recipients" -msgstr "宛先を確認する" - -#: src/seedsigner/gui/screens/psbt_screens.py:468 -#: src/seedsigner/gui/screens/psbt_screens.py:521 -msgid "btc" -msgstr "btc" - -#: src/seedsigner/gui/screens/psbt_screens.py:480 -msgid "sats" -msgstr "sat" - -#: src/seedsigner/gui/screens/psbt_screens.py:538 -msgid "input" -msgid_plural "inputs" -msgstr[0] "" - -#: src/seedsigner/gui/screens/psbt_screens.py:548 -msgid "recipient" -msgid_plural "recipients" -msgstr[0] "" - -#. Denonination is inserted (e.g. your "btc change" or "sats change") -#: src/seedsigner/gui/screens/psbt_screens.py:566 -msgid "{} change" -msgstr "" - -#: src/seedsigner/gui/screens/psbt_screens.py:671 -msgid "Address verified!" -msgstr "アドレスを確認済み。" - -#: src/seedsigner/gui/screens/psbt_screens.py:683 -msgid "Sign PSBT" -msgstr "PSBTに署名する" - -#: src/seedsigner/gui/screens/psbt_screens.py:697 -msgid "Click to authorize this transaction" -msgstr "この取り引きを許可する場合、クリックしてください。" - -#: src/seedsigner/gui/screens/psbt_screens.py:707 -msgid "Signed PSBT" -msgstr "署名済みPSBT" - -#. Prompt to specify which coordinator software to use (e.g. Specter, Sparrow, -#. etc) -#: src/seedsigner/gui/screens/psbt_screens.py:713 -msgid "Export as a QR code for:" -msgstr "{}用QRコードとしてエクスポートする。" - -#: src/seedsigner/gui/screens/scan_screens.py:22 -msgid "Scan a QR code" -msgstr "QRコードを読み取る" - -#: src/seedsigner/gui/screens/scan_screens.py:138 -msgid "Settings QR" -msgstr "デフォルト設定を保存するためのQR" - -#: src/seedsigner/gui/screens/scan_screens.py:156 -msgid "Settings imported successfully!" -msgstr "デフォルト設定を無事インポートできました。" - -#: src/seedsigner/gui/screens/screen.py:731 -#: src/seedsigner/views/seed_views.py:954 -#: src/seedsigner/views/seed_views.py:1201 -msgid "Success!" -msgstr "成功しました。" - -#: src/seedsigner/gui/screens/screen.py:735 -#: src/seedsigner/views/seed_views.py:956 -#: src/seedsigner/views/seed_views.py:1204 -#: src/seedsigner/views/seed_views.py:1647 src/seedsigner/views/view.py:145 -msgid "OK" -msgstr "OK" - -#: src/seedsigner/gui/screens/screen.py:852 -#: src/seedsigner/views/psbt_views.py:430 -msgid "Caution" -msgstr "注意" - -#: src/seedsigner/gui/screens/screen.py:855 -msgid "I Understand" -msgstr "承知しました。" - -#: src/seedsigner/gui/screens/screen.py:867 -msgid "Classified Info!" -msgstr "秘密情報!" - -#: src/seedsigner/gui/screens/seed_screens.py:411 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Finalize Seed" msgstr "シードを最終化する" -#: src/seedsigner/gui/screens/seed_screens.py:424 -#: src/seedsigner/gui/screens/tools_screens.py:291 +#. a label for the shortened Key-id of a BIP-32 master HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "fingerprint" msgstr "フィンガープリント値" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Backup Seed" +msgstr "復元用シードの控えを書き写す" + +#. Additional explainer for the two seed backup options (mnemonic phrase and +#. SeedQR). +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Backups do not include your passphrase." +msgstr "" + #. Displays the page number and total: (e.g. page 1 of 6) -#: src/seedsigner/gui/screens/seed_screens.py:461 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Seed Words: {}/{}" msgstr "{}/{}のシードワード" -#: src/seedsigner/gui/screens/seed_screens.py:541 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "BIP-85 Index" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py msgid "Verify Backup?" msgstr "復元用控えを確認する?" -#: src/seedsigner/gui/screens/seed_screens.py:547 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Optionally verify that your mnemonic backup is correct." msgstr "復元用シードの控えに誤りがないかを確認する(省略可)。" -#: src/seedsigner/gui/screens/seed_screens.py:556 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Derivation Path" msgstr "導出パス" -#: src/seedsigner/gui/screens/seed_screens.py:703 -msgid "Xpub Details" -msgstr "xpubの詳細" - -#: src/seedsigner/gui/screens/seed_screens.py:709 -#: src/seedsigner/gui/screens/seed_screens.py:713 -#: src/seedsigner/views/seed_views.py:354 -#: src/seedsigner/views/seed_views.py:471 -#: src/seedsigner/views/seed_views.py:504 -#: src/seedsigner/views/seed_views.py:578 +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py msgid "Export Xpub" msgstr "xpubをエクスポートする" -#: src/seedsigner/gui/screens/seed_screens.py:722 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Xpub Details" +msgstr "xpubの詳細" + +#. Short for "BIP32 Master Fingerprint" +#. a label for the shortened Key-id of a BIP-32 master HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Fingerprint" msgstr "フィンガープリント値" #. Short for "Derivation Path" -#: src/seedsigner/gui/screens/seed_screens.py:732 +#. a label for the derivation-path into a BIP-32 HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Derivation" msgstr "導出パス" -#: src/seedsigner/gui/screens/seed_screens.py:741 +#. Short for "BIP32 Extended Public Key" +#: src/seedsigner/gui/screens/seed_screens.py msgid "Xpub" msgstr "xpub" -#: src/seedsigner/gui/screens/seed_screens.py:754 -#: src/seedsigner/views/seed_views.py:217 -msgid "Add Passphrase" -msgstr "パスフレーズを追加する" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "BIP-39 Passphrase" +msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:1106 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Verify Passphrase" msgstr "パスフレーズを確認する" #. Describes the effect of applying a BIP-39 passphrase; it changes the seed's #. fingerprint -#: src/seedsigner/gui/screens/seed_screens.py:1115 +#: src/seedsigner/gui/screens/seed_screens.py msgid "changes fingerprint" -msgstr "フィンガープリント値を変更する" +msgstr "" #. Refers to the SeedQR type: Standard or Compact -#: src/seedsigner/gui/screens/seed_screens.py:1170 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Standard" msgstr "標準" #. Briefly explains the Standard SeedQR data format -#: src/seedsigner/gui/screens/seed_screens.py:1172 +#: src/seedsigner/gui/screens/seed_screens.py msgid "BIP-39 wordlist indices" msgstr "BIP-39のワードリストで何番目のワード" #. Refers to the SeedQR type: Standard or Compact -#: src/seedsigner/gui/screens/seed_screens.py:1180 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Compact" msgstr "コンパクト" #. Briefly explains the Compact SeedQR data format -#: src/seedsigner/gui/screens/seed_screens.py:1182 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Raw entropy bits" msgstr "生のエントロピー用ビット" -#: src/seedsigner/gui/screens/seed_screens.py:1196 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Transcribe SeedQR" msgstr "シードQRの控えを書き写す" #. Refers to the QR code size: 21x21, 25x25, or 29x29 -#: src/seedsigner/gui/screens/seed_screens.py:1198 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Begin {}x{}" msgstr "{}x{} 開始" -#: src/seedsigner/gui/screens/seed_screens.py:1275 +#: src/seedsigner/gui/screens/seed_screens.py msgid "click to exit" msgstr "戻るにはクリックしてください。" -#: src/seedsigner/gui/screens/seed_screens.py:1411 -msgid "" -"Optionally scan your transcribed SeedQR to confirm that it reads back " -"correctly." +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Optionally scan your transcribed SeedQR to confirm that it reads back correctly." msgstr "書き写されたシードQRの控えを読み取り、誤りがないかを確認する(省略可)。" -#: src/seedsigner/gui/screens/seed_screens.py:1468 -#: src/seedsigner/views/seed_views.py:1284 -#: src/seedsigner/views/seed_views.py:1342 +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py msgid "Verify Address" msgstr "アドレスを確認する" #. Inserts the nth address number (e.g. "Checking address 7") -#: src/seedsigner/gui/screens/seed_screens.py:1527 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Checking address {}" msgstr "アドレス{}を確認中" -#: src/seedsigner/gui/screens/seed_screens.py:1544 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Multisig Verification" msgstr "マルチシグの確認" -#: src/seedsigner/gui/screens/seed_screens.py:1549 -msgid "" -"Load your multisig wallet descriptor to verify your receive/self-transfer or" -" change address." +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Load your multisig wallet descriptor to verify your receive/self-transfer or change address." msgstr "宛先・自己送付・お釣り用アドレスを確認するには、マルチシグウォレットのdescriptorを入力してください。" -#: src/seedsigner/gui/screens/seed_screens.py:1562 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Descriptor Loaded" msgstr "descriptor入力済み" #. Label for the multisig wallet's signing policy (e.g. 2-of-3) -#: src/seedsigner/gui/screens/seed_screens.py:1568 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Policy" msgstr "スクリプト仕様" -#: src/seedsigner/gui/screens/seed_screens.py:1576 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Signing Keys" msgstr "署名用鍵" -#: src/seedsigner/gui/screens/settings_screens.py:26 -#: src/seedsigner/views/main_menu_views.py:21 -#: src/seedsigner/views/settings_views.py:36 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Review Message" +msgstr "" + +#. Short for "Next step" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +#: src/seedsigner/views/tools_views.py +msgid "Next" +msgstr "次" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Confirm Address" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Sign Message" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "derivation path" +msgstr "" + +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py msgid "Settings" msgstr "設定" #. Short for "Input/Output"; screen to make sure the buttons and camera are #. working properly -#: src/seedsigner/gui/screens/settings_screens.py:58 +#: src/seedsigner/gui/screens/settings_screens.py msgid "I/O Test" msgstr "入出力テスト" #. Blank the screen -#: src/seedsigner/gui/screens/settings_screens.py:129 -#: src/seedsigner/gui/screens/settings_screens.py:140 -#: src/seedsigner/gui/screens/settings_screens.py:221 +#: src/seedsigner/gui/screens/settings_screens.py msgid "Clear" msgstr "画像を破棄" -#: src/seedsigner/gui/screens/settings_screens.py:161 +#: src/seedsigner/gui/screens/settings_screens.py msgid "Exit" msgstr "戻る" -#: src/seedsigner/gui/screens/settings_screens.py:175 -#: src/seedsigner/gui/screens/tools_screens.py:71 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Capturing image..." msgstr "画像を読み取り中..." -#: src/seedsigner/gui/screens/settings_screens.py:288 -#: src/seedsigner/views/settings_views.py:21 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py msgid "Donate" msgstr "寄付する" -#. If your language uses the percent sign ("%"), your translation must also -#. use +#. If your language uses the percent sign ("%"), your translation must also use #. two percent signs ("%%") due to python formatting oddities. "100%%" will be #. rendered as "100%". -#: src/seedsigner/gui/screens/settings_screens.py:293 +#: src/seedsigner/gui/screens/settings_screens.py #, python-format msgid "" "SeedSigner is 100%% free & open source, funded solely by the Bitcoin community.\n" @@ -394,702 +439,992 @@ msgstr "" "\n" "BTC若しくはライトニングによる寄付をするには、下記リンクを使ってください。" -#. Instructions when taking a picture; "back" should point left. -#: src/seedsigner/gui/screens/tools_screens.py:40 -msgid "< back | click joystick" -msgstr "< 戻る | ジョイスティックをクリック" +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py +msgid "Settings QR" +msgstr "" + +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py +msgid "Home" +msgstr "メニュー" #. Instructions when reviewing an image; "reshoot" must point left, "accept" #. must point right -#: src/seedsigner/gui/screens/tools_screens.py:98 +#: src/seedsigner/gui/screens/tools_screens.py msgid " < reshoot | accept > " msgstr " < 撮り直す | 確定 > " #. current roll number vs total rolls (e.g. roll 7 of 50) -#: src/seedsigner/gui/screens/tools_screens.py:120 -#: src/seedsigner/gui/screens/tools_screens.py:245 +#: src/seedsigner/gui/screens/tools_screens.py msgid "Dice Roll {}/{}" msgstr "" -#: src/seedsigner/gui/screens/tools_screens.py:274 +#. Build the last word in a 12 or 24 word BIP-39 mnemonic seed phrase. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Build Final Word" +msgstr "" + +#. Number of BIP-39 seed words, and the entropy -- in bits, contained within. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "The {}th word is built from {} more entropy bits plus auto-calculated checksum." +msgstr "" + +#. current coin-flip number vs total flips (e.g. flip 3 of 4) +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Coin Flip {}/{}" +msgstr "" + +#. How we call the "front" side result during a coin toss. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Heads = 1" +msgstr "" + +#. How we call the "back" side result during a coin toss. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Tails = 0" +msgstr "" + +#. A labeled presentation of a user-supplied value +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Your input: \"{}\"" +msgstr "" + +#. A function of "x" to be used for detecting errors in "x" +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Checksum" +msgstr "" + +#. labeled presentation of the last word in a BIP-39 mnemonic seed phrase. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Final Word: \"{}\"" +msgstr "" + +#. a label for the last word of a 12-word BIP-39 mnemonic seed phrase +#: src/seedsigner/gui/screens/tools_screens.py msgid "12th Word" msgstr "" -#: src/seedsigner/gui/screens/tools_screens.py:276 +#. a label for the last word of a 24-word BIP-39 mnemonic seed phrase +#: src/seedsigner/gui/screens/tools_screens.py msgid "24th Word" msgstr "" -#: src/seedsigner/models/settings_definition.py:14 +#. a label for the tool to explore public addresses for this seed. +#: src/seedsigner/gui/screens/tools_screens.py +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py +msgid "Address Explorer" +msgstr "" + +#. a label for a BIP-380-ish Output Descriptor +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Wallet descriptor" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Enabled" msgstr "有効" -#: src/seedsigner/models/settings_definition.py:15 -#: src/seedsigner/models/settings_definition.py:20 +#: src/seedsigner/models/settings_definition.py msgid "Disabled" msgstr "無効" -#: src/seedsigner/models/settings_definition.py:18 -#: src/seedsigner/models/settings_definition.py:26 +#: src/seedsigner/models/settings_definition.py msgid "Prompt" msgstr "促す" -#: src/seedsigner/models/settings_definition.py:19 -#: src/seedsigner/models/settings_definition.py:23 -#: src/seedsigner/models/settings_definition.py:29 +#: src/seedsigner/models/settings_definition.py msgid "Required" msgstr "必須" -#: src/seedsigner/models/settings_definition.py:72 -msgid "Btc-only" -msgstr "btcのみ" +#. Translator is expected ONLY to change their own locale below +#: src/seedsigner/models/settings_definition.py +msgid "Arabic" +msgstr "عربي" -#: src/seedsigner/models/settings_definition.py:73 -msgid "Sats-only" -msgstr "satのみ" +#: src/seedsigner/models/settings_definition.py +msgid "Czech" +msgstr "čeština" -#: src/seedsigner/models/settings_definition.py:74 -msgid "Threshold at 0.01" -msgstr "閾値0.01で切り替え" +#: src/seedsigner/models/settings_definition.py +msgid "French" +msgstr "Français" -#: src/seedsigner/models/settings_definition.py:75 -msgid "Btc | Sats hybrid" -msgstr "btc | sat の切り替え表示" +#: src/seedsigner/models/settings_definition.py +msgid "German" +msgstr "Deutsch" -#: src/seedsigner/models/settings_definition.py:83 +#: src/seedsigner/models/settings_definition.py +msgid "Hebrew" +msgstr "עִברִית" + +#: src/seedsigner/models/settings_definition.py +msgid "Japanese" +msgstr "日本" + +#: src/seedsigner/models/settings_definition.py +msgid "Korean" +msgstr "한국인" + +#: src/seedsigner/models/settings_definition.py +msgid "Portuguese" +msgstr "Português" + +#: src/seedsigner/models/settings_definition.py +msgid "Russian" +msgstr "Русский" + +#: src/seedsigner/models/settings_definition.py +msgid "Spanish" +msgstr "Español" + +#: src/seedsigner/models/settings_definition.py msgid "0°" msgstr "0°" -#: src/seedsigner/models/settings_definition.py:84 +#: src/seedsigner/models/settings_definition.py msgid "90°" msgstr "90°" -#: src/seedsigner/models/settings_definition.py:85 +#: src/seedsigner/models/settings_definition.py msgid "180°" msgstr "180°" -#: src/seedsigner/models/settings_definition.py:86 +#: src/seedsigner/models/settings_definition.py msgid "270°" msgstr "270°" -#: src/seedsigner/models/settings_definition.py:94 +#: src/seedsigner/models/settings_definition.py msgid "Low" msgstr "低" -#: src/seedsigner/models/settings_definition.py:95 +#: src/seedsigner/models/settings_definition.py msgid "Medium" msgstr "中" -#: src/seedsigner/models/settings_definition.py:96 +#: src/seedsigner/models/settings_definition.py msgid "High" msgstr "高" -#: src/seedsigner/models/settings_definition.py:104 +#: src/seedsigner/models/settings_definition.py msgid "Mainnet" msgstr "メインネット" -#: src/seedsigner/models/settings_definition.py:105 +#: src/seedsigner/models/settings_definition.py msgid "Testnet" msgstr "テストネット" -#: src/seedsigner/models/settings_definition.py:106 +#: src/seedsigner/models/settings_definition.py msgid "Regtest" msgstr "リグレッションテスト" -#: src/seedsigner/models/settings_definition.py:122 -#: src/seedsigner/views/seed_views.py:466 +#: src/seedsigner/models/settings_definition.py +msgid "Store Settings on SD card" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Insert SD card to enable" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +#: src/seedsigner/views/seed_views.py msgid "Single Sig" msgstr "シングルシグ" -#: src/seedsigner/models/settings_definition.py:123 -#: src/seedsigner/views/seed_views.py:467 -msgid "Multisig" -msgstr "マルチシグ" +#: src/seedsigner/models/settings_definition.py +msgid "Native Segwit" +msgstr "" -#: src/seedsigner/models/settings_definition.py:354 +#: src/seedsigner/models/settings_definition.py +msgid "Nested Segwit (legacy)" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Taproot" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Custom Derivation" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Language" msgstr "言語" -#: src/seedsigner/models/settings_definition.py:362 +#: src/seedsigner/models/settings_definition.py msgid "Mnemonic language" msgstr "シードワード言語" -#: src/seedsigner/models/settings_definition.py:370 +#: src/seedsigner/models/settings_definition.py msgid "Persistent settings" msgstr "非揮発性設定" -#: src/seedsigner/models/settings_definition.py:376 +#: src/seedsigner/models/settings_definition.py msgid "Coordinator software" msgstr "コーディネーター用アプリ" -#: src/seedsigner/models/settings_definition.py:383 +#: src/seedsigner/models/settings_definition.py msgid "Denomination display" msgstr "表示金額の単位" -#: src/seedsigner/models/settings_definition.py:392 +#: src/seedsigner/models/settings_definition.py msgid "Bitcoin network" msgstr "ビットコインネットワーク" -#: src/seedsigner/models/settings_definition.py:400 +#: src/seedsigner/models/settings_definition.py msgid "QR code density" msgstr "QRコードの解像度" -#: src/seedsigner/models/settings_definition.py:408 +#: src/seedsigner/models/settings_definition.py msgid "Xpub export" msgstr "xpubをエクスポートする" -#: src/seedsigner/models/settings_definition.py:414 +#: src/seedsigner/models/settings_definition.py msgid "Sig types" msgstr "署名の書類" -#: src/seedsigner/models/settings_definition.py:422 +#: src/seedsigner/models/settings_definition.py msgid "Script types" msgstr "スクリプトの種類" -#: src/seedsigner/models/settings_definition.py:430 +#: src/seedsigner/models/settings_definition.py msgid "Show xpub details" msgstr "xpubの詳細を表示" -#: src/seedsigner/models/settings_definition.py:436 +#: src/seedsigner/models/settings_definition.py msgid "BIP-39 passphrase" msgstr "BIP-39パスフレーズ" -#: src/seedsigner/models/settings_definition.py:444 +#: src/seedsigner/models/settings_definition.py msgid "Camera rotation" msgstr "カメラ回転" -#: src/seedsigner/models/settings_definition.py:452 +#: src/seedsigner/models/settings_definition.py msgid "Compact SeedQR" msgstr "コンパクトなシードQR" -#: src/seedsigner/models/settings_definition.py:458 +#: src/seedsigner/models/settings_definition.py +msgid "BIP-85 child seeds" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Message signing" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Show privacy warnings" msgstr "個人情報漏洩注意の表示" -#: src/seedsigner/models/settings_definition.py:464 +#: src/seedsigner/models/settings_definition.py msgid "Show dire warnings" msgstr "深刻危険注意の表示" -#: src/seedsigner/models/settings_definition.py:470 +#: src/seedsigner/models/settings_definition.py +msgid "Show QR brightness tips" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Show partner logos" msgstr "パートナー ロゴの表示" -#: src/seedsigner/models/settings_definition.py:485 +#: src/seedsigner/models/settings_definition.py msgid "QR background color" msgstr "QR背景色" -#: src/seedsigner/views/main_menu_views.py:18 -msgid "Scan" -msgstr "読み取る" - -#: src/seedsigner/views/main_menu_views.py:19 -msgid "Seeds" -msgstr "シードを作る" - -#: src/seedsigner/views/main_menu_views.py:20 -#: src/seedsigner/views/tools_views.py:29 -msgid "Tools" -msgstr "シードを使う" - -#: src/seedsigner/views/main_menu_views.py:38 -msgid "Restart" -msgstr "再起動" - -#: src/seedsigner/views/main_menu_views.py:39 -msgid "Power Off" -msgstr "シャットダウン" - -#: src/seedsigner/views/main_menu_views.py:42 -msgid "Reset / Power" -msgstr "再起動・シャットダウン" - -#: src/seedsigner/views/psbt_views.py:36 -#: src/seedsigner/views/seed_views.py:1322 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py msgid "Scan a seed" msgstr "シードを読み取る" -#: src/seedsigner/views/psbt_views.py:37 -#: src/seedsigner/views/seed_views.py:1323 -msgid "Enter 12/24 words" -msgstr "12・24ワードを入力する" +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +msgid "Enter 12-word seed" +msgstr "" -#: src/seedsigner/views/psbt_views.py:53 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +msgid "Enter 24-word seed" +msgstr "" + +#: src/seedsigner/views/psbt_views.py msgid "Select Signer" msgstr "署名者を選択する" -#: src/seedsigner/views/psbt_views.py:85 +#: src/seedsigner/views/psbt_views.py msgid "Parsing PSBT..." msgstr "PSBTを解読中..." -#: src/seedsigner/views/psbt_views.py:155 +#: src/seedsigner/views/psbt_views.py msgid "Unsupported Script Type!" msgstr "未対応のスクリプトの種類!" -#: src/seedsigner/views/psbt_views.py:156 -msgid "" -"PSBT has unsupported input script type, please verify your change addresses." +#: src/seedsigner/views/psbt_views.py +msgid "PSBT has unsupported input script type, please verify your change addresses." msgstr "PSBTに未対応のスクリプトの種類があります。お釣り用アドレスをご確認ください。" -#: src/seedsigner/views/psbt_views.py:157 -#: src/seedsigner/views/psbt_views.py:176 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/view.py msgid "Continue" msgstr "続く" #. User will receive no change back; the inputs to this transaction are fully #. spent -#: src/seedsigner/views/psbt_views.py:174 +#: src/seedsigner/views/psbt_views.py msgid "Full Spend!" msgstr "全額が支払われるぞ!" -#: src/seedsigner/views/psbt_views.py:175 -msgid "" -"This PSBT spends its entire input value. No change is coming back to your " -"wallet." +#: src/seedsigner/views/psbt_views.py +msgid "This PSBT spends its entire input value. No change is coming back to your wallet." msgstr "このPSBTのインプットの全額が支払われるため、送金元のウォレットへお釣りが一切戻って来ません。" #. Future-tense used to indicate that this transaction will send this amount, -#. as opposed to "Send" on its own which could be misread as an instant -#. command +#. as opposed to "Send" on its own which could be misread as an instant command #. (e.g. "Send Now"). -#: src/seedsigner/views/psbt_views.py:242 +#: src/seedsigner/views/psbt_views.py msgid "Will Send" msgstr "送付する予定" -#: src/seedsigner/views/psbt_views.py:248 +#: src/seedsigner/views/psbt_views.py msgid "Next Recipient" msgstr "次の宛先" -#. Short for "Next step" -#: src/seedsigner/views/psbt_views.py:251 -#: src/seedsigner/views/psbt_views.py:320 -#: src/seedsigner/views/seed_views.py:796 -msgid "Next" -msgstr "次" - #. The amount you're receiving back from the transaction -#: src/seedsigner/views/psbt_views.py:324 +#: src/seedsigner/views/psbt_views.py msgid "Your Change" msgstr "お釣り" -#: src/seedsigner/views/psbt_views.py:325 +#: src/seedsigner/views/psbt_views.py msgid "Verify Multisig Change" msgstr "マルチシグのお釣りを確認する" -#: src/seedsigner/views/psbt_views.py:327 +#: src/seedsigner/views/psbt_views.py msgid "Self-Transfer" msgstr "自己送付" -#: src/seedsigner/views/psbt_views.py:328 +#: src/seedsigner/views/psbt_views.py msgid "Verify Multisig Addr" msgstr "マルチシグアドレスを確認する" -#: src/seedsigner/views/psbt_views.py:347 +#: src/seedsigner/views/psbt_views.py msgid "Verifying Change..." msgstr "お釣りを確認中..." -#: src/seedsigner/views/psbt_views.py:349 +#: src/seedsigner/views/psbt_views.py msgid "Verifying Self-Transfer..." msgstr "自己送付を確認中..." -#: src/seedsigner/views/psbt_views.py:431 -msgid "" -"PSBT's {} address could not be verified with your multisig wallet " -"descriptor." +#. Variable is either "change" or "self-transfer". +#: src/seedsigner/views/psbt_views.py +msgid "PSBT's {} address could not be verified with your multisig wallet descriptor." msgstr "このマルチシグウォレットのdescriptorでPSBTの{}のアドレスを確認できなかった!" -#: src/seedsigner/views/psbt_views.py:433 +#: src/seedsigner/views/psbt_views.py msgid "Suspicious PSBT" msgstr "怪しいPSBT!" -#: src/seedsigner/views/psbt_views.py:434 +#. Variable is either "change" or "self-transfer". +#: src/seedsigner/views/psbt_views.py msgid "PSBT's {} address could not be generated from your seed." msgstr "シードからPSBTの{}のアドレスを生成できなかった!" -#: src/seedsigner/views/psbt_views.py:438 +#: src/seedsigner/views/psbt_views.py msgid "Address Verification Failed" msgstr "アドレスを確認できなかった!" -#: src/seedsigner/views/psbt_views.py:440 +#: src/seedsigner/views/psbt_views.py msgid "Discard PSBT" msgstr "PSBTを破棄する" -#: src/seedsigner/views/psbt_views.py:465 +#: src/seedsigner/views/psbt_views.py msgid "Approve PSBT" msgstr "PSBTを認可する" -#: src/seedsigner/views/psbt_views.py:470 -msgid "Signing PSBT..." -msgstr "PSBTを署名中..." - -#: src/seedsigner/views/psbt_views.py:548 -msgid "PSBT Error" -msgstr "PSBTのエラーが発生した!" - -#: src/seedsigner/views/psbt_views.py:550 -msgid "Signing Failed" -msgstr "署名できなかった!" - -#: src/seedsigner/views/psbt_views.py:551 -msgid "Signing with this seed did not add a valid signature." -msgstr "このシードで署名しても、有効な署名を付加できなかった!" - -#: src/seedsigner/views/psbt_views.py:552 +#: src/seedsigner/views/psbt_views.py msgid "Select Diff Seed" msgstr "違うシードを選択" +#: src/seedsigner/views/psbt_views.py +msgid "PSBT Error" +msgstr "PSBTのエラーが発生した!" + +#: src/seedsigner/views/psbt_views.py +msgid "Signing Failed" +msgstr "署名できなかった!" + +#: src/seedsigner/views/psbt_views.py +msgid "Signing with this seed did not add a valid signature." +msgstr "このシードで署名しても、有効な署名を付加できなかった!" + #. This is on the opening splash screen, displayed above the HRF logo -#: src/seedsigner/views/screensaver.py:74 +#: src/seedsigner/views/screensaver.py msgid "With support from:" msgstr "" -#: src/seedsigner/views/seed_views.py:53 +#: src/seedsigner/views/seed_views.py msgid "Load a seed" msgstr "シードを入力" -#: src/seedsigner/views/seed_views.py:56 +#: src/seedsigner/views/seed_views.py msgid "In-Memory Seeds" msgstr "揮発性メモリー内のシード" -#: src/seedsigner/views/seed_views.py:77 +#: src/seedsigner/views/seed_views.py +msgid "Load the seed to verify" +msgstr "シードを入力して確認する" + +#: src/seedsigner/views/seed_views.py +msgid "Select seed to verify" +msgstr "シードを選択して確認する" + +#: src/seedsigner/views/seed_views.py +msgid "Load the seed to sign with" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Select seed to sign with" +msgstr "" + +#: src/seedsigner/views/seed_views.py msgid "Scan a SeedQR" msgstr "シードQRを読み取る" -#: src/seedsigner/views/seed_views.py:78 -msgid "Enter 24-word seed" -msgstr "" - -#: src/seedsigner/views/seed_views.py:79 -msgid "Enter 12-word seed" -msgstr "" - -#: src/seedsigner/views/seed_views.py:80 +#: src/seedsigner/views/seed_views.py msgid "Create a seed" msgstr "" -#: src/seedsigner/views/seed_views.py:89 +#: src/seedsigner/views/seed_views.py msgid "Load A Seed" msgstr "" #. Inserts the word number (e.g. "Seed Word #6") -#: src/seedsigner/views/seed_views.py:126 +#: src/seedsigner/views/seed_views.py msgid "Seed Word #{}" msgstr "{}番目のシードワード" -#: src/seedsigner/views/seed_views.py:187 +#: src/seedsigner/views/seed_views.py msgid "Review & Edit" msgstr "確認・編集" -#: src/seedsigner/views/seed_views.py:188 -#: src/seedsigner/views/seed_views.py:310 -#: src/seedsigner/views/tools_views.py:259 +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py msgid "Discard" msgstr "破棄" -#: src/seedsigner/views/seed_views.py:192 +#: src/seedsigner/views/seed_views.py msgid "Invalid Mnemonic!" msgstr "シードが無効です!" -#: src/seedsigner/views/seed_views.py:194 +#: src/seedsigner/views/seed_views.py msgid "Checksum failure; not a valid seed phrase." msgstr "チェックサム整合性無し、シードが無効です!" -#: src/seedsigner/views/seed_views.py:216 -#: src/seedsigner/views/seed_views.py:268 -#: src/seedsigner/views/seed_views.py:797 -#: src/seedsigner/views/seed_views.py:1146 +#: src/seedsigner/views/seed_views.py src/seedsigner/views/view.py msgid "Done" msgstr "終了" -#: src/seedsigner/views/seed_views.py:267 +#: src/seedsigner/views/seed_views.py msgid "Edit passphrase" msgstr "パスフレーズを編集する" -#: src/seedsigner/views/seed_views.py:309 +#: src/seedsigner/views/seed_views.py msgid "Keep Seed" msgstr "シードを保管" -#: src/seedsigner/views/seed_views.py:315 +#: src/seedsigner/views/seed_views.py msgid "Discard Seed?" msgstr "シードを破棄する?" -#: src/seedsigner/views/seed_views.py:317 +#: src/seedsigner/views/seed_views.py msgid "Wipe seed {} from the device?" msgstr "シード{}を消去する?" -#: src/seedsigner/views/seed_views.py:351 +#: src/seedsigner/views/seed_views.py msgid "Scan PSBT" msgstr "PSBTを読み取る" -#: src/seedsigner/views/seed_views.py:353 -#: src/seedsigner/views/seed_views.py:1646 +#: src/seedsigner/views/seed_views.py msgid "Verify Addr" msgstr "アドレスを確認する" -#: src/seedsigner/views/seed_views.py:355 -#: src/seedsigner/views/seed_views.py:436 -msgid "Backup Seed" -msgstr "復元用シードの控えを書き写す" +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Child Seed" +msgstr "" -#: src/seedsigner/views/seed_views.py:356 +#: src/seedsigner/views/seed_views.py msgid "Discard Seed" msgstr "シードを破棄" -#: src/seedsigner/views/seed_views.py:431 +#: src/seedsigner/views/seed_views.py msgid "View Seed Words" msgstr "シードワードを表示" -#: src/seedsigner/views/seed_views.py:432 +#: src/seedsigner/views/seed_views.py msgid "Export as SeedQR" msgstr "シードQRとしてエクスポート" -#: src/seedsigner/views/seed_views.py:620 -msgid "Privacy Leak!" -msgstr "個人情報漏洩注意!" - -#: src/seedsigner/views/seed_views.py:621 +#: src/seedsigner/views/seed_views.py msgid "Xpub can be used to view all future transactions." msgstr "xpubを知ってれば、今後の全ての取り引きを監視できる!" -#: src/seedsigner/views/seed_views.py:651 +#: src/seedsigner/views/seed_views.py msgid "Generating xpub..." msgstr "xpub生成中..." -#: src/seedsigner/views/seed_views.py:771 +#: src/seedsigner/views/seed_views.py msgid "You must keep your seed words private & away from all online devices." msgstr "シードワードを秘密に守り、インタネットに接続可能な機器等に対して露出しないよう、ご注意ください!" -#: src/seedsigner/views/seed_views.py:836 -msgid "Verify" -msgstr "確認する" +#: src/seedsigner/views/seed_views.py +msgid "Child #{}" +msgstr "" -#: src/seedsigner/views/seed_views.py:837 -msgid "Skip" -msgstr "省略する" +#: src/seedsigner/views/seed_views.py +msgid "Seed Words" +msgstr "" -#: src/seedsigner/views/seed_views.py:886 -msgid "Verify Word #{}" -msgstr "{}番目のワードを確認" +#: src/seedsigner/views/seed_views.py +msgid "12 Words" +msgstr "" -#: src/seedsigner/views/seed_views.py:926 -msgid "Review Seed Words" -msgstr "シードワードを確認" +#: src/seedsigner/views/seed_views.py +msgid "24 Words" +msgstr "" -#: src/seedsigner/views/seed_views.py:927 +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Num Words" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Index Error" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Invalid Child Index" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Child Index must be between 0 and 2^31-1." +msgstr "" + +#: src/seedsigner/views/seed_views.py msgid "Try Again" msgstr "もう一度やってみてください。" -#: src/seedsigner/views/seed_views.py:931 +#: src/seedsigner/views/seed_views.py +msgid "Verify" +msgstr "確認する" + +#: src/seedsigner/views/seed_views.py +msgid "Skip" +msgstr "省略する" + +#: src/seedsigner/views/seed_views.py +msgid "Verify Word #{}" +msgstr "{}番目のワードを確認" + +#: src/seedsigner/views/seed_views.py +msgid "Review Seed Words" +msgstr "シードワードを確認" + +#: src/seedsigner/views/seed_views.py msgid "Verification Error" msgstr "確認エラー!" -#: src/seedsigner/views/seed_views.py:933 +#: src/seedsigner/views/seed_views.py msgid "Wrong Word!" msgstr "ワードが誤っている!" -#: src/seedsigner/views/seed_views.py:934 +#: src/seedsigner/views/seed_views.py msgid "Word #{} is not \"{}\"!" msgstr "{}番目のワードは、「{}」ではありません!" -#: src/seedsigner/views/seed_views.py:952 +#: src/seedsigner/views/seed_views.py msgid "Backup Verified" msgstr "復元用シードの控えを確認済み" -#: src/seedsigner/views/seed_views.py:955 +#: src/seedsigner/views/seed_views.py msgid "All mnemonic backup words were successfully verified!" msgstr "復元用シードの控えにある全てのワードを確認済みです。" -#: src/seedsigner/views/seed_views.py:978 +#: src/seedsigner/views/seed_views.py msgid "Standard: 25x25" msgstr "標準: 25x25" -#: src/seedsigner/views/seed_views.py:979 +#: src/seedsigner/views/seed_views.py msgid "Compact: 21x21" msgstr "コンパクト: 21x21" -#: src/seedsigner/views/seed_views.py:983 +#: src/seedsigner/views/seed_views.py msgid "Standard: 29x29" msgstr "標準: 29x29" -#: src/seedsigner/views/seed_views.py:984 +#: src/seedsigner/views/seed_views.py msgid "Compact: 25x25" msgstr "コンパクト: 25x25" -#: src/seedsigner/views/seed_views.py:1003 +#: src/seedsigner/views/seed_views.py msgid "SeedQR Format" msgstr "シードQRの形式" -#: src/seedsigner/views/seed_views.py:1052 -msgid "SeedQR is your private key!" -msgstr "シードQRとは、秘密鍵のことです!" +#: src/seedsigner/views/seed_views.py +msgid "Never photograph or scan it into a device that connects to the internet." +msgstr "" -#: src/seedsigner/views/seed_views.py:1053 -msgid "Never photograph it or scan it into an online device." -msgstr "写真を撮ったり、インタネットに接続可能な機器等で読み取ったりしないよう、ご注意ください!" - -#: src/seedsigner/views/seed_views.py:1145 -#: src/seedsigner/views/seed_views.py:1189 -#: src/seedsigner/views/seed_views.py:1200 -#: src/seedsigner/views/seed_views.py:1212 +#: src/seedsigner/views/seed_views.py msgid "Confirm SeedQR" msgstr "シードQRを確認する" -#: src/seedsigner/views/seed_views.py:1150 +#: src/seedsigner/views/seed_views.py msgid "Confirm SeedQR?" msgstr "シードQRを確認する?" -#: src/seedsigner/views/seed_views.py:1180 +#: src/seedsigner/views/seed_views.py msgid "Scan your SeedQR" msgstr "シードQRを読み取る" -#: src/seedsigner/views/seed_views.py:1190 -#: src/seedsigner/views/seed_views.py:1213 +#: src/seedsigner/views/seed_views.py msgid "Error!" msgstr "エラー!" -#: src/seedsigner/views/seed_views.py:1191 +#: src/seedsigner/views/seed_views.py msgid "Your transcribed SeedQR does not match your original seed!" msgstr "書き写されたシードQRの控えが元のシードと一致しない!" -#: src/seedsigner/views/seed_views.py:1193 -#: src/seedsigner/views/seed_views.py:1216 +#: src/seedsigner/views/seed_views.py msgid "Review SeedQR" msgstr "シードQRを確認" -#: src/seedsigner/views/seed_views.py:1202 -msgid "" -"Your transcribed SeedQR successfully scanned and yielded the same seed." +#: src/seedsigner/views/seed_views.py +msgid "Your transcribed SeedQR successfully scanned and yielded the same seed." msgstr "書き写されたシードQRの控えが無事に読み取られて元のシードと一致する。" -#: src/seedsigner/views/seed_views.py:1214 +#: src/seedsigner/views/seed_views.py msgid "Your transcribed SeedQR could not be read!" msgstr "書き写されたシードQRの控えを読み取れなかった!" -#: src/seedsigner/views/seed_views.py:1285 +#: src/seedsigner/views/seed_views.py msgid "Sig type can't be auto-detected from this address. Please specify:" msgstr "このアドレスから署名の種類を自動検出できなかったため、署名の種類を指定してください。" -#: src/seedsigner/views/seed_views.py:1326 -msgid "Load the seed to verify" -msgstr "シードを入力して確認する" - -#: src/seedsigner/views/seed_views.py:1336 -msgid "Select seed to verify" -msgstr "シードを選択して確認する" - -#: src/seedsigner/views/seed_views.py:1388 +#: src/seedsigner/views/seed_views.py msgid "Can't validate a single sig addr without specifying a seed" msgstr "シングルシグのアドレスを検証するには、シードの指定が必要です。" #. Option when scanning for a matching address; skips ten addresses ahead -#: src/seedsigner/views/seed_views.py:1434 +#: src/seedsigner/views/seed_views.py msgid "Skip 10" msgstr "10を飛ばす" -#: src/seedsigner/views/seed_views.py:1435 -#: src/seedsigner/views/seed_views.py:1616 +#: src/seedsigner/views/seed_views.py msgid "Cancel" msgstr "キャンセル" -#: src/seedsigner/views/seed_views.py:1563 -#: src/seedsigner/views/seed_views.py:1579 -msgid "Taproot verification not yet implemented!" -msgstr "タップルート検証が未実施!" - -#: src/seedsigner/views/seed_views.py:1575 -msgid "Legacy P2PKH verification not implemented!" -msgstr "レガシーP2PKH検証が未実施!" - -#: src/seedsigner/views/seed_views.py:1600 +#: src/seedsigner/views/seed_views.py msgid "multisig" msgstr "マルチシグ" -#: src/seedsigner/views/seed_views.py:1602 +#: src/seedsigner/views/seed_views.py msgid "seed {}" msgstr "シード{}" -#: src/seedsigner/views/seed_views.py:1605 +#: src/seedsigner/views/seed_views.py msgid "Address Verified" msgstr "アドレス確認済み" -#: src/seedsigner/views/seed_views.py:1615 +#: src/seedsigner/views/seed_views.py msgid "Scan Descriptor" msgstr "descriptorを読み取る" -#: src/seedsigner/views/seed_views.py:1645 +#: src/seedsigner/views/seed_views.py msgid "Return to PSBT" msgstr "PSBTへ戻る" -#: src/seedsigner/views/settings_views.py:20 +#: src/seedsigner/views/settings_views.py msgid "I/O test" msgstr "入出力テスト" -#: src/seedsigner/views/settings_views.py:39 -#: src/seedsigner/views/settings_views.py:46 +#: src/seedsigner/views/settings_views.py msgid "Advanced" msgstr "詳細設定" -#: src/seedsigner/views/settings_views.py:54 +#: src/seedsigner/views/settings_views.py msgid "Dev Options" msgstr "開発者向け設定" -#: src/seedsigner/views/tools_views.py:24 -msgid " New seed" -msgstr "新しいシード" +#: src/seedsigner/views/settings_views.py +msgid "Persistent Settings enabled. Settings saved to SD card." +msgstr "" -#: src/seedsigner/views/tools_views.py:25 +#: src/seedsigner/views/settings_views.py +msgid "Settings updated in temporary memory" +msgstr "" + +#: src/seedsigner/views/tools_views.py msgid "New seed" msgstr "新しいシード" -#: src/seedsigner/views/tools_views.py:26 +#: src/seedsigner/views/tools_views.py msgid "Calc 12th/24th word" msgstr "12・24ワード目を算出" -#: src/seedsigner/views/tools_views.py:102 -#: src/seedsigner/views/tools_views.py:228 +#: src/seedsigner/views/tools_views.py src/seedsigner/views/view.py +msgid "Tools" +msgstr "シードを使う" + +#: src/seedsigner/views/tools_views.py msgid "12 words" msgstr "12ワード" -#: src/seedsigner/views/tools_views.py:103 -#: src/seedsigner/views/tools_views.py:229 +#: src/seedsigner/views/tools_views.py msgid "24 words" msgstr "24ワード" -#: src/seedsigner/views/tools_views.py:107 +#: src/seedsigner/views/tools_views.py msgid "Mnemonic Length?" msgstr "シード長は?" -#: src/seedsigner/views/tools_views.py:174 +#: src/seedsigner/views/tools_views.py msgid "12 words (50 rolls)" msgstr "12ワード(サイコロを50回投げる)" -#: src/seedsigner/views/tools_views.py:175 +#: src/seedsigner/views/tools_views.py msgid "24 words (99 rolls)" msgstr "24ワード(サイコロを99回投げる)" -#: src/seedsigner/views/tools_views.py:179 -#: src/seedsigner/views/tools_views.py:233 +#: src/seedsigner/views/tools_views.py msgid "Mnemonic Length" msgstr "シード長" -#: src/seedsigner/views/tools_views.py:258 +#. Label to gather entropy through coin tosses +#: src/seedsigner/views/tools_views.py +msgid "Coin flip entropy" +msgstr "" + +#. Label to gather entropy through user specified BIP-39 word +#: src/seedsigner/views/tools_views.py +msgid "Word selection entropy" +msgstr "" + +#. Label to allow user to default entropy as all-zeros +#: src/seedsigner/views/tools_views.py +msgid "Finalize with zeros" +msgstr "" + +#. label to calculate the last word of a BIP-39 mnemonic seed phrase +#: src/seedsigner/views/tools_views.py +msgid "Final Word Calc" +msgstr "" + +#: src/seedsigner/views/tools_views.py msgid "Load seed" msgstr "シードを入力" -#: src/seedsigner/views/view.py:124 +#. label for addresses where others send us incoming payments +#: src/seedsigner/views/tools_views.py +msgid "Receive Addresses" +msgstr "" + +#. label for addresses that collect the change from our own outgoing payments +#: src/seedsigner/views/tools_views.py +msgid "Change Addresses" +msgstr "" + +#. a status message that our payment addresses are being calculated +#: src/seedsigner/views/tools_views.py +msgid "Calculating addrs..." +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Next {}" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Receive Addrs" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Change Addrs" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Scan" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Seeds" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Restart" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Power Off" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Reset / Power" +msgstr "" + +#: src/seedsigner/views/view.py msgid "Work In Progress" msgstr "作業中" -#: src/seedsigner/views/view.py:125 +#: src/seedsigner/views/view.py msgid "Not Yet Implemented" msgstr "未実施" -#: src/seedsigner/views/view.py:126 +#: src/seedsigner/views/view.py msgid "This is still on our to-do list!" msgstr "これはやる予定です!" -#: src/seedsigner/views/view.py:127 +#: src/seedsigner/views/view.py msgid "Back to Main Menu" msgstr "メニューへ戻る" -#: src/seedsigner/views/view.py:142 +#: src/seedsigner/views/view.py +msgid "Error" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Network Mismatch" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Change Setting" +msgstr "" + +#: src/seedsigner/views/view.py msgid "System Error" msgstr "システムエラー" + +#: src/seedsigner/views/view.py +msgid "Update Setting" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Option Disabled" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Security Tip" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "" +"For maximum security,\n" +"remove the MicroSD card\n" +"before continuing." +msgstr "" + +#~ msgid "{}..." +#~ msgstr "{}..." + +#~ msgid "Click to authorize this transaction" +#~ msgstr "この取り引きを許可する場合、クリックしてください。" + +#~ msgid "Signed PSBT" +#~ msgstr "署名済みPSBT" + +#~ msgid "Export as a QR code for:" +#~ msgstr "{}用QRコードとしてエクスポートする。" + +#~ msgid "changes fingerprint" +#~ msgstr "フィンガープリント値を変更する" + +#~ msgid "Enter 12/24 words" +#~ msgstr "12・24ワードを入力する" + +#~ msgid "Signing PSBT..." +#~ msgstr "PSBTを署名中..." + +#~ msgid "Taproot verification not yet implemented!" +#~ msgstr "タップルート検証が未実施!" + +#~ msgid "Legacy P2PKH verification not implemented!" +#~ msgstr "レガシーP2PKH検証が未実施!" + +#~ msgid "Add Passphrase" +#~ msgstr "パスフレーズを追加する" + +#~ msgid "Btc-only" +#~ msgstr "btcのみ" + +#~ msgid "Sats-only" +#~ msgstr "satのみ" + +#~ msgid "Threshold at 0.01" +#~ msgstr "閾値0.01で切り替え" + +#~ msgid "Btc | Sats hybrid" +#~ msgstr "btc | sat の切り替え表示" + +#~ msgid "Scan" +#~ msgstr "" + +#~ msgid "Seeds" +#~ msgstr "" + +#~ msgid "Restart" +#~ msgstr "" + +#~ msgid "Power Off" +#~ msgstr "" + +#~ msgid "Reset / Power" +#~ msgstr "再起動・シャットダウン" + +#~ msgid " (?)" +#~ msgstr "" + +#~ msgid "Scan a QR code" +#~ msgstr "QRコードを読み取る" + +#~ msgid "Yes " +#~ msgstr "" + +#~ msgid "No " +#~ msgstr "" + +#~ msgid "< back | Scan a QR code" +#~ msgstr "" + +#~ msgid "Settings QR" +#~ msgstr "デフォルト設定を保存するためのQR" + +#~ msgid "Settings imported successfully!" +#~ msgstr "デフォルト設定を無事インポートできました。" + +#~ msgid "MicroSD removed" +#~ msgstr "" + +#~ msgid "MicroSD inserted" +#~ msgstr "" + +#~ msgid "changes seed:" +#~ msgstr "" + +#~ msgid "< back | click joystick" +#~ msgstr "< 戻る | ジョイスティックをクリック" + +#~ msgid "Store Settings on SD card." +#~ msgstr "" + +#~ msgid "MicroSD card is removed" +#~ msgstr "" + +#~ msgid "Show passphrase warning" +#~ msgstr "" + +#~ msgid "" +#~ "BIP-39 passphrase changes your seed, resulting in new xpubs, wallets, etc.\n" +#~ "Be sure you know what you're doing!" +#~ msgstr "" + +#~ msgid "SeedQR is your private key!" +#~ msgstr "シードQRとは、秘密鍵のことです!" + +#~ msgid "Never photograph it or scan it into an online device." +#~ msgstr "写真を撮ったり、インタネットに接続可能な機器等で読み取ったりしないよう、ご注意ください!" + +#~ msgid " New seed" +#~ msgstr "新しいシード" + +#~ msgid "Scan wallet descriptor" +#~ msgstr "" + diff --git a/src/seedsigner/resources/babel/kr/LC_MESSAGES/messages.po b/src/seedsigner/resources/babel/kr/LC_MESSAGES/messages.po index 8564709a..729c5c37 100644 --- a/src/seedsigner/resources/babel/kr/LC_MESSAGES/messages.po +++ b/src/seedsigner/resources/babel/kr/LC_MESSAGES/messages.po @@ -1,384 +1,429 @@ -# Translations template for PROJECT. +# Korean translations for PROJECT. # Copyright (C) 2022 ORGANIZATION # This file is distributed under the same license as the PROJECT project. # FIRST AUTHOR , 2022. -# +# # Translators: # Okami ., 2022 -# +# jdlcdl, 2022 #, fuzzy msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-05-10 17:32-0500\n" +"POT-Creation-Date: 2023-10-09 11:36-0400\n" "PO-Revision-Date: 2022-04-29 13:50+0000\n" "Last-Translator: Okami ., 2022\n" -"Language-Team: Korean (https://www.transifex.com/seedsigner/teams/135832/ko/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.10.1\n" "Language: ko\n" +"Language-Team: Korean (https://www.transifex.com/seedsigner/teams/135832/ko/)\n" "Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.10.3\n" -#: src/seedsigner/gui/screens/main_menu_screens.py:12 -#: src/seedsigner/gui/screens/scan_screens.py:140 -msgid "Home" -msgstr "메뉴" +#. The abbreviated label for the special key on a standard keyboard. +#: src/seedsigner/gui/keyboard.py +msgid "del" +msgstr "" -#: src/seedsigner/gui/screens/main_menu_screens.py:24 +#. The abbreviated label for the special key on a standard keyboard. +#: src/seedsigner/gui/keyboard.py +msgid "space" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "" +"You can remove\n" +"the SD card now" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "SD card removed" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "SD card inserted" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review PSBT" +msgstr "PSBT 리뷰" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review Details" +msgstr "디테일을 리뷰" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "1 input" +msgstr "1 인풋" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input 1" +msgstr "인풋 1" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input 2" +msgstr "인풋 2" + +#. Indicates that items have been omitted from a series: e.g. "1, 2, 3, [...], +#. 8" +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "[ ... ]" +msgstr "[ ... ]" + +#. Input number will be inserted (e.g. "input 3") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input {}" +msgstr "인풋 {}" + +#. Ellipsis ("...") characters used to truncate an address (e.g. "bc1qabc...") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "..." +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/views/psbt_views.py +msgid "self-transfer" +msgstr "자신에게 송금" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient 1" +msgstr "수령인 1" + +#. Inserts the recipient number (e.g. the fifth one is: "recipient 5") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient {}" +msgstr "수령인 {}" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "fee" +msgstr "수수료" + +#. Label for a change output in the PSBT Overview flow diagram +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/views/psbt_views.py +msgid "change" +msgstr "거스름돈" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "PSBT Math" +msgstr "PSBT 계산" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review Recipients" +msgstr "수령인를 리뷰" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "btc" +msgstr "btc" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "sats" +msgstr "sats" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input" +msgid_plural "inputs" +msgstr[0] "인풋들" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient" +msgid_plural "recipients" +msgstr[0] "수령인들" + +#. Denonination is inserted (e.g. your "btc change" or "sats change") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "{} change" +msgstr "{} 거스름돈" + +#. First variable is either "Multisig" or the fingerprint for single sig; +#. Second is "Change" or "Addr" (change vs receive addr); Third is the address +#. index number (e.g. your #4 receive addr). +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/models/settings_definition.py +#: src/seedsigner/views/seed_views.py +msgid "Multisig" +msgstr "멀티 서명" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Change" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Addr" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Address verified!" +msgstr "에이드레스 검증했다! " + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Sign PSBT" +msgstr "PSBT 사인하세요" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Click to approve this transaction" +msgstr "" + +#: src/seedsigner/gui/screens/scan_screens.py +msgid "< back | " +msgstr "" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "Success!" +msgstr "성공!" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "OK" +msgstr "OK" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/psbt_views.py +msgid "Caution" +msgstr "조심" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "Privacy Leak!" +msgstr "프라이버시 유출!" + +#: src/seedsigner/gui/screens/screen.py +msgid "I Understand" +msgstr "알겠습니다" + +#: src/seedsigner/gui/screens/screen.py +msgid "Classified Info!" +msgstr "비밀 정보!" + +#: src/seedsigner/gui/screens/screen.py msgid "Restarting" msgstr "재기동중" -#: src/seedsigner/gui/screens/main_menu_screens.py:29 +#: src/seedsigner/gui/screens/screen.py msgid "" "SeedSigner is restarting.\n" "\n" "All in-memory data will be wiped." msgstr "SeedSigner가 재기동중있습니다. 인메모리 데이터가 모든 삭제됩니다." -#: src/seedsigner/gui/screens/main_menu_screens.py:39 +#: src/seedsigner/gui/screens/screen.py msgid "Powering Off" msgstr "셧다운 중" -#: src/seedsigner/gui/screens/main_menu_screens.py:44 +#: src/seedsigner/gui/screens/screen.py msgid "Please wait about 30 seconds before disconnecting power." msgstr "전원을 뽑기 전에 약30초 기다리세요." -#: src/seedsigner/gui/screens/psbt_screens.py:29 -#: src/seedsigner/views/seed_views.py:352 -msgid "Review PSBT" -msgstr "PSBT 리뷰" - -#: src/seedsigner/gui/screens/psbt_screens.py:31 -msgid "Review Details" -msgstr "디테일을 리뷰" - -#: src/seedsigner/gui/screens/psbt_screens.py:88 -msgid "1 input" -msgstr "1 인풋" - -#: src/seedsigner/gui/screens/psbt_screens.py:90 -msgid "input 1" -msgstr "인풋 1" - -#: src/seedsigner/gui/screens/psbt_screens.py:91 -msgid "input 2" -msgstr "인풋 2" - -#. Indicates that items have been omitted from a series: e.g. "1, 2, 3, [...], -#. 8" -#: src/seedsigner/gui/screens/psbt_screens.py:93 -#: src/seedsigner/gui/screens/psbt_screens.py:141 -msgid "[ ... ]" -msgstr "[ ... ]" - -#. Input number will be inserted (e.g. "input 3") -#: src/seedsigner/gui/screens/psbt_screens.py:95 -#: src/seedsigner/gui/screens/psbt_screens.py:96 -#: src/seedsigner/gui/screens/psbt_screens.py:99 -msgid "input {}" -msgstr "인풋 {}" - -#. Display a truncated address (e.g. "bc1qabc...") -#: src/seedsigner/gui/screens/psbt_screens.py:127 -msgid "{}..." -msgstr "{}..." - -#: src/seedsigner/gui/screens/psbt_screens.py:136 -msgid "self-transfer" -msgstr "자신에게 송금" - -#: src/seedsigner/gui/screens/psbt_screens.py:139 -msgid "recipient 1" -msgstr "수령인 1" - -#. Inserts the recipient number (e.g. the fifth one is: "recipient 5") -#: src/seedsigner/gui/screens/psbt_screens.py:143 -msgid "recipient {}" -msgstr "수령인 {}" - -#: src/seedsigner/gui/screens/psbt_screens.py:145 -#: src/seedsigner/gui/screens/psbt_screens.py:555 -msgid "fee" -msgstr "수수료" - -#. Label for a change output in the PSBT Overview flow diagram -#: src/seedsigner/gui/screens/psbt_screens.py:150 -msgid "change" -msgstr "거스름돈" - -#: src/seedsigner/gui/screens/psbt_screens.py:461 -msgid "PSBT Math" -msgstr "PSBT 계산" - -#: src/seedsigner/gui/screens/psbt_screens.py:462 -msgid "Review Recipients" -msgstr "수령인를 리뷰" - -#: src/seedsigner/gui/screens/psbt_screens.py:468 -#: src/seedsigner/gui/screens/psbt_screens.py:521 -msgid "btc" -msgstr "btc" - -#: src/seedsigner/gui/screens/psbt_screens.py:480 -msgid "sats" -msgstr "sats" - -#: src/seedsigner/gui/screens/psbt_screens.py:538 -msgid "input" -msgid_plural "inputs" -msgstr[0] "인풋들" - -#: src/seedsigner/gui/screens/psbt_screens.py:548 -msgid "recipient" -msgid_plural "recipients" -msgstr[0] "수령인들" - -#. Denonination is inserted (e.g. your "btc change" or "sats change") -#: src/seedsigner/gui/screens/psbt_screens.py:566 -msgid "{} change" -msgstr "{} 거스름돈" - -#: src/seedsigner/gui/screens/psbt_screens.py:671 -msgid "Address verified!" -msgstr "에이드레스 검증했다! " - -#: src/seedsigner/gui/screens/psbt_screens.py:683 -msgid "Sign PSBT" -msgstr "PSBT 사인하세요" - -#: src/seedsigner/gui/screens/psbt_screens.py:697 -msgid "Click to authorize this transaction" -msgstr "이 트랜잭션 승인하실 클릭해주세요." - -#: src/seedsigner/gui/screens/psbt_screens.py:707 -msgid "Signed PSBT" -msgstr "사인 PSBT" - -#. Prompt to specify which coordinator software to use (e.g. Specter, Sparrow, -#. etc) -#: src/seedsigner/gui/screens/psbt_screens.py:713 -msgid "Export as a QR code for:" -msgstr "QR코드처럼 내보내기 for:" - -#: src/seedsigner/gui/screens/scan_screens.py:22 -msgid "Scan a QR code" -msgstr "QR 코드를 스캔" - -#: src/seedsigner/gui/screens/scan_screens.py:138 -msgid "Settings QR" -msgstr "" - -#: src/seedsigner/gui/screens/scan_screens.py:156 -msgid "Settings imported successfully!" -msgstr "설정 임포트 성공!" - -#: src/seedsigner/gui/screens/screen.py:731 -#: src/seedsigner/views/seed_views.py:954 -#: src/seedsigner/views/seed_views.py:1201 -msgid "Success!" -msgstr "성공!" - -#: src/seedsigner/gui/screens/screen.py:735 -#: src/seedsigner/views/seed_views.py:956 -#: src/seedsigner/views/seed_views.py:1204 -#: src/seedsigner/views/seed_views.py:1647 src/seedsigner/views/view.py:145 -msgid "OK" -msgstr "OK" - -#: src/seedsigner/gui/screens/screen.py:852 -#: src/seedsigner/views/psbt_views.py:430 -msgid "Caution" -msgstr "조심" - -#: src/seedsigner/gui/screens/screen.py:855 -msgid "I Understand" -msgstr "알겠습니다" - -#: src/seedsigner/gui/screens/screen.py:867 -msgid "Classified Info!" -msgstr "비밀 정보!" - -#: src/seedsigner/gui/screens/seed_screens.py:411 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Finalize Seed" msgstr " 시드 확정할까요?" -#: src/seedsigner/gui/screens/seed_screens.py:424 -#: src/seedsigner/gui/screens/tools_screens.py:291 +#. a label for the shortened Key-id of a BIP-32 master HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "fingerprint" msgstr "지문" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Backup Seed" +msgstr "백업 시드" + +#. Additional explainer for the two seed backup options (mnemonic phrase and +#. SeedQR). +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Backups do not include your passphrase." +msgstr "" + #. Displays the page number and total: (e.g. page 1 of 6) -#: src/seedsigner/gui/screens/seed_screens.py:461 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Seed Words: {}/{}" msgstr "시드 워드를: {}/{} " -#: src/seedsigner/gui/screens/seed_screens.py:541 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "BIP-85 Index" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py msgid "Verify Backup?" msgstr "백업을 검증시겠습니까?" -#: src/seedsigner/gui/screens/seed_screens.py:547 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Optionally verify that your mnemonic backup is correct." msgstr "시드 오류가 없는지 확인 (선택적). " -#: src/seedsigner/gui/screens/seed_screens.py:556 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Derivation Path" msgstr "유도 경로" -#: src/seedsigner/gui/screens/seed_screens.py:703 -msgid "Xpub Details" -msgstr "Xpub 디테일" - -#: src/seedsigner/gui/screens/seed_screens.py:709 -#: src/seedsigner/gui/screens/seed_screens.py:713 -#: src/seedsigner/views/seed_views.py:354 -#: src/seedsigner/views/seed_views.py:471 -#: src/seedsigner/views/seed_views.py:504 -#: src/seedsigner/views/seed_views.py:578 +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py msgid "Export Xpub" msgstr "Xpub 내보내기" -#: src/seedsigner/gui/screens/seed_screens.py:722 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Xpub Details" +msgstr "Xpub 디테일" + +#. Short for "BIP32 Master Fingerprint" +#. a label for the shortened Key-id of a BIP-32 master HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Fingerprint" msgstr "지문" #. Short for "Derivation Path" -#: src/seedsigner/gui/screens/seed_screens.py:732 +#. a label for the derivation-path into a BIP-32 HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Derivation" msgstr "유도 경로" -#: src/seedsigner/gui/screens/seed_screens.py:741 +#. Short for "BIP32 Extended Public Key" +#: src/seedsigner/gui/screens/seed_screens.py msgid "Xpub" msgstr "Xpub" -#: src/seedsigner/gui/screens/seed_screens.py:754 -#: src/seedsigner/views/seed_views.py:217 -msgid "Add Passphrase" -msgstr "패스 프레이즈 추가하다" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "BIP-39 Passphrase" +msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:1106 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Verify Passphrase" msgstr "패스 프레이즈 검증하다" #. Describes the effect of applying a BIP-39 passphrase; it changes the seed's #. fingerprint -#: src/seedsigner/gui/screens/seed_screens.py:1115 +#: src/seedsigner/gui/screens/seed_screens.py msgid "changes fingerprint" msgstr "" #. Refers to the SeedQR type: Standard or Compact -#: src/seedsigner/gui/screens/seed_screens.py:1170 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Standard" msgstr "표준" #. Briefly explains the Standard SeedQR data format -#: src/seedsigner/gui/screens/seed_screens.py:1172 +#: src/seedsigner/gui/screens/seed_screens.py msgid "BIP-39 wordlist indices" msgstr "BIP-39 워드 목록 색인" #. Refers to the SeedQR type: Standard or Compact -#: src/seedsigner/gui/screens/seed_screens.py:1180 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Compact" msgstr "콤팩트" #. Briefly explains the Compact SeedQR data format -#: src/seedsigner/gui/screens/seed_screens.py:1182 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Raw entropy bits" msgstr "엔트로피의 생데이터" -#: src/seedsigner/gui/screens/seed_screens.py:1196 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Transcribe SeedQR" msgstr " 시드 QR 베끼다 " #. Refers to the QR code size: 21x21, 25x25, or 29x29 -#: src/seedsigner/gui/screens/seed_screens.py:1198 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Begin {}x{}" msgstr "{}x{} 개시" -#: src/seedsigner/gui/screens/seed_screens.py:1275 +#: src/seedsigner/gui/screens/seed_screens.py msgid "click to exit" msgstr "종료하기 위해 클릭하세요." -#: src/seedsigner/gui/screens/seed_screens.py:1411 -msgid "" -"Optionally scan your transcribed SeedQR to confirm that it reads back " -"correctly." +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Optionally scan your transcribed SeedQR to confirm that it reads back correctly." msgstr "시드QR을 스캔하여 올바르게 읽혀지는지 확인합니다 (선택적)." -#: src/seedsigner/gui/screens/seed_screens.py:1468 -#: src/seedsigner/views/seed_views.py:1284 -#: src/seedsigner/views/seed_views.py:1342 +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py msgid "Verify Address" msgstr "에이드레스 검증하다" #. Inserts the nth address number (e.g. "Checking address 7") -#: src/seedsigner/gui/screens/seed_screens.py:1527 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Checking address {}" msgstr "{}번째 에이드레스 확인 중" -#: src/seedsigner/gui/screens/seed_screens.py:1544 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Multisig Verification" msgstr "멀티 서명 확인" -#: src/seedsigner/gui/screens/seed_screens.py:1549 -msgid "" -"Load your multisig wallet descriptor to verify your receive/self-transfer or" -" change address." +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Load your multisig wallet descriptor to verify your receive/self-transfer or change address." msgstr "수령인 / 자신송금나 거스름돈 에이드레스 검증하기 위해, 멀티 시그 지갑 descriptor을 로드해요. " -#: src/seedsigner/gui/screens/seed_screens.py:1562 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Descriptor Loaded" msgstr "Descriptor 완료" #. Label for the multisig wallet's signing policy (e.g. 2-of-3) -#: src/seedsigner/gui/screens/seed_screens.py:1568 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Policy" msgstr "법규" -#: src/seedsigner/gui/screens/seed_screens.py:1576 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Signing Keys" msgstr "서명 시드들 " -#: src/seedsigner/gui/screens/settings_screens.py:26 -#: src/seedsigner/views/main_menu_views.py:21 -#: src/seedsigner/views/settings_views.py:36 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Review Message" +msgstr "" + +#. Short for "Next step" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +#: src/seedsigner/views/tools_views.py +msgid "Next" +msgstr "다음" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Confirm Address" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Sign Message" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "derivation path" +msgstr "" + +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py msgid "Settings" msgstr "설정" #. Short for "Input/Output"; screen to make sure the buttons and camera are #. working properly -#: src/seedsigner/gui/screens/settings_screens.py:58 +#: src/seedsigner/gui/screens/settings_screens.py msgid "I/O Test" msgstr "I/O Test" #. Blank the screen -#: src/seedsigner/gui/screens/settings_screens.py:129 -#: src/seedsigner/gui/screens/settings_screens.py:140 -#: src/seedsigner/gui/screens/settings_screens.py:221 +#: src/seedsigner/gui/screens/settings_screens.py msgid "Clear" msgstr "지우다" -#: src/seedsigner/gui/screens/settings_screens.py:161 +#: src/seedsigner/gui/screens/settings_screens.py msgid "Exit" msgstr "종료하다" -#: src/seedsigner/gui/screens/settings_screens.py:175 -#: src/seedsigner/gui/screens/tools_screens.py:71 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Capturing image..." msgstr "이미지 캡처 중..." -#: src/seedsigner/gui/screens/settings_screens.py:288 -#: src/seedsigner/views/settings_views.py:21 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py msgid "Donate" msgstr "기부하다" -#. If your language uses the percent sign ("%"), your translation must also -#. use +#. If your language uses the percent sign ("%"), your translation must also use #. two percent signs ("%%") due to python formatting oddities. "100%%" will be #. rendered as "100%". -#: src/seedsigner/gui/screens/settings_screens.py:293 +#: src/seedsigner/gui/screens/settings_screens.py #, python-format msgid "" "SeedSigner is 100%% free & open source, funded solely by the Bitcoin community.\n" @@ -389,702 +434,992 @@ msgstr "" "\n" "블록체인 또는 라이트닝으로 기부를 하려면 여기를 사용하십시오:" -#. Instructions when taking a picture; "back" should point left. -#: src/seedsigner/gui/screens/tools_screens.py:40 -msgid "< back | click joystick" -msgstr "< 돌아가 | 조이스틱을 클릭 " +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py +msgid "Settings QR" +msgstr "" + +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py +msgid "Home" +msgstr "메뉴" #. Instructions when reviewing an image; "reshoot" must point left, "accept" #. must point right -#: src/seedsigner/gui/screens/tools_screens.py:98 +#: src/seedsigner/gui/screens/tools_screens.py msgid " < reshoot | accept > " msgstr " < 다시 찍다 | 확인 > " #. current roll number vs total rolls (e.g. roll 7 of 50) -#: src/seedsigner/gui/screens/tools_screens.py:120 -#: src/seedsigner/gui/screens/tools_screens.py:245 +#: src/seedsigner/gui/screens/tools_screens.py msgid "Dice Roll {}/{}" msgstr "{}/{} 번째 주사위 굴리기" -#: src/seedsigner/gui/screens/tools_screens.py:274 +#. Build the last word in a 12 or 24 word BIP-39 mnemonic seed phrase. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Build Final Word" +msgstr "" + +#. Number of BIP-39 seed words, and the entropy -- in bits, contained within. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "The {}th word is built from {} more entropy bits plus auto-calculated checksum." +msgstr "" + +#. current coin-flip number vs total flips (e.g. flip 3 of 4) +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Coin Flip {}/{}" +msgstr "" + +#. How we call the "front" side result during a coin toss. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Heads = 1" +msgstr "" + +#. How we call the "back" side result during a coin toss. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Tails = 0" +msgstr "" + +#. A labeled presentation of a user-supplied value +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Your input: \"{}\"" +msgstr "" + +#. A function of "x" to be used for detecting errors in "x" +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Checksum" +msgstr "" + +#. labeled presentation of the last word in a BIP-39 mnemonic seed phrase. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Final Word: \"{}\"" +msgstr "" + +#. a label for the last word of a 12-word BIP-39 mnemonic seed phrase +#: src/seedsigner/gui/screens/tools_screens.py msgid "12th Word" msgstr "12 번째 워드" -#: src/seedsigner/gui/screens/tools_screens.py:276 +#. a label for the last word of a 24-word BIP-39 mnemonic seed phrase +#: src/seedsigner/gui/screens/tools_screens.py msgid "24th Word" msgstr "24 번째 워드" -#: src/seedsigner/models/settings_definition.py:14 +#. a label for the tool to explore public addresses for this seed. +#: src/seedsigner/gui/screens/tools_screens.py +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py +msgid "Address Explorer" +msgstr "" + +#. a label for a BIP-380-ish Output Descriptor +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Wallet descriptor" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Enabled" msgstr "틀었다" -#: src/seedsigner/models/settings_definition.py:15 -#: src/seedsigner/models/settings_definition.py:20 +#: src/seedsigner/models/settings_definition.py msgid "Disabled" msgstr "껐다" -#: src/seedsigner/models/settings_definition.py:18 -#: src/seedsigner/models/settings_definition.py:26 +#: src/seedsigner/models/settings_definition.py msgid "Prompt" msgstr "" -#: src/seedsigner/models/settings_definition.py:19 -#: src/seedsigner/models/settings_definition.py:23 -#: src/seedsigner/models/settings_definition.py:29 +#: src/seedsigner/models/settings_definition.py msgid "Required" msgstr "필수" -#: src/seedsigner/models/settings_definition.py:72 -msgid "Btc-only" -msgstr "Btc-오로지  " +#. Translator is expected ONLY to change their own locale below +#: src/seedsigner/models/settings_definition.py +msgid "Arabic" +msgstr "عربي" -#: src/seedsigner/models/settings_definition.py:73 -msgid "Sats-only" -msgstr "Sats-오로지  " +#: src/seedsigner/models/settings_definition.py +msgid "Czech" +msgstr "čeština" -#: src/seedsigner/models/settings_definition.py:74 -msgid "Threshold at 0.01" -msgstr "역치 0.01로" +#: src/seedsigner/models/settings_definition.py +msgid "French" +msgstr "Français" -#: src/seedsigner/models/settings_definition.py:75 -msgid "Btc | Sats hybrid" -msgstr "Btc | Sats 하이브리드" +#: src/seedsigner/models/settings_definition.py +msgid "German" +msgstr "Deutsch" -#: src/seedsigner/models/settings_definition.py:83 +#: src/seedsigner/models/settings_definition.py +msgid "Hebrew" +msgstr "עִברִית" + +#: src/seedsigner/models/settings_definition.py +msgid "Japanese" +msgstr "日本" + +#: src/seedsigner/models/settings_definition.py +msgid "Korean" +msgstr "한국인" + +#: src/seedsigner/models/settings_definition.py +msgid "Portuguese" +msgstr "Português" + +#: src/seedsigner/models/settings_definition.py +msgid "Russian" +msgstr "Русский" + +#: src/seedsigner/models/settings_definition.py +msgid "Spanish" +msgstr "Español" + +#: src/seedsigner/models/settings_definition.py msgid "0°" msgstr "0°" -#: src/seedsigner/models/settings_definition.py:84 +#: src/seedsigner/models/settings_definition.py msgid "90°" msgstr "90°" -#: src/seedsigner/models/settings_definition.py:85 +#: src/seedsigner/models/settings_definition.py msgid "180°" msgstr "180°" -#: src/seedsigner/models/settings_definition.py:86 +#: src/seedsigner/models/settings_definition.py msgid "270°" msgstr "270°" -#: src/seedsigner/models/settings_definition.py:94 +#: src/seedsigner/models/settings_definition.py msgid "Low" msgstr "저" -#: src/seedsigner/models/settings_definition.py:95 +#: src/seedsigner/models/settings_definition.py msgid "Medium" msgstr "중" -#: src/seedsigner/models/settings_definition.py:96 +#: src/seedsigner/models/settings_definition.py msgid "High" msgstr "고" -#: src/seedsigner/models/settings_definition.py:104 +#: src/seedsigner/models/settings_definition.py msgid "Mainnet" msgstr "메인넷" -#: src/seedsigner/models/settings_definition.py:105 +#: src/seedsigner/models/settings_definition.py msgid "Testnet" msgstr "테스트넷" -#: src/seedsigner/models/settings_definition.py:106 +#: src/seedsigner/models/settings_definition.py msgid "Regtest" msgstr "리그레션 테스트" -#: src/seedsigner/models/settings_definition.py:122 -#: src/seedsigner/views/seed_views.py:466 +#: src/seedsigner/models/settings_definition.py +msgid "Store Settings on SD card" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Insert SD card to enable" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +#: src/seedsigner/views/seed_views.py msgid "Single Sig" msgstr "심쁠 서명" -#: src/seedsigner/models/settings_definition.py:123 -#: src/seedsigner/views/seed_views.py:467 -msgid "Multisig" -msgstr "멀티 서명" +#: src/seedsigner/models/settings_definition.py +msgid "Native Segwit" +msgstr "" -#: src/seedsigner/models/settings_definition.py:354 +#: src/seedsigner/models/settings_definition.py +msgid "Nested Segwit (legacy)" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Taproot" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Custom Derivation" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Language" msgstr "언어" -#: src/seedsigner/models/settings_definition.py:362 +#: src/seedsigner/models/settings_definition.py msgid "Mnemonic language" msgstr "시드 워드 언어" -#: src/seedsigner/models/settings_definition.py:370 +#: src/seedsigner/models/settings_definition.py msgid "Persistent settings" msgstr "Pesistent 설정" -#: src/seedsigner/models/settings_definition.py:376 +#: src/seedsigner/models/settings_definition.py msgid "Coordinator software" msgstr "코디네이터 소프트웨어" -#: src/seedsigner/models/settings_definition.py:383 +#: src/seedsigner/models/settings_definition.py msgid "Denomination display" msgstr "데노미네션 표시" -#: src/seedsigner/models/settings_definition.py:392 +#: src/seedsigner/models/settings_definition.py msgid "Bitcoin network" msgstr "비트코인 네트워크" -#: src/seedsigner/models/settings_definition.py:400 +#: src/seedsigner/models/settings_definition.py msgid "QR code density" msgstr "QR코드의 해상도" -#: src/seedsigner/models/settings_definition.py:408 +#: src/seedsigner/models/settings_definition.py msgid "Xpub export" msgstr "Xpub 내보내기" -#: src/seedsigner/models/settings_definition.py:414 +#: src/seedsigner/models/settings_definition.py msgid "Sig types" msgstr "서명 종류들" -#: src/seedsigner/models/settings_definition.py:422 +#: src/seedsigner/models/settings_definition.py msgid "Script types" msgstr "스크립트 종류들" -#: src/seedsigner/models/settings_definition.py:430 +#: src/seedsigner/models/settings_definition.py msgid "Show xpub details" msgstr "Xpub 디테일을 보이다" -#: src/seedsigner/models/settings_definition.py:436 +#: src/seedsigner/models/settings_definition.py msgid "BIP-39 passphrase" msgstr "BIP-39 패스 프레이즈" -#: src/seedsigner/models/settings_definition.py:444 +#: src/seedsigner/models/settings_definition.py msgid "Camera rotation" msgstr "카메라 회전" -#: src/seedsigner/models/settings_definition.py:452 +#: src/seedsigner/models/settings_definition.py msgid "Compact SeedQR" msgstr "콤팩트한 시드 QR" -#: src/seedsigner/models/settings_definition.py:458 +#: src/seedsigner/models/settings_definition.py +msgid "BIP-85 child seeds" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Message signing" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Show privacy warnings" msgstr "프라이버시 경고를 표시하다" -#: src/seedsigner/models/settings_definition.py:464 +#: src/seedsigner/models/settings_definition.py msgid "Show dire warnings" msgstr "심각하는 경고를 표시하다" -#: src/seedsigner/models/settings_definition.py:470 +#: src/seedsigner/models/settings_definition.py +msgid "Show QR brightness tips" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Show partner logos" msgstr "파트너 로고 표시하다" -#: src/seedsigner/models/settings_definition.py:485 +#: src/seedsigner/models/settings_definition.py msgid "QR background color" msgstr "QR 배경색" -#: src/seedsigner/views/main_menu_views.py:18 -msgid "Scan" -msgstr "스캔" - -#: src/seedsigner/views/main_menu_views.py:19 -msgid "Seeds" -msgstr "시드들" - -#: src/seedsigner/views/main_menu_views.py:20 -#: src/seedsigner/views/tools_views.py:29 -msgid "Tools" -msgstr "도구들" - -#: src/seedsigner/views/main_menu_views.py:38 -msgid "Restart" -msgstr "재기동" - -#: src/seedsigner/views/main_menu_views.py:39 -msgid "Power Off" -msgstr "Power OFF" - -#: src/seedsigner/views/main_menu_views.py:42 -msgid "Reset / Power" -msgstr "다시 시작 / Power ON" - -#: src/seedsigner/views/psbt_views.py:36 -#: src/seedsigner/views/seed_views.py:1322 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py msgid "Scan a seed" msgstr "시드 스캔" -#: src/seedsigner/views/psbt_views.py:37 -#: src/seedsigner/views/seed_views.py:1323 -msgid "Enter 12/24 words" -msgstr "12/24 워드를 입력하다" +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +msgid "Enter 12-word seed" +msgstr "12 시드 워드를 입력하다" -#: src/seedsigner/views/psbt_views.py:53 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +msgid "Enter 24-word seed" +msgstr "24 시드 워드를 입력하다" + +#: src/seedsigner/views/psbt_views.py msgid "Select Signer" msgstr "서명자를 선택하다" -#: src/seedsigner/views/psbt_views.py:85 +#: src/seedsigner/views/psbt_views.py msgid "Parsing PSBT..." msgstr "PSBT 분석 중..." -#: src/seedsigner/views/psbt_views.py:155 +#: src/seedsigner/views/psbt_views.py msgid "Unsupported Script Type!" msgstr "스크립트 종류 지원되지 않습니다!" -#: src/seedsigner/views/psbt_views.py:156 -msgid "" -"PSBT has unsupported input script type, please verify your change addresses." +#: src/seedsigner/views/psbt_views.py +msgid "PSBT has unsupported input script type, please verify your change addresses." msgstr "PSBT는 인풋 스크립트 종류 지원되지 않았습니다. 거스름돈 에이드레스 검증해주세요." -#: src/seedsigner/views/psbt_views.py:157 -#: src/seedsigner/views/psbt_views.py:176 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/view.py msgid "Continue" msgstr "계속" #. User will receive no change back; the inputs to this transaction are fully #. spent -#: src/seedsigner/views/psbt_views.py:174 +#: src/seedsigner/views/psbt_views.py msgid "Full Spend!" msgstr "" -#: src/seedsigner/views/psbt_views.py:175 -msgid "" -"This PSBT spends its entire input value. No change is coming back to your " -"wallet." +#: src/seedsigner/views/psbt_views.py +msgid "This PSBT spends its entire input value. No change is coming back to your wallet." msgstr "이 PSBT는 인풋 값 전액 사용합니다. 당신의 지갑에는 거스름돈을 돌아오지 않습니다." #. Future-tense used to indicate that this transaction will send this amount, -#. as opposed to "Send" on its own which could be misread as an instant -#. command +#. as opposed to "Send" on its own which could be misread as an instant command #. (e.g. "Send Now"). -#: src/seedsigner/views/psbt_views.py:242 +#: src/seedsigner/views/psbt_views.py msgid "Will Send" msgstr "송부할거예요" -#: src/seedsigner/views/psbt_views.py:248 +#: src/seedsigner/views/psbt_views.py msgid "Next Recipient" msgstr "다음 수령인" -#. Short for "Next step" -#: src/seedsigner/views/psbt_views.py:251 -#: src/seedsigner/views/psbt_views.py:320 -#: src/seedsigner/views/seed_views.py:796 -msgid "Next" -msgstr "다음" - #. The amount you're receiving back from the transaction -#: src/seedsigner/views/psbt_views.py:324 +#: src/seedsigner/views/psbt_views.py msgid "Your Change" msgstr "당신의 거스름돈" -#: src/seedsigner/views/psbt_views.py:325 +#: src/seedsigner/views/psbt_views.py msgid "Verify Multisig Change" msgstr "멀티 서명 거스름돈 검증하다" -#: src/seedsigner/views/psbt_views.py:327 +#: src/seedsigner/views/psbt_views.py msgid "Self-Transfer" msgstr "자신에게 송금" -#: src/seedsigner/views/psbt_views.py:328 +#: src/seedsigner/views/psbt_views.py msgid "Verify Multisig Addr" msgstr "멀티 서명 에이드레스 검증하다" -#: src/seedsigner/views/psbt_views.py:347 +#: src/seedsigner/views/psbt_views.py msgid "Verifying Change..." msgstr "거스름돈 검증 중..." -#: src/seedsigner/views/psbt_views.py:349 +#: src/seedsigner/views/psbt_views.py msgid "Verifying Self-Transfer..." msgstr "자신에게 송금 검증 중..." -#: src/seedsigner/views/psbt_views.py:431 -msgid "" -"PSBT's {} address could not be verified with your multisig wallet " -"descriptor." +#. Variable is either "change" or "self-transfer". +#: src/seedsigner/views/psbt_views.py +msgid "PSBT's {} address could not be verified with your multisig wallet descriptor." msgstr "멀티 시그 지갑 descriptor는 {} PSBT 에이드레스 검증해지 못했습니다." -#: src/seedsigner/views/psbt_views.py:433 +#: src/seedsigner/views/psbt_views.py msgid "Suspicious PSBT" msgstr "수상한 PSBT" -#: src/seedsigner/views/psbt_views.py:434 +#. Variable is either "change" or "self-transfer". +#: src/seedsigner/views/psbt_views.py msgid "PSBT's {} address could not be generated from your seed." msgstr "시드에서 PSBT의 {} 에이드레스 생성해지 못했습니다." -#: src/seedsigner/views/psbt_views.py:438 +#: src/seedsigner/views/psbt_views.py msgid "Address Verification Failed" msgstr "에이드레스 검증 실패" -#: src/seedsigner/views/psbt_views.py:440 +#: src/seedsigner/views/psbt_views.py msgid "Discard PSBT" msgstr "PSBT를 버리다" -#: src/seedsigner/views/psbt_views.py:465 +#: src/seedsigner/views/psbt_views.py msgid "Approve PSBT" msgstr "PSBT를 승인하다" -#: src/seedsigner/views/psbt_views.py:470 -msgid "Signing PSBT..." -msgstr "PSBT 서명 중..." - -#: src/seedsigner/views/psbt_views.py:548 -msgid "PSBT Error" -msgstr "PSBT Error" - -#: src/seedsigner/views/psbt_views.py:550 -msgid "Signing Failed" -msgstr "서명 실패" - -#: src/seedsigner/views/psbt_views.py:551 -msgid "Signing with this seed did not add a valid signature." -msgstr "이 시드 서명할 때 타당한 서명을 생성해지 못했습니다." - -#: src/seedsigner/views/psbt_views.py:552 +#: src/seedsigner/views/psbt_views.py msgid "Select Diff Seed" msgstr "다른 시드 선택" +#: src/seedsigner/views/psbt_views.py +msgid "PSBT Error" +msgstr "PSBT Error" + +#: src/seedsigner/views/psbt_views.py +msgid "Signing Failed" +msgstr "서명 실패" + +#: src/seedsigner/views/psbt_views.py +msgid "Signing with this seed did not add a valid signature." +msgstr "이 시드 서명할 때 타당한 서명을 생성해지 못했습니다." + #. This is on the opening splash screen, displayed above the HRF logo -#: src/seedsigner/views/screensaver.py:74 +#: src/seedsigner/views/screensaver.py msgid "With support from:" msgstr "다음 덕분에 지원:" -#: src/seedsigner/views/seed_views.py:53 +#: src/seedsigner/views/seed_views.py msgid "Load a seed" msgstr "시드를 로드하다" -#: src/seedsigner/views/seed_views.py:56 +#: src/seedsigner/views/seed_views.py msgid "In-Memory Seeds" msgstr "인메모리 시드들" -#: src/seedsigner/views/seed_views.py:77 +#: src/seedsigner/views/seed_views.py +msgid "Load the seed to verify" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Select seed to verify" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Load the seed to sign with" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Select seed to sign with" +msgstr "" + +#: src/seedsigner/views/seed_views.py msgid "Scan a SeedQR" msgstr "시드QR 스캔" -#: src/seedsigner/views/seed_views.py:78 -msgid "Enter 24-word seed" -msgstr "24 시드 워드를 입력하다" - -#: src/seedsigner/views/seed_views.py:79 -msgid "Enter 12-word seed" -msgstr "12 시드 워드를 입력하다" - -#: src/seedsigner/views/seed_views.py:80 +#: src/seedsigner/views/seed_views.py msgid "Create a seed" msgstr "시드를 만들다" -#: src/seedsigner/views/seed_views.py:89 +#: src/seedsigner/views/seed_views.py msgid "Load A Seed" msgstr "시드를 로드하다" #. Inserts the word number (e.g. "Seed Word #6") -#: src/seedsigner/views/seed_views.py:126 +#: src/seedsigner/views/seed_views.py msgid "Seed Word #{}" msgstr "시드 워드 #{}" -#: src/seedsigner/views/seed_views.py:187 +#: src/seedsigner/views/seed_views.py msgid "Review & Edit" msgstr "리뷰하고 첨삭" -#: src/seedsigner/views/seed_views.py:188 -#: src/seedsigner/views/seed_views.py:310 -#: src/seedsigner/views/tools_views.py:259 +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py msgid "Discard" msgstr "버리다" -#: src/seedsigner/views/seed_views.py:192 +#: src/seedsigner/views/seed_views.py msgid "Invalid Mnemonic!" msgstr "시드 워드 잘못된" -#: src/seedsigner/views/seed_views.py:194 +#: src/seedsigner/views/seed_views.py msgid "Checksum failure; not a valid seed phrase." msgstr "Checksum 실패. Seed phrase 타당하지 않습니다." -#: src/seedsigner/views/seed_views.py:216 -#: src/seedsigner/views/seed_views.py:268 -#: src/seedsigner/views/seed_views.py:797 -#: src/seedsigner/views/seed_views.py:1146 +#: src/seedsigner/views/seed_views.py src/seedsigner/views/view.py msgid "Done" msgstr "종료" -#: src/seedsigner/views/seed_views.py:267 +#: src/seedsigner/views/seed_views.py msgid "Edit passphrase" msgstr "패스 프레이즈 편집하다" -#: src/seedsigner/views/seed_views.py:309 +#: src/seedsigner/views/seed_views.py msgid "Keep Seed" msgstr "시드 보관하다" -#: src/seedsigner/views/seed_views.py:315 +#: src/seedsigner/views/seed_views.py msgid "Discard Seed?" msgstr "시드 버리고 싶어요?" -#: src/seedsigner/views/seed_views.py:317 +#: src/seedsigner/views/seed_views.py msgid "Wipe seed {} from the device?" msgstr "시드를{} 디바이스에서 삭제한다?" -#: src/seedsigner/views/seed_views.py:351 +#: src/seedsigner/views/seed_views.py msgid "Scan PSBT" msgstr "PSBT 스캔" -#: src/seedsigner/views/seed_views.py:353 -#: src/seedsigner/views/seed_views.py:1646 +#: src/seedsigner/views/seed_views.py msgid "Verify Addr" msgstr "에이드레스 검증하다" -#: src/seedsigner/views/seed_views.py:355 -#: src/seedsigner/views/seed_views.py:436 -msgid "Backup Seed" -msgstr "백업 시드" +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Child Seed" +msgstr "" -#: src/seedsigner/views/seed_views.py:356 +#: src/seedsigner/views/seed_views.py msgid "Discard Seed" msgstr "시드를 버리다" -#: src/seedsigner/views/seed_views.py:431 +#: src/seedsigner/views/seed_views.py msgid "View Seed Words" msgstr "시드 워드를 보기" -#: src/seedsigner/views/seed_views.py:432 +#: src/seedsigner/views/seed_views.py msgid "Export as SeedQR" msgstr "시드QR 처럼 내보내기" -#: src/seedsigner/views/seed_views.py:620 -msgid "Privacy Leak!" -msgstr "프라이버시 유출!" - -#: src/seedsigner/views/seed_views.py:621 +#: src/seedsigner/views/seed_views.py msgid "Xpub can be used to view all future transactions." msgstr "미래 거래들을 보기 위해, Xpub 쓸 수 있습니다." -#: src/seedsigner/views/seed_views.py:651 +#: src/seedsigner/views/seed_views.py msgid "Generating xpub..." msgstr "Xpub 생성중..." -#: src/seedsigner/views/seed_views.py:771 +#: src/seedsigner/views/seed_views.py msgid "You must keep your seed words private & away from all online devices." msgstr "시드 워드는 Online device에서 멀고 계속 비밀하는 멀리해야 해요." -#: src/seedsigner/views/seed_views.py:836 -msgid "Verify" -msgstr "검증하다" +#: src/seedsigner/views/seed_views.py +msgid "Child #{}" +msgstr "" -#: src/seedsigner/views/seed_views.py:837 -msgid "Skip" -msgstr "스킵하다" +#: src/seedsigner/views/seed_views.py +msgid "Seed Words" +msgstr "" -#: src/seedsigner/views/seed_views.py:886 -msgid "Verify Word #{}" -msgstr "워드를 검증하다 #{}" +#: src/seedsigner/views/seed_views.py +msgid "12 Words" +msgstr "" -#: src/seedsigner/views/seed_views.py:926 -msgid "Review Seed Words" -msgstr "시드 워드를 다시보기" +#: src/seedsigner/views/seed_views.py +msgid "24 Words" +msgstr "" -#: src/seedsigner/views/seed_views.py:927 +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Num Words" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Index Error" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Invalid Child Index" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Child Index must be between 0 and 2^31-1." +msgstr "" + +#: src/seedsigner/views/seed_views.py msgid "Try Again" msgstr "다시 한 번 해 봐요" -#: src/seedsigner/views/seed_views.py:931 +#: src/seedsigner/views/seed_views.py +msgid "Verify" +msgstr "검증하다" + +#: src/seedsigner/views/seed_views.py +msgid "Skip" +msgstr "스킵하다" + +#: src/seedsigner/views/seed_views.py +msgid "Verify Word #{}" +msgstr "워드를 검증하다 #{}" + +#: src/seedsigner/views/seed_views.py +msgid "Review Seed Words" +msgstr "시드 워드를 다시보기" + +#: src/seedsigner/views/seed_views.py msgid "Verification Error" msgstr "검증 Error" -#: src/seedsigner/views/seed_views.py:933 +#: src/seedsigner/views/seed_views.py msgid "Wrong Word!" msgstr "틀리는 워드!" -#: src/seedsigner/views/seed_views.py:934 +#: src/seedsigner/views/seed_views.py msgid "Word #{} is not \"{}\"!" msgstr "#{} 워드는 \"{}\" 아닙니다!" -#: src/seedsigner/views/seed_views.py:952 +#: src/seedsigner/views/seed_views.py msgid "Backup Verified" msgstr "백업 검증했다" -#: src/seedsigner/views/seed_views.py:955 +#: src/seedsigner/views/seed_views.py msgid "All mnemonic backup words were successfully verified!" msgstr "시드 워드의 백업 목록을 검증 성공했다!" -#: src/seedsigner/views/seed_views.py:978 +#: src/seedsigner/views/seed_views.py msgid "Standard: 25x25" msgstr "표준: 25x25" -#: src/seedsigner/views/seed_views.py:979 +#: src/seedsigner/views/seed_views.py msgid "Compact: 21x21" msgstr "콤팩트: 21x21" -#: src/seedsigner/views/seed_views.py:983 +#: src/seedsigner/views/seed_views.py msgid "Standard: 29x29" msgstr "표준: 29x29" -#: src/seedsigner/views/seed_views.py:984 +#: src/seedsigner/views/seed_views.py msgid "Compact: 25x25" msgstr "콤팩트: 25x25" -#: src/seedsigner/views/seed_views.py:1003 +#: src/seedsigner/views/seed_views.py msgid "SeedQR Format" msgstr "시드QR 형식" -#: src/seedsigner/views/seed_views.py:1052 -msgid "SeedQR is your private key!" +#: src/seedsigner/views/seed_views.py +msgid "Never photograph or scan it into a device that connects to the internet." msgstr "" -#: src/seedsigner/views/seed_views.py:1053 -msgid "Never photograph it or scan it into an online device." -msgstr "" - -#: src/seedsigner/views/seed_views.py:1145 -#: src/seedsigner/views/seed_views.py:1189 -#: src/seedsigner/views/seed_views.py:1200 -#: src/seedsigner/views/seed_views.py:1212 +#: src/seedsigner/views/seed_views.py msgid "Confirm SeedQR" msgstr "시드 QR 확인" -#: src/seedsigner/views/seed_views.py:1150 +#: src/seedsigner/views/seed_views.py msgid "Confirm SeedQR?" msgstr "시드 QR 확인하고 싶어요?" -#: src/seedsigner/views/seed_views.py:1180 +#: src/seedsigner/views/seed_views.py msgid "Scan your SeedQR" msgstr "당신의 시드 QR 스캔" -#: src/seedsigner/views/seed_views.py:1190 -#: src/seedsigner/views/seed_views.py:1213 +#: src/seedsigner/views/seed_views.py msgid "Error!" msgstr "Error!" -#: src/seedsigner/views/seed_views.py:1191 +#: src/seedsigner/views/seed_views.py msgid "Your transcribed SeedQR does not match your original seed!" msgstr "" -#: src/seedsigner/views/seed_views.py:1193 -#: src/seedsigner/views/seed_views.py:1216 +#: src/seedsigner/views/seed_views.py msgid "Review SeedQR" msgstr "시드 QR를 다시보기" -#: src/seedsigner/views/seed_views.py:1202 -msgid "" -"Your transcribed SeedQR successfully scanned and yielded the same seed." +#: src/seedsigner/views/seed_views.py +msgid "Your transcribed SeedQR successfully scanned and yielded the same seed." msgstr "" -#: src/seedsigner/views/seed_views.py:1214 +#: src/seedsigner/views/seed_views.py msgid "Your transcribed SeedQR could not be read!" msgstr "" -#: src/seedsigner/views/seed_views.py:1285 +#: src/seedsigner/views/seed_views.py msgid "Sig type can't be auto-detected from this address. Please specify:" msgstr "" -#: src/seedsigner/views/seed_views.py:1326 -msgid "Load the seed to verify" -msgstr "" - -#: src/seedsigner/views/seed_views.py:1336 -msgid "Select seed to verify" -msgstr "" - -#: src/seedsigner/views/seed_views.py:1388 +#: src/seedsigner/views/seed_views.py msgid "Can't validate a single sig addr without specifying a seed" msgstr "" #. Option when scanning for a matching address; skips ten addresses ahead -#: src/seedsigner/views/seed_views.py:1434 +#: src/seedsigner/views/seed_views.py msgid "Skip 10" msgstr "스킵 10" -#: src/seedsigner/views/seed_views.py:1435 -#: src/seedsigner/views/seed_views.py:1616 +#: src/seedsigner/views/seed_views.py msgid "Cancel" msgstr "취소" -#: src/seedsigner/views/seed_views.py:1563 -#: src/seedsigner/views/seed_views.py:1579 -msgid "Taproot verification not yet implemented!" -msgstr "" - -#: src/seedsigner/views/seed_views.py:1575 -msgid "Legacy P2PKH verification not implemented!" -msgstr "" - -#: src/seedsigner/views/seed_views.py:1600 +#: src/seedsigner/views/seed_views.py msgid "multisig" msgstr "멀티 서명 " -#: src/seedsigner/views/seed_views.py:1602 +#: src/seedsigner/views/seed_views.py msgid "seed {}" msgstr "시드 {}" -#: src/seedsigner/views/seed_views.py:1605 +#: src/seedsigner/views/seed_views.py msgid "Address Verified" msgstr "에이드레스 검증했다" -#: src/seedsigner/views/seed_views.py:1615 +#: src/seedsigner/views/seed_views.py msgid "Scan Descriptor" msgstr "Descriptor 스캔" -#: src/seedsigner/views/seed_views.py:1645 +#: src/seedsigner/views/seed_views.py msgid "Return to PSBT" msgstr "PBST로 돌아가다" -#: src/seedsigner/views/settings_views.py:20 +#: src/seedsigner/views/settings_views.py msgid "I/O test" msgstr "I/O test" -#: src/seedsigner/views/settings_views.py:39 -#: src/seedsigner/views/settings_views.py:46 +#: src/seedsigner/views/settings_views.py msgid "Advanced" msgstr "선진" -#: src/seedsigner/views/settings_views.py:54 +#: src/seedsigner/views/settings_views.py msgid "Dev Options" msgstr "개발자용 설정" -#: src/seedsigner/views/tools_views.py:24 -msgid " New seed" -msgstr "새로운 시드" +#: src/seedsigner/views/settings_views.py +msgid "Persistent Settings enabled. Settings saved to SD card." +msgstr "" -#: src/seedsigner/views/tools_views.py:25 +#: src/seedsigner/views/settings_views.py +msgid "Settings updated in temporary memory" +msgstr "" + +#: src/seedsigner/views/tools_views.py msgid "New seed" msgstr "새로운 시드" -#: src/seedsigner/views/tools_views.py:26 +#: src/seedsigner/views/tools_views.py msgid "Calc 12th/24th word" msgstr "12번째/24번째 워드를 계산하다" -#: src/seedsigner/views/tools_views.py:102 -#: src/seedsigner/views/tools_views.py:228 +#: src/seedsigner/views/tools_views.py src/seedsigner/views/view.py +msgid "Tools" +msgstr "도구들" + +#: src/seedsigner/views/tools_views.py msgid "12 words" msgstr "12 워드" -#: src/seedsigner/views/tools_views.py:103 -#: src/seedsigner/views/tools_views.py:229 +#: src/seedsigner/views/tools_views.py msgid "24 words" msgstr "24 워드" -#: src/seedsigner/views/tools_views.py:107 +#: src/seedsigner/views/tools_views.py msgid "Mnemonic Length?" msgstr "시드 워드 목록 길이?" -#: src/seedsigner/views/tools_views.py:174 +#: src/seedsigner/views/tools_views.py msgid "12 words (50 rolls)" msgstr "12 워드 (50회 주사위 굴리기)" -#: src/seedsigner/views/tools_views.py:175 +#: src/seedsigner/views/tools_views.py msgid "24 words (99 rolls)" msgstr "24 워드 (99회 주사위 굴리기)" -#: src/seedsigner/views/tools_views.py:179 -#: src/seedsigner/views/tools_views.py:233 +#: src/seedsigner/views/tools_views.py msgid "Mnemonic Length" msgstr "시드 워드 목록 길이" -#: src/seedsigner/views/tools_views.py:258 +#. Label to gather entropy through coin tosses +#: src/seedsigner/views/tools_views.py +msgid "Coin flip entropy" +msgstr "" + +#. Label to gather entropy through user specified BIP-39 word +#: src/seedsigner/views/tools_views.py +msgid "Word selection entropy" +msgstr "" + +#. Label to allow user to default entropy as all-zeros +#: src/seedsigner/views/tools_views.py +msgid "Finalize with zeros" +msgstr "" + +#. label to calculate the last word of a BIP-39 mnemonic seed phrase +#: src/seedsigner/views/tools_views.py +msgid "Final Word Calc" +msgstr "" + +#: src/seedsigner/views/tools_views.py msgid "Load seed" msgstr "시드를 로드하다" -#: src/seedsigner/views/view.py:124 +#. label for addresses where others send us incoming payments +#: src/seedsigner/views/tools_views.py +msgid "Receive Addresses" +msgstr "" + +#. label for addresses that collect the change from our own outgoing payments +#: src/seedsigner/views/tools_views.py +msgid "Change Addresses" +msgstr "" + +#. a status message that our payment addresses are being calculated +#: src/seedsigner/views/tools_views.py +msgid "Calculating addrs..." +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Next {}" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Receive Addrs" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Change Addrs" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Scan" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Seeds" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Restart" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Power Off" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Reset / Power" +msgstr "" + +#: src/seedsigner/views/view.py msgid "Work In Progress" msgstr "작업중" -#: src/seedsigner/views/view.py:125 +#: src/seedsigner/views/view.py msgid "Not Yet Implemented" msgstr "아직 실행하되지 않았습니다" -#: src/seedsigner/views/view.py:126 +#: src/seedsigner/views/view.py msgid "This is still on our to-do list!" msgstr "아직 우리 할 일 목록에 있어요!" -#: src/seedsigner/views/view.py:127 +#: src/seedsigner/views/view.py msgid "Back to Main Menu" msgstr "메인 메뉴에 돌아가다" -#: src/seedsigner/views/view.py:142 +#: src/seedsigner/views/view.py +msgid "Error" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Network Mismatch" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Change Setting" +msgstr "" + +#: src/seedsigner/views/view.py msgid "System Error" msgstr "시스템 Error" + +#: src/seedsigner/views/view.py +msgid "Update Setting" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Option Disabled" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Security Tip" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "" +"For maximum security,\n" +"remove the MicroSD card\n" +"before continuing." +msgstr "" + +#~ msgid "{}..." +#~ msgstr "{}..." + +#~ msgid "Click to authorize this transaction" +#~ msgstr "이 트랜잭션 승인하실 클릭해주세요." + +#~ msgid "Signed PSBT" +#~ msgstr "사인 PSBT" + +#~ msgid "Export as a QR code for:" +#~ msgstr "QR코드처럼 내보내기 for:" + +#~ msgid "changes fingerprint" +#~ msgstr "" + +#~ msgid "Enter 12/24 words" +#~ msgstr "12/24 워드를 입력하다" + +#~ msgid "Signing PSBT..." +#~ msgstr "PSBT 서명 중..." + +#~ msgid "Taproot verification not yet implemented!" +#~ msgstr "" + +#~ msgid "Legacy P2PKH verification not implemented!" +#~ msgstr "" + +#~ msgid "Add Passphrase" +#~ msgstr "패스 프레이즈 추가하다" + +#~ msgid "Btc-only" +#~ msgstr "Btc-오로지  " + +#~ msgid "Sats-only" +#~ msgstr "Sats-오로지  " + +#~ msgid "Threshold at 0.01" +#~ msgstr "역치 0.01로" + +#~ msgid "Btc | Sats hybrid" +#~ msgstr "Btc | Sats 하이브리드" + +#~ msgid "Scan" +#~ msgstr "" + +#~ msgid "Seeds" +#~ msgstr "" + +#~ msgid "Restart" +#~ msgstr "" + +#~ msgid "Power Off" +#~ msgstr "" + +#~ msgid "Reset / Power" +#~ msgstr "다시 시작 / Power ON" + +#~ msgid " (?)" +#~ msgstr "" + +#~ msgid "Scan a QR code" +#~ msgstr "QR 코드를 스캔" + +#~ msgid "Yes " +#~ msgstr "" + +#~ msgid "No " +#~ msgstr "" + +#~ msgid "< back | Scan a QR code" +#~ msgstr "" + +#~ msgid "Settings QR" +#~ msgstr "" + +#~ msgid "Settings imported successfully!" +#~ msgstr "설정 임포트 성공!" + +#~ msgid "MicroSD removed" +#~ msgstr "" + +#~ msgid "MicroSD inserted" +#~ msgstr "" + +#~ msgid "changes seed:" +#~ msgstr "" + +#~ msgid "< back | click joystick" +#~ msgstr "< 돌아가 | 조이스틱을 클릭 " + +#~ msgid "Store Settings on SD card." +#~ msgstr "" + +#~ msgid "MicroSD card is removed" +#~ msgstr "" + +#~ msgid "Show passphrase warning" +#~ msgstr "" + +#~ msgid "" +#~ "BIP-39 passphrase changes your seed, resulting in new xpubs, wallets, etc.\n" +#~ "Be sure you know what you're doing!" +#~ msgstr "" + +#~ msgid "SeedQR is your private key!" +#~ msgstr "" + +#~ msgid "Never photograph it or scan it into an online device." +#~ msgstr "" + +#~ msgid " New seed" +#~ msgstr "새로운 시드" + +#~ msgid "Scan wallet descriptor" +#~ msgstr "" + diff --git a/src/seedsigner/resources/babel/pt/LC_MESSAGES/messages.po b/src/seedsigner/resources/babel/pt/LC_MESSAGES/messages.po index d3581013..5485d054 100644 --- a/src/seedsigner/resources/babel/pt/LC_MESSAGES/messages.po +++ b/src/seedsigner/resources/babel/pt/LC_MESSAGES/messages.po @@ -1,37 +1,208 @@ -# Translations template for PROJECT. +# Portuguese translations for PROJECT. # Copyright (C) 2022 ORGANIZATION # This file is distributed under the same license as the PROJECT project. # FIRST AUTHOR , 2022. -# +# # Translators: # Dom Azeredo, 2022 -# +# jdlcdl, 2022 #, fuzzy msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-05-10 17:32-0500\n" +"POT-Creation-Date: 2023-10-09 11:36-0400\n" "PO-Revision-Date: 2022-04-29 13:50+0000\n" "Last-Translator: Dom Azeredo, 2022\n" -"Language-Team: Portuguese (https://www.transifex.com/seedsigner/teams/135832/pt/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.10.1\n" "Language: pt\n" +"Language-Team: Portuguese (https://www.transifex.com/seedsigner/teams/135832/pt/)\n" "Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.10.3\n" -#: src/seedsigner/gui/screens/main_menu_screens.py:12 -#: src/seedsigner/gui/screens/scan_screens.py:140 -msgid "Home" -msgstr "Início" +#. The abbreviated label for the special key on a standard keyboard. +#: src/seedsigner/gui/keyboard.py +msgid "del" +msgstr "" -#: src/seedsigner/gui/screens/main_menu_screens.py:24 +#. The abbreviated label for the special key on a standard keyboard. +#: src/seedsigner/gui/keyboard.py +msgid "space" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "" +"You can remove\n" +"the SD card now" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "SD card removed" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "SD card inserted" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review PSBT" +msgstr "Revisar PSBT" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review Details" +msgstr "Revisar Detalhes" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "1 input" +msgstr "1 entrada" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input 1" +msgstr "entrada 1" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input 2" +msgstr "entrada 2" + +#. Indicates that items have been omitted from a series: e.g. "1, 2, 3, [...], +#. 8" +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "[ ... ]" +msgstr "[ ... ]" + +#. Input number will be inserted (e.g. "input 3") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input {}" +msgstr "entrada {}" + +#. Ellipsis ("...") characters used to truncate an address (e.g. "bc1qabc...") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "..." +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/views/psbt_views.py +msgid "self-transfer" +msgstr "autotransferência" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient 1" +msgstr "destino 1" + +#. Inserts the recipient number (e.g. the fifth one is: "recipient 5") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient {}" +msgstr "destino {}" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "fee" +msgstr "taxa" + +#. Label for a change output in the PSBT Overview flow diagram +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/views/psbt_views.py +msgid "change" +msgstr "troco" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "PSBT Math" +msgstr "Cálculo do PSBT" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review Recipients" +msgstr "Revisar Destinos" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "btc" +msgstr "btc" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "sats" +msgstr "sats" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input" +msgid_plural "inputs" +msgstr[0] "entradas" +msgstr[1] "entradas" +msgstr[2] "entradas" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient" +msgid_plural "recipients" +msgstr[0] "destinatário" +msgstr[1] "destinatários" +msgstr[2] "destinos" + +#. Denonination is inserted (e.g. your "btc change" or "sats change") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "{} change" +msgstr "{} troco" + +#. First variable is either "Multisig" or the fingerprint for single sig; +#. Second is "Change" or "Addr" (change vs receive addr); Third is the address +#. index number (e.g. your #4 receive addr). +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/models/settings_definition.py +#: src/seedsigner/views/seed_views.py +msgid "Multisig" +msgstr "Multisig" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Change" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Addr" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Address verified!" +msgstr "Endereço verificado!" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Sign PSBT" +msgstr "Assinar PSBT" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Click to approve this transaction" +msgstr "" + +#: src/seedsigner/gui/screens/scan_screens.py +msgid "< back | " +msgstr "" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "Success!" +msgstr "Sucesso!" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "OK" +msgstr "OK" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/psbt_views.py +msgid "Caution" +msgstr "Cuidado" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "Privacy Leak!" +msgstr "Vazamento de privacidade!" + +#: src/seedsigner/gui/screens/screen.py +msgid "I Understand" +msgstr "Eu Entendo" + +#: src/seedsigner/gui/screens/screen.py +msgid "Classified Info!" +msgstr "Informação Sigilosa!" + +#: src/seedsigner/gui/screens/screen.py msgid "Restarting" msgstr "Reiniciando" -#: src/seedsigner/gui/screens/main_menu_screens.py:29 +#: src/seedsigner/gui/screens/screen.py msgid "" "SeedSigner is restarting.\n" "\n" @@ -41,355 +212,225 @@ msgstr "" "\n" "Todos os dados serão apagados. " -#: src/seedsigner/gui/screens/main_menu_screens.py:39 +#: src/seedsigner/gui/screens/screen.py msgid "Powering Off" msgstr "Desligando" -#: src/seedsigner/gui/screens/main_menu_screens.py:44 +#: src/seedsigner/gui/screens/screen.py msgid "Please wait about 30 seconds before disconnecting power." msgstr "Aguarde cerca de 30 segundos antes de desconectar a energia." -#: src/seedsigner/gui/screens/psbt_screens.py:29 -#: src/seedsigner/views/seed_views.py:352 -msgid "Review PSBT" -msgstr "Revisar PSBT" - -#: src/seedsigner/gui/screens/psbt_screens.py:31 -msgid "Review Details" -msgstr "Revisar Detalhes" - -#: src/seedsigner/gui/screens/psbt_screens.py:88 -msgid "1 input" -msgstr "1 entrada" - -#: src/seedsigner/gui/screens/psbt_screens.py:90 -msgid "input 1" -msgstr "entrada 1" - -#: src/seedsigner/gui/screens/psbt_screens.py:91 -msgid "input 2" -msgstr "entrada 2" - -#. Indicates that items have been omitted from a series: e.g. "1, 2, 3, [...], -#. 8" -#: src/seedsigner/gui/screens/psbt_screens.py:93 -#: src/seedsigner/gui/screens/psbt_screens.py:141 -msgid "[ ... ]" -msgstr "[ ... ]" - -#. Input number will be inserted (e.g. "input 3") -#: src/seedsigner/gui/screens/psbt_screens.py:95 -#: src/seedsigner/gui/screens/psbt_screens.py:96 -#: src/seedsigner/gui/screens/psbt_screens.py:99 -msgid "input {}" -msgstr "entrada {}" - -#. Display a truncated address (e.g. "bc1qabc...") -#: src/seedsigner/gui/screens/psbt_screens.py:127 -msgid "{}..." -msgstr "{}..." - -#: src/seedsigner/gui/screens/psbt_screens.py:136 -msgid "self-transfer" -msgstr "autotransferência" - -#: src/seedsigner/gui/screens/psbt_screens.py:139 -msgid "recipient 1" -msgstr "destino 1" - -#. Inserts the recipient number (e.g. the fifth one is: "recipient 5") -#: src/seedsigner/gui/screens/psbt_screens.py:143 -msgid "recipient {}" -msgstr "destino {}" - -#: src/seedsigner/gui/screens/psbt_screens.py:145 -#: src/seedsigner/gui/screens/psbt_screens.py:555 -msgid "fee" -msgstr "taxa" - -#. Label for a change output in the PSBT Overview flow diagram -#: src/seedsigner/gui/screens/psbt_screens.py:150 -msgid "change" -msgstr "troco" - -#: src/seedsigner/gui/screens/psbt_screens.py:461 -msgid "PSBT Math" -msgstr "Cálculo do PSBT" - -#: src/seedsigner/gui/screens/psbt_screens.py:462 -msgid "Review Recipients" -msgstr "Revisar Destinos" - -#: src/seedsigner/gui/screens/psbt_screens.py:468 -#: src/seedsigner/gui/screens/psbt_screens.py:521 -msgid "btc" -msgstr "btc" - -#: src/seedsigner/gui/screens/psbt_screens.py:480 -msgid "sats" -msgstr "sats" - -#: src/seedsigner/gui/screens/psbt_screens.py:538 -msgid "input" -msgid_plural "inputs" -msgstr[0] "entradas" -msgstr[1] "entradas" -msgstr[2] "entradas" - -#: src/seedsigner/gui/screens/psbt_screens.py:548 -msgid "recipient" -msgid_plural "recipients" -msgstr[0] "destinatário" -msgstr[1] "destinatários" -msgstr[2] "destinos" - -#. Denonination is inserted (e.g. your "btc change" or "sats change") -#: src/seedsigner/gui/screens/psbt_screens.py:566 -msgid "{} change" -msgstr "{} troco" - -#: src/seedsigner/gui/screens/psbt_screens.py:671 -msgid "Address verified!" -msgstr "Endereço verificado!" - -#: src/seedsigner/gui/screens/psbt_screens.py:683 -msgid "Sign PSBT" -msgstr "Assinar PSBT" - -#: src/seedsigner/gui/screens/psbt_screens.py:697 -msgid "Click to authorize this transaction" -msgstr "Clique para autorizar transação" - -#: src/seedsigner/gui/screens/psbt_screens.py:707 -msgid "Signed PSBT" -msgstr "PSBT assinado" - -#. Prompt to specify which coordinator software to use (e.g. Specter, Sparrow, -#. etc) -#: src/seedsigner/gui/screens/psbt_screens.py:713 -msgid "Export as a QR code for:" -msgstr "Exportar como QR code para:" - -#: src/seedsigner/gui/screens/scan_screens.py:22 -msgid "Scan a QR code" -msgstr "Ler QR code" - -#: src/seedsigner/gui/screens/scan_screens.py:138 -msgid "Settings QR" -msgstr "Ajustes via QR" - -#: src/seedsigner/gui/screens/scan_screens.py:156 -msgid "Settings imported successfully!" -msgstr "Ajustes importados com sucesso!" - -#: src/seedsigner/gui/screens/screen.py:731 -#: src/seedsigner/views/seed_views.py:954 -#: src/seedsigner/views/seed_views.py:1201 -msgid "Success!" -msgstr "Sucesso!" - -#: src/seedsigner/gui/screens/screen.py:735 -#: src/seedsigner/views/seed_views.py:956 -#: src/seedsigner/views/seed_views.py:1204 -#: src/seedsigner/views/seed_views.py:1647 src/seedsigner/views/view.py:145 -msgid "OK" -msgstr "OK" - -#: src/seedsigner/gui/screens/screen.py:852 -#: src/seedsigner/views/psbt_views.py:430 -msgid "Caution" -msgstr "Cuidado" - -#: src/seedsigner/gui/screens/screen.py:855 -msgid "I Understand" -msgstr "Eu Entendo" - -#: src/seedsigner/gui/screens/screen.py:867 -msgid "Classified Info!" -msgstr "Informação Sigilosa!" - -#: src/seedsigner/gui/screens/seed_screens.py:411 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Finalize Seed" msgstr "Finalizar Seed" -#: src/seedsigner/gui/screens/seed_screens.py:424 -#: src/seedsigner/gui/screens/tools_screens.py:291 +#. a label for the shortened Key-id of a BIP-32 master HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "fingerprint" msgstr "fingerprint" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Backup Seed" +msgstr "Backup da Seed" + +#. Additional explainer for the two seed backup options (mnemonic phrase and +#. SeedQR). +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Backups do not include your passphrase." +msgstr "" + #. Displays the page number and total: (e.g. page 1 of 6) -#: src/seedsigner/gui/screens/seed_screens.py:461 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Seed Words: {}/{}" msgstr "Palavras da Seed: {}/{}" -#: src/seedsigner/gui/screens/seed_screens.py:541 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "BIP-85 Index" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py msgid "Verify Backup?" msgstr "Verificar Backup?" -#: src/seedsigner/gui/screens/seed_screens.py:547 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Optionally verify that your mnemonic backup is correct." msgstr "Opcionalmente verifique se sua cópia do mnemônico está correto." -#: src/seedsigner/gui/screens/seed_screens.py:556 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Derivation Path" msgstr "Caminho de Derivação" -#: src/seedsigner/gui/screens/seed_screens.py:703 -msgid "Xpub Details" -msgstr "Xpub Detalhado" - -#: src/seedsigner/gui/screens/seed_screens.py:709 -#: src/seedsigner/gui/screens/seed_screens.py:713 -#: src/seedsigner/views/seed_views.py:354 -#: src/seedsigner/views/seed_views.py:471 -#: src/seedsigner/views/seed_views.py:504 -#: src/seedsigner/views/seed_views.py:578 +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py msgid "Export Xpub" msgstr "Exportar Xpub" -#: src/seedsigner/gui/screens/seed_screens.py:722 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Xpub Details" +msgstr "Xpub Detalhado" + +#. Short for "BIP32 Master Fingerprint" +#. a label for the shortened Key-id of a BIP-32 master HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Fingerprint" msgstr "Fingerprint" #. Short for "Derivation Path" -#: src/seedsigner/gui/screens/seed_screens.py:732 +#. a label for the derivation-path into a BIP-32 HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Derivation" msgstr "Derivação" -#: src/seedsigner/gui/screens/seed_screens.py:741 +#. Short for "BIP32 Extended Public Key" +#: src/seedsigner/gui/screens/seed_screens.py msgid "Xpub" msgstr "Xpub" -#: src/seedsigner/gui/screens/seed_screens.py:754 -#: src/seedsigner/views/seed_views.py:217 -msgid "Add Passphrase" -msgstr "Adicionar Passphrase" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "BIP-39 Passphrase" +msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:1106 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Verify Passphrase" msgstr "Verificar Passphrase" #. Describes the effect of applying a BIP-39 passphrase; it changes the seed's #. fingerprint -#: src/seedsigner/gui/screens/seed_screens.py:1115 +#: src/seedsigner/gui/screens/seed_screens.py msgid "changes fingerprint" -msgstr "muda o fingerprint" +msgstr "" #. Refers to the SeedQR type: Standard or Compact -#: src/seedsigner/gui/screens/seed_screens.py:1170 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Standard" msgstr "Padrão" #. Briefly explains the Standard SeedQR data format -#: src/seedsigner/gui/screens/seed_screens.py:1172 +#: src/seedsigner/gui/screens/seed_screens.py msgid "BIP-39 wordlist indices" msgstr "Índice de palavras BIP-39" #. Refers to the SeedQR type: Standard or Compact -#: src/seedsigner/gui/screens/seed_screens.py:1180 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Compact" msgstr "Compacto" #. Briefly explains the Compact SeedQR data format -#: src/seedsigner/gui/screens/seed_screens.py:1182 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Raw entropy bits" msgstr "Bits brutos de entropia" -#: src/seedsigner/gui/screens/seed_screens.py:1196 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Transcribe SeedQR" msgstr "Desenhar SeedQR" #. Refers to the QR code size: 21x21, 25x25, or 29x29 -#: src/seedsigner/gui/screens/seed_screens.py:1198 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Begin {}x{}" msgstr "Iniciar {}x{}" -#: src/seedsigner/gui/screens/seed_screens.py:1275 +#: src/seedsigner/gui/screens/seed_screens.py msgid "click to exit" msgstr "clique para sair" -#: src/seedsigner/gui/screens/seed_screens.py:1411 -msgid "" -"Optionally scan your transcribed SeedQR to confirm that it reads back " -"correctly." -msgstr "" -"Opcionalmente, escaneie sua SeedQR desenhada e confirme se ela é lida " -"corretamente." +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Optionally scan your transcribed SeedQR to confirm that it reads back correctly." +msgstr "Opcionalmente, escaneie sua SeedQR desenhada e confirme se ela é lida corretamente." -#: src/seedsigner/gui/screens/seed_screens.py:1468 -#: src/seedsigner/views/seed_views.py:1284 -#: src/seedsigner/views/seed_views.py:1342 +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py msgid "Verify Address" msgstr "Verificar Endereço" #. Inserts the nth address number (e.g. "Checking address 7") -#: src/seedsigner/gui/screens/seed_screens.py:1527 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Checking address {}" msgstr "Checando endereço {}" -#: src/seedsigner/gui/screens/seed_screens.py:1544 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Multisig Verification" msgstr "Verificação Multisig" -#: src/seedsigner/gui/screens/seed_screens.py:1549 -msgid "" -"Load your multisig wallet descriptor to verify your receive/self-transfer or" -" change address." -msgstr "" -"Adicione seu descriptor de multisig para verificar seu endereço de " -"recebimento, autotransferência ou troco." +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Load your multisig wallet descriptor to verify your receive/self-transfer or change address." +msgstr "Adicione seu descriptor de multisig para verificar seu endereço de recebimento, autotransferência ou troco." -#: src/seedsigner/gui/screens/seed_screens.py:1562 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Descriptor Loaded" msgstr "Descriptor Carregado" #. Label for the multisig wallet's signing policy (e.g. 2-of-3) -#: src/seedsigner/gui/screens/seed_screens.py:1568 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Policy" msgstr "Configuração" -#: src/seedsigner/gui/screens/seed_screens.py:1576 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Signing Keys" msgstr "Chaves de Assinaturas" -#: src/seedsigner/gui/screens/settings_screens.py:26 -#: src/seedsigner/views/main_menu_views.py:21 -#: src/seedsigner/views/settings_views.py:36 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Review Message" +msgstr "" + +#. Short for "Next step" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +#: src/seedsigner/views/tools_views.py +msgid "Next" +msgstr "Próximo" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Confirm Address" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Sign Message" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "derivation path" +msgstr "" + +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py msgid "Settings" msgstr "Ajustes" #. Short for "Input/Output"; screen to make sure the buttons and camera are #. working properly -#: src/seedsigner/gui/screens/settings_screens.py:58 +#: src/seedsigner/gui/screens/settings_screens.py msgid "I/O Test" msgstr "Testar Botões/Câmera" #. Blank the screen -#: src/seedsigner/gui/screens/settings_screens.py:129 -#: src/seedsigner/gui/screens/settings_screens.py:140 -#: src/seedsigner/gui/screens/settings_screens.py:221 +#: src/seedsigner/gui/screens/settings_screens.py msgid "Clear" msgstr "Limpar" -#: src/seedsigner/gui/screens/settings_screens.py:161 +#: src/seedsigner/gui/screens/settings_screens.py msgid "Exit" msgstr "Sair" -#: src/seedsigner/gui/screens/settings_screens.py:175 -#: src/seedsigner/gui/screens/tools_screens.py:71 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Capturing image..." msgstr "Capturando imagem..." -#: src/seedsigner/gui/screens/settings_screens.py:288 -#: src/seedsigner/views/settings_views.py:21 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py msgid "Donate" msgstr "Doar" -#. If your language uses the percent sign ("%"), your translation must also -#. use +#. If your language uses the percent sign ("%"), your translation must also use #. two percent signs ("%%") due to python formatting oddities. "100%%" will be #. rendered as "100%". -#: src/seedsigner/gui/screens/settings_screens.py:293 +#: src/seedsigner/gui/screens/settings_screens.py #, python-format msgid "" "SeedSigner is 100%% free & open source, funded solely by the Bitcoin community.\n" @@ -399,710 +440,992 @@ msgstr "" "SeedSigner é 100%% gratuito e de código aberto, financiado exclusivamente pela comunidade Bitcoin.\n" "Doe Onchain ou LN em:" -#. Instructions when taking a picture; "back" should point left. -#: src/seedsigner/gui/screens/tools_screens.py:40 -msgid "< back | click joystick" -msgstr "< voltar | clique p/ foto" +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py +msgid "Settings QR" +msgstr "" + +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py +msgid "Home" +msgstr "Início" #. Instructions when reviewing an image; "reshoot" must point left, "accept" #. must point right -#: src/seedsigner/gui/screens/tools_screens.py:98 +#: src/seedsigner/gui/screens/tools_screens.py msgid " < reshoot | accept > " msgstr "< repetir | aceitar >" #. current roll number vs total rolls (e.g. roll 7 of 50) -#: src/seedsigner/gui/screens/tools_screens.py:120 -#: src/seedsigner/gui/screens/tools_screens.py:245 +#: src/seedsigner/gui/screens/tools_screens.py msgid "Dice Roll {}/{}" msgstr "Jogar Dados {}/{}" -#: src/seedsigner/gui/screens/tools_screens.py:274 +#. Build the last word in a 12 or 24 word BIP-39 mnemonic seed phrase. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Build Final Word" +msgstr "" + +#. Number of BIP-39 seed words, and the entropy -- in bits, contained within. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "The {}th word is built from {} more entropy bits plus auto-calculated checksum." +msgstr "" + +#. current coin-flip number vs total flips (e.g. flip 3 of 4) +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Coin Flip {}/{}" +msgstr "" + +#. How we call the "front" side result during a coin toss. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Heads = 1" +msgstr "" + +#. How we call the "back" side result during a coin toss. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Tails = 0" +msgstr "" + +#. A labeled presentation of a user-supplied value +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Your input: \"{}\"" +msgstr "" + +#. A function of "x" to be used for detecting errors in "x" +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Checksum" +msgstr "" + +#. labeled presentation of the last word in a BIP-39 mnemonic seed phrase. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Final Word: \"{}\"" +msgstr "" + +#. a label for the last word of a 12-word BIP-39 mnemonic seed phrase +#: src/seedsigner/gui/screens/tools_screens.py msgid "12th Word" msgstr "12ª Palavra" -#: src/seedsigner/gui/screens/tools_screens.py:276 +#. a label for the last word of a 24-word BIP-39 mnemonic seed phrase +#: src/seedsigner/gui/screens/tools_screens.py msgid "24th Word" msgstr "24ª Palavra" -#: src/seedsigner/models/settings_definition.py:14 +#. a label for the tool to explore public addresses for this seed. +#: src/seedsigner/gui/screens/tools_screens.py +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py +msgid "Address Explorer" +msgstr "" + +#. a label for a BIP-380-ish Output Descriptor +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Wallet descriptor" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Enabled" msgstr "Habilitado" -#: src/seedsigner/models/settings_definition.py:15 -#: src/seedsigner/models/settings_definition.py:20 +#: src/seedsigner/models/settings_definition.py msgid "Disabled" msgstr "Desabilitado" -#: src/seedsigner/models/settings_definition.py:18 -#: src/seedsigner/models/settings_definition.py:26 +#: src/seedsigner/models/settings_definition.py msgid "Prompt" msgstr "Solicitar" -#: src/seedsigner/models/settings_definition.py:19 -#: src/seedsigner/models/settings_definition.py:23 -#: src/seedsigner/models/settings_definition.py:29 +#: src/seedsigner/models/settings_definition.py msgid "Required" msgstr "Obrigatório" -#: src/seedsigner/models/settings_definition.py:72 -msgid "Btc-only" -msgstr "Somente BTC" +#. Translator is expected ONLY to change their own locale below +#: src/seedsigner/models/settings_definition.py +msgid "Arabic" +msgstr "عربي" -#: src/seedsigner/models/settings_definition.py:73 -msgid "Sats-only" -msgstr "Somente sats" +#: src/seedsigner/models/settings_definition.py +msgid "Czech" +msgstr "čeština" -#: src/seedsigner/models/settings_definition.py:74 -msgid "Threshold at 0.01" -msgstr "Limiar em 0,01" +#: src/seedsigner/models/settings_definition.py +msgid "French" +msgstr "Français" -#: src/seedsigner/models/settings_definition.py:75 -msgid "Btc | Sats hybrid" -msgstr "Btc | Sats híbrido" +#: src/seedsigner/models/settings_definition.py +msgid "German" +msgstr "Deutsch" -#: src/seedsigner/models/settings_definition.py:83 +#: src/seedsigner/models/settings_definition.py +msgid "Hebrew" +msgstr "עִברִית" + +#: src/seedsigner/models/settings_definition.py +msgid "Japanese" +msgstr "日本" + +#: src/seedsigner/models/settings_definition.py +msgid "Korean" +msgstr "한국인" + +#: src/seedsigner/models/settings_definition.py +msgid "Portuguese" +msgstr "Português" + +#: src/seedsigner/models/settings_definition.py +msgid "Russian" +msgstr "Русский" + +#: src/seedsigner/models/settings_definition.py +msgid "Spanish" +msgstr "Español" + +#: src/seedsigner/models/settings_definition.py msgid "0°" msgstr "0°" -#: src/seedsigner/models/settings_definition.py:84 +#: src/seedsigner/models/settings_definition.py msgid "90°" msgstr "90°" -#: src/seedsigner/models/settings_definition.py:85 +#: src/seedsigner/models/settings_definition.py msgid "180°" msgstr "180°" -#: src/seedsigner/models/settings_definition.py:86 +#: src/seedsigner/models/settings_definition.py msgid "270°" msgstr "270°" -#: src/seedsigner/models/settings_definition.py:94 +#: src/seedsigner/models/settings_definition.py msgid "Low" msgstr "Baixa" -#: src/seedsigner/models/settings_definition.py:95 +#: src/seedsigner/models/settings_definition.py msgid "Medium" msgstr "Média" -#: src/seedsigner/models/settings_definition.py:96 +#: src/seedsigner/models/settings_definition.py msgid "High" msgstr "Alta" -#: src/seedsigner/models/settings_definition.py:104 +#: src/seedsigner/models/settings_definition.py msgid "Mainnet" msgstr "Rede Principal" -#: src/seedsigner/models/settings_definition.py:105 +#: src/seedsigner/models/settings_definition.py msgid "Testnet" msgstr "Rede Teste" -#: src/seedsigner/models/settings_definition.py:106 +#: src/seedsigner/models/settings_definition.py msgid "Regtest" msgstr "Regtest" -#: src/seedsigner/models/settings_definition.py:122 -#: src/seedsigner/views/seed_views.py:466 +#: src/seedsigner/models/settings_definition.py +msgid "Store Settings on SD card" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Insert SD card to enable" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +#: src/seedsigner/views/seed_views.py msgid "Single Sig" msgstr "Single Sig" -#: src/seedsigner/models/settings_definition.py:123 -#: src/seedsigner/views/seed_views.py:467 -msgid "Multisig" -msgstr "Multisig" +#: src/seedsigner/models/settings_definition.py +msgid "Native Segwit" +msgstr "" -#: src/seedsigner/models/settings_definition.py:354 +#: src/seedsigner/models/settings_definition.py +msgid "Nested Segwit (legacy)" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Taproot" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Custom Derivation" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Language" msgstr "Idioma" -#: src/seedsigner/models/settings_definition.py:362 +#: src/seedsigner/models/settings_definition.py msgid "Mnemonic language" msgstr "Idioma do Mnemônico" -#: src/seedsigner/models/settings_definition.py:370 +#: src/seedsigner/models/settings_definition.py msgid "Persistent settings" msgstr "Ajustes Persistentes" -#: src/seedsigner/models/settings_definition.py:376 +#: src/seedsigner/models/settings_definition.py msgid "Coordinator software" msgstr "Software coordenador" -#: src/seedsigner/models/settings_definition.py:383 +#: src/seedsigner/models/settings_definition.py msgid "Denomination display" msgstr "Unidade de Bitcoin" -#: src/seedsigner/models/settings_definition.py:392 +#: src/seedsigner/models/settings_definition.py msgid "Bitcoin network" msgstr "Rede Bitcoin" -#: src/seedsigner/models/settings_definition.py:400 +#: src/seedsigner/models/settings_definition.py msgid "QR code density" msgstr "Densidade do QR code" -#: src/seedsigner/models/settings_definition.py:408 +#: src/seedsigner/models/settings_definition.py msgid "Xpub export" msgstr "Exportar Xpub" -#: src/seedsigner/models/settings_definition.py:414 +#: src/seedsigner/models/settings_definition.py msgid "Sig types" msgstr "Tipos de Assinaturas" -#: src/seedsigner/models/settings_definition.py:422 +#: src/seedsigner/models/settings_definition.py msgid "Script types" msgstr "Tipo de endereço" -#: src/seedsigner/models/settings_definition.py:430 +#: src/seedsigner/models/settings_definition.py msgid "Show xpub details" msgstr "Detalhes da Xpub" -#: src/seedsigner/models/settings_definition.py:436 +#: src/seedsigner/models/settings_definition.py msgid "BIP-39 passphrase" msgstr "BIP-39 passphrase" -#: src/seedsigner/models/settings_definition.py:444 +#: src/seedsigner/models/settings_definition.py msgid "Camera rotation" msgstr "Rotação da Camera" -#: src/seedsigner/models/settings_definition.py:452 +#: src/seedsigner/models/settings_definition.py msgid "Compact SeedQR" msgstr "SeedQR Compacta" -#: src/seedsigner/models/settings_definition.py:458 +#: src/seedsigner/models/settings_definition.py +msgid "BIP-85 child seeds" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Message signing" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Show privacy warnings" msgstr "Mostrar avisos de privacidade" -#: src/seedsigner/models/settings_definition.py:464 +#: src/seedsigner/models/settings_definition.py msgid "Show dire warnings" msgstr "Mostrar avisos intimidadores" -#: src/seedsigner/models/settings_definition.py:470 +#: src/seedsigner/models/settings_definition.py +msgid "Show QR brightness tips" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Show partner logos" msgstr "Mostrar logos dos parceiros" -#: src/seedsigner/models/settings_definition.py:485 +#: src/seedsigner/models/settings_definition.py msgid "QR background color" msgstr "Cor de fundo do QR " -#: src/seedsigner/views/main_menu_views.py:18 -msgid "Scan" -msgstr "Ler" - -#: src/seedsigner/views/main_menu_views.py:19 -msgid "Seeds" -msgstr "Seeds" - -#: src/seedsigner/views/main_menu_views.py:20 -#: src/seedsigner/views/tools_views.py:29 -msgid "Tools" -msgstr "Recursos" - -#: src/seedsigner/views/main_menu_views.py:38 -msgid "Restart" -msgstr "Reiniciar" - -#: src/seedsigner/views/main_menu_views.py:39 -msgid "Power Off" -msgstr "Desligar" - -#: src/seedsigner/views/main_menu_views.py:42 -msgid "Reset / Power" -msgstr "Reiniciar / Desligar" - -#: src/seedsigner/views/psbt_views.py:36 -#: src/seedsigner/views/seed_views.py:1322 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py msgid "Scan a seed" msgstr "Ler Seed" -#: src/seedsigner/views/psbt_views.py:37 -#: src/seedsigner/views/seed_views.py:1323 -msgid "Enter 12/24 words" -msgstr "Digitar 12/24 palavras" +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +msgid "Enter 12-word seed" +msgstr "Digitar 12 palavras" -#: src/seedsigner/views/psbt_views.py:53 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +msgid "Enter 24-word seed" +msgstr "Digitar 24 palavras" + +#: src/seedsigner/views/psbt_views.py msgid "Select Signer" msgstr "Escolha o Assinador" -#: src/seedsigner/views/psbt_views.py:85 +#: src/seedsigner/views/psbt_views.py msgid "Parsing PSBT..." msgstr "Analizando PSBT..." -#: src/seedsigner/views/psbt_views.py:155 +#: src/seedsigner/views/psbt_views.py msgid "Unsupported Script Type!" msgstr "Tipo de script não suportado!" -#: src/seedsigner/views/psbt_views.py:156 -msgid "" -"PSBT has unsupported input script type, please verify your change addresses." -msgstr "" -"O PSBT usa um script não suportado, favor verificar seus endereços de troco." +#: src/seedsigner/views/psbt_views.py +msgid "PSBT has unsupported input script type, please verify your change addresses." +msgstr "O PSBT usa um script não suportado, favor verificar seus endereços de troco." -#: src/seedsigner/views/psbt_views.py:157 -#: src/seedsigner/views/psbt_views.py:176 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/view.py msgid "Continue" msgstr "Continuar" #. User will receive no change back; the inputs to this transaction are fully #. spent -#: src/seedsigner/views/psbt_views.py:174 +#: src/seedsigner/views/psbt_views.py msgid "Full Spend!" msgstr "Gastará Tudo!" -#: src/seedsigner/views/psbt_views.py:175 -msgid "" -"This PSBT spends its entire input value. No change is coming back to your " -"wallet." -msgstr "" -"Este PSBT gasta todo o valor das entradas. Nenhum troco voltará para sua " -"carteira." +#: src/seedsigner/views/psbt_views.py +msgid "This PSBT spends its entire input value. No change is coming back to your wallet." +msgstr "Este PSBT gasta todo o valor das entradas. Nenhum troco voltará para sua carteira." #. Future-tense used to indicate that this transaction will send this amount, -#. as opposed to "Send" on its own which could be misread as an instant -#. command +#. as opposed to "Send" on its own which could be misread as an instant command #. (e.g. "Send Now"). -#: src/seedsigner/views/psbt_views.py:242 +#: src/seedsigner/views/psbt_views.py msgid "Will Send" msgstr "Enviará" -#: src/seedsigner/views/psbt_views.py:248 +#: src/seedsigner/views/psbt_views.py msgid "Next Recipient" msgstr "Próximo Destino" -#. Short for "Next step" -#: src/seedsigner/views/psbt_views.py:251 -#: src/seedsigner/views/psbt_views.py:320 -#: src/seedsigner/views/seed_views.py:796 -msgid "Next" -msgstr "Próximo" - #. The amount you're receiving back from the transaction -#: src/seedsigner/views/psbt_views.py:324 +#: src/seedsigner/views/psbt_views.py msgid "Your Change" msgstr "Seu Troco" -#: src/seedsigner/views/psbt_views.py:325 +#: src/seedsigner/views/psbt_views.py msgid "Verify Multisig Change" msgstr "Verificar Troco Multisig" -#: src/seedsigner/views/psbt_views.py:327 +#: src/seedsigner/views/psbt_views.py msgid "Self-Transfer" msgstr "Autotransferência" -#: src/seedsigner/views/psbt_views.py:328 +#: src/seedsigner/views/psbt_views.py msgid "Verify Multisig Addr" msgstr "Verificar End. Mutisig" -#: src/seedsigner/views/psbt_views.py:347 +#: src/seedsigner/views/psbt_views.py msgid "Verifying Change..." msgstr "Verificando Troco..." -#: src/seedsigner/views/psbt_views.py:349 +#: src/seedsigner/views/psbt_views.py msgid "Verifying Self-Transfer..." msgstr "Verificando Auto-Transferência" -#: src/seedsigner/views/psbt_views.py:431 -msgid "" -"PSBT's {} address could not be verified with your multisig wallet " -"descriptor." -msgstr "" -"O endereço de {} do PSBT não pôde ser verificado com seu descriptor " -"multisig." +#. Variable is either "change" or "self-transfer". +#: src/seedsigner/views/psbt_views.py +msgid "PSBT's {} address could not be verified with your multisig wallet descriptor." +msgstr "O endereço de {} do PSBT não pôde ser verificado com seu descriptor multisig." -#: src/seedsigner/views/psbt_views.py:433 +#: src/seedsigner/views/psbt_views.py msgid "Suspicious PSBT" msgstr "PSBT Suspeito" -#: src/seedsigner/views/psbt_views.py:434 +#. Variable is either "change" or "self-transfer". +#: src/seedsigner/views/psbt_views.py msgid "PSBT's {} address could not be generated from your seed." msgstr "O endereço de {} do PSBT não pôde ser gerado pela sua seed." -#: src/seedsigner/views/psbt_views.py:438 +#: src/seedsigner/views/psbt_views.py msgid "Address Verification Failed" msgstr "Verificação de Endereço Falhou" -#: src/seedsigner/views/psbt_views.py:440 +#: src/seedsigner/views/psbt_views.py msgid "Discard PSBT" msgstr "Descartar PSBT" -#: src/seedsigner/views/psbt_views.py:465 +#: src/seedsigner/views/psbt_views.py msgid "Approve PSBT" msgstr "Aprovar PSBT" -#: src/seedsigner/views/psbt_views.py:470 -msgid "Signing PSBT..." -msgstr "Assinar PSBT" - -#: src/seedsigner/views/psbt_views.py:548 -msgid "PSBT Error" -msgstr "Erro no PSBT" - -#: src/seedsigner/views/psbt_views.py:550 -msgid "Signing Failed" -msgstr "Assinatura Falhou" - -#: src/seedsigner/views/psbt_views.py:551 -msgid "Signing with this seed did not add a valid signature." -msgstr "Assinar com esta seed não adicionou uma assinatura válida." - -#: src/seedsigner/views/psbt_views.py:552 +#: src/seedsigner/views/psbt_views.py msgid "Select Diff Seed" msgstr "Usar outra Seed" +#: src/seedsigner/views/psbt_views.py +msgid "PSBT Error" +msgstr "Erro no PSBT" + +#: src/seedsigner/views/psbt_views.py +msgid "Signing Failed" +msgstr "Assinatura Falhou" + +#: src/seedsigner/views/psbt_views.py +msgid "Signing with this seed did not add a valid signature." +msgstr "Assinar com esta seed não adicionou uma assinatura válida." + #. This is on the opening splash screen, displayed above the HRF logo -#: src/seedsigner/views/screensaver.py:74 +#: src/seedsigner/views/screensaver.py msgid "With support from:" msgstr "Com apoio de:" -#: src/seedsigner/views/seed_views.py:53 +#: src/seedsigner/views/seed_views.py msgid "Load a seed" msgstr "Adicionar Seed" -#: src/seedsigner/views/seed_views.py:56 +#: src/seedsigner/views/seed_views.py msgid "In-Memory Seeds" msgstr "Seeds na memória" -#: src/seedsigner/views/seed_views.py:77 +#: src/seedsigner/views/seed_views.py +msgid "Load the seed to verify" +msgstr "Adicionar Seed para verificar" + +#: src/seedsigner/views/seed_views.py +msgid "Select seed to verify" +msgstr "Selecione a Seed para verificar" + +#: src/seedsigner/views/seed_views.py +msgid "Load the seed to sign with" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Select seed to sign with" +msgstr "" + +#: src/seedsigner/views/seed_views.py msgid "Scan a SeedQR" msgstr "Ler uma SeedQR" -#: src/seedsigner/views/seed_views.py:78 -msgid "Enter 24-word seed" -msgstr "Digitar 24 palavras" - -#: src/seedsigner/views/seed_views.py:79 -msgid "Enter 12-word seed" -msgstr "Digitar 12 palavras" - -#: src/seedsigner/views/seed_views.py:80 +#: src/seedsigner/views/seed_views.py msgid "Create a seed" msgstr "Criar uma Seed" -#: src/seedsigner/views/seed_views.py:89 +#: src/seedsigner/views/seed_views.py msgid "Load A Seed" msgstr "Adicionar Seed" #. Inserts the word number (e.g. "Seed Word #6") -#: src/seedsigner/views/seed_views.py:126 +#: src/seedsigner/views/seed_views.py msgid "Seed Word #{}" msgstr "{}ª Palavra da Seed" -#: src/seedsigner/views/seed_views.py:187 +#: src/seedsigner/views/seed_views.py msgid "Review & Edit" msgstr "Revisar e Editar" -#: src/seedsigner/views/seed_views.py:188 -#: src/seedsigner/views/seed_views.py:310 -#: src/seedsigner/views/tools_views.py:259 +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py msgid "Discard" msgstr "Descartar" -#: src/seedsigner/views/seed_views.py:192 +#: src/seedsigner/views/seed_views.py msgid "Invalid Mnemonic!" msgstr "Mnemônico Inválido!" -#: src/seedsigner/views/seed_views.py:194 +#: src/seedsigner/views/seed_views.py msgid "Checksum failure; not a valid seed phrase." msgstr "Falha no Checksum; Palavras da Seed inválidas." -#: src/seedsigner/views/seed_views.py:216 -#: src/seedsigner/views/seed_views.py:268 -#: src/seedsigner/views/seed_views.py:797 -#: src/seedsigner/views/seed_views.py:1146 +#: src/seedsigner/views/seed_views.py src/seedsigner/views/view.py msgid "Done" msgstr "Terminar" -#: src/seedsigner/views/seed_views.py:267 +#: src/seedsigner/views/seed_views.py msgid "Edit passphrase" msgstr "Editar passphrase" -#: src/seedsigner/views/seed_views.py:309 +#: src/seedsigner/views/seed_views.py msgid "Keep Seed" msgstr "Manter Chave" -#: src/seedsigner/views/seed_views.py:315 +#: src/seedsigner/views/seed_views.py msgid "Discard Seed?" msgstr "Descartar Seed?" -#: src/seedsigner/views/seed_views.py:317 +#: src/seedsigner/views/seed_views.py msgid "Wipe seed {} from the device?" msgstr "Limpar Seed {} do dispositivo?" -#: src/seedsigner/views/seed_views.py:351 +#: src/seedsigner/views/seed_views.py msgid "Scan PSBT" msgstr "Ler PSBT" -#: src/seedsigner/views/seed_views.py:353 -#: src/seedsigner/views/seed_views.py:1646 +#: src/seedsigner/views/seed_views.py msgid "Verify Addr" msgstr "Verificar Endereço" -#: src/seedsigner/views/seed_views.py:355 -#: src/seedsigner/views/seed_views.py:436 -msgid "Backup Seed" -msgstr "Backup da Seed" +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Child Seed" +msgstr "" -#: src/seedsigner/views/seed_views.py:356 +#: src/seedsigner/views/seed_views.py msgid "Discard Seed" msgstr "Descartar Seed" -#: src/seedsigner/views/seed_views.py:431 +#: src/seedsigner/views/seed_views.py msgid "View Seed Words" msgstr "Ver Palavras da Seed" -#: src/seedsigner/views/seed_views.py:432 +#: src/seedsigner/views/seed_views.py msgid "Export as SeedQR" msgstr "Exportar como SeedQR" -#: src/seedsigner/views/seed_views.py:620 -msgid "Privacy Leak!" -msgstr "Vazamento de privacidade!" - -#: src/seedsigner/views/seed_views.py:621 +#: src/seedsigner/views/seed_views.py msgid "Xpub can be used to view all future transactions." msgstr "O Xpub pode ser usado para visualizar todas as transações futuras." -#: src/seedsigner/views/seed_views.py:651 +#: src/seedsigner/views/seed_views.py msgid "Generating xpub..." msgstr "Gerando Xpub..." -#: src/seedsigner/views/seed_views.py:771 +#: src/seedsigner/views/seed_views.py msgid "You must keep your seed words private & away from all online devices." +msgstr "Você deve manter suas palavras da Seed em sigilo e longe de todos os dispositivos online." + +#: src/seedsigner/views/seed_views.py +msgid "Child #{}" msgstr "" -"Você deve manter suas palavras da Seed em sigilo e longe de todos os " -"dispositivos online." -#: src/seedsigner/views/seed_views.py:836 -msgid "Verify" -msgstr "Verificar" +#: src/seedsigner/views/seed_views.py +msgid "Seed Words" +msgstr "" -#: src/seedsigner/views/seed_views.py:837 -msgid "Skip" -msgstr "Pular" +#: src/seedsigner/views/seed_views.py +msgid "12 Words" +msgstr "" -#: src/seedsigner/views/seed_views.py:886 -msgid "Verify Word #{}" -msgstr "Verificar Palavra nº {}" +#: src/seedsigner/views/seed_views.py +msgid "24 Words" +msgstr "" -#: src/seedsigner/views/seed_views.py:926 -msgid "Review Seed Words" -msgstr "Revisar Palavras da Seed" +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Num Words" +msgstr "" -#: src/seedsigner/views/seed_views.py:927 +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Index Error" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Invalid Child Index" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Child Index must be between 0 and 2^31-1." +msgstr "" + +#: src/seedsigner/views/seed_views.py msgid "Try Again" msgstr "Tentar de novo" -#: src/seedsigner/views/seed_views.py:931 +#: src/seedsigner/views/seed_views.py +msgid "Verify" +msgstr "Verificar" + +#: src/seedsigner/views/seed_views.py +msgid "Skip" +msgstr "Pular" + +#: src/seedsigner/views/seed_views.py +msgid "Verify Word #{}" +msgstr "Verificar Palavra nº {}" + +#: src/seedsigner/views/seed_views.py +msgid "Review Seed Words" +msgstr "Revisar Palavras da Seed" + +#: src/seedsigner/views/seed_views.py msgid "Verification Error" msgstr "Erro na Verificação" -#: src/seedsigner/views/seed_views.py:933 +#: src/seedsigner/views/seed_views.py msgid "Wrong Word!" msgstr "Palavra Errada!" -#: src/seedsigner/views/seed_views.py:934 +#: src/seedsigner/views/seed_views.py msgid "Word #{} is not \"{}\"!" msgstr "Palavra nº{} não é \"{}\"!" -#: src/seedsigner/views/seed_views.py:952 +#: src/seedsigner/views/seed_views.py msgid "Backup Verified" msgstr "Backup Verificado" -#: src/seedsigner/views/seed_views.py:955 +#: src/seedsigner/views/seed_views.py msgid "All mnemonic backup words were successfully verified!" msgstr "Todas as palavras do backup mnemônico foram verificadas com sucesso!" -#: src/seedsigner/views/seed_views.py:978 +#: src/seedsigner/views/seed_views.py msgid "Standard: 25x25" msgstr "Padrão: 25x25" -#: src/seedsigner/views/seed_views.py:979 +#: src/seedsigner/views/seed_views.py msgid "Compact: 21x21" msgstr "Compacto: 21x21" -#: src/seedsigner/views/seed_views.py:983 +#: src/seedsigner/views/seed_views.py msgid "Standard: 29x29" msgstr "Padrão: 29x29" -#: src/seedsigner/views/seed_views.py:984 +#: src/seedsigner/views/seed_views.py msgid "Compact: 25x25" msgstr "Compacto: 25x25" -#: src/seedsigner/views/seed_views.py:1003 +#: src/seedsigner/views/seed_views.py msgid "SeedQR Format" msgstr "Formato da SeedQR" -#: src/seedsigner/views/seed_views.py:1052 -msgid "SeedQR is your private key!" -msgstr "A SeedQR é sua chave privada!" +#: src/seedsigner/views/seed_views.py +msgid "Never photograph or scan it into a device that connects to the internet." +msgstr "" -#: src/seedsigner/views/seed_views.py:1053 -msgid "Never photograph it or scan it into an online device." -msgstr "Nunca fotografe ou leia em um dispositivo online." - -#: src/seedsigner/views/seed_views.py:1145 -#: src/seedsigner/views/seed_views.py:1189 -#: src/seedsigner/views/seed_views.py:1200 -#: src/seedsigner/views/seed_views.py:1212 +#: src/seedsigner/views/seed_views.py msgid "Confirm SeedQR" msgstr "Confirmar SeedQR" -#: src/seedsigner/views/seed_views.py:1150 +#: src/seedsigner/views/seed_views.py msgid "Confirm SeedQR?" msgstr "Confirmar SeedQR?" -#: src/seedsigner/views/seed_views.py:1180 +#: src/seedsigner/views/seed_views.py msgid "Scan your SeedQR" msgstr "Ler sua SeedQR" -#: src/seedsigner/views/seed_views.py:1190 -#: src/seedsigner/views/seed_views.py:1213 +#: src/seedsigner/views/seed_views.py msgid "Error!" msgstr "Erro!" -#: src/seedsigner/views/seed_views.py:1191 +#: src/seedsigner/views/seed_views.py msgid "Your transcribed SeedQR does not match your original seed!" msgstr "Sua SeedQR desenhada não corresponde à sua chave original!" -#: src/seedsigner/views/seed_views.py:1193 -#: src/seedsigner/views/seed_views.py:1216 +#: src/seedsigner/views/seed_views.py msgid "Review SeedQR" msgstr "Revisar SeedQR" -#: src/seedsigner/views/seed_views.py:1202 -msgid "" -"Your transcribed SeedQR successfully scanned and yielded the same seed." +#: src/seedsigner/views/seed_views.py +msgid "Your transcribed SeedQR successfully scanned and yielded the same seed." msgstr "Sua SeedQR desenhada foi lida com sucesso e gera a mesma Seed." -#: src/seedsigner/views/seed_views.py:1214 +#: src/seedsigner/views/seed_views.py msgid "Your transcribed SeedQR could not be read!" msgstr "Sua SeedQR desenhada não pôde ser lida!" -#: src/seedsigner/views/seed_views.py:1285 +#: src/seedsigner/views/seed_views.py msgid "Sig type can't be auto-detected from this address. Please specify:" -msgstr "" -"Tipo de Sig não detectado a partir deste endereço. Por favor, especifique:" +msgstr "Tipo de Sig não detectado a partir deste endereço. Por favor, especifique:" -#: src/seedsigner/views/seed_views.py:1326 -msgid "Load the seed to verify" -msgstr "Adicionar Seed para verificar" - -#: src/seedsigner/views/seed_views.py:1336 -msgid "Select seed to verify" -msgstr "Selecione a Seed para verificar" - -#: src/seedsigner/views/seed_views.py:1388 +#: src/seedsigner/views/seed_views.py msgid "Can't validate a single sig addr without specifying a seed" msgstr "Impossível validar um end. sem especificar uma Seed" #. Option when scanning for a matching address; skips ten addresses ahead -#: src/seedsigner/views/seed_views.py:1434 +#: src/seedsigner/views/seed_views.py msgid "Skip 10" msgstr "Pular 10" -#: src/seedsigner/views/seed_views.py:1435 -#: src/seedsigner/views/seed_views.py:1616 +#: src/seedsigner/views/seed_views.py msgid "Cancel" msgstr "Cancelar" -#: src/seedsigner/views/seed_views.py:1563 -#: src/seedsigner/views/seed_views.py:1579 -msgid "Taproot verification not yet implemented!" -msgstr "Verificação Taproot ainda não implementada!" - -#: src/seedsigner/views/seed_views.py:1575 -msgid "Legacy P2PKH verification not implemented!" -msgstr "Verificação P2PKH Legacy não implementada!" - -#: src/seedsigner/views/seed_views.py:1600 +#: src/seedsigner/views/seed_views.py msgid "multisig" msgstr "multisig" -#: src/seedsigner/views/seed_views.py:1602 +#: src/seedsigner/views/seed_views.py msgid "seed {}" msgstr "Seed {}" -#: src/seedsigner/views/seed_views.py:1605 +#: src/seedsigner/views/seed_views.py msgid "Address Verified" msgstr "Endereço Verificado" -#: src/seedsigner/views/seed_views.py:1615 +#: src/seedsigner/views/seed_views.py msgid "Scan Descriptor" msgstr "Ler Descriptor" -#: src/seedsigner/views/seed_views.py:1645 +#: src/seedsigner/views/seed_views.py msgid "Return to PSBT" msgstr "Retornar ao PSBT" -#: src/seedsigner/views/settings_views.py:20 +#: src/seedsigner/views/settings_views.py msgid "I/O test" msgstr "Testar Botões/Câmera" -#: src/seedsigner/views/settings_views.py:39 -#: src/seedsigner/views/settings_views.py:46 +#: src/seedsigner/views/settings_views.py msgid "Advanced" msgstr "Avançado" -#: src/seedsigner/views/settings_views.py:54 +#: src/seedsigner/views/settings_views.py msgid "Dev Options" msgstr "Opções de Desenvolvedor" -#: src/seedsigner/views/tools_views.py:24 -msgid " New seed" -msgstr "Nova Seed" +#: src/seedsigner/views/settings_views.py +msgid "Persistent Settings enabled. Settings saved to SD card." +msgstr "" -#: src/seedsigner/views/tools_views.py:25 +#: src/seedsigner/views/settings_views.py +msgid "Settings updated in temporary memory" +msgstr "" + +#: src/seedsigner/views/tools_views.py msgid "New seed" msgstr "Nova Seed" -#: src/seedsigner/views/tools_views.py:26 +#: src/seedsigner/views/tools_views.py msgid "Calc 12th/24th word" msgstr "Calc. 12ª/24ª palavra" -#: src/seedsigner/views/tools_views.py:102 -#: src/seedsigner/views/tools_views.py:228 +#: src/seedsigner/views/tools_views.py src/seedsigner/views/view.py +msgid "Tools" +msgstr "Recursos" + +#: src/seedsigner/views/tools_views.py msgid "12 words" msgstr "12 palavras" -#: src/seedsigner/views/tools_views.py:103 -#: src/seedsigner/views/tools_views.py:229 +#: src/seedsigner/views/tools_views.py msgid "24 words" msgstr "24 palavras" -#: src/seedsigner/views/tools_views.py:107 +#: src/seedsigner/views/tools_views.py msgid "Mnemonic Length?" msgstr "Tamanho do Mnemônico?" -#: src/seedsigner/views/tools_views.py:174 +#: src/seedsigner/views/tools_views.py msgid "12 words (50 rolls)" msgstr "12 palavras (50 jogadas)" -#: src/seedsigner/views/tools_views.py:175 +#: src/seedsigner/views/tools_views.py msgid "24 words (99 rolls)" msgstr "24 palavras (99 jogadas)" -#: src/seedsigner/views/tools_views.py:179 -#: src/seedsigner/views/tools_views.py:233 +#: src/seedsigner/views/tools_views.py msgid "Mnemonic Length" msgstr "Tamanho do Mnemônico" -#: src/seedsigner/views/tools_views.py:258 +#. Label to gather entropy through coin tosses +#: src/seedsigner/views/tools_views.py +msgid "Coin flip entropy" +msgstr "" + +#. Label to gather entropy through user specified BIP-39 word +#: src/seedsigner/views/tools_views.py +msgid "Word selection entropy" +msgstr "" + +#. Label to allow user to default entropy as all-zeros +#: src/seedsigner/views/tools_views.py +msgid "Finalize with zeros" +msgstr "" + +#. label to calculate the last word of a BIP-39 mnemonic seed phrase +#: src/seedsigner/views/tools_views.py +msgid "Final Word Calc" +msgstr "" + +#: src/seedsigner/views/tools_views.py msgid "Load seed" msgstr "Adicionar Seed" -#: src/seedsigner/views/view.py:124 +#. label for addresses where others send us incoming payments +#: src/seedsigner/views/tools_views.py +msgid "Receive Addresses" +msgstr "" + +#. label for addresses that collect the change from our own outgoing payments +#: src/seedsigner/views/tools_views.py +msgid "Change Addresses" +msgstr "" + +#. a status message that our payment addresses are being calculated +#: src/seedsigner/views/tools_views.py +msgid "Calculating addrs..." +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Next {}" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Receive Addrs" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Change Addrs" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Scan" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Seeds" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Restart" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Power Off" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Reset / Power" +msgstr "" + +#: src/seedsigner/views/view.py msgid "Work In Progress" msgstr "Trabalhando Nisso" -#: src/seedsigner/views/view.py:125 +#: src/seedsigner/views/view.py msgid "Not Yet Implemented" msgstr "Ainda Não Implementado" -#: src/seedsigner/views/view.py:126 +#: src/seedsigner/views/view.py msgid "This is still on our to-do list!" msgstr "Isto está na nossa lista de tarefas!" -#: src/seedsigner/views/view.py:127 +#: src/seedsigner/views/view.py msgid "Back to Main Menu" msgstr "Voltar ao Menu Principal" -#: src/seedsigner/views/view.py:142 +#: src/seedsigner/views/view.py +msgid "Error" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Network Mismatch" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Change Setting" +msgstr "" + +#: src/seedsigner/views/view.py msgid "System Error" msgstr "Erro no Sistema" + +#: src/seedsigner/views/view.py +msgid "Update Setting" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Option Disabled" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Security Tip" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "" +"For maximum security,\n" +"remove the MicroSD card\n" +"before continuing." +msgstr "" + +#~ msgid "{}..." +#~ msgstr "{}..." + +#~ msgid "Click to authorize this transaction" +#~ msgstr "Clique para autorizar transação" + +#~ msgid "Signed PSBT" +#~ msgstr "PSBT assinado" + +#~ msgid "Export as a QR code for:" +#~ msgstr "Exportar como QR code para:" + +#~ msgid "changes fingerprint" +#~ msgstr "muda o fingerprint" + +#~ msgid "Enter 12/24 words" +#~ msgstr "Digitar 12/24 palavras" + +#~ msgid "Signing PSBT..." +#~ msgstr "Assinar PSBT" + +#~ msgid "Taproot verification not yet implemented!" +#~ msgstr "Verificação Taproot ainda não implementada!" + +#~ msgid "Legacy P2PKH verification not implemented!" +#~ msgstr "Verificação P2PKH Legacy não implementada!" + +#~ msgid "Add Passphrase" +#~ msgstr "Adicionar Passphrase" + +#~ msgid "Btc-only" +#~ msgstr "Somente BTC" + +#~ msgid "Sats-only" +#~ msgstr "Somente sats" + +#~ msgid "Threshold at 0.01" +#~ msgstr "Limiar em 0,01" + +#~ msgid "Btc | Sats hybrid" +#~ msgstr "Btc | Sats híbrido" + +#~ msgid "Scan" +#~ msgstr "" + +#~ msgid "Seeds" +#~ msgstr "" + +#~ msgid "Restart" +#~ msgstr "" + +#~ msgid "Power Off" +#~ msgstr "" + +#~ msgid "Reset / Power" +#~ msgstr "Reiniciar / Desligar" + +#~ msgid " (?)" +#~ msgstr "" + +#~ msgid "Scan a QR code" +#~ msgstr "Ler QR code" + +#~ msgid "Yes " +#~ msgstr "" + +#~ msgid "No " +#~ msgstr "" + +#~ msgid "< back | Scan a QR code" +#~ msgstr "" + +#~ msgid "Settings QR" +#~ msgstr "Ajustes via QR" + +#~ msgid "Settings imported successfully!" +#~ msgstr "Ajustes importados com sucesso!" + +#~ msgid "MicroSD removed" +#~ msgstr "" + +#~ msgid "MicroSD inserted" +#~ msgstr "" + +#~ msgid "changes seed:" +#~ msgstr "" + +#~ msgid "< back | click joystick" +#~ msgstr "< voltar | clique p/ foto" + +#~ msgid "Store Settings on SD card." +#~ msgstr "" + +#~ msgid "MicroSD card is removed" +#~ msgstr "" + +#~ msgid "Show passphrase warning" +#~ msgstr "" + +#~ msgid "" +#~ "BIP-39 passphrase changes your seed, resulting in new xpubs, wallets, etc.\n" +#~ "Be sure you know what you're doing!" +#~ msgstr "" + +#~ msgid "SeedQR is your private key!" +#~ msgstr "A SeedQR é sua chave privada!" + +#~ msgid "Never photograph it or scan it into an online device." +#~ msgstr "Nunca fotografe ou leia em um dispositivo online." + +#~ msgid " New seed" +#~ msgstr "Nova Seed" + +#~ msgid "Scan wallet descriptor" +#~ msgstr "" + diff --git a/src/seedsigner/resources/babel/ru/LC_MESSAGES/messages.mo b/src/seedsigner/resources/babel/ru/LC_MESSAGES/messages.mo deleted file mode 100644 index b8f3a1bf..00000000 Binary files a/src/seedsigner/resources/babel/ru/LC_MESSAGES/messages.mo and /dev/null differ diff --git a/src/seedsigner/resources/babel/ru/LC_MESSAGES/messages.po b/src/seedsigner/resources/babel/ru/LC_MESSAGES/messages.po index c9dc716d..9cad28b6 100644 --- a/src/seedsigner/resources/babel/ru/LC_MESSAGES/messages.po +++ b/src/seedsigner/resources/babel/ru/LC_MESSAGES/messages.po @@ -1,37 +1,210 @@ -# Translations template for PROJECT. +# Russian translations for PROJECT. # Copyright (C) 2022 ORGANIZATION # This file is distributed under the same license as the PROJECT project. # FIRST AUTHOR , 2022. -# +# # Translators: # Daniel Babaev, 2022 -# +# jdlcdl, 2022 #, fuzzy msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-05-10 17:32-0500\n" +"POT-Creation-Date: 2023-10-09 11:36-0400\n" "PO-Revision-Date: 2022-04-29 13:50+0000\n" "Last-Translator: Daniel Babaev, 2022\n" -"Language-Team: Russian (https://www.transifex.com/seedsigner/teams/135832/ru/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.10.1\n" "Language: ru\n" +"Language-Team: Russian (https://www.transifex.com/seedsigner/teams/135832/ru/)\n" "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.10.3\n" -#: src/seedsigner/gui/screens/main_menu_screens.py:12 -#: src/seedsigner/gui/screens/scan_screens.py:140 -msgid "Home" -msgstr "Главная" +#. The abbreviated label for the special key on a standard keyboard. +#: src/seedsigner/gui/keyboard.py +msgid "del" +msgstr "" -#: src/seedsigner/gui/screens/main_menu_screens.py:24 +#. The abbreviated label for the special key on a standard keyboard. +#: src/seedsigner/gui/keyboard.py +msgid "space" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "" +"You can remove\n" +"the SD card now" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "SD card removed" +msgstr "" + +#: src/seedsigner/gui/toast.py +msgid "SD card inserted" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review PSBT" +msgstr "Обзор PSBT" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review Details" +msgstr "Обзор Детали" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "1 input" +msgstr "1 вход" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input 1" +msgstr "вход 1" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input 2" +msgstr "вход 2" + +#. Indicates that items have been omitted from a series: e.g. "1, 2, 3, [...], +#. 8" +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "[ ... ]" +msgstr "[ ... ]" + +#. Input number will be inserted (e.g. "input 3") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input {}" +msgstr "вход {}" + +#. Ellipsis ("...") characters used to truncate an address (e.g. "bc1qabc...") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "..." +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/views/psbt_views.py +msgid "self-transfer" +msgstr "самоперенос" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient 1" +msgstr "получатель 1" + +#. Inserts the recipient number (e.g. the fifth one is: "recipient 5") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient {}" +msgstr "получатель {}" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "fee" +msgstr "платеж" + +#. Label for a change output in the PSBT Overview flow diagram +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/views/psbt_views.py +msgid "change" +msgstr "сдача" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "PSBT Math" +msgstr "Математика PSBT" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Review Recipients" +msgstr "Обзор Получателей" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "btc" +msgstr "btc" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "sats" +msgstr "sats" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "input" +msgid_plural "inputs" +msgstr[0] "вход" +msgstr[1] "входа" +msgstr[2] "входы" +msgstr[3] "входы" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "recipient" +msgid_plural "recipients" +msgstr[0] "получатель" +msgstr[1] "получателя" +msgstr[2] "получатели" +msgstr[3] "получатели" + +#. Denonination is inserted (e.g. your "btc change" or "sats change") +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "{} change" +msgstr "{} сдача" + +#. First variable is either "Multisig" or the fingerprint for single sig; +#. Second is "Change" or "Addr" (change vs receive addr); Third is the address +#. index number (e.g. your #4 receive addr). +#: src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/models/settings_definition.py +#: src/seedsigner/views/seed_views.py +msgid "Multisig" +msgstr "Мультиподпись" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Change" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Addr" +msgstr "" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Address verified!" +msgstr "Адрес подтвержден!" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Sign PSBT" +msgstr "Подписать PSBT" + +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "Click to approve this transaction" +msgstr "" + +#: src/seedsigner/gui/screens/scan_screens.py +msgid "< back | " +msgstr "" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "Success!" +msgstr "Успех!" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "OK" +msgstr "ОК" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/psbt_views.py +msgid "Caution" +msgstr "Осторожно" + +#: src/seedsigner/gui/screens/screen.py src/seedsigner/views/seed_views.py +msgid "Privacy Leak!" +msgstr "Утечка Конфиден.!" + +#: src/seedsigner/gui/screens/screen.py +msgid "I Understand" +msgstr "Я Понимаю" + +#: src/seedsigner/gui/screens/screen.py +msgid "Classified Info!" +msgstr "Секретная инфо.!" + +#: src/seedsigner/gui/screens/screen.py msgid "Restarting" msgstr "Перезагрузка " -#: src/seedsigner/gui/screens/main_menu_screens.py:29 +#: src/seedsigner/gui/screens/screen.py msgid "" "SeedSigner is restarting.\n" "\n" @@ -41,358 +214,225 @@ msgstr "" "\n" "Все данные в памяти будут удалены." -#: src/seedsigner/gui/screens/main_menu_screens.py:39 +#: src/seedsigner/gui/screens/screen.py msgid "Powering Off" msgstr "Выключение" -#: src/seedsigner/gui/screens/main_menu_screens.py:44 +#: src/seedsigner/gui/screens/screen.py msgid "Please wait about 30 seconds before disconnecting power." msgstr "Пожалуйста подождите около 30 секунд прежде чем отключить питание." -#: src/seedsigner/gui/screens/psbt_screens.py:29 -#: src/seedsigner/views/seed_views.py:352 -msgid "Review PSBT" -msgstr "Обзор PSBT" - -#: src/seedsigner/gui/screens/psbt_screens.py:31 -msgid "Review Details" -msgstr "Обзор Детали" - -#: src/seedsigner/gui/screens/psbt_screens.py:88 -msgid "1 input" -msgstr "1 вход" - -#: src/seedsigner/gui/screens/psbt_screens.py:90 -msgid "input 1" -msgstr "вход 1" - -#: src/seedsigner/gui/screens/psbt_screens.py:91 -msgid "input 2" -msgstr "вход 2" - -#. Indicates that items have been omitted from a series: e.g. "1, 2, 3, [...], -#. 8" -#: src/seedsigner/gui/screens/psbt_screens.py:93 -#: src/seedsigner/gui/screens/psbt_screens.py:141 -msgid "[ ... ]" -msgstr "[ ... ]" - -#. Input number will be inserted (e.g. "input 3") -#: src/seedsigner/gui/screens/psbt_screens.py:95 -#: src/seedsigner/gui/screens/psbt_screens.py:96 -#: src/seedsigner/gui/screens/psbt_screens.py:99 -msgid "input {}" -msgstr "вход {}" - -#. Display a truncated address (e.g. "bc1qabc...") -#: src/seedsigner/gui/screens/psbt_screens.py:127 -msgid "{}..." -msgstr "{}..." - -#: src/seedsigner/gui/screens/psbt_screens.py:136 -msgid "self-transfer" -msgstr "самоперенос" - -#: src/seedsigner/gui/screens/psbt_screens.py:139 -msgid "recipient 1" -msgstr "получатель 1" - -#. Inserts the recipient number (e.g. the fifth one is: "recipient 5") -#: src/seedsigner/gui/screens/psbt_screens.py:143 -msgid "recipient {}" -msgstr "получатель {}" - -#: src/seedsigner/gui/screens/psbt_screens.py:145 -#: src/seedsigner/gui/screens/psbt_screens.py:555 -msgid "fee" -msgstr "платеж" - -#. Label for a change output in the PSBT Overview flow diagram -#: src/seedsigner/gui/screens/psbt_screens.py:150 -msgid "change" -msgstr "сдача" - -#: src/seedsigner/gui/screens/psbt_screens.py:461 -msgid "PSBT Math" -msgstr "Математика PSBT" - -#: src/seedsigner/gui/screens/psbt_screens.py:462 -msgid "Review Recipients" -msgstr "Обзор Получателей" - -#: src/seedsigner/gui/screens/psbt_screens.py:468 -#: src/seedsigner/gui/screens/psbt_screens.py:521 -msgid "btc" -msgstr "btc" - -#: src/seedsigner/gui/screens/psbt_screens.py:480 -msgid "sats" -msgstr "sats" - -#: src/seedsigner/gui/screens/psbt_screens.py:538 -msgid "input" -msgid_plural "inputs" -msgstr[0] "вход" -msgstr[1] "входа" -msgstr[2] "входы" -msgstr[3] "входы" - -#: src/seedsigner/gui/screens/psbt_screens.py:548 -msgid "recipient" -msgid_plural "recipients" -msgstr[0] "получатель" -msgstr[1] "получателя" -msgstr[2] "получатели" -msgstr[3] "получатели" - -#. Denonination is inserted (e.g. your "btc change" or "sats change") -#: src/seedsigner/gui/screens/psbt_screens.py:566 -msgid "{} change" -msgstr "{} сдача" - -#: src/seedsigner/gui/screens/psbt_screens.py:671 -msgid "Address verified!" -msgstr "Адрес подтвержден!" - -#: src/seedsigner/gui/screens/psbt_screens.py:683 -msgid "Sign PSBT" -msgstr "Подписать PSBT" - -#: src/seedsigner/gui/screens/psbt_screens.py:697 -msgid "Click to authorize this transaction" -msgstr "Нажмите, чтобы авторизовать эту транзакцию" - -#: src/seedsigner/gui/screens/psbt_screens.py:707 -msgid "Signed PSBT" -msgstr "Подписанная PSBT" - -#. Prompt to specify which coordinator software to use (e.g. Specter, Sparrow, -#. etc) -#: src/seedsigner/gui/screens/psbt_screens.py:713 -msgid "Export as a QR code for:" -msgstr "Экспорт. в виде QR-кода для:" - -#: src/seedsigner/gui/screens/scan_screens.py:22 -msgid "Scan a QR code" -msgstr "Сканировать QR-код" - -#: src/seedsigner/gui/screens/scan_screens.py:138 -msgid "Settings QR" -msgstr "Настр. QR" - -#: src/seedsigner/gui/screens/scan_screens.py:156 -msgid "Settings imported successfully!" -msgstr "Настройки успешно импортированы!" - -#: src/seedsigner/gui/screens/screen.py:731 -#: src/seedsigner/views/seed_views.py:954 -#: src/seedsigner/views/seed_views.py:1201 -msgid "Success!" -msgstr "Успех!" - -#: src/seedsigner/gui/screens/screen.py:735 -#: src/seedsigner/views/seed_views.py:956 -#: src/seedsigner/views/seed_views.py:1204 -#: src/seedsigner/views/seed_views.py:1647 src/seedsigner/views/view.py:145 -msgid "OK" -msgstr "ОК" - -#: src/seedsigner/gui/screens/screen.py:852 -#: src/seedsigner/views/psbt_views.py:430 -msgid "Caution" -msgstr "Осторожно" - -#: src/seedsigner/gui/screens/screen.py:855 -msgid "I Understand" -msgstr "Я Понимаю" - -#: src/seedsigner/gui/screens/screen.py:867 -msgid "Classified Info!" -msgstr "Секретная инфо.!" - -#: src/seedsigner/gui/screens/seed_screens.py:411 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Finalize Seed" msgstr "Завершить Ключ" -#: src/seedsigner/gui/screens/seed_screens.py:424 -#: src/seedsigner/gui/screens/tools_screens.py:291 +#. a label for the shortened Key-id of a BIP-32 master HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "fingerprint" msgstr "отпечаток пальца" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Backup Seed" +msgstr "Резервный Ключ" + +#. Additional explainer for the two seed backup options (mnemonic phrase and +#. SeedQR). +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Backups do not include your passphrase." +msgstr "" + #. Displays the page number and total: (e.g. page 1 of 6) -#: src/seedsigner/gui/screens/seed_screens.py:461 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Seed Words: {}/{}" msgstr "Ключевые Слова: {}/{}" -#: src/seedsigner/gui/screens/seed_screens.py:541 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "BIP-85 Index" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py msgid "Verify Backup?" msgstr "Подтвердить резервную копию?" -#: src/seedsigner/gui/screens/seed_screens.py:547 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Optionally verify that your mnemonic backup is correct." -msgstr "" -"При необходимости, подтвердите правильность резервной копии мнемоники." +msgstr "При необходимости, подтвердите правильность резервной копии мнемоники." -#: src/seedsigner/gui/screens/seed_screens.py:556 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Derivation Path" msgstr "Путь Вывода" -#: src/seedsigner/gui/screens/seed_screens.py:703 -msgid "Xpub Details" -msgstr "Детали Xpub" - -#: src/seedsigner/gui/screens/seed_screens.py:709 -#: src/seedsigner/gui/screens/seed_screens.py:713 -#: src/seedsigner/views/seed_views.py:354 -#: src/seedsigner/views/seed_views.py:471 -#: src/seedsigner/views/seed_views.py:504 -#: src/seedsigner/views/seed_views.py:578 +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py msgid "Export Xpub" msgstr "Экспорт Xpub" -#: src/seedsigner/gui/screens/seed_screens.py:722 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Xpub Details" +msgstr "Детали Xpub" + +#. Short for "BIP32 Master Fingerprint" +#. a label for the shortened Key-id of a BIP-32 master HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Fingerprint" msgstr "Отпечаток пальца" #. Short for "Derivation Path" -#: src/seedsigner/gui/screens/seed_screens.py:732 +#. a label for the derivation-path into a BIP-32 HD wallet +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Derivation" msgstr "Вывод" -#: src/seedsigner/gui/screens/seed_screens.py:741 +#. Short for "BIP32 Extended Public Key" +#: src/seedsigner/gui/screens/seed_screens.py msgid "Xpub" msgstr "Xpub" -#: src/seedsigner/gui/screens/seed_screens.py:754 -#: src/seedsigner/views/seed_views.py:217 -msgid "Add Passphrase" -msgstr "Добавить Парольную Фразу" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "BIP-39 Passphrase" +msgstr "" -#: src/seedsigner/gui/screens/seed_screens.py:1106 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Verify Passphrase" msgstr "Подтвердить Парольную Фразу" #. Describes the effect of applying a BIP-39 passphrase; it changes the seed's #. fingerprint -#: src/seedsigner/gui/screens/seed_screens.py:1115 +#: src/seedsigner/gui/screens/seed_screens.py msgid "changes fingerprint" -msgstr "меняет отпечаток пальца" +msgstr "" #. Refers to the SeedQR type: Standard or Compact -#: src/seedsigner/gui/screens/seed_screens.py:1170 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Standard" msgstr "Стандарт" #. Briefly explains the Standard SeedQR data format -#: src/seedsigner/gui/screens/seed_screens.py:1172 +#: src/seedsigner/gui/screens/seed_screens.py msgid "BIP-39 wordlist indices" msgstr "Индексы списка слов BIP-39" #. Refers to the SeedQR type: Standard or Compact -#: src/seedsigner/gui/screens/seed_screens.py:1180 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Compact" msgstr "Компактный" #. Briefly explains the Compact SeedQR data format -#: src/seedsigner/gui/screens/seed_screens.py:1182 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Raw entropy bits" msgstr "Необработанные биты энтропии" -#: src/seedsigner/gui/screens/seed_screens.py:1196 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Transcribe SeedQR" msgstr "Переписать SeedQR" #. Refers to the QR code size: 21x21, 25x25, or 29x29 -#: src/seedsigner/gui/screens/seed_screens.py:1198 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Begin {}x{}" msgstr "Начать {}х{}" -#: src/seedsigner/gui/screens/seed_screens.py:1275 +#: src/seedsigner/gui/screens/seed_screens.py msgid "click to exit" msgstr "нажмите, чтобы выйти" -#: src/seedsigner/gui/screens/seed_screens.py:1411 -msgid "" -"Optionally scan your transcribed SeedQR to confirm that it reads back " -"correctly." -msgstr "" -"При желании отсканируйте переписанный SeedQR, чтобы убедиться, что он " -"правильно считывается." +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Optionally scan your transcribed SeedQR to confirm that it reads back correctly." +msgstr "При желании отсканируйте переписанный SeedQR, чтобы убедиться, что он правильно считывается." -#: src/seedsigner/gui/screens/seed_screens.py:1468 -#: src/seedsigner/views/seed_views.py:1284 -#: src/seedsigner/views/seed_views.py:1342 +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py msgid "Verify Address" msgstr "Подтвердить Адрес" #. Inserts the nth address number (e.g. "Checking address 7") -#: src/seedsigner/gui/screens/seed_screens.py:1527 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Checking address {}" msgstr "Проверка адреса {}" -#: src/seedsigner/gui/screens/seed_screens.py:1544 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Multisig Verification" msgstr "Проверка Мультиподписи" -#: src/seedsigner/gui/screens/seed_screens.py:1549 -msgid "" -"Load your multisig wallet descriptor to verify your receive/self-transfer or" -" change address." -msgstr "" -"Загрузите свой дескриптор кошелька с мультиподписью, чтобы подтвердить " -"получение/самостоятельный перевод или изменение адреса." +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Load your multisig wallet descriptor to verify your receive/self-transfer or change address." +msgstr "Загрузите свой дескриптор кошелька с мультиподписью, чтобы подтвердить получение/самостоятельный перевод или изменение адреса." -#: src/seedsigner/gui/screens/seed_screens.py:1562 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Descriptor Loaded" msgstr "Дескриптор Загружен" #. Label for the multisig wallet's signing policy (e.g. 2-of-3) -#: src/seedsigner/gui/screens/seed_screens.py:1568 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Policy" msgstr "Полис" -#: src/seedsigner/gui/screens/seed_screens.py:1576 +#: src/seedsigner/gui/screens/seed_screens.py msgid "Signing Keys" msgstr "Подписание Ключей" -#: src/seedsigner/gui/screens/settings_screens.py:26 -#: src/seedsigner/views/main_menu_views.py:21 -#: src/seedsigner/views/settings_views.py:36 +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Review Message" +msgstr "" + +#. Short for "Next step" +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +#: src/seedsigner/views/tools_views.py +msgid "Next" +msgstr "Дальше" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "Confirm Address" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/views/seed_views.py +msgid "Sign Message" +msgstr "" + +#: src/seedsigner/gui/screens/seed_screens.py +msgid "derivation path" +msgstr "" + +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py msgid "Settings" msgstr "Настр." #. Short for "Input/Output"; screen to make sure the buttons and camera are #. working properly -#: src/seedsigner/gui/screens/settings_screens.py:58 +#: src/seedsigner/gui/screens/settings_screens.py msgid "I/O Test" msgstr "Тест ввода/вывода" #. Blank the screen -#: src/seedsigner/gui/screens/settings_screens.py:129 -#: src/seedsigner/gui/screens/settings_screens.py:140 -#: src/seedsigner/gui/screens/settings_screens.py:221 +#: src/seedsigner/gui/screens/settings_screens.py msgid "Clear" msgstr "Очистить" -#: src/seedsigner/gui/screens/settings_screens.py:161 +#: src/seedsigner/gui/screens/settings_screens.py msgid "Exit" msgstr "Выход" -#: src/seedsigner/gui/screens/settings_screens.py:175 -#: src/seedsigner/gui/screens/tools_screens.py:71 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/gui/screens/tools_screens.py msgid "Capturing image..." msgstr "Захват изображения..." -#: src/seedsigner/gui/screens/settings_screens.py:288 -#: src/seedsigner/views/settings_views.py:21 +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py msgid "Donate" msgstr "Пожертвовать" -#. If your language uses the percent sign ("%"), your translation must also -#. use +#. If your language uses the percent sign ("%"), your translation must also use #. two percent signs ("%%") due to python formatting oddities. "100%%" will be #. rendered as "100%". -#: src/seedsigner/gui/screens/settings_screens.py:293 +#: src/seedsigner/gui/screens/settings_screens.py #, python-format msgid "" "SeedSigner is 100%% free & open source, funded solely by the Bitcoin community.\n" @@ -403,712 +443,992 @@ msgstr "" "\n" "Пожертвуйте onchain или LN по адресу:" -#. Instructions when taking a picture; "back" should point left. -#: src/seedsigner/gui/screens/tools_screens.py:40 -msgid "< back | click joystick" -msgstr "< назад | нажмите джойстик" +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py +msgid "Settings QR" +msgstr "" + +#: src/seedsigner/gui/screens/settings_screens.py +#: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py +msgid "Home" +msgstr "Главная" #. Instructions when reviewing an image; "reshoot" must point left, "accept" #. must point right -#: src/seedsigner/gui/screens/tools_screens.py:98 +#: src/seedsigner/gui/screens/tools_screens.py msgid " < reshoot | accept > " msgstr "< переснять | принять >" #. current roll number vs total rolls (e.g. roll 7 of 50) -#: src/seedsigner/gui/screens/tools_screens.py:120 -#: src/seedsigner/gui/screens/tools_screens.py:245 +#: src/seedsigner/gui/screens/tools_screens.py msgid "Dice Roll {}/{}" msgstr "Бросок Костей {}/{}" -#: src/seedsigner/gui/screens/tools_screens.py:274 +#. Build the last word in a 12 or 24 word BIP-39 mnemonic seed phrase. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Build Final Word" +msgstr "" + +#. Number of BIP-39 seed words, and the entropy -- in bits, contained within. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "The {}th word is built from {} more entropy bits plus auto-calculated checksum." +msgstr "" + +#. current coin-flip number vs total flips (e.g. flip 3 of 4) +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Coin Flip {}/{}" +msgstr "" + +#. How we call the "front" side result during a coin toss. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Heads = 1" +msgstr "" + +#. How we call the "back" side result during a coin toss. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Tails = 0" +msgstr "" + +#. A labeled presentation of a user-supplied value +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Your input: \"{}\"" +msgstr "" + +#. A function of "x" to be used for detecting errors in "x" +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Checksum" +msgstr "" + +#. labeled presentation of the last word in a BIP-39 mnemonic seed phrase. +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Final Word: \"{}\"" +msgstr "" + +#. a label for the last word of a 12-word BIP-39 mnemonic seed phrase +#: src/seedsigner/gui/screens/tools_screens.py msgid "12th Word" msgstr "12-е Слово" -#: src/seedsigner/gui/screens/tools_screens.py:276 +#. a label for the last word of a 24-word BIP-39 mnemonic seed phrase +#: src/seedsigner/gui/screens/tools_screens.py msgid "24th Word" msgstr "24-е Слово" -#: src/seedsigner/models/settings_definition.py:14 +#. a label for the tool to explore public addresses for this seed. +#: src/seedsigner/gui/screens/tools_screens.py +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py +msgid "Address Explorer" +msgstr "" + +#. a label for a BIP-380-ish Output Descriptor +#: src/seedsigner/gui/screens/tools_screens.py +msgid "Wallet descriptor" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Enabled" msgstr "Включено" -#: src/seedsigner/models/settings_definition.py:15 -#: src/seedsigner/models/settings_definition.py:20 +#: src/seedsigner/models/settings_definition.py msgid "Disabled" msgstr "Отключено" -#: src/seedsigner/models/settings_definition.py:18 -#: src/seedsigner/models/settings_definition.py:26 +#: src/seedsigner/models/settings_definition.py msgid "Prompt" msgstr "Вызывать" -#: src/seedsigner/models/settings_definition.py:19 -#: src/seedsigner/models/settings_definition.py:23 -#: src/seedsigner/models/settings_definition.py:29 +#: src/seedsigner/models/settings_definition.py msgid "Required" msgstr "Необходимо" -#: src/seedsigner/models/settings_definition.py:72 -msgid "Btc-only" -msgstr "Только Btc" +#. Translator is expected ONLY to change their own locale below +#: src/seedsigner/models/settings_definition.py +msgid "Arabic" +msgstr "عربي" -#: src/seedsigner/models/settings_definition.py:73 -msgid "Sats-only" -msgstr "Только Sats" +#: src/seedsigner/models/settings_definition.py +msgid "Czech" +msgstr "čeština" -#: src/seedsigner/models/settings_definition.py:74 -msgid "Threshold at 0.01" -msgstr "Порог 0,01" +#: src/seedsigner/models/settings_definition.py +msgid "French" +msgstr "Français" -#: src/seedsigner/models/settings_definition.py:75 -msgid "Btc | Sats hybrid" -msgstr "гибрид Btc | Sats" +#: src/seedsigner/models/settings_definition.py +msgid "German" +msgstr "Deutsch" -#: src/seedsigner/models/settings_definition.py:83 +#: src/seedsigner/models/settings_definition.py +msgid "Hebrew" +msgstr "עִברִית" + +#: src/seedsigner/models/settings_definition.py +msgid "Japanese" +msgstr "日本" + +#: src/seedsigner/models/settings_definition.py +msgid "Korean" +msgstr "한국인" + +#: src/seedsigner/models/settings_definition.py +msgid "Portuguese" +msgstr "Português" + +#: src/seedsigner/models/settings_definition.py +msgid "Russian" +msgstr "Русский" + +#: src/seedsigner/models/settings_definition.py +msgid "Spanish" +msgstr "Español" + +#: src/seedsigner/models/settings_definition.py msgid "0°" msgstr "0°" -#: src/seedsigner/models/settings_definition.py:84 +#: src/seedsigner/models/settings_definition.py msgid "90°" msgstr "90°" -#: src/seedsigner/models/settings_definition.py:85 +#: src/seedsigner/models/settings_definition.py msgid "180°" msgstr "180°" -#: src/seedsigner/models/settings_definition.py:86 +#: src/seedsigner/models/settings_definition.py msgid "270°" msgstr "270°" -#: src/seedsigner/models/settings_definition.py:94 +#: src/seedsigner/models/settings_definition.py msgid "Low" msgstr "Низкий" -#: src/seedsigner/models/settings_definition.py:95 +#: src/seedsigner/models/settings_definition.py msgid "Medium" msgstr "Средний" -#: src/seedsigner/models/settings_definition.py:96 +#: src/seedsigner/models/settings_definition.py msgid "High" msgstr "Высоко" -#: src/seedsigner/models/settings_definition.py:104 +#: src/seedsigner/models/settings_definition.py msgid "Mainnet" msgstr "Основная сеть" -#: src/seedsigner/models/settings_definition.py:105 +#: src/seedsigner/models/settings_definition.py msgid "Testnet" msgstr "Тестовая сеть" -#: src/seedsigner/models/settings_definition.py:106 +#: src/seedsigner/models/settings_definition.py msgid "Regtest" msgstr "Регтест" -#: src/seedsigner/models/settings_definition.py:122 -#: src/seedsigner/views/seed_views.py:466 +#: src/seedsigner/models/settings_definition.py +msgid "Store Settings on SD card" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Insert SD card to enable" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +#: src/seedsigner/views/seed_views.py msgid "Single Sig" msgstr "Единая Подпись" -#: src/seedsigner/models/settings_definition.py:123 -#: src/seedsigner/views/seed_views.py:467 -msgid "Multisig" -msgstr "Мультиподпись" +#: src/seedsigner/models/settings_definition.py +msgid "Native Segwit" +msgstr "" -#: src/seedsigner/models/settings_definition.py:354 +#: src/seedsigner/models/settings_definition.py +msgid "Nested Segwit (legacy)" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Taproot" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Custom Derivation" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Language" msgstr "Язык" -#: src/seedsigner/models/settings_definition.py:362 +#: src/seedsigner/models/settings_definition.py msgid "Mnemonic language" msgstr "Язык мнемотехники" -#: src/seedsigner/models/settings_definition.py:370 +#: src/seedsigner/models/settings_definition.py msgid "Persistent settings" msgstr "Постоянные настр." -#: src/seedsigner/models/settings_definition.py:376 +#: src/seedsigner/models/settings_definition.py msgid "Coordinator software" msgstr "Программы координатора" -#: src/seedsigner/models/settings_definition.py:383 +#: src/seedsigner/models/settings_definition.py msgid "Denomination display" msgstr "Отобр. номинала" -#: src/seedsigner/models/settings_definition.py:392 +#: src/seedsigner/models/settings_definition.py msgid "Bitcoin network" msgstr "Биткойн-сеть" -#: src/seedsigner/models/settings_definition.py:400 +#: src/seedsigner/models/settings_definition.py msgid "QR code density" msgstr "Плотность QR-кода" -#: src/seedsigner/models/settings_definition.py:408 +#: src/seedsigner/models/settings_definition.py msgid "Xpub export" msgstr "экспорт xpub" -#: src/seedsigner/models/settings_definition.py:414 +#: src/seedsigner/models/settings_definition.py msgid "Sig types" msgstr "Типы подписи" -#: src/seedsigner/models/settings_definition.py:422 +#: src/seedsigner/models/settings_definition.py msgid "Script types" msgstr "Типы скриптов" -#: src/seedsigner/models/settings_definition.py:430 +#: src/seedsigner/models/settings_definition.py msgid "Show xpub details" msgstr "Показать детали xpub" -#: src/seedsigner/models/settings_definition.py:436 +#: src/seedsigner/models/settings_definition.py msgid "BIP-39 passphrase" msgstr "Кодовая фраза BIP-39" -#: src/seedsigner/models/settings_definition.py:444 +#: src/seedsigner/models/settings_definition.py msgid "Camera rotation" msgstr "Вращение камеры" -#: src/seedsigner/models/settings_definition.py:452 +#: src/seedsigner/models/settings_definition.py msgid "Compact SeedQR" msgstr "Компактный SeedQR" -#: src/seedsigner/models/settings_definition.py:458 +#: src/seedsigner/models/settings_definition.py +msgid "BIP-85 child seeds" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Message signing" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Show privacy warnings" msgstr "Показать предупреждения о конфиденциальности" -#: src/seedsigner/models/settings_definition.py:464 +#: src/seedsigner/models/settings_definition.py msgid "Show dire warnings" msgstr "Показать крайние предупреждения" -#: src/seedsigner/models/settings_definition.py:470 +#: src/seedsigner/models/settings_definition.py +msgid "Show QR brightness tips" +msgstr "" + +#: src/seedsigner/models/settings_definition.py msgid "Show partner logos" msgstr "Показать логотипы партнеров" -#: src/seedsigner/models/settings_definition.py:485 +#: src/seedsigner/models/settings_definition.py msgid "QR background color" msgstr "Цвет фона QR-кода" -#: src/seedsigner/views/main_menu_views.py:18 -msgid "Scan" -msgstr "Сканир." - -#: src/seedsigner/views/main_menu_views.py:19 -msgid "Seeds" -msgstr "Ключи" - -#: src/seedsigner/views/main_menu_views.py:20 -#: src/seedsigner/views/tools_views.py:29 -msgid "Tools" -msgstr "Инстр." - -#: src/seedsigner/views/main_menu_views.py:38 -msgid "Restart" -msgstr "Перезагрузить" - -#: src/seedsigner/views/main_menu_views.py:39 -msgid "Power Off" -msgstr "Выключить" - -#: src/seedsigner/views/main_menu_views.py:42 -msgid "Reset / Power" -msgstr "Сброс / Питание" - -#: src/seedsigner/views/psbt_views.py:36 -#: src/seedsigner/views/seed_views.py:1322 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py msgid "Scan a seed" msgstr "Сканр. ключ" -#: src/seedsigner/views/psbt_views.py:37 -#: src/seedsigner/views/seed_views.py:1323 -msgid "Enter 12/24 words" -msgstr "Введите 12/24 слова" +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +msgid "Enter 12-word seed" +msgstr "Введите ключ из 12 слов" -#: src/seedsigner/views/psbt_views.py:53 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +msgid "Enter 24-word seed" +msgstr "Введите ключ из 24 слов" + +#: src/seedsigner/views/psbt_views.py msgid "Select Signer" msgstr "Выберите Подписанта " -#: src/seedsigner/views/psbt_views.py:85 +#: src/seedsigner/views/psbt_views.py msgid "Parsing PSBT..." msgstr "Разбор PSBT..." -#: src/seedsigner/views/psbt_views.py:155 +#: src/seedsigner/views/psbt_views.py msgid "Unsupported Script Type!" msgstr "Неподдерживаемый Тип Скрипта!" -#: src/seedsigner/views/psbt_views.py:156 -msgid "" -"PSBT has unsupported input script type, please verify your change addresses." -msgstr "" -"PSBT имеет неподдерживаемый тип входного сценария, пожалуйста подтвердите " -"ваш адрес сдачи." +#: src/seedsigner/views/psbt_views.py +msgid "PSBT has unsupported input script type, please verify your change addresses." +msgstr "PSBT имеет неподдерживаемый тип входного сценария, пожалуйста подтвердите ваш адрес сдачи." -#: src/seedsigner/views/psbt_views.py:157 -#: src/seedsigner/views/psbt_views.py:176 +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/view.py msgid "Continue" msgstr "Продолжить" #. User will receive no change back; the inputs to this transaction are fully #. spent -#: src/seedsigner/views/psbt_views.py:174 +#: src/seedsigner/views/psbt_views.py msgid "Full Spend!" msgstr "Полная трата!" -#: src/seedsigner/views/psbt_views.py:175 -msgid "" -"This PSBT spends its entire input value. No change is coming back to your " -"wallet." -msgstr "" -"Эта PSBT тратит все входное ценносты. Никакая сдача не вернётся в ваш " -"кошелек." +#: src/seedsigner/views/psbt_views.py +msgid "This PSBT spends its entire input value. No change is coming back to your wallet." +msgstr "Эта PSBT тратит все входное ценносты. Никакая сдача не вернётся в ваш кошелек." #. Future-tense used to indicate that this transaction will send this amount, -#. as opposed to "Send" on its own which could be misread as an instant -#. command +#. as opposed to "Send" on its own which could be misread as an instant command #. (e.g. "Send Now"). -#: src/seedsigner/views/psbt_views.py:242 +#: src/seedsigner/views/psbt_views.py msgid "Will Send" msgstr "Пошлет" -#: src/seedsigner/views/psbt_views.py:248 +#: src/seedsigner/views/psbt_views.py msgid "Next Recipient" msgstr "Следующий получ." -#. Short for "Next step" -#: src/seedsigner/views/psbt_views.py:251 -#: src/seedsigner/views/psbt_views.py:320 -#: src/seedsigner/views/seed_views.py:796 -msgid "Next" -msgstr "Дальше" - #. The amount you're receiving back from the transaction -#: src/seedsigner/views/psbt_views.py:324 +#: src/seedsigner/views/psbt_views.py msgid "Your Change" msgstr "Твоя Сдача" -#: src/seedsigner/views/psbt_views.py:325 +#: src/seedsigner/views/psbt_views.py msgid "Verify Multisig Change" msgstr "Подтвердить Сдачу Мультиподписи" -#: src/seedsigner/views/psbt_views.py:327 +#: src/seedsigner/views/psbt_views.py msgid "Self-Transfer" msgstr "Самостоятельный Перевод" -#: src/seedsigner/views/psbt_views.py:328 +#: src/seedsigner/views/psbt_views.py msgid "Verify Multisig Addr" msgstr "Подтвердить Мультиподписной Адрес" -#: src/seedsigner/views/psbt_views.py:347 +#: src/seedsigner/views/psbt_views.py msgid "Verifying Change..." msgstr "Подтверждения Сдачи..." -#: src/seedsigner/views/psbt_views.py:349 +#: src/seedsigner/views/psbt_views.py msgid "Verifying Self-Transfer..." msgstr "Подтверждение Самостоятельного Перевода..." -#: src/seedsigner/views/psbt_views.py:431 -msgid "" -"PSBT's {} address could not be verified with your multisig wallet " -"descriptor." -msgstr "" -"Адрес PSBT {} не удалось подтвердить с помощью дескриптора мультиподписного " -"кошелька." +#. Variable is either "change" or "self-transfer". +#: src/seedsigner/views/psbt_views.py +msgid "PSBT's {} address could not be verified with your multisig wallet descriptor." +msgstr "Адрес PSBT {} не удалось подтвердить с помощью дескриптора мультиподписного кошелька." -#: src/seedsigner/views/psbt_views.py:433 +#: src/seedsigner/views/psbt_views.py msgid "Suspicious PSBT" msgstr "Подозрительный PSBT" -#: src/seedsigner/views/psbt_views.py:434 +#. Variable is either "change" or "self-transfer". +#: src/seedsigner/views/psbt_views.py msgid "PSBT's {} address could not be generated from your seed." msgstr "Адрес PSBT {} не может быть сгенерирован из вашего ключа." -#: src/seedsigner/views/psbt_views.py:438 +#: src/seedsigner/views/psbt_views.py msgid "Address Verification Failed" msgstr "Ошибка Подтверждения Адреса" -#: src/seedsigner/views/psbt_views.py:440 +#: src/seedsigner/views/psbt_views.py msgid "Discard PSBT" msgstr "Сбросить PSBT" -#: src/seedsigner/views/psbt_views.py:465 +#: src/seedsigner/views/psbt_views.py msgid "Approve PSBT" msgstr "Одобрить PSBT" -#: src/seedsigner/views/psbt_views.py:470 -msgid "Signing PSBT..." -msgstr "Подписание PSBT..." - -#: src/seedsigner/views/psbt_views.py:548 -msgid "PSBT Error" -msgstr "Ошибка PSBT" - -#: src/seedsigner/views/psbt_views.py:550 -msgid "Signing Failed" -msgstr "Подписания Не Удалась" - -#: src/seedsigner/views/psbt_views.py:551 -msgid "Signing with this seed did not add a valid signature." -msgstr "Подписание с помощью этого ключа не добавило действительную подпись." - -#: src/seedsigner/views/psbt_views.py:552 +#: src/seedsigner/views/psbt_views.py msgid "Select Diff Seed" msgstr "Выберите Другой Ключ" +#: src/seedsigner/views/psbt_views.py +msgid "PSBT Error" +msgstr "Ошибка PSBT" + +#: src/seedsigner/views/psbt_views.py +msgid "Signing Failed" +msgstr "Подписания Не Удалась" + +#: src/seedsigner/views/psbt_views.py +msgid "Signing with this seed did not add a valid signature." +msgstr "Подписание с помощью этого ключа не добавило действительную подпись." + #. This is on the opening splash screen, displayed above the HRF logo -#: src/seedsigner/views/screensaver.py:74 +#: src/seedsigner/views/screensaver.py msgid "With support from:" msgstr "При поддержке:" -#: src/seedsigner/views/seed_views.py:53 +#: src/seedsigner/views/seed_views.py msgid "Load a seed" msgstr "Загрузить ключ" -#: src/seedsigner/views/seed_views.py:56 +#: src/seedsigner/views/seed_views.py msgid "In-Memory Seeds" msgstr "Ключи В Памяти" -#: src/seedsigner/views/seed_views.py:77 +#: src/seedsigner/views/seed_views.py +msgid "Load the seed to verify" +msgstr "Загрузите ключ для подтверждения" + +#: src/seedsigner/views/seed_views.py +msgid "Select seed to verify" +msgstr "Выберите ключ для подтверждения" + +#: src/seedsigner/views/seed_views.py +msgid "Load the seed to sign with" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Select seed to sign with" +msgstr "" + +#: src/seedsigner/views/seed_views.py msgid "Scan a SeedQR" msgstr "Сканировать SeedQR" -#: src/seedsigner/views/seed_views.py:78 -msgid "Enter 24-word seed" -msgstr "Введите ключ из 24 слов" - -#: src/seedsigner/views/seed_views.py:79 -msgid "Enter 12-word seed" -msgstr "Введите ключ из 12 слов" - -#: src/seedsigner/views/seed_views.py:80 +#: src/seedsigner/views/seed_views.py msgid "Create a seed" msgstr "Создать ключ" -#: src/seedsigner/views/seed_views.py:89 +#: src/seedsigner/views/seed_views.py msgid "Load A Seed" msgstr "Загрузить Ключ" #. Inserts the word number (e.g. "Seed Word #6") -#: src/seedsigner/views/seed_views.py:126 +#: src/seedsigner/views/seed_views.py msgid "Seed Word #{}" msgstr "Ключевое Слово #{}" -#: src/seedsigner/views/seed_views.py:187 +#: src/seedsigner/views/seed_views.py msgid "Review & Edit" msgstr "Обзор и Редактировать" -#: src/seedsigner/views/seed_views.py:188 -#: src/seedsigner/views/seed_views.py:310 -#: src/seedsigner/views/tools_views.py:259 +#: src/seedsigner/views/seed_views.py src/seedsigner/views/tools_views.py msgid "Discard" msgstr "Сброс" -#: src/seedsigner/views/seed_views.py:192 +#: src/seedsigner/views/seed_views.py msgid "Invalid Mnemonic!" msgstr "Неверная Мнемоника!" -#: src/seedsigner/views/seed_views.py:194 +#: src/seedsigner/views/seed_views.py msgid "Checksum failure; not a valid seed phrase." msgstr "Ошибка контрольной суммы; недопустимая ключевая фраза." -#: src/seedsigner/views/seed_views.py:216 -#: src/seedsigner/views/seed_views.py:268 -#: src/seedsigner/views/seed_views.py:797 -#: src/seedsigner/views/seed_views.py:1146 +#: src/seedsigner/views/seed_views.py src/seedsigner/views/view.py msgid "Done" msgstr "Готово" -#: src/seedsigner/views/seed_views.py:267 +#: src/seedsigner/views/seed_views.py msgid "Edit passphrase" msgstr "Изменить кодовую фразу" -#: src/seedsigner/views/seed_views.py:309 +#: src/seedsigner/views/seed_views.py msgid "Keep Seed" msgstr "Сохранить Ключ" -#: src/seedsigner/views/seed_views.py:315 +#: src/seedsigner/views/seed_views.py msgid "Discard Seed?" msgstr "Сбросить ключ?" -#: src/seedsigner/views/seed_views.py:317 +#: src/seedsigner/views/seed_views.py msgid "Wipe seed {} from the device?" msgstr "Удалить ключ {} с устройства?" -#: src/seedsigner/views/seed_views.py:351 +#: src/seedsigner/views/seed_views.py msgid "Scan PSBT" msgstr "Сканировать PSBT" -#: src/seedsigner/views/seed_views.py:353 -#: src/seedsigner/views/seed_views.py:1646 +#: src/seedsigner/views/seed_views.py msgid "Verify Addr" msgstr "Подтвердить Адрес" -#: src/seedsigner/views/seed_views.py:355 -#: src/seedsigner/views/seed_views.py:436 -msgid "Backup Seed" -msgstr "Резервный Ключ" +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Child Seed" +msgstr "" -#: src/seedsigner/views/seed_views.py:356 +#: src/seedsigner/views/seed_views.py msgid "Discard Seed" msgstr "Сбросить ключ" -#: src/seedsigner/views/seed_views.py:431 +#: src/seedsigner/views/seed_views.py msgid "View Seed Words" msgstr "Посмотреть Ключевые Слова" -#: src/seedsigner/views/seed_views.py:432 +#: src/seedsigner/views/seed_views.py msgid "Export as SeedQR" msgstr "Экспортировать как SeedQR" -#: src/seedsigner/views/seed_views.py:620 -msgid "Privacy Leak!" -msgstr "Утечка Конфиден.!" - -#: src/seedsigner/views/seed_views.py:621 +#: src/seedsigner/views/seed_views.py msgid "Xpub can be used to view all future transactions." msgstr "Xpub можно использовать для просмотра всех будущих транзакций." -#: src/seedsigner/views/seed_views.py:651 +#: src/seedsigner/views/seed_views.py msgid "Generating xpub..." msgstr "Генерация xpub..." -#: src/seedsigner/views/seed_views.py:771 +#: src/seedsigner/views/seed_views.py msgid "You must keep your seed words private & away from all online devices." +msgstr "Вы должны держать свои ключевые слова в секрете и подальше от всех онлайн-устройств." + +#: src/seedsigner/views/seed_views.py +msgid "Child #{}" msgstr "" -"Вы должны держать свои ключевые слова в секрете и подальше от всех онлайн-" -"устройств." -#: src/seedsigner/views/seed_views.py:836 -msgid "Verify" -msgstr "Подтвердить" +#: src/seedsigner/views/seed_views.py +msgid "Seed Words" +msgstr "" -#: src/seedsigner/views/seed_views.py:837 -msgid "Skip" -msgstr "Пропустить" +#: src/seedsigner/views/seed_views.py +msgid "12 Words" +msgstr "" -#: src/seedsigner/views/seed_views.py:886 -msgid "Verify Word #{}" -msgstr "Подтвердить Слово #{}" +#: src/seedsigner/views/seed_views.py +msgid "24 Words" +msgstr "" -#: src/seedsigner/views/seed_views.py:926 -msgid "Review Seed Words" -msgstr "Обзор Ключевых Слов" +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Num Words" +msgstr "" -#: src/seedsigner/views/seed_views.py:927 +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Index Error" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Invalid Child Index" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "BIP-85 Child Index must be between 0 and 2^31-1." +msgstr "" + +#: src/seedsigner/views/seed_views.py msgid "Try Again" msgstr "Попробуйте Еще Раз" -#: src/seedsigner/views/seed_views.py:931 +#: src/seedsigner/views/seed_views.py +msgid "Verify" +msgstr "Подтвердить" + +#: src/seedsigner/views/seed_views.py +msgid "Skip" +msgstr "Пропустить" + +#: src/seedsigner/views/seed_views.py +msgid "Verify Word #{}" +msgstr "Подтвердить Слово #{}" + +#: src/seedsigner/views/seed_views.py +msgid "Review Seed Words" +msgstr "Обзор Ключевых Слов" + +#: src/seedsigner/views/seed_views.py msgid "Verification Error" msgstr "Ошибка Подтверждения" -#: src/seedsigner/views/seed_views.py:933 +#: src/seedsigner/views/seed_views.py msgid "Wrong Word!" msgstr "Неверное Слово!" -#: src/seedsigner/views/seed_views.py:934 +#: src/seedsigner/views/seed_views.py msgid "Word #{} is not \"{}\"!" msgstr "Слово #{} не является \"{}\"!" -#: src/seedsigner/views/seed_views.py:952 +#: src/seedsigner/views/seed_views.py msgid "Backup Verified" msgstr "Резервная Копия Подверждена " -#: src/seedsigner/views/seed_views.py:955 +#: src/seedsigner/views/seed_views.py msgid "All mnemonic backup words were successfully verified!" msgstr "Все резервные мнемонические слова успешно подтверждены!" -#: src/seedsigner/views/seed_views.py:978 +#: src/seedsigner/views/seed_views.py msgid "Standard: 25x25" msgstr "Стандарт: 25х25" -#: src/seedsigner/views/seed_views.py:979 +#: src/seedsigner/views/seed_views.py msgid "Compact: 21x21" msgstr "Компактный: 21х21" -#: src/seedsigner/views/seed_views.py:983 +#: src/seedsigner/views/seed_views.py msgid "Standard: 29x29" msgstr "Стандарт: 29х29" -#: src/seedsigner/views/seed_views.py:984 +#: src/seedsigner/views/seed_views.py msgid "Compact: 25x25" msgstr "Компактный: 25х25" -#: src/seedsigner/views/seed_views.py:1003 +#: src/seedsigner/views/seed_views.py msgid "SeedQR Format" msgstr "Формат SeedQR" -#: src/seedsigner/views/seed_views.py:1052 -msgid "SeedQR is your private key!" -msgstr "SeedQR это ваш частный ключ!" +#: src/seedsigner/views/seed_views.py +msgid "Never photograph or scan it into a device that connects to the internet." +msgstr "" -#: src/seedsigner/views/seed_views.py:1053 -msgid "Never photograph it or scan it into an online device." -msgstr "Никогда не фотографируйте его и не сканируйте на онлайн-устройство." - -#: src/seedsigner/views/seed_views.py:1145 -#: src/seedsigner/views/seed_views.py:1189 -#: src/seedsigner/views/seed_views.py:1200 -#: src/seedsigner/views/seed_views.py:1212 +#: src/seedsigner/views/seed_views.py msgid "Confirm SeedQR" msgstr "Подтвердить SeedQR" -#: src/seedsigner/views/seed_views.py:1150 +#: src/seedsigner/views/seed_views.py msgid "Confirm SeedQR?" msgstr "Подтвердить SeedQR?" -#: src/seedsigner/views/seed_views.py:1180 +#: src/seedsigner/views/seed_views.py msgid "Scan your SeedQR" msgstr "Сканируй свой SeedQR" -#: src/seedsigner/views/seed_views.py:1190 -#: src/seedsigner/views/seed_views.py:1213 +#: src/seedsigner/views/seed_views.py msgid "Error!" msgstr "Ошибка!" -#: src/seedsigner/views/seed_views.py:1191 +#: src/seedsigner/views/seed_views.py msgid "Your transcribed SeedQR does not match your original seed!" msgstr "Ваш переписанный SeedQR не соответствует исходному ключу!" -#: src/seedsigner/views/seed_views.py:1193 -#: src/seedsigner/views/seed_views.py:1216 +#: src/seedsigner/views/seed_views.py msgid "Review SeedQR" msgstr "Обзор SeedQR" -#: src/seedsigner/views/seed_views.py:1202 -msgid "" -"Your transcribed SeedQR successfully scanned and yielded the same seed." +#: src/seedsigner/views/seed_views.py +msgid "Your transcribed SeedQR successfully scanned and yielded the same seed." msgstr "Ваш переписанный SeedQR успешно отсканирован и дал тот же ключ." -#: src/seedsigner/views/seed_views.py:1214 +#: src/seedsigner/views/seed_views.py msgid "Your transcribed SeedQR could not be read!" msgstr "Ваш переписанный SeedQR не может быть прочитан!" -#: src/seedsigner/views/seed_views.py:1285 +#: src/seedsigner/views/seed_views.py msgid "Sig type can't be auto-detected from this address. Please specify:" -msgstr "" -"Тип подписи не может быть автоматически определен с этого адреса. " -"Пожалуйста уточните:" +msgstr "Тип подписи не может быть автоматически определен с этого адреса. Пожалуйста уточните:" -#: src/seedsigner/views/seed_views.py:1326 -msgid "Load the seed to verify" -msgstr "Загрузите ключ для подтверждения" - -#: src/seedsigner/views/seed_views.py:1336 -msgid "Select seed to verify" -msgstr "Выберите ключ для подтверждения" - -#: src/seedsigner/views/seed_views.py:1388 +#: src/seedsigner/views/seed_views.py msgid "Can't validate a single sig addr without specifying a seed" msgstr "Невозможно утвердить адрес одной-подписи без указания ключа" #. Option when scanning for a matching address; skips ten addresses ahead -#: src/seedsigner/views/seed_views.py:1434 +#: src/seedsigner/views/seed_views.py msgid "Skip 10" msgstr "Пропустить 10" -#: src/seedsigner/views/seed_views.py:1435 -#: src/seedsigner/views/seed_views.py:1616 +#: src/seedsigner/views/seed_views.py msgid "Cancel" msgstr "Отменить" -#: src/seedsigner/views/seed_views.py:1563 -#: src/seedsigner/views/seed_views.py:1579 -msgid "Taproot verification not yet implemented!" -msgstr "Проверка Taproot еще не реализована!" - -#: src/seedsigner/views/seed_views.py:1575 -msgid "Legacy P2PKH verification not implemented!" -msgstr "Устаревшая проверка P2PKH не реализована!" - -#: src/seedsigner/views/seed_views.py:1600 +#: src/seedsigner/views/seed_views.py msgid "multisig" msgstr "мультиподп." -#: src/seedsigner/views/seed_views.py:1602 +#: src/seedsigner/views/seed_views.py msgid "seed {}" msgstr "ключ {}" -#: src/seedsigner/views/seed_views.py:1605 +#: src/seedsigner/views/seed_views.py msgid "Address Verified" msgstr "Адрес Подтвержден" -#: src/seedsigner/views/seed_views.py:1615 +#: src/seedsigner/views/seed_views.py msgid "Scan Descriptor" msgstr "Сканировать Дескриптор" -#: src/seedsigner/views/seed_views.py:1645 +#: src/seedsigner/views/seed_views.py msgid "Return to PSBT" msgstr "Возврат к PSBT" -#: src/seedsigner/views/settings_views.py:20 +#: src/seedsigner/views/settings_views.py msgid "I/O test" msgstr "Тест Ввода/Вывода" -#: src/seedsigner/views/settings_views.py:39 -#: src/seedsigner/views/settings_views.py:46 +#: src/seedsigner/views/settings_views.py msgid "Advanced" msgstr "Расширенный" -#: src/seedsigner/views/settings_views.py:54 +#: src/seedsigner/views/settings_views.py msgid "Dev Options" msgstr "Параметры разраб." -#: src/seedsigner/views/tools_views.py:24 -msgid " New seed" -msgstr " Новый ключ" +#: src/seedsigner/views/settings_views.py +msgid "Persistent Settings enabled. Settings saved to SD card." +msgstr "" -#: src/seedsigner/views/tools_views.py:25 +#: src/seedsigner/views/settings_views.py +msgid "Settings updated in temporary memory" +msgstr "" + +#: src/seedsigner/views/tools_views.py msgid "New seed" msgstr "Новый ключ" -#: src/seedsigner/views/tools_views.py:26 +#: src/seedsigner/views/tools_views.py msgid "Calc 12th/24th word" msgstr "Вычислить 12-е/24-е слово" -#: src/seedsigner/views/tools_views.py:102 -#: src/seedsigner/views/tools_views.py:228 +#: src/seedsigner/views/tools_views.py src/seedsigner/views/view.py +msgid "Tools" +msgstr "Инстр." + +#: src/seedsigner/views/tools_views.py msgid "12 words" msgstr "12 слова" -#: src/seedsigner/views/tools_views.py:103 -#: src/seedsigner/views/tools_views.py:229 +#: src/seedsigner/views/tools_views.py msgid "24 words" msgstr "24 слова" -#: src/seedsigner/views/tools_views.py:107 +#: src/seedsigner/views/tools_views.py msgid "Mnemonic Length?" msgstr "Длина Мнемотехники?" -#: src/seedsigner/views/tools_views.py:174 +#: src/seedsigner/views/tools_views.py msgid "12 words (50 rolls)" msgstr "12 слов (50 бросков)" -#: src/seedsigner/views/tools_views.py:175 +#: src/seedsigner/views/tools_views.py msgid "24 words (99 rolls)" msgstr "24 слов (99 бросков)" -#: src/seedsigner/views/tools_views.py:179 -#: src/seedsigner/views/tools_views.py:233 +#: src/seedsigner/views/tools_views.py msgid "Mnemonic Length" msgstr "Длина Мнемоники" -#: src/seedsigner/views/tools_views.py:258 +#. Label to gather entropy through coin tosses +#: src/seedsigner/views/tools_views.py +msgid "Coin flip entropy" +msgstr "" + +#. Label to gather entropy through user specified BIP-39 word +#: src/seedsigner/views/tools_views.py +msgid "Word selection entropy" +msgstr "" + +#. Label to allow user to default entropy as all-zeros +#: src/seedsigner/views/tools_views.py +msgid "Finalize with zeros" +msgstr "" + +#. label to calculate the last word of a BIP-39 mnemonic seed phrase +#: src/seedsigner/views/tools_views.py +msgid "Final Word Calc" +msgstr "" + +#: src/seedsigner/views/tools_views.py msgid "Load seed" msgstr "Загрузить ключ" -#: src/seedsigner/views/view.py:124 +#. label for addresses where others send us incoming payments +#: src/seedsigner/views/tools_views.py +msgid "Receive Addresses" +msgstr "" + +#. label for addresses that collect the change from our own outgoing payments +#: src/seedsigner/views/tools_views.py +msgid "Change Addresses" +msgstr "" + +#. a status message that our payment addresses are being calculated +#: src/seedsigner/views/tools_views.py +msgid "Calculating addrs..." +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Next {}" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Receive Addrs" +msgstr "" + +#: src/seedsigner/views/tools_views.py +msgid "Change Addrs" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Scan" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Seeds" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Restart" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Power Off" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Reset / Power" +msgstr "" + +#: src/seedsigner/views/view.py msgid "Work In Progress" msgstr "Работа В Процессе" -#: src/seedsigner/views/view.py:125 +#: src/seedsigner/views/view.py msgid "Not Yet Implemented" msgstr "Ещё Не Реализовано" -#: src/seedsigner/views/view.py:126 +#: src/seedsigner/views/view.py msgid "This is still on our to-do list!" msgstr "Это всё ещё в нашем списке дел!" -#: src/seedsigner/views/view.py:127 +#: src/seedsigner/views/view.py msgid "Back to Main Menu" msgstr "Вернуться в Главное Меню" -#: src/seedsigner/views/view.py:142 +#: src/seedsigner/views/view.py +msgid "Error" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Network Mismatch" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Change Setting" +msgstr "" + +#: src/seedsigner/views/view.py msgid "System Error" msgstr "Ошибка Системы" + +#: src/seedsigner/views/view.py +msgid "Update Setting" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Option Disabled" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "Security Tip" +msgstr "" + +#: src/seedsigner/views/view.py +msgid "" +"For maximum security,\n" +"remove the MicroSD card\n" +"before continuing." +msgstr "" + +#~ msgid "{}..." +#~ msgstr "{}..." + +#~ msgid "Click to authorize this transaction" +#~ msgstr "Нажмите, чтобы авторизовать эту транзакцию" + +#~ msgid "Signed PSBT" +#~ msgstr "Подписанная PSBT" + +#~ msgid "Export as a QR code for:" +#~ msgstr "Экспорт. в виде QR-кода для:" + +#~ msgid "changes fingerprint" +#~ msgstr "меняет отпечаток пальца" + +#~ msgid "Enter 12/24 words" +#~ msgstr "Введите 12/24 слова" + +#~ msgid "Signing PSBT..." +#~ msgstr "Подписание PSBT..." + +#~ msgid "Taproot verification not yet implemented!" +#~ msgstr "Проверка Taproot еще не реализована!" + +#~ msgid "Legacy P2PKH verification not implemented!" +#~ msgstr "Устаревшая проверка P2PKH не реализована!" + +#~ msgid "Add Passphrase" +#~ msgstr "Добавить Парольную Фразу" + +#~ msgid "Btc-only" +#~ msgstr "Только Btc" + +#~ msgid "Sats-only" +#~ msgstr "Только Sats" + +#~ msgid "Threshold at 0.01" +#~ msgstr "Порог 0,01" + +#~ msgid "Btc | Sats hybrid" +#~ msgstr "гибрид Btc | Sats" + +#~ msgid "Scan" +#~ msgstr "" + +#~ msgid "Seeds" +#~ msgstr "" + +#~ msgid "Restart" +#~ msgstr "" + +#~ msgid "Power Off" +#~ msgstr "" + +#~ msgid "Reset / Power" +#~ msgstr "Сброс / Питание" + +#~ msgid " (?)" +#~ msgstr "" + +#~ msgid "Scan a QR code" +#~ msgstr "Сканировать QR-код" + +#~ msgid "Yes " +#~ msgstr "" + +#~ msgid "No " +#~ msgstr "" + +#~ msgid "< back | Scan a QR code" +#~ msgstr "" + +#~ msgid "Settings QR" +#~ msgstr "Настр. QR" + +#~ msgid "Settings imported successfully!" +#~ msgstr "Настройки успешно импортированы!" + +#~ msgid "MicroSD removed" +#~ msgstr "" + +#~ msgid "MicroSD inserted" +#~ msgstr "" + +#~ msgid "changes seed:" +#~ msgstr "" + +#~ msgid "< back | click joystick" +#~ msgstr "< назад | нажмите джойстик" + +#~ msgid "Store Settings on SD card." +#~ msgstr "" + +#~ msgid "MicroSD card is removed" +#~ msgstr "" + +#~ msgid "Show passphrase warning" +#~ msgstr "" + +#~ msgid "" +#~ "BIP-39 passphrase changes your seed, resulting in new xpubs, wallets, etc.\n" +#~ "Be sure you know what you're doing!" +#~ msgstr "" + +#~ msgid "SeedQR is your private key!" +#~ msgstr "SeedQR это ваш частный ключ!" + +#~ msgid "Never photograph it or scan it into an online device." +#~ msgstr "Никогда не фотографируйте его и не сканируйте на онлайн-устройство." + +#~ msgid " New seed" +#~ msgstr " Новый ключ" + +#~ msgid "Scan wallet descriptor" +#~ msgstr "" + diff --git a/src/seedsigner/resources/fonts/seedsigner-glyphs.otf b/src/seedsigner/resources/fonts/seedsigner-glyphs.otf deleted file mode 100644 index 63a0008c..00000000 Binary files a/src/seedsigner/resources/fonts/seedsigner-glyphs.otf and /dev/null differ diff --git a/src/seedsigner/resources/fonts/seedsigner-icons.otf b/src/seedsigner/resources/fonts/seedsigner-icons.otf new file mode 100644 index 00000000..ef4292b2 Binary files /dev/null and b/src/seedsigner/resources/fonts/seedsigner-icons.otf differ diff --git a/src/seedsigner/views/main_menu_views.py b/src/seedsigner/views/main_menu_views.py deleted file mode 100644 index 66d0bee3..00000000 --- a/src/seedsigner/views/main_menu_views.py +++ /dev/null @@ -1,94 +0,0 @@ -from gettext import gettext as _ - -from seedsigner.gui.components import FontAwesomeIconConstants -from seedsigner.gui.screens import main_menu_screens -from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON, LargeButtonScreen -from seedsigner.models.threads import BaseThread -from seedsigner.views.view import BackStackView, Destination, View - - -class MainMenuView(View): - def run(self): - from .seed_views import SeedsMenuView - from .settings_views import SettingsMenuView - from .scan_views import ScanView - from .tools_views import ToolsMenuView - from seedsigner.gui.screens.main_menu_screens import MainMenuScreen - menu_items = [ - ((_("Scan"), FontAwesomeIconConstants.QRCODE), ScanView), - ((_("Seeds"), FontAwesomeIconConstants.KEY), SeedsMenuView), - ((_("Tools"), FontAwesomeIconConstants.SCREWDRIVER_WRENCH), ToolsMenuView), - ((_("Settings"), FontAwesomeIconConstants.GEAR), SettingsMenuView), - ] - - screen = MainMenuScreen( - button_data=[entry[0] for entry in menu_items], - ) - selected_menu_num = screen.display() - - if selected_menu_num == RET_CODE__POWER_BUTTON: - return Destination(PowerOptionsView) - - return Destination(menu_items[selected_menu_num][1]) - - - -class PowerOptionsView(View): - def run(self): - RESET = (_("Restart"), FontAwesomeIconConstants.ROTATE_RIGHT) - POWER_OFF = (_("Power Off"), FontAwesomeIconConstants.POWER_OFF) - button_data = [RESET, POWER_OFF] - selected_menu_num = LargeButtonScreen( - title=_("Reset / Power"), - show_back_button=True, - button_data=button_data - ).display() - - if selected_menu_num == RET_CODE__BACK_BUTTON: - return Destination(BackStackView) - - elif button_data[selected_menu_num] == RESET: - return Destination(RestartView) - - elif button_data[selected_menu_num] == POWER_OFF: - return Destination(PowerOffView) - - - -class RestartView(View): - def run(self): - thread = RestartView.DoResetThread() - thread.start() - main_menu_screens.ResetScreen().display() - - - class DoResetThread(BaseThread): - def run(self): - import time - from subprocess import call - - # Give the screen just enough time to display the reset message before - # exiting. - time.sleep(0.25) - - # Kill the SeedSigner process; systemd will automatically restart it. - # `.*` is a wildcard to detect either `python`` or `python3` and with or - # without the `-u` flag. - call("kill $(ps aux | grep '[p]ython.*main.py' | awk '{print $2}')", shell=True) - - - -class PowerOffView(View): - def run(self): - thread = PowerOffView.PowerOffThread() - thread.start() - main_menu_screens.PowerOffScreen().display() - - - class PowerOffThread(BaseThread): - def run(self): - import time - from subprocess import call - while self.keep_running: - time.sleep(5) - call("sudo shutdown --poweroff now", shell=True) diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index a1356222..80251901 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -1,61 +1,61 @@ -import logging from gettext import gettext as _ -from typing import List - from embit.psbt import PSBT from embit import script from embit.networks import NETWORKS from seedsigner.controller import Controller -from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustomIconConstants +from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants from seedsigner.models.encode_qr import EncodeQR from seedsigner.models.psbt_parser import PSBTParser from seedsigner.models.qr_type import QRType from seedsigner.models.settings import SettingsConstants -from seedsigner.gui.screens import psbt_screens -from seedsigner.gui.screens.screen import (RET_CODE__BACK_BUTTON, ButtonListScreen, DireWarningScreen, - LoadingScreenThread, QRDisplayScreen, WarningScreen) -from seedsigner.views.main_menu_views import MainMenuView - -from .view import BackStackView, NotYetImplementedView, View, Destination - -logger = logging.getLogger(__name__) +from seedsigner.gui.screens.psbt_screens import PSBTOverviewScreen, PSBTMathScreen, PSBTAddressDetailsScreen, PSBTChangeDetailsScreen, PSBTFinalizeScreen +from seedsigner.gui.screens.screen import (RET_CODE__BACK_BUTTON, ButtonListScreen, WarningScreen, DireWarningScreen, QRDisplayScreen) +from seedsigner.views.view import BackStackView, MainMenuView, NotYetImplementedView, View, Destination class PSBTSelectSeedView(View): + SCAN_SEED = ("Scan a seed", SeedSignerIconConstants.QRCODE) + TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) + TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) + + def run(self): # Note: we can't just autoroute to the PSBT Overview because we might have a # multisig where we want to sign with more than one key on this device. if not self.controller.psbt: # Shouldn't be able to get here raise Exception("No PSBT currently loaded") - - seeds = self.controller.storage.seeds - SCAN_SEED = (_("Scan a seed"), FontAwesomeIconConstants.QRCODE) - TYPE_12WORD = (_("Enter 12-word seed"), FontAwesomeIconConstants.KEYBOARD) - TYPE_24WORD = (_("Enter 24-word seed"), FontAwesomeIconConstants.KEYBOARD) + self.SCAN_SEED = (_("Scan a seed"), SeedSignerIconConstants.QRCODE) + self.TYPE_12WORD = (_("Enter 12-word seed"), FontAwesomeIconConstants.KEYBOARD) + self.TYPE_24WORD = (_("Enter 24-word seed"), FontAwesomeIconConstants.KEYBOARD) + if self.controller.psbt_seed: + if PSBTParser.has_matching_input_fingerprint(psbt=self.controller.psbt, seed=self.controller.psbt_seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)): + # skip the seed prompt if a seed was previous selected and has matching input fingerprint + return Destination(PSBTOverviewView) + + seeds = self.controller.storage.seeds button_data = [] for seed in seeds: button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)) if not PSBTParser.has_matching_input_fingerprint(psbt=self.controller.psbt, seed=seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)): # Doesn't look like this seed can sign the current PSBT button_str += " (?)" - - if seed.passphrase is not None: - # TODO: Include lock icon on right side of button - pass - button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue")) - button_data.append(SCAN_SEED) - button_data.append(TYPE_12WORD) - button_data.append(TYPE_24WORD) - selected_menu_num = ButtonListScreen( + button_data.append((button_str, SeedSignerIconConstants.FINGERPRINT)) + + button_data.append(self.SCAN_SEED) + button_data.append(self.TYPE_12WORD) + button_data.append(self.TYPE_24WORD) + + selected_menu_num = self.run_screen( + ButtonListScreen, title=_("Select Signer"), is_button_text_centered=False, button_data=button_data - ).display() + ) if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) @@ -68,13 +68,13 @@ class PSBTSelectSeedView(View): # The remaining flows are a sub-flow; resume PSBT flow once the seed is loaded. self.controller.resume_main_flow = Controller.FLOW__PSBT - if button_data[selected_menu_num] == SCAN_SEED: - from seedsigner.views.scan_views import ScanView - return Destination(ScanView) + if button_data[selected_menu_num] == self.SCAN_SEED: + from seedsigner.views.scan_views import ScanSeedQRView + return Destination(ScanSeedQRView) - elif button_data[selected_menu_num] in [TYPE_12WORD, TYPE_24WORD]: + elif button_data[selected_menu_num] in [self.TYPE_12WORD, self.TYPE_24WORD]: from seedsigner.views.seed_views import SeedMnemonicEntryView - if button_data[selected_menu_num] == TYPE_12WORD: + if button_data[selected_menu_num] == self.TYPE_12WORD: self.controller.storage.init_pending_mnemonic(num_words=12) else: self.controller.storage.init_pending_mnemonic(num_words=24) @@ -91,8 +91,10 @@ class PSBTOverviewView(View): if not self.controller.psbt_parser or self.controller.psbt_parser.seed != self.controller.psbt_seed: # The PSBTParser takes a while to read the PSBT. Run the loading screen while # we wait. + from seedsigner.gui.screens.screen import LoadingScreenThread self.loading_screen = LoadingScreenThread(text=_("Parsing PSBT...")) self.loading_screen.start() + try: self.controller.psbt_parser = PSBTParser( self.controller.psbt, @@ -127,24 +129,24 @@ class PSBTOverviewView(View): else: num_self_transfer_outputs += 1 - screen = psbt_screens.PSBTOverviewScreen( + # Everything is set. Stop the loading screen + if self.loading_screen: + self.loading_screen.stop() + + # Run the overview screen + selected_menu_num = self.run_screen( + PSBTOverviewScreen, spend_amount=psbt_parser.spend_amount, change_amount=psbt_parser.change_amount, fee_amount=psbt_parser.fee_amount, num_inputs=psbt_parser.num_inputs, num_self_transfer_outputs=num_self_transfer_outputs, num_change_outputs=num_change_outputs, - destination_addresses=psbt_parser.destination_addresses, + destination_addresses=psbt_parser.destination_addresses ) - # Everything is set. Stop the loading screen - if self.loading_screen: - self.loading_screen.stop() - - # Run the overview screen - selected_menu_num = screen.display() - if selected_menu_num == RET_CODE__BACK_BUTTON: + self.controller.psbt_seed = None return Destination(BackStackView) # expecting p2sh (legacy multisig) and p2pkh to have no policy set @@ -178,6 +180,8 @@ class PSBTUnsupportedScriptTypeWarningView(View): skip_current_view=True, # Prevent going BACK to WarningViews ) + + class PSBTNoChangeWarningView(View): def run(self): selected_menu_num = WarningScreen( @@ -213,14 +217,15 @@ class PSBTMathView(View): # Should not be able to get here return Destination(MainMenuView) - selected_menu_num = psbt_screens.PSBTMathScreen( + selected_menu_num = self.run_screen( + PSBTMathScreen, input_amount=psbt_parser.input_amount, num_inputs=psbt_parser.num_inputs, spend_amount=psbt_parser.spend_amount, num_recipients=psbt_parser.num_destinations, fee_amount=psbt_parser.fee_amount, change_amount=psbt_parser.change_amount, - ).display() + ) if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) @@ -247,7 +252,7 @@ class PSBTAddressDetailsView(View): if not psbt_parser: # Should not be able to get here - return Destination(MainMenuView) + raise Exception("Routing error") # TRANSLATOR_NOTE: Future-tense used to indicate that this transaction will send this amount, as opposed to "Send" on its own which could be misread as an instant command (e.g. "Send Now"). title = _("Will Send") @@ -261,34 +266,36 @@ class PSBTAddressDetailsView(View): # TRANSLATOR_NOTE: Short for "Next step" button_data.append(_("Next")) - selected_menu_num = psbt_screens.PSBTAddressDetailsScreen( + selected_menu_num = self.run_screen( + PSBTAddressDetailsScreen, title=title, button_data=button_data, address=psbt_parser.destination_addresses[self.address_num], amount=psbt_parser.destination_amounts[self.address_num], - ).display() - - if selected_menu_num == 0: - if self.address_num < len(psbt_parser.destination_addresses) - 1: - # Show the next receive addr - return Destination(PSBTAddressDetailsView, view_args={"address_num": self.address_num + 1}) - - elif psbt_parser.change_amount > 0: - # Move on to display change - return Destination(PSBTChangeDetailsView, view_args={"change_address_num": 0}) - - else: - # There's no change output to verify. Move on to sign the PSBT. - return Destination(PSBTFinalizeView) - + ) + if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) + if self.address_num < len(psbt_parser.destination_addresses) - 1: + # Show the next receive addr + return Destination(PSBTAddressDetailsView, view_args={"address_num": self.address_num + 1}) + + elif psbt_parser.change_amount > 0: + # Move on to display change + return Destination(PSBTChangeDetailsView, view_args={"change_address_num": 0}) + + else: + # There's no change output to verify. Move on to sign the PSBT. + return Destination(PSBTFinalizeView) + class PSBTChangeDetailsView(View): - """ - """ + NEXT = "Next" + VERIFY_MULTISIG = "Verify Multisig Change" + + def __init__(self, change_address_num): super().__init__() self.change_address_num = change_address_num @@ -328,15 +335,15 @@ class PSBTChangeDetailsView(View): is_change_derivation_path = int(derivation_path.split("/")[-2]) == 1 derivation_path_addr_index = int(derivation_path.split("/")[-1]) - NEXT = _("Next") + self.NEXT = _("Next") if is_change_derivation_path: # TRANSLATOR_NOTE: The amount you're receiving back from the transaction title = _("Your Change") - VERIFY_MULTISIG = _("Verify Multisig Change") + self.VERIFY_MULTISIG = _("Verify Multisig Change") else: title = _("Self-Transfer") - VERIFY_MULTISIG = _("Verify Multisig Addr") + self.VERIFY_MULTISIG = _("Verify Multisig Addr") # if psbt_parser.num_change_outputs > 1: # title += f" (#{self.change_address_num + 1})" @@ -345,11 +352,11 @@ class PSBTChangeDetailsView(View): # if the known-good multisig descriptor is already onboard: if self.controller.multisig_wallet_descriptor: is_change_addr_verified = psbt_parser.verify_multisig_output(self.controller.multisig_wallet_descriptor, change_num=self.change_address_num) - button_data = [NEXT] + button_data = [self.NEXT] else: # Have the Screen offer to load in the multisig descriptor. - button_data = [VERIFY_MULTISIG, NEXT] + button_data = [self.VERIFY_MULTISIG, self.NEXT] else: # Single sig @@ -358,6 +365,7 @@ class PSBTChangeDetailsView(View): loading_screen_text = _("Verifying Change...") else: loading_screen_text = _("Verifying Self-Transfer...") + from seedsigner.gui.screens.screen import LoadingScreenThread loading_screen = LoadingScreenThread(text=loading_screen_text) loading_screen.start() @@ -391,7 +399,7 @@ class PSBTChangeDetailsView(View): if change_data["address"] == calc_address: is_change_addr_verified = True - button_data = [NEXT] + button_data = [self.NEXT] finally: loading_screen.stop() @@ -399,7 +407,8 @@ class PSBTChangeDetailsView(View): if is_change_addr_verified == False and (not psbt_parser.is_multisig or self.controller.multisig_wallet_descriptor is not None): return Destination(PSBTAddressVerificationFailedView, view_args=dict(is_change=is_change_derivation_path, is_multisig=psbt_parser.is_multisig), clear_history=True) - selected_menu_num = psbt_screens.PSBTChangeDetailsScreen( + selected_menu_num = self.run_screen( + PSBTChangeDetailsScreen, title=title, button_data=button_data, address=change_data.get("address"), @@ -410,19 +419,19 @@ class PSBTChangeDetailsView(View): is_change_derivation_path=is_change_derivation_path, derivation_path_addr_index=derivation_path_addr_index, is_change_addr_verified=is_change_addr_verified, - ).display() + ) if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - elif button_data[selected_menu_num] == NEXT: + elif button_data[selected_menu_num] == self.NEXT: if self.change_address_num < psbt_parser.num_change_outputs - 1: return Destination(PSBTChangeDetailsView, view_args={"change_address_num": self.change_address_num + 1}) else: # There's no more change to verify. Move on to sign the PSBT. return Destination(PSBTFinalizeView) - elif button_data[selected_menu_num] == VERIFY_MULTISIG: + elif button_data[selected_menu_num] == self.VERIFY_MULTISIG: from seedsigner.views.seed_views import LoadMultisigWalletDescriptorView self.controller.resume_main_flow = Controller.FLOW__PSBT return Destination(LoadMultisigWalletDescriptorView) @@ -454,11 +463,8 @@ class PSBTAddressVerificationFailedView(View): show_back_button=False, ).display() - # Clear out the bad PSBT - self.controller.psbt = None - self.controller.psbt_parser = None - self.controller.psbt_seed = None - + # We're done with this PSBT. Route back to MainMenuView which always + # clears all ephemeral data (except in-memory seeds). return Destination(MainMenuView, clear_history=True) @@ -466,19 +472,27 @@ class PSBTAddressVerificationFailedView(View): class PSBTFinalizeView(View): """ """ + APPROVE_PSBT = "Approve PSBT" + + def run(self): psbt_parser: PSBTParser = self.controller.psbt_parser psbt: PSBT = self.controller.psbt + self.APPROVE_PSBT = _("Approve PSBT") if not psbt_parser: # Should not be able to get here return Destination(MainMenuView) + + selected_menu_num = self.run_screen( + PSBTFinalizeScreen, + button_data=[self.APPROVE_PSBT] + ) - selected_menu_num = psbt_screens.PSBTFinalizeScreen( - button_data=[_("Approve PSBT")] - ).display() + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) - if selected_menu_num == 0: + else: # Sign PSBT sig_cnt = PSBTParser.sig_count(psbt) psbt.sign_with(psbt_parser.root) @@ -492,75 +506,49 @@ class PSBTFinalizeView(View): else: self.controller.psbt = trimmed_psbt - - if len(self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)) == 1: - return Destination(PSBTSignedQRDisplayView, view_args={"coordinator": self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[0]}) - else: - return Destination(PSBTSelectCoordinatorView) - - if selected_menu_num == RET_CODE__BACK_BUTTON: - return Destination(BackStackView) - - - -class PSBTSelectCoordinatorView(View): - def run(self): - button_data = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS) - selected_menu_num = psbt_screens.PSBTSelectCoordinatorScreen( - button_data=button_data - ).display() - - if selected_menu_num == RET_CODE__BACK_BUTTON: - return Destination(BackStackView) - - return Destination(PSBTSignedQRDisplayView, view_args={"coordinator": button_data[selected_menu_num]}) + return Destination(PSBTSignedQRDisplayView) class PSBTSignedQRDisplayView(View): - def __init__(self, coordinator: str): - super().__init__() - self.coordinator = coordinator - def run(self): - qr_psbt_type = QRType.PSBT__UR2 - if self.coordinator == SettingsConstants.COORDINATOR__SPECTER_DESKTOP: - qr_psbt_type = QRType.PSBT__SPECTER - qr_encoder = EncodeQR( psbt=self.controller.psbt, - qr_type=qr_psbt_type, + qr_type=QRType.PSBT__UR2, # All coordinators (as of 2022-08) use this format qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY), wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE), ) - QRDisplayScreen(qr_encoder=qr_encoder).display() - - # We're done with this PSBT. Remove all related data - self.controller.psbt = None - self.controller.psbt_parser = None - self.controller.psbt_seed = None + self.run_screen(QRDisplayScreen, qr_encoder=qr_encoder) + # We're done with this PSBT. Route back to MainMenuView which always + # clears all ephemeral data (except in-memory seeds). return Destination(MainMenuView, clear_history=True) class PSBTSigningErrorView(View): + SELECT_DIFF_SEED = "Select Diff Seed" + def run(self): psbt_parser: PSBTParser = self.controller.psbt_parser + self.SELECT_DIFF_SEED = _("Select Diff Seed") if not psbt_parser: # Should not be able to get here return Destination(MainMenuView) # Just a WarningScreen here; only use DireWarningScreen for true security risks. - selected_menu_num = WarningScreen( + selected_menu_num = self.run_screen( + WarningScreen, title=_("PSBT Error"), - status_icon_name=SeedSignerCustomIconConstants.CIRCLE_EXCLAMATION, + status_icon_name=SeedSignerIconConstants.WARNING, status_headline=_("Signing Failed"), text=_("Signing with this seed did not add a valid signature."), - button_data=[_("Select Diff Seed")], - ).display() + button_data=[self.SELECT_DIFF_SEED] + ) if selected_menu_num == 0: + # clear seed selected for psbt signing since it did not add a valid signature + self.controller.psbt_seed = None return Destination(PSBTSelectSeedView, clear_history=True) if selected_menu_num == RET_CODE__BACK_BUTTON: diff --git a/src/seedsigner/views/scan_views.py b/src/seedsigner/views/scan_views.py index b7ed2be8..ef6476f2 100644 --- a/src/seedsigner/views/scan_views.py +++ b/src/seedsigner/views/scan_views.py @@ -1,31 +1,70 @@ -import json import re from embit.descriptor import Descriptor from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON -from seedsigner.models import DecodeQR, Seed +from seedsigner.models.decode_qr import DecodeQR +from seedsigner.models.seed import Seed from seedsigner.models.settings import SettingsConstants -from seedsigner.views.main_menu_views import MainMenuView - -from .view import BackStackView, NotYetImplementedView, View, Destination +from seedsigner.views.settings_views import SettingsIngestSettingsQRView +from seedsigner.views.view import BackStackView, ErrorView, MainMenuView, NotYetImplementedView, OptionDisabledView, View, Destination class ScanView(View): + """ + The catch-all generic scanning View that will accept any of our supported QR + formats and will route to the most sensible next step. + + Can also be used as a base class for more specific scanning flows with + dedicated errors when an unexpected QR type is scanned (e.g. Scan PSBT was + selected but a SeedQR was scanned). + """ + instructions_text = "Scan a QR code" + invalid_qr_type_message = "QRCode not recognized or not yet supported." + + + def __init__(self): + super().__init__() + # Define the decoder here to make it available to child classes' is_valid_qr_type + # checks and so we can inject data into it in the test suite's `before_run()`. + self.wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) + self.decoder: DecodeQR = DecodeQR(wordlist_language_code=self.wordlist_language_code) + + + @property + def is_valid_qr_type(self): + return True + + def run(self): from seedsigner.gui.screens.scan_screens import ScanScreen - wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) - self.decoder = DecodeQR(wordlist_language_code=wordlist_language_code) - # Start the live preview and background QR reading - ScanScreen(decoder=self.decoder).display() + self.run_screen( + ScanScreen, + instructions_text=self.instructions_text, + decoder=self.decoder + ) # Handle the results if self.decoder.is_complete: + if not self.is_valid_qr_type: + # We recognized the QR type but it was not the type expected for the + # current flow. + # Report QR types in more human-readable text (e.g. QRType + # `seed__compactseedqr` as "seed: compactseedqr"). + return Destination(ErrorView, view_args=dict( + title="Error", + status_headline="Wrong QR Type", + text=self.invalid_qr_type_message + f""", received "{self.decoder.qr_type.replace("__", ": ").replace("_", " ")}\" format""", + button_text="Back", + next_destination=Destination(BackStackView, skip_current_view=True), + )) + if self.decoder.is_seed: seed_mnemonic = self.decoder.get_seed_phrase() + if not seed_mnemonic: # seed is not valid, Exit if not valid with message return Destination(NotYetImplementedView) @@ -34,7 +73,7 @@ class ScanView(View): # pending (might set a passphrase, SeedXOR, etc) until finalized. from .seed_views import SeedFinalizeView self.controller.storage.set_pending_seed( - Seed(mnemonic=seed_mnemonic, wordlist_language_code=wordlist_language_code) + Seed(mnemonic=seed_mnemonic, wordlist_language_code=self.wordlist_language_code) ) if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__REQUIRED: from seedsigner.views.seed_views import SeedAddPassphraseView @@ -47,16 +86,11 @@ class ScanView(View): psbt = self.decoder.get_psbt() self.controller.psbt = psbt self.controller.psbt_parser = None - return Destination(PSBTSelectSeedView) + return Destination(PSBTSelectSeedView, skip_current_view=True) elif self.decoder.is_settings: - from seedsigner.models.settings import Settings - settings = self.decoder.get_settings_data() - Settings.get_instance().update(new_settings=settings) - - print(json.dumps(Settings.get_instance()._data, indent=4)) - - return Destination(SettingsUpdatedView, {"config_name": self.decoder.get_settings_config_name()}) + data = self.decoder.get_settings_data() + return Destination(SettingsIngestSettingsQRView, view_args=dict(data=data)) elif self.decoder.is_wallet_descriptor: from seedsigner.views.seed_views import MultisigWalletDescriptorView @@ -84,7 +118,7 @@ class ScanView(View): return Destination(NotYetImplementedView) self.controller.multisig_wallet_descriptor = descriptor - return Destination(MultisigWalletDescriptorView) + return Destination(MultisigWalletDescriptorView, skip_current_view=True) elif self.decoder.is_address: from seedsigner.views.seed_views import AddressVerificationStartView @@ -93,6 +127,7 @@ class ScanView(View): return Destination( AddressVerificationStartView, + skip_current_view=True, view_args={ "address": address, "script_type": script_type, @@ -100,28 +135,71 @@ class ScanView(View): } ) + elif self.decoder.is_sign_message: + from seedsigner.views.seed_views import SeedSignMessageStartView + qr_data = self.decoder.get_qr_data() + + return Destination( + SeedSignMessageStartView, + view_args=dict( + derivation_path=qr_data["derivation_path"], + message=qr_data["message"], + ) + ) + else: return Destination(NotYetImplementedView) + elif self.decoder.is_invalid: + # For now, don't even try to re-do the attempted operation, just reset and + # start everything over. + self.controller.resume_main_flow = None + return Destination(ErrorView, view_args=dict( + title="Error", + status_headline="Unknown QR Type", + text="QRCode is invalid or is a data format not yet supported.", + button_text="Done", + next_destination=Destination(MainMenuView, clear_history=True), + )) + return Destination(MainMenuView) -class SettingsUpdatedView(View): - def __init__(self, config_name: str): - super().__init__() +class ScanPSBTView(ScanView): + instructions_text = "Scan PSBT" + invalid_qr_type_message = "Expected a PSBT" - self.config_name = config_name - + @property + def is_valid_qr_type(self): + return self.decoder.is_psbt - def run(self): - from seedsigner.gui.screens.scan_screens import SettingsUpdatedScreen - screen = SettingsUpdatedScreen(config_name=self.config_name) - selected_menu_num = screen.display() - if selected_menu_num == RET_CODE__BACK_BUTTON: - return Destination(BackStackView) - # Only one exit point - return Destination(MainMenuView) +class ScanSeedQRView(ScanView): + instructions_text = "Scan SeedQR" + invalid_qr_type_message = f"Expected a SeedQR" + @property + def is_valid_qr_type(self): + return self.decoder.is_seed + + + +class ScanWalletDescriptorView(ScanView): + instructions_text = "Scan descriptor" + invalid_qr_type_message = "Expected a wallet descriptor QR" + + @property + def is_valid_qr_type(self): + return self.decoder.is_wallet_descriptor + + + +class ScanAddressView(ScanView): + instructions_text = "Scan address QR" + invalid_qr_type_message = "Expected an address QR" + + @property + def is_valid_qr_type(self): + return self.decoder.is_address diff --git a/src/seedsigner/views/screensaver.py b/src/seedsigner/views/screensaver.py index d0ff001b..932645ef 100644 --- a/src/seedsigner/views/screensaver.py +++ b/src/seedsigner/views/screensaver.py @@ -91,7 +91,6 @@ class OpeningSplashScreen(LogoScreen): - class ScreensaverScreen(LogoScreen): def __init__(self, buttons): super().__init__() @@ -143,9 +142,9 @@ class ScreensaverScreen(LogoScreen): # never gives up the lock until it returns. with self.renderer.lock: try: - while True: - if self.buttons.has_any_input(): - return self.stop() + while self._is_running: + if self.buttons.has_any_input() or self.buttons.override_ind: + break # Must crop the image to the exact display size crop = self.image.crop(( @@ -178,20 +177,23 @@ class ScreensaverScreen(LogoScreen): self.increment_y = self.rand_increment() if self.increment_y > 0.0: self.increment_y *= -1.0 + except KeyboardInterrupt as e: # Exit triggered; close gracefully print("Shutting down Screensaver") - self.stop() # Have to let the interrupt bubble up to exit the main app raise e + finally: + self._is_running = False + + # Restore the original screen + self.renderer.show_image(self.last_screen) + def stop(self): - # Restore the original screen - self.renderer.show_image(self.last_screen) - self._is_running = False diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 5f2f6b4b..bbbfb168 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -10,11 +10,12 @@ from gettext import gettext as _ from typing import List from seedsigner.controller import Controller -from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustomIconConstants +from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants from seedsigner.helpers import embit_utils from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, WarningScreen, DireWarningScreen, seed_screens) -from seedsigner.gui.screens.screen import LargeIconStatusScreen, LoadingScreenThread, QRDisplayScreen +from seedsigner.gui.screens.screen import LargeIconStatusScreen, QRDisplayScreen +from seedsigner.helpers import embit_utils from seedsigner.models.decode_qr import DecodeQR from seedsigner.models.encode_qr import EncodeQR from seedsigner.models.psbt_parser import PSBTParser @@ -23,23 +24,20 @@ from seedsigner.models.seed import InvalidSeedException, Seed from seedsigner.models.settings import Settings, SettingsConstants from seedsigner.models.settings_definition import SettingsDefinition from seedsigner.models.threads import BaseThread, ThreadsafeCounter -from seedsigner.views.main_menu_views import MainMenuView -from seedsigner.views.psbt_views import PSBTChangeDetailsView -from seedsigner.views.scan_views import ScanView - -from .view import NotYetImplementedView, View, Destination, BackStackView +from seedsigner.views.view import NotYetImplementedView, OptionDisabledView, View, Destination, BackStackView, MainMenuView class SeedsMenuView(View): + LOAD = "Load a seed" + def __init__(self): super().__init__() self.seeds = [] for seed in self.controller.storage.seeds: self.seeds.append({ - "fingerprint": seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)), - "has_passphrase": seed.passphrase is not None + "fingerprint": seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)) }) @@ -50,14 +48,15 @@ class SeedsMenuView(View): button_data = [] for seed in self.seeds: - button_data.append((seed["fingerprint"], SeedSignerCustomIconConstants.FINGERPRINT, "blue")) + button_data.append((seed["fingerprint"], SeedSignerIconConstants.FINGERPRINT)) button_data.append(_("Load a seed")) - selected_menu_num = ButtonListScreen( + selected_menu_num = self.run_screen( + ButtonListScreen, title=_("In-Memory Seeds"), is_button_text_centered=False, button_data=button_data - ).display() + ) if len(self.seeds) > 0 and selected_menu_num < len(self.seeds): return Destination(SeedOptionsView, view_args={"seed_num": selected_menu_num}) @@ -70,44 +69,140 @@ class SeedsMenuView(View): -"""**************************************************************************** - Loading seeds, passphrases, etc -****************************************************************************""" -class LoadSeedView(View): - def run(self): - SEED_QR = (_("Scan a SeedQR"), FontAwesomeIconConstants.QRCODE) - TYPE_24WORD = (_("Enter 24-word seed"), FontAwesomeIconConstants.KEYBOARD) - TYPE_12WORD = (_("Enter 12-word seed"), FontAwesomeIconConstants.KEYBOARD) - CREATE = (_("Create a seed"), FontAwesomeIconConstants.PLUS) - button_data=[ - SEED_QR, - TYPE_12WORD, - TYPE_24WORD, - CREATE, - ] +class SeedSelectSeedView(View): + """ + Reusable seed selection UI. Prompts the user to select amongst the already-loaded + seeds OR to load a seed. - selected_menu_num = ButtonListScreen( - title=_("Load A Seed"), + * `flow`: indicates which user flow is in progress during seed selection (e.g. + verify single sig addr or sign message). + """ + SCAN_SEED = ("Scan a seed", SeedSignerIconConstants.QRCODE) + TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) + TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) + + + def __init__(self, flow: str = Controller.FLOW__VERIFY_SINGLESIG_ADDR): + super().__init__() + self.flow = flow + + + def run(self): + self.SCAN_SEED = (_("Scan a seed"), SeedSignerIconConstants.QRCODE) + self.TYPE_12WORD = (_("Enter 12-word seed"), FontAwesomeIconConstants.KEYBOARD) + self.TYPE_24WORD = (_("Enter 24-word seed"), FontAwesomeIconConstants.KEYBOARD) + seeds = self.controller.storage.seeds + + if self.flow == Controller.FLOW__VERIFY_SINGLESIG_ADDR: + title = "Verify Address" + if not seeds: + text = _("Load the seed to verify") + else: + text = _("Select seed to verify") + + elif self.flow == Controller.FLOW__SIGN_MESSAGE: + title = _("Sign Message") + if not seeds: + text = _("Load the seed to sign with") + else: + text = _("Select seed to sign with") + + else: + raise Exception(f"Unsupported `flow` specified: {self.flow}") + + button_data = [] + for seed in seeds: + button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)) + + if seed.passphrase is not None: + # TODO: Include lock icon on right side of button + pass + button_data.append((button_str, SeedSignerIconConstants.FINGERPRINT, "blue")) + + button_data.append(self.SCAN_SEED) + button_data.append(self.TYPE_12WORD) + button_data.append(self.TYPE_24WORD) + + selected_menu_num = self.run_screen( + seed_screens.SeedSelectSeedScreen, + title=title, + text=text, is_button_text_centered=False, - button_data=button_data - ).display() + button_data=button_data, + ) if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - if button_data[selected_menu_num] == SEED_QR: - from .scan_views import ScanView + if len(seeds) > 0 and selected_menu_num < len(seeds): + # User selected one of the n seeds + view_args = dict(seed_num=selected_menu_num) + if self.flow == Controller.FLOW__VERIFY_SINGLESIG_ADDR: + return Destination(SeedAddressVerificationView, view_args=view_args) + + elif self.flow == Controller.FLOW__SIGN_MESSAGE: + self.controller.sign_message_data["seed_num"] = selected_menu_num + return Destination(SeedSignMessageConfirmMessageView) + + self.controller.resume_main_flow = self.flow + + if button_data[selected_menu_num] == self.SCAN_SEED: + from seedsigner.views.scan_views import ScanView return Destination(ScanView) + + elif button_data[selected_menu_num] in [self.TYPE_12WORD, self.TYPE_24WORD]: + from seedsigner.views.seed_views import SeedMnemonicEntryView + if button_data[selected_menu_num] == self.TYPE_12WORD: + self.controller.storage.init_pending_mnemonic(num_words=12) + else: + self.controller.storage.init_pending_mnemonic(num_words=24) + return Destination(SeedMnemonicEntryView) + + + +"""**************************************************************************** + Loading seeds, passphrases, etc +****************************************************************************""" +class LoadSeedView(View): + SEED_QR = ("Scan a SeedQR", SeedSignerIconConstants.QRCODE) + TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) + TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) + CREATE = ("Create a seed", SeedSignerIconConstants.PLUS) + + def run(self): + self.SEED_QR = (_("Scan a SeedQR"), SeedSignerIconConstants.QRCODE) + self.TYPE_12WORD = (_("Enter 12-word seed"), FontAwesomeIconConstants.KEYBOARD) + self.TYPE_24WORD = (_("Enter 24-word seed"), FontAwesomeIconConstants.KEYBOARD) + self.CREATE = (_("Create a seed"), SeedSignerIconConstants.PLUS) + button_data = [ + self.SEED_QR, + self.TYPE_12WORD, + self.TYPE_24WORD, + self.CREATE, + ] + selected_menu_num = self.run_screen( + ButtonListScreen, + title=_("Load A Seed"), + is_button_text_centered=False, + button_data=button_data + ) + + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) - elif button_data[selected_menu_num] == TYPE_12WORD: + if button_data[selected_menu_num] == self.SEED_QR: + from .scan_views import ScanSeedQRView + return Destination(ScanSeedQRView) + + elif button_data[selected_menu_num] == self.TYPE_12WORD: self.controller.storage.init_pending_mnemonic(num_words=12) return Destination(SeedMnemonicEntryView) - elif button_data[selected_menu_num] == TYPE_24WORD: + elif button_data[selected_menu_num] == self.TYPE_24WORD: self.controller.storage.init_pending_mnemonic(num_words=24) return Destination(SeedMnemonicEntryView) - elif button_data[selected_menu_num] == CREATE: + elif button_data[selected_menu_num] == self.CREATE: from .tools_views import ToolsMenuView return Destination(ToolsMenuView) @@ -122,23 +217,17 @@ class SeedMnemonicEntryView(View): def run(self): - ret = seed_screens.SeedMnemonicEntryScreen( + ret = self.run_screen( + seed_screens.SeedMnemonicEntryScreen, # TRANSLATOR_NOTE: Inserts the word number (e.g. "Seed Word #6") title=_("Seed Word #{}").format(self.cur_word_index + 1), # Human-readable 1-indexing! initial_letters=list(self.cur_word) if self.cur_word else ["a"], wordlist=Seed.get_wordlist(wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)), - ).display() + ) if ret == RET_CODE__BACK_BUTTON: if self.cur_word_index > 0: return Destination(BackStackView) - # return Destination( - # SeedMnemonicEntryView, - # view_args={ - # "cur_word_index": self.cur_word_index - 1, - # "is_calc_final_word": self.is_calc_final_word - # } - # ) else: self.controller.storage.discard_pending_mnemonic() return Destination(MainMenuView) @@ -178,34 +267,40 @@ class SeedMnemonicEntryView(View): class SeedMnemonicInvalidView(View): + EDIT = "Review & Edit" + DISCARD = ("Discard", None, None, "red") + def __init__(self): super().__init__() self.mnemonic: List[str] = self.controller.storage.pending_mnemonic def run(self): - EDIT = _("Review & Edit") - DISCARD = (_("Discard"), None, None, "red") - button_data = [EDIT, DISCARD] - - selected_menu_num = WarningScreen( + self.EDIT = _("Review & Edit") + self.DISCARD = (_("Discard"), None, None, "red") + button_data = [self.EDIT, self.DISCARD] + selected_menu_num = self.run_screen( + WarningScreen, title=_("Invalid Mnemonic!"), status_headline=None, text=_("Checksum failure; not a valid seed phrase."), show_back_button=False, button_data=button_data, - ).display() + ) - if button_data[selected_menu_num] == EDIT: + if button_data[selected_menu_num] == self.EDIT: return Destination(SeedMnemonicEntryView, view_args={"cur_word_index": 0}) - elif button_data[selected_menu_num] == DISCARD: + elif button_data[selected_menu_num] == self.DISCARD: self.controller.storage.discard_pending_mnemonic() return Destination(MainMenuView) class SeedFinalizeView(View): + FINALIZE = "Done" + PASSPHRASE = "BIP-39 Passphrase" + def __init__(self): super().__init__() self.seed = self.controller.storage.get_pending_seed() @@ -226,42 +321,24 @@ class SeedFinalizeView(View): def run(self): - FINALIZE = _("Done") - PASSPHRASE = (_("Add Passphrase"), FontAwesomeIconConstants.LOCK) - button_data = [] - - button_data.append(FINALIZE) + self.FINALIZE = _("Done") + self.PASSPHRASE = _("BIP-39 Passphrase") + button_data = [self.FINALIZE] if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) != SettingsConstants.OPTION__DISABLED: - button_data.append(PASSPHRASE) + button_data.append(self.PASSPHRASE) - selected_menu_num = seed_screens.SeedFinalizeScreen( + selected_menu_num = self.run_screen( + seed_screens.SeedFinalizeScreen, fingerprint=self.fingerprint, button_data=button_data, - ).display() + ) - if button_data[selected_menu_num] == FINALIZE: + if button_data[selected_menu_num] == self.FINALIZE: seed_num = self.controller.storage.finalize_pending_seed() return Destination(SeedOptionsView, view_args={"seed_num": seed_num}, clear_history=True) - elif button_data[selected_menu_num] == PASSPHRASE: - if self.settings.get_value(attr_name=SettingsConstants.SETTING__PASSPHRASE_WARNING) == SettingsConstants.OPTION__ENABLED: - return Destination(SeedPassphraseWarningView) - - else: - return Destination(SeedAddPassphraseView) - - - -class SeedPassphraseWarningView(View): - def run(self): - selected_menu_num = WarningScreen( - status_headline=None, - text=_("""BIP-39 passphrase changes your seed, resulting in new xpubs, wallets, etc.\nBe sure you know what you're doing!""") - ).display() - - if selected_menu_num == 0: - # User clicked "I Understand" + elif button_data[selected_menu_num] == self.PASSPHRASE: return Destination(SeedAddPassphraseView) elif selected_menu_num == RET_CODE__BACK_BUTTON: @@ -276,14 +353,17 @@ class SeedAddPassphraseView(View): def run(self): - ret = seed_screens.SeedAddPassphraseScreen(passphrase=self.seed.passphrase).display() + ret = self.run_screen(seed_screens.SeedAddPassphraseScreen, passphrase=self.seed.passphrase) if ret == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - # The new passphrase will be the return value + # The new passphrase will be the return value; it might be empty. self.seed.set_passphrase(ret) - return Destination(SeedReviewPassphraseView) + if len(self.seed.passphrase) > 0: + return Destination(SeedReviewPassphraseView) + else: + return Destination(SeedFinalizeView) @@ -291,15 +371,17 @@ class SeedReviewPassphraseView(View): """ Display the completed passphrase back to the user. """ + EDIT = "Edit passphrase" + DONE = "Done" + def __init__(self): super().__init__() self.seed = self.controller.storage.get_pending_seed() def run(self): - EDIT = _("Edit passphrase") - DONE = _("Done") - button_data = [EDIT, DONE] + self.EDIT = _("Edit passphrase") + self.DONE = _("Done") # Get the before/after fingerprints network = self.settings.get_value(SettingsConstants.SETTING__NETWORK) @@ -309,26 +391,32 @@ class SeedReviewPassphraseView(View): fingerprint_without = self.seed.get_fingerprint(network=network) self.seed.set_passphrase(passphrase) - # Because we have ane explicit "Edit" button, we disable "BACK" to keep the + button_data = [self.EDIT, self.DONE] + + # Because we have an explicit "Edit" button, we disable "BACK" to keep the # routing options sane. - selected_menu_num = seed_screens.SeedReviewPassphraseScreen( + selected_menu_num = self.run_screen( + seed_screens.SeedReviewPassphraseScreen, fingerprint_without=fingerprint_without, fingerprint_with=fingerprint_with, passphrase=self.seed.passphrase, button_data=button_data, show_back_button=False, - ).display() + ) - if button_data[selected_menu_num] == EDIT: + if button_data[selected_menu_num] == self.EDIT: return Destination(SeedAddPassphraseView) - elif button_data[selected_menu_num] == DONE: + elif button_data[selected_menu_num] == self.DONE: seed_num = self.controller.storage.finalize_pending_seed() return Destination(SeedOptionsView, view_args={"seed_num": seed_num}, clear_history=True) - + class SeedDiscardView(View): + KEEP = "Keep Seed" + DISCARD = ("Discard", None, None, "red") + def __init__(self, seed_num: int = None): super().__init__() self.seed_num = seed_num @@ -339,27 +427,29 @@ class SeedDiscardView(View): def run(self): - KEEP = _("Keep Seed") - DISCARD = (_("Discard"), None, None, "red") - button_data = [KEEP, DISCARD] + self.KEEP = _("Keep Seed") + self.DISCARD = (_("Discard"), None, None, "red") + + button_data = [self.KEEP, self.DISCARD] fingerprint = self.seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)) - selected_menu_num = WarningScreen( + selected_menu_num = self.run_screen( + WarningScreen, title=_("Discard Seed?"), status_headline=None, text=_("Wipe seed {} from the device?").format(fingerprint), show_back_button=False, button_data=button_data, - ).display() + ) - if button_data[selected_menu_num] == KEEP: + if button_data[selected_menu_num] == self.KEEP: # Use skip_current_view=True to prevent BACK from landing on this warning screen if self.seed_num is not None: return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num}, skip_current_view=True) else: return Destination(SeedFinalizeView, skip_current_view=True) - elif button_data[selected_menu_num] == DISCARD: + elif button_data[selected_menu_num] == self.DISCARD: if self.seed_num is not None: self.controller.discard_seed(self.seed_num) else: @@ -372,6 +462,16 @@ class SeedDiscardView(View): Views for actions on individual seeds: ****************************************************************************""" class SeedOptionsView(View): + SCAN_PSBT = ("Scan PSBT", SeedSignerIconConstants.QRCODE) + VERIFY_ADDRESS = "Verify Addr" + EXPORT_XPUB = "Export Xpub" + EXPLORER = "Address Explorer" + SIGN_MESSAGE = "Sign Message" + BACKUP = ("Backup Seed", None, None, None, SeedSignerIconConstants.CHEVRON_RIGHT) + BIP85_CHILD_SEED = "BIP-85 Child Seed" + DISCARD = ("Discard Seed", None, None, "red") + + def __init__(self, seed_num: int): super().__init__() self.seed_num = seed_num @@ -381,91 +481,107 @@ class SeedOptionsView(View): def run(self): from seedsigner.views.psbt_views import PSBTOverviewView - SCAN_PSBT = ("Scan PSBT", FontAwesomeIconConstants.QRCODE) - REVIEW_PSBT = "Review PSBT" - VERIFY_ADDRESS = "Verify Addr" - EXPORT_XPUB = "Export Xpub" - EXPLORER = "Address Explorer" - BACKUP = ("Backup Seed", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT) - DISCARD = ("Discard Seed", None, None, "red") - - button_data = [] - - if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER: - # Jump straight back into the address explorer script type selection flow - # But do ont cancel the `resume_main_flow` as we'll still need that after - # derivation path is specified. - return Destination(SeedExportXpubScriptTypeView, view_args=dict(seed_num=self.seed_num, sig_type=SettingsConstants.SINGLE_SIG), skip_current_view=True) - + self.SCAN_PSBT = (_("Scan PSBT"), SeedSignerIconConstants.QRCODE) + self.VERIFY_ADDRESS = _("Verify Addr") + self.EXPORT_XPUB = _("Export Xpub") + self.EXPLORER = _("Address Explorer") + self.BACKUP = (_("Backup Seed"), None, None, None, SeedSignerIconConstants.CHEVRON_RIGHT) + self.BIP85_CHILD_SEED = _("BIP-85 Child Seed") + self.DISCARD = (_("Discard Seed"), None, None, "red") if self.controller.unverified_address: if self.controller.resume_main_flow == Controller.FLOW__VERIFY_SINGLESIG_ADDR: # Jump straight back into the single sig addr verification flow self.controller.resume_main_flow = None return Destination(SeedAddressVerificationView, view_args=dict(seed_num=self.seed_num), skip_current_view=True) - addr = self.controller.unverified_address["address"][:7] - VERIFY_ADDRESS += f" {addr}" - button_data.append(VERIFY_ADDRESS) + if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER: + # Jump straight back into the address explorer script type selection flow + # But don't cancel the `resume_main_flow` as we'll still need that after + # derivation path is specified. + return Destination(SeedExportXpubScriptTypeView, view_args=dict(seed_num=self.seed_num, sig_type=SettingsConstants.SINGLE_SIG), skip_current_view=True) + + elif self.controller.resume_main_flow == Controller.FLOW__SIGN_MESSAGE: + self.controller.sign_message_data["seed_num"] = self.seed_num + return Destination(SeedSignMessageConfirmMessageView, skip_current_view=True) if self.controller.psbt: if PSBTParser.has_matching_input_fingerprint(self.controller.psbt, self.seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)): if self.controller.resume_main_flow and self.controller.resume_main_flow == Controller.FLOW__PSBT: - # Re-route us directly back to the start of the PSBT flow + # Re-route us directly back to the start of the PSBT flow self.controller.resume_main_flow = None self.controller.psbt_seed = self.seed return Destination(PSBTOverviewView, skip_current_view=True) - else: - # This seed does not seem to be a signer for this PSBT - # TODO: How sure are we? Should disable this entirely if we're 100% sure? - REVIEW_PSBT += " (?)" - button_data.append(REVIEW_PSBT) - else: - button_data.append(SCAN_PSBT) + + button_data = [] + + if self.controller.unverified_address: + addr = self.controller.unverified_address["address"][:7] + self.VERIFY_ADDRESS += f" {addr}" + button_data.append(self.VERIFY_ADDRESS) + + button_data.append(self.SCAN_PSBT) if self.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED: - button_data.append(EXPORT_XPUB) + button_data.append(self.EXPORT_XPUB) - button_data.append(EXPLORER) - button_data.append(BACKUP) - button_data.append(DISCARD) + button_data.append(self.EXPLORER) + button_data.append(self.BACKUP) - selected_menu_num = seed_screens.SeedOptionsScreen( + if self.settings.get_value(SettingsConstants.SETTING__MESSAGE_SIGNING) == SettingsConstants.OPTION__ENABLED: + button_data.append(self.SIGN_MESSAGE) + + if self.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__ENABLED: + button_data.append(self.BIP85_CHILD_SEED) + + button_data.append(self.DISCARD) + + selected_menu_num = self.run_screen( + seed_screens.SeedOptionsScreen, button_data=button_data, fingerprint=self.seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)), has_passphrase=self.seed.passphrase is not None, - ).display() + ) if selected_menu_num == RET_CODE__BACK_BUTTON: # Force BACK to always return to the Main Menu return Destination(MainMenuView) - if button_data[selected_menu_num] == REVIEW_PSBT: + if button_data[selected_menu_num] == self.SCAN_PSBT: + from seedsigner.views.scan_views import ScanPSBTView self.controller.psbt_seed = self.controller.get_seed(self.seed_num) - return Destination(PSBTOverviewView) + return Destination(ScanPSBTView) - if button_data[selected_menu_num] == SCAN_PSBT: - from seedsigner.views.scan_views import ScanView - return Destination(ScanView) - - elif button_data[selected_menu_num] == VERIFY_ADDRESS: + elif button_data[selected_menu_num] == self.VERIFY_ADDRESS: return Destination(SeedAddressVerificationView, view_args=dict(seed_num=self.seed_num)) - elif button_data[selected_menu_num] == EXPORT_XPUB: + elif button_data[selected_menu_num] == self.EXPORT_XPUB: return Destination(SeedExportXpubSigTypeView, view_args=dict(seed_num=self.seed_num)) - elif button_data[selected_menu_num] == EXPLORER: + elif button_data[selected_menu_num] == self.EXPLORER: self.controller.resume_main_flow = Controller.FLOW__ADDRESS_EXPLORER return Destination(SeedExportXpubScriptTypeView, view_args=dict(seed_num=self.seed_num, sig_type=SettingsConstants.SINGLE_SIG)) - elif button_data[selected_menu_num] == BACKUP: + elif button_data[selected_menu_num] == self.SIGN_MESSAGE: + from seedsigner.views.scan_views import ScanView + self.controller.sign_message_data = dict(seed_num=self.seed_num) + self.controller.resume_main_flow = Controller.FLOW__SIGN_MESSAGE + return Destination(ScanView) + + elif button_data[selected_menu_num] == self.BACKUP: return Destination(SeedBackupView, view_args=dict(seed_num=self.seed_num)) - elif button_data[selected_menu_num] == DISCARD: + elif button_data[selected_menu_num] == self.BIP85_CHILD_SEED: + return Destination(SeedBIP85ApplicationModeView, view_args={"seed_num": self.seed_num}) + + elif button_data[selected_menu_num] == self.DISCARD: return Destination(SeedDiscardView, view_args=dict(seed_num=self.seed_num)) class SeedBackupView(View): + VIEW_WORDS = "View Seed Words" + EXPORT_SEEDQR = "Export as SeedQR" + def __init__(self, seed_num): super().__init__() self.seed_num = seed_num @@ -473,22 +589,24 @@ class SeedBackupView(View): def run(self): - VIEW_WORDS = _("View Seed Words") - EXPORT_SEEDQR = _("Export as SeedQR") - button_data = [VIEW_WORDS, EXPORT_SEEDQR] + self.VIEW_WORDS = _("View Seed Words") + self.EXPORT_SEEDQR = _("Export as SeedQR") + button_data = [self.VIEW_WORDS, self.EXPORT_SEEDQR] - selected_menu_num = seed_screens.SeedBackupScreen( + selected_menu_num = self.run_screen( + ButtonListScreen, + title=_("Backup Seed"), button_data=button_data, - has_passphrase=self.seed.has_passphrase, - ).display() + is_bottom_list=True, + ) if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - elif button_data[selected_menu_num] == VIEW_WORDS: + elif button_data[selected_menu_num] == self.VIEW_WORDS: return Destination(SeedWordsWarningView, view_args={"seed_num": self.seed_num}) - elif button_data[selected_menu_num] == EXPORT_SEEDQR: + elif button_data[selected_menu_num] == self.EXPORT_SEEDQR: return Destination(SeedTranscribeSeedQRFormatView, view_args={"seed_num": self.seed_num}) @@ -497,6 +615,9 @@ class SeedBackupView(View): Export Xpub flow ****************************************************************************""" class SeedExportXpubSigTypeView(View): + SINGLE_SIG = "Single Sig" + MULTISIG = "Multisig" + def __init__(self, seed_num: int): super().__init__() self.seed_num = seed_num @@ -507,22 +628,23 @@ class SeedExportXpubSigTypeView(View): # Nothing to select; skip this screen return Destination(SeedExportXpubScriptTypeView, view_args={"seed_num": self.seed_num, "sig_type": self.settings.get_value(SettingsConstants.SETTING__SIG_TYPES)[0]}, skip_current_view=True) - SINGLE_SIG = _("Single Sig") - MULTISIG = _("Multisig") - button_data=[SINGLE_SIG, MULTISIG] + self.SINGLE_SIG = _("Single Sig") + self.MULTISIG = _("Multisig") + button_data=[self.SINGLE_SIG, self.MULTISIG] - selected_menu_num = ButtonListScreen( + selected_menu_num = self.run_screen( + ButtonListScreen, title=_("Export Xpub"), button_data=button_data - ).display() + ) if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - if button_data[selected_menu_num] == SINGLE_SIG: + if button_data[selected_menu_num] == self.SINGLE_SIG: return Destination(SeedExportXpubScriptTypeView, view_args={"seed_num": self.seed_num, "sig_type": SettingsConstants.SINGLE_SIG}) - elif button_data[selected_menu_num] == MULTISIG: + elif button_data[selected_menu_num] == self.MULTISIG: return Destination(SeedExportXpubScriptTypeView, view_args={"seed_num": self.seed_num, "sig_type": SettingsConstants.MULTISIG}) @@ -546,29 +668,32 @@ class SeedExportXpubScriptTypeView(View): return Destination(ToolsAddressExplorerAddressTypeView, view_args=args, skip_current_view=True) else: return Destination(SeedExportXpubCoordinatorView, view_args=args, skip_current_view=True) - - button_data = [] - for script_type in self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES): - button_data.append(script_type) title = _("Export Xpub") if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER: title = _("Address Explorer") - selected_menu_num = ButtonListScreen( + button_data = [] + for script_type, display_name in SettingsConstants.ALL_SCRIPT_TYPES: + if script_type in self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES): + button_data.append((script_type, _(display_name))) + + selected_menu_num = self.run_screen( + ButtonListScreen, title=title, is_button_text_centered=False, - button_data=button_data, + button_data=[x[1] for x in button_data], is_bottom_list=True, - ).display() + ) if selected_menu_num == RET_CODE__BACK_BUTTON: + # If previous view is SeedOptionsView then that should be where resume_main_flow started (otherwise it would have been skipped). + if len(self.controller.back_stack) >= 2 and self.controller.back_stack[-2].View_cls == SeedOptionsView: + self.controller.resume_main_flow = None return Destination(BackStackView) else: - script_types_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES) - selected_display_name = button_data[selected_menu_num] - args["script_type"] = script_types_settings_entry.get_selection_option_value_by_display_name(selected_display_name) + args["script_type"] = button_data[selected_menu_num][0] if args["script_type"] == SettingsConstants.CUSTOM_DERIVATION: return Destination(SeedExportXpubCustomDerivationView, view_args=args) @@ -589,10 +714,12 @@ class SeedExportXpubCustomDerivationView(View): self.script_type = script_type self.custom_derivation_path = "m/" + def run(self): - ret = seed_screens.SeedExportXpubCustomDerivationScreen( + ret = self.run_screen( + seed_screens.SeedExportXpubCustomDerivationScreen, initial_value=self.custom_derivation_path, - ).display() + ) if ret == RET_CODE__BACK_BUTTON: return Destination(BackStackView) @@ -637,11 +764,12 @@ class SeedExportXpubCoordinatorView(View): args["coordinator"] = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[0] return Destination(SeedExportXpubWarningView, view_args=args, skip_current_view=True) - selected_menu_num = ButtonListScreen( + selected_menu_num = self.run_screen( + ButtonListScreen, title=_("Export Xpub"), is_button_text_centered=False, button_data=self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS), - ).display() + ) if selected_menu_num < len(self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)): args["coordinator"] = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)[selected_menu_num] @@ -679,10 +807,11 @@ class SeedExportXpubWarningView(View): # Skip the WarningView entirely return destination - selected_menu_num = WarningScreen( + selected_menu_num = self.run_screen( + WarningScreen, status_headline=_("Privacy Leak!"), text=_("Xpub can be used to view all future transactions."), - ).display() + ) if selected_menu_num == 0: # User clicked "I Understand" @@ -725,7 +854,8 @@ class SeedExportXpubDetailsView(View): else: # The derivation calc takes a few moments. Run the loading screen while we wait. - self.loading_screen = LoadingScreenThread(text="Generating xpub...") + from seedsigner.gui.screens.screen import LoadingScreenThread + self.loading_screen = LoadingScreenThread(text=_("Generating xpub...")) self.loading_screen.start() try: @@ -747,12 +877,13 @@ class SeedExportXpubDetailsView(View): finally: self.loading_screen.stop() - selected_menu_num = seed_screens.SeedExportXpubDetailsScreen( + selected_menu_num = self.run_screen( + seed_screens.SeedExportXpubDetailsScreen, fingerprint=fingerprint, has_passphrase=self.seed.passphrase is not None, derivation_path=derivation_path, xpub=xpub_base58, - ).display() + ) if selected_menu_num == 0: return Destination( @@ -780,6 +911,9 @@ class SeedExportXpubQRDisplayView(View): elif coordinator == SettingsConstants.COORDINATOR__BLUE_WALLET: qr_type = QRType.XPUB + elif coordinator == SettingsConstants.COORDINATOR__KEEPER: + qr_type = QRType.XPUB + elif coordinator == SettingsConstants.COORDINATOR__NUNCHUK: qr_type = QRType.XPUB__UR @@ -801,7 +935,10 @@ class SeedExportXpubQRDisplayView(View): def run(self): - QRDisplayScreen(qr_encoder=self.qr_encoder).display() + self.run_screen( + QRDisplayScreen, + qr_encoder=self.qr_encoder + ) return Destination(MainMenuView) @@ -811,24 +948,30 @@ class SeedExportXpubQRDisplayView(View): View Seed Words flow ****************************************************************************""" class SeedWordsWarningView(View): - def __init__(self, seed_num: int): + def __init__(self, seed_num: int, bip85_data: dict = None): super().__init__() self.seed_num = seed_num + self.bip85_data = bip85_data def run(self): destination = Destination( SeedWordsView, - view_args={"seed_num": self.seed_num, "page_index": 0}, + view_args=dict( + seed_num=self.seed_num, + page_index=0, + bip85_data=self.bip85_data + ), skip_current_view=True, # Prevent going BACK to WarningViews ) if self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__DISABLED: # Forward straight to showing the words return destination - selected_menu_num = DireWarningScreen( + selected_menu_num = self.run_screen( + DireWarningScreen, text=_("You must keep your seed words private & away from all online devices."), - ).display() + ) if selected_menu_num == 0: # User clicked "I Understand" @@ -840,15 +983,15 @@ class SeedWordsWarningView(View): class SeedWordsView(View): - def __init__(self, seed_num: int, page_index: int = 0): + def __init__(self, seed_num: int, bip85_data: dict = None, page_index: int = 0): super().__init__() self.seed_num = seed_num if self.seed_num is None: self.seed = self.controller.storage.get_pending_seed() else: self.seed = self.controller.get_seed(self.seed_num) + self.bip85_data = bip85_data self.page_index = page_index - self.num_pages=int(len(self.seed.mnemonic_list)/4) def run(self): @@ -858,20 +1001,26 @@ class SeedWordsView(View): # Slice the mnemonic to our current 4-word section words_per_page = 4 # TODO: eventually make this configurable for bigger screens? - mnemonic = self.seed.mnemonic_display_list + if self.bip85_data is not None: + mnemonic = self.seed.get_bip85_child_mnemonic(self.bip85_data["child_index"], self.bip85_data["num_words"]).split() + title = _("Child #{}").format(self.bip85_data["child_index"]) + else: + mnemonic = self.seed.mnemonic_display_list + title = _("Seed Words") words = mnemonic[self.page_index*words_per_page:(self.page_index + 1)*words_per_page] button_data = [] - if self.page_index < self.num_pages - 1 or self.seed_num is None: + num_pages = int(len(mnemonic)/words_per_page) + if self.page_index < num_pages - 1 or self.seed_num is None: button_data.append(NEXT) else: button_data.append(DONE) selected_menu_num = seed_screens.SeedWordsScreen( - title=f"Seed Words: {self.page_index+1}/{self.num_pages}", + title=f"{title}: {self.page_index+1}/{num_pages}", words=words, page_index=self.page_index, - num_pages=self.num_pages, + num_pages=num_pages, button_data=button_data, ).display() @@ -879,14 +1028,129 @@ class SeedWordsView(View): return Destination(BackStackView) if button_data[selected_menu_num] == NEXT: - if self.seed_num is None and self.page_index == self.num_pages - 1: - return Destination(SeedWordsBackupTestPromptView, view_args=dict(seed_num=self.seed_num)) + if self.seed_num is None and self.page_index == num_pages - 1: + return Destination( + SeedWordsBackupTestPromptView, + view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data), + ) else: - return Destination(SeedWordsView, view_args=dict(seed_num=self.seed_num, page_index=self.page_index + 1)) + return Destination( + SeedWordsView, + view_args=dict(seed_num=self.seed_num, page_index=self.page_index + 1, bip85_data=self.bip85_data) + ) elif button_data[selected_menu_num] == DONE: # Must clear history to avoid BACK button returning to private info - return Destination(SeedWordsBackupTestPromptView, view_args=dict(seed_num=self.seed_num)) + return Destination( + SeedWordsBackupTestPromptView, + view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data), + ) + + + +"""**************************************************************************** + BIP85 - Derive child mnemonic (seed) flow +****************************************************************************""" +class SeedBIP85ApplicationModeView(View): + """ + * Ask the user the application type as defined in the BIP0085 spec. + * Currently only Word mode of 12, 24 words (Application number: 39') + * Possible future additions are + * WIF (HDSEED) + * XPRV (BIP32) + """ + def __init__(self, seed_num: int): + super().__init__() + self.seed_num = seed_num + self.num_words = 0 + self.bip85_app_num = 39 # TODO: Support other Application numbers + + + def run(self): + # TODO: Future enhancement to display WIF (HD-SEED) and XPRV (Bip32)? + WORDS_12 = _("12 Words") + WORDS_24 = _("24 Words") + + button_data = [WORDS_12, WORDS_24] + + selected_menu_num = ButtonListScreen( + title=_("BIP-85 Num Words"), + button_data=button_data + ).display() + + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + if button_data[selected_menu_num] == WORDS_12: + self.num_words = 12 + elif button_data[selected_menu_num] == WORDS_24: + self.num_words = 24 + + return Destination( + SeedBIP85SelectChildIndexView, + view_args=dict(seed_num=self.seed_num, num_words=self.num_words) + ) + + + +# View to retrieve the derived seed index +class SeedBIP85SelectChildIndexView(View): + def __init__(self, seed_num: int, num_words: int): + super().__init__() + self.seed_num = seed_num + self.num_words = num_words + + + def run(self): + # Change this later to use the generic Screen input keyboard + ret = seed_screens.SeedBIP85SelectChildIndexScreen().display() + + if ret == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + if not 0 <= int(ret) < 2**31: + return Destination( + SeedBIP85InvalidChildIndexView, + view_args=dict( + seed_num=self.seed_num, + num_words=self.num_words + ), + skip_current_view=True + ) + + return Destination( + SeedWordsWarningView, + view_args=dict( + seed_num=self.seed_num, + bip85_data=dict(child_index=int(ret), num_words=self.num_words), + ) + ) + + +class SeedBIP85InvalidChildIndexView(View): + def __init__(self, seed_num: int, num_words: int): + super().__init__() + self.seed_num = seed_num + self.num_words = num_words + + + def run(self): + DireWarningScreen( + title=_("BIP-85 Index Error"), + show_back_button=False, + status_headline=_("Invalid Child Index"), + text=_("BIP-85 Child Index must be between 0 and 2^31-1."), + button_data=[_("Try Again")] + ).display() + + return Destination( + SeedBIP85SelectChildIndexView, + view_args=dict( + seed_num=self.seed_num, + num_words=self.num_words + ), + skip_current_view=True + ) @@ -894,9 +1158,11 @@ class SeedWordsView(View): Seed Words Backup Test ****************************************************************************""" class SeedWordsBackupTestPromptView(View): - def __init__(self, seed_num: int): + def __init__(self, seed_num: int, bip85_data: dict = None): + super().__init__() self.seed_num = seed_num - + self.bip85_data = bip85_data + def run(self): VERIFY = _("Verify") @@ -907,7 +1173,10 @@ class SeedWordsBackupTestPromptView(View): ).display() if button_data[selected_menu_num] == VERIFY: - return Destination(SeedWordsBackupTestView, view_args=dict(seed_num=self.seed_num)) + return Destination( + SeedWordsBackupTestView, + view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data), + ) elif button_data[selected_menu_num] == SKIP: if self.seed_num is not None: @@ -918,19 +1187,24 @@ class SeedWordsBackupTestPromptView(View): class SeedWordsBackupTestView(View): - def __init__(self, seed_num: int, confirmed_list: List[bool] = None, cur_index: int = None): + def __init__(self, seed_num: int, bip85_data: dict = None, confirmed_list: List[bool] = None, cur_index: int = None): super().__init__() self.seed_num = seed_num if self.seed_num is None: self.seed = self.controller.storage.get_pending_seed() else: self.seed = self.controller.get_seed(self.seed_num) + self.bip85_data = bip85_data + + if self.bip85_data is not None: + self.mnemonic_list = self.seed.get_bip85_child_mnemonic(self.bip85_data["child_index"], self.bip85_data["num_words"]).split() + else: + self.mnemonic_list = self.seed.mnemonic_display_list - self.mnemonic_list = self.seed.mnemonic_display_list self.confirmed_list = confirmed_list if not self.confirmed_list: self.confirmed_list = [] - + self.cur_index = cur_index @@ -939,7 +1213,7 @@ class SeedWordsBackupTestView(View): self.cur_index = int(random.random() * len(self.mnemonic_list)) while self.cur_index in self.confirmed_list: self.cur_index = int(random.random() * len(self.mnemonic_list)) - + real_word = self.mnemonic_list[self.cur_index] fake_word1 = bip39.WORDLIST[int(random.random() * 2047)] fake_word2 = bip39.WORDLIST[int(random.random() * 2047)] @@ -960,17 +1234,24 @@ class SeedWordsBackupTestView(View): self.confirmed_list.append(self.cur_index) if len(self.confirmed_list) == len(self.mnemonic_list): # Successfully confirmed the full mnemonic! - return Destination(SeedWordsBackupTestSuccessView, view_args=dict(seed_num=self.seed_num)) + return Destination( + SeedWordsBackupTestSuccessView, + view_args=dict(seed_num=self.seed_num), + ) else: # Continue testing the remaining words - return Destination(SeedWordsBackupTestView, view_args=dict(seed_num=self.seed_num, confirmed_list=self.confirmed_list)) - + return Destination( + SeedWordsBackupTestView, + view_args=dict(seed_num=self.seed_num, confirmed_list=self.confirmed_list, bip85_data=self.bip85_data), + ) + else: # Picked the WRONG WORD! return Destination( SeedWordsBackupTestMistakeView, view_args=dict( seed_num=self.seed_num, + bip85_data=self.bip85_data, cur_index=self.cur_index, wrong_word=button_data[selected_menu_num], confirmed_list=self.confirmed_list, @@ -980,14 +1261,15 @@ class SeedWordsBackupTestView(View): class SeedWordsBackupTestMistakeView(View): - def __init__(self, seed_num: int, cur_index: int, wrong_word: str, confirmed_list: List[bool] = None): + def __init__(self, seed_num: int, bip85_data: dict = None, cur_index: int = None, wrong_word: str = None, confirmed_list: List[bool] = None): super().__init__() self.seed_num = seed_num + self.bip85_data = bip85_data self.cur_index = cur_index self.wrong_word = wrong_word self.confirmed_list = confirmed_list - + def run(self): REVIEW = _("Review Seed Words") RETRY = _("Try Again") @@ -1002,17 +1284,29 @@ class SeedWordsBackupTestMistakeView(View): ).display() if button_data[selected_menu_num] == REVIEW: - return Destination(SeedWordsView, view_args=dict(seed_num=self.seed_num)) - + return Destination( + SeedWordsView, + view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data), + ) + elif button_data[selected_menu_num] == RETRY: - return Destination(SeedWordsBackupTestView, view_args=dict(seed_num=self.seed_num, confirmed_list=self.confirmed_list, cur_index=self.cur_index)) - + return Destination( + SeedWordsBackupTestView, + view_args=dict( + seed_num=self.seed_num, + confirmed_list=self.confirmed_list, + cur_index=self.cur_index, + bip85_data=self.bip85_data, + ) + ) + class SeedWordsBackupTestSuccessView(View): def __init__(self, seed_num: int): + super().__init__() self.seed_num = seed_num - + def run(self): LargeIconStatusScreen( title=_("Backup Verified"), @@ -1118,7 +1412,7 @@ class SeedTranscribeSeedQRWarningView(View): selected_menu_num = DireWarningScreen( status_headline=_("SeedQR is your private key!"), - text=_("Never photograph it or scan it into an online device."), + text=_("Never photograph or scan it into a device that connects to the internet."), ).display() if selected_menu_num == RET_CODE__BACK_BUTTON: @@ -1210,7 +1504,7 @@ class SeedTranscribeSeedQRConfirmQRPromptView(View): def run(self): - SCAN = (_("Confirm SeedQR"), FontAwesomeIconConstants.QRCODE) + SCAN = (_("Confirm SeedQR"), SeedSignerIconConstants.QRCODE) DONE = _("Done") button_data = [SCAN, DONE] @@ -1312,21 +1606,22 @@ class AddressVerificationStartView(View): sig_type = SettingsConstants.MULTISIG if self.controller.multisig_wallet_descriptor: # Can jump straight to the brute-force verification View - destination = Destination(SeedAddressVerificationView) + destination = Destination(SeedAddressVerificationView, skip_current_view=True) else: self.controller.resume_main_flow = Controller.FLOW__VERIFY_MULTISIG_ADDR - destination = Destination(LoadMultisigWalletDescriptorView) + destination = Destination(LoadMultisigWalletDescriptorView, skip_current_view=True) else: sig_type = SettingsConstants.SINGLE_SIG - destination = Destination(SeedSingleSigAddressVerificationSelectSeedView) + destination = Destination(SeedSelectSeedView, skip_current_view=True) elif self.controller.unverified_address["script_type"] == SettingsConstants.TAPROOT: - destination = Destination(NotYetImplementedView) + # TODO: add Taproot support + return Destination(NotYetImplementedView) elif self.controller.unverified_address["script_type"] == SettingsConstants.LEGACY_P2PKH: # TODO: detect single sig vs multisig or have to prompt? - destination = Destination(NotYetImplementedView) + return Destination(NotYetImplementedView) derivation_path = embit_utils.get_standard_derivation_path( network=self.controller.unverified_address["network"], @@ -1361,7 +1656,7 @@ class AddressVerificationSigTypeView(View): elif button_data[selected_menu_num] == SINGLE_SIG: sig_type = SettingsConstants.SINGLE_SIG - destination = Destination(SeedSingleSigAddressVerificationSelectSeedView) + destination = Destination(SeedSelectSeedView, view_args=dict(flow=Controller.FLOW__VERIFY_SINGLESIG_ADDR)) elif button_data[selected_menu_num] == MULTISIG: sig_type = SettingsConstants.MULTISIG @@ -1383,66 +1678,6 @@ class AddressVerificationSigTypeView(View): -class SeedSingleSigAddressVerificationSelectSeedView(View): - def run(self): - seeds = self.controller.storage.seeds - - SCAN_SEED = (_("Scan a seed"), FontAwesomeIconConstants.QRCODE) - TYPE_12WORD = (_("Enter 12-word seed"), FontAwesomeIconConstants.KEYBOARD) - TYPE_24WORD = (_("Enter 24-word seed"), FontAwesomeIconConstants.KEYBOARD) - button_data = [] - - text = _("Load the seed to verify") - - for seed in seeds: - button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)) - - if seed.passphrase is not None: - # TODO: Include lock icon on right side of button - pass - button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue")) - - text = _("Select seed to verify") - - button_data.append(SCAN_SEED) - button_data.append(TYPE_12WORD) - button_data.append(TYPE_24WORD) - - selected_menu_num = seed_screens.SeedSingleSigAddressVerificationSelectSeedScreen( - title=_("Verify Address"), - text=text, - is_button_text_centered=False, - button_data=button_data - ).display() - - if selected_menu_num == RET_CODE__BACK_BUTTON: - return Destination(BackStackView) - - if len(seeds) > 0 and selected_menu_num < len(seeds): - # User selected one of the n seeds - return Destination( - SeedAddressVerificationView, - view_args=dict( - seed_num=selected_menu_num, - ) - ) - - self.controller.resume_main_flow = Controller.FLOW__VERIFY_SINGLESIG_ADDR - - if button_data[selected_menu_num] == SCAN_SEED: - from seedsigner.views.scan_views import ScanView - return Destination(ScanView) - - elif button_data[selected_menu_num] in [TYPE_12WORD, TYPE_24WORD]: - from seedsigner.views.seed_views import SeedMnemonicEntryView - if button_data[selected_menu_num] == TYPE_12WORD: - self.controller.storage.init_pending_mnemonic(num_words=12) - else: - self.controller.storage.init_pending_mnemonic(num_words=24) - return Destination(SeedMnemonicEntryView) - - - class SeedAddressVerificationView(View): """ Creates a worker thread to brute-force calculate addresses. Writes its @@ -1639,7 +1874,8 @@ class AddressVerificationSuccessView(View): LargeIconStatusScreen( status_headline=_("Address Verified"), - text=f"""{address[:7]} = {source}'s {"change" if verified_index_is_change else "receive"} address #{verified_index}.""" + text=f"""{address[:7]} = {source}'s {"change" if verified_index_is_change else "receive"} address #{verified_index}.""", + show_back_button=False, ).display() return Destination(MainMenuView) @@ -1648,7 +1884,7 @@ class AddressVerificationSuccessView(View): class LoadMultisigWalletDescriptorView(View): def run(self): - SCAN = (_("Scan Descriptor"), FontAwesomeIconConstants.QRCODE) + SCAN = (_("Scan Descriptor"), SeedSignerIconConstants.QRCODE) CANCEL = _("Cancel") button_data = [SCAN, CANCEL] selected_menu_num = seed_screens.LoadMultisigWalletDescriptorScreen( @@ -1657,8 +1893,9 @@ class LoadMultisigWalletDescriptorView(View): ).display() if button_data[selected_menu_num] == SCAN: - return Destination(ScanView) - + from seedsigner.views.scan_views import ScanWalletDescriptorView + return Destination(ScanWalletDescriptorView) + elif button_data[selected_menu_num] == CANCEL: if self.controller.resume_main_flow == Controller.FLOW__PSBT: return Destination(BackStackView) @@ -1677,6 +1914,7 @@ class MultisigWalletDescriptorView(View): fingerprints.append(fingerprint) policy = descriptor.brief_policy.split("multisig")[0].strip() + policy = " / ".join(policy.split(" of ")) # i18n w/o l10n since coming from non-l10n embit RETURN = _("Return to PSBT") VERIFY = _("Verify Addr") @@ -1705,6 +1943,7 @@ class MultisigWalletDescriptorView(View): elif button_data[selected_menu_num] == RETURN: # Jump straight back to PSBT change verification + from seedsigner.views.psbt_views import PSBTChangeDetailsView self.controller.resume_main_flow = None return Destination(PSBTChangeDetailsView, view_args=dict(change_address_num=0)) @@ -1718,3 +1957,176 @@ class MultisigWalletDescriptorView(View): return Destination(ToolsAddressExplorerAddressTypeView) return Destination(MainMenuView) + + + +"""**************************************************************************** + Sign Message Views +****************************************************************************""" +class SeedSignMessageStartView(View): + """ + Routes users straight through to the "Sign" screen if a signing `seed_num` has + already been selected. Otherwise routes to `SeedSelectSeedView` to select or + load a seed first. + """ + def __init__(self, derivation_path: str, message: str): + super().__init__() + self.derivation_path = derivation_path + self.message = message + + if self.settings.get_value(SettingsConstants.SETTING__MESSAGE_SIGNING) == SettingsConstants.OPTION__DISABLED: + self.set_redirect(Destination(OptionDisabledView, view_args=dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING))) + return + + # calculate the actual receive address + addr_format = embit_utils.parse_derivation_path(derivation_path) + if not addr_format["clean_match"]: + raise NotYetImplementedView("Signing messages for custom derivation paths not supported") + + # Note: addr_format["network"] can be MAINNET or [TESTNET, REGTEST] + if self.settings.get_value(SettingsConstants.SETTING__NETWORK) not in addr_format["network"]: + from seedsigner.views.view import NetworkMismatchErrorView + self.set_redirect(Destination(NetworkMismatchErrorView, view_args=dict(text=f"Current network setting ({self.settings.get_value_display_name(SettingsConstants.SETTING__NETWORK)}) doesn't match {self.derivation_path}"))) + + # cleanup. Note: We could leave this in place so the user can resume the + # flow, but for now we avoid complications and keep things simple. + self.controller.resume_main_flow = None + return + + data = self.controller.sign_message_data + if not data: + data = {} + self.controller.sign_message_data = data + data["derivation_path"] = derivation_path + data["message"] = message + data["addr_format"] = addr_format + + # May be None + self.seed_num = data.get("seed_num") + + if self.seed_num is not None: + # We already know which seed we're signing with + self.set_redirect(Destination(SeedSignMessageConfirmMessageView, skip_current_view=True)) + else: + self.set_redirect(Destination(SeedSelectSeedView, view_args=dict(flow=Controller.FLOW__SIGN_MESSAGE), skip_current_view=True)) + + + +class SeedSignMessageConfirmMessageView(View): + def __init__(self, page_num: int = 0): + super().__init__() + self.page_num = page_num # Note: zero-indexed numbering! + + self.seed_num = self.controller.sign_message_data.get("seed_num") + if self.seed_num is None: + raise Exception("Routing error: seed_num hasn't been set") + + + def run(self): + from seedsigner.gui.screens.seed_screens import SeedSignMessageConfirmMessageScreen + + selected_menu_num = self.run_screen( + SeedSignMessageConfirmMessageScreen, + page_num=self.page_num, + ) + + if selected_menu_num == RET_CODE__BACK_BUTTON: + if self.page_num == 0: + # We're exiting this flow entirely + self.controller.resume_main_flow = None + self.controller.sign_message_data = None + return Destination(BackStackView) + + # User clicked "Next" + if self.page_num == len(self.controller.sign_message_data["paged_message"]) - 1: + # We've reached the end of the paged message + return Destination(SeedSignMessageConfirmAddressView) + else: + return Destination(SeedSignMessageConfirmMessageView, view_args=dict(page_num=self.page_num + 1)) + + + +class SeedSignMessageConfirmAddressView(View): + def __init__(self): + super().__init__() + data = self.controller.sign_message_data + seed_num = data.get("seed_num") + self.derivation_path = data.get("derivation_path") + + if seed_num is None or not self.derivation_path: + raise Exception("Routing error: sign_message_data hasn't been set") + + seed = self.controller.storage.seeds[seed_num] + addr_format = data.get("addr_format") + + # calculate the actual receive address + seed = self.controller.storage.seeds[seed_num] + addr_format = embit_utils.parse_derivation_path(self.derivation_path) + if not addr_format["clean_match"] or addr_format["script_type"] == SettingsConstants.CUSTOM_DERIVATION: + raise Exception("Signing messages for custom derivation paths not supported") + + if addr_format["network"] != SettingsConstants.MAINNET: + # We're in either Testnet or Regtest or...? + if self.settings.get_value(SettingsConstants.SETTING__NETWORK) in [SettingsConstants.TESTNET, SettingsConstants.REGTEST]: + addr_format["network"] = self.settings.get_value(SettingsConstants.SETTING__NETWORK) + else: + from seedsigner.views.view import NetworkMismatchErrorView + self.set_redirect(Destination(NetworkMismatchErrorView, view_args=dict(text=f"Current network setting ({self.settings.get_value_display_name(SettingsConstants.SETTING__NETWORK)}) doesn't match {self.derivation_path}"))) + + # cleanup. Note: We could leave this in place so the user can resume the + # flow, but for now we avoid complications and keep things simple. + self.controller.resume_main_flow = None + self.controller.sign_message_data = None + return + + xpub = seed.get_xpub(wallet_path=addr_format["wallet_derivation_path"], network=addr_format["network"]) + embit_network = embit_utils.get_embit_network_name(addr_format["network"]) + self.address = embit_utils.get_single_sig_address(xpub=xpub, script_type=addr_format["script_type"], index=addr_format["index"], is_change=addr_format["is_change"], embit_network=embit_network) + + + def run(self): + from seedsigner.gui.screens.seed_screens import SeedSignMessageConfirmAddressScreen + selected_menu_num = self.run_screen( + SeedSignMessageConfirmAddressScreen, + derivation_path=self.derivation_path, + address=self.address, + ) + + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + # User clicked "Sign Message" + return Destination(SeedSignMessageSignedMessageQRView) + + + +class SeedSignMessageSignedMessageQRView(View): + """ + Displays the signed message as a QR code. + """ + def __init__(self): + super().__init__() + data = self.controller.sign_message_data + + self.seed_num = data["seed_num"] + seed = self.controller.get_seed(self.seed_num) + derivation_path = data["derivation_path"] + message: str = data["message"] + + self.signed_message = embit_utils.sign_message(seed_bytes=seed.seed_bytes, derivation=derivation_path, msg=message.encode()) + + + def run(self): + qr_encoder = EncodeQR(qr_type=QRType.SIGN_MESSAGE, signed_message=self.signed_message) + + self.run_screen( + QRDisplayScreen, + qr_encoder=qr_encoder, + ) + + # cleanup + self.controller.resume_main_flow = None + self.controller.sign_message_data = None + + # Exiting/Canceling the QR display screen always returns Home + return Destination(MainMenuView, skip_current_view=True) diff --git a/src/seedsigner/views/settings_views.py b/src/seedsigner/views/settings_views.py index 6849a1fd..81636556 100644 --- a/src/seedsigner/views/settings_views.py +++ b/src/seedsigner/views/settings_views.py @@ -1,27 +1,39 @@ from gettext import gettext as _ +import logging +from seedsigner.gui.components import SeedSignerIconConstants +from seedsigner.hardware.microsd import MicroSD -from seedsigner.gui.components import SeedSignerCustomIconConstants -from seedsigner.views.main_menu_views import MainMenuView +from .view import View, Destination, MainMenuView + +from seedsigner.gui.components import SeedSignerIconConstants from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, settings_screens) -from seedsigner.models.settings import SettingsConstants, SettingsDefinition +from seedsigner.models.settings import Settings, SettingsConstants, SettingsDefinition -from .view import View, Destination +logger = logging.getLogger(__name__) + +from .view import View, Destination, BackStackView, MainMenuView class SettingsMenuView(View): - def __init__(self, visibility: str = SettingsConstants.VISIBILITY__GENERAL, selected_attr: str = None): + IO_TEST = "I/O test" + DONATE = "Donate" + + def __init__(self, visibility: str = SettingsConstants.VISIBILITY__GENERAL, selected_attr: str = None, initial_scroll: int = 0): super().__init__() self.visibility = visibility self.selected_attr = selected_attr + # Used to preserve the rendering position in the list + self.initial_scroll = initial_scroll + def run(self): - IO_TEST = _("I/O test") - DONATE = _("Donate") + self.IO_TEST = _("I/O test") + self.DONATE = _("Donate") settings_entries = SettingsDefinition.get_settings_entries( - visibiilty=self.visibility + visibility=self.visibility ) button_data=[_(e.display_name) for e in settings_entries] @@ -36,30 +48,35 @@ class SettingsMenuView(View): title = _("Settings") # Set up the next nested level of menuing - button_data.append((_("Advanced"), None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)) - next = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__ADVANCED}) + button_data.append((_("Advanced"), None, None, None, SeedSignerIconConstants.CHEVRON_RIGHT)) + next_destination = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__ADVANCED}) - button_data.append(IO_TEST) - button_data.append(DONATE) + button_data.append(self.IO_TEST) + button_data.append(self.DONATE) elif self.visibility == SettingsConstants.VISIBILITY__ADVANCED: title = _("Advanced") # So far there are no real Developer options; disabling for now - # button_data.append(("Developer Options", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)) - # next = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__DEVELOPER}) - next = None + # button_data.append(("Developer Options", None, None, None, SeedSignerIconConstants.CHEVRON_RIGHT)) + # next_destination = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__DEVELOPER}) + next_destination = None elif self.visibility == SettingsConstants.VISIBILITY__DEVELOPER: title = _("Dev Options") - next = None + next_destination = None - selected_menu_num = ButtonListScreen( + selected_menu_num = self.run_screen( + ButtonListScreen, title=title, is_button_text_centered=False, button_data=button_data, selected_button=selected_button, - ).display() + scroll_y_initial_offset=self.initial_scroll, + ) + + # Preserve our scroll position in this Screen so we can return + initial_scroll = self.screen.buttons[0].scroll_y if selected_menu_num == RET_CODE__BACK_BUTTON: if self.visibility == SettingsConstants.VISIBILITY__GENERAL: @@ -70,17 +87,16 @@ class SettingsMenuView(View): return Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__ADVANCED}) elif selected_menu_num == len(settings_entries): - return next + return next_destination - elif len(button_data) > selected_menu_num and button_data[selected_menu_num] == IO_TEST: + elif len(button_data) > selected_menu_num and button_data[selected_menu_num] == self.IO_TEST: return Destination(IOTestView) - elif len(button_data) > selected_menu_num and button_data[selected_menu_num] == DONATE: + elif len(button_data) > selected_menu_num and button_data[selected_menu_num] == self.DONATE: return Destination(DonateView) else: - # TODO: Free-entry types (are there any?) will need their own SettingsEntryUpdateFreeEntryView(?). - return Destination(SettingsEntryUpdateSelectionView, view_args={"attr_name": settings_entries[selected_menu_num].attr_name}) + return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=settings_entries[selected_menu_num].attr_name, parent_initial_scroll=initial_scroll)) @@ -89,10 +105,11 @@ class SettingsEntryUpdateSelectionView(View): Handles changes to all selection-type settings (Multiselect, SELECT_1, Enabled/Disabled, etc). """ - def __init__(self, attr_name: str): + def __init__(self, attr_name: str, parent_initial_scroll: int = 0, selected_button: int = None): super().__init__() self.settings_entry = SettingsDefinition.get_settings_entry(attr_name) - self.selected_button = None + self.selected_button = selected_button + self.parent_initial_scroll = parent_initial_scroll def run(self): @@ -105,6 +122,7 @@ class SettingsEntryUpdateSelectionView(View): else: display_name = value button_data.append(_(display_name)) + if (type(initial_value) == list and value in initial_value) or value == initial_value: checked_buttons.append(i) @@ -113,24 +131,26 @@ class SettingsEntryUpdateSelectionView(View): # selected option). self.selected_button = i - if not self.selected_button: + if self.selected_button is None: self.selected_button = 0 - - ret_value = settings_screens.SettingsEntryUpdateSelectionScreen( + + ret_value = self.run_screen( + settings_screens.SettingsEntryUpdateSelectionScreen, display_name=self.settings_entry.display_name, help_text=self.settings_entry.help_text, button_data=button_data, selected_button=self.selected_button, checked_buttons=checked_buttons, settings_entry_type=self.settings_entry.type, - ).display() + ) destination = None settings_menu_view_destination = Destination( SettingsMenuView, view_args={ "visibility": self.settings_entry.visibility, - "selected_attr": self.settings_entry.attr_name + "selected_attr": self.settings_entry.attr_name, + "initial_scroll": self.parent_initial_scroll, } ) @@ -170,7 +190,39 @@ class SettingsEntryUpdateSelectionView(View): # All selects stay in place; re-initialize where in the list we left off self.selected_button = ret_value - return self.run() + + return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=self.settings_entry.attr_name, parent_initial_scroll=self.parent_initial_scroll, selected_button=self.selected_button), skip_current_view=True) + + + +class SettingsIngestSettingsQRView(View): + def __init__(self, data: str): + super().__init__() + + # May raise an Exception which will bubble up to the Controller to display to the + # user. + self.config_name, settings_update_dict = Settings.parse_settingsqr(data) + + self.settings.update(settings_update_dict) + + if MicroSD.get_instance().is_inserted and self.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__ENABLED: + self.status_message = _("Persistent Settings enabled. Settings saved to SD card.") + else: + self.status_message = _("Settings updated in temporary memory") + + + def run(self): + from seedsigner.gui.screens.settings_screens import SettingsQRConfirmationScreen + self.run_screen( + SettingsQRConfirmationScreen, + title=_("Settings QR"), + config_name=self.config_name, + button_data=[_("Home")], + status_message=self.status_message, + ) + + # Only one exit point + return Destination(MainMenuView) @@ -179,7 +231,7 @@ class SettingsEntryUpdateSelectionView(View): ****************************************************************************""" class IOTestView(View): def run(self): - settings_screens.IOTestScreen().display() + self.run_screen(settings_screens.IOTestScreen) return Destination(SettingsMenuView) @@ -187,6 +239,6 @@ class IOTestView(View): class DonateView(View): def run(self): - settings_screens.DonateScreen().display() + self.run_screen(settings_screens.DonateScreen) return Destination(SettingsMenuView) diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index b6abf4c7..69a90600 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass import hashlib import os import time @@ -6,53 +7,64 @@ from gettext import gettext as _ from embit.descriptor import Descriptor from PIL import Image from PIL.ImageOps import autocontrast -from seedsigner.controller import Controller -from seedsigner.gui.screens.screen import LoadingScreenThread, QRDisplayScreen -from seedsigner.hardware.camera import Camera -from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants, SeedSignerCustomIconConstants +from seedsigner.controller import Controller +from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants, SeedSignerIconConstants from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen) -from seedsigner.gui.screens.tools_screens import ToolsCalcFinalWordDoneScreen, ToolsCalcFinalWordFinalizePromptScreen, ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsAddressExplorerAddressTypeScreen +from seedsigner.gui.screens.tools_screens import (ToolsCalcFinalWordDoneScreen, ToolsCalcFinalWordFinalizePromptScreen, + ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, + ToolsImageEntropyLivePreviewScreen, ToolsAddressExplorerAddressTypeScreen) from seedsigner.helpers import embit_utils, mnemonic_generation from seedsigner.models.encode_qr import EncodeQR from seedsigner.models.qr_type import QRType from seedsigner.models.seed import Seed from seedsigner.models.settings_definition import SettingsConstants -from seedsigner.views.seed_views import SeedDiscardView, SeedFinalizeView, SeedMnemonicEntryView, SeedWordsWarningView, SeedExportXpubScriptTypeView +from seedsigner.views.seed_views import SeedDiscardView, SeedFinalizeView, SeedMnemonicEntryView, SeedOptionsView, SeedWordsWarningView, SeedExportXpubScriptTypeView from .view import View, Destination, BackStackView class ToolsMenuView(View): + IMAGE = ("New seed", FontAwesomeIconConstants.CAMERA) + DICE = ("New seed", FontAwesomeIconConstants.DICE) + KEYBOARD = ("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD) + EXPLORER = "Address Explorer" + ADDRESS = "Verify Address" + def run(self): - IMAGE = (_(" New seed"), FontAwesomeIconConstants.CAMERA) - DICE = (_("New seed"), FontAwesomeIconConstants.DICE) - KEYBOARD = (_("Calc 12th/24th word"), FontAwesomeIconConstants.KEYBOARD) - EXPLORER = _("Address Explorer") - button_data = [IMAGE, DICE, KEYBOARD, EXPLORER] - screen = ButtonListScreen( + self.IMAGE = (_("New seed"), FontAwesomeIconConstants.CAMERA) + self.DICE = (_("New seed"), FontAwesomeIconConstants.DICE) + self.KEYBOARD = (_("Calc 12th/24th word"), FontAwesomeIconConstants.KEYBOARD) + self.EXPLORER = _("Address Explorer") + self.ADDRESS = _("Verify Address") + button_data = [self.IMAGE, self.DICE, self.KEYBOARD, self.EXPLORER, self.ADDRESS] + + selected_menu_num = self.run_screen( + ButtonListScreen, title=_("Tools"), is_button_text_centered=False, button_data=button_data ) - selected_menu_num = screen.display() if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - elif button_data[selected_menu_num] == IMAGE: + elif button_data[selected_menu_num] == self.IMAGE: return Destination(ToolsImageEntropyLivePreviewView) - elif button_data[selected_menu_num] == DICE: + elif button_data[selected_menu_num] == self.DICE: return Destination(ToolsDiceEntropyMnemonicLengthView) - elif button_data[selected_menu_num] == KEYBOARD: + elif button_data[selected_menu_num] == self.KEYBOARD: return Destination(ToolsCalcFinalWordNumWordsView) - elif button_data[selected_menu_num] == EXPLORER: + elif button_data[selected_menu_num] == self.EXPLORER: return Destination(ToolsAddressExplorerSelectSourceView) + elif button_data[selected_menu_num] == self.ADDRESS: + from seedsigner.views.scan_views import ScanAddressView + return Destination(ScanAddressView) @@ -75,6 +87,7 @@ class ToolsImageEntropyLivePreviewView(View): class ToolsImageEntropyFinalImageView(View): def run(self): if not self.controller.image_entropy_final_image: + from seedsigner.hardware.camera import Camera # Take the final full-res image camera = Camera.get_instance() camera.start_single_frame_mode(resolution=(720, 480)) @@ -217,9 +230,7 @@ class ToolsDiceEntropyEntryView(View): if ret == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - print(f"Dice rolls: {ret}") dice_seed_phrase = mnemonic_generation.generate_mnemonic_from_dice(ret) - print(f"""Mnemonic: "{dice_seed_phrase}" """) # Add the mnemonic as an in-memory Seed seed = Seed(dice_seed_phrase, wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)) @@ -234,28 +245,33 @@ class ToolsDiceEntropyEntryView(View): Calc final word Views ****************************************************************************""" class ToolsCalcFinalWordNumWordsView(View): + TWELVE = "12 words" + TWENTY_FOUR = "24 words" + def run(self): - TWELVE = _("12 words") - TWENTY_FOUR = _("24 words") + self.TWELVE = _("12 words") + self.TWENTY_FOUR = _("24 words") - button_data = [TWELVE, TWENTY_FOUR] - selected_menu_num = ButtonListScreen( + button_data = [self.TWELVE, self.TWENTY_FOUR] + + selected_menu_num = self.run_screen( + ButtonListScreen, title=_("Mnemonic Length"), is_bottom_list=True, is_button_text_centered=True, button_data=button_data, - ).display() + ) if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - elif button_data[selected_menu_num] == TWELVE: + elif button_data[selected_menu_num] == self.TWELVE: self.controller.storage.init_pending_mnemonic(12) # return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True)) return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True)) - elif button_data[selected_menu_num] == TWENTY_FOUR: + elif button_data[selected_menu_num] == self.TWENTY_FOUR: self.controller.storage.init_pending_mnemonic(24) # return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True)) @@ -272,9 +288,12 @@ class ToolsCalcFinalWordFinalizePromptView(View): else: num_entropy_bits = 3 - COIN_FLIPS = "Coin flip entropy" - SELECT_WORD = f"Word selection entropy" - ZEROS = "Finalize with zeros" + # TRANSLATOR_NOTE: Label to gather entropy through coin tosses + COIN_FLIPS = _("Coin flip entropy") + # TRANSLATOR_NOTE: Label to gather entropy through user specified BIP-39 word + SELECT_WORD = _("Word selection entropy") + # TRANSLATOR_NOTE: Label to allow user to default entropy as all-zeros + ZEROS = _("Finalize with zeros") button_data = [COIN_FLIPS, SELECT_WORD, ZEROS] selected_menu_num = ToolsCalcFinalWordFinalizePromptScreen( @@ -305,8 +324,7 @@ class ToolsCalcFinalWordFinalizePromptView(View): class ToolsCalcFinalWordCoinFlipsView(View): def run(self): - mnemonic = self.controller.storage.pending_mnemonic - mnemonic_length = len(mnemonic) + mnemonic_length = len(self.controller.storage.pending_mnemonic) if mnemonic_length == 12: total_flips = 7 @@ -321,68 +339,76 @@ class ToolsCalcFinalWordCoinFlipsView(View): return Destination(BackStackView) else: - print(ret_val) - binary_string = ret_val + "0" * (11 - total_flips) - wordlist_index = int(binary_string, 2) - wordlist = Seed.get_wordlist(self.controller.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)) - word = wordlist[wordlist_index] - self.controller.storage.update_pending_mnemonic(word, mnemonic_length - 1) - return Destination(ToolsCalcFinalWordShowFinalWordView, view_args=dict(coin_flips=ret_val)) class ToolsCalcFinalWordShowFinalWordView(View): - def __init__(self, coin_flips=None): + def __init__(self, coin_flips: str = None): super().__init__() - self.coin_flips = coin_flips - - - def run(self): # Construct the actual final word. The user's selected_final_word # contributes: # * 3 bits to a 24-word seed (plus 8-bit checksum) # * 7 bits to a 12-word seed (plus 4-bit checksum) from seedsigner.helpers import mnemonic_generation - mnemonic = self.controller.storage.pending_mnemonic - mnemonic_length = len(mnemonic) wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) wordlist = Seed.get_wordlist(wordlist_language_code) + # Prep the user's selected word / coin flips and the actual final word for + # the display. + if coin_flips: + self.selected_final_word = None + self.selected_final_bits = coin_flips + else: + # Convert the user's final word selection into its binary index equivalent + self.selected_final_word = self.controller.storage.pending_mnemonic[-1] + self.selected_final_bits = format(wordlist.index(self.selected_final_word), '011b') + + if coin_flips: + # fill the last bits (what will eventually be the checksum) with zeros + binary_string = coin_flips + "0" * (11 - len(coin_flips)) + + # retrieve the matching word for the resulting index + wordlist_index = int(binary_string, 2) + wordlist = Seed.get_wordlist(self.controller.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)) + word = wordlist[wordlist_index] + + # update the pending mnemonic with our new "final" (pre-checksum) word + self.controller.storage.update_pending_mnemonic(word, -1) + + # Now calculate the REAL final word (has a proper checksum) final_mnemonic = mnemonic_generation.calculate_checksum( mnemonic=self.controller.storage.pending_mnemonic, wordlist_language_code=wordlist_language_code, ) - self.controller.storage.update_pending_mnemonic(final_mnemonic[-1], mnemonic_length - 1) - # Prep the user's selected word (if there was one) and the actual final word for - # the display. - if self.coin_flips: - selected_final_word = None - selected_final_bits = self.coin_flips - else: - # Convert the user's final word selection into its binary index equivalent - selected_final_word = mnemonic[-1] - selected_final_bits = format(wordlist.index(selected_final_word), '011b') + # Update our pending mnemonic with the real final word + self.controller.storage.update_pending_mnemonic(final_mnemonic[-1], -1) + + mnemonic = self.controller.storage.pending_mnemonic + mnemonic_length = len(mnemonic) # And grab the actual final word's checksum bits - actual_final_word = self.controller.storage.pending_mnemonic[-1] - if mnemonic_length == 12: - checksum_bits = format(wordlist.index(actual_final_word), '011b')[-4:] - else: - checksum_bits = format(wordlist.index(actual_final_word), '011b')[-8:] + self.actual_final_word = self.controller.storage.pending_mnemonic[-1] + num_checksum_bits = 4 if mnemonic_length == 12 else 8 + self.checksum_bits = format(wordlist.index(self.actual_final_word), '011b')[-num_checksum_bits:] - NEXT = "Next" + + def run(self): + # l10n_note already exists. + NEXT = _("Next") button_data = [NEXT] - selected_menu_num = ToolsCalcFinalWordScreen( - title="Final Word Calc", + selected_menu_num = self.run_screen( + ToolsCalcFinalWordScreen, + # TRANSLATOR_NOTE: label to calculate the last word of a BIP-39 mnemonic seed phrase + title=_("Final Word Calc"), button_data=button_data, - selected_final_word=selected_final_word, - selected_final_bits=selected_final_bits, - checksum_bits=checksum_bits, - actual_final_word=actual_final_word, - ).display() + selected_final_word=self.selected_final_word, + selected_final_bits=self.selected_final_bits, + checksum_bits=self.checksum_bits, + actual_final_word=self.actual_final_word, + ) if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) @@ -421,38 +447,35 @@ class ToolsCalcFinalWordDoneView(View): return Destination(SeedDiscardView) - """**************************************************************************** Address Explorer Views ****************************************************************************""" class ToolsAddressExplorerSelectSourceView(View): - def run(self): - SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE) - SCAN_DESCRIPTOR = ("Scan wallet descriptor", FontAwesomeIconConstants.QRCODE) - TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) - TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) - button_data = [] + SCAN_SEED = ("Scan a seed", SeedSignerIconConstants.QRCODE) + SCAN_DESCRIPTOR = ("Scan wallet descriptor", SeedSignerIconConstants.QRCODE) + TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) + TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) + + def run(self): seeds = self.controller.storage.seeds + self.SCAN_SEED = (_("Scan a seed"), SeedSignerIconConstants.QRCODE) + self.SCAN_DESCRIPTOR = (_("Scan wallet descriptor"), SeedSignerIconConstants.QRCODE) + self.TYPE_12WORD = (_("Enter 12-word seed"), FontAwesomeIconConstants.KEYBOARD) + self.TYPE_24WORD = (_("Enter 24-word seed"), FontAwesomeIconConstants.KEYBOARD) + button_data = [] for seed in seeds: button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)) - - if seed.passphrase is not None: - # TODO: Include lock icon on right side of button - pass - button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue")) - - button_data.append(SCAN_SEED) - button_data.append(SCAN_DESCRIPTOR) - button_data.append(TYPE_12WORD) - button_data.append(TYPE_24WORD) - - selected_menu_num = ButtonListScreen( - title="Address Explorer", + button_data.append((button_str, SeedSignerIconConstants.FINGERPRINT)) + button_data = button_data + [self.SCAN_SEED, self.SCAN_DESCRIPTOR, self.TYPE_12WORD, self.TYPE_24WORD] + + selected_menu_num = self.run_screen( + ButtonListScreen, + title=_("Address Explorer"), button_data=button_data, is_button_text_centered=False, is_bottom_list=True, - ).display() + ) if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) @@ -472,13 +495,17 @@ class ToolsAddressExplorerSelectSourceView(View): ) ) - elif button_data[selected_menu_num] in [SCAN_SEED, SCAN_DESCRIPTOR]: - from seedsigner.views.scan_views import ScanView - return Destination(ScanView) + elif button_data[selected_menu_num] == self.SCAN_SEED: + from seedsigner.views.scan_views import ScanSeedQRView + return Destination(ScanSeedQRView) - elif button_data[selected_menu_num] in [TYPE_12WORD, TYPE_24WORD]: + elif button_data[selected_menu_num] == self.SCAN_DESCRIPTOR: + from seedsigner.views.scan_views import ScanWalletDescriptorView + return Destination(ScanWalletDescriptorView) + + elif button_data[selected_menu_num] in [self.TYPE_12WORD, self.TYPE_24WORD]: from seedsigner.views.seed_views import SeedMnemonicEntryView - if button_data[selected_menu_num] == TYPE_12WORD: + if button_data[selected_menu_num] == self.TYPE_12WORD: self.controller.storage.init_pending_mnemonic(num_words=12) else: self.controller.storage.init_pending_mnemonic(num_words=24) @@ -487,6 +514,10 @@ class ToolsAddressExplorerSelectSourceView(View): class ToolsAddressExplorerAddressTypeView(View): + RECEIVE = "Receive Addresses" + CHANGE = "Change Addresses" + + def __init__(self, seed_num: int = None, script_type: str = None, custom_derivation: str = None): """ If the explorer source is a seed, `seed_num` and `script_type` must be @@ -535,58 +566,75 @@ class ToolsAddressExplorerAddressTypeView(View): def run(self): data = self.controller.address_explorer_data - RECEIVE = "Receive Addresses" - CHANGE = "Change Addresses" - button_data = [RECEIVE, CHANGE] + # TRANSLATOR_NOTE: label for addresses where others send us incoming payments + self.RECEIVE = _("Receive Addresses") + # TRANSLATOR_NOTE: label for addresses that collect the change from our own outgoing payments + self.CHANGE = _("Change Addresses") wallet_descriptor_display_name = None if "wallet_descriptor" in data: wallet_descriptor_display_name = data["wallet_descriptor"].brief_policy.replace(" (sorted)", "") + wallet_descriptor_display_name = " / ".join(wallet_descriptor_display_name.split(" of ")) # i18n w/o l10n since coming from non-l10n embit script_type = data["script_type"] if "script_type" in data else None - selected_menu_num = ToolsAddressExplorerAddressTypeScreen( + button_data = [self.RECEIVE, self.CHANGE] + + selected_menu_num = self.run_screen( + ToolsAddressExplorerAddressTypeScreen, button_data=button_data, fingerprint=self.seed.get_fingerprint() if self.seed_num is not None else None, wallet_descriptor_display_name=wallet_descriptor_display_name, script_type=script_type, custom_derivation_path=self.custom_derivation, - ).display() + ) if selected_menu_num == RET_CODE__BACK_BUTTON: + # If we entered this flow via an already-loaded seed's SeedOptionsView, we + # need to clear the `resume_main_flow` so that we don't get stuck in a + # SeedOptionsView redirect loop. + # TODO: Refactor to a cleaner `BackStack.get_previous_View_cls()` + if len(self.controller.back_stack) > 1 and self.controller.back_stack[-2].View_cls == SeedOptionsView: + # The BackStack has the current View on the top with the real "back" in second position. + self.controller.resume_main_flow = None + self.controller.address_explorer_data = None return Destination(BackStackView) - elif button_data[selected_menu_num] in [RECEIVE, CHANGE]: - return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=button_data[selected_menu_num] == CHANGE)) + elif button_data[selected_menu_num] in [self.RECEIVE, self.CHANGE]: + return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=button_data[selected_menu_num] == self.CHANGE)) class ToolsAddressExplorerAddressListView(View): - def __init__(self, is_change: bool = False, start_index: int = 0, selected_button_index: int = 0): + def __init__(self, is_change: bool = False, start_index: int = 0, selected_button_index: int = 0, initial_scroll: int = 0): super().__init__() self.is_change = is_change self.start_index = start_index self.selected_button_index = selected_button_index + self.initial_scroll = initial_scroll def run(self): self.loading_screen = None - try: - addresses = [] - button_data = [] - data = self.controller.address_explorer_data - addrs_per_screen = 10 - addr_storage_key = "receive_addrs" - if self.is_change: - addr_storage_key = "change_addrs" + addresses = [] + button_data = [] + data = self.controller.address_explorer_data + addrs_per_screen = 10 - if addr_storage_key in data and len(data[addr_storage_key]) >= self.start_index + addrs_per_screen: - # We already calculated this range addresses; just retrieve them - addresses = data[addr_storage_key][self.start_index:self.start_index + addrs_per_screen] + addr_storage_key = "receive_addrs" + if self.is_change: + addr_storage_key = "change_addrs" - else: - self.loading_screen = LoadingScreenThread(text="Calculating addrs...") + if addr_storage_key in data and len(data[addr_storage_key]) >= self.start_index + addrs_per_screen: + # We already calculated this range of addresses; just retrieve them + addresses = data[addr_storage_key][self.start_index:self.start_index + addrs_per_screen] + + else: + try: + from seedsigner.gui.screens.screen import LoadingScreenThread + # TRANSLATOR_NOTE: a status message that our payment addresses are being calculated + self.loading_screen = LoadingScreenThread(text=_("Calculating addrs...")) self.loading_screen.start() if addr_storage_key not in data: @@ -614,34 +662,36 @@ class ToolsAddressExplorerAddressListView(View): else: raise Exception("Single sig descriptors not yet supported") - - for i, address in enumerate(addresses): - cur_index = i + self.start_index - if cur_index < 10: - end_digits = -6 - elif cur_index < 100: - end_digits = -5 - else: - end_digits = -4 - button_data.append(f"{cur_index}:{address[:8]}...{address[end_digits:]}") - - button_data.append(("Next {}".format(addrs_per_screen), None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)) - - screen = ButtonListScreen( - title="{} Addrs".format("Receive" if not self.is_change else "Change"), - button_data=button_data, - button_font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, - button_font_size=GUIConstants.BUTTON_FONT_SIZE + 4, - is_button_text_centered=False, - is_bottom_list=True, - selected_button=self.selected_button_index, - ) - finally: - # Everything is set. Stop the loading screen - if self.loading_screen: + finally: + # Everything is set. Stop the loading screen self.loading_screen.stop() - selected_menu_num = screen.display() + for i, address in enumerate(addresses): + cur_index = i + self.start_index + + # Adjust the trailing addr display length based on available room + # (the index number will push it out on each order of magnitude) + if cur_index < 10: + end_digits = -6 + elif cur_index < 100: + end_digits = -5 + else: + end_digits = -4 + button_data.append(f"{cur_index}:{address[:8]}...{address[end_digits:]}") + + button_data.append((_("Next {}").format(addrs_per_screen), None, None, None, SeedSignerIconConstants.CHEVRON_RIGHT)) + + selected_menu_num = self.run_screen( + ButtonListScreen, + title=_("Receive Addrs") if not self.is_change else _("Change Addrs"), + button_data=button_data, + button_font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, + button_font_size=GUIConstants.get_button_font_size() + 4, + is_button_text_centered=False, + is_bottom_list=True, + selected_button=self.selected_button_index, + scroll_y_initial_offset=self.initial_scroll, + ) if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) @@ -650,26 +700,31 @@ class ToolsAddressExplorerAddressListView(View): # User clicked NEXT return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=self.is_change, start_index=self.start_index + addrs_per_screen)) + # Preserve the list's current scroll so we can return to the same spot + initial_scroll = self.screen.buttons[0].scroll_y + index = selected_menu_num + self.start_index - return Destination(ToolsAddressExplorerAddressView, view_args=dict(index=index, address=addresses[selected_menu_num], is_change=self.is_change, start_index=self.start_index), skip_current_view=True) + return Destination(ToolsAddressExplorerAddressView, view_args=dict(index=index, address=addresses[selected_menu_num], is_change=self.is_change, start_index=self.start_index, parent_initial_scroll=initial_scroll), skip_current_view=True) class ToolsAddressExplorerAddressView(View): - def __init__(self, index: int, address: str, is_change: bool, start_index: int): + def __init__(self, index: int, address: str, is_change: bool, start_index: int, parent_initial_scroll: int = 0): super().__init__() self.index = index self.address = address self.is_change = is_change self.start_index = start_index + self.parent_initial_scroll = parent_initial_scroll - data = self.controller.address_explorer_data def run(self): + from seedsigner.gui.screens.screen import QRDisplayScreen qr_encoder = EncodeQR(qr_type=QRType.BITCOIN_ADDRESS, bitcoin_address=self.address) - QRDisplayScreen( + self.run_screen( + QRDisplayScreen, qr_encoder=qr_encoder, - ).display() + ) # Exiting/Cancelling the QR display screen always returns to the list - return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=self.is_change, start_index=self.start_index, selected_button_index=self.index - self.start_index), skip_current_view=True) + return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=self.is_change, start_index=self.start_index, selected_button_index=self.index - self.start_index, initial_scroll=self.parent_initial_scroll), skip_current_view=True) diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index d89c1c03..2869baa2 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -1,10 +1,13 @@ from dataclasses import dataclass from gettext import gettext as _ -from typing import List - -from seedsigner.gui.screens import RET_CODE__POWER_BUTTON -from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, DireWarningScreen, WarningScreen +from typing import Type +from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants +from seedsigner.gui.screens import RET_CODE__POWER_BUTTON, RET_CODE__BACK_BUTTON +from seedsigner.gui.screens.screen import BaseScreen, DireWarningScreen, LargeButtonScreen, PowerOffScreen, PowerOffNotRequiredScreen, ResetScreen, WarningScreen +from seedsigner.models.settings import Settings, SettingsConstants +from seedsigner.models.settings_definition import SettingsDefinition +from seedsigner.models.threads import BaseThread class BackStackView: @@ -41,11 +44,15 @@ class BackStackView: "Cancel" - End task and return to entry point (destructive) """ class View: - def __init__(self) -> None: + def _initialize(self): + """ + Whether the View is a regular class initialized by __init__() or a dataclass + initialized by __post_init__(), this method will be called to set up the View's + instance variables. + """ # Import here to avoid circular imports from seedsigner.controller import Controller from seedsigner.gui import Renderer - from seedsigner.models import Settings self.controller: Controller = Controller.get_instance() self.settings = Settings.get_instance() @@ -55,14 +62,57 @@ class View: self.canvas_width = self.renderer.canvas_width self.canvas_height = self.renderer.canvas_height - self.buttons = self.controller.buttons + self.screen = None + + self._redirect: 'Destination' = None - def run(self, **kwargs): - if hasattr(self, "screen"): - self.screen.display() - else: - raise Exception("Must implement in the child class") + def __init__(self): + self._initialize() + + + def __post_init__(self): + self._initialize() + + + @property + def has_redirect(self) -> bool: + if not hasattr(self, '_redirect'): + # Easy for a View to forget to call super().__init__() + raise Exception(f"{self.__class__.__name__} did not call super().__init__()") + return self._redirect is not None + + + def set_redirect(self, destination: 'Destination'): + """ + Enables early `__init__()` / `__post_init__()` logic to redirect away from the + current View. + + Set a redirect Destination and then immediately `return` to exit `__init__()` or + `__post_init__()`. When the `Destination.run()` is called, it will see the redirect + and immediately return that new Destination to the Controller without running + the View's `run()`. + """ + # Always insure skip_current_view is set for a redirect + destination.skip_current_view = True + self._redirect = destination + + + def get_redirect(self) -> 'Destination': + return self._redirect + + + def run_screen(self, Screen_cls: Type[BaseScreen], **kwargs) -> int | str: + """ + Instantiates the provided Screen_cls and runs its interactive display. + Returns the user's input upon completion. + """ + self.screen = Screen_cls(**kwargs) + return self.screen.display() + + + def run(self, **kwargs) -> 'Destination': + raise Exception("Must implement in the child class") @@ -72,10 +122,10 @@ class Destination: Basic struct to pass back to the Controller to tell it which View the user should be presented with next. """ - View_cls: View # The target View to route to - view_args: dict = None # The input args required to instantiate the target View - skip_current_view: bool = False # The current View is just forwarding; omit current View from history - clear_history: bool = False # Optionally clears the back_stack to prevent "back" + View_cls: Type[View] # The target View to route to + view_args: dict = None # The input args required to instantiate the target View + skip_current_view: bool = False # The current View is just forwarding; omit current View from history + clear_history: bool = False # Optionally clears the back_stack to prevent "back" def __repr__(self): @@ -92,12 +142,24 @@ class Destination: return out - def run(self): + def _instantiate_view(self): if not self.view_args: # Can't unpack (**) None so we replace with an empty dict self.view_args = {} - # Instantiate the `View_cls` and run() it with the `view_args` dict - return self.View_cls(**self.view_args).run() + + # Instantiate the `View_cls` with the `view_args` dict + self.view = self.View_cls(**self.view_args) + + + def _run_view(self): + if self.view.has_redirect: + return self.view.get_redirect() + return self.view.run() + + + def run(self): + self._instantiate_view() + return self._run_view() def __eq__(self, obj): @@ -114,36 +176,252 @@ class Destination: +######################################################################################### +# +# Root level Views don't have a sub-module home so they live at the top level here. +# +######################################################################################### +class MainMenuView(View): + SCAN = ("Scan", SeedSignerIconConstants.SCAN) + SEEDS = ("Seeds", SeedSignerIconConstants.SEEDS) + TOOLS = ("Tools", SeedSignerIconConstants.TOOLS) + SETTINGS = ("Settings", SeedSignerIconConstants.SETTINGS) + + def run(self): + self.SCAN = (_("Scan"), SeedSignerIconConstants.SCAN) + self.SEEDS = (_("Seeds"), SeedSignerIconConstants.SEEDS) + self.TOOLS = (_("Tools"), SeedSignerIconConstants.TOOLS) + self.SETTINGS = (_("Settings"), SeedSignerIconConstants.SETTINGS) + from seedsigner.gui.screens.screen import MainMenuScreen + button_data = [self.SCAN, self.SEEDS, self.TOOLS, self.SETTINGS] + selected_menu_num = self.run_screen( + MainMenuScreen, + title=_("Home"), + button_data=button_data, + ) + + if selected_menu_num == RET_CODE__POWER_BUTTON: + return Destination(PowerOptionsView) + + if button_data[selected_menu_num] == self.SCAN: + from seedsigner.views.scan_views import ScanView + return Destination(ScanView) + + elif button_data[selected_menu_num] == self.SEEDS: + from seedsigner.views.seed_views import SeedsMenuView + return Destination(SeedsMenuView) + + elif button_data[selected_menu_num] == self.TOOLS: + from seedsigner.views.tools_views import ToolsMenuView + return Destination(ToolsMenuView) + + elif button_data[selected_menu_num] == self.SETTINGS: + from seedsigner.views.settings_views import SettingsMenuView + return Destination(SettingsMenuView) + + + +class PowerOptionsView(View): + RESET = ("Restart", SeedSignerIconConstants.RESTART) + POWER_OFF = ("Power Off", SeedSignerIconConstants.POWER) + + def run(self): + self.RESET = (_("Restart"), SeedSignerIconConstants.RESTART) + self.POWER_OFF = (_("Power Off"), SeedSignerIconConstants.POWER) + button_data = [self.RESET, self.POWER_OFF] + selected_menu_num = self.run_screen( + LargeButtonScreen, + title=_("Reset / Power"), + show_back_button=True, + button_data=button_data + ) + + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + elif button_data[selected_menu_num] == self.RESET: + return Destination(RestartView) + + elif button_data[selected_menu_num] == self.POWER_OFF: + return Destination(PowerOffView) + + + +class RestartView(View): + def run(self): + thread = RestartView.DoResetThread() + thread.start() + self.run_screen(ResetScreen) + + + class DoResetThread(BaseThread): + def run(self): + import time + from subprocess import call + + # Give the screen just enough time to display the reset message before + # exiting. + time.sleep(0.25) + + # Kill the SeedSigner process; Running the process again. + # `.*` is a wildcard to detect either `python`` or `python3`. + if Settings.HOSTNAME == Settings.SEEDSIGNER_OS: + call("kill $(pidof python*) & python /opt/src/main.py", shell=True) + else: + call("kill $(ps aux | grep '[p]ython.*main.py' | awk '{print $2}')", shell=True) + + + +class PowerOffView(View): + def run(self): + if Settings.HOSTNAME == Settings.SEEDSIGNER_OS: + self.run_screen(PowerOffNotRequiredScreen) + return Destination(BackStackView) + else: + thread = PowerOffView.PowerOffThread() + thread.start() + self.run_screen(PowerOffScreen) + + + class PowerOffThread(BaseThread): + def run(self): + import time + from subprocess import call + while self.keep_running: + time.sleep(5) + call("sudo shutdown --poweroff now", shell=True) + + class NotYetImplementedView(View): """ Temporary View to use during dev. """ def run(self): - from seedsigner.views.main_menu_views import MainMenuView - WarningScreen( + self.run_screen( + WarningScreen, title=_("Work In Progress"), status_headline=_("Not Yet Implemented"), text=_("This is still on our to-do list!"), button_data=[_("Back to Main Menu")], - ).display() + ) return Destination(MainMenuView) -class UnhandledExceptionView(View): - def __init__(self, error: List[str]): - self.error = error +@dataclass +class ErrorView(View): + title: str = "Error" + show_back_button: bool = True + status_headline: str = None + text: str = None + button_text: str = None + next_destination: Destination = Destination(MainMenuView, clear_history=True) def run(self): - from seedsigner.views.main_menu_views import MainMenuView - DireWarningScreen( + self.title = _("Error") + self.run_screen( + WarningScreen, + title=self.title, + status_headline=self.status_headline, + text=self.text, + button_data=[self.button_text], + show_back_button=self.show_back_button, + ) + + return self.next_destination + + + +@dataclass +class NetworkMismatchErrorView(ErrorView): + title: str = "Network Mismatch" + show_back_button: bool = False + button_text: str = "Change Setting" + next_destination: Destination = None + + + def __post_init__(self): + super().__post_init__() + self.title = _("Network Mismatch") + self.button_text = _("Change Setting") + if not self.text: + self.text = f"Current network setting ({self.settings.get_value_display_name(SettingsConstants.SETTING__NETWORK)}) doesn't match current action." + + if not self.next_destination: + from seedsigner.views.settings_views import SettingsEntryUpdateSelectionView + self.next_destination = Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=SettingsConstants.SETTING__NETWORK), clear_history=True) + + + +@dataclass +class UnhandledExceptionView(View): + error: list[str] + + + def run(self): + self.run_screen( + DireWarningScreen, title=_("System Error"), - status_headline=self.error[0], - text=self.error[1] + "\n" + self.error[2], - button_data=[_("OK")], - show_back_button=False, - ).display() + allow_text_overflow=True, # Fit what we can, let the rest go off the edges + ) return Destination(MainMenuView, clear_history=True) + + + +@dataclass +class OptionDisabledView(View): + UPDATE_SETTING = "Update Setting" + DONE = "Done" + settings_attr: str + + def __post_init__(self): + super().__post_init__() + self.UPDATE_SETTING = _("Update Setting") + self.DONE = _("Done") + self.settings_entry = SettingsDefinition.get_settings_entry(self.settings_attr) + self.error_msg = f"\"{self.settings_entry.display_name}\" is currently disabled in Settings." + + + def run(self): + button_data = [self.UPDATE_SETTING, self.DONE] + selected_menu_num = self.run_screen( + WarningScreen, + title=_("Option Disabled"), + status_headline=None, + text=self.error_msg, + button_data=button_data, + show_back_button=False, + allow_text_overflow=True, # Fit what we can, let the rest go off the edges + ) + + if button_data[selected_menu_num] == self.UPDATE_SETTING: + from seedsigner.views.settings_views import SettingsEntryUpdateSelectionView + return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=self.settings_attr), clear_history=True) + else: + return Destination(MainMenuView, clear_history=True) + + + +class RemoveMicroSDWarningView(View): + """ + Warning to remove the microsd + """ + def __init__(self, next_view: View): + super().__init__() + self.next_view = next_view + + def run(self): + self.run_screen( + WarningScreen, + title=_("Security Tip"), + status_icon_name=FontAwesomeIconConstants.SDCARD, + status_headline="", + text=_("For maximum security,\nremove the MicroSD card\nbefore continuing."), + show_back_button=False, + button_data=[_("Continue")], + ) + + return Destination(self.next_view, clear_history=True) diff --git a/tests/README.md b/tests/README.md index aa79d2a7..fe2181fe 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,7 +1,13 @@ # Running Tests -You must install the test-runner dependencies: -``` +The tests are designed to be run on non-Raspi hardware. + +On your testing machine you'll have to install: +```bash +# general dependencies +pip3 install -r requirements.txt + +# test suite dependencies pip3 install -r tests/requirements.txt ``` @@ -24,3 +30,19 @@ Run a specific test: ``` pytest tests/test_this_file.py::test_this_specific_test ``` + +### Test Coverage +Run tests and generate test coverage +``` +coverage run -m pytest +``` + +Show the resulting test coverage details: +``` +coverage report +``` + +Generate the html overview: +``` +coverage html +``` diff --git a/tests/base.py b/tests/base.py new file mode 100644 index 00000000..496e1b86 --- /dev/null +++ b/tests/base.py @@ -0,0 +1,237 @@ +import sys +from dataclasses import dataclass +from mock import MagicMock, Mock, patch +from typing import Callable + +# Prevent importing modules w/Raspi hardware dependencies. +# These must precede any SeedSigner imports. +sys.modules['seedsigner.gui.renderer'] = MagicMock() +sys.modules['seedsigner.gui.screens.screensaver'] = MagicMock() +sys.modules['seedsigner.gui.toast'] = MagicMock() +sys.modules['seedsigner.views.screensaver'] = MagicMock() +sys.modules['seedsigner.hardware.buttons'] = MagicMock() +sys.modules['seedsigner.hardware.camera'] = MagicMock() + +from seedsigner.controller import Controller, FlowBasedTestException, StopFlowBasedTest +from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON +from seedsigner.hardware.microsd import MicroSD +from seedsigner.models.settings import Settings +from seedsigner.views.view import Destination, MainMenuView, View + + + + +class BaseTest: + + class MockMicroSD(Mock): + """ + A test suite-friendly replacement for `MicroSD` that gives a test explicit + control over the reported state of the SD card. + """ + # Tests are free to directly manipulate this attribute as needed (it's reset to + # True before each test in `BaseTest.setup_method()`). + is_inserted: bool = True + + + @classmethod + def setup_class(cls): + # Ensure there are no on-disk artifacts after running tests. + Settings.SETTINGS_FILENAME = "settings-test.json" + + # Mock out the loading screen so it can't spawn. View classes must import locally! + patch('seedsigner.gui.screens.screen.LoadingScreenThread').start() + + # Instantiate the mocked MicroSD; hold on to the instance so tests can manipulate + # it later. + cls.mock_microsd = BaseTest.MockMicroSD() + + # And mock it over `MicroSD`'s instance + MicroSD.get_instance = Mock(return_value=cls.mock_microsd) + + + @classmethod + def teardown_class(cls): + pass + + + @classmethod + def reset_settings(cls): + """ Wipe and re-initialize the Settings singleton """ + Settings._instance = None + BaseTest.remove_settings() + + + @classmethod + def remove_settings(cls): + """ If settings were written to disk, delete """ + import os + try: + if os.path.exists(Settings.SETTINGS_FILENAME): + os.remove(Settings.SETTINGS_FILENAME) + except: + print(f"{Settings.SETTINGS_FILENAME} could not be removed") + + + @classmethod + def reset_controller(cls): + """ Wipe and re-initialize the Controller singleton """ + Controller._instance = None + Controller.configure_instance() + + + def setup_method(self): + """ Guarantee a clean/default Controller, Settings, & MicroSD state for each test case """ + BaseTest.reset_controller() + BaseTest.reset_settings() + self.controller = Controller.get_instance() + self.settings = Settings.get_instance() + self.mock_microsd.is_inserted = True + + + def teardown_method(self): + BaseTest.remove_settings() + + + +@dataclass +class FlowStep: + """ + Trivial helper class to express FlowTest sequences below. + + * expected_view: verify that the current step in the sequence instantiates the right View. + * before_run: function that takes a View instance as an arg and modifies it before running the View. + * screen_return_value: mocked Screen interaction result: raw return value as if from the Screen. + * button_data_selection: mocked Screen interaction result: the View.button_data value of the desired option. + * is_redirect: expects the Destination to specify `skip_current_view=True`. + """ + expected_view: type[View] = None + before_run: Callable[[View], None] = None + screen_return_value: int | str = None + button_data_selection: str | tuple = None + is_redirect: bool = False + + def __post_init__(self): + if self.screen_return_value is not None and self.button_data_selection is not None: + raise Exception("Can't specify both `screen_return_value` and `button_data_selection`") + + + +class FlowTestInvalidButtonDataSelectionException(FlowBasedTestException): + """ The FlowStep's button_data_selection value was not found in the View's button_data """ + pass + + + +class FlowTestUnexpectedViewException(FlowBasedTestException): + """ The Controller's current View did not match the expected View in the FlowStep sequence """ + pass + + + +class FlowTestRunScreenNotExecutedException(FlowBasedTestException): + """ The View's run_screen() method was not called but the FlowStep expected it to need user input """ + pass + + + +class FlowTest(BaseTest): + """ Base class for any tests that do flow-based testing """ + + def stop_test(self): + """ Raise our special Exception to stop the test """ + raise StopFlowBasedTest() + + + def run_sequence(self, sequence: list[FlowStep], initial_destination_view_args: dict = None) -> None: + """ + Run a pre-set sequence of Views w/manually-specified return values in order to test + the Controller's flow control logic and the routing from View to View. + """ + with patch("seedsigner.views.view.Destination._run_view", autospec=True) as mock_run_view: + with patch("seedsigner.views.view.View.run_screen", autospec=True) as mock_run_screen: + def run_view(destination: Destination, *args, **kwargs): + """ Replaces Destination._run_view() """ + if len(sequence) == 0: + self.stop_test() + + cur_flow_step = sequence[0] + + # Verify that the View class specified in the test sequence matches the + # View class that is being run. + if destination.View_cls != cur_flow_step.expected_view: + raise FlowTestUnexpectedViewException(f"Expected {cur_flow_step.expected_view}, got {destination.View_cls}") + + # Run the optional pre-run function to modify the View. + if cur_flow_step.before_run: + cur_flow_step.before_run(destination.view) + + if cur_flow_step.is_redirect: + # The current View is going to auto-redirect without calling run_screen(), + # so we need to remove the current step from the sequence before the + # View.run() call below. + sequence.pop(0) + + if destination.view.has_redirect: + # TODO: Migrate all View redirects to use `View.set_redirect()` + # in their `__init__()` rather than `run()` and then refactor + # here to explicitly require `has_redirect` to be True. + # For now: Support the newer `set_redirect()` routing while + # still letting redirects get returned by `View.run()` further + # below. + return destination.view.get_redirect() + + # Some Views reach into their Screen's variables directly (e.g. + # Screen.buttons to preserve the scroll position), so we need to mock out the + # Screen instance that is created by the View. + destination.view.screen = MagicMock() + + prev_mock_run_screen_call_count = mock_run_screen.call_count + + # Run the View (with our mocked run_screen) and get the next Destination that results + destination = destination.view.run() + + if (cur_flow_step.button_data_selection or cur_flow_step.screen_return_value is not None) and mock_run_screen.call_count == prev_mock_run_screen_call_count: + # The FlowStep was expecting some kind of user interaction, but the View + # never called run_screen(). + raise FlowTestRunScreenNotExecutedException(f"View.run_screen() was not run for {destination.View_cls.__name__}") + + return destination + + def run_screen(view: View, *args, **kwargs): + """ Replaces View.run_screen() """ + # Return the return value specified in the test sequence and + # remove the completed test step from the sequence. + flow_step = sequence.pop(0) + + if flow_step.button_data_selection: + # We're mocking out the View.run_screen() method, so we'll get all of the + # input args that are normally passed into the Screen.run() method, + # including the button_data kwarg. + if "button_data" in kwargs: + if flow_step.button_data_selection not in kwargs.get("button_data") and flow_step.button_data_selection not in [RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON]: + raise FlowTestInvalidButtonDataSelectionException(f"'{flow_step.button_data_selection}' not found in button_data: {kwargs.get('button_data')}") + return kwargs.get("button_data").index(flow_step.button_data_selection) + else: + raise Exception(f"Can't specify `FlowStep.button_data_selection` if `button_data` isn't a kwarg in {view.__class__.__name__}'s run_screen()") + + elif type(flow_step.screen_return_value) in [StopFlowBasedTest, FlowBasedTestException]: + raise flow_step.screen_return_value + + return flow_step.screen_return_value + + # Mock out the Destination._run_view() method so we can verify the View class + # that is specified in the test sequence and then run the View. + mock_run_view.side_effect = run_view + + # Mock out the View.run_screen() method so we can provide the + # return value that is specified in the test sequence. + mock_run_screen.side_effect = run_screen + + # Start the Controller with the first View_cls specified in the test sequence + if sequence[0].expected_view != MainMenuView: + initial_destination = Destination(sequence[0].expected_view, view_args=initial_destination_view_args) + else: + initial_destination = None + + # Start the Controller and run the sequence + Controller.get_instance().start(initial_destination=initial_destination) diff --git a/tests/requirements.txt b/tests/requirements.txt index 6643e54e..5bdf1466 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,3 +1,4 @@ attrs==21.4.0 +coverage==7.2.1 +mock==4.0.3 pytest==6.2.4 -mock==4.0.3 \ No newline at end of file diff --git a/tests/screenshot_generator/README.md b/tests/screenshot_generator/README.md index b4ccb83b..7030691f 100644 --- a/tests/screenshot_generator/README.md +++ b/tests/screenshot_generator/README.md @@ -3,6 +3,7 @@ From the project root, run: ``` pytest tests/screenshot_generator/generator.py --locale en +pytest tests/screenshot_generator/generator.py ``` -By default it writes the screenshots to a dir that's parallel to the project root: `seedsigner-screenshots/`. +Writes the screenshots to a dir in the project root: `seedsigner-screenshots`. diff --git a/tests/screenshot_generator/generator.py b/tests/screenshot_generator/generator.py index 0315ab16..4e6c7ec8 100644 --- a/tests/screenshot_generator/generator.py +++ b/tests/screenshot_generator/generator.py @@ -1,24 +1,45 @@ import embit import os +import sys +import time import pathlib import pytest -from mock import Mock, patch +import shutil + +from mock import Mock, patch, MagicMock + +from seedsigner.helpers import embit_utils +from seedsigner.models.settings import Settings + +# Prevent importing modules w/Raspi hardware dependencies. +# These must precede any SeedSigner imports. +sys.modules['seedsigner.hardware.ST7789'] = MagicMock() +sys.modules['seedsigner.gui.screens.screensaver'] = MagicMock() +sys.modules['seedsigner.views.screensaver'] = MagicMock() +sys.modules['RPi'] = MagicMock() +sys.modules['RPi.GPIO'] = MagicMock() +sys.modules['seedsigner.hardware.camera'] = MagicMock() +sys.modules['seedsigner.hardware.microsd'] = MagicMock() + from seedsigner.controller import Controller from seedsigner.gui.renderer import Renderer +from seedsigner.gui.toast import BaseToastOverlayManagerThread, RemoveSDCardToastManagerThread, SDCardStateChangeToastManagerThread from seedsigner.hardware.buttons import HardwareButtons from seedsigner.hardware.camera import Camera +from seedsigner.hardware.microsd import MicroSD from seedsigner.models.decode_qr import DecodeQR from seedsigner.models.qr_type import QRType from seedsigner.models.seed import Seed -from seedsigner.models.settings import Settings from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition -from seedsigner.views import (main_menu_views, psbt_views, scan_views, seed_views, - settings_views, tools_views) -from seedsigner.views.view import View +from seedsigner.views import (MainMenuView, PowerOptionsView, RestartView, NotYetImplementedView, UnhandledExceptionView, + psbt_views, seed_views, settings_views, tools_views) +from seedsigner.views.view import ErrorView, NetworkMismatchErrorView, OptionDisabledView, PowerOffView, View from .utils import ScreenshotComplete, ScreenshotRenderer +# Ignore Pillow 10 DeprecationWarnings "use getbbox() or getlength() instead of getsize()" +import warnings; warnings.warn = lambda *args, **kwargs: None def test_generate_screenshots(target_locale): @@ -29,12 +50,8 @@ def test_generate_screenshots(target_locale): When the `Renderer` instance is needed, we patch in our own test-only `ScreenshotRenderer`. """ - # Disable hardware dependencies by essentially wiping out this class - HardwareButtons.get_instance = Mock() - Camera.get_instance = Mock() - # Prep the ScreenshotRenderer that will be patched over the normal Renderer - screenshot_root = "/Users/kdmukai/dev/seedsigner-screenshots" + screenshot_root = os.path.join(os.getcwd(), "seedsigner-screenshots") ScreenshotRenderer.configure_instance() screenshot_renderer: ScreenshotRenderer = ScreenshotRenderer.get_instance() @@ -42,123 +59,232 @@ def test_generate_screenshots(target_locale): Renderer.configure_instance = Mock() Renderer.get_instance = Mock(return_value=screenshot_renderer) + # Additional mocks needed + PowerOffView.PowerOffThread = Mock() # Don't let this View actually send the `shutdown` command! + controller = Controller.get_instance() - # Set up some test data that we'll need in the `Controller` for certain Views - mnemonic_12 = "forum undo fragile fade shy sign arrest garment culture tube off merit".split() - mnemonic_24 = "attack pizza motion avocado network gather crop fresh patrol unusual wild holiday candy pony ranch winter theme error hybrid van cereal salon goddess expire".split() - mnemonic_12b = ["abandon"] * 11 + ["about"] - seed_12 = Seed(mnemonic=mnemonic_12, passphrase="cap*BRACKET3stove", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) - seed_12b = Seed(mnemonic=mnemonic_12b, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) - seed_24 = Seed(mnemonic=mnemonic_24, passphrase="some-PASS*phrase9", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) - controller.storage.seeds.append(seed_12) - controller.storage.seeds.append(seed_12b) - controller.storage.set_pending_seed(seed_24) - # Load a PSBT into memory - BASE64_PSBT_1 = """cHNidP8BAP06AQIAAAAC5l4E3oEjI+H0im8t/K2nLmF5iJFdKEiuQs8ESveWJKcAAAAAAP3///8iBZMRhYIq4s/LmnTmKBi79M8ITirmsbO++63evK4utwAAAAAA/f///wZYQuoDAAAAACIAIAW5jm3UnC5fyjKCUZ8LTzjENtb/ioRTaBMXeSXsB3n+bK2fCgAAAAAWABReJY7akT1+d+jx475yBRWORdBd7VxbUgUAAAAAFgAU4wj9I/jB3GjNQudNZAca+7g9R16iWtYOAAAAABYAFIotPApLZlfscg8f3ppKqO3qA5nv7BnMFAAAAAAiACAs6SGc8qv4FwuNl0G0SpMZG8ODUEk5RXiWUcuzzw5iaRSfAhMAAAAAIgAgW0f5QxQIgVCGQqKzsvfkXZjUxdFop5sfez6Pt8mUbmZ1AgAAAAEAkgIAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////BQIRAgEB/////wJAvkAlAAAAACIAIIRPoo2LvkrwrhrYFhLhlP43izxbA4Eo6Y6iFFiQYdXRAAAAAAAAAAAmaiSqIant4vYcP3HR3v0/qZnfo2lTdVxpBol5mWK0i+vYNpdOjPkAAAAAAQErQL5AJQAAAAAiACCET6KNi75K8K4a2BYS4ZT+N4s8WwOBKOmOohRYkGHV0QEFR1EhArGhNdUqlR4BAOLGTMrY2ZJYTQNRudp7fU7i8crRJqgEIQNDxn7PjUzvsP6KYw4s7dmoZE0qO1K6MaM+2ScRZ7hyxFKuIgYCsaE11SqVHgEA4sZMytjZklhNA1G52nt9TuLxytEmqAQcc8XaCjAAAIABAACAAAAAgAIAAIAAAAAAAwAAACIGA0PGfs+NTO+w/opjDizt2ahkTSo7Uroxoz7ZJxFnuHLEHCK94akwAACAAQAAgAAAAIACAACAAAAAAAMAAAAAAQCSAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8FAhACAQH/////AkC+QCUAAAAAIgAghE+ijYu+SvCuGtgWEuGU/jeLPFsDgSjpjqIUWJBh1dEAAAAAAAAAACZqJKohqe3i9hw/cdHe/T+pmd+jaVN1XGkGiXmZYrSL69g2l06M+QAAAAABAStAvkAlAAAAACIAIIRPoo2LvkrwrhrYFhLhlP43izxbA4Eo6Y6iFFiQYdXRAQVHUSECsaE11SqVHgEA4sZMytjZklhNA1G52nt9TuLxytEmqAQhA0PGfs+NTO+w/opjDizt2ahkTSo7Uroxoz7ZJxFnuHLEUq4iBgKxoTXVKpUeAQDixkzK2NmSWE0DUbnae31O4vHK0SaoBBxzxdoKMAAAgAEAAIAAAACAAgAAgAAAAAADAAAAIgYDQ8Z+z41M77D+imMOLO3ZqGRNKjtSujGjPtknEWe4csQcIr3hqTAAAIABAACAAAAAgAIAAIAAAAAAAwAAAAABAUdRIQJ5XLCBS0hdo4NANq4lNhimzhyHj7dvObmPAwNj8L2xASEC9mwwoH28/WHnxbb6z05sJ/lHuvrLs/wOooHgFn5ulI1SriICAnlcsIFLSF2jg0A2riU2GKbOHIePt285uY8DA2PwvbEBHCK94akwAACAAQAAgAAAAIACAACAAQAAAAEAAAAiAgL2bDCgfbz9YefFtvrPTmwn+Ue6+suz/A6igeAWfm6UjRxzxdoKMAAAgAEAAIAAAACAAgAAgAEAAAABAAAAAAAAAAEBR1EhAgpbWcEh7rgvRE5UaCcqzWL/TR1B/DS8UeZsKVEvuKLrIQOwLg0emiQbbxafIh69Xjtpj4eclsMhKq1y/7vYDdE7LVKuIgICCltZwSHuuC9ETlRoJyrNYv9NHUH8NLxR5mwpUS+4ouscc8XaCjAAAIABAACAAAAAgAIAAIAAAAAABQAAACICA7AuDR6aJBtvFp8iHr1eO2mPh5yWwyEqrXL/u9gN0TstHCK94akwAACAAQAAgAAAAIACAACAAAAAAAUAAAAAAQFHUSECk50GLh/YhZaLJkDq/dugU3H/WvE6rTgQuY6N57pI4ykhA/H8MdLVP9SA/Hg8l3hvibSaC1bCBzwz7kTW+rsEZ8uFUq4iAgKTnQYuH9iFlosmQOr926BTcf9a8TqtOBC5jo3nukjjKRxzxdoKMAAAgAEAAIAAAACAAgAAgAAAAAAGAAAAIgID8fwx0tU/1ID8eDyXeG+JtJoLVsIHPDPuRNb6uwRny4UcIr3hqTAAAIABAACAAAAAgAIAAIAAAAAABgAAAAA=""" - decoder = DecodeQR() - decoder.add_data(BASE64_PSBT_1) - controller.psbt = decoder.get_psbt() - controller.psbt_seed = seed_12b + def setup_screenshots(locale: str) -> dict: + # Set up some test data that we'll need in the `Controller` for certain Views + controller = Controller.get_instance() + + controller.settings.set_value(SettingsConstants.SETTING__SIG_TYPES, [attr for attr, name in SettingsConstants.ALL_SIG_TYPES]) + controller.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [attr for attr, name in SettingsConstants.ALL_SCRIPT_TYPES]) + + mnemonic_12 = "forum undo fragile fade shy sign arrest garment culture tube off merit".split() + mnemonic_24 = "attack pizza motion avocado network gather crop fresh patrol unusual wild holiday candy pony ranch winter theme error hybrid van cereal salon goddess expire".split() + mnemonic_12b = ["abandon"] * 11 + ["about"] + seed_12 = Seed(mnemonic=mnemonic_12, passphrase="cap*BRACKET3stove", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + seed_12b = Seed(mnemonic=mnemonic_12b, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + seed_24 = Seed(mnemonic=mnemonic_24, passphrase="some-PASS*phrase9", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + controller.storage.seeds.append(seed_12) + controller.storage.seeds.append(seed_12b) + controller.storage.set_pending_seed(seed_24) + UnhandledExceptionViewFood = ["IndexError", "line 1, in some_buggy_code.py", "list index out of range"] + + # Pending mnemonic for ToolsCalcFinalWordShowFinalWordView + controller.storage.init_pending_mnemonic(num_words=12) + for i, word in enumerate(mnemonic_12[:11]): + controller.storage.update_pending_mnemonic(word=word, index=i) + controller.storage.update_pending_mnemonic(word="satoshi", index=11) # random last word; not supposed to be a valid checksum (yet) + + # Load a PSBT into memory + BASE64_PSBT_1 = """cHNidP8BAP06AQIAAAAC5l4E3oEjI+H0im8t/K2nLmF5iJFdKEiuQs8ESveWJKcAAAAAAP3///8iBZMRhYIq4s/LmnTmKBi79M8ITirmsbO++63evK4utwAAAAAA/f///wZYQuoDAAAAACIAIAW5jm3UnC5fyjKCUZ8LTzjENtb/ioRTaBMXeSXsB3n+bK2fCgAAAAAWABReJY7akT1+d+jx475yBRWORdBd7VxbUgUAAAAAFgAU4wj9I/jB3GjNQudNZAca+7g9R16iWtYOAAAAABYAFIotPApLZlfscg8f3ppKqO3qA5nv7BnMFAAAAAAiACAs6SGc8qv4FwuNl0G0SpMZG8ODUEk5RXiWUcuzzw5iaRSfAhMAAAAAIgAgW0f5QxQIgVCGQqKzsvfkXZjUxdFop5sfez6Pt8mUbmZ1AgAAAAEAkgIAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////BQIRAgEB/////wJAvkAlAAAAACIAIIRPoo2LvkrwrhrYFhLhlP43izxbA4Eo6Y6iFFiQYdXRAAAAAAAAAAAmaiSqIant4vYcP3HR3v0/qZnfo2lTdVxpBol5mWK0i+vYNpdOjPkAAAAAAQErQL5AJQAAAAAiACCET6KNi75K8K4a2BYS4ZT+N4s8WwOBKOmOohRYkGHV0QEFR1EhArGhNdUqlR4BAOLGTMrY2ZJYTQNRudp7fU7i8crRJqgEIQNDxn7PjUzvsP6KYw4s7dmoZE0qO1K6MaM+2ScRZ7hyxFKuIgYCsaE11SqVHgEA4sZMytjZklhNA1G52nt9TuLxytEmqAQcc8XaCjAAAIABAACAAAAAgAIAAIAAAAAAAwAAACIGA0PGfs+NTO+w/opjDizt2ahkTSo7Uroxoz7ZJxFnuHLEHCK94akwAACAAQAAgAAAAIACAACAAAAAAAMAAAAAAQCSAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8FAhACAQH/////AkC+QCUAAAAAIgAghE+ijYu+SvCuGtgWEuGU/jeLPFsDgSjpjqIUWJBh1dEAAAAAAAAAACZqJKohqe3i9hw/cdHe/T+pmd+jaVN1XGkGiXmZYrSL69g2l06M+QAAAAABAStAvkAlAAAAACIAIIRPoo2LvkrwrhrYFhLhlP43izxbA4Eo6Y6iFFiQYdXRAQVHUSECsaE11SqVHgEA4sZMytjZklhNA1G52nt9TuLxytEmqAQhA0PGfs+NTO+w/opjDizt2ahkTSo7Uroxoz7ZJxFnuHLEUq4iBgKxoTXVKpUeAQDixkzK2NmSWE0DUbnae31O4vHK0SaoBBxzxdoKMAAAgAEAAIAAAACAAgAAgAAAAAADAAAAIgYDQ8Z+z41M77D+imMOLO3ZqGRNKjtSujGjPtknEWe4csQcIr3hqTAAAIABAACAAAAAgAIAAIAAAAAAAwAAAAABAUdRIQJ5XLCBS0hdo4NANq4lNhimzhyHj7dvObmPAwNj8L2xASEC9mwwoH28/WHnxbb6z05sJ/lHuvrLs/wOooHgFn5ulI1SriICAnlcsIFLSF2jg0A2riU2GKbOHIePt285uY8DA2PwvbEBHCK94akwAACAAQAAgAAAAIACAACAAQAAAAEAAAAiAgL2bDCgfbz9YefFtvrPTmwn+Ue6+suz/A6igeAWfm6UjRxzxdoKMAAAgAEAAIAAAACAAgAAgAEAAAABAAAAAAAAAAEBR1EhAgpbWcEh7rgvRE5UaCcqzWL/TR1B/DS8UeZsKVEvuKLrIQOwLg0emiQbbxafIh69Xjtpj4eclsMhKq1y/7vYDdE7LVKuIgICCltZwSHuuC9ETlRoJyrNYv9NHUH8NLxR5mwpUS+4ouscc8XaCjAAAIABAACAAAAAgAIAAIAAAAAABQAAACICA7AuDR6aJBtvFp8iHr1eO2mPh5yWwyEqrXL/u9gN0TstHCK94akwAACAAQAAgAAAAIACAACAAAAAAAUAAAAAAQFHUSECk50GLh/YhZaLJkDq/dugU3H/WvE6rTgQuY6N57pI4ykhA/H8MdLVP9SA/Hg8l3hvibSaC1bCBzwz7kTW+rsEZ8uFUq4iAgKTnQYuH9iFlosmQOr926BTcf9a8TqtOBC5jo3nukjjKRxzxdoKMAAAgAEAAIAAAACAAgAAgAAAAAAGAAAAIgID8fwx0tU/1ID8eDyXeG+JtJoLVsIHPDPuRNb6uwRny4UcIr3hqTAAAIABAACAAAAAgAIAAIAAAAAABgAAAAA=""" + decoder = DecodeQR() + decoder.add_data(BASE64_PSBT_1) + controller.psbt = decoder.get_psbt() + controller.psbt_seed = seed_12b + + # Multisig wallet descriptor for the multisig in the above PSBT + MULTISIG_WALLET_DESCRIPTOR = """wsh(sortedmulti(1,[22bde1a9/48h/1h/0h/2h]tpubDFfsBrmpj226ZYiRszYi2qK6iGvh2vkkghfGB2YiRUVY4rqqedHCFEgw12FwDkm7rUoVtq9wLTKc6BN2sxswvQeQgp7m8st4FP8WtP8go76/{0,1}/*,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/{0,1}/*))#3jhtf6yx""" + controller.multisig_wallet_descriptor = embit.descriptor.Descriptor.from_string(MULTISIG_WALLET_DESCRIPTOR) + + # Message signing data + derivation_path = "m/84h/0h/0h/0/0" + controller.sign_message_data = { + "seed_num": 0, + "derivation_path": derivation_path, + "message": "I attest that I control this bitcoin address blah blah blah", + "addr_format": embit_utils.parse_derivation_path(derivation_path) + } + + # Automatically populate all Settings options Views + settings_views_list = [] + settings_views_list.append(settings_views.SettingsMenuView) + + # so we get a choice for transcribe seed qr format + controller.settings.set_value( + attr_name=SettingsConstants.SETTING__COMPACT_SEEDQR, + value=SettingsConstants.OPTION__ENABLED + ) + for settings_entry in SettingsDefinition.settings_entries: + if settings_entry.visibility == SettingsConstants.VISIBILITY__HIDDEN: + continue + + settings_views_list.append((settings_views.SettingsEntryUpdateSelectionView, dict(attr_name=settings_entry.attr_name), f"SettingsEntryUpdateSelectionView_{settings_entry.attr_name}")) + + settingsqr_data_persistent = f"settings::v1 name=English_noob_mode persistent=E coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E locale={locale}" + settingsqr_data_not_persistent = f"settings::v1 name=Mode_Ephemeral persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E locale={locale}" + + screenshot_sections = { + "Main Menu Views": [ + MainMenuView, + (MainMenuView, {}, 'MainMenuView_SDCardStateChangeToast_removed', SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__REMOVED)), + (MainMenuView, {}, 'MainMenuView_SDCardStateChangeToast_inserted', SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__INSERTED)), + (MainMenuView, {}, 'MainMenuView_RemoveSDCardToast', RemoveSDCardToastManagerThread(activation_delay=0)), + PowerOptionsView, + RestartView, + PowerOffView, + ], + "Seed Views": [ + seed_views.SeedsMenuView, + seed_views.LoadSeedView, + seed_views.SeedMnemonicEntryView, + seed_views.SeedMnemonicInvalidView, + seed_views.SeedFinalizeView, + seed_views.SeedAddPassphraseView, + seed_views.SeedReviewPassphraseView, + + (seed_views.SeedOptionsView, dict(seed_num=0)), + (seed_views.SeedBackupView, dict(seed_num=0)), + (seed_views.SeedExportXpubSigTypeView, dict(seed_num=0)), + (seed_views.SeedExportXpubScriptTypeView, dict(seed_num=0, sig_type="msig")), + (seed_views.SeedExportXpubCustomDerivationView, dict(seed_num=0, sig_type="ss", script_type="")), + (seed_views.SeedExportXpubCoordinatorView, dict(seed_num=0, sig_type="ss", script_type="nat")), + (seed_views.SeedExportXpubWarningView, dict(seed_num=0, sig_type="msig", script_type="nes", coordinator="spd", custom_derivation="")), + (seed_views.SeedExportXpubDetailsView, dict(seed_num=0, sig_type="ss", script_type="nat", coordinator="bw", custom_derivation="")), + #SeedExportXpubQRDisplayView, + (seed_views.SeedWordsWarningView, dict(seed_num=0)), + (seed_views.SeedWordsView, dict(seed_num=0)), + (seed_views.SeedWordsView, dict(seed_num=0, page_index=2), "SeedWordsView_2"), + (seed_views.SeedBIP85ApplicationModeView, dict(seed_num=0)), + (seed_views.SeedBIP85SelectChildIndexView, dict(seed_num=0, num_words=24)), + (seed_views.SeedBIP85InvalidChildIndexView, dict(seed_num=0, num_words=12)), + (seed_views.SeedWordsBackupTestPromptView, dict(seed_num=0)), + (seed_views.SeedWordsBackupTestView, dict(seed_num=0)), + (seed_views.SeedWordsBackupTestMistakeView, dict(seed_num=0, cur_index=7, wrong_word="unlucky")), + (seed_views.SeedWordsBackupTestSuccessView, dict(seed_num=0)), + (seed_views.SeedTranscribeSeedQRFormatView, dict(seed_num=0)), + (seed_views.SeedTranscribeSeedQRWarningView, dict(seed_num=0)), + (seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=0, seedqr_format=QRType.SEED__SEEDQR, num_modules=25), "SeedTranscribeSeedQRWholeQRView_12_Standard"), + (seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=0, seedqr_format=QRType.SEED__COMPACTSEEDQR, num_modules=21), "SeedTranscribeSeedQRWholeQRView_12_Compact"), + + # Screenshot doesn't render properly due to how the transparency mask is pre-rendered + # (seed_views.SeedTranscribeSeedQRZoomedInView, dict(seed_num=0, seedqr_format=QRType.SEED__SEEDQR)), + + (seed_views.SeedTranscribeSeedQRConfirmQRPromptView, dict(seed_num=0)), + + # Screenshot can't render live preview screens + # (seed_views.SeedTranscribeSeedQRConfirmScanView, dict(seed_num=0)), + + #(seed_views.AddressVerificationStartView, dict(address=, script_type="nat", network="M")), + #seed_views.AddressVerificationSigTypeView, + #seed_views.SeedSingleSigAddressVerificationSelectSeedView, + #seed_views.SeedAddressVerificationView, + #seed_views.AddressVerificationSuccessView, + + seed_views.LoadMultisigWalletDescriptorView, + seed_views.MultisigWalletDescriptorView, + (seed_views.SeedDiscardView, dict(seed_num=0)), + + seed_views.SeedSignMessageConfirmMessageView, + seed_views.SeedSignMessageConfirmAddressView, + ], + "PSBT Views": [ + psbt_views.PSBTSelectSeedView, # this will fail, be rerun below + psbt_views.PSBTOverviewView, + psbt_views.PSBTUnsupportedScriptTypeWarningView, + psbt_views.PSBTNoChangeWarningView, + psbt_views.PSBTMathView, + (psbt_views.PSBTAddressDetailsView, dict(address_num=0)), + + # TODO: Render Multisig change w/ and w/out the multisig wallet descriptor onboard + (psbt_views.PSBTChangeDetailsView, dict(change_address_num=0)), + (psbt_views.PSBTAddressVerificationFailedView, dict(is_change=True, is_multisig=False), "PSBTAddressVerificationFailedView_singlesig_change"), + (psbt_views.PSBTAddressVerificationFailedView, dict(is_change=False, is_multisig=False), "PSBTAddressVerificationFailedView_singlesig_selftransfer"), + (psbt_views.PSBTAddressVerificationFailedView, dict(is_change=True, is_multisig=True), "PSBTAddressVerificationFailedView_multisig_change"), + (psbt_views.PSBTAddressVerificationFailedView, dict(is_change=False, is_multisig=True), "PSBTAddressVerificationFailedView_multisig_selftransfer"), + psbt_views.PSBTFinalizeView, + #PSBTSignedQRDisplayView + psbt_views.PSBTSigningErrorView, + ], + "Tools Views": [ + tools_views.ToolsMenuView, + #ToolsImageEntropyLivePreviewView + #ToolsImageEntropyFinalImageView + tools_views.ToolsImageEntropyMnemonicLengthView, + tools_views.ToolsDiceEntropyMnemonicLengthView, + (tools_views.ToolsDiceEntropyEntryView, dict(total_rolls=50)), + tools_views.ToolsCalcFinalWordNumWordsView, + tools_views.ToolsCalcFinalWordFinalizePromptView, + tools_views.ToolsCalcFinalWordCoinFlipsView, + (tools_views.ToolsCalcFinalWordShowFinalWordView, {}, "ToolsCalcFinalWordShowFinalWordView_pick_word"), + (tools_views.ToolsCalcFinalWordShowFinalWordView, dict(coin_flips="0010101"), "ToolsCalcFinalWordShowFinalWordView_coin_flips"), + #tools_views.ToolsCalcFinalWordDoneView, + tools_views.ToolsAddressExplorerSelectSourceView, + tools_views.ToolsAddressExplorerAddressTypeView, + tools_views.ToolsAddressExplorerAddressListView, + #tools_views.ToolsAddressExplorerAddressView, + ], + "Settings Views": settings_views_list + [ + settings_views.IOTestView, + settings_views.DonateView, + (settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_persistent), "SettingsIngestSettingsQRView_persistent"), + (settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_not_persistent), "SettingsIngestSettingsQRView_not_persistent"), + ], + "Misc Error Views": [ + NotYetImplementedView, + (UnhandledExceptionView, dict(error=UnhandledExceptionViewFood)), + NetworkMismatchErrorView, + (OptionDisabledView, dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)), + (ErrorView, dict( + title="Error", + status_headline="Unknown QR Type", + text="QRCode is invalid or is a data format not yet supported.", + button_text="Back", + )), + ] + } + + return screenshot_sections + + + def screencap_view(view_cls: View, view_name: str, view_args: dict={}, toast_thread: BaseToastOverlayManagerThread = None): + screenshot_renderer.set_screenshot_filename(f"{view_name}.png") + controller = Controller.get_instance() + try: + print(f"Running {view_name}") + try: + view_cls(**view_args).run() + except ScreenshotComplete: + # The target View has run and its Screen has rendered what it needs to + if toast_thread is not None: + # Now run the Toast so it can render on top of the current image buffer + controller.activate_toast(toast_thread) + while controller.toast_notification_thread.is_alive(): + # Give the Toast a moment to complete its work + time.sleep(0.01) + raise ScreenshotComplete() + except ScreenshotComplete: + # Slightly hacky way to exit ScreenshotRenderer as expected + print(f"Completed {view_name}") + except Exception as e: + # Something else went wrong + from traceback import print_exc + print_exc() + raise e + finally: + if toast_thread: + toast_thread.stop() + time.sleep(0.1) #jdlcdl - # Multisig wallet descriptor for the multisig in the above PSBT - MULTISIG_WALLET_DESCRIPTOR = """wsh(sortedmulti(1,[22bde1a9/48h/1h/0h/2h]tpubDFfsBrmpj226ZYiRszYi2qK6iGvh2vkkghfGB2YiRUVY4rqqedHCFEgw12FwDkm7rUoVtq9wLTKc6BN2sxswvQeQgp7m8st4FP8WtP8go76/{0,1}/*,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/{0,1}/*))#3jhtf6yx""" - controller.multisig_wallet_descriptor = embit.descriptor.Descriptor.from_string(MULTISIG_WALLET_DESCRIPTOR) # Parse the main `babel/messages.pot` for overall stats messages_source_path = os.path.join(pathlib.Path(__file__).parent.resolve().parent.resolve().parent.resolve(), "babel", "messages.pot") with open(messages_source_path, 'r') as messages_source_file: num_source_messages = messages_source_file.read().count("msgid \"") - 1 - def screencap_view(view_cls: View, view_name: str, view_args: dict={}): - screenshot_renderer.set_screenshot_filename(f"{view_name}.png") - try: - print(f"Running {view_name}") - view_cls(**view_args).run() - except ScreenshotComplete: - # Slightly hacky way to exit ScreenshotRenderer as expected - pass - print(f"Completed {view_name}") - except Exception as e: - # Something else went wrong - print(repr(e)) - raise e - - # Automatically populate all Settings options Views - settings_views_list = [] - settings_views_list.append(settings_views.SettingsMenuView) - for settings_entry in SettingsDefinition.settings_entries: - if settings_entry.visibility == SettingsConstants.VISIBILITY__HIDDEN: - continue - settings_views_list.append((settings_views.SettingsEntryUpdateSelectionView, dict(attr_name=settings_entry.attr_name), f"SettingsEntryUpdateSelectionView_{settings_entry.attr_name}")) - settings_views_list.append(settings_views.IOTestView) - settings_views_list.append(settings_views.DonateView) - - - screenshot_sections = { - "Main Menu Views": [ - main_menu_views.MainMenuView, - main_menu_views.PowerOffView, - (scan_views.SettingsUpdatedView, dict(config_name="Keith's Settings")), - ], - "Seed Views": [ - seed_views.SeedsMenuView, - seed_views.LoadSeedView, - seed_views.SeedMnemonicEntryView, - seed_views.SeedMnemonicInvalidView, - seed_views.SeedFinalizeView, - seed_views.SeedPassphraseWarningView, - seed_views.SeedAddPassphraseView, - seed_views.SeedReviewPassphraseView, - (seed_views.SeedOptionsView, dict(seed_num=0)), - (seed_views.SeedBackupView, dict(seed_num=0)), - (seed_views.SeedWordsWarningView, dict(seed_num=0)), - (seed_views.SeedWordsView, dict(seed_num=0)), - (seed_views.SeedWordsView, dict(seed_num=0, page_index=2), "SeedWordsView_2"), - (seed_views.SeedTranscribeSeedQRFormatView, dict(seed_num=0)), - (seed_views.SeedTranscribeSeedQRWarningView, dict(seed_num=0)), - (seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=0, seedqr_format=QRType.SEED__SEEDQR, num_modules=25), "SeedTranscribeSeedQRWholeQRView_12_Standard"), - (seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=0, seedqr_format=QRType.SEED__COMPACTSEEDQR, num_modules=21), "SeedTranscribeSeedQRWholeQRView_12_Compact"), - - # Screenshot doesn't render properly due to how the transparency mask is pre-rendered - # (seed_views.SeedTranscribeSeedQRZoomedInView, dict(seed_num=0, seedqr_format=QRType.SEED__SEEDQR)), - - (seed_views.SeedTranscribeSeedQRConfirmQRPromptView, dict(seed_num=0)), - - # Screenshot can't render live preview screens - # (seed_views.SeedTranscribeSeedQRConfirmScanView, dict(seed_num=0)), - - seed_views.LoadMultisigWalletDescriptorView, - seed_views.MultisigWalletDescriptorView, - ], - "PSBT Views": [ - psbt_views.PSBTSelectSeedView, - psbt_views.PSBTOverviewView, - psbt_views.PSBTUnsupportedScriptTypeWarningView, - psbt_views.PSBTNoChangeWarningView, - psbt_views.PSBTMathView, - (psbt_views.PSBTAddressDetailsView, dict(address_num=0)), - - # TODO: Render Multisig change w/ and w/out the multisig wallet descriptor onboard - (psbt_views.PSBTChangeDetailsView, dict(change_address_num=0)), - (psbt_views.PSBTAddressVerificationFailedView, dict(is_change=True, is_multisig=False), "PSBTAddressVerificationFailedView_singlesig_change"), - (psbt_views.PSBTAddressVerificationFailedView, dict(is_change=False, is_multisig=False), "PSBTAddressVerificationFailedView_singlesig_selftransfer"), - (psbt_views.PSBTAddressVerificationFailedView, dict(is_change=True, is_multisig=True), "PSBTAddressVerificationFailedView_multisig_change"), - (psbt_views.PSBTAddressVerificationFailedView, dict(is_change=False, is_multisig=True), "PSBTAddressVerificationFailedView_multisig_selftransfer"), - psbt_views.PSBTFinalizeView, - psbt_views.PSBTSelectCoordinatorView, - psbt_views.PSBTSigningErrorView, - ], - "Tools Views": [ - tools_views.ToolsMenuView, - tools_views.ToolsDiceEntropyMnemonicLengthView, - (tools_views.ToolsDiceEntropyEntryView, dict(total_rolls=50)), - ], - "Settings Views": settings_views_list, - } - - locales = [] if target_locale is None: locales = SettingsConstants.ALL_LOCALES @@ -169,6 +295,7 @@ def test_generate_screenshots(target_locale): raise Exception(f"Invalid locale: {target_locale}") for locale, display_name in locales: + print(f"\nLooping for locale: {locale}...") Settings.get_instance().set_value(SettingsConstants.SETTING__LOCALE, value=locale) screenshot_renderer.set_screenshot_path(os.path.join(screenshot_root, locale)) @@ -176,15 +303,23 @@ def test_generate_screenshots(target_locale): # Report the translation progress if locale != SettingsConstants.LOCALE__ENGLISH: - translated_messages_path = os.path.join(pathlib.Path(__file__).parent.resolve().parent.resolve().parent.resolve(), "src", "seedsigner", "resources", "babel", locale, "LC_MESSAGES", "messages.po") - with open(translated_messages_path, 'r') as translation_file: - locale_translations = translation_file.read() - num_locale_translations = locale_translations.count("msgid \"") - locale_translations.count("""msgstr ""\n\n""") - 1 + try: + translated_messages_path = os.path.join(pathlib.Path(__file__).parent.resolve().parent.resolve().parent.resolve(), "src", "seedsigner", "resources", "babel", locale, "LC_MESSAGES", "messages.po") + with open(translated_messages_path, 'r') as translation_file: + locale_translations = translation_file.read() + num_locale_translations = locale_translations.count("msgid \"") - locale_translations.count("""msgstr ""\n\n""") - 1 - locale_readme += f"## Translation progress: {num_locale_translations / num_source_messages:.1%}\n\n" - locale_readme += "---\n\n" + if locale != "en": + locale_readme += f"## Translation progress: {num_locale_translations / num_source_messages:.1%}\n\n" + locale_readme += f"[{display_name} messages.po catalog](messages.po)\n" + locale_readme += "---\n\n" + except Exception as e: + from traceback import print_exc + print_exc() - for section_name, screenshot_list in screenshot_sections.items(): + for section_name, screenshot_list in setup_screenshots(locale).items(): + subdir = section_name.lower().replace(" ", "_") + screenshot_renderer.set_screenshot_path(os.path.join(screenshot_root, locale, subdir)) locale_readme += "\n\n---\n\n" locale_readme += f"## {section_name}\n\n" locale_readme += """""" @@ -196,21 +331,35 @@ def test_generate_screenshots(target_locale): view_name = view_cls.__name__ elif len(screenshot) == 3: view_cls, view_args, view_name = screenshot + elif len(screenshot) == 4: + view_cls, view_args, view_name, toast_thread = screenshot else: view_cls = screenshot view_args = {} view_name = view_cls.__name__ + toast_thread = None - screencap_view(view_cls, view_name, view_args) - locale_readme += """
""" - locale_readme += f"""""" - locale_readme += """
{view_name}

""" + screencap_view(view_cls, view_name, view_args, toast_thread=toast_thread) + locale_readme += """ """ + locale_readme += f"""""" + locale_readme += """
{view_name}

\n""" locale_readme += "" - with open(os.path.join(screenshot_renderer.screenshot_path, "README.md"), 'w') as readme_file: + # many screens don't work, leaving a missing image, re-run here for now + controller.psbt_seed = None + screenshot_renderer.set_screenshot_path(os.path.join(screenshot_root, locale, "psbt_views")) + screencap_view(psbt_views.PSBTSelectSeedView, 'PSBTSelectSeedView', {}) + + with open(os.path.join(screenshot_root, locale, "README.md"), 'w') as readme_file: readme_file.write(locale_readme) + if locale != "en": + shutil.copy(translated_messages_path, os.path.join(screenshot_root, locale, "messages.po")) + + print(f"Done with locale: {locale}.") + + # Write the main README; ensure it writes all locales, not just the one that may # have been specified for this run. main_readme = """# SeedSigner Screenshots \n\n""" diff --git a/tests/test_bip85.py b/tests/test_bip85.py new file mode 100644 index 00000000..5ade3917 --- /dev/null +++ b/tests/test_bip85.py @@ -0,0 +1,23 @@ +import pytest +from mock import MagicMock +from seedsigner.models.seed import Seed +from embit import bip39 + +from seedsigner.models.settings import SettingsConstants + + + +def test_derive_child_mnemonic(): + + expected = "unusual topic foot figure pulp target glimpse core electric spot neglect fame" + seed = Seed(mnemonic="resource timber firm banner horror pupil frozen main pear direct pioneer broken grid core insane begin sister pony end debate task silk empty curious".split()) + + actual = seed.get_bip85_child_mnemonic(0, 12) + assert actual == expected + + + expected = "imitate post very mandate retreat prevent tiny snow fetch canvas town shrug fix food summer library symptom occur slam style cruise wolf phone key" + seed = Seed(mnemonic="resource timber firm banner horror pupil frozen main pear direct pioneer broken grid core insane begin sister pony end debate task silk empty curious".split()) + + actual = seed.get_bip85_child_mnemonic(0, 24) + assert actual == expected diff --git a/tests/test_controller.py b/tests/test_controller.py index b6887369..eea8162d 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -1,53 +1,114 @@ -import configparser import pytest -from mock import MagicMock + +# Must import this before the Controller +from base import BaseTest + from seedsigner.controller import Controller -from seedsigner.models.settings import Settings from seedsigner.models.settings_definition import SettingsConstants +class TestController(BaseTest): -def test_singleton_init_fails(): - """ The Controller should not allow any code to instantiate it via Controller() """ - with pytest.raises(Exception): - c = Controller() + def test_reset_controller(self): + """ The reset_controller util should completely reset the Controller singleton """ + controller = Controller.get_instance() + controller.address_explorer_data = "foo" + + BaseTest.reset_controller() + controller = Controller.get_instance() + assert controller.address_explorer_data is None -def test_singleton_get_instance_preserves_state(): - """ Changes to the Controller singleton should be preserved across calls to get_instance() """ - - # Must reset Singleton instances; pytest cannot properly isolate Singletons for us - # automatically. - # TODO: Cleaner solution here would be nice. - Settings._instance = None - Controller._instance = None - - # Initialize the instance and verify that it read the config settings - Controller.configure_instance(disable_hardware=True) - controller = Controller.get_instance() - assert controller.unverified_address is None - - # Change a value in the instance... - controller.unverified_address = "123abc" - - # ...get a new copy of the instance and confirm change - controller = Controller.get_instance() - assert controller.unverified_address == "123abc" + def test_singleton_init_fails(self): + """ The Controller should not allow any code to instantiate it via Controller() """ + with pytest.raises(Exception): + c = Controller() -def test_missing_settings_get_defaults(): - """ Should gracefully handle any missing fields from `settings.ini` """ - # TODO: This is not complete; currently only handles missing compact_seedqr_enabled. + def test_handle_exception(reset_controller): + """ Handle exceptions that get caught by the controller """ - # Must reset Singleton instances; pytest cannot properly isolate Singletons for us - # automatically. - # TODO: Cleaner solution here would be nice. - Settings._instance = None - Controller._instance = None + def process_exception_asserting_valid_error(exception_type, exception_msg=None): + """ + Exceptions caught by the controller are forwarded to the + UnhandledExceptionView with view_args["error"] being a list + of three strings, ie: [exception_type, line_info, exception_msg] + """ + try: + if exception_msg: + raise exception_type(exception_msg) + else: + raise exception_type() + except Exception as e: + error = controller.handle_exception(e).view_args["error"] - # Controller should parse the settings fine, even though a field is missing - Controller.configure_instance(disable_hardware=True) + # assert that error structure is valid + assert len(error) == 3 + assert error[0] in str(exception_type) + assert type(error[1]) == str + if exception_msg: + assert exception_msg in error[2] + else: + assert error[2] == "" + + # Initialize the controller + controller = Controller.get_instance() + + exception_tests = [ + # exceptions with an exception_msg + (Exception, "foo"), + (KeyError, "key not found"), + # exceptions without an exception_msg + (Exception, ""), + (Exception, None), + ] + + for exception_type, exception_msg in exception_tests: + process_exception_asserting_valid_error(exception_type, exception_msg) + + + def test_singleton_get_instance_preserves_state(self): + """ Changes to the Controller singleton should be preserved across calls to get_instance() """ + + # Initialize the instance and verify that it read the config settings + controller = Controller.get_instance() + assert controller.unverified_address is None + + # Change a value in the instance... + controller.unverified_address = "123abc" + + # ...get a new copy of the instance and confirm change + controller = Controller.get_instance() + assert controller.unverified_address == "123abc" + + + def test_missing_settings_get_defaults(self): + """ Should gracefully handle all missing fields from `settings.json` """ + + controller = Controller.get_instance() + + # Settings defaults + assert controller.settings.get_value(SettingsConstants.SETTING__LOCALE) == SettingsConstants.LOCALE__ENGLISH + assert controller.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) == SettingsConstants.WORDLIST_LANGUAGE__ENGLISH + assert controller.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED + assert controller.settings.get_value(SettingsConstants.SETTING__COORDINATORS) == [i for i,j in SettingsConstants.ALL_COORDINATORS if i!="kpr"] + assert controller.settings.get_value(SettingsConstants.SETTING__BTC_DENOMINATION) == SettingsConstants.BTC_DENOMINATION__THRESHOLD + + # Advanced Settings defaults + assert controller.settings.get_value(SettingsConstants.SETTING__NETWORK) == SettingsConstants.MAINNET + assert controller.settings.get_value(SettingsConstants.SETTING__QR_DENSITY) == SettingsConstants.DENSITY__MEDIUM + assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED + assert controller.settings.get_value(SettingsConstants.SETTING__SIG_TYPES) == [i for i,j in SettingsConstants.ALL_SIG_TYPES] + assert controller.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES) == [SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT] + assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__ENABLED + assert controller.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__ENABLED + assert controller.settings.get_value(SettingsConstants.SETTING__CAMERA_ROTATION) == SettingsConstants.CAMERA_ROTATION__180 + assert controller.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__ENABLED + assert controller.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__DISABLED + assert controller.settings.get_value(SettingsConstants.SETTING__PRIVACY_WARNINGS) == SettingsConstants.OPTION__ENABLED + assert controller.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__ENABLED + assert controller.settings.get_value(SettingsConstants.SETTING__PARTNER_LOGOS) == SettingsConstants.OPTION__ENABLED + + # Hidden Settings defaults + assert controller.settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS) == 62 - # Controller should still have a default value - controller = Controller.get_instance() - assert controller.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__DISABLED diff --git a/tests/test_decodepsbtqr.py b/tests/test_decodepsbtqr.py index ef9baa4a..9b7d6981 100644 --- a/tests/test_decodepsbtqr.py +++ b/tests/test_decodepsbtqr.py @@ -1,7 +1,7 @@ -import pytest -from mock import MagicMock -from seedsigner.models import Seed, DecodeQR, DecodeQRStatus, QRType, PSBTParser -from embit import psbt, bip39 +from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus +from seedsigner.models.psbt_parser import PSBTParser +from seedsigner.models.qr_type import QRType +from seedsigner.models.seed import Seed from seedsigner.models.settings_definition import SettingsConstants diff --git a/tests/test_embit_utils.py b/tests/test_embit_utils.py new file mode 100644 index 00000000..562d6aa2 --- /dev/null +++ b/tests/test_embit_utils.py @@ -0,0 +1,402 @@ +import pytest + +from seedsigner.models.settings_definition import SettingsConstants as SC +from seedsigner.helpers import embit_utils + + +def test_get_standard_derivation_path(): + """ + tests seedsigner.helpers.embit_utils.get_standard_derivation_path() + """ + + vectors_args_expected = { + # single sig + tuple(): "m/84'/0'/0'", + (SC.MAINNET,): "m/84'/0'/0'", + (SC.MAINNET, SC.SINGLE_SIG, ): "m/84'/0'/0'", + + (SC.MAINNET, SC.SINGLE_SIG, SC.NATIVE_SEGWIT): "m/84'/0'/0'", + (SC.TESTNET, SC.SINGLE_SIG, SC.NATIVE_SEGWIT): "m/84'/1'/0'", + (SC.REGTEST, SC.SINGLE_SIG, SC.NATIVE_SEGWIT): "m/84'/1'/0'", + + (SC.MAINNET, SC.SINGLE_SIG, SC.NESTED_SEGWIT): "m/49'/0'/0'", + (SC.TESTNET, SC.SINGLE_SIG, SC.NESTED_SEGWIT): "m/49'/1'/0'", + (SC.REGTEST, SC.SINGLE_SIG, SC.NESTED_SEGWIT): "m/49'/1'/0'", + + (SC.MAINNET, SC.SINGLE_SIG, SC.TAPROOT): "m/86'/0'/0'", + (SC.TESTNET, SC.SINGLE_SIG, SC.TAPROOT): "m/86'/1'/0'", + (SC.REGTEST, SC.SINGLE_SIG, SC.TAPROOT): "m/86'/1'/0'", + + + # multi sig + (SC.MAINNET, SC.MULTISIG, SC.NATIVE_SEGWIT): "m/48'/0'/0'/2'", + (SC.TESTNET, SC.MULTISIG, SC.NATIVE_SEGWIT): "m/48'/1'/0'/2'", + (SC.REGTEST, SC.MULTISIG, SC.NATIVE_SEGWIT): "m/48'/1'/0'/2'", + + (SC.MAINNET, SC.MULTISIG, SC.NESTED_SEGWIT): "m/48'/0'/0'/1'", + (SC.TESTNET, SC.MULTISIG, SC.NESTED_SEGWIT): "m/48'/1'/0'/1'", + (SC.REGTEST, SC.MULTISIG, SC.NESTED_SEGWIT): "m/48'/1'/0'/1'", + + (SC.MAINNET, SC.MULTISIG, SC.TAPROOT): Exception, + (SC.TESTNET, SC.MULTISIG, SC.TAPROOT): Exception, + (SC.REGTEST, SC.MULTISIG, SC.TAPROOT): Exception, + + # intentionally fall into exceptions + (SC.MAINNET, SC.SINGLE_SIG, 'invalid'): Exception, + (SC.MAINNET, SC.MULTISIG, 'invalid'): Exception, + (SC.MAINNET, 'invalid', SC.NATIVE_SEGWIT): Exception, + + # nonsense arguments + ("A",): Exception, + ("B", "A"): Exception, + ("C", "B", "A"): Exception, + (True,): Exception, + (False, True): Exception, + (tuple(),): Exception, + } + func = embit_utils.get_standard_derivation_path + + print() + for args, expected in vectors_args_expected.items(): + + # test successful calls + if type(expected) is str: + # call with ordered params + print(f"asserting {func.__name__}(*{args}) == {repr(expected)}") + assert func(*args) == expected + + # call with named params + a_dict = {} + if len(args) == 1: a_dict = {'network': args[0]} + elif len(args) == 2: a_dict = {'network': args[0], 'wallet_type': args[1]} + elif len(args) == 3: a_dict = {'network': args[0], 'wallet_type': args[1], 'script_type': args[2]} + print(f"asserting {func.__name__}(**{a_dict}) == {repr(expected)}") + assert func(**a_dict) == expected + + # test exceptions + else: + # call with ordered params + with pytest.raises(expected): + print(f"asserting {func.__name__}(*{args}) raises Exception") + func(*args) + + # call with named params + a_dict = {} + if len(args) == 1: a_dict = {'network': args[0]} + elif len(args) == 2: a_dict = {'network': args[0], 'wallet_type': args[1]} + elif len(args) == 3: a_dict = {'network': args[0], 'wallet_type': args[1], 'script_type': args[2]} + print(f"asserting {func.__name__}(**{a_dict}) raises Exception") + with pytest.raises(expected): + func(**a_dict) + + +def test_get_xpub(): + """ + tests seedsigner.helpers.embit_utils.get_xpub() + """ + + from binascii import unhexlify + from embit import bip39, bip32 + + # test vectors originate from: + # https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki + # https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki + # https://github.com/satoshilabs/slips/blob/master/slip-0132.md + # https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki + # https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki + vector_seeds = ( + unhexlify("000102030405060708090a0b0c0d0e0f"), + unhexlify("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"), + unhexlify("4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be"), + unhexlify("3ddd5602285899a946114506157c7997e5444528f3003f6134712147db19b678"), + bip39.mnemonic_to_seed("abandon "*11 + "about"), + ) + + vectors_args_expected = { + # https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vector-1 + (vector_seeds[0], "m/", "main"): "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", + (vector_seeds[0], "m/0'", "main"): "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw", + (vector_seeds[0], "m/0h/1", "main"): "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ", + (vector_seeds[0], "m/0'/1/2h", "main"): "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5", + (vector_seeds[0], "m/0'/1/2h/2", "main"): "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV", + (vector_seeds[0], "m/0'/1/2h/2/1000000000", "main"): "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy", + + # https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vector-2 + (vector_seeds[1], "m/", "main"): "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB", + (vector_seeds[1], "m/0", "main"): "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH", + (vector_seeds[1], "m/0/2147483647'", "main"): "xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a", + (vector_seeds[1], "m/0/2147483647h/1", "main"): "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon", + (vector_seeds[1], "m/0/2147483647'/1/2147483646h", "main"): "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL", + (vector_seeds[1], "m/0/2147483647h/1/2147483646'/2", "main"): "xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt", + + # https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vector-3 + (vector_seeds[2], "m/", "main"): "xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13", + (vector_seeds[2], "m/0h", "main"): "xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y", + + # https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vector-4 + (vector_seeds[3], "m/", "main"): "xpub661MyMwAqRbcGczjuMoRm6dXaLDEhW1u34gKenbeYqAix21mdUKJyuyu5F1rzYGVxyL6tmgBUAEPrEz92mBXjByMRiJdba9wpnN37RLLAXa", + (vector_seeds[3], "m/0'", "main"): "xpub69AUMk3qDBi3uW1sXgjCmVjJ2G6WQoYSnNHyzkmdCHEhSZ4tBok37xfFEqHd2AddP56Tqp4o56AePAgCjYdvpW2PU2jbUPFKsav5ut6Ch1m", + (vector_seeds[3], "m/0h/1'", "main"): "xpub6BJA1jSqiukeaesWfxe6sNK9CCGaujFFSJLomWHprUL9DePQ4JDkM5d88n49sMGJxrhpjazuXYWdMf17C9T5XnxkopaeS7jGk1GyyVziaMt", + + # + # embit_utils.get_xpub() returns the generic bip32 version "xpub", NOT the zpub/Zpub/ypub/Ypub extended versions + # + + # https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki#test-vectors + # https://github.com/satoshilabs/slips/blob/master/slip-0132.md#bitcoin-test-vectors + (vector_seeds[4], "m/84'/0'/0'", "main"): + bip32.HDKey.from_string("zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs").to_base58(version=b'\x04\x88\xb2\x1e'), + + # https://github.com/satoshilabs/slips/blob/master/slip-0132.md#bitcoin-test-vectors + (vector_seeds[4], "m/49'/0'/0'", "main"): + bip32.HDKey.from_string("ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP").to_base58(version=b'\x04\x88\xb2\x1e'), + + # https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki#test-vectors + (vector_seeds[4], "m/49'/1'/0'", "test"): + bip32.HDKey.from_string("upub5EFU65HtV5TeiSHmZZm7FUffBGy8UKeqp7vw43jYbvZPpoVsgU93oac7Wk3u6moKegAEWtGNF8DehrnHtv21XXEMYRUocHqguyjknFHYfgY").to_base58(version=b'\x04\x35\x87\xcf'), + + # https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki#test-vectors + (vector_seeds[4], "m/86'/0'/0'", "main"): "xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ", + + } + func = embit_utils.get_xpub + + print() + for args, expected in vectors_args_expected.items(): + print("\nasserting...") + + # call without optional params (default is "main") + if args[2] == "main": + print(f' {func.__name__}({args[0]}, "{args[1]}") == "{expected}"') + assert str(func(args[0], args[1])) == expected + + # call with ordered params + print(f' {func.__name__}(*{args}) == "{expected}"') + assert str(func(*args)) == expected + + # call with named params + print(f' {func.__name__}(seed_bytes={args[0]}, derivation_path="{args[1]}", embit_network="{args[2]}") == "{expected}"') + assert str(func(seed_bytes=args[0], derivation_path=args[1], embit_network=args[2])) == expected + + +def test_get_single_sig_address(): + """ + tests seedsigner.helpers.embit_utils.get_single_sig_address() + """ + + from embit.bip32 import HDKey + + # test vectors originate from: + # https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki + # https://github.com/satoshilabs/slips/blob/master/slip-0132.md + # https://iancoleman.io/bip39/ + # https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki + + vectors_args_expected = { + # https://github.com/satoshilabs/slips/blob/master/slip-0132.md#bitcoin-test-vectors (first payment address of native segwit on mainnet) + (HDKey.from_string("zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs"), "nat", 0, False, "main"): + "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu", + # jdlcdl: derived via iancoleman test vector for first change address of native segwit on mainnet + (HDKey.from_string("zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs"), "nat", 0, True, "main"): + "bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el", + + # https://github.com/satoshilabs/slips/blob/master/slip-0132.md#bitcoin-test-vectors (first payment address of nested segwit on mainnet) + (HDKey.from_string("ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP"), "nes", 0, False, "main"): + "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf", + # jdlcdl: derived via iancoleman test vector for first change address of nested segwit on mainnet + (HDKey.from_string("ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP"), "nes", 0, True, "main"): + "34K56kSjgUCUSD8GTtuF7c9Zzwokbs6uZ7", + + # https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki#test-vectors (first payment address of nested segwit on testnet) + (HDKey.from_string("upub5EFU65HtV5TeiSHmZZm7FUffBGy8UKeqp7vw43jYbvZPpoVsgU93oac7Wk3u6moKegAEWtGNF8DehrnHtv21XXEMYRUocHqguyjknFHYfgY"), "nes", 0, False, "test"): + "2Mww8dCYPUpKHofjgcXcBCEGmniw9CoaiD2", + # jdlcdl: derived via iancoleman test vector for first change address of nested segwit on testnet + (HDKey.from_string("upub5EFU65HtV5TeiSHmZZm7FUffBGy8UKeqp7vw43jYbvZPpoVsgU93oac7Wk3u6moKegAEWtGNF8DehrnHtv21XXEMYRUocHqguyjknFHYfgY"), "nes", 0, True, "test"): + "2MvdUi5o3f2tnEFh9yGvta6FzptTZtkPJC8", + + # https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki#test-vectors (first payment address of taproot on mainnet) + (HDKey.from_string("xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ"), "tr", 0, False, "main"): + "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr", + + # https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki#test-vectors (second payment address of taproot on mainnet) + (HDKey.from_string("xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ"), "tr", 1, False, "main"): + "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh", + + # https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki#test-vectors (first change address of taproot on mainnet) + (HDKey.from_string("xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ"), "tr", 0, True, "main"): + "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7", + + # jdlcdl: derived via electrum m/44'/1'/0 (first payment address p2pkh on testnet) + (HDKey.from_string("tpubDC5FSnBiZDMmhiuCmWAYsLwgLYrrT9rAqvTySfuCCrgsWz8wxMXUS9Tb9iVMvcRbvFcAHGkMD5Kx8koh4GquNGNTfohfk7pgjhaPCdXpoba"), "leg", 0, False, "test"): + "mkpZhYtJu2r87Js3pDiWJDmPte2NRZ8bJV", + + # jdlcdl: derived via electrum m/44'/1'/0 (first change address p2pkh on testnet) + (HDKey.from_string("tpubDC5FSnBiZDMmhiuCmWAYsLwgLYrrT9rAqvTySfuCCrgsWz8wxMXUS9Tb9iVMvcRbvFcAHGkMD5Kx8koh4GquNGNTfohfk7pgjhaPCdXpoba"), "leg", 0, True, "test"): + "mi8nhzZgGZQthq6DQHbru9crMDerUdTKva", + + # jdlcdl: nonsense script_type falls off end of function returning None. TODO: Would it be preferred to "else: raise ValueError"? + (HDKey.from_string("tpubDC5FSnBiZDMmhiuCmWAYsLwgLYrrT9rAqvTySfuCCrgsWz8wxMXUS9Tb9iVMvcRbvFcAHGkMD5Kx8koh4GquNGNTfohfk7pgjhaPCdXpoba"), "NONSENSE", 0, True, "test"): + "None", + } + func = embit_utils.get_single_sig_address + + print() + for args, expected in vectors_args_expected.items(): + print("\nasserting...") + + # call without optional params (defaults: script_type="nat", index=0, is_change=False, embit_network="main") + if args[1:5] == ("nat", 0, False, "main"): + print(f' {func.__name__}(HDKey.from_string("{args[0]}")) == "{expected}"') + assert str(func(args[0])) == expected + + # call with ordered params + print(f' {func.__name__}(HDKey.from_string("{args[0]}"), *{args[1:5]}) == "{expected}"') + assert str(func(*args)) == expected + + # call with named params + print(f' {func.__name__}(xpub=HDKey.from_string("{args[0]}"), script_type="{args[1]}", index={args[2]}, is_change={args[3]}, embit_network="{args[4]}") == "{expected}"') + assert str(func(xpub=args[0], script_type=args[1], index=args[2], is_change=args[3], embit_network=args[4])) == expected + + +def test_get_multisig_address(): + """ + tests seedsigner.helpers.embit_utils.get_multisig_address() + """ + + from embit.descriptor import Descriptor + + # jdlcdl: these vectors created with electrum & sparrow as a 2 of 3 multisig based on bip39-bip32-standard-path wallets + # keystore1 = 0x00*16 = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about' + # keystore2 = 0x11*16 = 'baby mass dust captain baby mass dust captain baby mass dust casino' + # keystore3 = 0x22*16 = 'captain baby mass dust captain baby mass dust captain baby mass dutch' + + vector_args_expected = { + # multisig native segwit on testnet, first payment and change addresses + ("wsh(sortedmulti(2,[8d55ff0d/48h/1h/0h/2h]tpubDDxNVWk924RTUhdkVB2uLHw1hGMPNMGufpZefhkkswjbZppVZcuMdjYKQN4ewUog9vbL6RBLFPRWcgTGT7kYP79N6thyJ43ELUs4N2szXMg/{0,1}/*,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/{0,1}/*,[0be174ee/48h/1h/0h/2h]tpubDEsePyLPkbxbrDiZSTTWdsviiNtiQjrvvzZnkLtG72QYLBygEsXePRsTdXi8DeMA7taCuuvoEBjUAfFrsNZeQJqfvG9fFoujYWbFPYUn7ux/{0,1}/*))#zw6cnrlk" , 0, False, "test"): "tb1q7tpecll8jhp77yqdeyt2t8q5swxmmqeh2v22cqpms5dxlp6p27dqlftet8", + ("wsh(sortedmulti(2,[8d55ff0d/48h/1h/0h/2h]tpubDDxNVWk924RTUhdkVB2uLHw1hGMPNMGufpZefhkkswjbZppVZcuMdjYKQN4ewUog9vbL6RBLFPRWcgTGT7kYP79N6thyJ43ELUs4N2szXMg/{0,1}/*,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/{0,1}/*,[0be174ee/48h/1h/0h/2h]tpubDEsePyLPkbxbrDiZSTTWdsviiNtiQjrvvzZnkLtG72QYLBygEsXePRsTdXi8DeMA7taCuuvoEBjUAfFrsNZeQJqfvG9fFoujYWbFPYUn7ux/{0,1}/*))#zw6cnrlk" , 0, True, "test"): "tb1q7h94ywhfjrpxdfzwl4dcawrg80r4rywswjrh447x4n3e5t3m0jms9jh7pm", + + # multisig nested segwit on testnet, first payment and change addresses + ("sh(wsh(sortedmulti(2,[73c5da0a/48h/1h/1h/0h/1h]tpubDFH9dgzveyD8yHQb8VrpG8FYAuwcLMHMje2CCcbBo1FpaGzYVtJeYYxcYgRqSTta5utUFts8nPPHs9C2bqoxrey5jia6Dwf9mpwrPq7YvcJ/{0,1}/*,[0be174ee/48h/1h/0h/1h]tpubDEsePyLPkbxbnj6XuKvWwdERHaKkikZxaGJ9sJqmM7okbZXgkNSFiGU6GX6qEes6kD8f9Z9FosYB9UEnBSgBEyEwwJhj4uUcFE1WE8VtKoh/{0,1}/*,[8d55ff0d/48h/1h/0h/1h]tpubDDxNVWk924RTT3vyGLHdSDoZ2JUVX7jUsPcwCQ9MrKHAtJrW5zECTF9rFHCvqu526E4PjHp61hBknts2c5aGexvX7hvCZ8TGPvQFdzxxy59/{0,1}/*)))#2ujlfp73", 0, False, "test"): "2MtgJH28mZWNWU7VRU4ba6ciFbRRGYWZDt3", + ("sh(wsh(sortedmulti(2,[73c5da0a/48h/1h/1h/0h/1h]tpubDFH9dgzveyD8yHQb8VrpG8FYAuwcLMHMje2CCcbBo1FpaGzYVtJeYYxcYgRqSTta5utUFts8nPPHs9C2bqoxrey5jia6Dwf9mpwrPq7YvcJ/{0,1}/*,[0be174ee/48h/1h/0h/1h]tpubDEsePyLPkbxbnj6XuKvWwdERHaKkikZxaGJ9sJqmM7okbZXgkNSFiGU6GX6qEes6kD8f9Z9FosYB9UEnBSgBEyEwwJhj4uUcFE1WE8VtKoh/{0,1}/*,[8d55ff0d/48h/1h/0h/1h]tpubDDxNVWk924RTT3vyGLHdSDoZ2JUVX7jUsPcwCQ9MrKHAtJrW5zECTF9rFHCvqu526E4PjHp61hBknts2c5aGexvX7hvCZ8TGPvQFdzxxy59/{0,1}/*)))#2ujlfp73", 0, True, "test"): "2NAjjwUQqwD9XRGLeQ6TitSUyMHUz3cLiWm", + + # legacy multisig p2sh on testnet, not supported + ("sh(sortedmulti(2,[8d55ff0d/45h]tpubDANogJ2yfnizHwX7fSi5kUVzybyuPXDhgHB2TR9TUvkSLZFW73cRq4STKFDpx7qjJJiisyq82tbu4CeiYtmKEmT1xoCq9P8BPvXV31HUh6d/{0,1}/*,[0be174ee/45h]tpubDBkeVF2tDNT1Pz7L47iJeBB6RokU12LX6x4E6Ph8T89hmjQfB77q1AMyGwL8qpREVGq9sCJEbWwmnemwNTxnpxGn1di7BGy8jx9wEi5Vahu/{0,1}/*,[73c5da0a/45h]tpubDBKsGC1UqBDNvx9aivFmxZNgeZTUnmsCFGhWrqkLzucUCDePvbWWm3n8tAaAwMmxBG2ihdKCG9fzBdUnMxKx5PrkiqSZFi6Vkv6msUs9ddN/{0,1}/*))#p5t8sa8c", 0, False, "test"): Exception, + + # multisig taproot on testnet, not supported + # TODO: find what a multisig-taproot descriptor would look like and add a test so we can fall into the last condition exception. + + # some policy that is not supported: + # TODO: find anything non supported so we can drop off the function: Would it be preferred to "else: raise ValueError()"? + #("sh(multi(2,[8d55ff0d/45h]tpubDANogJ2yfnizHwX7fSi5kUVzybyuPXDhgHB2TR9TUvkSLZFW73cRq4STKFDpx7qjJJiisyq82tbu4CeiYtmKEmT1xoCq9P8BPvXV31HUh6d/{0,1}/*,[0be174ee/45h]tpubDBkeVF2tDNT1Pz7L47iJeBB6RokU12LX6x4E6Ph8T89hmjQfB77q1AMyGwL8qpREVGq9sCJEbWwmnemwNTxnpxGn1di7BGy8jx9wEi5Vahu/{0,1}/*,[73c5da0a/45h]tpubDBKsGC1UqBDNvx9aivFmxZNgeZTUnmsCFGhWrqkLzucUCDePvbWWm3n8tAaAwMmxBG2ihdKCG9fzBdUnMxKx5PrkiqSZFi6Vkv6msUs9ddN/{0,1}/*))#", 0, False, "test"): None, + + } + func = embit_utils.get_multisig_address + + print() + for args, expected in vector_args_expected.items(): + descriptor = Descriptor.from_string(args[0]) + + print("\nasserting...") + + # test successful calls + if type(expected) == str: + # call with optional params (defaults: index=0, is_change=False, embit_network="main") + if args[1:4] == (0, False, 'main'): + print(f' {func.__name__}(Descriptor.from_string("{descriptor}")) == "{expected}"') + assert func(descriptor) == expected + + # call with ordered params + print(f' {func.__name__}(Descriptor.from_string("{descriptor}"), *{args[1:4]}) == "{expected}"') + assert func(descriptor, *args[1:4]) == expected + + # call with named params + print(f' {func.__name__}(descriptor=Descriptor.from_string("{descriptor}"), index={args[1]}, is_change={args[2]}, embit_network="{args[3]}") == "{expected}"') + assert func(descriptor=descriptor, index=args[1], is_change=args[2], embit_network=args[3]) == expected + + # test exceptions + else: + # call with ordered params + with pytest.raises(expected): + print(f' {func.__name__}(Descriptor.from_string("{descriptor}"), *{args[1:4]}) raises Exception') + func(descriptor, *args[1:4]) + + # call with named params + with pytest.raises(expected): + print(f' {func.__name__}(descriptor=Descriptor.from_string("{descriptor}"), index={args[1]}, is_change={args[2]}, embit_network="{args[3]}") raises Exception"') + func(descriptor=descriptor, index=args[1], is_change=args[2], embit_network=args[3]) + + +def test_parse_derivation_path(): + # Shouldn't care if input uses "'" or "h" + derivation_path = "m/84'/0'/0'/0/0" + + result = embit_utils.parse_derivation_path(derivation_path) + assert result["script_type"] == SC.NATIVE_SEGWIT + assert result["network"] == SC.MAINNET + + result = embit_utils.parse_derivation_path(derivation_path.replace("'", "h")) + assert result["script_type"] == SC.NATIVE_SEGWIT + assert result["network"] == SC.MAINNET + + # Now exhaustively test supported permutations + vectors_args = { + (SC.MAINNET, SC.NATIVE_SEGWIT, False): "m/84'/0'/0'/0/5", + (SC.TESTNET, SC.NATIVE_SEGWIT, False): "m/84'/1'/0'/0/5", + (SC.REGTEST, SC.NATIVE_SEGWIT, False): "m/84'/1'/0'/0/5", + (SC.MAINNET, SC.NATIVE_SEGWIT, True): "m/84'/0'/0'/1/5", + (SC.TESTNET, SC.NATIVE_SEGWIT, True): "m/84'/1'/0'/1/5", + (SC.REGTEST, SC.NATIVE_SEGWIT, True): "m/84'/1'/0'/1/5", + + (SC.MAINNET, SC.NESTED_SEGWIT, False): "m/49'/0'/0'/0/5", + (SC.TESTNET, SC.NESTED_SEGWIT, False): "m/49'/1'/0'/0/5", + (SC.REGTEST, SC.NESTED_SEGWIT, False): "m/49'/1'/0'/0/5", + (SC.MAINNET, SC.NESTED_SEGWIT, True): "m/49'/0'/0'/1/5", + (SC.TESTNET, SC.NESTED_SEGWIT, True): "m/49'/1'/0'/1/5", + (SC.REGTEST, SC.NESTED_SEGWIT, True): "m/49'/1'/0'/1/5", + + (SC.MAINNET, SC.TAPROOT, False): "m/86'/0'/0'/0/5", + (SC.TESTNET, SC.TAPROOT, False): "m/86'/1'/0'/0/5", + (SC.REGTEST, SC.TAPROOT, False): "m/86'/1'/0'/0/5", + (SC.MAINNET, SC.TAPROOT, True): "m/86'/0'/0'/1/5", + (SC.TESTNET, SC.TAPROOT, True): "m/86'/1'/0'/1/5", + (SC.REGTEST, SC.TAPROOT, True): "m/86'/1'/0'/1/5", + + # Try a typical custom derivation path (Unchained vault keys) + (SC.MAINNET, SC.CUSTOM_DERIVATION, False): "m/45'/0'/0'/0/5", + (SC.TESTNET, SC.CUSTOM_DERIVATION, False): "m/45'/1'/0'/0/5", + (SC.REGTEST, SC.CUSTOM_DERIVATION, False): "m/45'/1'/0'/0/5", + (SC.MAINNET, SC.CUSTOM_DERIVATION, True): "m/45'/0'/0'/1/5", + (SC.TESTNET, SC.CUSTOM_DERIVATION, True): "m/45'/1'/0'/1/5", + (SC.REGTEST, SC.CUSTOM_DERIVATION, True): "m/45'/1'/0'/1/5", + + # CRAZY custom derivation paths + (None, SC.CUSTOM_DERIVATION, False, 5): "m/123'/9083270/9083270/9083270/9083270/0/5", + + # non-standard change and/or index + (None, SC.CUSTOM_DERIVATION, None, 5): "m/9'/78/5", + (None, SC.CUSTOM_DERIVATION, None, 5): "m/9'/78'/5", + (None, SC.CUSTOM_DERIVATION, None, None): "m/9'/78'/5'", + (None, SC.CUSTOM_DERIVATION, False, None): "m/9'/0/5'", + } + + for expected_result, derivation_path in vectors_args.items(): + actual_result = embit_utils.parse_derivation_path(derivation_path) + + if expected_result[0] == SC.MAINNET: + assert actual_result["network"] == expected_result[0] + assert actual_result["clean_match"] is True + elif expected_result[0] is None: + assert actual_result["network"] is None + assert actual_result["clean_match"] is False + else: + # Testnet and regtest are returned as a list since the parser can't tell which is intended + assert expected_result[0] in actual_result["network"] + assert actual_result["clean_match"] is True + + assert actual_result["script_type"] == expected_result[1] + assert actual_result["is_change"] == expected_result[2] + + if len(expected_result) == 4: + assert actual_result["index"] == expected_result[3] + else: + assert actual_result["index"] == int(derivation_path.split("/")[-1]) diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py index 0dc5c0f4..75ed796f 100644 --- a/tests/test_encodepsbtqr.py +++ b/tests/test_encodepsbtqr.py @@ -1,6 +1,5 @@ -import pytest -from mock import MagicMock -from seedsigner.models import EncodeQR, QRType +from seedsigner.models.encode_qr import EncodeQR +from seedsigner.models.qr_type import QRType from embit import psbt from binascii import a2b_base64 diff --git a/tests/test_flows.py b/tests/test_flows.py new file mode 100644 index 00000000..3b99eb9d --- /dev/null +++ b/tests/test_flows.py @@ -0,0 +1,131 @@ +import pytest + +# Must import test base before the Controller +from base import FlowTest, FlowStep, FlowTestUnexpectedViewException, FlowTestInvalidButtonDataSelectionException, FlowTestRunScreenNotExecutedException + +from seedsigner.controller import Controller +from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON +from seedsigner.models.seed import Seed +from seedsigner.models.settings_definition import SettingsConstants +from seedsigner.views.seed_views import SeedBackupView, SeedMnemonicEntryView, SeedOptionsView, SeedWordsWarningView +from seedsigner.views.view import MainMenuView, PowerOptionsView, UnhandledExceptionView +from seedsigner.views.tools_views import ToolsMenuView, ToolsCalcFinalWordNumWordsView + + + +class TestFlowTest(FlowTest): + + def test_simple_flow(self): + """ + Basic test to ensure the FlowTest can flow through a sequence of Views and + terminate via the StopControllerCommand. + """ + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS), + FlowStep(ToolsMenuView, button_data_selection=ToolsMenuView.KEYBOARD), + FlowStep(ToolsCalcFinalWordNumWordsView, button_data_selection=ToolsCalcFinalWordNumWordsView.TWELVE), + FlowStep(SeedMnemonicEntryView), + ]) + + + def test_FlowTestUnexpectedViewException(self): + """ + Ensure that the FlowTest will raise a FlowTestUnexpectedViewException if the next + View in the sequence is not the expected View. + """ + with pytest.raises(FlowTestUnexpectedViewException): + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=RET_CODE__POWER_BUTTON), + FlowStep(ToolsMenuView), # <-- Wrong target View! Should raise an AssertionError. + ]) + + + def test_FlowTestInvalidButtonDataSelectionException(self): + """ + Ensure that the FlowTest will raise a FlowTestUnexpectedViewException if the + specified button_data_selection in invalid. + """ + with pytest.raises(FlowTestInvalidButtonDataSelectionException): + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection="this is not a real button option!"), + ]) + + + def test_FlowTestRunScreenNotExecutedException(self): + """ + Ensure that the FlowTest will raise a FlowTestRunScreenNotExecutedException if the next + View in the sequence doesn't call its View.run_screen(). + """ + # Disable dire warnings so that the SeedWordsWarningView won't execute its run_screen() + self.settings.set_value(SettingsConstants.SETTING__DIRE_WARNINGS, SettingsConstants.OPTION__DISABLED) + self.controller.storage.set_pending_seed(Seed(mnemonic=["bacon"] * 24)) + self.controller.storage.finalize_pending_seed() + + with pytest.raises(FlowTestRunScreenNotExecutedException): + self.run_sequence( + initial_destination_view_args=dict(seed_num=0), + sequence=[ + FlowStep(SeedOptionsView, button_data_selection=SeedOptionsView.BACKUP), + FlowStep(SeedBackupView, button_data_selection=SeedBackupView.VIEW_WORDS), + FlowStep(SeedWordsWarningView, screen_return_value=0), + ], + ) + + + def test_before_run_executes(self): + """ + Ensure that the FlowTest can execute a function before running a View. + """ + # TODO + pass + + + def test_back_button_flow(self): + """ + Ensure that the FlowTest works correctly with the Controller and its BackStack. + """ + self.run_sequence([ + FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON), + FlowStep(PowerOptionsView, screen_return_value=RET_CODE__BACK_BUTTON), + FlowStep(MainMenuView), + ]) + + + def test_initial_destination(self): + """ + Ensure that the FlowTest can start from a View other than MainMenuView. + """ + # Don't have to start at the MainMenuView; can jump straight in + self.run_sequence([ + FlowStep(ToolsCalcFinalWordNumWordsView), + ]) + + # And again, but this time with a View that requires input view_args + self.reset_controller() + self.controller = Controller.get_instance() + + # Load a seed into the Controller + seed = Seed(mnemonic=["abandon "* 11 + "about"]) + self.controller.storage.set_pending_seed(seed) + self.controller.storage.finalize_pending_seed() + + self.run_sequence( + initial_destination_view_args=dict(seed_num=0), + sequence=[ + FlowStep(SeedOptionsView, button_data_selection=SeedOptionsView.BACKUP), + FlowStep(SeedBackupView), + ] + ) + + + def test_raise_exception_via_screen_return_value(self): + """ + Ensure that the FlowTest can raise an exception via the screen_return_value. + """ + # A generic Exception should be caught by the Controller and routed to the + # UnhandledExceptionView. + self.run_sequence([ + FlowStep(MainMenuView, screen_return_value=Exception("Test exception")), + FlowStep(UnhandledExceptionView), + ]) + diff --git a/tests/test_flows_psbt.py b/tests/test_flows_psbt.py new file mode 100644 index 00000000..02b71446 --- /dev/null +++ b/tests/test_flows_psbt.py @@ -0,0 +1,93 @@ +from base import FlowTest, FlowStep + +from seedsigner.controller import Controller +from seedsigner.views.view import MainMenuView +from seedsigner.views import scan_views, seed_views, psbt_views + +class TestPSBTFlows(FlowTest): + + def test_scan_psbt_first_then_correct_seedqr_flow(self): + """ + Selecting "Scan" from the MainMenuView and scanning a PSBT should enter the PSBTSelectSeedView flow + when Scan a Seed is selected from PSBTSelectSeedView it should enter the ScanView flow + when a SeedQR is scanned it should enter the PSBTOverviewView flow + since the PSBT has change no warning is displayed and it should enter the PSBTMathView flow + since the PSBT is not a self transfer it should enter the PSBTAddressDetailsView flow + """ + def load_psbt_into_decoder(view: scan_views.ScanView): + """ + PSBT Tx and Wallet Details + - Single Sig Wallet P2WPKH (Native Segwit) with no passphrase + - Regtest c751dc07 m/84'/1'/0' tpubDDZBrnxMxbVzqt8EoEiABPxeKzFWma5pra5UEbg3Wst1hrwr6feuvcy7Sov7cpuYx94ypuy1PQ9NDNoQagFs37wGALzLb5Ei3FvyJWPPPKZ + - 2 Inputs + - 56,522,834 sats + - 1,990,245,069 sats + - 4 Outputs + - 1 Output to another wallet (bcrt1q7cw0wzy8g6mq5qvkpvhnk5gsps5ncy3srp0n2j) of 123,456 sats + - 3 Outputs change + - 3 outputs to emulate a fake mix to increase privacy + - Change addresses are index 1/7, 1/8, 1/9 + - 1/7 address bcrt1q53j0xwuskuf5gnvynadh0hlazyy8srydlucrhg with amount 123,456 sats + - 1/8 address bcrt1q5gtw3zfp4cx67yk5q42q6j6rfza8aqcwpyyslv with amount 1,990,121,477 sats + - 1/9 address bcrt1q9rrg7399m43cn0yg4tz0v0ate89jgf2d6kpz7v with amount 56,399,242 sats + - Fee 272 sats + """ + view.decoder.add_data("cHNidP8BANgCAAAAAsTXZs3fz/dmGb6M80+jjvJZdYya+cw5bT/dGuhZFdSlAAAAAAD9////qo6xg/UZAvUkcbse1F+C9zbP/FeZNjThx7SCIn6eMCgBAAAAAP3///8EQOIBAAAAAAAWABSkZPM7kLcTRE2En1t33/0RCHgMjQXYnnYAAAAAFgAUKMaPRKXdY4m8iKrE9j+rycskJU1A4gEAAAAAABYAFPYc9wiHRrYKAZYLLztREAwpPBIwipVcAwAAAAAWABSiFuiJIa4NrxLUBVQNS0NIun6DDtoRAABPAQQ1h88DBcQGZIAAAAA+0J+jlNL3dpWwlnBi8Dx+Ipg4e6uvB3HdjzFPX7r9CAOOlAIxgII+/xCcj+XoEenKH7wj5s5wlu7Q7CCZWFLGLhA5Su0UVAAAgAEAAIAAAACAAAEA7QIAAAAEE6njX/fnvn7hbkKIRcxzNYFOSfbCdNeWnd7Fe/1UcQ0BAAAAAP3///8TqeNf9+e+fuFuQohFzHM1gU5J9sJ015ad3sV7/VRxDQMAAAAA/f///xOp41/3575+4W5CiEXMczWBTkn2wnTXlp3exXv9VHENBAAAAAD9////E6njX/fnvn7hbkKIRcxzNYFOSfbCdNeWnd7Fe/1UcQ0GAAAAAP3///8CUnheAwAAAAAWABRCfygPJ+Fjsx4BknYvvm3A3qKn2xJ/XQcAAAAAF6kU1I4TAst5nAj15ey7vwe5cM3OFq+HlhEAAAEBH1J4XgMAAAAAFgAUQn8oDyfhY7MeAZJ2L75twN6ip9sBAwQBAAAAIgYCo7sfm78RQY3B5n0ac/QF8VtMAzFnci+h5D1MtpgRY7oYOUrtFFQAAIABAACAAAAAgAEAAAAGAAAAAAEAcQIAAAABxY7wh0nsfJQfzWrD/9rN9BYsM+iOmPaO6I0ANFgO/PcAAAAAAP3///8CptiUAAAAAAAWABRIm4HhQY/TzOjeWSPRrbuJo9MlW826oHYAAAAAFgAU0z+0L2QSLGtyQTn8FhbCpcI7jbliAQAAAQEfzbqgdgAAAAAWABTTP7QvZBIsa3JBOfwWFsKlwjuNuQEDBAEAAAAiBgITHmebEANk81CraV4xZIpqkNjjw0tIvezl1Ism1NRH3Rg5Su0UVAAAgAEAAIAAAACAAQAAAAAAAAAAIgICuTT7WnuiUTpObjWnZFHzIeEvW9PTB+1LLVFNQJVFeIIYOUrtFFQAAIABAACAAAAAgAEAAAAHAAAAACICAk8f3hpc5C35chgSg+Pe2zZ9IhHREd4aKW2+yAMRIFeqGDlK7RRUAACAAQAAgAAAAIABAAAACQAAAAAAIgIDjt1CjvrnMMnjbmTNKUAYoKEDRbmKjNjbq+6Ppqj3bqQYOUrtFFQAAIABAACAAAAAgAEAAAAIAAAAAA==") + + def load_seed_into_decoder(view: scan_views.ScanView): + view.decoder.add_data("080115060387063104071857067618681125136207731354") + + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), + FlowStep(scan_views.ScanView, before_run=load_psbt_into_decoder), # simulate read PSBT; ret val is ignored + FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.SCAN_SEED), + FlowStep(scan_views.ScanSeedQRView, before_run=load_seed_into_decoder), + FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), + FlowStep(seed_views.SeedOptionsView, is_redirect=True), + FlowStep(psbt_views.PSBTOverviewView), + FlowStep(psbt_views.PSBTMathView), + FlowStep(psbt_views.PSBTAddressDetailsView, button_data_selection=0), + FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT), + FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT), + FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT), + FlowStep(psbt_views.PSBTFinalizeView, button_data_selection=psbt_views.PSBTFinalizeView.APPROVE_PSBT), + FlowStep(psbt_views.PSBTSignedQRDisplayView), + FlowStep(MainMenuView) + ]) + + def test_scan_multisig_psbt_seed_already_signed_flow(self): + + def load_psbt_into_decoder(view: scan_views.ScanView): + view.decoder.add_data("cHNidP8BAIkCAAAAAc9dCSh2RcRPfHaT5bNVBpbg0jAekRLqOK+bpN/QA0jeAAAAAAD9////AtAHAAAAAAAAIlEg24shYsV3IRCzlgmMKjAsR4Ad9tX896z7zDAi5q0TU9H3CgAAAAAAACIAIByGQg/VP2aRID62ty40E64HYZeRRsKRGLt8J/76R6stQ04FAE8BBDWHzwSLLGdzgAAAAq3q6nR20JnHR+vKrBQdWxN9C7xU8zNX942mVF7AQpl2ArrdLwVlkGxaatQJ4wwkvypNBKbwOq9hXGLNlKi7rZWAFDUxzXUwAACAAQAAgAAAAIACAACATwEENYfPBHOCZmWAAAACmH6KTXIny0vueRgQFBq4M6oMuG8f1QM0I/RzKQ03bCgCHrF0fyUtV0+FD2N34u/woqb8MAt/o+7Ed58RddhY8zYUCUjSaDAAAIABAACAAAAAgAIAAIAAAQEriBMAAAAAAAAiACBY4WsjDgJXLj3VW222jU1tkIIhT26ce/2efH73BWGGBiICAqyfkrdUO662QBrdvJcSOZMFxniD7M1awm9U0Kb5XCm5RzBEAiAPkQTY84YjFFkpD6MI2cc5rJySqws5fsTQA/8XEZFpbAIgTNVykbEH4Z7bqyzhhy6lty0K8rtCUDCaHNv+47NNIWgBAQMEAQAAAAEFR1IhApL4XO+VE1pPYn5wnRFyJQKVSc9TX2dO6KIBH6jwvgPaIQKsn5K3VDuutkAa3byXEjmTBcZ4g+zNWsJvVNCm+VwpuVKuIgYCkvhc75UTWk9ifnCdEXIlApVJz1NfZ07oogEfqPC+A9ocNTHNdTAAAIABAACAAAAAgAIAAIAAAAAAAAAAACIGAqyfkrdUO662QBrdvJcSOZMFxniD7M1awm9U0Kb5XCm5HAlI0mgwAACAAQAAgAAAAIACAACAAAAAAAAAAAAAAAEBR1IhApYXaczuYbBM/A+EH639Ir2yIB4PxL46dK/I1V1O9aHgIQLa02HCI/+EP+9gGpxHskjYWFN5hZzXY7RRvwV4UF42ylKuIgIClhdpzO5hsEz8D4Qfrf0ivbIgHg/Evjp0r8jVXU71oeAcNTHNdTAAAIABAACAAAAAgAIAAIABAAAAAAAAACICAtrTYcIj/4Q/72AanEeySNhYU3mFnNdjtFG/BXhQXjbKHAlI0mgwAACAAQAAgAAAAIACAACAAQAAAAAAAAAA") + + def load_seed_into_decoder(view: scan_views.ScanView): + view.decoder.add_data("073318950739065415961602009907670428187212261116") + + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), + FlowStep(scan_views.ScanView, before_run=load_psbt_into_decoder), # simulate read PSBT; ret val is ignored + FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.SCAN_SEED), + FlowStep(scan_views.ScanSeedQRView, before_run=load_seed_into_decoder), + FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), + FlowStep(seed_views.SeedOptionsView, is_redirect=True), + FlowStep(psbt_views.PSBTOverviewView), + FlowStep(psbt_views.PSBTMathView), + FlowStep(psbt_views.PSBTAddressDetailsView, button_data_selection=0), + FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT), + FlowStep(psbt_views.PSBTFinalizeView, button_data_selection=psbt_views.PSBTFinalizeView.APPROVE_PSBT), + FlowStep(psbt_views.PSBTSigningErrorView, button_data_selection=psbt_views.PSBTSigningErrorView.SELECT_DIFF_SEED), + FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.SCAN_SEED), + FlowStep(scan_views.ScanSeedQRView, before_run=load_seed_into_decoder), + FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE), + FlowStep(seed_views.SeedAddPassphraseView, screen_return_value="abc"), + FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.DONE), + FlowStep(seed_views.SeedOptionsView, is_redirect=True), + FlowStep(psbt_views.PSBTOverviewView), + FlowStep(psbt_views.PSBTMathView), + FlowStep(psbt_views.PSBTAddressDetailsView, button_data_selection=0), + FlowStep(psbt_views.PSBTChangeDetailsView, button_data_selection=psbt_views.PSBTChangeDetailsView.NEXT), + FlowStep(psbt_views.PSBTFinalizeView, button_data_selection=psbt_views.PSBTFinalizeView.APPROVE_PSBT), + FlowStep(psbt_views.PSBTSignedQRDisplayView), + FlowStep(MainMenuView), + ]) + diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py new file mode 100644 index 00000000..3aedef94 --- /dev/null +++ b/tests/test_flows_seed.py @@ -0,0 +1,505 @@ +from typing import Callable +import pytest + +# Must import test base before the Controller +from base import BaseTest, FlowTest, FlowStep +from base import FlowTestRunScreenNotExecutedException, FlowTestInvalidButtonDataSelectionException + +from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON +from seedsigner.models.settings import Settings, SettingsConstants +from seedsigner.models.seed import Seed +from seedsigner.views.view import ErrorView, MainMenuView, OptionDisabledView, RemoveMicroSDWarningView, View, NetworkMismatchErrorView +from seedsigner.views import seed_views, scan_views, settings_views, tools_views + + +def load_seed_into_decoder(view: scan_views.ScanView): + view.decoder.add_data("0000" * 11 + "0003") + + + +class TestSeedFlows(FlowTest): + + def test_scan_seedqr_flow(self): + """ + Selecting "Scan" from the MainMenuView and scanning a SeedQR should enter the + Finalize Seed flow and end at the SeedOptionsView. + """ + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), + FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder), # simulate read SeedQR; ret val is ignored + FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), + FlowStep(seed_views.SeedOptionsView), + ]) + + + def test_mnemonic_entry_flow(self): + """ + Manually entering a mnemonic should land at the Finalize Seed flow and end at + the SeedOptionsView. + """ + def test_with_mnemonic(mnemonic): + Settings.HOSTNAME = "not seedsigner-os" + sequence = [ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS), + FlowStep(seed_views.SeedsMenuView, is_redirect=True), # When no seeds are loaded it auto-redirects to LoadSeedView + FlowStep(seed_views.LoadSeedView, button_data_selection=seed_views.LoadSeedView.TYPE_12WORD if len(mnemonic) == 12 else seed_views.LoadSeedView.TYPE_24WORD), + ] + + # Now add each manual word entry step + for word in mnemonic: + sequence.append( + FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word) + ) + + # With the mnemonic completely entered, we land on the SeedFinalizeView + sequence += [ + FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), + FlowStep(seed_views.SeedOptionsView), + ] + + self.run_sequence(sequence) + + # Test data from iancoleman.io; 12- and 24-word mnemonic + test_with_mnemonic("tone flat shed cool census soul paddle boy flight fantasy stem social".split()) + + BaseTest.reset_controller() + + test_with_mnemonic("cotton artefact spy mind wing there echo steak child oak awful host despair online bicycle divorce middle firm diamond rare execute chimney almost hollow".split()) + + + def test_invalid_mnemonic(self): + """ Should be able to go back and edit or discard an invalid mnemonic """ + # Test data from iancoleman.io + mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split() + sequence = [ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS), + FlowStep(seed_views.SeedsMenuView, is_redirect=True), # When no seeds are loaded it auto-redirects to LoadSeedView + FlowStep(seed_views.LoadSeedView, button_data_selection=seed_views.LoadSeedView.TYPE_12WORD if len(mnemonic) == 12 else seed_views.LoadSeedView.TYPE_24WORD), + ] + for word in mnemonic[:-1]: + sequence.append(FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word)) + + sequence += [ + FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value="zoo"), # But finish with an INVALID checksum word + FlowStep(seed_views.SeedMnemonicInvalidView, button_data_selection=seed_views.SeedMnemonicInvalidView.EDIT), + ] + + # Restarts from first word + for word in mnemonic[:-1]: + sequence.append(FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word)) + + sequence += [ + FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value="zebra"), # provide yet another invalid checksum word + FlowStep(seed_views.SeedMnemonicInvalidView, button_data_selection=seed_views.SeedMnemonicInvalidView.DISCARD), + FlowStep(MainMenuView), + ] + + self.run_sequence(sequence) + + + def test_export_xpub_standard_flow(self): + """ + Selecting "Export XPUB" from the SeedOptionsView should enter the Export XPUB flow and end at the MainMenuView + """ + + def flowtest_standard_xpub(sig_tuple, script_tuple, coord_tuple): + self.run_sequence( + initial_destination_view_args=dict(seed_num=0), + sequence=[ + FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB), + FlowStep(seed_views.SeedExportXpubSigTypeView, button_data_selection=sig_tuple[1]), + FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=script_tuple[1]), + FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=coord_tuple[1]), + FlowStep(seed_views.SeedExportXpubWarningView, screen_return_value=0), + FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0), + FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0), + FlowStep(MainMenuView), + ] + ) + + # Load a finalized Seed into the Controller + mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split() + self.controller.storage.set_pending_seed(Seed(mnemonic=mnemonic)) + self.controller.storage.finalize_pending_seed() + + # these are lists of (constant_value, display_name) tuples + sig_types: list[tuple[str, str]] = SettingsConstants.ALL_SIG_TYPES + script_types: list[tuple[str, str]] = SettingsConstants.ALL_SCRIPT_TYPES + coordinators: list[tuple[str, str]] = SettingsConstants.ALL_COORDINATORS + + # enable non-defaults so they're available in views + self.settings.set_value(SettingsConstants.SETTING__SIG_TYPES, [x for x,y in sig_types]) + self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [x for x,y in script_types]) + self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [x for x,y in coordinators]) + + # exhaustively test flows thru standard sig_types, script_types, and coordinators + for sig_tuple in sig_types: + for script_tuple in script_types: + for coord_tuple in coordinators: + # skip custom derivation + if script_tuple[0] == SettingsConstants.CUSTOM_DERIVATION: + continue + # skip multisig taproot + elif sig_tuple[0] == SettingsConstants.MULTISIG and script_tuple[0] == SettingsConstants.TAPROOT: + continue + else: + print('\n\ntest_standard_xpubs(%s, %s, %s)' % (sig_tuple, script_tuple, coord_tuple)) + flowtest_standard_xpub(sig_tuple, script_tuple, coord_tuple) + + + def test_export_xpub_disabled_not_available_flow(self): + """ + If sig_type/script_type/coordinator disabled, then these options are not available + """ + # Load a finalized Seed into the Controller + mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split() + self.controller.storage.set_pending_seed(Seed(mnemonic=mnemonic)) + self.controller.storage.finalize_pending_seed() + + # these are lists of (constant_value, display_name) tuples + sig_types: list[tuple[str, str]] = SettingsConstants.ALL_SIG_TYPES + script_types: list[tuple[str, str]] = SettingsConstants.ALL_SCRIPT_TYPES + coordinators: list[tuple[str, str]] = SettingsConstants.ALL_COORDINATORS + + # these are the disabled types that we will be testing + disabled_sig = SettingsConstants.MULTISIG + disabled_script = SettingsConstants.TAPROOT + disabled_coord = SettingsConstants.COORDINATOR__NUNCHUK + + # enable all but our target disabled type + self.settings.set_value(SettingsConstants.SETTING__SIG_TYPES, [x for x,y in sig_types if x!=disabled_sig]) + self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [x for x,y in script_types if x!=disabled_script]) + self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [x for x,y in coordinators if x!=disabled_coord]) + + # test that multisig is not an option via exception raised when redirected to next step instead of having a choice + with pytest.raises(FlowTestRunScreenNotExecutedException) as e: + self.run_sequence( + initial_destination_view_args=dict(seed_num=0), + sequence=[ + FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB), + FlowStep(seed_views.SeedExportXpubSigTypeView, button_data_selection=disabled_sig), + ] + ) + + # test that taproot is not an option via exception raised when choice is taproot + with pytest.raises(FlowTestInvalidButtonDataSelectionException) as e: + self.run_sequence( + initial_destination_view_args=dict(seed_num=0), + sequence=[ + FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB), + FlowStep(seed_views.SeedExportXpubSigTypeView, is_redirect=True), + FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=disabled_script), + ] + ) + + # test that nunchuk is not an option via exception raised when choice is nunchuk + with pytest.raises(FlowTestInvalidButtonDataSelectionException) as e: + self.run_sequence( + initial_destination_view_args=dict(seed_num=0), + sequence=[ + FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB), + FlowStep(seed_views.SeedExportXpubSigTypeView, is_redirect=True), + FlowStep(seed_views.SeedExportXpubScriptTypeView, screen_return_value=0), + FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=disabled_coord), + ] + ) + + + def test_export_xpub_custom_derivation_flow(self): + """ + Export XPUB flow for custom derivation finishes at MainMenuView + """ + # Load a finalized Seed into the Controller + mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split() + self.controller.storage.set_pending_seed(Seed(mnemonic=mnemonic)) + self.controller.storage.finalize_pending_seed() + + # enable custom derivation script_type setting (plus at least one more for a choice) + self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [ + SettingsConstants.NATIVE_SEGWIT, + SettingsConstants.NESTED_SEGWIT, + SettingsConstants.CUSTOM_DERIVATION + ]) + + # get display names to access button choices in the views (ugh: hardcoding, is there a better way?) + sig_type = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SIG_TYPES)[0] # single sig + script_type = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES)[2] # custom derivation + coordinator = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)[3] # specter + + self.run_sequence( + initial_destination_view_args=dict(seed_num=0), + sequence=[ + FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB), + FlowStep(seed_views.SeedExportXpubSigTypeView, button_data_selection=sig_type), + FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=script_type), + FlowStep(seed_views.SeedExportXpubCustomDerivationView, screen_return_value="m/0'/0'"), + FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=coordinator), + FlowStep(seed_views.SeedExportXpubWarningView, screen_return_value=0), + FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0), + FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0), + FlowStep(MainMenuView), + ] + ) + + + def test_export_xpub_skip_non_option_flow(self): + """ + Export XPUB flows w/o user choices when no other options for sig_types, script_types, and/or coordinators + """ + # Load a finalized Seed into the Controller + mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split() + self.controller.storage.set_pending_seed(Seed(mnemonic=mnemonic)) + self.controller.storage.finalize_pending_seed() + + # exclusively set only one choice for each of sig_types, script_types and coordinators + self.settings.update({ + SettingsConstants.SETTING__SIG_TYPES: SettingsConstants.MULTISIG, + SettingsConstants.SETTING__SCRIPT_TYPES: SettingsConstants.NESTED_SEGWIT, + SettingsConstants.SETTING__COORDINATORS: SettingsConstants.COORDINATOR__SPECTER_DESKTOP, + }) + + self.run_sequence( + initial_destination_view_args=dict(seed_num=0), + sequence=[ + FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB), + FlowStep(seed_views.SeedExportXpubSigTypeView, is_redirect=True), + FlowStep(seed_views.SeedExportXpubScriptTypeView, is_redirect=True), + FlowStep(seed_views.SeedExportXpubCoordinatorView, is_redirect=True), + FlowStep(seed_views.SeedExportXpubWarningView, screen_return_value=0), + FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0), + FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0), + FlowStep(MainMenuView), + ] + ) + + + def test_discard_seed_flow(self): + """ + Selecting "Discard Seed" from the SeedOptionsView should enter the Discard Seed flow and + remove the in-memory seed from the Controller. + """ + # Load a finalized Seed into the Controller + mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split() + self.controller.storage.set_pending_seed(Seed(mnemonic=mnemonic)) + self.controller.storage.finalize_pending_seed() + + self.run_sequence( + initial_destination_view_args=dict(seed_num=0), + sequence=[ + FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.DISCARD), + FlowStep(seed_views.SeedDiscardView, button_data_selection=seed_views.SeedDiscardView.DISCARD), + FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS), + FlowStep(seed_views.SeedsMenuView, is_redirect=True), # When no seeds are loaded it auto-redirects to LoadSeedView + FlowStep(seed_views.LoadSeedView), + ] + ) + + + +class TestMessageSigningFlows(FlowTest): + MAINNET_DERIVATION_PATH = "m/84h/0h/0h/0/0" + TESTNET_DERIVATION_PATH = "m/84h/1h/0h/0/0" + SHORT_MESSAGE = "I attest that I control this bitcoin address blah blah blah" + MULTIPAGE_MESSAGE = """Chancellor on brink of second bailout for banks + + Billions may be needed as lending squeeze tightens + + Alistair Darling has been forced to consider a second bailout for banks as the lending drought worsens. + + The Chancellor will decide within weeks whether to pump billions more into the economy as evidence mounts that the £37 billion part-nationalisation last year has failed to keep credit flowing. Options include cash injections, offering banks cheaper state guarantees to raise money privately or buying up “toxic assets”, The Times has learnt.""" + + + def load_seed_into_decoder(self, view: scan_views.ScanView): + view.decoder.add_data("0000" * 11 + "0003") + + + def load_signmessage_into_decoder(self, view:View, derivation_path: str, message: str): + view.decoder.add_data(f"signmessage {derivation_path} ascii:{message}") + + + def load_short_message_into_decoder(self, view: View): + self.load_signmessage_into_decoder(view, self.MAINNET_DERIVATION_PATH, self.SHORT_MESSAGE) + + + def load_testnet_message_into_decoder(self, view: View): + self.load_signmessage_into_decoder(view, self.TESTNET_DERIVATION_PATH, self.SHORT_MESSAGE) + + + def load_multipage_message_into_decoder(self, view: View): + self.load_signmessage_into_decoder(view, self.MAINNET_DERIVATION_PATH, self.MULTIPAGE_MESSAGE) + + + def inject_mesage_as_paged_message(self, view: View): + # Because the Screen won't actually run, we have to do the Screen's work here + from seedsigner.gui.components import reflow_text_into_pages, GUIConstants + paged = reflow_text_into_pages( + text=self.controller.sign_message_data["message"], + width=240 - 2*GUIConstants.EDGE_PADDING, + height=240 - GUIConstants.TOP_NAV_HEIGHT - 3*GUIConstants.EDGE_PADDING - GUIConstants.BUTTON_HEIGHT, + ) + self.controller.sign_message_data["paged_message"] = paged + + + def test_sign_message_flow(self): + """ + Should scan a `signmessage` QR and complete the message review, address review, + and signing flow. + """ + # Ensure message signing is enabled + self.settings.set_value(SettingsConstants.SETTING__MESSAGE_SIGNING, SettingsConstants.OPTION__ENABLED) + + # Scenario 1: Load the mesage first, then the seed + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), + FlowStep(scan_views.ScanView, before_run=self.load_short_message_into_decoder), # simulate read message QR; ret val is ignored + FlowStep(seed_views.SeedSignMessageStartView, is_redirect=True), + FlowStep(seed_views.SeedSelectSeedView, button_data_selection=seed_views.SeedSelectSeedView.SCAN_SEED), + FlowStep(scan_views.ScanView, before_run=self.load_seed_into_decoder), # simulate read SeedQR; ret val is ignored + FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), + FlowStep(seed_views.SeedOptionsView, is_redirect=True), + FlowStep(seed_views.SeedSignMessageConfirmMessageView, before_run=self.inject_mesage_as_paged_message, screen_return_value=0), + FlowStep(seed_views.SeedSignMessageConfirmAddressView, screen_return_value=0), + FlowStep(seed_views.SeedSignMessageSignedMessageQRView, screen_return_value=0), + FlowStep(MainMenuView), + ]) + + # Scenario 2: Scan the seed first, then select Sign Message + self.controller.discard_seed(0) + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), + FlowStep(scan_views.ScanView, before_run=self.load_seed_into_decoder), # simulate read SeedQR; ret val is ignored + FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), + FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.SIGN_MESSAGE), + FlowStep(scan_views.ScanView, before_run=self.load_short_message_into_decoder), # simulate read message QR; ret val is ignored + FlowStep(seed_views.SeedSignMessageStartView, is_redirect=True), + FlowStep(seed_views.SeedSignMessageConfirmMessageView, before_run=self.inject_mesage_as_paged_message, screen_return_value=0), + FlowStep(seed_views.SeedSignMessageConfirmAddressView, screen_return_value=0), + FlowStep(seed_views.SeedSignMessageSignedMessageQRView, screen_return_value=0), + FlowStep(MainMenuView), + ]) + + # Scenario 3: Load a long, multipage message + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), + FlowStep(scan_views.ScanView, before_run=self.load_multipage_message_into_decoder), # simulate read message QR; ret val is ignored + FlowStep(seed_views.SeedSignMessageStartView, is_redirect=True), + FlowStep(seed_views.SeedSelectSeedView, button_data_selection=seed_views.SeedSelectSeedView.SCAN_SEED), + FlowStep(scan_views.ScanView, before_run=self.load_seed_into_decoder), # simulate read SeedQR; ret val is ignored + FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), + FlowStep(seed_views.SeedOptionsView, is_redirect=True), + FlowStep(seed_views.SeedSignMessageConfirmMessageView, before_run=self.inject_mesage_as_paged_message, screen_return_value=0), # page 1/5 + FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=0), # page 2/5 + FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=0), # page 3/5 + FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=0), # page 4/5 + FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=0), # page 5/5 + + # Arrive at the address confirmation, then go backwards to re-review the paged message + FlowStep(seed_views.SeedSignMessageConfirmAddressView, screen_return_value=RET_CODE__BACK_BUTTON), # then back to page 5/5 + FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=RET_CODE__BACK_BUTTON), # back to page 4/5 + FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=RET_CODE__BACK_BUTTON), # back to page 3/5 + FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=RET_CODE__BACK_BUTTON), # back to page 2/5 + FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=RET_CODE__BACK_BUTTON), # back to page 1/5 + + # Now proceed forward again to the end + FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=0), # page 1/5 + FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=0), # page 2/5 + FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=0), # page 3/5 + FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=0), # page 4/5 + FlowStep(seed_views.SeedSignMessageConfirmMessageView, screen_return_value=0), # page 5/5 + FlowStep(seed_views.SeedSignMessageConfirmAddressView, screen_return_value=0), + FlowStep(seed_views.SeedSignMessageSignedMessageQRView, screen_return_value=0), + FlowStep(MainMenuView), + ]) + + + def test_sign_message_network_mismatch_flow(self): + """ + Should redirect to NetworkMismatchErrorView if a message's derivation path network doesn't match the current network. + + The error view should then forward to the Network Settings update View. + """ + # Ensure message signing is enabled + self.settings.set_value(SettingsConstants.SETTING__MESSAGE_SIGNING, SettingsConstants.OPTION__ENABLED) + + def expect_network_mismatch_error(load_message: Callable): + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), + FlowStep(scan_views.ScanView, before_run=load_message), # simulate read message QR; ret val is ignored + FlowStep(seed_views.SeedSignMessageStartView, is_redirect=True), + FlowStep(NetworkMismatchErrorView), + FlowStep(settings_views.SettingsEntryUpdateSelectionView), + ]) + + # MAINNET settings vs TESTNET derivation path with the message + self.settings.set_value(SettingsConstants.SETTING__NETWORK, SettingsConstants.MAINNET) + expect_network_mismatch_error(self.load_testnet_message_into_decoder) + + # TESTNET settings vs MAINNET derivation path with the message + self.settings.set_value(SettingsConstants.SETTING__NETWORK, SettingsConstants.TESTNET) + expect_network_mismatch_error(self.load_short_message_into_decoder) + + # REGTEST settings vs MAINNET derivation path with the message + self.settings.set_value(SettingsConstants.SETTING__NETWORK, SettingsConstants.REGTEST) + expect_network_mismatch_error(self.load_short_message_into_decoder) + + + + + def test_sign_message_option_disabled(self): + """ + Should redirect to OptionDisabledView if a `signmessage` QR is scanned with + message signing disabled. + + Should offer the option to route directly to enable that settings or return to + MainMenuView. + """ + # Ensure message signing is disabled + self.settings.set_value(SettingsConstants.SETTING__MESSAGE_SIGNING, SettingsConstants.OPTION__DISABLED) + + sequence = [ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), + FlowStep(scan_views.ScanView, before_run=self.load_short_message_into_decoder), # simulate read message QR; ret val is ignored + FlowStep(seed_views.SeedSignMessageStartView, is_redirect=True), + ] + + # First test routing to update the setting + self.run_sequence( + sequence + [ + FlowStep(OptionDisabledView, button_data_selection=OptionDisabledView.UPDATE_SETTING, is_redirect=True), + FlowStep(settings_views.SettingsEntryUpdateSelectionView), + ] + ) + + # Now test exiting to Main Menu + self.run_sequence( + sequence + [ + FlowStep(OptionDisabledView, button_data_selection=OptionDisabledView.DONE, is_redirect=True), + FlowStep(MainMenuView), + ] + ) + + + def test_sign_message_invalid_qr_flow(self): + """ + Should clear `Controller.resume_main_flow` and redirect to ErrorView if an + invalid signmessage QR is scanned. + + The error view should then forward to MainMenuView. + """ + # Ensure message signing is enabled + self.settings.set_value(SettingsConstants.SETTING__MESSAGE_SIGNING, SettingsConstants.OPTION__ENABLED) + + def load_invalid_signmessage_qr(view: scan_views.ScanView): + view.decoder.add_data("this text will not make sense to the decoder") + + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), + FlowStep(scan_views.ScanView, before_run=self.load_seed_into_decoder), # simulate read SeedQR; ret val is ignored + FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), + FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.SIGN_MESSAGE), + FlowStep(scan_views.ScanView, before_run=load_invalid_signmessage_qr), # simulate read message QR; ret val is ignored + FlowStep(ErrorView), + FlowStep(MainMenuView), + ]) + + assert self.controller.resume_main_flow is None diff --git a/tests/test_flows_settings.py b/tests/test_flows_settings.py new file mode 100644 index 00000000..a6eb34d9 --- /dev/null +++ b/tests/test_flows_settings.py @@ -0,0 +1,138 @@ +import os +from typing import Callable + +from mock import PropertyMock, patch + +# Must import test base before the Controller +from base import FlowTest, FlowStep + +from seedsigner.models.settings import Settings +from seedsigner.models.settings_definition import SettingsDefinition, SettingsConstants +from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON +from seedsigner.hardware.microsd import MicroSD +from seedsigner.views.view import MainMenuView +from seedsigner.views import scan_views, settings_views + + + +class TestSettingsFlows(FlowTest): + def test_persistent_settings(self): + """ Basic flow from MainMenuView to enable/disable persistent settings """ + # Which option are we testing? + settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__PERSISTENT_SETTINGS) + + # No settings file should exist before we enable persistent settings + assert os.path.exists(Settings.SETTINGS_FILENAME) == False + + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS), + FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_entry.display_name), + FlowStep(settings_views.SettingsEntryUpdateSelectionView, button_data_selection=settings_entry.get_selection_option_display_name_by_value(SettingsConstants.OPTION__ENABLED)), + FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=RET_CODE__BACK_BUTTON), + FlowStep(settings_views.SettingsMenuView), + ]) + + # Settings file should now exist + assert os.path.exists(Settings.SETTINGS_FILENAME) == True + + + def test_multiselect(self): + """ Multiselect Settings options should stay in-place; requires BACK to exit. """ + # Which option are we testing? + settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__COORDINATORS) + + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS), + FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_entry.display_name), + FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=0), # select/deselect first option + FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # select/deselect second option + FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # select/deselect second option + FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=RET_CODE__BACK_BUTTON), # BACK to exit + FlowStep(settings_views.SettingsMenuView), + ]) + + + def test_io_test(self): + """ Basic flow from MainMenuView to I/O Test View """ + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS), + FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_views.SettingsMenuView.IO_TEST), + FlowStep(settings_views.IOTestView), + FlowStep(settings_views.SettingsMenuView), + ]) + + + def test_donate(self): + """ Basic flow from MainMenuView to Donate View """ + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS), + FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_views.SettingsMenuView.DONATE), + FlowStep(settings_views.DonateView), + FlowStep(settings_views.SettingsMenuView), + ]) + + + def test_settingsqr(self): + """ + Scanning a SettingsQR should present the success screen and then return to + MainMenuView. + """ + def load_persistent_settingsqr_into_decoder(view: scan_views.ScanView): + settingsqr_data_persistent: str = "settings::v1 name=Total_noob_mode persistent=E coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E" + view.decoder.add_data(settingsqr_data_persistent) + + def load_not_persistent_settingsqr_into_decoder(view: scan_views.ScanView): + settingsqr_data_not_persistent: str = "settings::v1 name=Ephemeral_noob_mode persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E" + view.decoder.add_data(settingsqr_data_not_persistent) + + def _run_test(initial_setting_state: str, load_settingsqr_into_decoder: Callable, expected_setting_state: str): + self.settings.set_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS, initial_setting_state) + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), + FlowStep(scan_views.ScanView, before_run=load_settingsqr_into_decoder), # simulate read message QR; ret val is ignored + FlowStep(settings_views.SettingsIngestSettingsQRView), # ret val is ignored + FlowStep(MainMenuView), + ]) + + assert self.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == expected_setting_state + + + # First load a SettingsQR that enables persistent settings + self.mock_microsd.is_inserted = True + assert MicroSD.get_instance().is_inserted is True + + _run_test( + initial_setting_state=SettingsConstants.OPTION__DISABLED, + load_settingsqr_into_decoder=load_persistent_settingsqr_into_decoder, + expected_setting_state=SettingsConstants.OPTION__ENABLED + ) + + # Then one that disables it + _run_test( + initial_setting_state=SettingsConstants.OPTION__ENABLED, + load_settingsqr_into_decoder=load_not_persistent_settingsqr_into_decoder, + expected_setting_state=SettingsConstants.OPTION__DISABLED + ) + + # Now try to enable persistent settings when the SD card is not inserted + self.mock_microsd.is_inserted = False + assert MicroSD.get_instance().is_inserted is False + + # Have to jump through some hoops to completely simulate the SD card being + # removed; we need Settings to restrict Persistent Settings to only allow + # DISABLED. + with patch('seedsigner.models.settings.Settings.HOSTNAME', new_callable=PropertyMock) as mock_hostname: + # Must identify itself as SeedSigner OS to trigger the SD card removal logic + mock_hostname.return_value = Settings.SEEDSIGNER_OS + Settings.handle_microsd_state_change(MicroSD.ACTION__REMOVED) + + selection_options = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__PERSISTENT_SETTINGS).selection_options + assert len(selection_options) == 1 + assert selection_options[0][0] == SettingsConstants.OPTION__DISABLED + assert self.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED + + _run_test( + initial_setting_state=SettingsConstants.OPTION__DISABLED, + load_settingsqr_into_decoder=load_persistent_settingsqr_into_decoder, + expected_setting_state=SettingsConstants.OPTION__DISABLED + ) diff --git a/tests/test_flows_tools.py b/tests/test_flows_tools.py new file mode 100644 index 00000000..0fb0c4a1 --- /dev/null +++ b/tests/test_flows_tools.py @@ -0,0 +1,160 @@ +# Must import test base before the Controller +from base import FlowTest, FlowStep + +from seedsigner.controller import Controller +from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON +from seedsigner.models.seed import Seed +from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition +from seedsigner.views.view import ErrorView, MainMenuView +from seedsigner.views import scan_views, seed_views, tools_views + + + +class TestToolsFlows(FlowTest): + + def test_addressexplorer_flow(self): + """ + Test the simplest AddressExplorer flow when a seed is already loaded. + """ + controller = Controller.get_instance() + seed = Seed(mnemonic=["abandon "* 11 + "about"]) + controller.storage.set_pending_seed(seed) + controller.storage.finalize_pending_seed() + + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS), + FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.EXPLORER), + FlowStep(tools_views.ToolsAddressExplorerSelectSourceView, screen_return_value=0), # ret 1st onboard seed + FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES).get_selection_option_display_name_by_value(SettingsConstants.NATIVE_SEGWIT)), + FlowStep(tools_views.ToolsAddressExplorerAddressTypeView, button_data_selection=tools_views.ToolsAddressExplorerAddressTypeView.RECEIVE), + FlowStep(tools_views.ToolsAddressExplorerAddressListView, screen_return_value=10), # ret NEXT page of addrs + FlowStep(tools_views.ToolsAddressExplorerAddressListView, screen_return_value=4), # ret a specific addr from the list + FlowStep(tools_views.ToolsAddressExplorerAddressView), # runs until dismissed; no ret value + FlowStep(tools_views.ToolsAddressExplorerAddressListView), + ]) + + + def test_addressexplorer_loadseed_sideflow(self): + """ + Finalizing a seed during the Address Explorer flow should return to the next + Address Explorer step upon completion. + """ + controller = Controller.get_instance() + + def load_seed_into_decoder(view: scan_views.ScanView): + view.decoder.add_data("0000" * 11 + "0003") + + # Finalize the new seed w/out passphrase + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS), + FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.EXPLORER), + FlowStep(tools_views.ToolsAddressExplorerSelectSourceView, button_data_selection=tools_views.ToolsAddressExplorerSelectSourceView.SCAN_SEED), + FlowStep(scan_views.ScanSeedQRView, before_run=load_seed_into_decoder), # simulate read SeedQR + FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), + FlowStep(seed_views.SeedOptionsView, is_redirect=True), + FlowStep(seed_views.SeedExportXpubScriptTypeView), + ]) + + assert controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER + + # Reset + controller.storage.seeds.clear() + controller.storage.set_pending_seed(Seed(mnemonic=["abandon "* 11 + "about"])) + + # Finalize the new seed w/passphrase + self.run_sequence( + sequence=[ + FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE), + FlowStep(seed_views.SeedAddPassphraseView, screen_return_value="mypassphrase"), + FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.DONE), + FlowStep(seed_views.SeedOptionsView, is_redirect=True), + FlowStep(seed_views.SeedExportXpubScriptTypeView), + ] + ) + + + def test_addressexplorer_scan_wrong_qrtype(self): + """ + Scanning the wrong type of QR code when a SeedQR is expected should route to ErrorView + """ + def load_wrong_data_into_decoder(view: scan_views.ScanView): + view.decoder.add_data("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq") + + # Finalize the new seed w/out passphrase + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS), + FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.EXPLORER), + FlowStep(tools_views.ToolsAddressExplorerSelectSourceView, button_data_selection=tools_views.ToolsAddressExplorerSelectSourceView.SCAN_SEED), + FlowStep(scan_views.ScanSeedQRView, before_run=load_wrong_data_into_decoder), # simulate scanning the wrong QR type + FlowStep(ErrorView), + ]) + + + def test_addressexplorer_back_button(self): + """ + Backing out of AddressExplorer behavior depends on current Settings: + * Multiple script types enabled: BACK to SeedExportXpubScriptTypeView + * One script type enabled: BACK to where we started: + * SeedOptions + * ToolsAddressExplorerSelectSourceView if seed was already onboard + * MainMenu if no seed was onboard when we entered via ToolsMenu (loading a + seed during the flow wipes out any history before the load so our only + option is to return to MainMenu). + """ + def load_seed_into_decoder(view: scan_views.ScanView): + view.decoder.add_data("0000" * 11 + "0003") + + controller = Controller.get_instance() + seed = Seed(mnemonic=["abandon "* 11 + "about"]) + controller.storage.set_pending_seed(seed) + controller.storage.finalize_pending_seed() + + # Scenario 1: Seed already onboard, multiple script types enabled, BACK can still + # change script type selection. + self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [SettingsConstants.NATIVE_SEGWIT, SettingsConstants.TAPROOT]) + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS), + FlowStep(seed_views.SeedsMenuView, screen_return_value=0), # select the first onboard seed + FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPLORER), + FlowStep(seed_views.SeedExportXpubScriptTypeView, screen_return_value=0), + FlowStep(tools_views.ToolsAddressExplorerAddressTypeView, screen_return_value=RET_CODE__BACK_BUTTON), + FlowStep(seed_views.SeedExportXpubScriptTypeView), + ]) + + # Scenario 2: Seed already onboard, one script type enabled, started from + # SeedOptionsView, BACK to SeedOptionsView. + self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [SettingsConstants.NATIVE_SEGWIT]) + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS), + FlowStep(seed_views.SeedsMenuView, screen_return_value=0), # select the first onboard seed + FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPLORER), + FlowStep(seed_views.SeedExportXpubScriptTypeView, is_redirect=True), + FlowStep(tools_views.ToolsAddressExplorerAddressTypeView, screen_return_value=RET_CODE__BACK_BUTTON), + FlowStep(seed_views.SeedOptionsView), + ]) + + # Scenario 3: Seed already onboard, one script type enabled, started from + # ToolsMenu, BACK to ToolsAddressExplorerSelectSourceView. + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS), + FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.EXPLORER), + FlowStep(tools_views.ToolsAddressExplorerSelectSourceView, screen_return_value=0), # select the first onboard seed + FlowStep(seed_views.SeedExportXpubScriptTypeView, is_redirect=True), + FlowStep(tools_views.ToolsAddressExplorerAddressTypeView, screen_return_value=RET_CODE__BACK_BUTTON), + FlowStep(tools_views.ToolsAddressExplorerSelectSourceView), + ]) + + # Scenario 4: No seed onboard, one script type enabled, started from Tools, BACK + # can only go to MainMenu because of mid-flow seed load. + controller.discard_seed(0) + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS), + FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.EXPLORER), + FlowStep(tools_views.ToolsAddressExplorerSelectSourceView, button_data_selection=tools_views.ToolsAddressExplorerSelectSourceView.SCAN_SEED), + FlowStep(scan_views.ScanSeedQRView, before_run=load_seed_into_decoder), # simulate read SeedQR + FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE), + FlowStep(seed_views.SeedOptionsView, is_redirect=True), + FlowStep(seed_views.SeedExportXpubScriptTypeView, is_redirect=True), + FlowStep(tools_views.ToolsAddressExplorerAddressTypeView, screen_return_value=RET_CODE__BACK_BUTTON), + FlowStep(MainMenuView), + ]) \ No newline at end of file diff --git a/tests/test_flows_view.py b/tests/test_flows_view.py new file mode 100644 index 00000000..6d032460 --- /dev/null +++ b/tests/test_flows_view.py @@ -0,0 +1,75 @@ +from mock import patch + +# Must import test base before the Controller +from base import FlowTest, FlowStep + +from seedsigner.gui.screens.screen import RET_CODE__POWER_BUTTON +from seedsigner.models.settings import Settings +from seedsigner.views.tools_views import ToolsCalcFinalWordNumWordsView, ToolsMenuView +from seedsigner.views.view import MainMenuView, NotYetImplementedView, PowerOptionsView, PowerOffView, RestartView, UnhandledExceptionView, View + + + +class TestViewFlows(FlowTest): + + def test_restart_flow(self): + """ + Basic flow from MainMenuView to RestartView + """ + with patch('seedsigner.views.view.RestartView.DoResetThread'): + self.run_sequence([ + FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON), + FlowStep(PowerOptionsView, button_data_selection=PowerOptionsView.RESET), + FlowStep(RestartView), + ]) + + + def test_power_off_flow(self): + """ + Basic flow from MainMenuView to PowerOffView + """ + with patch('seedsigner.views.view.PowerOffView.PowerOffThread'): + self.run_sequence([ + FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON), + FlowStep(PowerOptionsView, button_data_selection=PowerOptionsView.POWER_OFF), + FlowStep(PowerOffView), + ]) + + # And again, but this time as if we were in the SeedSigner OS + Settings.HOSTNAME = Settings.SEEDSIGNER_OS + self.run_sequence([ + FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON), + FlowStep(PowerOptionsView, button_data_selection=PowerOptionsView.POWER_OFF), + FlowStep(PowerOffView), # returns BackStackView + FlowStep(PowerOptionsView), + ]) + + + def test_not_yet_implemented_flow(self): + """ + Run an incomplete View that returns None and ensure that we get the NotYetImplementedView + """ + class IncompleteView(View): + def run(self): + self.run_screen(None) + return None + + self.run_sequence([ + FlowStep(IncompleteView), + FlowStep(NotYetImplementedView), + FlowStep(MainMenuView), + ]) + + + def test_unhandled_exception_flow(self): + """ + Basic flow from any arbitrary View to the UnhandledExceptionView + """ + Settings.HOSTNAME = "NOT seedsigner-os" + self.run_sequence([ + FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS), + FlowStep(ToolsMenuView, button_data_selection=ToolsMenuView.KEYBOARD), + FlowStep(ToolsCalcFinalWordNumWordsView, screen_return_value=Exception("Test exception")), # <-- force an exception + FlowStep(UnhandledExceptionView), + FlowStep(MainMenuView), + ]) diff --git a/tests/test_mnemonic_generation.py b/tests/test_mnemonic_generation.py index a0c04ed8..76298d34 100644 --- a/tests/test_mnemonic_generation.py +++ b/tests/test_mnemonic_generation.py @@ -1,3 +1,4 @@ +import pytest import random from embit import bip39 @@ -42,6 +43,35 @@ def test_calculate_checksum(): assert bip39.mnemonic_is_valid(" ".join(mnemonic)) +def test_calculate_checksum_invalid_mnemonics(): + """ + Should raise an Exception on a mnemonic that is invalid due to length or using invalid words. + """ + with pytest.raises(Exception) as e: + # Mnemonic is too short: 10 words instead of 11 + partial_mnemonic = "abandon " * 9 + "about" + mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + assert "12- or 24-word" in str(e) + + with pytest.raises(Exception) as e: + # Valid mnemonic but unsupported length + mnemonic = "devote myth base logic dust horse nut collect buddy element eyebrow visit empty dress jungle" + mnemonic_generation.calculate_checksum(mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + assert "12- or 24-word" in str(e) + + with pytest.raises(Exception) as e: + # Mnemonic is too short: 22 words instead of 23 + partial_mnemonic = "abandon " * 21 + "about" + mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + assert "12- or 24-word" in str(e) + + with pytest.raises(ValueError) as e: + # Invalid BIP-39 word + partial_mnemonic = "foobar " * 11 + "about" + mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + assert "not in the dictionary" in str(e) + + def test_calculate_checksum_with_default_final_word(): """ 11-word and 23-word mnemonics use word `0000` as a temp final word to complete @@ -62,6 +92,22 @@ def test_calculate_checksum_with_default_final_word(): assert mnemonic1 == mnemonic2 +def test_generate_mnemonic_from_bytes(): + """ + Should generate a valid BIP-39 mnemonic from entropy bytes + """ + # From iancoleman.io + entropy = "3350f6ac9eeb07d2c6209932808aa7f6" + expected_mnemonic = "crew marble private differ race truly blush basket crater affair prepare unique".split() + mnemonic = mnemonic_generation.generate_mnemonic_from_bytes(bytes.fromhex(entropy)) + assert mnemonic == expected_mnemonic + + entropy = "5bf41629fce815c3570955e8f45422abd7e2234141bd4d7ec63b741043b98cad" + expected_mnemonic = "fossil pass media what life ticket found click trophy pencil anger fish lawsuit balance agree dash estate wage mom trial aerobic system crawl review".split() + mnemonic = mnemonic_generation.generate_mnemonic_from_bytes(bytes.fromhex(entropy)) + assert mnemonic == expected_mnemonic + + def test_verify_against_coldcard_sample(): """ https://coldcard.com/docs/verifying-dice-roll-math """ diff --git a/tests/test_psbt_parser.py b/tests/test_psbt_parser.py index 7fb84c60..4de00533 100644 --- a/tests/test_psbt_parser.py +++ b/tests/test_psbt_parser.py @@ -1,8 +1,46 @@ -import pytest -from mock import MagicMock -from seedsigner.models import PSBTParser +from seedsigner.models.psbt_parser import PSBTParser +from seedsigner.models.seed import Seed from embit import psbt +from binascii import a2b_base64 +from seedsigner.models.settings_definition import SettingsConstants -def test_sig_count(): - print("test") - \ No newline at end of file +def test_p2tr_change_detection(): + """ Should successfully detect change in a p2tr to p2tr psbt spend + + PSBT Tx and Wallet Details + - Single Sig Wallet P2TR (Taproot) with no passphrase + - Regtest 394aed14 m/86'/1'/0' tpubDCawGrRg7YdHdFb9p4mmD8GBaZjJegL53FPFRrMkGoLcgLATJfksUs2y1Q7dVzixAkgecazsxEsUuyj3LyDw7eVVYHQyojwrc2hfesK4wXW + - 1 Inputs + - 3,190,493,401 sats + - 2 Outputs + - 1 Output spend to another wallet (bcrt1p6p00wazu4nnqac29fvky6vhjnnhku5u2g9njss62rvy7e0yuperq86f5ek) p2tr address + - 1 Output change + - addresss bcrt1prz4g6saush37epdwhvwpu78td3q7yfz3xxz37axlx7udck6wracq3rwq30) + - amount 2,871,443,918 sats + - Change addresses is index 1/1 + - Fee 155 sats + """ + + psbt_base64 = "cHNidP8BAIkCAAAAAf8upuiIWF1VTgC/Q8ZWRrameRigaXpRcQcBe8ye+TK3AQAAAAAXCgAAAs7BJqsAAAAAIlEgGKqNQ7yF4+yFrrscHnjrbEHiJFExhR903ze43FtOH3BwTgQTAAAAACJRINBe93RcrOYO4UVLLE0y8pzvblOKQWcoQ0obCey8nA5GAAAAAE8BBDWHzwNMUx9OgAAAAJdr+WtwWfVa6IPbpKZ4KgRC0clbm11Gl155IPA27n2FAvQCrFGH6Ac2U0Gcy1IH5f5ltgUBDz2+fe8iqL6JzZdgEDlK7RRWAACAAQAAgAAAAIAAAQB9AgAAAAGAKOOUFIzw9pbRDaZ7F0DYhLImrdMn//OSm++ff5VNdAAAAAAAAQAAAAKsjLwAAAAAABYAFKEcuxvXmB3rWHSqSviP5mrKMZoL2RArvgAAAAAiUSBGU0Lg5fx/ECsB1Z4ZUqXQFSLFnlmpm0rm5R2l599h2AAAAAABASvZECu+AAAAACJRIEZTQuDl/H8QKwHVnhlSpdAVIsWeWambSublHaXn32HYAQMEAAAAACEWF7hZVn7pIDR429kAn/WDeQiWjZey1iGHztsL1H83QLMZADlK7RRWAACAAQAAgAAAAIABAAAAAAAAAAEXIBe4WVZ+6SA0eNvZAJ/1g3kIlo2XstYhh87bC9R/N0CzACEHbJdqWyMxF2eOPr6YRXUJmry04HUbgKyeM2IZeG+NI9AZADlK7RRWAACAAQAAgAAAAIABAAAAAQAAAAEFIGyXalsjMRdnjj6+mEV1CZq8tOB1G4CsnjNiGXhvjSPQAAA=" + + raw = a2b_base64(psbt_base64) + tx = psbt.PSBT.parse(raw) + + mnemonic = "goddess rough corn exclude cream trial fee trumpet million prevent gaze power".split() + pw = "" + seed = Seed(mnemonic, passphrase=pw) + + pp = PSBTParser(p=tx, seed=seed, network=SettingsConstants.REGTEST) + + assert pp.change_data == [ + { + 'output_index': 0, + 'address': 'bcrt1prz4g6saush37epdwhvwpu78td3q7yfz3xxz37axlx7udck6wracq3rwq30', + 'amount': 2871443918, + 'fingerprint': ['394aed14'], + 'derivation_path': ['m/86h/1h/0h/1/1']} + ] + assert pp.spend_amount == 319049328 + assert pp.change_amount == 2871443918 + assert pp.destination_addresses == ['bcrt1p6p00wazu4nnqac29fvky6vhjnnhku5u2g9njss62rvy7e0yuperq86f5ek'] + assert pp.destination_amounts == [319049328] \ No newline at end of file diff --git a/tests/test_seed.py b/tests/test_seed.py index cbfa8964..a7e082ee 100644 --- a/tests/test_seed.py +++ b/tests/test_seed.py @@ -1,7 +1,4 @@ -import pytest -from mock import MagicMock -from seedsigner.models import Seed -from embit import bip39 +from seedsigner.models.seed import Seed from seedsigner.models.settings import SettingsConstants diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 00000000..94ac626a --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,106 @@ +import pytest +from base import BaseTest +from seedsigner.models.settings import InvalidSettingsQRData, Settings +from seedsigner.models.settings_definition import SettingsConstants + + + +class TestSettings(BaseTest): + @classmethod + def setup_class(cls): + super().setup_class() + cls.settings = Settings.get_instance() + + + def test_reset_settings(self): + """ BaseTest.reset_settings() should wipe out any previous Settings changes """ + settings = Settings.get_instance() + settings.set_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS, SettingsConstants.OPTION__ENABLED) + assert settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__ENABLED + + BaseTest.reset_settings() + settings = Settings.get_instance() + assert settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED + + + def test_parse_settingsqr_data(self): + """ + SettingsQR parser should successfully parse a valid settingsqr input string and + return the resulting config_name and formatted settings_update_dict. + """ + settings_name = "Test SettingsQR" + settingsqr_data = f"""settings::v1 name={ settings_name.replace(" ", "_") } persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss,ms scripts=nat,nes,tr xpub_details=E passphrase=E camera=180 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E""" + + # First explicitly set settings that differ from the settingsqr_data + self.settings.set_value(SettingsConstants.SETTING__COMPACT_SEEDQR, SettingsConstants.OPTION__DISABLED) + self.settings.set_value(SettingsConstants.SETTING__DIRE_WARNINGS, SettingsConstants.OPTION__DISABLED) + self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [SettingsConstants.COORDINATOR__BLUE_WALLET, SettingsConstants.COORDINATOR__SPARROW]) + + # Now parse the settingsqr_data + config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) + assert config_name == settings_name + self.settings.update(new_settings=settings_update_dict) + + # Now verify that the settings were updated correctly + assert self.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__ENABLED + assert self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__ENABLED + + coordinators = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS) + assert SettingsConstants.COORDINATOR__BLUE_WALLET not in coordinators + assert SettingsConstants.COORDINATOR__SPARROW in coordinators + assert SettingsConstants.COORDINATOR__SPECTER_DESKTOP in coordinators + + + def test_settingsqr_version(self): + """ SettingsQR parser should accept SettingsQR v1 and reject any others """ + settingsqr_data = "settings::v1 name=Foo" + config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) + + # Accepts update with no Exceptions + self.settings.update(new_settings=settings_update_dict) + + settingsqr_data = "settings::v2 name=Foo" + with pytest.raises(InvalidSettingsQRData) as e: + Settings.parse_settingsqr(settingsqr_data) + assert "Unsupported SettingsQR version" in str(e.value) + + # Should also fail if version omitted + settingsqr_data = "settings name=Foo" + with pytest.raises(InvalidSettingsQRData) as e: + Settings.parse_settingsqr(settingsqr_data) + + # And if "settings" is omitted entirely + settingsqr_data = "name=Foo" + with pytest.raises(InvalidSettingsQRData) as e: + Settings.parse_settingsqr(settingsqr_data) + + + def test_settingsqr_ignores_unrecognized_setting(self): + """ SettingsQR parser should ignore unrecognized settings """ + settingsqr_data = "settings::v1 name=Foo favorite_food=bacon xpub_export=D" + config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) + + assert "favorite_food" not in settings_update_dict + assert "xpub_export" in settings_update_dict + + # Accepts update with no Exceptions + self.settings.update(new_settings=settings_update_dict) + + + def test_settingsqr_fails_unrecognized_option(self): + """ SettingsQR parser should fail if a settings has an unrecognized option """ + settingsqr_data = "settings::v1 name=Foo xpub_export=Yep" + with pytest.raises(InvalidSettingsQRData) as e: + Settings.parse_settingsqr(settingsqr_data) + assert "xpub_export" in str(e.value) + + + def test_settingsqr_parses_line_break_separators(self): + """ SettingsQR parser should read line breaks as acceptable separators """ + settingsqr_data = "settings::v1\nname=Foo\nsigs=ss,ms\nscripts=nat,nes,tr\nxpub_export=E\n" + config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data) + + assert len(settings_update_dict.keys()) == 3 + + # Accepts update with no Exceptions + self.settings.update(new_settings=settings_update_dict) diff --git a/tests/test_settingsqr_decoder.py b/tests/test_settingsqr_decoder.py new file mode 100644 index 00000000..bb124c4f --- /dev/null +++ b/tests/test_settingsqr_decoder.py @@ -0,0 +1,29 @@ +from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus + + + +class TestSettingsQRDecoder: + def test_decode_settingsqr(self): + """ + Assume the QR reader decodes the SettingsQR content correctly and begin this test + with parsing the result. + """ + settings_name = "Test SettingsQR" + settings_qr_str = f"""settings::v1 name={ settings_name.replace(" ", "_") } persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss,ms scripts=nat,nes,tr xpub_details=E passphrase=E camera=180 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E""" + + # Now parse the settings_qr_str + decoder = DecodeQR() + status = decoder.add_data(settings_qr_str) + assert decoder.is_settings + assert status == DecodeQRStatus.COMPLETE + + data = decoder.get_settings_data() + assert data == settings_qr_str + + + def test_settingsqr_version(self): + """ Should fail if the "settings" header is missing """ + settings_qr_str = "name=Foo" + decoder = DecodeQR() + status = decoder.add_data(settings_qr_str) + assert status == DecodeQRStatus.INVALID