From 5ec2b79d03b74a174620e743036561a475b19435 Mon Sep 17 00:00:00 2001 From: svatantrya Date: Sat, 3 Oct 2026 13:00:23 -0400 Subject: [PATCH] core+gui: BBQR-only QR transfer, disable Audio when modem plugin unavailable - Drop BALQR / BC-UR v1 / v2 wire formats, keep BBQR only (animated_qr, qrtransfer, dialogs); remove the settings "QR Code Size" selector and persist the export-dialog size choice instead. - Disable the Audio transport radio button in the import/export dialogs when the audio_modem plugin (or its amodem dependency) is unavailable. - Refresh translation catalogs (bal.pot, es_ES, it_IT). --- CHANGELOG.md | 34 ++ COMPATIBILITY.md | 15 +- HANDOFF.md | 61 +- README.md | 25 +- bal/core/animated_qr.py | 896 +--------------------------- bal/core/plugin_base.py | 9 +- bal/core/qrtransfer.py | 236 +------- bal/gui/qt/dialogs.py | 150 ++--- bal/gui/qt/plugin.py | 45 -- bal/locale/bal.pot | 640 +++++++++----------- bal/locale/es_ES/LC_MESSAGES/bal.po | 655 ++++++++++---------- bal/locale/it_IT/LC_MESSAGES/bal.po | 659 ++++++++++---------- tests/test_core_animated_qr.py | 359 +---------- tests/test_core_qr_transfer.py | 362 +---------- tests/test_gui_export_dialogs.py | 10 +- tests/test_gui_qr_transfer.py | 203 +++---- 16 files changed, 1114 insertions(+), 3245 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8992630..6215885 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3515,3 +3515,37 @@ delivery; when it is not, the plugin raises an explicit error (see below). `tests/test_core_plugin_base.py`, `tests/test_core_input_rules.py`. **Outcome:** DONE. + +## 62. Remove BALQR and BC-UR v1/v2 — keep only BBQR + +**Date:** 2026-10-03 + +**Goal:** Drop the BALQR, BC-UR v1 and BC-UR v2 QR wire formats and keep only +**BBQR** for will transfer. + +**What changed:** + +- `bal/core/animated_qr.py`: removed the BC32, bytewords-minimal, CRC-32, + CBOR, xoshiro256/fountain codecs and the BALQR / UR v1 / UR v2 receive + sessions; the module now implements only the BBQR codec plus the + `AnimatedQrSession` facade and `detect_format` / `parse_for_detection` + (BBQR-only). +- `bal/core/qrtransfer.py`: removed the BALQR framing helpers (`split_frames`, + `parse_frame`, `assemble`, `encode_transfer_best`, the frame errors); + kept the transfer-string codec (`encode_transfer` / `decode_transfer`) and + the QR-size presets. +- `bal/gui/qt/dialogs.py`: the export widget no longer shows a Format selector + and always emits BBQR; the import widget treats BBQR indexes as 0-based. +- Removed the "QR Code Size" setting from the plugin settings dialog; the + QR-size selector now lives only in the export dialog, and its chosen value + is remembered (persisted to `QR_CHUNK_SIZE`) for the next export. +- Android reader: `bal/core/*` copies re-synced, `bal/encoder.py` and the + Kotlin bridges (`BalDecoder`, `QrEncoder`, `QrSendActivity`, `MainActivity`, + `strings.xml`) reduced to BBQR; `tools/emitter.py` and + `test_chain/verify_chain.py` updated. +- Tests: `test_core_animated_qr.py`, `test_core_qr_transfer.py` and + `test_gui_qr_transfer.py` rewritten for BBQR only. +- Docs: README, COMPATIBILITY, HANDOFF and android/README updated; translation + catalogs refreshed (removed format strings marked obsolete). + +**Outcome:** DONE. diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index a42079d..f27df88 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -25,22 +25,15 @@ and 4.8.0). ## QR wire-format compatibility -BAL exports/imports wills as QR codes. **BAL QR** (the default) is the plugin's -own frame format and is only understood by BAL itself. The export page also -supports **BC-UR v1**, **BC-UR v2** and **BBQR**: +BAL exports/imports wills as QR codes using **BBQR**: | Format | Wire appearance | Interop target | |-----------|----------------------------|------------------------------------------------------| -| BAL QR | `BAL1…` (v2) / `BALQR1\|total\|index\|…` (legacy import-only) | Past/other BAL versions: **v2 exports are NOT readable by old builds**; old `BALQR1` exports still import here (default, best-of compression, flag `0` = plain, `Z` = deflate) | -| BC-UR v1 | `ur:bytes/` | Blockchain Commons / Coldcard-style UR (BC32, SHA-256 digest, part counts per part) | -| BC-UR v2 | `ur:bytes/-/` | BC-UR 2.x fountain codes (CBOR parts, CRC-32, bytewords-minimal) | | BBQR | `B$…` | Coinkite BitKit / Coldcard's BBQR animated-QR mode | -Import auto-detects the format of each scanned code; out-of-order, duplicate -and (for UR v2) partially-lost fountain frames are handled. Interop is -validation-tested against the reference C++ bc-ur encoder output and the -BCR-2020-004/005 BC32 test vectors; it has not yet been cross-verified against -third-party libraries (`ur`, `bbqr`, Coldcard firmwares). +Import auto-detects the format of each scanned code; out-of-order and duplicate +BBQR frames are handled. Interop has not yet been cross-verified against +third-party libraries (`bbqr`, Coldcard firmwares). ## Reporting compatibility issues diff --git a/HANDOFF.md b/HANDOFF.md index 5544849..3bf3961 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -421,50 +421,29 @@ See Section 5 for details. the audio path — buttons only appear when the `audio_modem` plugin + `amodem` are installed; see the prerequisites below). -### In progress: animated-QR interop (BC-UR v1/v2, BBQR) +### Animated-QR interop: BBQR only -- `bal/core/animated_qr.py` implements stdlib-only codecs for **BC-UR v1** - (BC32 + SHA-256 digest; the bech32_bis checksum variant per - BCR-2020-004/005), **BC-UR v2** (CBOR part structure, bytewords-minimal, - CRC-32, xoshiro256-based fountain with alias-sampled mixing) and **BBQR** +- `bal/core/animated_qr.py` implements a stdlib-only codec for **BBQR** (Coinkite `B$…` base32/hex/zlib frames), plus one shared `AnimatedQrSession` with `detect_format` auto-detection and - `parse_for_detection` frame identity for the GUI debounce. -- Current status as of this session: reference parity, GUI, and tests done; - not yet committed. - - **BC32/bytewords/codec parity:** BC32 reproduces the BCR-2020-004/005 - test vectors (`Hello, world`, `Hello world`, the long seed vector); - bytewords-minimal round-trips with CRC rejection; UR v2 part encode + - decode is byte-exact against the reference C++ bc-ur encoder for a - single part, seq_len=2 (12 frames) and seq_len=7 (3 sampled mixes), - validating CBOR framing, bytewords, alias+ary-threshold sampling, - xoshiro256** and the XOR mix. - - **Sessions:** UR v2 single-part (no seq header), out-of-order frames, - duplicate drops, solve with a missing pure fragment (a second redundant - mixed wave is emitted by `ur2_frames`), UR v1 single-part - (digest-less `ur:bytes/` accepted) and multipart, BBQR full-frame - decode in any order for Z/2/H encodings. - - **Safety:** `_MAX_SESSION_PARTS = 20000`, `_MAX_MESSAGE_BYTES = 32 MB`, - `TransferConflictError` on a frame from a different transfer, - `SessionLimitError`, BBQR zlib-bomb guard, UTF-8 payloads only. - - **GUI:** `BalQrExportWidget` gained a Format selector (BAL QR default, - BC-UR v1, BC-UR v2, BBQR) reusing the QR-size presets; the importer now - routes every frame through `detect_format` + - `AnimatedQrSession.add_part` with the shared - `qr_import_accept_frame(state, fmt, session_key, frame_total, index, - payload, stable_reads=2)` debounce (reset on session-key change). - `_review_and_sign` resolves the session to the transfer text and decodes - parts uniformly across formats. - - **Verification:** `tests/test_core_animated_qr.py` (32 tests incl. the - C++-reference parity vectors and BC32 spec vectors) and the extended - `tests/test_gui_qr_transfer.py` pass; ruff clean on the new/changed - files; pyright 0 errors; smoke test, `build_zip.py` and - `external_zip_test.py` green. Only the pre-existing failures remain - (`test_bt_to_date_*`, fee-exceeds-balance, karen7 pollution). - - **Any remaining work:** manual on-device walkthrough of the QR path with - the new formats; optionally validate against third-party libraries - (`ur`, `bbqr`) once available; add the docstrings/branch notes already - captured in `ag1.md`/`ag2.md` context where needed. + `parse_for_detection` frame identity for the GUI debounce. The previously + supported **BAL QR**, **BC-UR v1** and **BC-UR v2** wire formats were + removed; only BBQR remains. +- **Sessions:** BBQR full-frame decode in any order for Z/2/H encodings, + duplicate drops. +- **Safety:** `_MAX_SESSION_PARTS = 20000`, `_MAX_MESSAGE_BYTES = 32 MB`, + `TransferConflictError` on a frame from a different transfer, + `SessionLimitError`, BBQR zlib-bomb guard, UTF-8 payloads only. +- **GUI:** `BalQrExportWidget` emits BBQR frames using the QR-size presets; + the importer routes every frame through `detect_format` + + `AnimatedQrSession.add_part` with the shared + `qr_import_accept_frame(state, fmt, session_key, frame_total, index, + payload, stable_reads=2)` debounce (reset on session-key change). + `_review_and_sign` resolves the session to the transfer text and decodes + parts uniformly. +- **Verification:** `tests/test_core_animated_qr.py` (BBQR round-trips, + encoding fallback, zlib-bomb guard, detection) and the extended + `tests/test_gui_qr_transfer.py` pass. **Dev-box audio prerequisites (audio_modem channel):** See `AUDIO_MODEM_DEBIAN.md` — the full Debian setup + verification, with the diff --git a/README.md b/README.md index f7c8c58..9868fbb 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,8 @@ bal/ the installable Electrum plugin package │ ├── willexecutors.py │ ├── checkalive.py │ ├── reminders.py -│ ├── qrtransfer.py BAL QR will-transfer wire format / chunk scheduler -│ ├── animated_qr.py BC-UR v1/v2 + BBQR codecs (stdlib-only) +│ ├── qrtransfer.py transfer-string codec / QR size presets +│ ├── animated_qr.py BBQR codec (stdlib-only) │ └── input_rules.py ├── cli/ headless command-line layer (no Qt) │ ├── commands.py bal_* daemon commands (@plugin_command) @@ -94,25 +94,20 @@ exists, then enable it from **Tools → Plugins**. From the will list (**Export → QR Codes**) a will can be exported as a sequence of QR codes and imported on another device (**Import via QR**). The export offers All / Valid / Valid-NC filters plus a QR size preset -(150–1800 bytes/frame) and ships the default **BAL QR** format already -compressed whenever that is smaller (best-of zlib, flag per frame); the import +(150–1800 bytes/frame) and ships the **BBQR** format; the import flow reviews and sign each transaction one at a time, then proposes exporting the signed transactions. When Electrum's `audio_modem` plugin is enabled (optional, requires `amodem` + PortAudio) Send/Receive audio buttons complement the QR channel. See -[`PLAN_QR_TRANSFER.md`](PLAN_QR_TRANSFER.md) for the BAL QR wire-format spec. +[`PLAN_QR_TRANSFER.md`](PLAN_QR_TRANSFER.md) for the transfer wire-format spec. -### Animated-QR formats (interop) +### Animated-QR format (BBQR) -BAL QR is the default export format, but the export page's **Format** selector -also emits **BC-UR v1** (`ur:bytes`, BC32 + SHA-256), **BC-UR v2** -(`ur:bytes`, CBOR fountain codes) and **BBQR** (`B$…`, Coinkite, used by -BitKit) animated-QR sequences. The importer auto-detects the format of each -code it sees, so any of the four formats can be imported on a BAL device, and -a BAL export can be imported by any tool that understands these standards. -UR v2 imports tolerate out-of-order and duplicate frames (fountain decoding); -BBQR frames may arrive in any order. Rotation/redundancy caps and the -32 MB message limit (zlib-bomb guard) bound untrusted scanner input. +The export page emits **BBQR** (`B$…`, Coinkite, used by BitKit and Coldcard) +animated-QR sequences. The importer auto-detects each code it sees, so a BAL +export can be imported by any tool that understands BBQR. BBQR frames may +arrive in any order, and duplicates are ignored. The 32 MB message limit +(zlib-bomb guard) bounds untrusted scanner input. ## Command-line / headless usage diff --git a/bal/core/animated_qr.py b/bal/core/animated_qr.py index 58cfd30..5d9d4b8 100644 --- a/bal/core/animated_qr.py +++ b/bal/core/animated_qr.py @@ -2,33 +2,18 @@ bal.core.animated_qr ==================== -GUI-free implementation of the interoperable animated-QR transfer formats -used to move BAL will data between devices. +GUI-free implementation of the BBQR animated-QR transfer format used to +move BAL will data between devices. -Supported wire formats (each self-describing and order-independent on -receive): +Supported wire format (self-describing and order-independent on receive): -* **BALQR** (native): ``BAL1`` compact fixed-width - header (11 chars, 3-digit base36 count fields, no separators); legacy - ``BALQR1|total|index|flags|payload`` still imported. -* **BC-UR v1** (BCR-2020-005 rev1 draft, May 2020):: - ur:bytes/1of7// - Fragments partition the BC32 rendering of the CBOR byte string; the - SHA-256 digest of the wrapped payload ties the parts together. -* **BC-UR v2** (BCR-2020-005 rev 2 / BCR-2020-012):: - ur:bytes/2-9/ - Fountain-coded parts; each part is a CBOR array - ``[seq_num, seq_len, message_len, checksum, data]`` whose CBOR bytes are - bytewords-minimal encoded with a trailing per-part CRC-32. The - ``checksum`` field holds the CRC-32 of the whole wrapped message, so the - parts are mixable and order-independent. * **BBQR** (Coinkite):: B$<2 base36 total><2 base36 index> Equal-length text frames; the payload is uppercase hex, RFC-4648 base32, or raw-deflate (``wbits=-10``) zlib plus base32. Everything is implemented from scratch on top of the Python standard library -only (``zlib``, ``hashlib``, ``base64``), so the shipped plugin zip stays a +only (``zlib``, ``base64``), so the shipped plugin zip stays a self-contained bundle with no third-party dependencies (house rule). This module never imports Qt or any Electrum GUI code (house rule). @@ -37,9 +22,8 @@ This module never imports Qt or any Electrum GUI code (house rule). from __future__ import annotations import base64 -import hashlib import zlib -from typing import Dict, FrozenSet, List, Optional, Sequence, Set, Tuple +from typing import Dict, List, Optional, Sequence, Tuple # --------------------------------------------------------------------------- # # Errors & safety caps @@ -62,604 +46,10 @@ class SessionLimitError(AnimatedQrError): """A receive session exceeded its safety caps.""" -class ChecksumError(AnimatedQrError): - """A part failed its checksum / digest validation.""" - - # Safety caps for untrusted scanner input. _MAX_SESSION_PARTS = 20000 _MAX_MESSAGE_BYTES = 32 * 1024 * 1024 -# --------------------------------------------------------------------------- # -# CBOR minimals (byte-string envelope + the fountain part header) -# --------------------------------------------------------------------------- # - -_BYTE_STR_RES = 0x40 # byte string, length < 24 -_BYTE_STR_1 = 0x58 # byte string, 1-byte length -_BYTE_STR_2 = 0x59 # byte string, 2-byte length -_BYTE_STR_4 = 0x60 # byte string, 4-byte length -_ARRAY_RES = 0x80 -_UNSIGNED_RES = 0x00 - - -def cbor_byte_string(data: bytes) -> bytes: - """Wrap ``data`` in the minimal CBOR byte-string envelope (0x40..0x60).""" - n = len(data) - if n < 24: - head = bytes([_BYTE_STR_RES + n]) - elif n <= 0xFF: - head = bytes([_BYTE_STR_1, n]) - elif n <= 0xFFFF: - head = bytes([_BYTE_STR_2]) + n.to_bytes(2, "big") - elif n <= 0xFFFFFFFF: - head = bytes([_BYTE_STR_4]) + n.to_bytes(4, "big") - else: - raise AnimatedQrError("payload too large for the UR byte-string envelope") - return head + data - - -def unwrap_ur_cbor(message: bytes) -> bytes: - """Strip the CBOR byte-string envelope, falling back to the raw bytes. - - Receivers keep working even when the emitter embedded the payload without - any CBOR wrapping (some third-party ``ur:bytes`` emitters do). - """ - if not message: - raise AnimatedQrError("empty decoded message") - b0 = message[0] - if _BYTE_STR_RES <= b0 <= 0x57: - header_len, n = 1, b0 - _BYTE_STR_RES - elif b0 == _BYTE_STR_1 and len(message) >= 2: - header_len, n = 2, message[1] - elif b0 == _BYTE_STR_2 and len(message) >= 3: - header_len, n = 3, int.from_bytes(message[1:3], "big") - elif b0 == _BYTE_STR_4 and len(message) >= 5: - header_len, n = 5, int.from_bytes(message[1:5], "big") - else: - return message - if header_len + n != len(message): - raise AnimatedQrError("decoded message has an inconsistent CBOR length") - return message[header_len:] - - -def _cbor_unsigned(value: int) -> bytes: - if value < 24: - return bytes([_UNSIGNED_RES + value]) - if value <= 0xFF: - return bytes([0x18, value]) - if value <= 0xFFFF: - return bytes([0x19]) + value.to_bytes(2, "big") - if value <= 0xFFFFFFFF: - return bytes([0x1A]) + value.to_bytes(4, "big") - return bytes([0x1B]) + value.to_bytes(8, "big") - - -def cbor_part(seq_num: int, seq_len: int, message_len: int, checksum: int, data: bytes) -> bytes: - """The CBOR body of a BC-UR v2 fountain part (``[seq, seq_len, message_len, checksum, data]``).""" - out = bytearray([_ARRAY_RES + 5]) - out += _cbor_unsigned(seq_num) - out += _cbor_unsigned(seq_len) - out += _cbor_unsigned(message_len) - out += _cbor_unsigned(checksum) - out += cbor_byte_string(data) - return bytes(out) - - -def _need(buf: bytes, pos: int, count: int) -> None: - if pos + count > len(buf): - raise AnimatedQrError("truncated CBOR part header") - - -def _cbor_read_unsigned(buf: bytes, pos: int) -> Tuple[int, int]: - if pos >= len(buf): - raise AnimatedQrError("truncated CBOR part header") - octet = buf[pos] - if octet & 0xE0 != _UNSIGNED_RES: - raise AnimatedQrError("unexpected CBOR type in part header") - pos += 1 - additional = octet & 0x1F - if additional < 24: - return additional, pos - if additional == 24: - _need(buf, pos, 1) - return buf[pos], pos + 1 - if additional == 25: - _need(buf, pos, 2) - return int.from_bytes(buf[pos : pos + 2], "big"), pos + 2 - if additional == 26: - _need(buf, pos, 4) - return int.from_bytes(buf[pos : pos + 4], "big"), pos + 4 - if additional == 27: - _need(buf, pos, 8) - return int.from_bytes(buf[pos : pos + 8], "big"), pos + 8 - raise AnimatedQrError("unsupported CBOR integer width in part header") - - -def _cbor_read_bytes(buf: bytes, pos: int) -> Tuple[bytes, int]: - if pos >= len(buf): - raise AnimatedQrError("truncated CBOR part header") - octet = buf[pos] - pos += 1 - if octet & 0xE0 != _BYTE_STR_RES: - raise AnimatedQrError("expected a CBOR byte string in part header") - additional = octet & 0x1F - if additional < 24: - n = additional - elif additional == 24: - _need(buf, pos, 1) - n, pos = buf[pos], pos + 1 - elif additional == 25: - _need(buf, pos, 2) - n, pos = int.from_bytes(buf[pos : pos + 2], "big"), pos + 2 - elif additional == 26: - _need(buf, pos, 4) - n, pos = int.from_bytes(buf[pos : pos + 4], "big"), pos + 4 - else: - raise AnimatedQrError("unsupported CBOR byte-string width in part header") - _need(buf, pos, n) - return buf[pos : pos + n], pos + n - - -def _cbor_read_array(buf: bytes, pos: int) -> Tuple[int, int]: - if pos >= len(buf): - raise AnimatedQrError("truncated CBOR part header") - octet = buf[pos] - pos += 1 - if octet & 0xE0 != _ARRAY_RES: - raise AnimatedQrError("expected a CBOR array in part header") - additional = octet & 0x1F - if additional < 24: - return additional, pos - if additional == 24: - _need(buf, pos, 1) - return buf[pos], pos + 1 - if additional == 25: - _need(buf, pos, 2) - return int.from_bytes(buf[pos : pos + 2], "big"), pos + 2 - raise AnimatedQrError("unsupported CBOR array header in part") - - -# --------------------------------------------------------------------------- # -# CRC-32 (same polynomial as ``zlib.crc32``, network byte order) -# --------------------------------------------------------------------------- # - - -def crc32_int(data: bytes) -> int: - """CRC-32 over ``data`` as an unsigned 32-bit integer.""" - return zlib.crc32(data) & 0xFFFFFFFF - - -def crc32_bytes(data: bytes) -> bytes: - """CRC-32 over ``data`` as 4 network-order (big-endian) bytes.""" - return crc32_int(data).to_bytes(4, "big") - - -# --------------------------------------------------------------------------- # -# Bytewords (BCR-2020-012) -# --------------------------------------------------------------------------- # - -_BYTEWORDS = ( - "ableacidalsoapexaquaarchatomauntawayaxisbackbaldbarnbeltbetabiasbluebodybragbr" - "ewbulbbuzzcalmcashcatschefcityclawcodecolacookcostcruxcurlcuspcyandarkdatadays" - "delidicedietdoordowndrawdropdrumdulldutyeacheasyechoedgeepicevenexamexiteyesfa" - "ctfairfernfigsfilmfishfizzflapflewfluxfoxyfreefrogfuelfundgalagamegeargemsgift" - "girlglowgoodgraygrimgurugushgyrohalfhanghardhawkheathelphighhillholyhopehornhu" - "tsicedideaidleinchinkyintoirisironitemjadejazzjoinjoltjowljudojugsjumpjunkjury" - "keepkenokeptkeyskickkilnkingkitekiwiknoblamblavalazyleaflegsliarlimplionlistlo" - "goloudloveluaulucklungmainmanymathmazememomenumeowmildmintmissmonknailnavyneed" - "newsnextnoonnotenumbobeyoboeomitonyxopenovalowlspaidpartpeckplaypluspoempoolpo" - "sepuffpumapurrquadquizraceramprealredorichroadrockroofrubyruinrunsrustsafesaga" - "scarsetssilkskewslotsoapsolosongstubsurfswantacotasktaxitenttiedtimetinytoilto" - "mbtoystriptunatwinuglyundouniturgeuservastveryvetovialvibeviewvisavoidvowswall" - "wandwarmwaspwavewaxywebswhatwhenwhizwolfworkyankyawnyellyogayurtzapszerozestzi" - "nczonezoom" -) - -_WORDS = [_BYTEWORDS[i : i + 4] for i in range(0, 1024, 4)] -_DIM = 26 -_WORD_LOOKUP: Optional[List[int]] = None - - -def _word_lookup() -> List[int]: - """First/last-letter lookup table (built lazily, mirrors Bytewords).""" - global _WORD_LOOKUP - if _WORD_LOOKUP is None: - table = [-1] * (_DIM * _DIM) - for i, word in enumerate(_WORDS): - x = ord(word[0]) - ord("a") - y = ord(word[3]) - ord("a") - table[y * _DIM + x] = i - _WORD_LOOKUP = table - return _WORD_LOOKUP - - -def _decode_word(word: str, word_len: int) -> int: - if len(word) != word_len: - raise AnimatedQrError("invalid bytewords word length") - x = ord(word[0]) - ord("a") - y = ord(word[3] if word_len == 4 else word[1]) - ord("a") - if not (0 <= x < _DIM and 0 <= y < _DIM): - raise AnimatedQrError("invalid bytewords characters") - value = _word_lookup()[y * _DIM + x] - if value == -1: - raise AnimatedQrError("invalid bytewords first/last pair") - if word_len == 4: - full = _WORDS[value] - if word[1] != full[1] or word[2] != full[2]: - raise AnimatedQrError("invalid bytewords middle letters") - return value - - -def bytewords_minimal_encode(data: bytes) -> str: - """BCR-2020-012 bytewords-minimal: one two-letter word per byte, then CRC.""" - crc = data + crc32_bytes(data) - return "".join(_WORDS[b][0] + _WORDS[b][3] for b in crc) - - -def bytewords_minimal_decode(text: str) -> bytes: - """Inverse of :func:`bytewords_minimal_encode` (validates the CRC-32).""" - if len(text) % 2: - raise AnimatedQrError("invalid bytewords length (odd)") - values = [_decode_word(text[i : i + 2], 2) for i in range(0, len(text), 2)] - payload = bytes(values) - if len(payload) < 5: - raise AnimatedQrError("bytewords payload too short") - body, checksum = payload[:-4], payload[-4:] - if crc32_bytes(body) != checksum: - raise AnimatedQrError("bytewords CRC-32 mismatch") - return body - - -# --------------------------------------------------------------------------- # -# BC32 (the deprecated bech32-derived codec used by BC-UR v1) -# --------------------------------------------------------------------------- # - -_BC32_ALPHABET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" -_BC32_REV = {ch: i for i, ch in enumerate(_BC32_ALPHABET)} -_BECH32_GENERATOR = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3] - - -def bech32_polymod(values: Sequence[int]) -> int: - chk = 1 - for value in values: - top = chk >> 25 - chk = (chk & 0x1FFFFFF) << 5 ^ value - for i in range(5): - if (top >> i) & 1: - chk ^= _BECH32_GENERATOR[i] - return chk - - -def _bc32_checksum(values: List[int]) -> List[int]: - polymod = bech32_polymod([0] + values + [0] * 6) ^ 0x3FFFFFFF - return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] - - -def _bc32_verify(values: List[int]) -> bool: - return bech32_polymod([0] + values) == 0x3FFFFFFF - - -def bc32_encode(data: bytes) -> str: - """BCR-2020-005 BC32: bech32 without the human-readable part and divider.""" - acc = 0 - bits = 0 - values: List[int] = [] - for byte in data: - acc = (acc << 8) | byte - bits += 8 - while bits >= 5: - bits -= 5 - values.append((acc >> bits) & 31) - if bits: - values.append((acc << (5 - bits)) & 31) - values += _bc32_checksum(values) - return "".join(_BC32_ALPHABET[v] for v in values) - - -def bc32_decode(text: str) -> bytes: - """Inverse of :func:`bc32_encode` (validates the 6-char checksum).""" - lowered = text.lower() - try: - values = [_BC32_REV[ch] for ch in lowered] - except KeyError: - raise AnimatedQrError("invalid BC-UR v1 character") from None - if len(values) < 6 or not _bc32_verify(values): - raise AnimatedQrError("invalid BC-UR v1 checksum") - data = values[:-6] - acc = 0 - bits = 0 - out = bytearray() - for value in data: - acc = (acc << 5) | value - bits += 5 - if bits >= 8: - bits -= 8 - out.append((acc >> bits) & 0xFF) - return bytes(out) - - -# --------------------------------------------------------------------------- # -# xoshiro256** + alias sampler (exact ports of the reference RNG chain) -# --------------------------------------------------------------------------- # - -_MASK64 = (1 << 64) - 1 - - -class _Xoshiro256: - """xoshiro256** 1.0, seeded via SHA-256 of a byte sequence.""" - - def __init__(self, seed: bytes): - digest = hashlib.sha256(seed).digest() - self._s = [ - int.from_bytes(digest[offset : offset + 8], "big") - for offset in range(0, 32, 8) - ] - - @staticmethod - def _rotl(x: int, k: int) -> int: - return ((x << k) | (x >> (64 - k))) & _MASK64 - - def next(self) -> int: - result = (self._rotl((self._s[1] * 5) & _MASK64, 7) * 9) & _MASK64 - t = (self._s[1] << 17) & _MASK64 - s = self._s - s[2] ^= s[0] - s[3] ^= s[1] - s[1] ^= s[2] - s[0] ^= s[3] - s[2] ^= t - s[3] = self._rotl(s[3], 45) - return result - - def next_double(self) -> float: - return self.next() / float(1 << 64) - - def next_int(self, low: int, high: int) -> int: - return int(self.next_double() * (high - low + 1)) + low - - -class _RandomAliasSampler: - """Vose's alias method, built in the exact order of the reference code.""" - - def __init__(self, probs: Sequence[float]): - total = sum(probs) - assert total > 0 - n = len(probs) - normalized = [p * float(n) / total for p in probs] - - small: List[int] = [] - large: List[int] = [] - for i in range(n - 1, -1, -1): - (small if normalized[i] < 1 else large).append(i) - - self._probs = [0] * n - self._aliases = [0] * n - while small and large: - a = small.pop() - g = large.pop() - self._probs[a] = normalized[a] - self._aliases[a] = g - normalized[g] += normalized[a] - 1 - (small if normalized[g] < 1 else large).append(g) - - while large: - self._probs[large.pop()] = 1 - while small: - self._probs[small.pop()] = 1 - - def next(self, rng: _Xoshiro256) -> int: - r1 = rng.next_double() - r2 = rng.next_double() - n = len(self._probs) - i = int(float(n) * r1) - return i if r2 < self._probs[i] else self._aliases[i] - - -def choose_fragments(seq_num: int, seq_len: int, checksum: int) -> Set[int]: - """The fragments mixed into a BC-UR v2 fountain part (reference seed math). - - Sequence numbers ``1..seq_len`` emit the pure fragment ``{seq_num - 1}``; - every larger sequence number deterministically mixes a pseudo-random - subset of fragments seeded by ``SHA256(seq ‖ checksum)``. - """ - if seq_num <= seq_len: - return {seq_num - 1} - seed = seq_num.to_bytes(4, "big") + checksum.to_bytes(4, "big") - rng = _Xoshiro256(seed) - probs: List[float] = [1.0 / i for i in range(1, seq_len + 1)] - degree = _RandomAliasSampler(probs).next(rng) + 1 - remaining = list(range(seq_len)) - shuffled: List[int] = [] - while remaining: - index = rng.next_int(0, len(remaining) - 1) - shuffled.append(remaining.pop(index)) - return set(shuffled[:degree]) - - -def _partition_message(message: bytes, fragment_len: int) -> List[bytes]: - fragments: List[bytes] = [] - for offset in range(0, len(message), fragment_len): - fragment = message[offset : offset + fragment_len] - if len(fragment) < fragment_len: - fragment += b"\x00" * (fragment_len - len(fragment)) - fragments.append(fragment) - return fragments - - -def _mix_fragments(fragments: Sequence[bytes], indexes: Set[int], fragment_len: int) -> bytes: - result = bytearray(fragment_len) - for index in indexes: - for i, byte in enumerate(fragments[index]): - result[i] ^= byte - return bytes(result) - - -# --------------------------------------------------------------------------- # -# BC-UR v2 (bytewords-minimal + fountain) -# --------------------------------------------------------------------------- # - - -def _ur2_header(seq_num: int, seq_len: int) -> str: - return "ur:bytes/{}-{}/".format(seq_num, seq_len) - - -def _ur2_part_string(seq_num: int, seq_len: int, message_len: int, checksum: int, data: bytes) -> str: - body = cbor_part(seq_num, seq_len, message_len, checksum, data) - return _ur2_header(seq_num, seq_len) + bytewords_minimal_encode(body) - - -def _ur2_part_cost(seq_num: int, seq_len: int, message_len: int, checksum: int, data_len: int) -> int: - body_len = len(cbor_part(seq_num, seq_len, message_len, checksum, b"\x00" * data_len)) - # bytewords_minimal_encode appends a 4-byte CRC over the body. - return len(_ur2_header(seq_num, seq_len)) + 2 * (body_len + 4) - - -def ur2_frames(payload: bytes, budget_chars: int) -> List[str]: - """Encode ``payload`` into BC-UR v2 fountain frames. - - ``budget_chars`` is the largest frame string the carrying QR code may - hold. The first ``seq_len`` frames are pure (one fragment each); a second - wave of ``seq_len`` mixed (fountain) frames follows so the receiver can - recover with a few parts still missing. - """ - message = cbor_byte_string(payload) - message_len = len(message) - checksum = crc32_int(message) - single_cost = len("ur:bytes/") + len(bytewords_minimal_encode(message)) - if single_cost <= budget_chars: - return ["ur:bytes/" + bytewords_minimal_encode(message)] - - fragment_len = message_len - fragment_count = 1 - while True: - seq_len = fragment_count - worst_seq = 2 * seq_len # the export loop emits up to 2*seq_len parts - cost = _ur2_part_cost(worst_seq, seq_len, message_len, checksum, fragment_len) - if cost <= budget_chars: - break - fragment_count += 1 - fragment_len = -(-message_len // fragment_count) - if fragment_count > message_len: - raise AnimatedQrError("QR budget too small for a BC-UR v2 part") - - fragments = _partition_message(message, fragment_len) - frames: List[str] = [] - for seq_num in range(1, seq_len + 1): - frames.append(_ur2_part_string(seq_num, seq_len, message_len, checksum, fragments[seq_num - 1])) - for seq_num in range(seq_len + 1, 2 * seq_len + 1): - data = _mix_fragments(fragments, choose_fragments(seq_num, seq_len, checksum), fragment_len) - frames.append(_ur2_part_string(seq_num, seq_len, message_len, checksum, data)) - return frames - - -def ur2_parse_part(frame_text: str) -> Tuple[int, int, int, int, bytes]: - """Parse a BC-UR v2 part into ``(seq, seq_len, message_len, checksum, data)``.""" - frame_text = frame_text.strip().lower() - prefix = "ur:bytes/" - if not frame_text.startswith(prefix): - raise AnimatedQrError("not a BC-UR v2 part") - tail = frame_text[len(prefix) :] - if "/" not in tail: - body = bytewords_minimal_decode(tail) - return 1, 1, len(body), crc32_int(body), body - seq_head, words = tail.split("/", 1) - try: - seq_num_s, seq_len_s = seq_head.split("-", 1) - seq_num, seq_len = int(seq_num_s), int(seq_len_s) - except ValueError: - raise AnimatedQrError("bad BC-UR v2 sequence header") from None - if seq_len < 1 or not 1 <= seq_num <= 2**32 - 1: - raise AnimatedQrError("bad BC-UR v2 sequence numbers") - body = bytewords_minimal_decode(words) - arr, pos = _cbor_read_array(body, 0) - if arr != 5: - raise AnimatedQrError("bad BC-UR v2 part header arity") - seq_again, pos = _cbor_read_unsigned(body, pos) - seq_len_again, pos = _cbor_read_unsigned(body, pos) - message_len, pos = _cbor_read_unsigned(body, pos) - checksum, pos = _cbor_read_unsigned(body, pos) - data, pos = _cbor_read_bytes(body, pos) - if pos != len(body): - raise AnimatedQrError("trailing garbage in BC-UR v2 part header") - if seq_again != seq_num or seq_len_again != seq_len: - raise AnimatedQrError("BC-UR v2 part header mismatch") - return seq_num, seq_len, message_len, checksum, bytes(data) - - -# --------------------------------------------------------------------------- # -# BC-UR v1 (BCR-2020-005 rev1: BC32 fragments + SHA-256 digest) -# --------------------------------------------------------------------------- # - - -def _ur1_digest(message: bytes) -> str: - return bc32_encode(hashlib.sha256(message).digest()) - - -def _ur1_prefix(index: int, total: int, digest: str) -> str: - return "ur:bytes/{}{}/{}/".format( - index, "of{}".format(total), digest - ) - - -def ur1_frames(payload: bytes, budget_chars: int) -> List[str]: - """Encode ``payload`` into BC-UR v1 fragments (``NofM`` + BC32 + digest).""" - message = cbor_byte_string(payload) - digest = _ur1_digest(message) - full = bc32_encode(message) - - total = 1 - while True: - longest = _ur1_prefix(total, total, digest) - capacity = budget_chars - len(longest) - if capacity < 1: - raise AnimatedQrError("QR budget too small for BC-UR v1") - if len(full) <= capacity * total: - break - total += 1 - if total > _MAX_SESSION_PARTS: - raise AnimatedQrError("BC-UR v1 transfer demands too many parts") - - frames: List[str] = [] - pos = 0 - for index in range(1, total + 1): - prefix = _ur1_prefix(index, total, digest) - capacity = budget_chars - len(prefix) - frames.append(prefix + full[pos : pos + capacity]) - pos += capacity - return frames - - -def ur1_parse_part(frame_text: str) -> Tuple[int, int, str, str]: - """Parse a BC-UR v1 part into ``(index, total, digest, fragment)``. - - Accepts both the multipart form (``ur:bytes/NofM//``) and - the single-part form (``ur:bytes/``, no sequence header or digest). - """ - frame_text = frame_text.strip().lower() - prefix = "ur:bytes/" - if not frame_text.startswith(prefix): - raise AnimatedQrError("not a BC-UR v1 part") - tail = frame_text[len(prefix) :] - parts = tail.split("/") - if len(parts) == 1: - return 1, 1, "", parts[0] - if len(parts) != 3: - raise AnimatedQrError("bad BC-UR v1 part structure") - seq_head, digest, fragment = parts - if "of" not in seq_head: - raise AnimatedQrError("BC-UR v1 part misses the sequence header") - try: - index_s, total_s = seq_head.split("of", 1) - index, total = int(index_s), int(total_s) - except ValueError: - raise AnimatedQrError("bad BC-UR v1 sequence header") from None - if total < 1 or not 1 <= index <= total: - raise AnimatedQrError("bad BC-UR v1 sequence numbers") - if len(digest) != 58: - raise AnimatedQrError("bad BC-UR v1 digest") - return index, total, digest, fragment - - # --------------------------------------------------------------------------- # # BBQR (Coinkite) # --------------------------------------------------------------------------- # @@ -774,9 +164,6 @@ def _bbqr_decode(encoded_parts: Sequence[str], encoding: str) -> bytes: # --------------------------------------------------------------------------- # FORMAT_LABELS = { - "balqr": "BAL QR", - "ur1": "BC-UR v1", - "ur2": "BC-UR v2", "bbqr": "BBQR", } @@ -791,34 +178,8 @@ def detect_format(text: str) -> Optional[str]: text = text.strip() if not text: return None - lowered = text.lower() - if lowered.startswith(("balqr", "bal1")): - return "balqr" if text.startswith(_BBQR_PREFIX): return "bbqr" - if not lowered.startswith("ur:"): - return None - if lowered.startswith("ur:bytes/"): - remainder = lowered[len("ur:bytes/") :] - first = remainder.split("/", 1)[0] - if "of" in first: - return "ur1" - if "-" in first: - return "ur2" - # Single-part: the whole remainder is the body. Prefer a bytewords v2 - # body (CBOR byte-string head 0x40..0x60), then BC32 v1. - try: - body = bytewords_minimal_decode(remainder) - except AnimatedQrError: - pass - else: - if body and 0x40 <= body[0] <= 0x60: - return "ur2" - try: - bc32_decode(remainder) - except AnimatedQrError: - return None - return "ur1" return None @@ -830,253 +191,16 @@ def parse_for_detection(text: str) -> Tuple[str, str, int, int]: :class:`AnimatedQrError` when the text cannot be parsed. """ fmt = detect_format(text) - if fmt == "balqr": - total, index, _compressed, _payload = _parse_balqr(text) - return "balqr", "balqr:{}".format(total), total, index - if fmt == "ur1": - index, total, digest, _frag = ur1_parse_part(text) - return "ur1", "ur1:{}".format(digest), total, index - if fmt == "ur2": - seq, seq_len, message_len, checksum, _data = ur2_parse_part(text) - return "ur2", "ur2:{}-{}-{}".format(seq_len, message_len, checksum), seq_len, seq if fmt == "bbqr": encoding, type_code, total, index, _payload = bbqr_parse_part(text) return "bbqr", "bbqr:{}{}:{}".format(encoding, type_code, total), total, index raise FormatNotDetectedError("Not a supported QR transfer format") -def _parse_balqr(text: str) -> Tuple[int, int, bool, str]: - from bal.core.qrtransfer import parse_frame - - return parse_frame(text) - - # --------------------------------------------------------------------------- # -# Receive sessions (order-independent assembly per format) +# Receive session (order-independent assembly) # --------------------------------------------------------------------------- # -class _BalQrSession: - def __init__(self): - self._frames: Dict[int, str] = {} - self._total = 0 - self._compressed = False - - @property - def total(self) -> int: - return self._total - - @property - def received(self) -> int: - return len(self._frames) - - @property - def done(self) -> bool: - return bool(self._total) and len(self._frames) >= self._total - - def add(self, text: str) -> str: - total, index, compressed, payload = _parse_balqr(text) - if self._total and total != self._total: - raise TransferConflictError("BAL QR transfer total changed") - if len(self._frames) >= _MAX_SESSION_PARTS: - raise SessionLimitError("too many BAL QR frames") - if not self._total: - self._total = total - self._compressed = compressed - if index in self._frames: - return "dup" - self._frames[index] = payload - return "ok" - - def resolve(self) -> Tuple[str, bool]: - from bal.core.qrtransfer import assemble - - text = assemble(self._frames, self._total) - return text, self._compressed - - -class _Ur1Session: - def __init__(self): - self._total = 0 - self._digest = "" - self._fragments: Dict[int, str] = {} - - @property - def total(self) -> int: - return self._total - - @property - def received(self) -> int: - return len(self._fragments) - - @property - def done(self) -> bool: - return bool(self._total) and len(self._fragments) >= self._total - - def add(self, text: str) -> str: - index, total, digest, fragment = ur1_parse_part(text) - if self._total: - if total != self._total or digest != self._digest: - raise TransferConflictError("BC-UR v1 transfer digest changed") - else: - self._total = total - self._digest = digest - if total > _MAX_SESSION_PARTS: - raise SessionLimitError("BC-UR v1 demands too many parts") - if index in self._fragments: - return "dup" - self._fragments[index] = fragment - return "ok" - - def resolve(self) -> Tuple[str, bool]: - full = "".join(self._fragments[i] for i in range(1, self._total + 1)) - try: - message = bc32_decode(full) - except AnimatedQrError: - raise ChecksumError("BC-UR v1 checksum mismatch") from None - if self._digest and _ur1_digest(message) != self._digest: - raise ChecksumError("BC-UR v1 digest mismatch") - return _transfer_text(unwrap_ur_cbor(message)), False - - -class _Ur2Session: - """Fountain decoder mirroring the reference (C++/python) semantics.""" - - def __init__(self): - self._seq_len = 0 - self._message_len = 0 - self._checksum = 0 - self._fragment_len = 0 - self._received: Set[int] = set() - self._simple: Dict[FrozenSet[int], bytes] = {} - self._mixed: Dict[FrozenSet[int], bytes] = {} - self._queue: List[Tuple[FrozenSet[int], bytes]] = [] - self._processed = 0 - self._result: Optional[bytes] = None - self._bad = False - - @property - def total(self) -> int: - return self._seq_len - - @property - def received(self) -> int: - return self._processed - - @property - def done(self) -> bool: - return self._result is not None - - def add(self, text: str) -> str: - seq, seq_len, message_len, checksum, data = ur2_parse_part(text) - if self._seq_len: - if not self._validate(seq_len, message_len, checksum, len(data)): - raise TransferConflictError("BC-UR v2 transfer header changed") - else: - self._seq_len = seq_len - self._message_len = message_len - self._checksum = checksum - self._fragment_len = len(data) - if seq_len > _MAX_SESSION_PARTS or message_len > _MAX_MESSAGE_BYTES: - raise SessionLimitError("BC-UR v2 session exceeds safety caps") - indexes = frozenset(choose_fragments(seq, self._seq_len, self._checksum)) - self._receive(indexes, bytes(data)) - return "ok" - - def _validate(self, seq_len: int, message_len: int, checksum: int, data_len: int) -> bool: - return ( - seq_len == self._seq_len - and message_len == self._message_len - and checksum == self._checksum - and data_len == self._fragment_len - ) - - def _receive(self, indexes: FrozenSet[int], data: bytes) -> None: - if self._result is not None or self._bad: - return - self._queue.append((indexes, data)) - while self._result is None and not self._bad and self._queue: - self._process(self._queue.pop(0)) - self._processed += 1 - - def _process(self, item: Tuple[FrozenSet[int], bytes]) -> None: - indexes, data = item - if len(indexes) == 1: - self._process_simple(indexes, data) - else: - self._process_mixed(indexes, data) - - def _process_simple(self, indexes: FrozenSet[int], data: bytes) -> None: - fragment_index = next(iter(indexes)) - if fragment_index in self._received: - return - self._simple[indexes] = data - self._received.add(fragment_index) - if self._received == set(range(self._seq_len)): - self._finish() - return - self._reduce_mixed_by(indexes, data) - - def _reduce_mixed_by(self, indexes: FrozenSet[int], data: bytes) -> None: - new_mixed: Dict[FrozenSet[int], bytes] = {} - for other_indexes, other_data in self._mixed.items(): - reduced = self._reduce_part(other_indexes, other_data, indexes, data) - if len(reduced[0]) == 1: - self._queue.append(reduced) - else: - new_mixed[reduced[0]] = reduced[1] - self._mixed = new_mixed - - def _process_mixed(self, indexes: FrozenSet[int], data: bytes) -> None: - if indexes in self._mixed: - return - reduced_indexes, reduced_data = indexes, data - for simple_indexes, simple_data in self._simple.items(): - reduced_indexes, reduced_data = self._reduce_part( - reduced_indexes, reduced_data, simple_indexes, simple_data - ) - for other_indexes, other_data in list(self._mixed.items()): - reduced_indexes, reduced_data = self._reduce_part( - reduced_indexes, reduced_data, other_indexes, other_data - ) - if len(reduced_indexes) == 1: - self._queue.append((reduced_indexes, reduced_data)) - else: - self._reduce_mixed_by(reduced_indexes, reduced_data) - if reduced_indexes not in self._mixed: - self._mixed[reduced_indexes] = reduced_data - - @staticmethod - def _reduce_part( - a_indexes: FrozenSet[int], a_data: bytes, b_indexes: FrozenSet[int], b_data: bytes - ) -> Tuple[FrozenSet[int], bytes]: - if b_indexes == a_indexes or not b_indexes.issubset(a_indexes): - return a_indexes, a_data - new_indexes = a_indexes - b_indexes - new_data = bytes(x ^ y for x, y in zip(a_data, b_data, strict=True)) - return new_indexes, new_data - - def _finish(self) -> None: - fragments = [] - for index in range(self._seq_len): - key = frozenset([index]) - if key not in self._simple: - self._bad = True - return - fragments.append(self._simple[key]) - message = b"".join(fragments)[: self._message_len] - if crc32_int(message) != self._checksum: - self._bad = True - return - self._result = message - - def resolve(self) -> Tuple[str, bool]: - if self._bad: - raise ChecksumError("BC-UR v2 message checksum mismatch") - if self._result is None: - raise AnimatedQrError("BC-UR v2 session is not complete") - return _transfer_text(unwrap_ur_cbor(self._result)), False - - class _BbqrSession: def __init__(self): self._total = 0 @@ -1126,8 +250,6 @@ def _transfer_text(raw: bytes) -> str: class AnimatedQrSession: """Facade over the per-format receive sessions used by the QR import page.""" - _DIALECTS = (("balqr", "_BalQrSession"), ("ur1", "_Ur1Session"), ("ur2", "_Ur2Session"), ("bbqr", "_BbqrSession")) - def __init__(self): self._inner: Optional[object] = None self._fmt: Optional[str] = None @@ -1152,12 +274,6 @@ class AnimatedQrSession: @staticmethod def _make(fmt: str) -> object: - if fmt == "balqr": - return _BalQrSession() - if fmt == "ur1": - return _Ur1Session() - if fmt == "ur2": - return _Ur2Session() if fmt == "bbqr": return _BbqrSession() raise AssertionError("unknown animated-QR format {}".format(fmt)) diff --git a/bal/core/plugin_base.py b/bal/core/plugin_base.py index 36c7642..37a853b 100644 --- a/bal/core/plugin_base.py +++ b/bal/core/plugin_base.py @@ -305,11 +305,10 @@ class BalPlugin(BasePlugin): # stay display-only outside the wizard unless the user opts in. self.EDITABLE_DATES = BalConfig(config, "bal_editable_dates", False) - # QR_CHUNK_SIZE (will transfer via QR): payload budget, in bytes, used - # per QR frame when exporting/importing a will through the QR channel. - # The settings dialog offers the 4 standard presets of - # bal.core.qrtransfer.CHUNK_PRESETS; this stores the selected budget. - # Default 150 (small QR, low-resolution cameras). + # QR_CHUNK_SIZE (will transfer via QR): payload budget, in characters, + # used per QR frame when exporting/importing a will through the QR + # channel. The export dialog seeds its own size selector from this + # value (default 150, small QR, low-resolution cameras). self.QR_CHUNK_SIZE = BalConfig(config, "bal_qr_chunk_size", 150) # NUM_REMINDERS (Group D / D1): how many SEPARATE reminder events the diff --git a/bal/core/qrtransfer.py b/bal/core/qrtransfer.py index 6c7d4f2..3cb813e 100644 --- a/bal/core/qrtransfer.py +++ b/bal/core/qrtransfer.py @@ -9,31 +9,11 @@ Scope ----- * converts will transactions into a compact ``transfer_string`` (newline-joined serialized transactions, optionally zlib + base64 - compressed); -* splits that string into fixed-size ``BAL1`` frames for - multi-QR export, and reassembles/validates them on import. + compressed). The transfer string is the payload carried by BBQR QR frames + (``bal.core.animated_qr``). -Wire format (v2, compact) -------------------------- -A frame is:: - - BAL1 - -* ``BAL1`` - magic + format era (4 chars). -* ``TTT`` - frame total as exactly 3 base36 digits (1-based, cap 46655). -* ``iii`` - frame index as exactly 3 base36 digits (1-based). -* ``flag`` - one char: ``Z`` (zlib + base64) or ``0`` (plain ASCII). -* ``payload`` - every other character of the frame; the payloads of all - frames, concatenated in index order, rebuild the transfer string. - -The fixed 11-char header replaces the legacy ``BALQR1|N|i|flags|`` form -(same 5 pieces of information) without any pipe separator, so the whole -frame is scan-friendly and the overhead no longer grows with the frame -count. Legacy ``BALQR1|…`` frames are still accepted on import. - -The audio-modem channel deliberately bypasses the framing helpers here -(PLAN_QR_TRANSFER.md section 4.4): its transport compresses internally and -carries the whole transfer string in a single blob, so callers only use +The audio-modem channel carries the whole transfer string in a single blob +(PLAN_QR_TRANSFER.md section 4.4), so callers only use :func:`encode_transfer` / :func:`decode_transfer`. This module never imports Qt or any Electrum GUI code (house rule). @@ -44,12 +24,7 @@ from __future__ import annotations import base64 import zlib -MAGIC = "BALQR" -VERSION = 1 -FLAG_COMPRESSED = "Z" -FLAG_PLAIN = "0" - -# 4 standard presets (label, payload budget in bytes per QR). Ordered from +# 4 standard presets (label, payload budget per QR). Ordered from # low-resolution cameras to high-resolution cameras (owner decision D5). CHUNK_PRESETS = ( ("Small - ~150 bytes/QR (low-res cameras)", 150), @@ -58,61 +33,16 @@ CHUNK_PRESETS = ( ("XL - ~1800 bytes/QR (high-res cameras)", 1800), ) -# Smallest allowed payload budget per frame, below which the frame header -# could consume the whole budget. -MIN_CHUNK_SIZE = 40 - -# Legacy wire format (still imported); the exporter emits the v2 form below. -_FRAME_MAGIC_V1 = MAGIC + str(VERSION) - -# Compact v2 wire format: fixed-width base36 count fields, no separators. -_FRAME_MAGIC_V2 = "BAL1" -_BASE36_DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" -_BASE36_WIDTH = 3 -_HEADER_V2_LEN = len(_FRAME_MAGIC_V2) + 2 * _BASE36_WIDTH + 1 -_MAX_TOTAL = 36 ** _BASE36_WIDTH - 1 - - -class QrTransferError(ValueError): - """Base error for will QR / audio transfer processing.""" - - -class MissingFramesError(QrTransferError): - """Some frame indices of a multi-QR transfer are missing.""" - - def __init__(self, missing): - self.missing = list(missing) - super().__init__("Missing QR frames: {}".format(self.missing)) - - -class InconsistentTotalError(QrTransferError): - """Frames disagree about the advertised frame total.""" - def encode_transfer(tx_strings, compress=False): """Join serialized transaction strings into a transfer string. ``compress=True`` wraps the joined text in zlib + base64 (ASCII-safe) so - the whole bundle shrinks before being printed/scanned. The optional flag - of the frame header lets the importer reverse this automatically. + the whole bundle shrinks before being printed/scanned. """ return __compress("\n".join(tx_strings), enabled=compress) -def encode_transfer_best(tx_strings): - """Encode ``tx_strings`` with the smaller of plain vs compressed form. - - Returns ``(transfer_string, compressed: bool)``. Compressed wins only - when zlib + base64 really is shorter (best-of, never larger). - """ - joined = "\n".join(tx_strings) - plain = joined - compressed = __compress(joined, enabled=True) - if len(compressed) < len(plain): - return compressed, True - return plain, False - - def decode_transfer(transfer_string, compressed): """Inverse of :func:`encode_transfer`. @@ -124,73 +54,6 @@ def decode_transfer(transfer_string, compressed): return [part for part in text.split("\n") if part] -def split_frames(transfer_string, chunk_size, compressed=False): - """Split ``transfer_string`` into full compact ``BAL1`` frames. - - Every returned frame has the fixed 11-char v2 header followed by its - share of the payload, so each frame is at most ``chunk_size`` characters - long. ``compressed`` stamps the ``Z`` flag into every frame so the - importer knows how to reverse the encoding. - - Raises :class:`QrTransferError` when ``chunk_size`` is too small to hold - the header plus any payload, or when the transfer needs more than - :data:`_MAX_TOTAL` frames. - """ - flag = FLAG_COMPRESSED if compressed else FLAG_PLAIN - total = __compute_total(len(transfer_string), chunk_size) - budget = chunk_size - _HEADER_V2_LEN - frames = [] - pos = 0 - length = len(transfer_string) - for index in range(1, total + 1): - end = min(pos + budget, length) - frames.append( - _FRAME_MAGIC_V2 - + _base36(total) - + _base36(index) - + flag - + transfer_string[pos:end] - ) - pos = end - if pos < length: - # __compute_total guarantees this cannot happen; keep a safety net. - raise QrTransferError("internal error: frames did not cover the transfer string") - return frames - - -def parse_frame(frame): - """Parse a single frame. - - Accepts both the legacy ``BALQR1|total|index|flags|payload`` form and - the compact ``BAL1`` v2 form. - - Returns ``(total, index, compressed: bool, payload: str)``. Raises - :class:`QrTransferError` on malformed input (bad magic/version, wrong - arity, non-integer or out-of-range frame numbers, unknown flags). - """ - if frame.startswith(_FRAME_MAGIC_V2): - return _parse_v2(frame) - return _parse_v1(frame) - - -def assemble(frames, total): - """Concatenate frame payloads back into a transfer string. - - ``frames`` maps 1-based index -> payload. Every index ``1..total`` must - be present (else :class:`MissingFramesError`) and no index may exceed - ``total`` (else :class:`InconsistentTotalError`). - """ - if total < 1: - raise QrTransferError("invalid frame total") - missing = [index for index in range(1, total + 1) if index not in frames] - if missing: - raise MissingFramesError(missing) - extra = [index for index in frames if index > total] - if extra: - raise InconsistentTotalError() - return "".join(frames[index] for index in range(1, total + 1)) - - def preset_index_for_chunk_size(chunk_size): """Return the :data:`CHUNK_PRESETS` index whose budget best matches a size.""" best, best_diff = 0, abs(chunk_size - CHUNK_PRESETS[0][1]) @@ -215,90 +78,3 @@ def __decompress(text, *, enabled): if not enabled: return text return zlib.decompress(base64.b64decode(text.encode("ascii"))).decode("utf-8") - - -def _base36(n): - """Zero-padded :data:`_BASE36_WIDTH` base36 render of ``n``.""" - if not 0 <= n <= _MAX_TOTAL: - raise QrTransferError("BAL QR part number out of range: {}".format(n)) - chars = [] - for _ in range(_BASE36_WIDTH): - chars.append(_BASE36_DIGITS[n % 36]) - n //= 36 - return "".join(reversed(chars)) - - -def _base36_decode(text): - """Inverse of :func:`_base36`; raises ``ValueError`` on bad input.""" - if len(text) != _BASE36_WIDTH or any(c not in _BASE36_DIGITS for c in text): - raise ValueError(text) - n = 0 - for c in text: - n = n * 36 + _BASE36_DIGITS.index(c) - return n - - -def _parse_v1(frame): - parts = frame.split("|", maxsplit=4) - if len(parts) != 5: - raise QrTransferError("Not a BAL will QR (bad frame structure)") - magic_seen, total_s, index_s, flags, payload = parts - if magic_seen != _FRAME_MAGIC_V1: - raise QrTransferError("Not a BAL will QR (unknown magic/version)") - try: - total = int(total_s) - index = int(index_s) - except ValueError as e: - raise QrTransferError("Not a BAL will QR (bad frame numbers)") from e - if total < 1 or not 1 <= index <= total: - raise QrTransferError("Not a BAL will QR (frame numbering out of range)") - if flags not in ("", FLAG_COMPRESSED): - raise QrTransferError("Not a BAL will QR (unknown flags)") - return total, index, flags == FLAG_COMPRESSED, payload - - -def _parse_v2(frame): - if len(frame) < _HEADER_V2_LEN: - raise QrTransferError("Not a BAL will QR (bad frame structure)") - # Magic is length _FRAME_MAGIC_V2; the two base36 fields and the flag - # make up the rest of the fixed header. - offset = len(_FRAME_MAGIC_V2) - total_s = frame[offset : offset + _BASE36_WIDTH] - index_s = frame[offset + _BASE36_WIDTH : offset + 2 * _BASE36_WIDTH] - flag = frame[offset + 2 * _BASE36_WIDTH] - try: - total = _base36_decode(total_s) - index = _base36_decode(index_s) - except ValueError: - raise QrTransferError("Not a BAL will QR (bad frame numbers)") from None - if total < 1 or not 1 <= index <= total: - raise QrTransferError("Not a BAL will QR (frame numbering out of range)") - if flag not in (FLAG_PLAIN, FLAG_COMPRESSED): - raise QrTransferError("Not a BAL will QR (unknown flags)") - payload = frame[_HEADER_V2_LEN:] - return total, index, flag == FLAG_COMPRESSED, payload - - -def __compute_total(transfer_len, chunk_size): - """Smallest frame count whose budget covers the whole transfer string. - - The v2 header is fixed-width, so the budget is constant and the count is - a plain ceiling division, capped at :data:`_MAX_TOTAL`. - """ - if chunk_size < MIN_CHUNK_SIZE: - raise QrTransferError( - "chunk size too small to hold a BAL QR frame: {}".format(chunk_size) - ) - budget = chunk_size - _HEADER_V2_LEN - if budget <= 0: - raise QrTransferError( - "chunk size too small for the BAL QR frame header: {}".format(chunk_size) - ) - total = -(-transfer_len // budget) - if total < 1: - total = 1 - if total > _MAX_TOTAL: - raise QrTransferError( - "BAL QR transfer demands too many frames: {}".format(total) - ) - return total diff --git a/bal/gui/qt/dialogs.py b/bal/gui/qt/dialogs.py index 87d5ab7..fb4ac72 100644 --- a/bal/gui/qt/dialogs.py +++ b/bal/gui/qt/dialogs.py @@ -43,8 +43,6 @@ from ...core.animated_qr import ( bbqr_frames, format_name, parse_for_detection, - ur1_frames, - ur2_frames, ) from ...core.checkalive import ( CheckAliveAfterDeliveryError, @@ -54,13 +52,9 @@ from ...core.checkalive import ( ) from ...core.qrtransfer import ( CHUNK_PRESETS, - MissingFramesError, - QrTransferError, decode_transfer, encode_transfer, - encode_transfer_best, preset_index_for_chunk_size, - split_frames, ) from ...core.reminders import build_ics_reminders from .calendar import BalCalendarButton @@ -147,29 +141,6 @@ if TYPE_CHECKING: # imported lazily where needed to avoid a dialogs<->lists import cycle. -# Animated-QR formats beyond the legacy BAL QR. Combined with the format -# selector in :class:`BalQrExportWidget` they give the export page -# interoperable BC-UR v1/v2 and BBQR output while keeping BAL QR as the -# default (and the only format understood by older plugin versions). -ANIMATED_QR_FORMATS = ("ur1", "ur2", "bbqr") - - -def encode_animated_frames(transfer, fmt, budget_chars): - """Encode the transfer text into the given animated-QR format. - - ``transfer`` is the BAL QR transfer text (str). ``budget_chars`` is the - maximum length of one frame (the exporter's "QR code size" preset). - """ - payload = transfer.encode("utf-8") - if fmt == "ur1": - return ur1_frames(payload, budget_chars) - if fmt == "ur2": - return ur2_frames(payload, budget_chars) - if fmt == "bbqr": - return bbqr_frames(payload, budget_chars, encoding="Z") - raise QrTransferError("unknown animated QR format: {}".format(fmt)) - - class BalDialog(QDialog,MessageBoxMixin): _stopping = False def __init__(self, parent, bal_plugin, title=None, icon="icons/bal16x16.png"): @@ -2764,10 +2735,10 @@ class BalQrExportWidget(QWidget): The page (re)displays itself on :meth:`set_tx_strings`. """ - def __init__(self, chunk_size=CHUNK_PRESETS[0][1], parent=None): + def __init__(self, chunk_size=CHUNK_PRESETS[0][1], parent=None, on_size_change=None): QWidget.__init__(self, parent) self.chunk_size = chunk_size - self.format = "balqr" + self._on_size_change = on_size_change self.tx_strings = [] self.transfer = "" self.frames = [] @@ -2833,36 +2804,11 @@ class BalQrExportWidget(QWidget): size_row.addStretch(1) vbox.addLayout(size_row) - format_row = QHBoxLayout() - format_row.addWidget(QLabel(_("Format:"))) - self.format_combo = QComboBox() - self._format_options = ["balqr"] + list(ANIMATED_QR_FORMATS) - self.format_combo.addItems( - [ - format_name(fmt) - + (" {}".format(_("(default)")) if fmt == "balqr" else "") - for fmt in self._format_options - ] - ) - self.format_combo.setCurrentIndex(0) - self.format_combo.currentIndexChanged.connect(self._on_format_change) - format_row.addWidget(self.format_combo) - self.format_hint = QLabel() - self.format_hint.setWordWrap(True) - format_row.addWidget(self.format_hint, 1) - vbox.addLayout(format_row) - def set_tx_strings(self, tx_strings): """Rebuild the transfer frames from the given serialized transactions.""" self.tx_strings = list(tx_strings) self._stop_auto() self.transfer = encode_transfer(self.tx_strings, compress=False) - # BAL QR now ships compact (best-of) compressed by default: smaller - # frames, and the importer reverses it via the per-frame flag. The - # other formats keep the raw transfer (they compress internally). - self._balqr_transfer, self._balqr_compressed = encode_transfer_best( - self.tx_strings - ) self._refresh_frames() self._update_intro() self._render() @@ -2872,30 +2818,18 @@ class BalQrExportWidget(QWidget): return len(self.frames) def _refresh_frames(self): - if self.format == "balqr": - self.frames = split_frames( - self._balqr_transfer, - self.chunk_size, - compressed=self._balqr_compressed, - ) - else: - self.frames = encode_animated_frames( - self.transfer, self.format, self.chunk_size - ) + self.frames = bbqr_frames( + self.transfer.encode("utf-8"), self.chunk_size, encoding="Z" + ) self.index = 0 - def _on_format_change(self, index): - self._stop_auto() - self.format = self._format_options[index] - self._refresh_frames() - self._render() - self._update_intro() - def _on_chunk_change(self, index): self._stop_auto() self.chunk_size = CHUNK_PRESETS[index][1] self._refresh_frames() self._render() + if self._on_size_change is not None: + self._on_size_change(self.chunk_size) def _toggle_auto(self): """Start/stop the automatic QR slideshow.""" @@ -2927,32 +2861,13 @@ class BalQrExportWidget(QWidget): self._next() def _update_intro(self): - self.format_hint.setText( - { - "balqr": _( - "Proprietary format; every other BAL wallet understands it." - ), - "ur1": _("Legacy BC-UR v1 (ur:bytes), compatible with older " - "Blockchain Commons tools."), - "ur2": _("Standard BC-UR v2 fountain codes (ur:bytes)."), - "bbqr": _("Coinkite BBQR (B$ frames) for BitKit & friends."), - }[self.format] + self.intro_label.setText( + _( + "Scan the QR codes below with the will-opening device.\n" + "{} codes carry the whole transfer (any order works, " + "duplicates are ignored)." + ).format(self.total_frames) ) - if self.format == "balqr": - self.intro_label.setText( - _( - "Scan the QR codes below, in order, with the will-opening " - "device.\nFrame 1 of {} carries the total number of codes." - ).format(self.total_frames) - ) - else: - self.intro_label.setText( - _( - "Scan the QR codes below with the will-opening device.\n" - "{} codes carry the whole transfer (any order works, " - "duplicates are ignored)." - ).format(self.total_frames) - ) def _prev(self): if self.index > 0: @@ -2994,6 +2909,7 @@ class WillExportDialog(BalDialog): def __init__(self, bal_window, will=None, bal_plugin=None, initial_mode="file"): BalDialog.__init__(self, bal_window.window, bal_plugin, _("Export will")) self.bal_window = bal_window + self.bal_plugin = bal_plugin self._source = will if will is not None else bal_window.willitems self._filters = export_filter_options() self._filter_index = 0 @@ -3047,7 +2963,10 @@ class WillExportDialog(BalDialog): self.stacked = QStackedWidget() self.file_page = self._build_file_page() self.stacked.addWidget(self.file_page) - self.qr_page = BalQrExportWidget(chunk_size=chunk) + self.qr_page = BalQrExportWidget( + chunk_size=chunk, + on_size_change=self._remember_qr_size, + ) self.stacked.addWidget(self.qr_page) self.audio_page = self._build_audio_page() self.stacked.addWidget(self.audio_page) @@ -3057,12 +2976,19 @@ class WillExportDialog(BalDialog): close_btn.clicked.connect(self.close) vbox.addWidget(close_btn, alignment=Qt.AlignmentFlag.AlignRight) + # The audio transport is only offered when the audio_modem plugin is + # available; otherwise its radio button is disabled (file and QR stay + # usable). + self.transport_audio.setEnabled(self._audio_available) + mode_ids = { "file": self.MODE_FILE, "qr": self.MODE_QR, "audio": self.MODE_AUDIO, } mode = mode_ids.get(initial_mode, self.MODE_FILE) + if mode == self.MODE_AUDIO and not self._audio_available: + mode = self.MODE_FILE (self.transport_file, self.transport_qr, self.transport_audio)[ mode ].setChecked(True) @@ -3097,6 +3023,7 @@ class WillExportDialog(BalDialog): self._audio_plugin = self.bal_window.get_audio_modem_plugin() self.bitrates = audio_bitrates() available = self._audio_plugin is not None + self._audio_available = available if available: self.audio_warn_label.hide() else: @@ -3125,6 +3052,14 @@ class WillExportDialog(BalDialog): def _selected_items(self): return filter_willitems(self._source, self._filters, self._filter_index) + def _remember_qr_size(self, size): + """Persist the QR size chosen in the export page for the next export.""" + if self.bal_plugin is not None: + try: + self.bal_plugin.QR_CHUNK_SIZE.set(size) + except Exception: + pass + def _payload_strings(self): """The data handed to the QR page, honouring the content scope. @@ -3296,6 +3231,10 @@ class WillImportDialog(BalDialog): close_btn.clicked.connect(self.close) vbox.addWidget(close_btn, alignment=Qt.AlignmentFlag.AlignRight) + # The audio transport is only offered when the audio_modem plugin is + # available; otherwise its radio button is disabled. + self.transport_audio.setEnabled(self._audio_available) + self.transport_file.setChecked(True) self._on_mode_clicked(self.MODE_FILE) @@ -3323,6 +3262,7 @@ class WillImportDialog(BalDialog): self._audio_plugin = self.bal_window.get_audio_modem_plugin() self.bitrates = audio_bitrates() available = self._audio_plugin is not None + self._audio_available = available if available: self.audio_warn_label.hide() else: @@ -3563,9 +3503,9 @@ def qr_import_accept_frame( seen ``stable_reads`` times in a row -- this mirrors the export-side slideshow pacing and swallows transition artifacts; * re-reading the currently accepted frame is ``ignore``d; - * a frame whose ``session_key`` (transfer identity, e.g. ``"balqr:3"`` or - ``"ur2:2-31-3804692811"``) contradicts the transfer already being built - is a ``reset`` (a different transfer was presented). + * a frame whose ``session_key`` (transfer identity, e.g. ``"bbqr:Z:B:3"``) + contradicts the transfer already being built is a ``reset`` (a + different transfer was presented). ``state`` is a mutable mapping with keys ``last_index``, ``last_payload``, ``pending_index``, ``pending_payload``, ``pending_count`` and ``key``. @@ -3751,8 +3691,8 @@ class BalQrImportWidget(QWidget): self._key = key self._init_slots() if status == "ok": - # Slot grid is always 1-based even for 0-based wire formats. - slot = index + 1 if fmt == "bbqr" else index + # Slot grid is always 1-based even for the 0-based BBQR indexes. + slot = index + 1 self.frames[slot] = frame_text self._update_slots() self._update_status() @@ -4008,7 +3948,7 @@ class BalQrImportWidget(QWidget): try: transfer, compressed = session.resolve() parts = decode_transfer(transfer, compressed) - except (MissingFramesError, QrTransferError, AnimatedQrError) as e: + except AnimatedQrError as e: self.show_error(str(e)) self._reset_all() return diff --git a/bal/gui/qt/plugin.py b/bal/gui/qt/plugin.py index da234f2..54de33c 100644 --- a/bal/gui/qt/plugin.py +++ b/bal/gui/qt/plugin.py @@ -19,7 +19,6 @@ from electrum.plugin import hook from electrum.util import EventListener, event_listener from PyQt6.QtWidgets import QLayout -from ...core.qrtransfer import CHUNK_PRESETS, preset_index_for_chunk_size from ...i18n import init_from_config from .common import ( BalPlugin, @@ -538,22 +537,6 @@ class Plugin(BalPlugin, EventListener): # users (BASIC and ADVANCED). heir_auto_rebuild = BalCheckBox(self.AUTO_REBUILD) - # QR Code Size selector (will transfer via QR). A 4-standard-size combo - # bound to the QR_CHUNK_SIZE config (payload budget in bytes per frame). - # Ordered low -> high so the user picks the resolution matching their - # camera. Visible to all users (BASIC and ADVANCED). - qr_size_combo = QComboBox() - # Labels marked for translation in common.QR_PRESET_LABELS. - qr_size_combo.addItems([_(label) for label, _budget in CHUNK_PRESETS]) - qr_size_combo.setCurrentIndex( - preset_index_for_chunk_size(int(self.QR_CHUNK_SIZE.get())) - ) - - def on_qr_size_change(index): - self.QR_CHUNK_SIZE.set(CHUNK_PRESETS[index][1]) - - qr_size_combo.currentIndexChanged.connect(on_qr_size_change) - # USER TYPE selector (SIMPLE / ADVANCED, global). A two-choice combo # (not a free-text field) bound to the USER_TYPE config: # index 0 -> "BASIC" -> stored value "basic" (DEFAULT) @@ -670,10 +653,6 @@ class Plugin(BalPlugin, EventListener): widget.setCurrentIndex( 1 if str(cfg.default).lower() == "advanced" else 0 ) - elif kind == "qr_size": - widget.setCurrentIndex( - preset_index_for_chunk_size(int(cfg.default)) - ) btn.clicked.connect(reset) return btn @@ -932,25 +911,6 @@ class Plugin(BalPlugin, EventListener): ) grid.addWidget(reset_btn_auto_rebuild, 15, 3) - # "QR Code Size" row (always visible, BASIC + ADVANCED). Default QR - # size used when exporting a will via QR codes; changeable per export - # inside the export dialog itself. - lbl_qr_size = QLabel(_("QR Code Size")) - help_qr_size = HelpButton(_( - "Payload size of a single QR code when exporting a will via QR.\n\n" - "Larger QR codes hold more data (fewer shots) but are easier to " - "scan with a high-resolution camera; smaller QR codes scan fine " - "even with low-resolution cameras but require more shots.\n" - "The same selector is available inside the export dialog." - )) - grid.addWidget(lbl_qr_size, 16, 0) - grid.addWidget(qr_size_combo, 16, 1) - grid.addWidget(help_qr_size, 16, 2) - reset_btn_qr_size = _make_reset_btn( - self.QR_CHUNK_SIZE, qr_size_combo, "qr_size" - ) - grid.addWidget(reset_btn_qr_size, 16, 3) - # ----------------------------------------------------------------- # # Group C / C4b: "Reset" button that restores the dialog settings to # # their factory defaults. It only resets the settings exposed by THIS # @@ -984,7 +944,6 @@ class Plugin(BalPlugin, EventListener): (self.HISTORY_LABEL, edit_history_label, "line"), (self.REBUILD_ON_CLOSE, heir_rebuild_on_close, "check"), (self.AUTO_REBUILD, heir_auto_rebuild, "check"), - (self.QR_CHUNK_SIZE, qr_size_combo, "qr_size"), ] for cfg, widget, kind in resets: # Persist the default value back into the Electrum config. @@ -1005,10 +964,6 @@ class Plugin(BalPlugin, EventListener): widget.setCurrentIndex( 1 if str(cfg.default).lower() == "advanced" else 0 ) - elif kind == "qr_size": - widget.setCurrentIndex( - preset_index_for_chunk_size(int(cfg.default)) - ) # Re-sync the history-label field's enabled state after a reset: the # reset restores SAVE_HISTORY to its default, so the field must # follow the (default) checkbox state again. diff --git a/bal/locale/bal.pot b/bal/locale/bal.pot index 99d843d..6a91873 100644 --- a/bal/locale/bal.pot +++ b/bal/locale/bal.pot @@ -1,35 +1,34 @@ -# Translations template for BAL Electrum plugin. -# Copyright (C) 2026 Bitcoin After Life -# This file is distributed under the same license as the BAL Electrum plugin project. +# Translations template for PROJECT. +# Copyright (C) 2026 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. # FIRST AUTHOR , 2026. # #, fuzzy msgid "" msgstr "" -"Project-Id-Version: BAL Electrum plugin VERSION\n" -"Report-Msgid-Bugs-To: info@bitcoin-after.life\n" -"POT-Creation-Date: 2026-10-03 11:24-0400\n" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2026-10-03 12:24-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.18.0\n" +"Generated-By: Babel 2.10.3\n" -#: bal/core/plugin_base.py:361 +#: bal/core/plugin_base.py:360 msgid "" "BAL will execution of $wallet_name\r\n" " heirs list: \r\n" "$heirs_complete" msgstr "" -#: bal/core/plugin_base.py:367 +#: bal/core/plugin_base.py:366 msgid "BAL -Will execution of $wallet_name" msgstr "" #: bal/core/will.py:1317 -#, python-brace-format msgid "NOT {}" msgstr "" @@ -61,7 +60,7 @@ msgstr "" msgid "Error" msgstr "" -#: bal/core/will.py:1348 bal/gui/qt/dialogs.py:853 +#: bal/core/will.py:1348 bal/gui/qt/dialogs.py:824 msgid "Expired" msgstr "" @@ -105,7 +104,7 @@ msgstr "" msgid "Updated" msgstr "" -#: bal/core/will.py:1359 bal/gui/qt/dialogs.py:2661 +#: bal/core/will.py:1359 bal/gui/qt/dialogs.py:2632 msgid "Valid" msgstr "" @@ -138,12 +137,11 @@ msgstr "" msgid "Save" msgstr "" -#: bal/gui/qt/calendar.py:73 bal/gui/qt/widgets.py:1044 +#: bal/gui/qt/calendar.py:72 bal/gui/qt/widgets.py:1043 msgid "No reminders were saved: the delivery date is too close (or already passed)" msgstr "" #: bal/gui/qt/calendar.py:105 bal/gui/qt/calendar.py:126 -#, python-brace-format msgid "Could not open the calendar file: {}" msgstr "" @@ -160,14 +158,12 @@ msgid "Save calendar reminder (.ics)" msgstr "" #: bal/gui/qt/calendar.py:150 bal/gui/qt/widgets.py:1082 -#, python-brace-format msgid "" "Calendar file saved to:\n" "{}" msgstr "" #: bal/gui/qt/calendar.py:155 bal/gui/qt/widgets.py:1078 -#, python-brace-format msgid "Could not save the calendar file: {}" msgstr "" @@ -188,232 +184,222 @@ msgid "XL - ~1800 bytes/QR (high-res cameras)" msgstr "" #: bal/gui/qt/common.py:256 -#, python-brace-format msgid "Select file to save your {}" msgstr "" #: bal/gui/qt/common.py:271 -#, python-brace-format msgid "Your {0} were exported to '{1}'" msgstr "" -#: bal/gui/qt/dialogs.py:217 +#: bal/gui/qt/dialogs.py:188 msgid "Bal Wizard Setup" msgstr "" -#: bal/gui/qt/dialogs.py:354 bal/gui/qt/dialogs.py:2790 +#: bal/gui/qt/dialogs.py:325 bal/gui/qt/dialogs.py:2760 msgid "Previous" msgstr "" -#: bal/gui/qt/dialogs.py:358 bal/gui/qt/dialogs.py:2793 +#: bal/gui/qt/dialogs.py:329 bal/gui/qt/dialogs.py:2763 msgid "Next" msgstr "" -#: bal/gui/qt/dialogs.py:362 bal/gui/qt/dialogs.py:4107 +#: bal/gui/qt/dialogs.py:333 bal/gui/qt/dialogs.py:4019 msgid "Cancel" msgstr "" -#: bal/gui/qt/dialogs.py:388 +#: bal/gui/qt/dialogs.py:359 msgid "Bitcoin After Life Heirs" msgstr "" -#: bal/gui/qt/dialogs.py:390 +#: bal/gui/qt/dialogs.py:360 #, python-format msgid "" "Please add your heirs\n" " remember that 100% of wallet balance will be spent" msgstr "" -#: bal/gui/qt/dialogs.py:398 bal/gui/qt/lists.py:1229 +#: bal/gui/qt/dialogs.py:369 bal/gui/qt/lists.py:1229 msgid "Add" msgstr "" -#: bal/gui/qt/dialogs.py:400 bal/gui/qt/dialogs.py:2622 bal/gui/qt/lists.py:311 +#: bal/gui/qt/dialogs.py:371 bal/gui/qt/dialogs.py:2593 bal/gui/qt/lists.py:311 #: bal/gui/qt/lists.py:673 bal/gui/qt/lists.py:1237 msgid "Import" msgstr "" -#: bal/gui/qt/dialogs.py:402 bal/gui/qt/dialogs.py:2458 -#: bal/gui/qt/dialogs.py:2626 bal/gui/qt/lists.py:312 bal/gui/qt/lists.py:672 +#: bal/gui/qt/dialogs.py:373 bal/gui/qt/dialogs.py:2429 +#: bal/gui/qt/dialogs.py:2597 bal/gui/qt/lists.py:312 bal/gui/qt/lists.py:672 #: bal/gui/qt/lists.py:1249 msgid "Export" msgstr "" -#: bal/gui/qt/dialogs.py:427 bal/gui/qt/dialogs.py:535 +#: bal/gui/qt/dialogs.py:398 bal/gui/qt/dialogs.py:506 msgid "Bitcoin After Life Will-Executors" msgstr "" -#: bal/gui/qt/dialogs.py:428 +#: bal/gui/qt/dialogs.py:399 msgid "Choose willexecutors download method" msgstr "" -#: bal/gui/qt/dialogs.py:435 +#: bal/gui/qt/dialogs.py:406 msgid "Automatically download and select willexecutors" msgstr "" -#: bal/gui/qt/dialogs.py:436 +#: bal/gui/qt/dialogs.py:407 msgid "Only download willexecutors list" msgstr "" -#: bal/gui/qt/dialogs.py:437 +#: bal/gui/qt/dialogs.py:408 msgid "Import willexecutor list from file" msgstr "" -#: bal/gui/qt/dialogs.py:438 +#: bal/gui/qt/dialogs.py:409 msgid "Manual" msgstr "" -#: bal/gui/qt/dialogs.py:467 bal/gui/qt/lists.py:1559 +#: bal/gui/qt/dialogs.py:438 bal/gui/qt/lists.py:1559 msgid "willexecutors" msgstr "" -#: bal/gui/qt/dialogs.py:536 +#: bal/gui/qt/dialogs.py:507 msgid "Configure and select your willexecutors" msgstr "" -#: bal/gui/qt/dialogs.py:555 +#: bal/gui/qt/dialogs.py:526 msgid "Bitcoin After Life Will Settings" msgstr "" -#: bal/gui/qt/dialogs.py:590 bal/gui/qt/dialogs.py:643 +#: bal/gui/qt/dialogs.py:561 bal/gui/qt/dialogs.py:614 msgid "Please wait" msgstr "" -#: bal/gui/qt/dialogs.py:673 +#: bal/gui/qt/dialogs.py:644 msgid "Building Will" msgstr "" -#: bal/gui/qt/dialogs.py:678 +#: bal/gui/qt/dialogs.py:649 msgid "Building Will:" msgstr "" -#: bal/gui/qt/dialogs.py:759 bal/gui/qt/dialogs.py:784 -#: bal/gui/qt/dialogs.py:794 bal/gui/qt/dialogs.py:817 +#: bal/gui/qt/dialogs.py:730 bal/gui/qt/dialogs.py:755 +#: bal/gui/qt/dialogs.py:765 bal/gui/qt/dialogs.py:788 msgid "Checking variables" msgstr "" -#: bal/gui/qt/dialogs.py:785 +#: bal/gui/qt/dialogs.py:756 msgid "Check Alive Threshold Passed: you have to Invalidate your old Will" msgstr "" -#: bal/gui/qt/dialogs.py:794 bal/gui/qt/dialogs.py:876 +#: bal/gui/qt/dialogs.py:765 bal/gui/qt/dialogs.py:847 msgid "No Heirs" msgstr "" -#: bal/gui/qt/dialogs.py:817 bal/gui/qt/dialogs.py:1364 -#: bal/gui/qt/dialogs.py:2310 bal/gui/qt/window.py:2315 +#: bal/gui/qt/dialogs.py:788 bal/gui/qt/dialogs.py:1335 +#: bal/gui/qt/dialogs.py:2281 bal/gui/qt/window.py:2315 msgid "Ok" msgstr "" -#: bal/gui/qt/dialogs.py:822 +#: bal/gui/qt/dialogs.py:792 msgid "" "In the inheritance process, the entire wallet will always be fully emptied. \n" "Your settings require an adjustment of the amounts" msgstr "" -#: bal/gui/qt/dialogs.py:830 bal/gui/qt/window.py:844 -#, python-brace-format +#: bal/gui/qt/dialogs.py:801 bal/gui/qt/window.py:844 msgid "Will-executor fee too high: {}" msgstr "" -#: bal/gui/qt/dialogs.py:864 +#: bal/gui/qt/dialogs.py:835 msgid "Postponed: invalidating old will" msgstr "" -#: bal/gui/qt/dialogs.py:899 bal/gui/qt/dialogs.py:908 -#: bal/gui/qt/dialogs.py:2270 +#: bal/gui/qt/dialogs.py:870 bal/gui/qt/dialogs.py:879 +#: bal/gui/qt/dialogs.py:2241 msgid "Checking your will" msgstr "" -#: bal/gui/qt/dialogs.py:901 +#: bal/gui/qt/dialogs.py:872 msgid "An inheritance of this wallet is already executed (on blockchain)" msgstr "" -#: bal/gui/qt/dialogs.py:910 +#: bal/gui/qt/dialogs.py:881 msgid "Inheritance in mempool (waiting confirmation)" msgstr "" -#: bal/gui/qt/dialogs.py:943 +#: bal/gui/qt/dialogs.py:914 msgid "Will-Executor excluded" msgstr "" -#: bal/gui/qt/dialogs.py:943 bal/gui/qt/dialogs.py:1037 -#: bal/gui/qt/dialogs.py:2251 bal/gui/qt/dialogs.py:2262 +#: bal/gui/qt/dialogs.py:914 bal/gui/qt/dialogs.py:1008 +#: bal/gui/qt/dialogs.py:2222 bal/gui/qt/dialogs.py:2233 msgid "Skipped" msgstr "" -#: bal/gui/qt/dialogs.py:949 bal/gui/qt/dialogs.py:1734 bal/gui/qt/lists.py:345 +#: bal/gui/qt/dialogs.py:920 bal/gui/qt/dialogs.py:1705 bal/gui/qt/lists.py:345 #: bal/gui/qt/theme.py:116 msgid "Will-Executor" msgstr "" -#: bal/gui/qt/dialogs.py:950 +#: bal/gui/qt/dialogs.py:921 msgid "Not present - select one or enable backup mode" msgstr "" -#: bal/gui/qt/dialogs.py:1012 +#: bal/gui/qt/dialogs.py:982 msgid "All heirs' shares are below the dust limit: the inheritance cannot be created. Increase the amounts or reduce the number of heirs." msgstr "" -#: bal/gui/qt/dialogs.py:1028 -#, python-brace-format +#: bal/gui/qt/dialogs.py:998 msgid "Wallet balance is too low: {} satoshi available, but the miner and will-executor fees need {} satoshi (the minimum usable amount is {} satoshi). Add funds, or select fewer will-executors." msgstr "" -#: bal/gui/qt/dialogs.py:1071 -#, python-brace-format +#: bal/gui/qt/dialogs.py:1042 msgid "Heir {}" msgstr "" -#: bal/gui/qt/dialogs.py:1073 -#, python-brace-format +#: bal/gui/qt/dialogs.py:1044 msgid "{} is DUST - excluded (amount below dust limit)" msgstr "" -#: bal/gui/qt/dialogs.py:1129 +#: bal/gui/qt/dialogs.py:1100 msgid "Heir" msgstr "" -#: bal/gui/qt/dialogs.py:1246 bal/gui/qt/dialogs.py:2299 -#, python-brace-format +#: bal/gui/qt/dialogs.py:1217 bal/gui/qt/dialogs.py:2270 msgid "Please wait {}secs" msgstr "" -#: bal/gui/qt/dialogs.py:1253 bal/gui/qt/dialogs.py:1305 -#: bal/gui/qt/dialogs.py:1343 +#: bal/gui/qt/dialogs.py:1224 bal/gui/qt/dialogs.py:1276 +#: bal/gui/qt/dialogs.py:1314 msgid "Broadcasting" msgstr "" -#: bal/gui/qt/dialogs.py:1364 bal/gui/qt/window.py:2317 +#: bal/gui/qt/dialogs.py:1335 bal/gui/qt/window.py:2317 msgid "Ko" msgstr "" -#: bal/gui/qt/dialogs.py:1375 +#: bal/gui/qt/dialogs.py:1346 msgid "Timeout - no answer" msgstr "" -#: bal/gui/qt/dialogs.py:1417 -#, python-brace-format +#: bal/gui/qt/dialogs.py:1388 msgid "checking {} - {}" msgstr "" -#: bal/gui/qt/dialogs.py:1420 bal/gui/qt/dialogs.py:2268 +#: bal/gui/qt/dialogs.py:1391 bal/gui/qt/dialogs.py:2239 #: bal/gui/qt/window.py:1691 msgid "Waiting" msgstr "" -#: bal/gui/qt/dialogs.py:1434 -#, python-brace-format +#: bal/gui/qt/dialogs.py:1405 msgid "checked {} - {}" msgstr "" -#: bal/gui/qt/dialogs.py:1484 bal/gui/qt/dialogs.py:2054 -#: bal/gui/qt/dialogs.py:2065 -#, python-brace-format +#: bal/gui/qt/dialogs.py:1455 bal/gui/qt/dialogs.py:2025 +#: bal/gui/qt/dialogs.py:2036 msgid "Error: {}" msgstr "" -#: bal/gui/qt/dialogs.py:1546 +#: bal/gui/qt/dialogs.py:1516 msgid "" "Your will has expired and must be invalidated before it can be rebuilt.\n" "A transaction window will now open:\n" @@ -421,163 +407,159 @@ msgid "" "After the invalidation is confirmed, press the Check button to finish the will." msgstr "" -#: bal/gui/qt/dialogs.py:1575 bal/gui/qt/window.py:850 +#: bal/gui/qt/dialogs.py:1545 bal/gui/qt/window.py:849 msgid "Check Alive must be after today and before the delivery date, please update it" msgstr "" -#: bal/gui/qt/dialogs.py:1598 +#: bal/gui/qt/dialogs.py:1568 msgid "" "Your old will has been invalidated and the transaction was broadcast.\n" "Electrum may need a little time to register it.\n" "Please wait until the invalidation transaction is confirmed, then press the Check button again to finish updating your will." msgstr "" -#: bal/gui/qt/dialogs.py:1613 +#: bal/gui/qt/dialogs.py:1584 msgid "Invalidate your old will" msgstr "" -#: bal/gui/qt/dialogs.py:1627 bal/gui/qt/dialogs.py:1672 +#: bal/gui/qt/dialogs.py:1598 bal/gui/qt/dialogs.py:1643 msgid "Aborted" msgstr "" -#: bal/gui/qt/dialogs.py:1646 +#: bal/gui/qt/dialogs.py:1617 msgid "Auto-sign disabled — sign manually" msgstr "" -#: bal/gui/qt/dialogs.py:1660 +#: bal/gui/qt/dialogs.py:1630 msgid "" "The delivery date was automatically moved one day earlier so the updated will can correctly replace the previous one.\n" "Please sign (and broadcast) to confirm the change." msgstr "" -#: bal/gui/qt/dialogs.py:1669 +#: bal/gui/qt/dialogs.py:1640 msgid "Sign your will" msgstr "" -#: bal/gui/qt/dialogs.py:1674 bal/gui/qt/dialogs.py:1957 +#: bal/gui/qt/dialogs.py:1645 bal/gui/qt/dialogs.py:1928 msgid "Nothing to do" msgstr "" -#: bal/gui/qt/dialogs.py:1693 +#: bal/gui/qt/dialogs.py:1664 msgid "Finished" msgstr "" -#: bal/gui/qt/dialogs.py:1710 +#: bal/gui/qt/dialogs.py:1681 msgid "Calendar" msgstr "" -#: bal/gui/qt/dialogs.py:1712 bal/gui/qt/dialogs.py:1742 -#: bal/gui/qt/dialogs.py:1809 bal/gui/qt/dialogs.py:2057 -#: bal/gui/qt/dialogs.py:3056 bal/gui/qt/dialogs.py:3295 -#: bal/gui/qt/dialogs.py:4126 +#: bal/gui/qt/dialogs.py:1683 bal/gui/qt/dialogs.py:1713 +#: bal/gui/qt/dialogs.py:1780 bal/gui/qt/dialogs.py:2028 +#: bal/gui/qt/dialogs.py:2968 bal/gui/qt/dialogs.py:3207 +#: bal/gui/qt/dialogs.py:4038 msgid "Close" msgstr "" -#: bal/gui/qt/dialogs.py:1738 bal/gui/qt/dialogs.py:1805 +#: bal/gui/qt/dialogs.py:1709 bal/gui/qt/dialogs.py:1776 msgid "🔮 Wizard" msgstr "" -#: bal/gui/qt/dialogs.py:1801 bal/gui/qt/dialogs.py:2609 +#: bal/gui/qt/dialogs.py:1772 bal/gui/qt/dialogs.py:2580 msgid "Heirs" msgstr "" -#: bal/gui/qt/dialogs.py:1910 bal/gui/qt/widgets.py:1150 -#, python-brace-format +#: bal/gui/qt/dialogs.py:1881 bal/gui/qt/widgets.py:1150 msgid "(reminder {idx}/{total})" msgstr "" -#: bal/gui/qt/dialogs.py:1974 +#: bal/gui/qt/dialogs.py:1945 msgid "All done" msgstr "" -#: bal/gui/qt/dialogs.py:2026 +#: bal/gui/qt/dialogs.py:1996 msgid "Next steps (manual): press 'Sign' to sign your will, then 'Broadcast' to send it to the will-executors." msgstr "" -#: bal/gui/qt/dialogs.py:2031 +#: bal/gui/qt/dialogs.py:2001 msgid "Next step (manual): press 'Sign' to sign your will." msgstr "" -#: bal/gui/qt/dialogs.py:2035 +#: bal/gui/qt/dialogs.py:2005 msgid "Next step (manual): press 'Broadcast' to send your will to the will-executors." msgstr "" -#: bal/gui/qt/dialogs.py:2130 -#, python-brace-format +#: bal/gui/qt/dialogs.py:2100 msgid "Miner fee rate changed (the will was built with {} sat/byte, now it is {}): a new will must be prepared." msgstr "" -#: bal/gui/qt/dialogs.py:2135 +#: bal/gui/qt/dialogs.py:2105 msgid "The miner fee rate changed: a new will must be prepared." msgstr "" -#: bal/gui/qt/dialogs.py:2139 -#, python-brace-format +#: bal/gui/qt/dialogs.py:2109 msgid "Will-executor \"{}\" is not covered by the current will: a new will must be prepared." msgstr "" -#: bal/gui/qt/dialogs.py:2144 +#: bal/gui/qt/dialogs.py:2114 msgid "Backup mode is enabled but the will has no backup transaction: a new will must be prepared." msgstr "" -#: bal/gui/qt/dialogs.py:2153 -#, python-brace-format +#: bal/gui/qt/dialogs.py:2123 msgid "Heir \"{}\" is not covered by the current will (it was added or removed, or its delivery date changed): a new will must be prepared." msgstr "" -#: bal/gui/qt/dialogs.py:2161 +#: bal/gui/qt/dialogs.py:2132 msgid "The heirs changed: a new will must be prepared." msgstr "" -#: bal/gui/qt/dialogs.py:2163 +#: bal/gui/qt/dialogs.py:2134 msgid "A will-executor changed: a new will must be prepared." msgstr "" -#: bal/gui/qt/dialogs.py:2173 +#: bal/gui/qt/dialogs.py:2143 msgid "The wallet contains funds that the current will does not cover yet: a new will must be prepared." msgstr "" -#: bal/gui/qt/dialogs.py:2177 +#: bal/gui/qt/dialogs.py:2147 msgid "The will contains no valid transaction: a new will must be prepared." msgstr "" -#: bal/gui/qt/dialogs.py:2183 +#: bal/gui/qt/dialogs.py:2153 msgid "The will is no longer coherent and must be rebuilt; the exact reason could not be determined." msgstr "" -#: bal/gui/qt/dialogs.py:2213 +#: bal/gui/qt/dialogs.py:2183 msgid "No heirs: add at least one heir before building the will." msgstr "" -#: bal/gui/qt/dialogs.py:2216 +#: bal/gui/qt/dialogs.py:2186 msgid "The wallet has no spendable funds, so no inheritance transaction can be created." msgstr "" -#: bal/gui/qt/dialogs.py:2220 +#: bal/gui/qt/dialogs.py:2190 msgid "No usable will-executor: none of the servers in the list is both selected and valid. Open the will-executor settings, select at least one server and check that it is reachable." msgstr "" -#: bal/gui/qt/dialogs.py:2225 +#: bal/gui/qt/dialogs.py:2195 msgid "No delivery date left to build: every heir's date is already covered by the existing will. Choose a later delivery date, or change an heir's date." msgstr "" -#: bal/gui/qt/dialogs.py:2230 +#: bal/gui/qt/dialogs.py:2200 msgid "The amount to send must cover the miner fees plus this will-executor's fee, and the wallet balance is not enough: select cheaper will-executors, or add funds to the wallet." msgstr "" -#: bal/gui/qt/dialogs.py:2235 +#: bal/gui/qt/dialogs.py:2205 msgid "A will-executor asks for more than the maximum fee you allowed: raise the maximum fee in the settings, or select a cheaper will-executor." msgstr "" -#: bal/gui/qt/dialogs.py:2240 +#: bal/gui/qt/dialogs.py:2210 msgid "The inheritance transactions could not be assembled from the available funds (the balance may not cover the miner fees)." msgstr "" -#: bal/gui/qt/dialogs.py:2244 +#: bal/gui/qt/dialogs.py:2214 msgid "An unexpected error stopped the transactions being prepared for a will-executor, so it was skipped. Try again, or select a different will-executor." msgstr "" -#: bal/gui/qt/dialogs.py:2255 +#: bal/gui/qt/dialogs.py:2225 msgid "" "Could not build the will, and the exact cause could not be determined. Please check that:\n" "- the wallet balance covers the miner and will-executor fees,\n" @@ -586,455 +568,403 @@ msgid "" "- at least one will-executor is selected and reachable." msgstr "" -#: bal/gui/qt/dialogs.py:2275 +#: bal/gui/qt/dialogs.py:2246 msgid "Invalidating old will" msgstr "" -#: bal/gui/qt/dialogs.py:2281 +#: bal/gui/qt/dialogs.py:2252 msgid "Building your will" msgstr "" -#: bal/gui/qt/dialogs.py:2287 +#: bal/gui/qt/dialogs.py:2258 msgid "Signing your will" msgstr "" -#: bal/gui/qt/dialogs.py:2293 +#: bal/gui/qt/dialogs.py:2264 msgid "Broadcasting your will to executors" msgstr "" -#: bal/gui/qt/dialogs.py:2341 +#: bal/gui/qt/dialogs.py:2312 msgid "Wait" msgstr "" -#: bal/gui/qt/dialogs.py:2444 +#: bal/gui/qt/dialogs.py:2415 msgid "Will Details" msgstr "" -#: bal/gui/qt/dialogs.py:2450 bal/gui/qt/dialogs.py:4101 +#: bal/gui/qt/dialogs.py:2421 bal/gui/qt/dialogs.py:4013 #: bal/gui/qt/lists.py:668 msgid "Sign" msgstr "" -#: bal/gui/qt/dialogs.py:2454 bal/gui/qt/lists.py:675 +#: bal/gui/qt/dialogs.py:2425 bal/gui/qt/lists.py:675 msgid "Broadcast" msgstr "" -#: bal/gui/qt/dialogs.py:2461 bal/gui/qt/lists.py:677 +#: bal/gui/qt/dialogs.py:2432 bal/gui/qt/lists.py:677 msgid "Invalidate" msgstr "" -#: bal/gui/qt/dialogs.py:2466 bal/gui/qt/lists.py:674 +#: bal/gui/qt/dialogs.py:2437 bal/gui/qt/lists.py:674 msgid "Merge" msgstr "" -#: bal/gui/qt/dialogs.py:2474 -#, python-brace-format +#: bal/gui/qt/dialogs.py:2445 msgid "Expiration date: {}" msgstr "" -#: bal/gui/qt/dialogs.py:2480 -#, python-brace-format +#: bal/gui/qt/dialogs.py:2451 msgid "Valid Txs:{}" msgstr "" -#: bal/gui/qt/dialogs.py:2482 -#, python-brace-format +#: bal/gui/qt/dialogs.py:2453 msgid "Total Txs:{}" msgstr "" -#: bal/gui/qt/dialogs.py:2536 +#: bal/gui/qt/dialogs.py:2507 msgid "Hide replaced" msgstr "" -#: bal/gui/qt/dialogs.py:2538 +#: bal/gui/qt/dialogs.py:2509 msgid "Unhide replaced" msgstr "" -#: bal/gui/qt/dialogs.py:2544 +#: bal/gui/qt/dialogs.py:2515 msgid "Hide invalidated" msgstr "" -#: bal/gui/qt/dialogs.py:2546 +#: bal/gui/qt/dialogs.py:2517 msgid "Unhide invalidated" msgstr "" -#: bal/gui/qt/dialogs.py:2571 +#: bal/gui/qt/dialogs.py:2542 msgid "Will-Executor Service List" msgstr "" -#: bal/gui/qt/dialogs.py:2618 bal/gui/qt/lists.py:314 +#: bal/gui/qt/dialogs.py:2589 bal/gui/qt/lists.py:314 msgid "New Heir" msgstr "" -#: bal/gui/qt/dialogs.py:2660 +#: bal/gui/qt/dialogs.py:2631 msgid "All" msgstr "" -#: bal/gui/qt/dialogs.py:2663 +#: bal/gui/qt/dialogs.py:2634 msgid "Valid NC" msgstr "" -#: bal/gui/qt/dialogs.py:2801 bal/gui/qt/dialogs.py:2916 +#: bal/gui/qt/dialogs.py:2771 bal/gui/qt/dialogs.py:2847 msgid "Auto" msgstr "" -#: bal/gui/qt/dialogs.py:2803 +#: bal/gui/qt/dialogs.py:2773 msgid "Automatically advance through the QR codes." msgstr "" -#: bal/gui/qt/dialogs.py:2807 +#: bal/gui/qt/dialogs.py:2777 msgid "QR codes per second:" msgstr "" -#: bal/gui/qt/dialogs.py:2811 +#: bal/gui/qt/dialogs.py:2781 msgid " /s" msgstr "" -#: bal/gui/qt/dialogs.py:2813 +#: bal/gui/qt/dialogs.py:2783 msgid "Loop" msgstr "" -#: bal/gui/qt/dialogs.py:2816 +#: bal/gui/qt/dialogs.py:2785 msgid "When the last QR code is reached, keep cycling from the first one instead of stopping." msgstr "" -#: bal/gui/qt/dialogs.py:2825 +#: bal/gui/qt/dialogs.py:2795 msgid "QR code size:" msgstr "" -#: bal/gui/qt/dialogs.py:2837 -msgid "Format:" -msgstr "" - -#: bal/gui/qt/dialogs.py:2843 -msgid "(default)" -msgstr "" - -#: bal/gui/qt/dialogs.py:2910 +#: bal/gui/qt/dialogs.py:2841 msgid "Stop" msgstr "" -#: bal/gui/qt/dialogs.py:2933 -msgid "Proprietary format; every other BAL wallet understands it." -msgstr "" - -#: bal/gui/qt/dialogs.py:2935 -msgid "Legacy BC-UR v1 (ur:bytes), compatible with older Blockchain Commons tools." -msgstr "" - -#: bal/gui/qt/dialogs.py:2937 -msgid "Standard BC-UR v2 fountain codes (ur:bytes)." -msgstr "" - -#: bal/gui/qt/dialogs.py:2938 -msgid "Coinkite BBQR (B$ frames) for BitKit & friends." -msgstr "" - -#: bal/gui/qt/dialogs.py:2944 -#, python-brace-format -msgid "" -"Scan the QR codes below, in order, with the will-opening device.\n" -"Frame 1 of {} carries the total number of codes." -msgstr "" - -#: bal/gui/qt/dialogs.py:2951 -#, python-brace-format +#: bal/gui/qt/dialogs.py:2862 msgid "" "Scan the QR codes below with the will-opening device.\n" "{} codes carry the whole transfer (any order works, duplicates are ignored)." msgstr "" -#: bal/gui/qt/dialogs.py:2972 -#, python-brace-format +#: bal/gui/qt/dialogs.py:2884 msgid "Frame {} of {}" msgstr "" -#: bal/gui/qt/dialogs.py:2995 +#: bal/gui/qt/dialogs.py:2907 msgid "Export will" msgstr "" -#: bal/gui/qt/dialogs.py:3005 +#: bal/gui/qt/dialogs.py:2917 msgid "No will transaction to export." msgstr "" -#: bal/gui/qt/dialogs.py:3012 +#: bal/gui/qt/dialogs.py:2924 msgid "Export:" msgstr "" -#: bal/gui/qt/dialogs.py:3019 +#: bal/gui/qt/dialogs.py:2931 msgid "Whole will" msgstr "" -#: bal/gui/qt/dialogs.py:3023 +#: bal/gui/qt/dialogs.py:2934 msgid "Export the full will items (all details/statuses) as JSON, or only the serialized transactions." msgstr "" -#: bal/gui/qt/dialogs.py:3033 +#: bal/gui/qt/dialogs.py:2945 msgid "Send as:" msgstr "" -#: bal/gui/qt/dialogs.py:3035 bal/gui/qt/dialogs.py:3272 +#: bal/gui/qt/dialogs.py:2947 bal/gui/qt/dialogs.py:3184 msgid "File" msgstr "" -#: bal/gui/qt/dialogs.py:3036 bal/gui/qt/dialogs.py:3273 +#: bal/gui/qt/dialogs.py:2948 bal/gui/qt/dialogs.py:3185 msgid "QR Code" msgstr "" -#: bal/gui/qt/dialogs.py:3037 bal/gui/qt/dialogs.py:3274 +#: bal/gui/qt/dialogs.py:2949 bal/gui/qt/dialogs.py:3186 msgid "Audio" msgstr "" -#: bal/gui/qt/dialogs.py:3079 +#: bal/gui/qt/dialogs.py:2990 msgid "Export the will as a JSON file. Uncheck \"Whole will\" above to export only the serialized transactions (comma-separated)." msgstr "" -#: bal/gui/qt/dialogs.py:3087 +#: bal/gui/qt/dialogs.py:2999 msgid "Export…" msgstr "" -#: bal/gui/qt/dialogs.py:3104 bal/gui/qt/dialogs.py:3330 +#: bal/gui/qt/dialogs.py:3016 bal/gui/qt/dialogs.py:3242 #: bal/gui/qt/window.py:1787 bal/gui/qt/window.py:1800 msgid "Audio MODEM plugin is not available." msgstr "" -#: bal/gui/qt/dialogs.py:3108 bal/gui/qt/dialogs.py:3334 +#: bal/gui/qt/dialogs.py:3020 bal/gui/qt/dialogs.py:3246 msgid "Speed (KB/sec):" msgstr "" -#: bal/gui/qt/dialogs.py:3119 +#: bal/gui/qt/dialogs.py:3031 msgid "Send" msgstr "" -#: bal/gui/qt/dialogs.py:3164 bal/gui/qt/dialogs.py:3198 -#: bal/gui/qt/dialogs.py:3223 +#: bal/gui/qt/dialogs.py:3076 bal/gui/qt/dialogs.py:3110 +#: bal/gui/qt/dialogs.py:3135 msgid "No will transaction matches the selected filter." msgstr "" -#: bal/gui/qt/dialogs.py:3187 +#: bal/gui/qt/dialogs.py:3099 msgid "item(s)" msgstr "" -#: bal/gui/qt/dialogs.py:3187 +#: bal/gui/qt/dialogs.py:3099 msgid "transaction(s)" msgstr "" -#: bal/gui/qt/dialogs.py:3189 -#, python-brace-format +#: bal/gui/qt/dialogs.py:3101 msgid "{} {} will be exported." msgstr "" -#: bal/gui/qt/dialogs.py:3192 -#, python-brace-format +#: bal/gui/qt/dialogs.py:3104 msgid "{} {} will be sent." msgstr "" -#: bal/gui/qt/dialogs.py:3216 bal/gui/qt/dialogs.py:3219 -#: bal/gui/qt/dialogs.py:3370 bal/gui/qt/dialogs.py:3373 +#: bal/gui/qt/dialogs.py:3128 bal/gui/qt/dialogs.py:3131 +#: bal/gui/qt/dialogs.py:3282 bal/gui/qt/dialogs.py:3285 msgid "Invalid audio speed." msgstr "" -#: bal/gui/qt/dialogs.py:3255 +#: bal/gui/qt/dialogs.py:3167 msgid "Import will" msgstr "" -#: bal/gui/qt/dialogs.py:3262 +#: bal/gui/qt/dialogs.py:3173 msgid "" "Choose how the will was exported: from a file, QR codes or audio.\n" "The import never touches the live will." msgstr "" -#: bal/gui/qt/dialogs.py:3270 +#: bal/gui/qt/dialogs.py:3182 msgid "Import from:" msgstr "" -#: bal/gui/qt/dialogs.py:3307 +#: bal/gui/qt/dialogs.py:3218 msgid "Import the JSON will file written by the Export ▶ File option. The will opens in a read-only preview window." msgstr "" -#: bal/gui/qt/dialogs.py:3313 +#: bal/gui/qt/dialogs.py:3225 msgid "Choose will file…" msgstr "" -#: bal/gui/qt/dialogs.py:3343 +#: bal/gui/qt/dialogs.py:3255 msgid "Waiting for the audio transfer…" msgstr "" -#: bal/gui/qt/dialogs.py:3346 +#: bal/gui/qt/dialogs.py:3258 msgid "Receive by audio…" msgstr "" -#: bal/gui/qt/dialogs.py:3387 +#: bal/gui/qt/dialogs.py:3299 msgid "Receiving…" msgstr "" -#: bal/gui/qt/dialogs.py:3407 +#: bal/gui/qt/dialogs.py:3319 msgid "No transaction data received." msgstr "" -#: bal/gui/qt/dialogs.py:3425 -#, python-brace-format +#: bal/gui/qt/dialogs.py:3337 msgid "Waiting for audio ({:.1f} kbps)…" msgstr "" -#: bal/gui/qt/dialogs.py:3498 -#, python-brace-format +#: bal/gui/qt/dialogs.py:3410 msgid "Could not parse a transferred will item: {}" msgstr "" -#: bal/gui/qt/dialogs.py:3518 -#, python-brace-format +#: bal/gui/qt/dialogs.py:3430 msgid "Could not parse a transferred transaction: {}" msgstr "" -#: bal/gui/qt/dialogs.py:3530 +#: bal/gui/qt/dialogs.py:3441 msgid "The imported will contains no valid transaction in this wallet." msgstr "" -#: bal/gui/qt/dialogs.py:3541 -#, python-brace-format +#: bal/gui/qt/dialogs.py:3452 msgid "{} imported transaction(s) are not valid in this wallet and were skipped." msgstr "" -#: bal/gui/qt/dialogs.py:3641 +#: bal/gui/qt/dialogs.py:3552 msgid "" "Show the will QR codes to the camera, one after another.\n" "After the first code, every new code is captured automatically.\n" "The first frame sets the total number of codes; duplicates are ignored." msgstr "" -#: bal/gui/qt/dialogs.py:3650 bal/gui/qt/dialogs.py:3828 +#: bal/gui/qt/dialogs.py:3562 bal/gui/qt/dialogs.py:3740 msgid "Waiting for the first frame…" msgstr "" -#: bal/gui/qt/dialogs.py:3667 +#: bal/gui/qt/dialogs.py:3579 msgid "…or paste/type the frame text here" msgstr "" -#: bal/gui/qt/dialogs.py:3671 +#: bal/gui/qt/dialogs.py:3583 msgid "Add frame" msgstr "" -#: bal/gui/qt/dialogs.py:3677 bal/gui/qt/dialogs.py:3920 +#: bal/gui/qt/dialogs.py:3589 bal/gui/qt/dialogs.py:3832 msgid "Scan with camera…" msgstr "" -#: bal/gui/qt/dialogs.py:3680 +#: bal/gui/qt/dialogs.py:3591 msgid "Start the live camera. While it runs, every new QR code shown to the camera is captured automatically." msgstr "" -#: bal/gui/qt/dialogs.py:3686 +#: bal/gui/qt/dialogs.py:3598 msgid "Reset" msgstr "" -#: bal/gui/qt/dialogs.py:3693 +#: bal/gui/qt/dialogs.py:3605 msgid "Review and Sign…" msgstr "" -#: bal/gui/qt/dialogs.py:3713 -#, python-brace-format +#: bal/gui/qt/dialogs.py:3624 msgid "The scanned code belongs to a different transfer ({} frames, {}). The import was reset; scan the first code again." msgstr "" -#: bal/gui/qt/dialogs.py:3803 -#, python-brace-format +#: bal/gui/qt/dialogs.py:3715 msgid "All {} frames stored." msgstr "" -#: bal/gui/qt/dialogs.py:3809 -#, python-brace-format +#: bal/gui/qt/dialogs.py:3721 msgid "Stored {} of {} frames." msgstr "" -#: bal/gui/qt/dialogs.py:3874 +#: bal/gui/qt/dialogs.py:3786 msgid "Cannot start QR scanner, no usable camera found." msgstr "" -#: bal/gui/qt/dialogs.py:3879 +#: bal/gui/qt/dialogs.py:3791 msgid "Stop scanning" msgstr "" -#: bal/gui/qt/dialogs.py:3926 -#, python-brace-format +#: bal/gui/qt/dialogs.py:3838 msgid "Camera error: {}" msgstr "" -#: bal/gui/qt/dialogs.py:3973 -#, python-brace-format +#: bal/gui/qt/dialogs.py:3884 msgid "The scanned code belongs to a different transfer ({} frames, {}). The import was reset; show the first code again." msgstr "" -#: bal/gui/qt/dialogs.py:4016 +#: bal/gui/qt/dialogs.py:3928 msgid "The transferred will contains no transactions." msgstr "" -#: bal/gui/qt/dialogs.py:4045 +#: bal/gui/qt/dialogs.py:3957 msgid "Review and sign imported will" msgstr "" -#: bal/gui/qt/dialogs.py:4058 +#: bal/gui/qt/dialogs.py:3969 msgid "Enter your wallet password to sign the imported transactions." msgstr "" -#: bal/gui/qt/dialogs.py:4104 +#: bal/gui/qt/dialogs.py:4016 msgid "Skip" msgstr "" -#: bal/gui/qt/dialogs.py:4120 +#: bal/gui/qt/dialogs.py:4032 msgid "Save signed file…" msgstr "" -#: bal/gui/qt/dialogs.py:4123 +#: bal/gui/qt/dialogs.py:4035 msgid "Show signed QR…" msgstr "" -#: bal/gui/qt/dialogs.py:4147 -#, python-brace-format +#: bal/gui/qt/dialogs.py:4059 msgid "Transaction {} of {}" msgstr "" -#: bal/gui/qt/dialogs.py:4149 -#, python-brace-format +#: bal/gui/qt/dialogs.py:4061 msgid "TXID: {}" msgstr "" -#: bal/gui/qt/dialogs.py:4152 +#: bal/gui/qt/dialogs.py:4064 msgid "unknown" msgstr "" -#: bal/gui/qt/dialogs.py:4158 +#: bal/gui/qt/dialogs.py:4070 msgid "(no outputs)" msgstr "" -#: bal/gui/qt/dialogs.py:4172 +#: bal/gui/qt/dialogs.py:4084 msgid "unknown (partial transaction)" msgstr "" -#: bal/gui/qt/dialogs.py:4174 -#, python-brace-format +#: bal/gui/qt/dialogs.py:4086 msgid "" "Total outputs: {}\n" "Fee: {}" msgstr "" -#: bal/gui/qt/dialogs.py:4186 -#, python-brace-format +#: bal/gui/qt/dialogs.py:4097 msgid "" "Signed {} of {} transactions.\n" "\n" "Save a signed file to carry to the broadcast device, or show the signed transactions as QR codes." msgstr "" -#: bal/gui/qt/dialogs.py:4204 -#, python-brace-format +#: bal/gui/qt/dialogs.py:4116 msgid "Could not sign the transaction: {}" msgstr "" -#: bal/gui/qt/dialogs.py:4223 +#: bal/gui/qt/dialogs.py:4135 msgid "Signed will saved." msgstr "" -#: bal/gui/qt/dialogs.py:4234 +#: bal/gui/qt/dialogs.py:4146 msgid "No signed transaction to show." msgstr "" @@ -1052,12 +982,10 @@ msgid "Amount" msgstr "" #: bal/gui/qt/lists.py:232 -#, python-brace-format msgid "Copy {}" msgstr "" #: bal/gui/qt/lists.py:240 bal/gui/qt/lists.py:1007 -#, python-brace-format msgid "Edit {}" msgstr "" @@ -1130,7 +1058,6 @@ msgid "Open transaction file" msgstr "" #: bal/gui/qt/lists.py:536 -#, python-brace-format msgid "Invalid transaction file: {}" msgstr "" @@ -1247,7 +1174,6 @@ msgid "Deselect only invalid" msgstr "" #: bal/gui/qt/lists.py:1288 bal/gui/qt/window.py:290 -#, python-brace-format msgid "Edit: {}" msgstr "" @@ -1261,12 +1187,10 @@ msgid "URL is required" msgstr "" #: bal/gui/qt/lists.py:1355 -#, python-brace-format msgid "Could not reach server at {}" msgstr "" #: bal/gui/qt/lists.py:1369 -#, python-brace-format msgid "Error contacting server: {}" msgstr "" @@ -1306,97 +1230,97 @@ msgstr "" msgid "No will-executor is selected" msgstr "" -#: bal/gui/qt/plugin.py:140 +#: bal/gui/qt/plugin.py:139 msgid "Electrum Plugins" msgstr "" -#: bal/gui/qt/plugin.py:307 +#: bal/gui/qt/plugin.py:306 msgid "Bitcoin After Life" msgstr "" -#: bal/gui/qt/plugin.py:426 bal/gui/qt/plugin.py:451 +#: bal/gui/qt/plugin.py:425 bal/gui/qt/plugin.py:450 msgid "Settings" msgstr "" -#: bal/gui/qt/plugin.py:568 +#: bal/gui/qt/plugin.py:551 msgid "BASIC" msgstr "" -#: bal/gui/qt/plugin.py:568 +#: bal/gui/qt/plugin.py:551 msgid "ADVANCED" msgstr "" -#: bal/gui/qt/plugin.py:588 +#: bal/gui/qt/plugin.py:571 msgid "Enable ADVANCED mode" msgstr "" -#: bal/gui/qt/plugin.py:589 +#: bal/gui/qt/plugin.py:572 msgid "Type 'at My Risk' to enable ADVANCED mode" msgstr "" -#: bal/gui/qt/plugin.py:658 +#: bal/gui/qt/plugin.py:641 msgid "Reset to default" msgstr "" -#: bal/gui/qt/plugin.py:680 +#: bal/gui/qt/plugin.py:659 msgid "Rebroadcast transactions" msgstr "" -#: bal/gui/qt/plugin.py:691 +#: bal/gui/qt/plugin.py:670 msgid "Warning: change these settings only if you know what you are doing." msgstr "" -#: bal/gui/qt/plugin.py:706 +#: bal/gui/qt/plugin.py:685 msgid "Hide Replaced" msgstr "" -#: bal/gui/qt/plugin.py:709 +#: bal/gui/qt/plugin.py:688 msgid "Hide replaced transactions from will detail and list" msgstr "" -#: bal/gui/qt/plugin.py:714 +#: bal/gui/qt/plugin.py:693 msgid "Hide Invalidated" msgstr "" -#: bal/gui/qt/plugin.py:717 +#: bal/gui/qt/plugin.py:696 msgid "Hide invalidated transactions from will detail and list" msgstr "" -#: bal/gui/qt/plugin.py:720 +#: bal/gui/qt/plugin.py:699 msgid "Auto-sign on Check" msgstr "" -#: bal/gui/qt/plugin.py:722 +#: bal/gui/qt/plugin.py:700 msgid "" "When checking, automatically sign and broadcast the will transactions to their will-executors.\n" "The wallet password is requested only if the wallet is encrypted." msgstr "" -#: bal/gui/qt/plugin.py:736 +#: bal/gui/qt/plugin.py:715 msgid "Panel editable Date and Fee" msgstr "" -#: bal/gui/qt/plugin.py:740 +#: bal/gui/qt/plugin.py:718 msgid "" "When enabled, the delivery-time and check-alive date fields can be edited everywhere (toolbar / Heirs tab), not only in the will-building wizard.\n" "When disabled, those dates are display-only outside the wizard." msgstr "" -#: bal/gui/qt/plugin.py:751 +#: bal/gui/qt/plugin.py:730 msgid "Max Will-Executor Fee (satoshi)" msgstr "" -#: bal/gui/qt/plugin.py:755 +#: bal/gui/qt/plugin.py:733 msgid "" "Maximum fee (in satoshi) allowed to be paid to a single will-executor. If a will-executor charges more than this, the will will not be built.\n" "Default: 500,000 satoshi (0.005 BTC)." msgstr "" -#: bal/gui/qt/plugin.py:766 +#: bal/gui/qt/plugin.py:745 msgid "User Type" msgstr "" -#: bal/gui/qt/plugin.py:770 +#: bal/gui/qt/plugin.py:748 msgid "" "Choose how much detail the plugin shows.\n" "\n" @@ -1405,19 +1329,19 @@ msgid "" "ADVANCED: shows every control, including the 'Check Alive' field and the Raw/Date selector. Allows configuring the welist server URL and keeps all date/fee fields always editable." msgstr "" -#: bal/gui/qt/plugin.py:783 +#: bal/gui/qt/plugin.py:762 msgid "Number of reminders" msgstr "" -#: bal/gui/qt/plugin.py:785 +#: bal/gui/qt/plugin.py:763 msgid "How many reminder alarms the exported calendar (.ics) event contains. Range: 1 to 5 (default 3). Only used in ADVANCED mode." msgstr "" -#: bal/gui/qt/plugin.py:794 +#: bal/gui/qt/plugin.py:773 msgid "Event summary" msgstr "" -#: bal/gui/qt/plugin.py:796 +#: bal/gui/qt/plugin.py:774 msgid "" "Default message to be used in event summary\n" "Variables:\n" @@ -1426,11 +1350,11 @@ msgid "" "Only used in ADVANCED mode." msgstr "" -#: bal/gui/qt/plugin.py:808 +#: bal/gui/qt/plugin.py:787 msgid "Event description" msgstr "" -#: bal/gui/qt/plugin.py:810 +#: bal/gui/qt/plugin.py:788 msgid "" "Default message to be used in event description\n" "Variables:\n" @@ -1439,31 +1363,30 @@ msgid "" "Only used in ADVANCED mode." msgstr "" -#: bal/gui/qt/plugin.py:823 +#: bal/gui/qt/plugin.py:802 msgid "Welist Server URL" msgstr "" -#: bal/gui/qt/plugin.py:825 +#: bal/gui/qt/plugin.py:803 msgid "URL of the server that provides the will-executor list. Only available in ADVANCED mode." msgstr "" -#: bal/gui/qt/plugin.py:834 +#: bal/gui/qt/plugin.py:813 msgid "Calendar app command" msgstr "" -#: bal/gui/qt/plugin.py:836 +#: bal/gui/qt/plugin.py:814 msgid "" "Command used to open .ics calendar files.\n" "Leave empty to use the system default (xdg-open/open/start).\n" "Only used in ADVANCED mode." msgstr "" -#: bal/gui/qt/plugin.py:849 +#: bal/gui/qt/plugin.py:828 msgid "Save inheritance transactions in history" msgstr "" -#: bal/gui/qt/plugin.py:851 -#, python-brace-format +#: bal/gui/qt/plugin.py:829 msgid "" "After each check, save the valid will transactions into the wallet's local history (the History tab), each with a label.\n" "The label may contain the variable:\n" @@ -1471,12 +1394,11 @@ msgid "" "Only used in ADVANCED mode." msgstr "" -#: bal/gui/qt/plugin.py:863 +#: bal/gui/qt/plugin.py:842 msgid "History label" msgstr "" -#: bal/gui/qt/plugin.py:865 -#, python-brace-format +#: bal/gui/qt/plugin.py:843 msgid "" "Label applied to the will transactions saved into the wallet's local history.\n" "Variables:\n" @@ -1484,61 +1406,48 @@ msgid "" "Only used in ADVANCED mode." msgstr "" -#: bal/gui/qt/plugin.py:887 +#: bal/gui/qt/plugin.py:866 msgid "Broadcast all transactions to willexecutors including those already pushed" msgstr "" -#: bal/gui/qt/plugin.py:896 +#: bal/gui/qt/plugin.py:875 msgid "Rebuild will on wallet close" msgstr "" -#: bal/gui/qt/plugin.py:898 +#: bal/gui/qt/plugin.py:876 msgid "" "Run the 'Build your will' wizard every time the wallet is closed or Electrum is quit, so the will is rebuilt and re-validated.\n" "When disabled, the will is only rebuilt when you press Check or Prepare. The last built state is still saved to the wallet." msgstr "" -#: bal/gui/qt/plugin.py:914 +#: bal/gui/qt/plugin.py:893 msgid "Rebuild automatically on new transactions" msgstr "" -#: bal/gui/qt/plugin.py:916 +#: bal/gui/qt/plugin.py:894 msgid "" "When a new transaction arrives for the wallet, automatically rebuild the will the same way the wizard does at wallet close: the delivery date is anticipated by one day so the new will replaces the previous one, and the rebuilt transactions are signed and sent to their will-executors.\n" "An on-chain invalidation transaction is only built when the anticipated delivery date would fall before the Check Alive threshold, or when the threshold is already in the past.\n" "When disabled (default), the will is only rebuilt on Check / Prepare / wallet close." msgstr "" -#: bal/gui/qt/plugin.py:938 -msgid "QR Code Size" -msgstr "" - -#: bal/gui/qt/plugin.py:940 -msgid "" -"Payload size of a single QR code when exporting a will via QR.\n" -"\n" -"Larger QR codes hold more data (fewer shots) but are easier to scan with a high-resolution camera; smaller QR codes scan fine even with low-resolution cameras but require more shots.\n" -"The same selector is available inside the export dialog." -msgstr "" - -#: bal/gui/qt/plugin.py:1021 +#: bal/gui/qt/plugin.py:976 msgid "Reset to Default Setting" msgstr "" -#: bal/gui/qt/plugin.py:1022 +#: bal/gui/qt/plugin.py:977 msgid "Reset these settings to their default values" msgstr "" -#: bal/gui/qt/plugin.py:1029 +#: bal/gui/qt/plugin.py:984 msgid "Open the Bitcoin After Life support website" msgstr "" -#: bal/gui/qt/plugin.py:1041 +#: bal/gui/qt/plugin.py:996 msgid "Support:" msgstr "" -#: bal/gui/qt/plugin.py:1098 -#, python-brace-format +#: bal/gui/qt/plugin.py:1053 msgid "BAL - {}" msgstr "" @@ -1584,7 +1493,7 @@ msgstr "" msgid "Miner fee, click for more information" msgstr "" -#: bal/gui/qt/widgets.py:214 +#: bal/gui/qt/widgets.py:213 msgid "" "if you choose Raw, you can insert various options based on suffix:\n" " - d: number of days after current day(ex: 1d means tomorrow)\n" @@ -1599,7 +1508,7 @@ msgstr "" msgid "Date" msgstr "" -#: bal/gui/qt/widgets.py:646 +#: bal/gui/qt/widgets.py:645 msgid "CHECK ALIVE

In DATA mode:
set the date for the “check alive” parameter.
When you open the wallet, if the “check alive” date has passed, the plugin will ask if you want to postpone the inheritance, since it assumes you still have control of the wallet and that you are still alive.

In RAW mode:
When less than this time is missing, ask to invalidate.
If you fail to invalidate during this time, your transactions will be delivered to your heirs.

if you choose Raw, you can insert various options based on suffix:
- d: number of days after current day(ex: 1d means tomorrow)
- y: number of years after current day(ex: 1y means one year from today)
The sign selects the reference date:
- a NEGATIVE value (ex: -30d) counts backwards from the delivery date (30 days before the delivery);
- a POSITIVE value (ex: 30d) counts forward from the inheritance creation date (30 days after the will was created).

Note: the date/time is expressed in your computer's Local time (not UTC time).
" msgstr "" @@ -1607,7 +1516,7 @@ msgstr "" msgid "Check Alive" msgstr "" -#: bal/gui/qt/widgets.py:687 +#: bal/gui/qt/widgets.py:686 msgid "DELIVERY TIME

Set Locktime for transactions.
Any time is needed transaction will be anticipated by 1day

(ONLY IN ADVANCED MODE)
if you choose Raw, you can insert various options based on suffix:
- d: number of days after current day(ex: 1d means tomorrow)
- y: number of years after currrent day(ex: 1y means one year from today)

Note: the date/time is expressed in your computer's Local time (not UTC time).
" msgstr "" @@ -1619,20 +1528,19 @@ msgstr "" msgid "Export reminder dates to your calendar (.ics)" msgstr "" -#: bal/gui/qt/widgets.py:855 +#: bal/gui/qt/widgets.py:854 msgid "" "Enter the date on which you want the inheritance (or backup)\n" "of your Electrum wallet to take effect." msgstr "" -#: bal/gui/qt/widgets.py:886 +#: bal/gui/qt/widgets.py:885 msgid "" "Please note: Do not reduce the miner fees\n" "unless you know what you’re doing" msgstr "" #: bal/gui/qt/widgets.py:1235 -#, python-brace-format msgid "{} or perc value" msgstr "" @@ -1704,12 +1612,11 @@ msgstr "" msgid "Heirs are not defined" msgstr "" -#: bal/gui/qt/window.py:839 -#, python-brace-format +#: bal/gui/qt/window.py:838 msgid "In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts.{}" msgstr "" -#: bal/gui/qt/window.py:857 +#: bal/gui/qt/window.py:856 msgid "CheckAlive is in the past please update it to a date in the future but less than locktime" msgstr "" @@ -1721,7 +1628,7 @@ msgstr "" msgid " no backup transaction or willexecutor selected" msgstr "" -#: bal/gui/qt/window.py:916 +#: bal/gui/qt/window.py:915 msgid "" "This inheritance was already signed/sent to will-executors and you are postponing it.\n" "\n" @@ -1746,7 +1653,7 @@ msgstr "" msgid "Txfees are changed" msgstr "" -#: bal/gui/qt/window.py:951 +#: bal/gui/qt/window.py:950 msgid "" "Found CHANGES to the DATE or the HEIRS,\n" "a NEW WILL must be prepared." @@ -1757,7 +1664,6 @@ msgid "will have to be built" msgstr "" #: bal/gui/qt/window.py:977 -#, python-brace-format msgid "" "Error:{}\n" " {}" @@ -1767,7 +1673,7 @@ msgstr "" msgid "Please, check your heirs, locktime and threshold!" msgstr "" -#: bal/gui/qt/window.py:994 +#: bal/gui/qt/window.py:993 msgid "" "Your inheritance has been rebuilt and now needs to be signed again.\n" "\n" @@ -1775,7 +1681,7 @@ msgid "" " 1. Press 'Sign' to sign the new transaction." msgstr "" -#: bal/gui/qt/window.py:1001 +#: bal/gui/qt/window.py:1000 msgid "" "Your inheritance has been rebuilt and now needs to be signed and re-sent to the will-executors.\n" "\n" @@ -1792,7 +1698,7 @@ msgstr "" msgid "no tx" msgstr "" -#: bal/gui/qt/window.py:1067 +#: bal/gui/qt/window.py:1066 msgid "Please sign and broadcast this transaction to invalidate current will" msgstr "" @@ -1805,12 +1711,10 @@ msgid "Calculating Transactions" msgstr "" #: bal/gui/qt/window.py:1107 -#, python-brace-format msgid "signed: {}" msgstr "" #: bal/gui/qt/window.py:1108 -#, python-brace-format msgid "signing: {}" msgstr "" @@ -1843,18 +1747,15 @@ msgid "checking..." msgstr "" #: bal/gui/qt/window.py:1690 -#, python-brace-format msgid "checking {} - {} : {}" msgstr "" #: bal/gui/qt/window.py:1699 -#, python-brace-format msgid "checked {} - {} : {}" msgstr "" #: bal/gui/qt/window.py:1895 bal/gui/qt/window.py:1933 #: bal/gui/qt/window.py:1957 -#, python-brace-format msgid "Invalid will file: {}" msgstr "" @@ -1870,14 +1771,14 @@ msgstr "" msgid "Check Transaction" msgstr "" -#: bal/gui/qt/window.py:2169 +#: bal/gui/qt/window.py:2168 msgid "" "Could not download the will-executors list.\n" "\n" "This is usually caused by your internet connection or a firewall, not by the plugin. Please check your connection (a VPN often helps) and try again." msgstr "" -#: bal/gui/qt/window.py:2179 +#: bal/gui/qt/window.py:2178 msgid "" "Could not download the will-executors list over Tor.\n" "\n" @@ -1888,16 +1789,14 @@ msgstr "" msgid "Downloading will-executors list..." msgstr "" -#: bal/gui/qt/window.py:2251 -#, python-brace-format +#: bal/gui/qt/window.py:2250 msgid "" "No active will-executor servers found for the {chain} network.\n" "\n" "The welist server at {url} responded but returned no will-executors for this chain." msgstr "" -#: bal/gui/qt/window.py:2259 -#, python-brace-format +#: bal/gui/qt/window.py:2258 msgid "" "Could not reach the configured welist server.\n" "\n" @@ -1907,8 +1806,7 @@ msgid "" "Please verify the welist server URL in the plugin settings." msgstr "" -#: bal/gui/qt/window.py:2269 -#, python-brace-format +#: bal/gui/qt/window.py:2268 msgid "No active will-executor found for the {chain} network." msgstr "" diff --git a/bal/locale/es_ES/LC_MESSAGES/bal.po b/bal/locale/es_ES/LC_MESSAGES/bal.po index e66fa96..c1a3d09 100644 --- a/bal/locale/es_ES/LC_MESSAGES/bal.po +++ b/bal/locale/es_ES/LC_MESSAGES/bal.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: BAL Electrum plugin VERSION\n" "Report-Msgid-Bugs-To: info@bitcoin-after.life\n" -"POT-Creation-Date: 2026-10-03 11:24-0400\n" +"POT-Creation-Date: 2026-10-03 12:24-0400\n" "PO-Revision-Date: 2026-10-03 11:40-0400\n" "Last-Translator: FULL NAME \n" "Language: es_ES\n" @@ -16,9 +16,9 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.18.0\n" +"Generated-By: Babel 2.10.3\n" -#: bal/core/plugin_base.py:361 +#: bal/core/plugin_base.py:360 msgid "" "BAL will execution of $wallet_name\r\n" " heirs list: \r\n" @@ -28,7 +28,7 @@ msgstr "" " lista de herederos: \r\n" "$heirs_complete" -#: bal/core/plugin_base.py:367 +#: bal/core/plugin_base.py:366 msgid "BAL -Will execution of $wallet_name" msgstr "BAL - Ejecución del testamento de $wallet_name" @@ -53,7 +53,6 @@ msgstr "Comprobado" msgid "Check Failed" msgstr "Comprobación fallida" -#. from Electrum es_ES #: bal/core/will.py:1345 msgid "Signed" msgstr "Firmado" @@ -62,13 +61,11 @@ msgstr "Firmado" msgid "Confirmed" msgstr "Confirmado" -#. from Electrum es_ES #: bal/core/will.py:1347 bal/gui/qt/lists.py:1354 bal/gui/qt/lists.py:1368 msgid "Error" msgstr "Error" -#. from Electrum es_ES -#: bal/core/will.py:1348 bal/gui/qt/dialogs.py:853 +#: bal/core/will.py:1348 bal/gui/qt/dialogs.py:824 msgid "Expired" msgstr "Caducado" @@ -84,7 +81,6 @@ msgstr "Importado" msgid "Invalidated" msgstr "Invalidado" -#. from Electrum es_ES #: bal/core/will.py:1352 msgid "Mempool" msgstr "Mempool" @@ -113,7 +109,7 @@ msgstr "Restaurado" msgid "Updated" msgstr "Actualizado" -#: bal/core/will.py:1359 bal/gui/qt/dialogs.py:2661 +#: bal/core/will.py:1359 bal/gui/qt/dialogs.py:2632 msgid "Valid" msgstr "Válido" @@ -125,13 +121,11 @@ msgstr "Nuevo" msgid "Waiting..." msgstr "Esperando..." -#. from Electrum es_ES #: bal/core/willexecutors.py:379 bal/gui/qt/window.py:245 #: bal/gui/qt/window.py:1663 msgid "Success" msgstr "Completado" -#. from Electrum es_ES #: bal/core/willexecutors.py:389 bal/gui/qt/window.py:1668 msgid "Failed" msgstr "Fallido" @@ -144,12 +138,11 @@ msgstr "Abrir" msgid "Open with..." msgstr "Abrir con..." -#. from Electrum es_ES #: bal/gui/qt/calendar.py:47 msgid "Save" msgstr "Guardar" -#: bal/gui/qt/calendar.py:73 bal/gui/qt/widgets.py:1044 +#: bal/gui/qt/calendar.py:72 bal/gui/qt/widgets.py:1043 msgid "No reminders were saved: the delivery date is too close (or already passed)" msgstr "No se guardó ningún recordatorio: la fecha de entrega está demasiado cerca (o ya ha pasado)" @@ -200,41 +193,37 @@ msgstr "Grande - ~900 bytes/QR" msgid "XL - ~1800 bytes/QR (high-res cameras)" msgstr "XL - ~1800 bytes/QR (cámaras de alta resolución)" -#. from Electrum es_ES #: bal/gui/qt/common.py:256 #, python-brace-format msgid "Select file to save your {}" msgstr "Seleccione el archivo para guardar su {}" -#. from Electrum es_ES #: bal/gui/qt/common.py:271 #, python-brace-format msgid "Your {0} were exported to '{1}'" msgstr "Sus {0} se exportaron a '{1}'" -#: bal/gui/qt/dialogs.py:217 +#: bal/gui/qt/dialogs.py:188 msgid "Bal Wizard Setup" msgstr "Configuración del asistente de BAL" -#: bal/gui/qt/dialogs.py:354 bal/gui/qt/dialogs.py:2790 +#: bal/gui/qt/dialogs.py:325 bal/gui/qt/dialogs.py:2760 msgid "Previous" msgstr "Anterior" -#. from Electrum es_ES -#: bal/gui/qt/dialogs.py:358 bal/gui/qt/dialogs.py:2793 +#: bal/gui/qt/dialogs.py:329 bal/gui/qt/dialogs.py:2763 msgid "Next" msgstr "Siguiente" -#. from Electrum es_ES -#: bal/gui/qt/dialogs.py:362 bal/gui/qt/dialogs.py:4107 +#: bal/gui/qt/dialogs.py:333 bal/gui/qt/dialogs.py:4019 msgid "Cancel" msgstr "Cancelar" -#: bal/gui/qt/dialogs.py:388 +#: bal/gui/qt/dialogs.py:359 msgid "Bitcoin After Life Heirs" msgstr "Bitcoin After Life - Herederos" -#: bal/gui/qt/dialogs.py:390 +#: bal/gui/qt/dialogs.py:360 #, python-format msgid "" "Please add your heirs\n" @@ -243,92 +232,88 @@ msgstr "" "Añade tus herederos\n" " recuerda que se gastará el 100% del saldo de la cartera" -#. from Electrum es_ES -#: bal/gui/qt/dialogs.py:398 bal/gui/qt/lists.py:1229 +#: bal/gui/qt/dialogs.py:369 bal/gui/qt/lists.py:1229 msgid "Add" msgstr "Añadir" -#. from Electrum es_ES -#: bal/gui/qt/dialogs.py:400 bal/gui/qt/dialogs.py:2622 bal/gui/qt/lists.py:311 +#: bal/gui/qt/dialogs.py:371 bal/gui/qt/dialogs.py:2593 bal/gui/qt/lists.py:311 #: bal/gui/qt/lists.py:673 bal/gui/qt/lists.py:1237 msgid "Import" msgstr "Importar" -#. from Electrum es_ES -#: bal/gui/qt/dialogs.py:402 bal/gui/qt/dialogs.py:2458 -#: bal/gui/qt/dialogs.py:2626 bal/gui/qt/lists.py:312 bal/gui/qt/lists.py:672 +#: bal/gui/qt/dialogs.py:373 bal/gui/qt/dialogs.py:2429 +#: bal/gui/qt/dialogs.py:2597 bal/gui/qt/lists.py:312 bal/gui/qt/lists.py:672 #: bal/gui/qt/lists.py:1249 msgid "Export" msgstr "Exportar" -#: bal/gui/qt/dialogs.py:427 bal/gui/qt/dialogs.py:535 +#: bal/gui/qt/dialogs.py:398 bal/gui/qt/dialogs.py:506 msgid "Bitcoin After Life Will-Executors" msgstr "Bitcoin After Life - Will-Executor" -#: bal/gui/qt/dialogs.py:428 +#: bal/gui/qt/dialogs.py:399 msgid "Choose willexecutors download method" msgstr "Elige el método de descarga de los Will-Executor" -#: bal/gui/qt/dialogs.py:435 +#: bal/gui/qt/dialogs.py:406 msgid "Automatically download and select willexecutors" msgstr "Descargar y seleccionar automáticamente los Will-Executor" -#: bal/gui/qt/dialogs.py:436 +#: bal/gui/qt/dialogs.py:407 msgid "Only download willexecutors list" msgstr "Solo descargar la lista de Will-Executor" -#: bal/gui/qt/dialogs.py:437 +#: bal/gui/qt/dialogs.py:408 msgid "Import willexecutor list from file" msgstr "Importar la lista de Will-Executor desde un archivo" -#: bal/gui/qt/dialogs.py:438 +#: bal/gui/qt/dialogs.py:409 msgid "Manual" msgstr "Manual" -#: bal/gui/qt/dialogs.py:467 bal/gui/qt/lists.py:1559 +#: bal/gui/qt/dialogs.py:438 bal/gui/qt/lists.py:1559 msgid "willexecutors" msgstr "Will-Executor" -#: bal/gui/qt/dialogs.py:536 +#: bal/gui/qt/dialogs.py:507 msgid "Configure and select your willexecutors" msgstr "Configura y selecciona tus Will-Executor" -#: bal/gui/qt/dialogs.py:555 +#: bal/gui/qt/dialogs.py:526 msgid "Bitcoin After Life Will Settings" msgstr "Bitcoin After Life - Configuración del testamento" -#. from Electrum es_ES -#: bal/gui/qt/dialogs.py:590 bal/gui/qt/dialogs.py:643 +#: bal/gui/qt/dialogs.py:561 bal/gui/qt/dialogs.py:614 msgid "Please wait" msgstr "Espere por favor" -#: bal/gui/qt/dialogs.py:673 +#: bal/gui/qt/dialogs.py:644 msgid "Building Will" msgstr "Creación del testamento" -#: bal/gui/qt/dialogs.py:678 +#: bal/gui/qt/dialogs.py:649 msgid "Building Will:" msgstr "Creación del testamento:" -#: bal/gui/qt/dialogs.py:759 bal/gui/qt/dialogs.py:784 -#: bal/gui/qt/dialogs.py:794 bal/gui/qt/dialogs.py:817 +#: bal/gui/qt/dialogs.py:730 bal/gui/qt/dialogs.py:755 +#: bal/gui/qt/dialogs.py:765 bal/gui/qt/dialogs.py:788 msgid "Checking variables" msgstr "Comprobando variables" -#: bal/gui/qt/dialogs.py:785 +#: bal/gui/qt/dialogs.py:756 msgid "Check Alive Threshold Passed: you have to Invalidate your old Will" msgstr "El umbral de verificación de vida se ha superado: debes invalidar tu antiguo testamento" -#: bal/gui/qt/dialogs.py:794 bal/gui/qt/dialogs.py:876 +#: bal/gui/qt/dialogs.py:765 bal/gui/qt/dialogs.py:847 msgid "No Heirs" msgstr "Sin herederos" -#: bal/gui/qt/dialogs.py:817 bal/gui/qt/dialogs.py:1364 -#: bal/gui/qt/dialogs.py:2310 bal/gui/qt/window.py:2315 +#: bal/gui/qt/dialogs.py:788 bal/gui/qt/dialogs.py:1335 +#: bal/gui/qt/dialogs.py:2281 bal/gui/qt/window.py:2315 msgid "Ok" msgstr "Aceptar" -#: bal/gui/qt/dialogs.py:822 +#: bal/gui/qt/dialogs.py:792 msgid "" "In the inheritance process, the entire wallet will always be fully emptied. \n" "Your settings require an adjustment of the amounts" @@ -336,109 +321,109 @@ msgstr "" "En el proceso de herencia, la cartera siempre se vaciará por completo. \n" "Tu configuración requiere un ajuste de los importes" -#: bal/gui/qt/dialogs.py:830 bal/gui/qt/window.py:844 +#: bal/gui/qt/dialogs.py:801 bal/gui/qt/window.py:844 #, python-brace-format msgid "Will-executor fee too high: {}" msgstr "Comisión del Will-Executor demasiado alta: {}" -#: bal/gui/qt/dialogs.py:864 +#: bal/gui/qt/dialogs.py:835 msgid "Postponed: invalidating old will" msgstr "Aplazado: invalidando el antiguo testamento" -#: bal/gui/qt/dialogs.py:899 bal/gui/qt/dialogs.py:908 -#: bal/gui/qt/dialogs.py:2270 +#: bal/gui/qt/dialogs.py:870 bal/gui/qt/dialogs.py:879 +#: bal/gui/qt/dialogs.py:2241 msgid "Checking your will" msgstr "Comprobando tu testamento" -#: bal/gui/qt/dialogs.py:901 +#: bal/gui/qt/dialogs.py:872 msgid "An inheritance of this wallet is already executed (on blockchain)" msgstr "Una herencia de esta cartera ya se ha ejecutado (en la blockchain)" -#: bal/gui/qt/dialogs.py:910 +#: bal/gui/qt/dialogs.py:881 msgid "Inheritance in mempool (waiting confirmation)" msgstr "Herencia en la mempool (esperando confirmación)" -#: bal/gui/qt/dialogs.py:943 +#: bal/gui/qt/dialogs.py:914 msgid "Will-Executor excluded" msgstr "Will-Executor excluido" -#: bal/gui/qt/dialogs.py:943 bal/gui/qt/dialogs.py:1037 -#: bal/gui/qt/dialogs.py:2251 bal/gui/qt/dialogs.py:2262 +#: bal/gui/qt/dialogs.py:914 bal/gui/qt/dialogs.py:1008 +#: bal/gui/qt/dialogs.py:2222 bal/gui/qt/dialogs.py:2233 msgid "Skipped" msgstr "Omitido" -#: bal/gui/qt/dialogs.py:949 bal/gui/qt/dialogs.py:1734 bal/gui/qt/lists.py:345 +#: bal/gui/qt/dialogs.py:920 bal/gui/qt/dialogs.py:1705 bal/gui/qt/lists.py:345 #: bal/gui/qt/theme.py:116 msgid "Will-Executor" msgstr "Will-Executor" -#: bal/gui/qt/dialogs.py:950 +#: bal/gui/qt/dialogs.py:921 msgid "Not present - select one or enable backup mode" msgstr "No presente - selecciona uno o activa el modo de copia de seguridad" -#: bal/gui/qt/dialogs.py:1012 +#: bal/gui/qt/dialogs.py:982 msgid "All heirs' shares are below the dust limit: the inheritance cannot be created. Increase the amounts or reduce the number of heirs." msgstr "Todas las partes de los herederos están por debajo del límite de polvo (dust): no se puede crear la herencia. Aumenta los importes o reduce el número de herederos." -#: bal/gui/qt/dialogs.py:1028 +#: bal/gui/qt/dialogs.py:998 #, python-brace-format msgid "Wallet balance is too low: {} satoshi available, but the miner and will-executor fees need {} satoshi (the minimum usable amount is {} satoshi). Add funds, or select fewer will-executors." msgstr "El saldo de la cartera es demasiado bajo: {} satoshis disponibles, pero las comisiones de minería y del Will-Executor necesitan {} satoshis (el importe mínimo utilizable es {} satoshis). Añade fondos o selecciona menos Will-Executor." -#: bal/gui/qt/dialogs.py:1071 +#: bal/gui/qt/dialogs.py:1042 #, python-brace-format msgid "Heir {}" msgstr "Heredero {}" -#: bal/gui/qt/dialogs.py:1073 +#: bal/gui/qt/dialogs.py:1044 #, python-brace-format msgid "{} is DUST - excluded (amount below dust limit)" msgstr "{} es POLVO (dust) - excluido (importe por debajo del límite de polvo)" -#: bal/gui/qt/dialogs.py:1129 +#: bal/gui/qt/dialogs.py:1100 msgid "Heir" msgstr "Heredero" -#: bal/gui/qt/dialogs.py:1246 bal/gui/qt/dialogs.py:2299 +#: bal/gui/qt/dialogs.py:1217 bal/gui/qt/dialogs.py:2270 #, python-brace-format msgid "Please wait {}secs" msgstr "Espera {} segundos" -#: bal/gui/qt/dialogs.py:1253 bal/gui/qt/dialogs.py:1305 -#: bal/gui/qt/dialogs.py:1343 +#: bal/gui/qt/dialogs.py:1224 bal/gui/qt/dialogs.py:1276 +#: bal/gui/qt/dialogs.py:1314 msgid "Broadcasting" msgstr "Transmitiendo" -#: bal/gui/qt/dialogs.py:1364 bal/gui/qt/window.py:2317 +#: bal/gui/qt/dialogs.py:1335 bal/gui/qt/window.py:2317 msgid "Ko" msgstr "Error" -#: bal/gui/qt/dialogs.py:1375 +#: bal/gui/qt/dialogs.py:1346 msgid "Timeout - no answer" msgstr "Tiempo agotado - sin respuesta" -#: bal/gui/qt/dialogs.py:1417 +#: bal/gui/qt/dialogs.py:1388 #, python-brace-format msgid "checking {} - {}" msgstr "comprobando {} - {}" -#: bal/gui/qt/dialogs.py:1420 bal/gui/qt/dialogs.py:2268 +#: bal/gui/qt/dialogs.py:1391 bal/gui/qt/dialogs.py:2239 #: bal/gui/qt/window.py:1691 msgid "Waiting" msgstr "Esperando" -#: bal/gui/qt/dialogs.py:1434 +#: bal/gui/qt/dialogs.py:1405 #, python-brace-format msgid "checked {} - {}" msgstr "comprobado {} - {}" -#: bal/gui/qt/dialogs.py:1484 bal/gui/qt/dialogs.py:2054 -#: bal/gui/qt/dialogs.py:2065 +#: bal/gui/qt/dialogs.py:1455 bal/gui/qt/dialogs.py:2025 +#: bal/gui/qt/dialogs.py:2036 #, python-brace-format msgid "Error: {}" msgstr "Error: {}" -#: bal/gui/qt/dialogs.py:1546 +#: bal/gui/qt/dialogs.py:1516 msgid "" "Your will has expired and must be invalidated before it can be rebuilt.\n" "A transaction window will now open:\n" @@ -450,11 +435,11 @@ msgstr "" "firma la transacción y luego TRANSMÍTELA para invalidar tu antiguo testamento.\n" "Cuando se confirme la invalidación, pulsa el botón Comprobar para terminar el testamento." -#: bal/gui/qt/dialogs.py:1575 bal/gui/qt/window.py:850 +#: bal/gui/qt/dialogs.py:1545 bal/gui/qt/window.py:849 msgid "Check Alive must be after today and before the delivery date, please update it" msgstr "La fecha de verificación de vida debe ser posterior a hoy y anterior a la fecha de entrega: actualízala" -#: bal/gui/qt/dialogs.py:1598 +#: bal/gui/qt/dialogs.py:1568 msgid "" "Your old will has been invalidated and the transaction was broadcast.\n" "Electrum may need a little time to register it.\n" @@ -464,19 +449,19 @@ msgstr "" "Electrum puede tardar un poco en registrarla.\n" "Espera a que se confirme la transacción de invalidación y vuelve a pulsar el botón Comprobar para terminar de actualizar tu testamento." -#: bal/gui/qt/dialogs.py:1613 +#: bal/gui/qt/dialogs.py:1584 msgid "Invalidate your old will" msgstr "Invalida tu antiguo testamento" -#: bal/gui/qt/dialogs.py:1627 bal/gui/qt/dialogs.py:1672 +#: bal/gui/qt/dialogs.py:1598 bal/gui/qt/dialogs.py:1643 msgid "Aborted" msgstr "Abortado" -#: bal/gui/qt/dialogs.py:1646 +#: bal/gui/qt/dialogs.py:1617 msgid "Auto-sign disabled — sign manually" msgstr "Firma automática desactivada — firma manualmente" -#: bal/gui/qt/dialogs.py:1660 +#: bal/gui/qt/dialogs.py:1630 msgid "" "The delivery date was automatically moved one day earlier so the updated will can correctly replace the previous one.\n" "Please sign (and broadcast) to confirm the change." @@ -484,135 +469,134 @@ msgstr "" "La fecha de entrega se ha adelantado automáticamente un día para que el testamento actualizado pueda sustituir correctamente al anterior.\n" "Firma (y transmite) para confirmar el cambio." -#: bal/gui/qt/dialogs.py:1669 +#: bal/gui/qt/dialogs.py:1640 msgid "Sign your will" msgstr "Firma tu testamento" -#: bal/gui/qt/dialogs.py:1674 bal/gui/qt/dialogs.py:1957 +#: bal/gui/qt/dialogs.py:1645 bal/gui/qt/dialogs.py:1928 msgid "Nothing to do" msgstr "Nada que hacer" -#: bal/gui/qt/dialogs.py:1693 +#: bal/gui/qt/dialogs.py:1664 msgid "Finished" msgstr "Terminado" -#: bal/gui/qt/dialogs.py:1710 +#: bal/gui/qt/dialogs.py:1681 msgid "Calendar" msgstr "Calendario" -#. from Electrum es_ES -#: bal/gui/qt/dialogs.py:1712 bal/gui/qt/dialogs.py:1742 -#: bal/gui/qt/dialogs.py:1809 bal/gui/qt/dialogs.py:2057 -#: bal/gui/qt/dialogs.py:3056 bal/gui/qt/dialogs.py:3295 -#: bal/gui/qt/dialogs.py:4126 +#: bal/gui/qt/dialogs.py:1683 bal/gui/qt/dialogs.py:1713 +#: bal/gui/qt/dialogs.py:1780 bal/gui/qt/dialogs.py:2028 +#: bal/gui/qt/dialogs.py:2968 bal/gui/qt/dialogs.py:3207 +#: bal/gui/qt/dialogs.py:4038 msgid "Close" msgstr "Cerrar" -#: bal/gui/qt/dialogs.py:1738 bal/gui/qt/dialogs.py:1805 +#: bal/gui/qt/dialogs.py:1709 bal/gui/qt/dialogs.py:1776 msgid "🔮 Wizard" msgstr "🔮 Asistente" -#: bal/gui/qt/dialogs.py:1801 bal/gui/qt/dialogs.py:2609 +#: bal/gui/qt/dialogs.py:1772 bal/gui/qt/dialogs.py:2580 msgid "Heirs" msgstr "Herederos" -#: bal/gui/qt/dialogs.py:1910 bal/gui/qt/widgets.py:1150 +#: bal/gui/qt/dialogs.py:1881 bal/gui/qt/widgets.py:1150 #, python-brace-format msgid "(reminder {idx}/{total})" msgstr "(recordatorio {idx}/{total})" -#: bal/gui/qt/dialogs.py:1974 +#: bal/gui/qt/dialogs.py:1945 msgid "All done" msgstr "Todo listo" -#: bal/gui/qt/dialogs.py:2026 +#: bal/gui/qt/dialogs.py:1996 msgid "Next steps (manual): press 'Sign' to sign your will, then 'Broadcast' to send it to the will-executors." msgstr "Siguientes pasos (manual): pulsa 'Firmar' para firmar tu testamento y luego 'Transmitir' para enviarlo a los Will-Executor." -#: bal/gui/qt/dialogs.py:2031 +#: bal/gui/qt/dialogs.py:2001 msgid "Next step (manual): press 'Sign' to sign your will." msgstr "Siguiente paso (manual): pulsa 'Firmar' para firmar tu testamento." -#: bal/gui/qt/dialogs.py:2035 +#: bal/gui/qt/dialogs.py:2005 msgid "Next step (manual): press 'Broadcast' to send your will to the will-executors." msgstr "Siguiente paso (manual): pulsa 'Transmitir' para enviar tu testamento a los Will-Executor." -#: bal/gui/qt/dialogs.py:2130 +#: bal/gui/qt/dialogs.py:2100 #, python-brace-format msgid "Miner fee rate changed (the will was built with {} sat/byte, now it is {}): a new will must be prepared." msgstr "La tasa de comisión de minería ha cambiado (el testamento se creó con {} sat/byte, ahora es {}): hay que preparar un nuevo testamento." -#: bal/gui/qt/dialogs.py:2135 +#: bal/gui/qt/dialogs.py:2105 msgid "The miner fee rate changed: a new will must be prepared." msgstr "La tasa de comisión de minería ha cambiado: hay que preparar un nuevo testamento." -#: bal/gui/qt/dialogs.py:2139 +#: bal/gui/qt/dialogs.py:2109 #, python-brace-format msgid "Will-executor \"{}\" is not covered by the current will: a new will must be prepared." msgstr "El Will-Executor \"{}\" no está cubierto por el testamento actual: hay que preparar un nuevo testamento." -#: bal/gui/qt/dialogs.py:2144 +#: bal/gui/qt/dialogs.py:2114 msgid "Backup mode is enabled but the will has no backup transaction: a new will must be prepared." msgstr "El modo de copia de seguridad está activado pero el testamento no tiene transacción de copia de seguridad: hay que preparar un nuevo testamento." -#: bal/gui/qt/dialogs.py:2153 +#: bal/gui/qt/dialogs.py:2123 #, python-brace-format msgid "Heir \"{}\" is not covered by the current will (it was added or removed, or its delivery date changed): a new will must be prepared." msgstr "El heredero \"{}\" no está cubierto por el testamento actual (se añadió o se eliminó, o cambió su fecha de entrega): hay que preparar un nuevo testamento." -#: bal/gui/qt/dialogs.py:2161 +#: bal/gui/qt/dialogs.py:2132 msgid "The heirs changed: a new will must be prepared." msgstr "Los herederos han cambiado: hay que preparar un nuevo testamento." -#: bal/gui/qt/dialogs.py:2163 +#: bal/gui/qt/dialogs.py:2134 msgid "A will-executor changed: a new will must be prepared." msgstr "Un Will-Executor ha cambiado: hay que preparar un nuevo testamento." -#: bal/gui/qt/dialogs.py:2173 +#: bal/gui/qt/dialogs.py:2143 msgid "The wallet contains funds that the current will does not cover yet: a new will must be prepared." msgstr "La cartera contiene fondos que el testamento actual aún no cubre: hay que preparar un nuevo testamento." -#: bal/gui/qt/dialogs.py:2177 +#: bal/gui/qt/dialogs.py:2147 msgid "The will contains no valid transaction: a new will must be prepared." msgstr "El testamento no contiene ninguna transacción válida: hay que preparar un nuevo testamento." -#: bal/gui/qt/dialogs.py:2183 +#: bal/gui/qt/dialogs.py:2153 msgid "The will is no longer coherent and must be rebuilt; the exact reason could not be determined." msgstr "El testamento ya no es coherente y debe recrearse; no se pudo determinar el motivo exacto." -#: bal/gui/qt/dialogs.py:2213 +#: bal/gui/qt/dialogs.py:2183 msgid "No heirs: add at least one heir before building the will." msgstr "Sin herederos: añade al menos un heredero antes de crear el testamento." -#: bal/gui/qt/dialogs.py:2216 +#: bal/gui/qt/dialogs.py:2186 msgid "The wallet has no spendable funds, so no inheritance transaction can be created." msgstr "La cartera no tiene fondos gastables, por lo que no se puede crear ninguna transacción de herencia." -#: bal/gui/qt/dialogs.py:2220 +#: bal/gui/qt/dialogs.py:2190 msgid "No usable will-executor: none of the servers in the list is both selected and valid. Open the will-executor settings, select at least one server and check that it is reachable." msgstr "No hay ningún Will-Executor utilizable: ninguno de los servidores de la lista está a la vez seleccionado y es válido. Abre la configuración de los Will-Executor, selecciona al menos un servidor y comprueba que es accesible." -#: bal/gui/qt/dialogs.py:2225 +#: bal/gui/qt/dialogs.py:2195 msgid "No delivery date left to build: every heir's date is already covered by the existing will. Choose a later delivery date, or change an heir's date." msgstr "No queda ninguna fecha de entrega para crear: la fecha de cada heredero ya está cubierta por el testamento existente. Elige una fecha de entrega posterior o cambia la fecha de un heredero." -#: bal/gui/qt/dialogs.py:2230 +#: bal/gui/qt/dialogs.py:2200 msgid "The amount to send must cover the miner fees plus this will-executor's fee, and the wallet balance is not enough: select cheaper will-executors, or add funds to the wallet." msgstr "El importe a enviar debe cubrir las comisiones de minería más la comisión de este Will-Executor, y el saldo de la cartera no es suficiente: selecciona Will-Executor más baratos o añade fondos a la cartera." -#: bal/gui/qt/dialogs.py:2235 +#: bal/gui/qt/dialogs.py:2205 msgid "A will-executor asks for more than the maximum fee you allowed: raise the maximum fee in the settings, or select a cheaper will-executor." msgstr "Un Will-Executor pide más que la comisión máxima que permitiste: sube la comisión máxima en la configuración o selecciona un Will-Executor más barato." -#: bal/gui/qt/dialogs.py:2240 +#: bal/gui/qt/dialogs.py:2210 msgid "The inheritance transactions could not be assembled from the available funds (the balance may not cover the miner fees)." msgstr "No se pudieron componer las transacciones de herencia con los fondos disponibles (el saldo puede no cubrir las comisiones de minería)." -#: bal/gui/qt/dialogs.py:2244 +#: bal/gui/qt/dialogs.py:2214 msgid "An unexpected error stopped the transactions being prepared for a will-executor, so it was skipped. Try again, or select a different will-executor." msgstr "Un error inesperado detuvo la preparación de las transacciones para un Will-Executor, así que se omitió. Inténtalo de nuevo o selecciona otro Will-Executor." -#: bal/gui/qt/dialogs.py:2255 +#: bal/gui/qt/dialogs.py:2225 msgid "" "Could not build the will, and the exact cause could not be determined. Please check that:\n" "- the wallet balance covers the miner and will-executor fees,\n" @@ -626,163 +610,127 @@ msgstr "" "- la fecha de verificación de vida es ANTERIOR a la fecha de entrega,\n" "- hay al menos un Will-Executor seleccionado y accesible." -#: bal/gui/qt/dialogs.py:2275 +#: bal/gui/qt/dialogs.py:2246 msgid "Invalidating old will" msgstr "Invalidando el antiguo testamento" -#: bal/gui/qt/dialogs.py:2281 +#: bal/gui/qt/dialogs.py:2252 msgid "Building your will" msgstr "Creando tu testamento" -#: bal/gui/qt/dialogs.py:2287 +#: bal/gui/qt/dialogs.py:2258 msgid "Signing your will" msgstr "Firmando tu testamento" -#: bal/gui/qt/dialogs.py:2293 +#: bal/gui/qt/dialogs.py:2264 msgid "Broadcasting your will to executors" msgstr "Transmitiendo tu testamento a los ejecutores" -#: bal/gui/qt/dialogs.py:2341 +#: bal/gui/qt/dialogs.py:2312 msgid "Wait" msgstr "Espera" -#: bal/gui/qt/dialogs.py:2444 +#: bal/gui/qt/dialogs.py:2415 msgid "Will Details" msgstr "Detalles del testamento" -#. from Electrum es_ES -#: bal/gui/qt/dialogs.py:2450 bal/gui/qt/dialogs.py:4101 +#: bal/gui/qt/dialogs.py:2421 bal/gui/qt/dialogs.py:4013 #: bal/gui/qt/lists.py:668 msgid "Sign" msgstr "Firmar" -#. from Electrum es_ES -#: bal/gui/qt/dialogs.py:2454 bal/gui/qt/lists.py:675 +#: bal/gui/qt/dialogs.py:2425 bal/gui/qt/lists.py:675 msgid "Broadcast" msgstr "Transmitir" -#: bal/gui/qt/dialogs.py:2461 bal/gui/qt/lists.py:677 +#: bal/gui/qt/dialogs.py:2432 bal/gui/qt/lists.py:677 msgid "Invalidate" msgstr "Invalidar" -#: bal/gui/qt/dialogs.py:2466 bal/gui/qt/lists.py:674 +#: bal/gui/qt/dialogs.py:2437 bal/gui/qt/lists.py:674 msgid "Merge" msgstr "Combinar" -#: bal/gui/qt/dialogs.py:2474 +#: bal/gui/qt/dialogs.py:2445 #, python-brace-format msgid "Expiration date: {}" msgstr "Fecha de caducidad: {}" -#: bal/gui/qt/dialogs.py:2480 +#: bal/gui/qt/dialogs.py:2451 #, python-brace-format msgid "Valid Txs:{}" msgstr "Transacciones válidas: {}" -#: bal/gui/qt/dialogs.py:2482 +#: bal/gui/qt/dialogs.py:2453 #, python-brace-format msgid "Total Txs:{}" msgstr "Transacciones totales: {}" -#: bal/gui/qt/dialogs.py:2536 +#: bal/gui/qt/dialogs.py:2507 msgid "Hide replaced" msgstr "Ocultar sustituidas" -#: bal/gui/qt/dialogs.py:2538 +#: bal/gui/qt/dialogs.py:2509 msgid "Unhide replaced" msgstr "Mostrar sustituidas" -#: bal/gui/qt/dialogs.py:2544 +#: bal/gui/qt/dialogs.py:2515 msgid "Hide invalidated" msgstr "Ocultar invalidadas" -#: bal/gui/qt/dialogs.py:2546 +#: bal/gui/qt/dialogs.py:2517 msgid "Unhide invalidated" msgstr "Mostrar invalidadas" -#: bal/gui/qt/dialogs.py:2571 +#: bal/gui/qt/dialogs.py:2542 msgid "Will-Executor Service List" msgstr "Lista de servicios Will-Executor" -#: bal/gui/qt/dialogs.py:2618 bal/gui/qt/lists.py:314 +#: bal/gui/qt/dialogs.py:2589 bal/gui/qt/lists.py:314 msgid "New Heir" msgstr "Nuevo heredero" -#. from Electrum es_ES -#: bal/gui/qt/dialogs.py:2660 +#: bal/gui/qt/dialogs.py:2631 msgid "All" msgstr "Todo" -#: bal/gui/qt/dialogs.py:2663 +#: bal/gui/qt/dialogs.py:2634 msgid "Valid NC" msgstr "Válido NC" -#: bal/gui/qt/dialogs.py:2801 bal/gui/qt/dialogs.py:2916 +#: bal/gui/qt/dialogs.py:2771 bal/gui/qt/dialogs.py:2847 msgid "Auto" msgstr "Automático" -#: bal/gui/qt/dialogs.py:2803 +#: bal/gui/qt/dialogs.py:2773 msgid "Automatically advance through the QR codes." msgstr "Avanza automáticamente por los códigos QR." -#: bal/gui/qt/dialogs.py:2807 +#: bal/gui/qt/dialogs.py:2777 msgid "QR codes per second:" msgstr "Códigos QR por segundo:" -#: bal/gui/qt/dialogs.py:2811 +#: bal/gui/qt/dialogs.py:2781 msgid " /s" msgstr " /s" -#: bal/gui/qt/dialogs.py:2813 +#: bal/gui/qt/dialogs.py:2783 msgid "Loop" msgstr "Bucle" -#: bal/gui/qt/dialogs.py:2816 +#: bal/gui/qt/dialogs.py:2785 msgid "When the last QR code is reached, keep cycling from the first one instead of stopping." msgstr "Cuando se alcanza el último código QR, sigue repitiendo desde el primero en lugar de detenerse." -#: bal/gui/qt/dialogs.py:2825 +#: bal/gui/qt/dialogs.py:2795 msgid "QR code size:" msgstr "Tamaño del código QR:" -#: bal/gui/qt/dialogs.py:2837 -msgid "Format:" -msgstr "Formato:" - -#: bal/gui/qt/dialogs.py:2843 -msgid "(default)" -msgstr "(predeterminado)" - -#: bal/gui/qt/dialogs.py:2910 +#: bal/gui/qt/dialogs.py:2841 msgid "Stop" msgstr "Detener" -#: bal/gui/qt/dialogs.py:2933 -msgid "Proprietary format; every other BAL wallet understands it." -msgstr "Formato propietario; cualquier otra cartera BAL lo entiende." - -#: bal/gui/qt/dialogs.py:2935 -msgid "Legacy BC-UR v1 (ur:bytes), compatible with older Blockchain Commons tools." -msgstr "BC-UR v1 heredado (ur:bytes), compatible con herramientas antiguas de Blockchain Commons." - -#: bal/gui/qt/dialogs.py:2937 -msgid "Standard BC-UR v2 fountain codes (ur:bytes)." -msgstr "Códigos fuente (fountain) BC-UR v2 estándar (ur:bytes)." - -#: bal/gui/qt/dialogs.py:2938 -msgid "Coinkite BBQR (B$ frames) for BitKit & friends." -msgstr "Coinkite BBQR (fotogramas B$) para BitKit y similares." - -#: bal/gui/qt/dialogs.py:2944 -#, python-brace-format -msgid "" -"Scan the QR codes below, in order, with the will-opening device.\n" -"Frame 1 of {} carries the total number of codes." -msgstr "" -"Escanea los códigos QR de abajo, en orden, con el dispositivo que abre el testamento.\n" -"El código 1 de {} indica el número total de códigos." - -#: bal/gui/qt/dialogs.py:2951 +#: bal/gui/qt/dialogs.py:2862 #, python-brace-format msgid "" "Scan the QR codes below with the will-opening device.\n" @@ -791,103 +739,101 @@ msgstr "" "Escanea los códigos QR de abajo con el dispositivo que abre el testamento.\n" "{} códigos contienen la transferencia completa (sirve cualquier orden y se ignoran los duplicados)." -#: bal/gui/qt/dialogs.py:2972 +#: bal/gui/qt/dialogs.py:2884 #, python-brace-format msgid "Frame {} of {}" msgstr "Código {} de {}" -#: bal/gui/qt/dialogs.py:2995 +#: bal/gui/qt/dialogs.py:2907 msgid "Export will" msgstr "Exportar testamento" -#: bal/gui/qt/dialogs.py:3005 +#: bal/gui/qt/dialogs.py:2917 msgid "No will transaction to export." msgstr "No hay ninguna transacción del testamento que exportar." -#: bal/gui/qt/dialogs.py:3012 +#: bal/gui/qt/dialogs.py:2924 msgid "Export:" msgstr "Exportar:" -#: bal/gui/qt/dialogs.py:3019 +#: bal/gui/qt/dialogs.py:2931 msgid "Whole will" msgstr "Testamento completo" -#: bal/gui/qt/dialogs.py:3023 +#: bal/gui/qt/dialogs.py:2934 msgid "Export the full will items (all details/statuses) as JSON, or only the serialized transactions." msgstr "Exporta los elementos completos del testamento (todos los detalles y estados) en JSON, o solo las transacciones serializadas." -#: bal/gui/qt/dialogs.py:3033 +#: bal/gui/qt/dialogs.py:2945 msgid "Send as:" msgstr "Enviar como:" -#. from Electrum es_ES -#: bal/gui/qt/dialogs.py:3035 bal/gui/qt/dialogs.py:3272 +#: bal/gui/qt/dialogs.py:2947 bal/gui/qt/dialogs.py:3184 msgid "File" msgstr "Archivo" -#: bal/gui/qt/dialogs.py:3036 bal/gui/qt/dialogs.py:3273 +#: bal/gui/qt/dialogs.py:2948 bal/gui/qt/dialogs.py:3185 msgid "QR Code" msgstr "Código QR" -#: bal/gui/qt/dialogs.py:3037 bal/gui/qt/dialogs.py:3274 +#: bal/gui/qt/dialogs.py:2949 bal/gui/qt/dialogs.py:3186 msgid "Audio" msgstr "Audio" -#: bal/gui/qt/dialogs.py:3079 +#: bal/gui/qt/dialogs.py:2990 msgid "Export the will as a JSON file. Uncheck \"Whole will\" above to export only the serialized transactions (comma-separated)." msgstr "Exporta el testamento como archivo JSON. Desmarca \"Testamento completo\" arriba para exportar solo las transacciones serializadas (separadas por comas)." -#: bal/gui/qt/dialogs.py:3087 +#: bal/gui/qt/dialogs.py:2999 msgid "Export…" msgstr "Exportar…" -#: bal/gui/qt/dialogs.py:3104 bal/gui/qt/dialogs.py:3330 +#: bal/gui/qt/dialogs.py:3016 bal/gui/qt/dialogs.py:3242 #: bal/gui/qt/window.py:1787 bal/gui/qt/window.py:1800 msgid "Audio MODEM plugin is not available." msgstr "El plugin del MÓDEM de audio no está disponible." -#: bal/gui/qt/dialogs.py:3108 bal/gui/qt/dialogs.py:3334 +#: bal/gui/qt/dialogs.py:3020 bal/gui/qt/dialogs.py:3246 msgid "Speed (KB/sec):" msgstr "Velocidad (KB/s):" -#. from Electrum es_ES -#: bal/gui/qt/dialogs.py:3119 +#: bal/gui/qt/dialogs.py:3031 msgid "Send" msgstr "Enviar" -#: bal/gui/qt/dialogs.py:3164 bal/gui/qt/dialogs.py:3198 -#: bal/gui/qt/dialogs.py:3223 +#: bal/gui/qt/dialogs.py:3076 bal/gui/qt/dialogs.py:3110 +#: bal/gui/qt/dialogs.py:3135 msgid "No will transaction matches the selected filter." msgstr "Ninguna transacción del testamento coincide con el filtro seleccionado." -#: bal/gui/qt/dialogs.py:3187 +#: bal/gui/qt/dialogs.py:3099 msgid "item(s)" msgstr "elemento(s)" -#: bal/gui/qt/dialogs.py:3187 +#: bal/gui/qt/dialogs.py:3099 msgid "transaction(s)" msgstr "transacción(es)" -#: bal/gui/qt/dialogs.py:3189 +#: bal/gui/qt/dialogs.py:3101 #, python-brace-format msgid "{} {} will be exported." msgstr "Se exportará: {} {}." -#: bal/gui/qt/dialogs.py:3192 +#: bal/gui/qt/dialogs.py:3104 #, python-brace-format msgid "{} {} will be sent." msgstr "Se enviará: {} {}." -#: bal/gui/qt/dialogs.py:3216 bal/gui/qt/dialogs.py:3219 -#: bal/gui/qt/dialogs.py:3370 bal/gui/qt/dialogs.py:3373 +#: bal/gui/qt/dialogs.py:3128 bal/gui/qt/dialogs.py:3131 +#: bal/gui/qt/dialogs.py:3282 bal/gui/qt/dialogs.py:3285 msgid "Invalid audio speed." msgstr "Velocidad de audio no válida." -#: bal/gui/qt/dialogs.py:3255 +#: bal/gui/qt/dialogs.py:3167 msgid "Import will" msgstr "Importar testamento" -#: bal/gui/qt/dialogs.py:3262 +#: bal/gui/qt/dialogs.py:3173 msgid "" "Choose how the will was exported: from a file, QR codes or audio.\n" "The import never touches the live will." @@ -895,59 +841,59 @@ msgstr "" "Elige cómo se exportó el testamento: desde un archivo, códigos QR o audio.\n" "La importación nunca afecta al testamento activo." -#: bal/gui/qt/dialogs.py:3270 +#: bal/gui/qt/dialogs.py:3182 msgid "Import from:" msgstr "Importar desde:" -#: bal/gui/qt/dialogs.py:3307 +#: bal/gui/qt/dialogs.py:3218 msgid "Import the JSON will file written by the Export ▶ File option. The will opens in a read-only preview window." msgstr "Importa el archivo JSON del testamento creado con la opción Exportar ▶ Archivo. El testamento se abre en una ventana de vista previa de solo lectura." -#: bal/gui/qt/dialogs.py:3313 +#: bal/gui/qt/dialogs.py:3225 msgid "Choose will file…" msgstr "Elige el archivo del testamento…" -#: bal/gui/qt/dialogs.py:3343 +#: bal/gui/qt/dialogs.py:3255 msgid "Waiting for the audio transfer…" msgstr "Esperando la transferencia de audio…" -#: bal/gui/qt/dialogs.py:3346 +#: bal/gui/qt/dialogs.py:3258 msgid "Receive by audio…" msgstr "Recibir por audio…" -#: bal/gui/qt/dialogs.py:3387 +#: bal/gui/qt/dialogs.py:3299 msgid "Receiving…" msgstr "Recibiendo…" -#: bal/gui/qt/dialogs.py:3407 +#: bal/gui/qt/dialogs.py:3319 msgid "No transaction data received." msgstr "No se recibió ningún dato de transacción." -#: bal/gui/qt/dialogs.py:3425 +#: bal/gui/qt/dialogs.py:3337 #, python-brace-format msgid "Waiting for audio ({:.1f} kbps)…" msgstr "Esperando audio ({:.1f} kbps)…" -#: bal/gui/qt/dialogs.py:3498 +#: bal/gui/qt/dialogs.py:3410 #, python-brace-format msgid "Could not parse a transferred will item: {}" msgstr "No se pudo interpretar un elemento del testamento transferido: {}" -#: bal/gui/qt/dialogs.py:3518 +#: bal/gui/qt/dialogs.py:3430 #, python-brace-format msgid "Could not parse a transferred transaction: {}" msgstr "No se pudo interpretar una transacción transferida: {}" -#: bal/gui/qt/dialogs.py:3530 +#: bal/gui/qt/dialogs.py:3441 msgid "The imported will contains no valid transaction in this wallet." msgstr "El testamento importado no contiene ninguna transacción válida en esta cartera." -#: bal/gui/qt/dialogs.py:3541 +#: bal/gui/qt/dialogs.py:3452 #, python-brace-format msgid "{} imported transaction(s) are not valid in this wallet and were skipped." msgstr "{} transacción(es) importada(s) no son válidas en esta cartera y se omitieron." -#: bal/gui/qt/dialogs.py:3641 +#: bal/gui/qt/dialogs.py:3552 msgid "" "Show the will QR codes to the camera, one after another.\n" "After the first code, every new code is captured automatically.\n" @@ -957,117 +903,114 @@ msgstr "" "Después del primer código, cada código nuevo se captura automáticamente.\n" "El primer código indica el número total de códigos; se ignoran los duplicados." -#: bal/gui/qt/dialogs.py:3650 bal/gui/qt/dialogs.py:3828 +#: bal/gui/qt/dialogs.py:3562 bal/gui/qt/dialogs.py:3740 msgid "Waiting for the first frame…" msgstr "Esperando el primer código…" -#: bal/gui/qt/dialogs.py:3667 +#: bal/gui/qt/dialogs.py:3579 msgid "…or paste/type the frame text here" msgstr "…o pega/escribe aquí el texto del código" -#: bal/gui/qt/dialogs.py:3671 +#: bal/gui/qt/dialogs.py:3583 msgid "Add frame" msgstr "Añadir código" -#: bal/gui/qt/dialogs.py:3677 bal/gui/qt/dialogs.py:3920 +#: bal/gui/qt/dialogs.py:3589 bal/gui/qt/dialogs.py:3832 msgid "Scan with camera…" msgstr "Escanear con la cámara…" -#: bal/gui/qt/dialogs.py:3680 +#: bal/gui/qt/dialogs.py:3591 msgid "Start the live camera. While it runs, every new QR code shown to the camera is captured automatically." msgstr "Inicia la cámara en vivo. Mientras esté en marcha, cada código QR nuevo que se muestre a la cámara se capturará automáticamente." -#. from Electrum es_ES -#: bal/gui/qt/dialogs.py:3686 +#: bal/gui/qt/dialogs.py:3598 msgid "Reset" msgstr "Reiniciar" -#: bal/gui/qt/dialogs.py:3693 +#: bal/gui/qt/dialogs.py:3605 msgid "Review and Sign…" msgstr "Revisar y firmar…" -#: bal/gui/qt/dialogs.py:3713 +#: bal/gui/qt/dialogs.py:3624 #, python-brace-format msgid "The scanned code belongs to a different transfer ({} frames, {}). The import was reset; scan the first code again." msgstr "El código escaneado pertenece a otra transferencia ({} códigos, {}). Se reinició la importación; escanea de nuevo el primer código." -#: bal/gui/qt/dialogs.py:3803 +#: bal/gui/qt/dialogs.py:3715 #, python-brace-format msgid "All {} frames stored." msgstr "Se guardaron los {} códigos." -#: bal/gui/qt/dialogs.py:3809 +#: bal/gui/qt/dialogs.py:3721 #, python-brace-format msgid "Stored {} of {} frames." msgstr "Guardados {} de {} códigos." -#. from Electrum es_ES -#: bal/gui/qt/dialogs.py:3874 +#: bal/gui/qt/dialogs.py:3786 msgid "Cannot start QR scanner, no usable camera found." msgstr "No se puede inciar scanner QR, no se encontró la camara." -#: bal/gui/qt/dialogs.py:3879 +#: bal/gui/qt/dialogs.py:3791 msgid "Stop scanning" msgstr "Detener escaneo" -#: bal/gui/qt/dialogs.py:3926 +#: bal/gui/qt/dialogs.py:3838 #, python-brace-format msgid "Camera error: {}" msgstr "Error de cámara: {}" -#: bal/gui/qt/dialogs.py:3973 +#: bal/gui/qt/dialogs.py:3884 #, python-brace-format msgid "The scanned code belongs to a different transfer ({} frames, {}). The import was reset; show the first code again." msgstr "El código escaneado pertenece a otra transferencia ({} códigos, {}). Se reinició la importación; muestra de nuevo el primer código." -#: bal/gui/qt/dialogs.py:4016 +#: bal/gui/qt/dialogs.py:3928 msgid "The transferred will contains no transactions." msgstr "El testamento transferido no contiene transacciones." -#: bal/gui/qt/dialogs.py:4045 +#: bal/gui/qt/dialogs.py:3957 msgid "Review and sign imported will" msgstr "Revisar y firmar el testamento importado" -#: bal/gui/qt/dialogs.py:4058 +#: bal/gui/qt/dialogs.py:3969 msgid "Enter your wallet password to sign the imported transactions." msgstr "Introduce la contraseña de tu cartera para firmar las transacciones importadas." -#: bal/gui/qt/dialogs.py:4104 +#: bal/gui/qt/dialogs.py:4016 msgid "Skip" msgstr "Omitir" -#: bal/gui/qt/dialogs.py:4120 +#: bal/gui/qt/dialogs.py:4032 msgid "Save signed file…" msgstr "Guardar archivo firmado…" -#: bal/gui/qt/dialogs.py:4123 +#: bal/gui/qt/dialogs.py:4035 msgid "Show signed QR…" msgstr "Mostrar QR firmado…" -#: bal/gui/qt/dialogs.py:4147 +#: bal/gui/qt/dialogs.py:4059 #, python-brace-format msgid "Transaction {} of {}" msgstr "Transacción {} de {}" -#: bal/gui/qt/dialogs.py:4149 +#: bal/gui/qt/dialogs.py:4061 #, python-brace-format msgid "TXID: {}" msgstr "TXID: {}" -#. from Electrum es_ES -#: bal/gui/qt/dialogs.py:4152 +#: bal/gui/qt/dialogs.py:4064 msgid "unknown" msgstr "desconocido" -#: bal/gui/qt/dialogs.py:4158 +#: bal/gui/qt/dialogs.py:4070 msgid "(no outputs)" msgstr "(sin salidas)" -#: bal/gui/qt/dialogs.py:4172 +#: bal/gui/qt/dialogs.py:4084 msgid "unknown (partial transaction)" msgstr "desconocida (transacción parcial)" -#: bal/gui/qt/dialogs.py:4174 +#: bal/gui/qt/dialogs.py:4086 #, python-brace-format msgid "" "Total outputs: {}\n" @@ -1076,7 +1019,7 @@ msgstr "" "Salidas totales: {}\n" "Comisión: {}" -#: bal/gui/qt/dialogs.py:4186 +#: bal/gui/qt/dialogs.py:4097 #, python-brace-format msgid "" "Signed {} of {} transactions.\n" @@ -1087,48 +1030,42 @@ msgstr "" "\n" "Guarda un archivo firmado para llevarlo al dispositivo de transmisión o muestra las transacciones firmadas como códigos QR." -#: bal/gui/qt/dialogs.py:4204 +#: bal/gui/qt/dialogs.py:4116 #, python-brace-format msgid "Could not sign the transaction: {}" msgstr "No se pudo firmar la transacción: {}" -#: bal/gui/qt/dialogs.py:4223 +#: bal/gui/qt/dialogs.py:4135 msgid "Signed will saved." msgstr "Testamento firmado guardado." -#: bal/gui/qt/dialogs.py:4234 +#: bal/gui/qt/dialogs.py:4146 msgid "No signed transaction to show." msgstr "No hay ninguna transacción firmada que mostrar." -#. from Electrum es_ES #: bal/gui/qt/lists.py:134 bal/gui/qt/window.py:370 msgid "Name" msgstr "Nombre" -#. from Electrum es_ES #: bal/gui/qt/lists.py:135 bal/gui/qt/lists.py:1432 bal/gui/qt/widgets.py:1420 #: bal/gui/qt/window.py:374 msgid "Address" msgstr "Dirección" -#. from Electrum es_ES #: bal/gui/qt/lists.py:136 bal/gui/qt/window.py:309 msgid "Amount" msgstr "Cantidad" -#. from Electrum es_ES #: bal/gui/qt/lists.py:232 #, python-brace-format msgid "Copy {}" msgstr "Copiar {}" -#. from Electrum es_ES #: bal/gui/qt/lists.py:240 bal/gui/qt/lists.py:1007 #, python-brace-format msgid "Edit {}" msgstr "Editar {}" -#. from Electrum es_ES #: bal/gui/qt/lists.py:243 msgid "Delete" msgstr "Eliminar" @@ -1145,12 +1082,10 @@ msgstr "Locktime" msgid "Txid" msgstr "Txid" -#. from Electrum es_ES #: bal/gui/qt/lists.py:346 bal/gui/qt/widgets.py:1372 msgid "Status" msgstr "Estado" -#. from Electrum es_ES #: bal/gui/qt/lists.py:347 msgid "Server" msgstr "Servidor" @@ -1191,7 +1126,6 @@ msgstr "eliminar" msgid "Transaction IDs" msgstr "ID de transacciones" -#. from Electrum es_ES #: bal/gui/qt/lists.py:510 msgid "Transactions" msgstr "Transacciones" @@ -1241,7 +1175,6 @@ msgstr "S" msgid "Base fee" msgstr "Comisión base" -#. from Electrum es_ES #: bal/gui/qt/lists.py:914 bal/gui/qt/lists.py:1414 msgid "Info" msgstr "Información" @@ -1250,7 +1183,6 @@ msgstr "Información" msgid "Default Address" msgstr "Dirección predeterminada" -#. from Electrum es_ES #: bal/gui/qt/lists.py:990 msgid "Copy" msgstr "Copiar" @@ -1379,69 +1311,67 @@ msgstr "Ningún Will-Executor coincide con el filtro seleccionado" msgid "No will-executor is selected" msgstr "No hay ningún Will-Executor seleccionado" -#. from Electrum es_ES -#: bal/gui/qt/plugin.py:140 +#: bal/gui/qt/plugin.py:139 msgid "Electrum Plugins" msgstr "Complementos de Electrum" -#: bal/gui/qt/plugin.py:307 +#: bal/gui/qt/plugin.py:306 msgid "Bitcoin After Life" msgstr "Bitcoin After Life" -#. from Electrum es_ES -#: bal/gui/qt/plugin.py:426 bal/gui/qt/plugin.py:451 +#: bal/gui/qt/plugin.py:425 bal/gui/qt/plugin.py:450 msgid "Settings" msgstr "Ajustes" -#: bal/gui/qt/plugin.py:568 +#: bal/gui/qt/plugin.py:551 msgid "BASIC" msgstr "BÁSICO" -#: bal/gui/qt/plugin.py:568 +#: bal/gui/qt/plugin.py:551 msgid "ADVANCED" msgstr "AVANZADO" -#: bal/gui/qt/plugin.py:588 +#: bal/gui/qt/plugin.py:571 msgid "Enable ADVANCED mode" msgstr "Activar el modo AVANZADO" -#: bal/gui/qt/plugin.py:589 +#: bal/gui/qt/plugin.py:572 msgid "Type 'at My Risk' to enable ADVANCED mode" msgstr "Escribe 'at My Risk' para activar el modo AVANZADO" -#: bal/gui/qt/plugin.py:658 +#: bal/gui/qt/plugin.py:641 msgid "Reset to default" msgstr "Restablecer a los valores predeterminados" -#: bal/gui/qt/plugin.py:680 +#: bal/gui/qt/plugin.py:659 msgid "Rebroadcast transactions" msgstr "Retransmitir transacciones" -#: bal/gui/qt/plugin.py:691 +#: bal/gui/qt/plugin.py:670 msgid "Warning: change these settings only if you know what you are doing." msgstr "Aviso: cambia estos ajustes solo si sabes lo que haces." -#: bal/gui/qt/plugin.py:706 +#: bal/gui/qt/plugin.py:685 msgid "Hide Replaced" msgstr "Ocultar sustituidas" -#: bal/gui/qt/plugin.py:709 +#: bal/gui/qt/plugin.py:688 msgid "Hide replaced transactions from will detail and list" msgstr "Oculta las transacciones sustituidas del detalle y la lista del testamento" -#: bal/gui/qt/plugin.py:714 +#: bal/gui/qt/plugin.py:693 msgid "Hide Invalidated" msgstr "Ocultar invalidadas" -#: bal/gui/qt/plugin.py:717 +#: bal/gui/qt/plugin.py:696 msgid "Hide invalidated transactions from will detail and list" msgstr "Oculta las transacciones invalidadas del detalle y la lista del testamento" -#: bal/gui/qt/plugin.py:720 +#: bal/gui/qt/plugin.py:699 msgid "Auto-sign on Check" msgstr "Firma automática al comprobar" -#: bal/gui/qt/plugin.py:722 +#: bal/gui/qt/plugin.py:700 msgid "" "When checking, automatically sign and broadcast the will transactions to their will-executors.\n" "The wallet password is requested only if the wallet is encrypted." @@ -1449,11 +1379,11 @@ msgstr "" "Al comprobar, firma y transmite automáticamente las transacciones del testamento a sus Will-Executor.\n" "La contraseña de la cartera solo se solicita si la cartera está cifrada." -#: bal/gui/qt/plugin.py:736 +#: bal/gui/qt/plugin.py:715 msgid "Panel editable Date and Fee" msgstr "Panel editable de fecha y comisión" -#: bal/gui/qt/plugin.py:740 +#: bal/gui/qt/plugin.py:718 msgid "" "When enabled, the delivery-time and check-alive date fields can be edited everywhere (toolbar / Heirs tab), not only in the will-building wizard.\n" "When disabled, those dates are display-only outside the wizard." @@ -1461,11 +1391,11 @@ msgstr "" "Si está activado, los campos de fecha de entrega y de verificación de vida se pueden editar en cualquier parte (barra de herramientas / pestaña Herederos), no solo en el asistente de creación del testamento.\n" "Si está desactivado, esas fechas son de solo lectura fuera del asistente." -#: bal/gui/qt/plugin.py:751 +#: bal/gui/qt/plugin.py:730 msgid "Max Will-Executor Fee (satoshi)" msgstr "Comisión máxima del Will-Executor (satoshi)" -#: bal/gui/qt/plugin.py:755 +#: bal/gui/qt/plugin.py:733 msgid "" "Maximum fee (in satoshi) allowed to be paid to a single will-executor. If a will-executor charges more than this, the will will not be built.\n" "Default: 500,000 satoshi (0.005 BTC)." @@ -1473,11 +1403,11 @@ msgstr "" "Comisión máxima (en satoshis) que se permite pagar a un único Will-Executor. Si un Will-Executor cobra más, el testamento no se creará.\n" "Predeterminado: 500 000 satoshis (0,005 BTC)." -#: bal/gui/qt/plugin.py:766 +#: bal/gui/qt/plugin.py:745 msgid "User Type" msgstr "Tipo de usuario" -#: bal/gui/qt/plugin.py:770 +#: bal/gui/qt/plugin.py:748 msgid "" "Choose how much detail the plugin shows.\n" "\n" @@ -1491,19 +1421,19 @@ msgstr "" "\n" "AVANZADO: muestra todos los controles, incluido el campo 'Verificación de vida' y el selector Raw/Fecha. Permite configurar la URL del servidor welist y mantiene todos los campos de fecha/comisión siempre editables." -#: bal/gui/qt/plugin.py:783 +#: bal/gui/qt/plugin.py:762 msgid "Number of reminders" msgstr "Número de recordatorios" -#: bal/gui/qt/plugin.py:785 +#: bal/gui/qt/plugin.py:763 msgid "How many reminder alarms the exported calendar (.ics) event contains. Range: 1 to 5 (default 3). Only used in ADVANCED mode." msgstr "Cuántas alarmas de recordatorio contiene el evento de calendario (.ics) exportado. Rango: de 1 a 5 (predeterminado 3). Solo se usa en modo AVANZADO." -#: bal/gui/qt/plugin.py:794 +#: bal/gui/qt/plugin.py:773 msgid "Event summary" msgstr "Resumen del evento" -#: bal/gui/qt/plugin.py:796 +#: bal/gui/qt/plugin.py:774 msgid "" "Default message to be used in event summary\n" "Variables:\n" @@ -1517,11 +1447,11 @@ msgstr "" " $heirs_complete: lista de herederos nombre,dirección,importe\n" "Solo se usa en modo AVANZADO." -#: bal/gui/qt/plugin.py:808 +#: bal/gui/qt/plugin.py:787 msgid "Event description" msgstr "Descripción del evento" -#: bal/gui/qt/plugin.py:810 +#: bal/gui/qt/plugin.py:788 msgid "" "Default message to be used in event description\n" "Variables:\n" @@ -1535,19 +1465,19 @@ msgstr "" " $heirs_complete: lista de herederos nombre,dirección,importe\n" "Solo se usa en modo AVANZADO." -#: bal/gui/qt/plugin.py:823 +#: bal/gui/qt/plugin.py:802 msgid "Welist Server URL" msgstr "URL del servidor welist" -#: bal/gui/qt/plugin.py:825 +#: bal/gui/qt/plugin.py:803 msgid "URL of the server that provides the will-executor list. Only available in ADVANCED mode." msgstr "URL del servidor que proporciona la lista de Will-Executor. Solo disponible en modo AVANZADO." -#: bal/gui/qt/plugin.py:834 +#: bal/gui/qt/plugin.py:813 msgid "Calendar app command" msgstr "Comando de la aplicación de calendario" -#: bal/gui/qt/plugin.py:836 +#: bal/gui/qt/plugin.py:814 msgid "" "Command used to open .ics calendar files.\n" "Leave empty to use the system default (xdg-open/open/start).\n" @@ -1557,11 +1487,11 @@ msgstr "" "Déjalo vacío para usar el predeterminado del sistema (xdg-open/open/start).\n" "Solo se usa en modo AVANZADO." -#: bal/gui/qt/plugin.py:849 +#: bal/gui/qt/plugin.py:828 msgid "Save inheritance transactions in history" msgstr "Guardar las transacciones de herencia en el historial" -#: bal/gui/qt/plugin.py:851 +#: bal/gui/qt/plugin.py:829 #, python-brace-format msgid "" "After each check, save the valid will transactions into the wallet's local history (the History tab), each with a label.\n" @@ -1574,11 +1504,11 @@ msgstr "" " {willexecutor}: se sustituye por la URL del Will-Executor del elemento\n" "Solo se usa en modo AVANZADO." -#: bal/gui/qt/plugin.py:863 +#: bal/gui/qt/plugin.py:842 msgid "History label" msgstr "Etiqueta del historial" -#: bal/gui/qt/plugin.py:865 +#: bal/gui/qt/plugin.py:843 #, python-brace-format msgid "" "Label applied to the will transactions saved into the wallet's local history.\n" @@ -1591,15 +1521,15 @@ msgstr "" " {willexecutor}: se sustituye por la URL del Will-Executor del elemento\n" "Solo se usa en modo AVANZADO." -#: bal/gui/qt/plugin.py:887 +#: bal/gui/qt/plugin.py:866 msgid "Broadcast all transactions to willexecutors including those already pushed" msgstr "Transmitir todas las transacciones a los Will-Executor, incluidas las ya enviadas" -#: bal/gui/qt/plugin.py:896 +#: bal/gui/qt/plugin.py:875 msgid "Rebuild will on wallet close" msgstr "Recrear el testamento al cerrar la cartera" -#: bal/gui/qt/plugin.py:898 +#: bal/gui/qt/plugin.py:876 msgid "" "Run the 'Build your will' wizard every time the wallet is closed or Electrum is quit, so the will is rebuilt and re-validated.\n" "When disabled, the will is only rebuilt when you press Check or Prepare. The last built state is still saved to the wallet." @@ -1607,11 +1537,11 @@ msgstr "" "Ejecuta el asistente 'Crea tu testamento' cada vez que se cierra la cartera o se sale de Electrum, para recrear y volver a validar el testamento.\n" "Si está desactivado, el testamento solo se recrea al pulsar Comprobar o Preparar. El último estado creado se guarda igualmente en la cartera." -#: bal/gui/qt/plugin.py:914 +#: bal/gui/qt/plugin.py:893 msgid "Rebuild automatically on new transactions" msgstr "Recrear automáticamente con nuevas transacciones" -#: bal/gui/qt/plugin.py:916 +#: bal/gui/qt/plugin.py:894 msgid "" "When a new transaction arrives for the wallet, automatically rebuild the will the same way the wizard does at wallet close: the delivery date is anticipated by one day so the new will replaces the previous one, and the rebuilt transactions are signed and sent to their will-executors.\n" "An on-chain invalidation transaction is only built when the anticipated delivery date would fall before the Check Alive threshold, or when the threshold is already in the past.\n" @@ -1621,39 +1551,23 @@ msgstr "" "Solo se crea una transacción de invalidación en cadena cuando la fecha de entrega adelantada quedaría antes del umbral de verificación de vida, o cuando el umbral ya está en el pasado.\n" "Si está desactivado (predeterminado), el testamento solo se recrea al comprobar / preparar / cerrar la cartera." -#: bal/gui/qt/plugin.py:938 -msgid "QR Code Size" -msgstr "Tamaño del código QR" - -#: bal/gui/qt/plugin.py:940 -msgid "" -"Payload size of a single QR code when exporting a will via QR.\n" -"\n" -"Larger QR codes hold more data (fewer shots) but are easier to scan with a high-resolution camera; smaller QR codes scan fine even with low-resolution cameras but require more shots.\n" -"The same selector is available inside the export dialog." -msgstr "" -"Tamaño de los datos de un único código QR al exportar un testamento por QR.\n" -"\n" -"Los códigos QR más grandes contienen más datos (menos tomas) pero son más fáciles de escanear con una cámara de alta resolución; los códigos más pequeños se escanean bien incluso con cámaras de baja resolución pero requieren más tomas.\n" -"El mismo selector está disponible dentro del diálogo de exportación." - -#: bal/gui/qt/plugin.py:1021 +#: bal/gui/qt/plugin.py:976 msgid "Reset to Default Setting" msgstr "Restablecer a los valores predeterminados" -#: bal/gui/qt/plugin.py:1022 +#: bal/gui/qt/plugin.py:977 msgid "Reset these settings to their default values" msgstr "Restablece estos ajustes a sus valores predeterminados" -#: bal/gui/qt/plugin.py:1029 +#: bal/gui/qt/plugin.py:984 msgid "Open the Bitcoin After Life support website" msgstr "Abrir el sitio web de soporte de Bitcoin After Life" -#: bal/gui/qt/plugin.py:1041 +#: bal/gui/qt/plugin.py:996 msgid "Support:" msgstr "Soporte:" -#: bal/gui/qt/plugin.py:1098 +#: bal/gui/qt/plugin.py:1053 #, python-brace-format msgid "BAL - {}" msgstr "BAL - {}" @@ -1702,7 +1616,7 @@ msgstr "" msgid "Miner fee, click for more information" msgstr "Comisión de minería, haz clic para más información" -#: bal/gui/qt/widgets.py:214 +#: bal/gui/qt/widgets.py:213 msgid "" "if you choose Raw, you can insert various options based on suffix:\n" " - d: number of days after current day(ex: 1d means tomorrow)\n" @@ -1716,12 +1630,11 @@ msgstr "" msgid "Raw" msgstr "Raw" -#. from Electrum es_ES #: bal/gui/qt/widgets.py:241 msgid "Date" msgstr "Fecha" -#: bal/gui/qt/widgets.py:646 +#: bal/gui/qt/widgets.py:645 msgid "CHECK ALIVE

In DATA mode:
set the date for the “check alive” parameter.
When you open the wallet, if the “check alive” date has passed, the plugin will ask if you want to postpone the inheritance, since it assumes you still have control of the wallet and that you are still alive.

In RAW mode:
When less than this time is missing, ask to invalidate.
If you fail to invalidate during this time, your transactions will be delivered to your heirs.

if you choose Raw, you can insert various options based on suffix:
- d: number of days after current day(ex: 1d means tomorrow)
- y: number of years after current day(ex: 1y means one year from today)
The sign selects the reference date:
- a NEGATIVE value (ex: -30d) counts backwards from the delivery date (30 days before the delivery);
- a POSITIVE value (ex: 30d) counts forward from the inheritance creation date (30 days after the will was created).

Note: the date/time is expressed in your computer's Local time (not UTC time).
" msgstr "VERIFICACIÓN DE VIDA

En modo DATA:
establece la fecha del parámetro “verificación de vida”.
Cuando abras la cartera, si la fecha de “verificación de vida” ya ha pasado, el plugin te preguntará si quieres posponer la herencia, porque asume que sigues teniendo el control de la cartera y que sigues vivo.

En modo RAW:
cuando falte menos de este tiempo, pide invalidar.
Si no invalidas durante este tiempo, tus transacciones se entregarán a tus herederos.

si eliges Raw, puedes introducir varias opciones según el sufijo:
- d: número de días a partir de hoy (ej.: 1d significa mañana)
- y: número de años a partir de hoy (ej.: 1y significa dentro de un año)
El signo selecciona la fecha de referencia:
- un valor NEGATIVO (ej.: -30d) cuenta hacia atrás desde la fecha de entrega (30 días antes de la entrega);
- un valor POSITIVO (ej.: 30d) cuenta hacia delante desde la fecha de creación de la herencia (30 días después de crear el testamento).

Nota: la fecha y la hora se expresan en la hora local de tu ordenador (no en hora UTC).
" @@ -1729,7 +1642,7 @@ msgstr "VERIFICACIÓN DE VIDA

En modo DATA:
establece la fecha msgid "Check Alive" msgstr "Verificación de vida" -#: bal/gui/qt/widgets.py:687 +#: bal/gui/qt/widgets.py:686 msgid "DELIVERY TIME

Set Locktime for transactions.
Any time is needed transaction will be anticipated by 1day

(ONLY IN ADVANCED MODE)
if you choose Raw, you can insert various options based on suffix:
- d: number of days after current day(ex: 1d means tomorrow)
- y: number of years after currrent day(ex: 1y means one year from today)

Note: the date/time is expressed in your computer's Local time (not UTC time).
" msgstr "FECHA DE ENTREGA

Establece el locktime de las transacciones.
Cuando haga falta, la transacción se adelanta 1 día.

(SOLO EN MODO AVANZADO)
si eliges Raw, puedes introducir varias opciones según el sufijo:
- d: número de días a partir de hoy (ej.: 1d significa mañana)
- y: número de años a partir de hoy (ej.: 1y significa dentro de un año)

Nota: la fecha y la hora se expresan en la hora local de tu ordenador (no en hora UTC).
" @@ -1741,7 +1654,7 @@ msgstr "Fecha de entrega, haz clic para más información" msgid "Export reminder dates to your calendar (.ics)" msgstr "Exportar las fechas de recordatorio a tu calendario (.ics)" -#: bal/gui/qt/widgets.py:855 +#: bal/gui/qt/widgets.py:854 msgid "" "Enter the date on which you want the inheritance (or backup)\n" "of your Electrum wallet to take effect." @@ -1749,7 +1662,7 @@ msgstr "" "Introduce la fecha en la que quieres que la herencia\n" "de tu cartera Electrum tenga efecto." -#: bal/gui/qt/widgets.py:886 +#: bal/gui/qt/widgets.py:885 msgid "" "Please note: Do not reduce the miner fees\n" "unless you know what you’re doing" @@ -1830,12 +1743,12 @@ msgstr "herederos" msgid "Heirs are not defined" msgstr "Los herederos no están definidos" -#: bal/gui/qt/window.py:839 +#: bal/gui/qt/window.py:838 #, python-brace-format msgid "In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts.{}" msgstr "En el proceso de herencia, la cartera siempre se vaciará por completo. Tu configuración requiere un ajuste de los importes.{}" -#: bal/gui/qt/window.py:857 +#: bal/gui/qt/window.py:856 msgid "CheckAlive is in the past please update it to a date in the future but less than locktime" msgstr "La fecha de verificación de vida está en el pasado: actualízala a una fecha futura pero anterior al locktime" @@ -1847,7 +1760,7 @@ msgstr "el locktime es anterior al umbral de verificación de vida" msgid " no backup transaction or willexecutor selected" msgstr " sin transacción de copia de seguridad ni Will-Executor seleccionado" -#: bal/gui/qt/window.py:916 +#: bal/gui/qt/window.py:915 msgid "" "This inheritance was already signed/sent to will-executors and you are postponing it.\n" "\n" @@ -1877,7 +1790,7 @@ msgstr "Will-Executor cambiado" msgid "Txfees are changed" msgstr "Las comisiones de transacción han cambiado" -#: bal/gui/qt/window.py:951 +#: bal/gui/qt/window.py:950 msgid "" "Found CHANGES to the DATE or the HEIRS,\n" "a NEW WILL must be prepared." @@ -1902,7 +1815,7 @@ msgstr "" msgid "Please, check your heirs, locktime and threshold!" msgstr "¡Comprueba tus herederos, el locktime y el umbral de verificación de vida!" -#: bal/gui/qt/window.py:994 +#: bal/gui/qt/window.py:993 msgid "" "Your inheritance has been rebuilt and now needs to be signed again.\n" "\n" @@ -1914,7 +1827,7 @@ msgstr "" "Siguiente paso (manual):\n" " 1. Pulsa 'Firmar' para firmar la nueva transacción." -#: bal/gui/qt/window.py:1001 +#: bal/gui/qt/window.py:1000 msgid "" "Your inheritance has been rebuilt and now needs to be signed and re-sent to the will-executors.\n" "\n" @@ -1928,7 +1841,6 @@ msgstr "" " 1. Pulsa 'Firmar' para firmar la nueva transacción.\n" " 2. Pulsa 'Transmitir' para enviarla a los Will-Executor." -#. from Electrum es_ES #: bal/gui/qt/window.py:1038 msgid "Electrum was unable to deserialize the transaction:" msgstr "Electrum no ha podido analizar la transacción:" @@ -1937,7 +1849,7 @@ msgstr "Electrum no ha podido analizar la transacción:" msgid "no tx" msgstr "sin transacciones" -#: bal/gui/qt/window.py:1067 +#: bal/gui/qt/window.py:1066 msgid "Please sign and broadcast this transaction to invalidate current will" msgstr "Firma y transmite esta transacción para invalidar el testamento actual" @@ -2015,7 +1927,7 @@ msgstr "Comprobando transacciones" msgid "Check Transaction" msgstr "Comprobar transacción" -#: bal/gui/qt/window.py:2169 +#: bal/gui/qt/window.py:2168 msgid "" "Could not download the will-executors list.\n" "\n" @@ -2025,7 +1937,7 @@ msgstr "" "\n" "Normalmente la causa es tu conexión a internet o un cortafuegos, no el plugin. Comprueba tu conexión (una VPN suele ayudar) e inténtalo de nuevo." -#: bal/gui/qt/window.py:2179 +#: bal/gui/qt/window.py:2178 msgid "" "Could not download the will-executors list over Tor.\n" "\n" @@ -2039,7 +1951,7 @@ msgstr "" msgid "Downloading will-executors list..." msgstr "Descargando la lista de Will-Executor..." -#: bal/gui/qt/window.py:2251 +#: bal/gui/qt/window.py:2250 #, python-brace-format msgid "" "No active will-executor servers found for the {chain} network.\n" @@ -2050,7 +1962,7 @@ msgstr "" "\n" "El servidor welist de {url} respondió pero no devolvió ningún Will-Executor para esta cadena." -#: bal/gui/qt/window.py:2259 +#: bal/gui/qt/window.py:2258 #, python-brace-format msgid "" "Could not reach the configured welist server.\n" @@ -2067,7 +1979,7 @@ msgstr "" "\n" "Comprueba la URL del servidor welist en la configuración del plugin." -#: bal/gui/qt/window.py:2269 +#: bal/gui/qt/window.py:2268 #, python-brace-format msgid "No active will-executor found for the {chain} network." msgstr "No se encontró ningún Will-Executor activo para la red {chain}." @@ -2080,3 +1992,42 @@ msgstr "Ping a los Will-Executor:" msgid "Ping Will-Executors" msgstr "Ping a los Will-Executor" +#~ msgid "Format:" +#~ msgstr "Formato:" + +#~ msgid "(default)" +#~ msgstr "(predeterminado)" + +#~ msgid "Proprietary format; every other BAL wallet understands it." +#~ msgstr "Formato propietario; cualquier otra cartera BAL lo entiende." + +#~ msgid "Legacy BC-UR v1 (ur:bytes), compatible with older Blockchain Commons tools." +#~ msgstr "BC-UR v1 heredado (ur:bytes), compatible con herramientas antiguas de Blockchain Commons." + +#~ msgid "Standard BC-UR v2 fountain codes (ur:bytes)." +#~ msgstr "Códigos fuente (fountain) BC-UR v2 estándar (ur:bytes)." + +#~ msgid "Coinkite BBQR (B$ frames) for BitKit & friends." +#~ msgstr "Coinkite BBQR (fotogramas B$) para BitKit y similares." + +#~ msgid "" +#~ "Scan the QR codes below, in order, with the will-opening device.\n" +#~ "Frame 1 of {} carries the total number of codes." +#~ msgstr "" +#~ "Escanea los códigos QR de abajo, en orden, con el dispositivo que abre el testamento.\n" +#~ "El código 1 de {} indica el número total de códigos." + +#~ msgid "QR Code Size" +#~ msgstr "Tamaño del código QR" + +#~ msgid "" +#~ "Payload size of a single QR code when exporting a will via QR.\n" +#~ "\n" +#~ "Larger QR codes hold more data (fewer shots) but are easier to scan with a high-resolution camera; smaller QR codes scan fine even with low-resolution cameras but require more shots.\n" +#~ "The same selector is available inside the export dialog." +#~ msgstr "" +#~ "Tamaño de los datos de un único código QR al exportar un testamento por QR.\n" +#~ "\n" +#~ "Los códigos QR más grandes contienen más datos (menos tomas) pero son más fáciles de escanear con una cámara de alta resolución; los códigos más pequeños se escanean bien incluso con cámaras de baja resolución pero requieren más tomas.\n" +#~ "El mismo selector está disponible dentro del diálogo de exportación." + diff --git a/bal/locale/it_IT/LC_MESSAGES/bal.po b/bal/locale/it_IT/LC_MESSAGES/bal.po index 2dbac83..0f95392 100644 --- a/bal/locale/it_IT/LC_MESSAGES/bal.po +++ b/bal/locale/it_IT/LC_MESSAGES/bal.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: BAL Electrum plugin VERSION\n" "Report-Msgid-Bugs-To: info@bitcoin-after.life\n" -"POT-Creation-Date: 2026-10-03 11:24-0400\n" +"POT-Creation-Date: 2026-10-03 12:24-0400\n" "PO-Revision-Date: 2026-09-26 22:02+0200\n" "Last-Translator: FULL NAME \n" "Language: it_IT\n" @@ -16,9 +16,9 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.18.0\n" +"Generated-By: Babel 2.10.3\n" -#: bal/core/plugin_base.py:361 +#: bal/core/plugin_base.py:360 msgid "" "BAL will execution of $wallet_name\r\n" " heirs list: \r\n" @@ -28,7 +28,7 @@ msgstr "" " elenco eredi: \r\n" "$heirs_complete" -#: bal/core/plugin_base.py:367 +#: bal/core/plugin_base.py:366 msgid "BAL -Will execution of $wallet_name" msgstr "BAL - Esecuzione del testamento di $wallet_name" @@ -53,7 +53,6 @@ msgstr "Verificato" msgid "Check Failed" msgstr "Verifica fallita" -#. from Electrum it_IT #: bal/core/will.py:1345 msgid "Signed" msgstr "Firmato" @@ -62,13 +61,11 @@ msgstr "Firmato" msgid "Confirmed" msgstr "Confermato" -#. from Electrum it_IT #: bal/core/will.py:1347 bal/gui/qt/lists.py:1354 bal/gui/qt/lists.py:1368 msgid "Error" msgstr "Errore" -#. from Electrum it_IT -#: bal/core/will.py:1348 bal/gui/qt/dialogs.py:853 +#: bal/core/will.py:1348 bal/gui/qt/dialogs.py:824 msgid "Expired" msgstr "Scaduto" @@ -84,7 +81,6 @@ msgstr "Importato" msgid "Invalidated" msgstr "Invalidato" -#. from Electrum it_IT #: bal/core/will.py:1352 msgid "Mempool" msgstr "Mempool" @@ -113,11 +109,10 @@ msgstr "Ripristinato" msgid "Updated" msgstr "Aggiornato" -#: bal/core/will.py:1359 bal/gui/qt/dialogs.py:2661 +#: bal/core/will.py:1359 bal/gui/qt/dialogs.py:2632 msgid "Valid" msgstr "Valido" -#. from Electrum it_IT #: bal/core/will.py:1365 bal/gui/qt/window.py:554 msgid "New" msgstr "Nuovo" @@ -126,13 +121,11 @@ msgstr "Nuovo" msgid "Waiting..." msgstr "In attesa..." -#. from Electrum it_IT #: bal/core/willexecutors.py:379 bal/gui/qt/window.py:245 #: bal/gui/qt/window.py:1663 msgid "Success" msgstr "Riuscito" -#. from Electrum it_IT #: bal/core/willexecutors.py:389 bal/gui/qt/window.py:1668 msgid "Failed" msgstr "Fallito" @@ -145,12 +138,11 @@ msgstr "Apri" msgid "Open with..." msgstr "Apri con..." -#. from Electrum it_IT #: bal/gui/qt/calendar.py:47 msgid "Save" msgstr "Salva" -#: bal/gui/qt/calendar.py:73 bal/gui/qt/widgets.py:1044 +#: bal/gui/qt/calendar.py:72 bal/gui/qt/widgets.py:1043 msgid "No reminders were saved: the delivery date is too close (or already passed)" msgstr "Nessun promemoria salvato: la data di consegna è troppo vicina (o già passata)" @@ -201,41 +193,37 @@ msgstr "Grande - ~900 byte/QR" msgid "XL - ~1800 bytes/QR (high-res cameras)" msgstr "XL - ~1800 byte/QR (fotocamere ad alta risoluzione)" -#. from Electrum it_IT #: bal/gui/qt/common.py:256 #, python-brace-format msgid "Select file to save your {}" msgstr "Seleziona file per salvare {}" -#. from Electrum it_IT #: bal/gui/qt/common.py:271 #, python-brace-format msgid "Your {0} were exported to '{1}'" msgstr "Il tuo {0} è stato esportato a '{1}'" -#: bal/gui/qt/dialogs.py:217 +#: bal/gui/qt/dialogs.py:188 msgid "Bal Wizard Setup" msgstr "Bal - Configurazione guidata" -#: bal/gui/qt/dialogs.py:354 bal/gui/qt/dialogs.py:2790 +#: bal/gui/qt/dialogs.py:325 bal/gui/qt/dialogs.py:2760 msgid "Previous" msgstr "Indietro" -#. from Electrum it_IT -#: bal/gui/qt/dialogs.py:358 bal/gui/qt/dialogs.py:2793 +#: bal/gui/qt/dialogs.py:329 bal/gui/qt/dialogs.py:2763 msgid "Next" msgstr "Avanti" -#. from Electrum it_IT -#: bal/gui/qt/dialogs.py:362 bal/gui/qt/dialogs.py:4107 +#: bal/gui/qt/dialogs.py:333 bal/gui/qt/dialogs.py:4019 msgid "Cancel" msgstr "Annulla" -#: bal/gui/qt/dialogs.py:388 +#: bal/gui/qt/dialogs.py:359 msgid "Bitcoin After Life Heirs" msgstr "Bitcoin After Life - Eredi" -#: bal/gui/qt/dialogs.py:390 +#: bal/gui/qt/dialogs.py:360 #, python-format msgid "" "Please add your heirs\n" @@ -244,92 +232,88 @@ msgstr "" "Aggiungi i tuoi eredi\n" " ricorda che verrà speso il 100% del saldo del portafogli" -#: bal/gui/qt/dialogs.py:398 bal/gui/qt/lists.py:1229 +#: bal/gui/qt/dialogs.py:369 bal/gui/qt/lists.py:1229 msgid "Add" msgstr "Aggiungi" -#. from Electrum it_IT -#: bal/gui/qt/dialogs.py:400 bal/gui/qt/dialogs.py:2622 bal/gui/qt/lists.py:311 +#: bal/gui/qt/dialogs.py:371 bal/gui/qt/dialogs.py:2593 bal/gui/qt/lists.py:311 #: bal/gui/qt/lists.py:673 bal/gui/qt/lists.py:1237 msgid "Import" msgstr "Importa" -#. from Electrum it_IT -#: bal/gui/qt/dialogs.py:402 bal/gui/qt/dialogs.py:2458 -#: bal/gui/qt/dialogs.py:2626 bal/gui/qt/lists.py:312 bal/gui/qt/lists.py:672 +#: bal/gui/qt/dialogs.py:373 bal/gui/qt/dialogs.py:2429 +#: bal/gui/qt/dialogs.py:2597 bal/gui/qt/lists.py:312 bal/gui/qt/lists.py:672 #: bal/gui/qt/lists.py:1249 msgid "Export" msgstr "Esporta" -#: bal/gui/qt/dialogs.py:427 bal/gui/qt/dialogs.py:535 +#: bal/gui/qt/dialogs.py:398 bal/gui/qt/dialogs.py:506 msgid "Bitcoin After Life Will-Executors" msgstr "Bitcoin After Life - Will-Executor" -#: bal/gui/qt/dialogs.py:428 +#: bal/gui/qt/dialogs.py:399 msgid "Choose willexecutors download method" msgstr "Scegli come scaricare i Will-Executor" -#: bal/gui/qt/dialogs.py:435 +#: bal/gui/qt/dialogs.py:406 msgid "Automatically download and select willexecutors" msgstr "Scarica e seleziona automaticamente i Will-Executor" -#: bal/gui/qt/dialogs.py:436 +#: bal/gui/qt/dialogs.py:407 msgid "Only download willexecutors list" msgstr "Scarica solo l'elenco dei Will-Executor" -#: bal/gui/qt/dialogs.py:437 +#: bal/gui/qt/dialogs.py:408 msgid "Import willexecutor list from file" msgstr "Importa l'elenco dei Will-Executor da file" -#: bal/gui/qt/dialogs.py:438 +#: bal/gui/qt/dialogs.py:409 msgid "Manual" msgstr "Manuale" -#: bal/gui/qt/dialogs.py:467 bal/gui/qt/lists.py:1559 +#: bal/gui/qt/dialogs.py:438 bal/gui/qt/lists.py:1559 msgid "willexecutors" msgstr "Will-Executor" -#: bal/gui/qt/dialogs.py:536 +#: bal/gui/qt/dialogs.py:507 msgid "Configure and select your willexecutors" msgstr "Configura e seleziona i tuoi Will-Executor" -#: bal/gui/qt/dialogs.py:555 +#: bal/gui/qt/dialogs.py:526 msgid "Bitcoin After Life Will Settings" msgstr "Bitcoin After Life - Impostazioni del testamento" -#. from Electrum it_IT -#: bal/gui/qt/dialogs.py:590 bal/gui/qt/dialogs.py:643 +#: bal/gui/qt/dialogs.py:561 bal/gui/qt/dialogs.py:614 msgid "Please wait" msgstr "Rimani in attesa" -#: bal/gui/qt/dialogs.py:673 +#: bal/gui/qt/dialogs.py:644 msgid "Building Will" msgstr "Preparazione del testamento" -#: bal/gui/qt/dialogs.py:678 +#: bal/gui/qt/dialogs.py:649 msgid "Building Will:" msgstr "Preparazione del testamento:" -#: bal/gui/qt/dialogs.py:759 bal/gui/qt/dialogs.py:784 -#: bal/gui/qt/dialogs.py:794 bal/gui/qt/dialogs.py:817 +#: bal/gui/qt/dialogs.py:730 bal/gui/qt/dialogs.py:755 +#: bal/gui/qt/dialogs.py:765 bal/gui/qt/dialogs.py:788 msgid "Checking variables" msgstr "Controllo dei parametri" -#: bal/gui/qt/dialogs.py:785 +#: bal/gui/qt/dialogs.py:756 msgid "Check Alive Threshold Passed: you have to Invalidate your old Will" msgstr "La data della verifica se sei vivo è passata: devi invalidare il piano del vecchio testamento" -#: bal/gui/qt/dialogs.py:794 bal/gui/qt/dialogs.py:876 +#: bal/gui/qt/dialogs.py:765 bal/gui/qt/dialogs.py:847 msgid "No Heirs" msgstr "Nessun erede" -#. from Electrum it_IT -#: bal/gui/qt/dialogs.py:817 bal/gui/qt/dialogs.py:1364 -#: bal/gui/qt/dialogs.py:2310 bal/gui/qt/window.py:2315 +#: bal/gui/qt/dialogs.py:788 bal/gui/qt/dialogs.py:1335 +#: bal/gui/qt/dialogs.py:2281 bal/gui/qt/window.py:2315 msgid "Ok" msgstr "OK" -#: bal/gui/qt/dialogs.py:822 +#: bal/gui/qt/dialogs.py:792 msgid "" "In the inheritance process, the entire wallet will always be fully emptied. \n" "Your settings require an adjustment of the amounts" @@ -337,110 +321,109 @@ msgstr "" "Nel processo di eredità il portafogli viene sempre svuotato del tutto. \n" "Le tue impostazioni richiedono una correzione degli importi" -#: bal/gui/qt/dialogs.py:830 bal/gui/qt/window.py:844 +#: bal/gui/qt/dialogs.py:801 bal/gui/qt/window.py:844 #, python-brace-format msgid "Will-executor fee too high: {}" msgstr "Commissione del Will-Executor troppo alta: {}" -#: bal/gui/qt/dialogs.py:864 +#: bal/gui/qt/dialogs.py:835 msgid "Postponed: invalidating old will" msgstr "Posticipato: invalidazione del piano del vecchio testamento" -#: bal/gui/qt/dialogs.py:899 bal/gui/qt/dialogs.py:908 -#: bal/gui/qt/dialogs.py:2270 +#: bal/gui/qt/dialogs.py:870 bal/gui/qt/dialogs.py:879 +#: bal/gui/qt/dialogs.py:2241 msgid "Checking your will" msgstr "Controllo del testamento" -#: bal/gui/qt/dialogs.py:901 +#: bal/gui/qt/dialogs.py:872 msgid "An inheritance of this wallet is already executed (on blockchain)" msgstr "Un'eredità di questo portafogli è già stata eseguita (sulla blockchain)" -#: bal/gui/qt/dialogs.py:910 +#: bal/gui/qt/dialogs.py:881 msgid "Inheritance in mempool (waiting confirmation)" msgstr "Eredità nella mempool (in attesa di conferma)" -#: bal/gui/qt/dialogs.py:943 +#: bal/gui/qt/dialogs.py:914 msgid "Will-Executor excluded" msgstr "Will-Executor escluso" -#: bal/gui/qt/dialogs.py:943 bal/gui/qt/dialogs.py:1037 -#: bal/gui/qt/dialogs.py:2251 bal/gui/qt/dialogs.py:2262 +#: bal/gui/qt/dialogs.py:914 bal/gui/qt/dialogs.py:1008 +#: bal/gui/qt/dialogs.py:2222 bal/gui/qt/dialogs.py:2233 msgid "Skipped" msgstr "Saltato" -#: bal/gui/qt/dialogs.py:949 bal/gui/qt/dialogs.py:1734 bal/gui/qt/lists.py:345 +#: bal/gui/qt/dialogs.py:920 bal/gui/qt/dialogs.py:1705 bal/gui/qt/lists.py:345 #: bal/gui/qt/theme.py:116 msgid "Will-Executor" msgstr "Will-Executor" -#: bal/gui/qt/dialogs.py:950 +#: bal/gui/qt/dialogs.py:921 msgid "Not present - select one or enable backup mode" msgstr "Assente - selezionane uno o attiva la modalità 'transazione senza Will-Executor'" -#: bal/gui/qt/dialogs.py:1012 +#: bal/gui/qt/dialogs.py:982 msgid "All heirs' shares are below the dust limit: the inheritance cannot be created. Increase the amounts or reduce the number of heirs." msgstr "Le quote di tutti gli eredi sono sotto il limite minimo (dust): l'eredità non può essere creata. Aumenta gli importi o riduci il numero di eredi." -#: bal/gui/qt/dialogs.py:1028 +#: bal/gui/qt/dialogs.py:998 #, python-brace-format msgid "Wallet balance is too low: {} satoshi available, but the miner and will-executor fees need {} satoshi (the minimum usable amount is {} satoshi). Add funds, or select fewer will-executors." msgstr "Il saldo del portafogli è troppo basso: {} satoshi disponibili, ma le commissioni dei miner e dei Will-Executor richiedono {} satoshi (l'importo minimo utilizzabile è {} satoshi). Aggiungi fondi, oppure seleziona meno Will-Executor." -#: bal/gui/qt/dialogs.py:1071 +#: bal/gui/qt/dialogs.py:1042 #, python-brace-format msgid "Heir {}" msgstr "Erede {}" -#: bal/gui/qt/dialogs.py:1073 +#: bal/gui/qt/dialogs.py:1044 #, python-brace-format msgid "{} is DUST - excluded (amount below dust limit)" msgstr "{} è DUST - escluso (importo sotto il limite minimo)" -#: bal/gui/qt/dialogs.py:1129 +#: bal/gui/qt/dialogs.py:1100 msgid "Heir" msgstr "Erede" -#: bal/gui/qt/dialogs.py:1246 bal/gui/qt/dialogs.py:2299 +#: bal/gui/qt/dialogs.py:1217 bal/gui/qt/dialogs.py:2270 #, python-brace-format msgid "Please wait {}secs" msgstr "Attendi {} secondi" -#. from Electrum it_IT -#: bal/gui/qt/dialogs.py:1253 bal/gui/qt/dialogs.py:1305 -#: bal/gui/qt/dialogs.py:1343 +#: bal/gui/qt/dialogs.py:1224 bal/gui/qt/dialogs.py:1276 +#: bal/gui/qt/dialogs.py:1314 msgid "Broadcasting" msgstr "Trasmissione" -#: bal/gui/qt/dialogs.py:1364 bal/gui/qt/window.py:2317 +#: bal/gui/qt/dialogs.py:1335 bal/gui/qt/window.py:2317 msgid "Ko" msgstr "Ko" -#: bal/gui/qt/dialogs.py:1375 +#: bal/gui/qt/dialogs.py:1346 msgid "Timeout - no answer" msgstr "Tempo scaduto - nessuna risposta" -#: bal/gui/qt/dialogs.py:1417 +#: bal/gui/qt/dialogs.py:1388 #, python-brace-format msgid "checking {} - {}" msgstr "controllo {} - {}" -#: bal/gui/qt/dialogs.py:1420 bal/gui/qt/dialogs.py:2268 +#: bal/gui/qt/dialogs.py:1391 bal/gui/qt/dialogs.py:2239 #: bal/gui/qt/window.py:1691 msgid "Waiting" msgstr "In attesa" -#: bal/gui/qt/dialogs.py:1434 +#: bal/gui/qt/dialogs.py:1405 #, python-brace-format msgid "checked {} - {}" msgstr "controllato {} - {}" -#: bal/gui/qt/dialogs.py:1484 bal/gui/qt/dialogs.py:2054 -#: bal/gui/qt/dialogs.py:2065 +#: bal/gui/qt/dialogs.py:1455 bal/gui/qt/dialogs.py:2025 +#: bal/gui/qt/dialogs.py:2036 #, python-brace-format msgid "Error: {}" msgstr "Errore: {}" -#: bal/gui/qt/dialogs.py:1546 +#: bal/gui/qt/dialogs.py:1516 msgid "" "Your will has expired and must be invalidated before it can be rebuilt.\n" "A transaction window will now open:\n" @@ -452,11 +435,11 @@ msgstr "" "FIRMALA e poi TRASMETTILA per invalidare il piano del vecchio testamento.\n" "Quando l'invalidazione del piano è confermata, premi il pulsante Controlla per completare il testamento." -#: bal/gui/qt/dialogs.py:1575 bal/gui/qt/window.py:850 +#: bal/gui/qt/dialogs.py:1545 bal/gui/qt/window.py:849 msgid "Check Alive must be after today and before the delivery date, please update it" msgstr "La data della verifica se sei vivo deve essere dopo oggi e prima della data di consegna: aggiornala" -#: bal/gui/qt/dialogs.py:1598 +#: bal/gui/qt/dialogs.py:1568 msgid "" "Your old will has been invalidated and the transaction was broadcast.\n" "Electrum may need a little time to register it.\n" @@ -466,19 +449,19 @@ msgstr "" "Electrum potrebbe avere bisogno di un po' di tempo per registrarla.\n" "Attendi che la transazione di invalidazione sia confermata, poi premi di nuovo il pulsante Controlla per completare l'aggiornamento del testamento." -#: bal/gui/qt/dialogs.py:1613 +#: bal/gui/qt/dialogs.py:1584 msgid "Invalidate your old will" msgstr "Invalida il piano del vecchio testamento" -#: bal/gui/qt/dialogs.py:1627 bal/gui/qt/dialogs.py:1672 +#: bal/gui/qt/dialogs.py:1598 bal/gui/qt/dialogs.py:1643 msgid "Aborted" msgstr "Interrotto" -#: bal/gui/qt/dialogs.py:1646 +#: bal/gui/qt/dialogs.py:1617 msgid "Auto-sign disabled — sign manually" msgstr "Firma automatica disattivata — firma manualmente" -#: bal/gui/qt/dialogs.py:1660 +#: bal/gui/qt/dialogs.py:1630 msgid "" "The delivery date was automatically moved one day earlier so the updated will can correctly replace the previous one.\n" "Please sign (and broadcast) to confirm the change." @@ -486,135 +469,134 @@ msgstr "" "La data di consegna è stata anticipata automaticamente di un giorno, così il testamento aggiornato può sostituire correttamente il precedente.\n" "Firma (e trasmetti il piano) per confermare la modifica." -#: bal/gui/qt/dialogs.py:1669 +#: bal/gui/qt/dialogs.py:1640 msgid "Sign your will" msgstr "Firma il testamento" -#: bal/gui/qt/dialogs.py:1674 bal/gui/qt/dialogs.py:1957 +#: bal/gui/qt/dialogs.py:1645 bal/gui/qt/dialogs.py:1928 msgid "Nothing to do" msgstr "Niente da fare" -#: bal/gui/qt/dialogs.py:1693 +#: bal/gui/qt/dialogs.py:1664 msgid "Finished" msgstr "Finito" -#: bal/gui/qt/dialogs.py:1710 +#: bal/gui/qt/dialogs.py:1681 msgid "Calendar" msgstr "Calendario" -#. from Electrum it_IT -#: bal/gui/qt/dialogs.py:1712 bal/gui/qt/dialogs.py:1742 -#: bal/gui/qt/dialogs.py:1809 bal/gui/qt/dialogs.py:2057 -#: bal/gui/qt/dialogs.py:3056 bal/gui/qt/dialogs.py:3295 -#: bal/gui/qt/dialogs.py:4126 +#: bal/gui/qt/dialogs.py:1683 bal/gui/qt/dialogs.py:1713 +#: bal/gui/qt/dialogs.py:1780 bal/gui/qt/dialogs.py:2028 +#: bal/gui/qt/dialogs.py:2968 bal/gui/qt/dialogs.py:3207 +#: bal/gui/qt/dialogs.py:4038 msgid "Close" msgstr "Chiudi" -#: bal/gui/qt/dialogs.py:1738 bal/gui/qt/dialogs.py:1805 +#: bal/gui/qt/dialogs.py:1709 bal/gui/qt/dialogs.py:1776 msgid "🔮 Wizard" msgstr "🔮 Procedura guidata" -#: bal/gui/qt/dialogs.py:1801 bal/gui/qt/dialogs.py:2609 +#: bal/gui/qt/dialogs.py:1772 bal/gui/qt/dialogs.py:2580 msgid "Heirs" msgstr "Eredi" -#: bal/gui/qt/dialogs.py:1910 bal/gui/qt/widgets.py:1150 +#: bal/gui/qt/dialogs.py:1881 bal/gui/qt/widgets.py:1150 #, python-brace-format msgid "(reminder {idx}/{total})" msgstr "(promemoria {idx}/{total})" -#: bal/gui/qt/dialogs.py:1974 +#: bal/gui/qt/dialogs.py:1945 msgid "All done" msgstr "Tutto fatto" -#: bal/gui/qt/dialogs.py:2026 +#: bal/gui/qt/dialogs.py:1996 msgid "Next steps (manual): press 'Sign' to sign your will, then 'Broadcast' to send it to the will-executors." msgstr "Prossimi passi (manuali): premi 'Firma' per firmare il testamento, poi 'Trasmetti il piano' per inviarlo ai Will-Executor." -#: bal/gui/qt/dialogs.py:2031 +#: bal/gui/qt/dialogs.py:2001 msgid "Next step (manual): press 'Sign' to sign your will." msgstr "Prossimo passo (manuale): premi 'Firma' per firmare il testamento." -#: bal/gui/qt/dialogs.py:2035 +#: bal/gui/qt/dialogs.py:2005 msgid "Next step (manual): press 'Broadcast' to send your will to the will-executors." msgstr "Prossimo passo (manuale): premi 'Trasmetti il piano' per inviare il testamento ai Will-Executor." -#: bal/gui/qt/dialogs.py:2130 +#: bal/gui/qt/dialogs.py:2100 #, python-brace-format msgid "Miner fee rate changed (the will was built with {} sat/byte, now it is {}): a new will must be prepared." msgstr "La commissione dei miner è cambiata (il testamento è stato creato con {} sat/byte, ora è {}): bisogna preparare un nuovo testamento." -#: bal/gui/qt/dialogs.py:2135 +#: bal/gui/qt/dialogs.py:2105 msgid "The miner fee rate changed: a new will must be prepared." msgstr "La commissione dei miner è cambiata: bisogna preparare un nuovo testamento." -#: bal/gui/qt/dialogs.py:2139 +#: bal/gui/qt/dialogs.py:2109 #, python-brace-format msgid "Will-executor \"{}\" is not covered by the current will: a new will must be prepared." msgstr "Il Will-Executor \"{}\" non è compreso nel testamento attuale: bisogna preparare un nuovo testamento." -#: bal/gui/qt/dialogs.py:2144 +#: bal/gui/qt/dialogs.py:2114 msgid "Backup mode is enabled but the will has no backup transaction: a new will must be prepared." msgstr "La transazione senza Will-Executor (tx di backup) è attiva ma il testamento non ha la transazione scritta: bisogna preparare un nuovo testamento." -#: bal/gui/qt/dialogs.py:2153 +#: bal/gui/qt/dialogs.py:2123 #, python-brace-format msgid "Heir \"{}\" is not covered by the current will (it was added or removed, or its delivery date changed): a new will must be prepared." msgstr "L'erede \"{}\" non è compreso nel testamento attuale (è stato aggiunto o rimosso, o la sua data di consegna è cambiata): bisogna preparare un nuovo testamento." -#: bal/gui/qt/dialogs.py:2161 +#: bal/gui/qt/dialogs.py:2132 msgid "The heirs changed: a new will must be prepared." msgstr "Gli eredi sono cambiati: bisogna preparare un nuovo testamento." -#: bal/gui/qt/dialogs.py:2163 +#: bal/gui/qt/dialogs.py:2134 msgid "A will-executor changed: a new will must be prepared." msgstr "Un Will-Executor è cambiato: bisogna preparare un nuovo testamento." -#: bal/gui/qt/dialogs.py:2173 +#: bal/gui/qt/dialogs.py:2143 msgid "The wallet contains funds that the current will does not cover yet: a new will must be prepared." msgstr "Il portafogli contiene fondi che il testamento attuale non comprende ancora: bisogna preparare un nuovo testamento." -#: bal/gui/qt/dialogs.py:2177 +#: bal/gui/qt/dialogs.py:2147 msgid "The will contains no valid transaction: a new will must be prepared." msgstr "Il testamento non contiene transazioni valide: bisogna preparare un nuovo testamento." -#: bal/gui/qt/dialogs.py:2183 +#: bal/gui/qt/dialogs.py:2153 msgid "The will is no longer coherent and must be rebuilt; the exact reason could not be determined." msgstr "Il testamento non è più coerente e va ricreato; non è stato possibile determinare il motivo esatto." -#: bal/gui/qt/dialogs.py:2213 +#: bal/gui/qt/dialogs.py:2183 msgid "No heirs: add at least one heir before building the will." msgstr "Nessun erede: aggiungi almeno un erede prima di preparare il testamento." -#: bal/gui/qt/dialogs.py:2216 +#: bal/gui/qt/dialogs.py:2186 msgid "The wallet has no spendable funds, so no inheritance transaction can be created." msgstr "Il portafogli non ha fondi spendibili, quindi non si può creare nessuna transazione di eredità." -#: bal/gui/qt/dialogs.py:2220 +#: bal/gui/qt/dialogs.py:2190 msgid "No usable will-executor: none of the servers in the list is both selected and valid. Open the will-executor settings, select at least one server and check that it is reachable." msgstr "Nessun Will-Executor utilizzabile: nessun server dell'elenco è sia selezionato sia valido. Apri le impostazioni dei Will-Executor, seleziona almeno un server e controlla che sia raggiungibile." -#: bal/gui/qt/dialogs.py:2225 +#: bal/gui/qt/dialogs.py:2195 msgid "No delivery date left to build: every heir's date is already covered by the existing will. Choose a later delivery date, or change an heir's date." msgstr "Nessuna data di consegna da preparare: la data di ogni erede è già compresa nel testamento esistente. Scegli una data di consegna successiva, oppure cambia la data di un erede." -#: bal/gui/qt/dialogs.py:2230 +#: bal/gui/qt/dialogs.py:2200 msgid "The amount to send must cover the miner fees plus this will-executor's fee, and the wallet balance is not enough: select cheaper will-executors, or add funds to the wallet." msgstr "L'importo da inviare deve coprire le commissioni dei miner più la commissione di questo Will-Executor, e il saldo del portafogli non basta: seleziona Will-Executor più economici, oppure aggiungi fondi al portafogli." -#: bal/gui/qt/dialogs.py:2235 +#: bal/gui/qt/dialogs.py:2205 msgid "A will-executor asks for more than the maximum fee you allowed: raise the maximum fee in the settings, or select a cheaper will-executor." msgstr "Un Will-Executor chiede più della commissione massima che hai permesso: alza la commissione massima nelle impostazioni, oppure seleziona un Will-Executor più economico." -#: bal/gui/qt/dialogs.py:2240 +#: bal/gui/qt/dialogs.py:2210 msgid "The inheritance transactions could not be assembled from the available funds (the balance may not cover the miner fees)." msgstr "Non è stato possibile comporre le transazioni di eredità con i fondi disponibili (il saldo potrebbe non coprire le commissioni dei miner)." -#: bal/gui/qt/dialogs.py:2244 +#: bal/gui/qt/dialogs.py:2214 msgid "An unexpected error stopped the transactions being prepared for a will-executor, so it was skipped. Try again, or select a different will-executor." msgstr "Un errore imprevisto ha interrotto la preparazione delle transazioni per un Will-Executor, che è stato saltato. Riprova, oppure seleziona un altro Will-Executor." -#: bal/gui/qt/dialogs.py:2255 +#: bal/gui/qt/dialogs.py:2225 msgid "" "Could not build the will, and the exact cause could not be determined. Please check that:\n" "- the wallet balance covers the miner and will-executor fees,\n" @@ -628,163 +610,127 @@ msgstr "" "- la data della verifica se sei vivo sia PRECEDENTE alla data di consegna,\n" "- almeno un Will-Executor sia selezionato e raggiungibile." -#: bal/gui/qt/dialogs.py:2275 +#: bal/gui/qt/dialogs.py:2246 msgid "Invalidating old will" msgstr "Invalidazione del piano del vecchio testamento" -#: bal/gui/qt/dialogs.py:2281 +#: bal/gui/qt/dialogs.py:2252 msgid "Building your will" msgstr "Preparazione del testamento" -#: bal/gui/qt/dialogs.py:2287 +#: bal/gui/qt/dialogs.py:2258 msgid "Signing your will" msgstr "Firma del testamento" -#: bal/gui/qt/dialogs.py:2293 +#: bal/gui/qt/dialogs.py:2264 msgid "Broadcasting your will to executors" msgstr "Trasmissione del piano ai Will-Executor" -#: bal/gui/qt/dialogs.py:2341 +#: bal/gui/qt/dialogs.py:2312 msgid "Wait" msgstr "Attendi" -#: bal/gui/qt/dialogs.py:2444 +#: bal/gui/qt/dialogs.py:2415 msgid "Will Details" msgstr "Dettagli del testamento" -#. from Electrum it_IT -#: bal/gui/qt/dialogs.py:2450 bal/gui/qt/dialogs.py:4101 +#: bal/gui/qt/dialogs.py:2421 bal/gui/qt/dialogs.py:4013 #: bal/gui/qt/lists.py:668 msgid "Sign" msgstr "Firma" -#. from Electrum it_IT -#: bal/gui/qt/dialogs.py:2454 bal/gui/qt/lists.py:675 +#: bal/gui/qt/dialogs.py:2425 bal/gui/qt/lists.py:675 msgid "Broadcast" msgstr "Trasmissione" -#: bal/gui/qt/dialogs.py:2461 bal/gui/qt/lists.py:677 +#: bal/gui/qt/dialogs.py:2432 bal/gui/qt/lists.py:677 msgid "Invalidate" msgstr "Invalida il piano" -#: bal/gui/qt/dialogs.py:2466 bal/gui/qt/lists.py:674 +#: bal/gui/qt/dialogs.py:2437 bal/gui/qt/lists.py:674 msgid "Merge" msgstr "Unisci" -#: bal/gui/qt/dialogs.py:2474 +#: bal/gui/qt/dialogs.py:2445 #, python-brace-format msgid "Expiration date: {}" msgstr "Data di scadenza: {}" -#: bal/gui/qt/dialogs.py:2480 +#: bal/gui/qt/dialogs.py:2451 #, python-brace-format msgid "Valid Txs:{}" msgstr "Tx valide: {}" -#: bal/gui/qt/dialogs.py:2482 +#: bal/gui/qt/dialogs.py:2453 #, python-brace-format msgid "Total Txs:{}" msgstr "Tx totali: {}" -#: bal/gui/qt/dialogs.py:2536 +#: bal/gui/qt/dialogs.py:2507 msgid "Hide replaced" msgstr "Nascondi sostituite" -#: bal/gui/qt/dialogs.py:2538 +#: bal/gui/qt/dialogs.py:2509 msgid "Unhide replaced" msgstr "Mostra sostituite" -#: bal/gui/qt/dialogs.py:2544 +#: bal/gui/qt/dialogs.py:2515 msgid "Hide invalidated" msgstr "Nascondi invalidate" -#: bal/gui/qt/dialogs.py:2546 +#: bal/gui/qt/dialogs.py:2517 msgid "Unhide invalidated" msgstr "Mostra invalidate" -#: bal/gui/qt/dialogs.py:2571 +#: bal/gui/qt/dialogs.py:2542 msgid "Will-Executor Service List" msgstr "Elenco dei servizi Will-Executor" -#: bal/gui/qt/dialogs.py:2618 bal/gui/qt/lists.py:314 +#: bal/gui/qt/dialogs.py:2589 bal/gui/qt/lists.py:314 msgid "New Heir" msgstr "Nuovo erede" -#. from Electrum it_IT -#: bal/gui/qt/dialogs.py:2660 +#: bal/gui/qt/dialogs.py:2631 msgid "All" msgstr "Tutti" -#: bal/gui/qt/dialogs.py:2663 +#: bal/gui/qt/dialogs.py:2634 msgid "Valid NC" msgstr "Valido NC" -#: bal/gui/qt/dialogs.py:2801 bal/gui/qt/dialogs.py:2916 +#: bal/gui/qt/dialogs.py:2771 bal/gui/qt/dialogs.py:2847 msgid "Auto" msgstr "Auto" -#: bal/gui/qt/dialogs.py:2803 +#: bal/gui/qt/dialogs.py:2773 msgid "Automatically advance through the QR codes." msgstr "Scorri automaticamente i codici QR." -#: bal/gui/qt/dialogs.py:2807 +#: bal/gui/qt/dialogs.py:2777 msgid "QR codes per second:" msgstr "Codici QR al secondo:" -#: bal/gui/qt/dialogs.py:2811 +#: bal/gui/qt/dialogs.py:2781 msgid " /s" msgstr " /s" -#: bal/gui/qt/dialogs.py:2813 +#: bal/gui/qt/dialogs.py:2783 msgid "Loop" msgstr "Ripeti" -#: bal/gui/qt/dialogs.py:2816 +#: bal/gui/qt/dialogs.py:2785 msgid "When the last QR code is reached, keep cycling from the first one instead of stopping." msgstr "Arrivato all'ultimo codice QR, riparti dal primo invece di fermarti." -#: bal/gui/qt/dialogs.py:2825 +#: bal/gui/qt/dialogs.py:2795 msgid "QR code size:" msgstr "Dimensione del codice QR:" -#: bal/gui/qt/dialogs.py:2837 -msgid "Format:" -msgstr "Formato:" - -#: bal/gui/qt/dialogs.py:2843 -msgid "(default)" -msgstr "(predefinito)" - -#: bal/gui/qt/dialogs.py:2910 +#: bal/gui/qt/dialogs.py:2841 msgid "Stop" msgstr "Ferma" -#: bal/gui/qt/dialogs.py:2933 -msgid "Proprietary format; every other BAL wallet understands it." -msgstr "Formato proprietario; ogni altro portafogli BAL lo capisce." - -#: bal/gui/qt/dialogs.py:2935 -msgid "Legacy BC-UR v1 (ur:bytes), compatible with older Blockchain Commons tools." -msgstr "BC-UR v1 legacy (ur:bytes), compatibile con i vecchi strumenti Blockchain Commons." - -#: bal/gui/qt/dialogs.py:2937 -msgid "Standard BC-UR v2 fountain codes (ur:bytes)." -msgstr "Codici fountain standard BC-UR v2 (ur:bytes)." - -#: bal/gui/qt/dialogs.py:2938 -msgid "Coinkite BBQR (B$ frames) for BitKit & friends." -msgstr "Coinkite BBQR (frame B$) per BitKit e simili." - -#: bal/gui/qt/dialogs.py:2944 -#, python-brace-format -msgid "" -"Scan the QR codes below, in order, with the will-opening device.\n" -"Frame 1 of {} carries the total number of codes." -msgstr "" -"Scansiona i codici QR qui sotto, in ordine, con il dispositivo che apre il testamento.\n" -"Il frame 1 di {} contiene il numero totale di codici." - -#: bal/gui/qt/dialogs.py:2951 +#: bal/gui/qt/dialogs.py:2862 #, python-brace-format msgid "" "Scan the QR codes below with the will-opening device.\n" @@ -793,103 +739,101 @@ msgstr "" "Scansiona i codici QR qui sotto con il dispositivo che apre il testamento.\n" "{} codici contengono tutto il trasferimento (l'ordine non conta, i doppioni vengono ignorati)." -#: bal/gui/qt/dialogs.py:2972 +#: bal/gui/qt/dialogs.py:2884 #, python-brace-format msgid "Frame {} of {}" msgstr "Frame {} di {}" -#: bal/gui/qt/dialogs.py:2995 +#: bal/gui/qt/dialogs.py:2907 msgid "Export will" msgstr "Esporta il testamento" -#: bal/gui/qt/dialogs.py:3005 +#: bal/gui/qt/dialogs.py:2917 msgid "No will transaction to export." msgstr "Nessuna transazione del testamento da esportare." -#: bal/gui/qt/dialogs.py:3012 +#: bal/gui/qt/dialogs.py:2924 msgid "Export:" msgstr "Esporta:" -#: bal/gui/qt/dialogs.py:3019 +#: bal/gui/qt/dialogs.py:2931 msgid "Whole will" msgstr "Testamento completo" -#: bal/gui/qt/dialogs.py:3023 +#: bal/gui/qt/dialogs.py:2934 msgid "Export the full will items (all details/statuses) as JSON, or only the serialized transactions." msgstr "Esporta gli elementi completi del testamento (tutti i dettagli e gli stati) in JSON, oppure solo le transazioni serializzate." -#: bal/gui/qt/dialogs.py:3033 +#: bal/gui/qt/dialogs.py:2945 msgid "Send as:" msgstr "Invia come:" -#. from Electrum it_IT -#: bal/gui/qt/dialogs.py:3035 bal/gui/qt/dialogs.py:3272 +#: bal/gui/qt/dialogs.py:2947 bal/gui/qt/dialogs.py:3184 msgid "File" msgstr "File" -#: bal/gui/qt/dialogs.py:3036 bal/gui/qt/dialogs.py:3273 +#: bal/gui/qt/dialogs.py:2948 bal/gui/qt/dialogs.py:3185 msgid "QR Code" msgstr "Codice QR" -#: bal/gui/qt/dialogs.py:3037 bal/gui/qt/dialogs.py:3274 +#: bal/gui/qt/dialogs.py:2949 bal/gui/qt/dialogs.py:3186 msgid "Audio" msgstr "Audio" -#: bal/gui/qt/dialogs.py:3079 +#: bal/gui/qt/dialogs.py:2990 msgid "Export the will as a JSON file. Uncheck \"Whole will\" above to export only the serialized transactions (comma-separated)." msgstr "Esporta il testamento come file JSON. Togli la spunta a \"Testamento completo\" qui sopra per esportare solo le transazioni serializzate (separate da virgola)." -#: bal/gui/qt/dialogs.py:3087 +#: bal/gui/qt/dialogs.py:2999 msgid "Export…" msgstr "Esporta…" -#: bal/gui/qt/dialogs.py:3104 bal/gui/qt/dialogs.py:3330 +#: bal/gui/qt/dialogs.py:3016 bal/gui/qt/dialogs.py:3242 #: bal/gui/qt/window.py:1787 bal/gui/qt/window.py:1800 msgid "Audio MODEM plugin is not available." msgstr "Il plugin Audio MODEM non è disponibile." -#: bal/gui/qt/dialogs.py:3108 bal/gui/qt/dialogs.py:3334 +#: bal/gui/qt/dialogs.py:3020 bal/gui/qt/dialogs.py:3246 msgid "Speed (KB/sec):" msgstr "Velocità (KB/sec):" -#. from Electrum it_IT -#: bal/gui/qt/dialogs.py:3119 +#: bal/gui/qt/dialogs.py:3031 msgid "Send" msgstr "Invia" -#: bal/gui/qt/dialogs.py:3164 bal/gui/qt/dialogs.py:3198 -#: bal/gui/qt/dialogs.py:3223 +#: bal/gui/qt/dialogs.py:3076 bal/gui/qt/dialogs.py:3110 +#: bal/gui/qt/dialogs.py:3135 msgid "No will transaction matches the selected filter." msgstr "Nessuna transazione del testamento corrisponde al filtro selezionato." -#: bal/gui/qt/dialogs.py:3187 +#: bal/gui/qt/dialogs.py:3099 msgid "item(s)" msgstr "elemento/i" -#: bal/gui/qt/dialogs.py:3187 +#: bal/gui/qt/dialogs.py:3099 msgid "transaction(s)" msgstr "transazione/i" -#: bal/gui/qt/dialogs.py:3189 +#: bal/gui/qt/dialogs.py:3101 #, python-brace-format msgid "{} {} will be exported." msgstr "Verranno esportati: {} {}." -#: bal/gui/qt/dialogs.py:3192 +#: bal/gui/qt/dialogs.py:3104 #, python-brace-format msgid "{} {} will be sent." msgstr "Verranno inviati: {} {}." -#: bal/gui/qt/dialogs.py:3216 bal/gui/qt/dialogs.py:3219 -#: bal/gui/qt/dialogs.py:3370 bal/gui/qt/dialogs.py:3373 +#: bal/gui/qt/dialogs.py:3128 bal/gui/qt/dialogs.py:3131 +#: bal/gui/qt/dialogs.py:3282 bal/gui/qt/dialogs.py:3285 msgid "Invalid audio speed." msgstr "Velocità audio non valida." -#: bal/gui/qt/dialogs.py:3255 +#: bal/gui/qt/dialogs.py:3167 msgid "Import will" msgstr "Importa il testamento" -#: bal/gui/qt/dialogs.py:3262 +#: bal/gui/qt/dialogs.py:3173 msgid "" "Choose how the will was exported: from a file, QR codes or audio.\n" "The import never touches the live will." @@ -897,59 +841,59 @@ msgstr "" "Scegli come è stato esportato il testamento: da file, codici QR o audio.\n" "L'importazione non modifica mai il testamento in uso." -#: bal/gui/qt/dialogs.py:3270 +#: bal/gui/qt/dialogs.py:3182 msgid "Import from:" msgstr "Importa da:" -#: bal/gui/qt/dialogs.py:3307 +#: bal/gui/qt/dialogs.py:3218 msgid "Import the JSON will file written by the Export ▶ File option. The will opens in a read-only preview window." msgstr "Importa il file JSON del testamento creato con l'opzione Esporta ▶ File. Il testamento si apre in una finestra di anteprima in sola lettura." -#: bal/gui/qt/dialogs.py:3313 +#: bal/gui/qt/dialogs.py:3225 msgid "Choose will file…" msgstr "Scegli il file del testamento…" -#: bal/gui/qt/dialogs.py:3343 +#: bal/gui/qt/dialogs.py:3255 msgid "Waiting for the audio transfer…" msgstr "In attesa del trasferimento audio…" -#: bal/gui/qt/dialogs.py:3346 +#: bal/gui/qt/dialogs.py:3258 msgid "Receive by audio…" msgstr "Ricevi via audio…" -#: bal/gui/qt/dialogs.py:3387 +#: bal/gui/qt/dialogs.py:3299 msgid "Receiving…" msgstr "Ricezione in corso…" -#: bal/gui/qt/dialogs.py:3407 +#: bal/gui/qt/dialogs.py:3319 msgid "No transaction data received." msgstr "Nessun dato di transazione ricevuto." -#: bal/gui/qt/dialogs.py:3425 +#: bal/gui/qt/dialogs.py:3337 #, python-brace-format msgid "Waiting for audio ({:.1f} kbps)…" msgstr "In attesa dell'audio ({:.1f} kbps)…" -#: bal/gui/qt/dialogs.py:3498 +#: bal/gui/qt/dialogs.py:3410 #, python-brace-format msgid "Could not parse a transferred will item: {}" msgstr "Impossibile leggere un elemento del testamento trasferito: {}" -#: bal/gui/qt/dialogs.py:3518 +#: bal/gui/qt/dialogs.py:3430 #, python-brace-format msgid "Could not parse a transferred transaction: {}" msgstr "Impossibile leggere una transazione trasferita: {}" -#: bal/gui/qt/dialogs.py:3530 +#: bal/gui/qt/dialogs.py:3441 msgid "The imported will contains no valid transaction in this wallet." msgstr "Il testamento importato non contiene transazioni valide in questo portafogli." -#: bal/gui/qt/dialogs.py:3541 +#: bal/gui/qt/dialogs.py:3452 #, python-brace-format msgid "{} imported transaction(s) are not valid in this wallet and were skipped." msgstr "{} transazione/i importata/e non valide in questo portafogli, quindi saltate." -#: bal/gui/qt/dialogs.py:3641 +#: bal/gui/qt/dialogs.py:3552 msgid "" "Show the will QR codes to the camera, one after another.\n" "After the first code, every new code is captured automatically.\n" @@ -959,117 +903,114 @@ msgstr "" "Dopo il primo codice, ogni nuovo codice viene acquisito automaticamente.\n" "Il primo frame indica il numero totale di codici; i doppioni vengono ignorati." -#: bal/gui/qt/dialogs.py:3650 bal/gui/qt/dialogs.py:3828 +#: bal/gui/qt/dialogs.py:3562 bal/gui/qt/dialogs.py:3740 msgid "Waiting for the first frame…" msgstr "In attesa del primo frame…" -#: bal/gui/qt/dialogs.py:3667 +#: bal/gui/qt/dialogs.py:3579 msgid "…or paste/type the frame text here" msgstr "…oppure incolla o scrivi qui il testo del frame" -#: bal/gui/qt/dialogs.py:3671 +#: bal/gui/qt/dialogs.py:3583 msgid "Add frame" msgstr "Aggiungi frame" -#: bal/gui/qt/dialogs.py:3677 bal/gui/qt/dialogs.py:3920 +#: bal/gui/qt/dialogs.py:3589 bal/gui/qt/dialogs.py:3832 msgid "Scan with camera…" msgstr "Scansiona con la fotocamera…" -#: bal/gui/qt/dialogs.py:3680 +#: bal/gui/qt/dialogs.py:3591 msgid "Start the live camera. While it runs, every new QR code shown to the camera is captured automatically." msgstr "Avvia la fotocamera. Mentre è attiva, ogni nuovo codice QR mostrato viene acquisito automaticamente." -#. from Electrum it_IT -#: bal/gui/qt/dialogs.py:3686 +#: bal/gui/qt/dialogs.py:3598 msgid "Reset" msgstr "Reimposta" -#: bal/gui/qt/dialogs.py:3693 +#: bal/gui/qt/dialogs.py:3605 msgid "Review and Sign…" msgstr "Controlla e firma…" -#: bal/gui/qt/dialogs.py:3713 +#: bal/gui/qt/dialogs.py:3624 #, python-brace-format msgid "The scanned code belongs to a different transfer ({} frames, {}). The import was reset; scan the first code again." msgstr "Il codice scansionato appartiene a un altro trasferimento ({} frame, {}). L'importazione è stata azzerata; scansiona di nuovo il primo codice." -#: bal/gui/qt/dialogs.py:3803 +#: bal/gui/qt/dialogs.py:3715 #, python-brace-format msgid "All {} frames stored." msgstr "Tutti i {} frame salvati." -#: bal/gui/qt/dialogs.py:3809 +#: bal/gui/qt/dialogs.py:3721 #, python-brace-format msgid "Stored {} of {} frames." msgstr "Salvati {} frame su {}." -#. from Electrum it_IT -#: bal/gui/qt/dialogs.py:3874 +#: bal/gui/qt/dialogs.py:3786 msgid "Cannot start QR scanner, no usable camera found." msgstr "Impossibile avviare lo scanner QR, nessuna fotocamera utilizzabile trovata." -#: bal/gui/qt/dialogs.py:3879 +#: bal/gui/qt/dialogs.py:3791 msgid "Stop scanning" msgstr "Ferma la scansione" -#: bal/gui/qt/dialogs.py:3926 +#: bal/gui/qt/dialogs.py:3838 #, python-brace-format msgid "Camera error: {}" msgstr "Errore della fotocamera: {}" -#: bal/gui/qt/dialogs.py:3973 +#: bal/gui/qt/dialogs.py:3884 #, python-brace-format msgid "The scanned code belongs to a different transfer ({} frames, {}). The import was reset; show the first code again." msgstr "Il codice scansionato appartiene a un altro trasferimento ({} frame, {}). L'importazione è stata azzerata; mostra di nuovo il primo codice." -#: bal/gui/qt/dialogs.py:4016 +#: bal/gui/qt/dialogs.py:3928 msgid "The transferred will contains no transactions." msgstr "Il testamento trasferito non contiene transazioni." -#: bal/gui/qt/dialogs.py:4045 +#: bal/gui/qt/dialogs.py:3957 msgid "Review and sign imported will" msgstr "Controlla e firma il testamento importato" -#: bal/gui/qt/dialogs.py:4058 +#: bal/gui/qt/dialogs.py:3969 msgid "Enter your wallet password to sign the imported transactions." msgstr "Inserisci la password del portafogli per firmare le transazioni importate." -#: bal/gui/qt/dialogs.py:4104 +#: bal/gui/qt/dialogs.py:4016 msgid "Skip" msgstr "Salta" -#: bal/gui/qt/dialogs.py:4120 +#: bal/gui/qt/dialogs.py:4032 msgid "Save signed file…" msgstr "Salva il file firmato…" -#: bal/gui/qt/dialogs.py:4123 +#: bal/gui/qt/dialogs.py:4035 msgid "Show signed QR…" msgstr "Mostra i QR firmati…" -#: bal/gui/qt/dialogs.py:4147 +#: bal/gui/qt/dialogs.py:4059 #, python-brace-format msgid "Transaction {} of {}" msgstr "Transazione {} di {}" -#: bal/gui/qt/dialogs.py:4149 +#: bal/gui/qt/dialogs.py:4061 #, python-brace-format msgid "TXID: {}" msgstr "TXID: {}" -#. from Electrum it_IT -#: bal/gui/qt/dialogs.py:4152 +#: bal/gui/qt/dialogs.py:4064 msgid "unknown" msgstr "sconosciuto" -#: bal/gui/qt/dialogs.py:4158 +#: bal/gui/qt/dialogs.py:4070 msgid "(no outputs)" msgstr "(nessun output)" -#: bal/gui/qt/dialogs.py:4172 +#: bal/gui/qt/dialogs.py:4084 msgid "unknown (partial transaction)" msgstr "sconosciuta (transazione parziale)" -#: bal/gui/qt/dialogs.py:4174 +#: bal/gui/qt/dialogs.py:4086 #, python-brace-format msgid "" "Total outputs: {}\n" @@ -1078,7 +1019,7 @@ msgstr "" "Totale output: {}\n" "Commissione: {}" -#: bal/gui/qt/dialogs.py:4186 +#: bal/gui/qt/dialogs.py:4097 #, python-brace-format msgid "" "Signed {} of {} transactions.\n" @@ -1089,48 +1030,42 @@ msgstr "" "\n" "Salva un file firmato da portare sul dispositivo che trasmette il piano, oppure mostra le transazioni firmate come codici QR." -#: bal/gui/qt/dialogs.py:4204 +#: bal/gui/qt/dialogs.py:4116 #, python-brace-format msgid "Could not sign the transaction: {}" msgstr "Impossibile firmare la transazione: {}" -#: bal/gui/qt/dialogs.py:4223 +#: bal/gui/qt/dialogs.py:4135 msgid "Signed will saved." msgstr "Testamento firmato salvato." -#: bal/gui/qt/dialogs.py:4234 +#: bal/gui/qt/dialogs.py:4146 msgid "No signed transaction to show." msgstr "Nessuna transazione firmata da mostrare." -#. from Electrum it_IT #: bal/gui/qt/lists.py:134 bal/gui/qt/window.py:370 msgid "Name" msgstr "Nome" -#. from Electrum it_IT #: bal/gui/qt/lists.py:135 bal/gui/qt/lists.py:1432 bal/gui/qt/widgets.py:1420 #: bal/gui/qt/window.py:374 msgid "Address" msgstr "Indirizzo" -#. from Electrum it_IT #: bal/gui/qt/lists.py:136 bal/gui/qt/window.py:309 msgid "Amount" msgstr "Importo" -#. from Electrum it_IT #: bal/gui/qt/lists.py:232 #, python-brace-format msgid "Copy {}" msgstr "Copia {}" -#. from Electrum it_IT #: bal/gui/qt/lists.py:240 bal/gui/qt/lists.py:1007 #, python-brace-format msgid "Edit {}" msgstr "Modifica {}" -#. from Electrum it_IT #: bal/gui/qt/lists.py:243 msgid "Delete" msgstr "Elimina" @@ -1147,12 +1082,10 @@ msgstr "Locktime" msgid "Txid" msgstr "Txid" -#. from Electrum it_IT #: bal/gui/qt/lists.py:346 bal/gui/qt/widgets.py:1372 msgid "Status" msgstr "Stato" -#. from Electrum it_IT #: bal/gui/qt/lists.py:347 msgid "Server" msgstr "Server" @@ -1193,7 +1126,6 @@ msgstr "elimina" msgid "Transaction IDs" msgstr "ID delle transazioni" -#. from Electrum it_IT #: bal/gui/qt/lists.py:510 msgid "Transactions" msgstr "Transazioni" @@ -1243,7 +1175,6 @@ msgstr "S" msgid "Base fee" msgstr "Commissione base" -#. from Electrum it_IT #: bal/gui/qt/lists.py:914 bal/gui/qt/lists.py:1414 msgid "Info" msgstr "Info" @@ -1252,7 +1183,6 @@ msgstr "Info" msgid "Default Address" msgstr "Indirizzo predefinito" -#. from Electrum it_IT #: bal/gui/qt/lists.py:990 msgid "Copy" msgstr "Copia" @@ -1301,7 +1231,6 @@ msgstr "Ping a tutti" msgid "Ping selected" msgstr "Ping ai selezionati" -#. from Electrum it_IT #: bal/gui/qt/lists.py:1268 msgid "Select All" msgstr "Seleziona Tutto" @@ -1382,69 +1311,67 @@ msgstr "Nessun Will-Executor corrisponde al filtro selezionato" msgid "No will-executor is selected" msgstr "Nessun Will-Executor selezionato" -#. from Electrum it_IT -#: bal/gui/qt/plugin.py:140 +#: bal/gui/qt/plugin.py:139 msgid "Electrum Plugins" msgstr "Plugin Electrum" -#: bal/gui/qt/plugin.py:307 +#: bal/gui/qt/plugin.py:306 msgid "Bitcoin After Life" msgstr "Bitcoin After Life" -#. from Electrum it_IT -#: bal/gui/qt/plugin.py:426 bal/gui/qt/plugin.py:451 +#: bal/gui/qt/plugin.py:425 bal/gui/qt/plugin.py:450 msgid "Settings" msgstr "Impostazioni" -#: bal/gui/qt/plugin.py:568 +#: bal/gui/qt/plugin.py:551 msgid "BASIC" msgstr "BASE" -#: bal/gui/qt/plugin.py:568 +#: bal/gui/qt/plugin.py:551 msgid "ADVANCED" msgstr "AVANZATO" -#: bal/gui/qt/plugin.py:588 +#: bal/gui/qt/plugin.py:571 msgid "Enable ADVANCED mode" msgstr "Attiva la modalità AVANZATA" -#: bal/gui/qt/plugin.py:589 +#: bal/gui/qt/plugin.py:572 msgid "Type 'at My Risk' to enable ADVANCED mode" msgstr "Scrivi 'at My Risk' per attivare la modalità AVANZATA" -#: bal/gui/qt/plugin.py:658 +#: bal/gui/qt/plugin.py:641 msgid "Reset to default" msgstr "Ripristina il valore predefinito" -#: bal/gui/qt/plugin.py:680 +#: bal/gui/qt/plugin.py:659 msgid "Rebroadcast transactions" msgstr "Trasmetti di nuovo il piano" -#: bal/gui/qt/plugin.py:691 +#: bal/gui/qt/plugin.py:670 msgid "Warning: change these settings only if you know what you are doing." msgstr "Attenzione: cambia queste impostazioni solo se sai cosa stai facendo." -#: bal/gui/qt/plugin.py:706 +#: bal/gui/qt/plugin.py:685 msgid "Hide Replaced" msgstr "Nascondi sostituite" -#: bal/gui/qt/plugin.py:709 +#: bal/gui/qt/plugin.py:688 msgid "Hide replaced transactions from will detail and list" msgstr "Nascondi le transazioni sostituite dal dettaglio e dall'elenco del testamento" -#: bal/gui/qt/plugin.py:714 +#: bal/gui/qt/plugin.py:693 msgid "Hide Invalidated" msgstr "Nascondi invalidate" -#: bal/gui/qt/plugin.py:717 +#: bal/gui/qt/plugin.py:696 msgid "Hide invalidated transactions from will detail and list" msgstr "Nascondi le transazioni invalidate dal dettaglio e dall'elenco del testamento" -#: bal/gui/qt/plugin.py:720 +#: bal/gui/qt/plugin.py:699 msgid "Auto-sign on Check" msgstr "Firma automatica al Controllo" -#: bal/gui/qt/plugin.py:722 +#: bal/gui/qt/plugin.py:700 msgid "" "When checking, automatically sign and broadcast the will transactions to their will-executors.\n" "The wallet password is requested only if the wallet is encrypted." @@ -1452,11 +1379,11 @@ msgstr "" "Durante il controllo, firma automaticamente le transazioni del testamento e trasmetti il piano ai rispettivi Will-Executor.\n" "La password del portafogli viene chiesta solo se il portafogli è cifrato." -#: bal/gui/qt/plugin.py:736 +#: bal/gui/qt/plugin.py:715 msgid "Panel editable Date and Fee" msgstr "Data e commissione modificabili nel pannello" -#: bal/gui/qt/plugin.py:740 +#: bal/gui/qt/plugin.py:718 msgid "" "When enabled, the delivery-time and check-alive date fields can be edited everywhere (toolbar / Heirs tab), not only in the will-building wizard.\n" "When disabled, those dates are display-only outside the wizard." @@ -1464,11 +1391,11 @@ msgstr "" "Se attivo, i campi della data di consegna e della data della verifica se sei vivo si possono modificare ovunque (barra degli strumenti / scheda Eredi), non solo nella procedura guidata del testamento.\n" "Se disattivo, fuori dalla procedura guidata queste date si possono solo vedere." -#: bal/gui/qt/plugin.py:751 +#: bal/gui/qt/plugin.py:730 msgid "Max Will-Executor Fee (satoshi)" msgstr "Commissione massima del Will-Executor (satoshi)" -#: bal/gui/qt/plugin.py:755 +#: bal/gui/qt/plugin.py:733 msgid "" "Maximum fee (in satoshi) allowed to be paid to a single will-executor. If a will-executor charges more than this, the will will not be built.\n" "Default: 500,000 satoshi (0.005 BTC)." @@ -1476,11 +1403,11 @@ msgstr "" "Commissione massima (in satoshi) che si può pagare a un singolo Will-Executor. Se un Will-Executor chiede di più, il testamento non viene preparato.\n" "Predefinito: 500.000 satoshi (0,005 BTC)." -#: bal/gui/qt/plugin.py:766 +#: bal/gui/qt/plugin.py:745 msgid "User Type" msgstr "Tipo di utente" -#: bal/gui/qt/plugin.py:770 +#: bal/gui/qt/plugin.py:748 msgid "" "Choose how much detail the plugin shows.\n" "\n" @@ -1494,19 +1421,19 @@ msgstr "" "\n" "AVANZATO: mostra tutti i controlli, compresi il campo 'Verifica se sei vivo' e il selettore Raw/Data. Permette di configurare l'URL del server welist e lascia sempre modificabili tutti i campi di data e commissione." -#: bal/gui/qt/plugin.py:783 +#: bal/gui/qt/plugin.py:762 msgid "Number of reminders" msgstr "Numero di promemoria" -#: bal/gui/qt/plugin.py:785 +#: bal/gui/qt/plugin.py:763 msgid "How many reminder alarms the exported calendar (.ics) event contains. Range: 1 to 5 (default 3). Only used in ADVANCED mode." msgstr "Quanti promemoria contiene l'evento del calendario esportato (.ics). Da 1 a 5 (predefinito 3). Usato solo in modalità AVANZATA." -#: bal/gui/qt/plugin.py:794 +#: bal/gui/qt/plugin.py:773 msgid "Event summary" msgstr "Titolo dell'evento" -#: bal/gui/qt/plugin.py:796 +#: bal/gui/qt/plugin.py:774 msgid "" "Default message to be used in event summary\n" "Variables:\n" @@ -1520,11 +1447,11 @@ msgstr "" " $heirs_complete: elenco degli eredi con nome, indirizzo, importo\n" "Usato solo in modalità AVANZATA." -#: bal/gui/qt/plugin.py:808 +#: bal/gui/qt/plugin.py:787 msgid "Event description" msgstr "Descrizione dell'evento" -#: bal/gui/qt/plugin.py:810 +#: bal/gui/qt/plugin.py:788 msgid "" "Default message to be used in event description\n" "Variables:\n" @@ -1538,19 +1465,19 @@ msgstr "" " $heirs_complete: elenco degli eredi con nome, indirizzo, importo\n" "Usato solo in modalità AVANZATA." -#: bal/gui/qt/plugin.py:823 +#: bal/gui/qt/plugin.py:802 msgid "Welist Server URL" msgstr "URL del server welist" -#: bal/gui/qt/plugin.py:825 +#: bal/gui/qt/plugin.py:803 msgid "URL of the server that provides the will-executor list. Only available in ADVANCED mode." msgstr "URL del server che fornisce l'elenco dei Will-Executor. Disponibile solo in modalità AVANZATA." -#: bal/gui/qt/plugin.py:834 +#: bal/gui/qt/plugin.py:813 msgid "Calendar app command" msgstr "Comando dell'app calendario" -#: bal/gui/qt/plugin.py:836 +#: bal/gui/qt/plugin.py:814 msgid "" "Command used to open .ics calendar files.\n" "Leave empty to use the system default (xdg-open/open/start).\n" @@ -1560,11 +1487,11 @@ msgstr "" "Lascia vuoto per usare quello predefinito del sistema (xdg-open/open/start).\n" "Usato solo in modalità AVANZATA." -#: bal/gui/qt/plugin.py:849 +#: bal/gui/qt/plugin.py:828 msgid "Save inheritance transactions in history" msgstr "Salva le transazioni di eredità nella cronologia" -#: bal/gui/qt/plugin.py:851 +#: bal/gui/qt/plugin.py:829 #, python-brace-format msgid "" "After each check, save the valid will transactions into the wallet's local history (the History tab), each with a label.\n" @@ -1577,11 +1504,11 @@ msgstr "" " {willexecutor}: sostituita con l'URL del Will-Executor dell'elemento\n" "Usato solo in modalità AVANZATA." -#: bal/gui/qt/plugin.py:863 +#: bal/gui/qt/plugin.py:842 msgid "History label" msgstr "Etichetta nella cronologia" -#: bal/gui/qt/plugin.py:865 +#: bal/gui/qt/plugin.py:843 #, python-brace-format msgid "" "Label applied to the will transactions saved into the wallet's local history.\n" @@ -1594,15 +1521,15 @@ msgstr "" " {willexecutor}: sostituita con l'URL del Will-Executor dell'elemento\n" "Usato solo in modalità AVANZATA." -#: bal/gui/qt/plugin.py:887 +#: bal/gui/qt/plugin.py:866 msgid "Broadcast all transactions to willexecutors including those already pushed" msgstr "Trasmetti il piano ai Will-Executor con tutte le transazioni, comprese quelle già inviate" -#: bal/gui/qt/plugin.py:896 +#: bal/gui/qt/plugin.py:875 msgid "Rebuild will on wallet close" msgstr "Ricrea il testamento alla chiusura del portafogli" -#: bal/gui/qt/plugin.py:898 +#: bal/gui/qt/plugin.py:876 msgid "" "Run the 'Build your will' wizard every time the wallet is closed or Electrum is quit, so the will is rebuilt and re-validated.\n" "When disabled, the will is only rebuilt when you press Check or Prepare. The last built state is still saved to the wallet." @@ -1610,11 +1537,11 @@ msgstr "" "Avvia la procedura 'Crea il tuo testamento' ogni volta che il portafogli viene chiuso o si esce da Electrum, così il testamento viene ricreato e riconvalidato.\n" "Se disattivo, il testamento viene ricreato solo quando premi Controlla o Prepara. L'ultimo stato preparato viene comunque salvato nel portafogli." -#: bal/gui/qt/plugin.py:914 +#: bal/gui/qt/plugin.py:893 msgid "Rebuild automatically on new transactions" msgstr "Ricrea automaticamente con nuove transazioni" -#: bal/gui/qt/plugin.py:916 +#: bal/gui/qt/plugin.py:894 msgid "" "When a new transaction arrives for the wallet, automatically rebuild the will the same way the wizard does at wallet close: the delivery date is anticipated by one day so the new will replaces the previous one, and the rebuilt transactions are signed and sent to their will-executors.\n" "An on-chain invalidation transaction is only built when the anticipated delivery date would fall before the Check Alive threshold, or when the threshold is already in the past.\n" @@ -1624,39 +1551,23 @@ msgstr "" "Una transazione di invalidazione del piano sulla blockchain viene creata solo se la data di consegna anticipata cadrebbe prima della data della verifica se sei vivo, o se quella data è già passata.\n" "Se disattivo (predefinito), il testamento viene ricreato solo con Controlla / Prepara / chiusura del portafogli." -#: bal/gui/qt/plugin.py:938 -msgid "QR Code Size" -msgstr "Dimensione del codice QR" - -#: bal/gui/qt/plugin.py:940 -msgid "" -"Payload size of a single QR code when exporting a will via QR.\n" -"\n" -"Larger QR codes hold more data (fewer shots) but are easier to scan with a high-resolution camera; smaller QR codes scan fine even with low-resolution cameras but require more shots.\n" -"The same selector is available inside the export dialog." -msgstr "" -"Quantità di dati in un singolo codice QR quando si esporta un testamento via QR.\n" -"\n" -"I codici QR più grandi contengono più dati (meno scatti) ma si leggono meglio con una fotocamera ad alta risoluzione; quelli più piccoli si leggono bene anche con fotocamere a bassa risoluzione ma richiedono più scatti.\n" -"Lo stesso selettore è disponibile anche nella finestra di esportazione." - -#: bal/gui/qt/plugin.py:1021 +#: bal/gui/qt/plugin.py:976 msgid "Reset to Default Setting" msgstr "Ripristina le impostazioni predefinite" -#: bal/gui/qt/plugin.py:1022 +#: bal/gui/qt/plugin.py:977 msgid "Reset these settings to their default values" msgstr "Riporta queste impostazioni ai valori predefiniti" -#: bal/gui/qt/plugin.py:1029 +#: bal/gui/qt/plugin.py:984 msgid "Open the Bitcoin After Life support website" msgstr "Apri il sito di supporto di Bitcoin After Life" -#: bal/gui/qt/plugin.py:1041 +#: bal/gui/qt/plugin.py:996 msgid "Support:" msgstr "Supporto:" -#: bal/gui/qt/plugin.py:1098 +#: bal/gui/qt/plugin.py:1053 #, python-brace-format msgid "BAL - {}" msgstr "BAL - {}" @@ -1705,7 +1616,7 @@ msgstr "" msgid "Miner fee, click for more information" msgstr "Commissione dei miner, clicca per maggiori informazioni" -#: bal/gui/qt/widgets.py:214 +#: bal/gui/qt/widgets.py:213 msgid "" "if you choose Raw, you can insert various options based on suffix:\n" " - d: number of days after current day(ex: 1d means tomorrow)\n" @@ -1715,17 +1626,15 @@ msgstr "" " - d: numero di giorni dopo oggi (es.: 1d significa domani)\n" " - y: numero di anni dopo oggi (es.: 1y significa tra un anno)\n" -#. from Electrum it_IT #: bal/gui/qt/widgets.py:241 msgid "Raw" msgstr "Raw" -#. from Electrum it_IT #: bal/gui/qt/widgets.py:241 msgid "Date" msgstr "Data" -#: bal/gui/qt/widgets.py:646 +#: bal/gui/qt/widgets.py:645 msgid "CHECK ALIVE

In DATA mode:
set the date for the “check alive” parameter.
When you open the wallet, if the “check alive” date has passed, the plugin will ask if you want to postpone the inheritance, since it assumes you still have control of the wallet and that you are still alive.

In RAW mode:
When less than this time is missing, ask to invalidate.
If you fail to invalidate during this time, your transactions will be delivered to your heirs.

if you choose Raw, you can insert various options based on suffix:
- d: number of days after current day(ex: 1d means tomorrow)
- y: number of years after current day(ex: 1y means one year from today)
The sign selects the reference date:
- a NEGATIVE value (ex: -30d) counts backwards from the delivery date (30 days before the delivery);
- a POSITIVE value (ex: 30d) counts forward from the inheritance creation date (30 days after the will was created).

Note: the date/time is expressed in your computer's Local time (not UTC time).
" msgstr "VERIFICA SE SEI VIVO

In modalità DATA:
imposta la data della “verifica se sei vivo”.
Quando apri il portafogli, se quella data è passata, il plugin ti chiede se vuoi posticipare l'eredità, perché presume che tu abbia ancora il controllo del portafogli e che tu sia ancora vivo.

In modalità RAW:
quando manca meno di questo tempo, chiede di invalidare il piano.
Se non invalidi il piano entro questo tempo, le tue transazioni verranno consegnate ai tuoi eredi.

se scegli Raw, puoi inserire diverse opzioni in base al suffisso:
- d: numero di giorni dopo oggi (es.: 1d significa domani)
- y: numero di anni dopo oggi (es.: 1y significa tra un anno)
Il segno seleziona la data di riferimento:
- un valore NEGATIVO (es.: -30d) conta all'indietro a partire dalla data di consegna (30 giorni prima della consegna);
- un valore POSITIVO (es.: 30d) conta in avanti a partire dalla data di creazione dell'eredità (30 giorni dopo la creazione del testamento).

Nota: la data e l'ora sono espresse nell'ora locale del tuo computer (non in ora UTC).
" @@ -1733,7 +1642,7 @@ msgstr "VERIFICA SE SEI VIVO

In modalità DATA:
imposta la data msgid "Check Alive" msgstr "Verifica se sei vivo" -#: bal/gui/qt/widgets.py:687 +#: bal/gui/qt/widgets.py:686 msgid "DELIVERY TIME

Set Locktime for transactions.
Any time is needed transaction will be anticipated by 1day

(ONLY IN ADVANCED MODE)
if you choose Raw, you can insert various options based on suffix:
- d: number of days after current day(ex: 1d means tomorrow)
- y: number of years after currrent day(ex: 1y means one year from today)

Note: the date/time is expressed in your computer's Local time (not UTC time).
" msgstr "DATA DI CONSEGNA

Imposta il locktime delle transazioni.
Ogni volta che serve, la transazione viene anticipata di 1 giorno

(SOLO IN MODALITÀ AVANZATA)
se scegli Raw, puoi inserire diverse opzioni in base al suffisso:
- d: numero di giorni dopo oggi (es.: 1d significa domani)
- y: numero di anni dopo oggi (es.: 1y significa tra un anno)

Nota: la data e l'ora sono espresse nell'ora locale del tuo computer (non in ora UTC).
" @@ -1745,7 +1654,7 @@ msgstr "Data di consegna, clicca per maggiori informazioni" msgid "Export reminder dates to your calendar (.ics)" msgstr "Esporta le date dei promemoria nel tuo calendario (.ics)" -#: bal/gui/qt/widgets.py:855 +#: bal/gui/qt/widgets.py:854 msgid "" "Enter the date on which you want the inheritance (or backup)\n" "of your Electrum wallet to take effect." @@ -1753,7 +1662,7 @@ msgstr "" "Inserisci la data in cui vuoi che l'eredità\n" "del tuo portafogli Electrum abbia effetto." -#: bal/gui/qt/widgets.py:886 +#: bal/gui/qt/widgets.py:885 msgid "" "Please note: Do not reduce the miner fees\n" "unless you know what you’re doing" @@ -1834,12 +1743,12 @@ msgstr "eredi" msgid "Heirs are not defined" msgstr "Gli eredi non sono definiti" -#: bal/gui/qt/window.py:839 +#: bal/gui/qt/window.py:838 #, python-brace-format msgid "In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts.{}" msgstr "Nel processo di eredità il portafogli viene sempre svuotato del tutto. Le tue impostazioni richiedono una correzione degli importi.{}" -#: bal/gui/qt/window.py:857 +#: bal/gui/qt/window.py:856 msgid "CheckAlive is in the past please update it to a date in the future but less than locktime" msgstr "La data della verifica se sei vivo è nel passato: aggiornala a una data futura ma precedente al locktime" @@ -1851,7 +1760,7 @@ msgstr "il locktime è precedente alla data della verifica se sei vivo" msgid " no backup transaction or willexecutor selected" msgstr " nessuna transazione senza Will-Executor attiva o nessun Will-Executor selezionato" -#: bal/gui/qt/window.py:916 +#: bal/gui/qt/window.py:915 msgid "" "This inheritance was already signed/sent to will-executors and you are postponing it.\n" "\n" @@ -1881,7 +1790,7 @@ msgstr "Will-Executor cambiato" msgid "Txfees are changed" msgstr "Le commissioni sono cambiate" -#: bal/gui/qt/window.py:951 +#: bal/gui/qt/window.py:950 msgid "" "Found CHANGES to the DATE or the HEIRS,\n" "a NEW WILL must be prepared." @@ -1906,7 +1815,7 @@ msgstr "" msgid "Please, check your heirs, locktime and threshold!" msgstr "Controlla i tuoi eredi, il locktime e la data della verifica se sei vivo!" -#: bal/gui/qt/window.py:994 +#: bal/gui/qt/window.py:993 msgid "" "Your inheritance has been rebuilt and now needs to be signed again.\n" "\n" @@ -1918,7 +1827,7 @@ msgstr "" "Prossimo passo (manuale):\n" " 1. Premi 'Firma' per firmare la nuova transazione." -#: bal/gui/qt/window.py:1001 +#: bal/gui/qt/window.py:1000 msgid "" "Your inheritance has been rebuilt and now needs to be signed and re-sent to the will-executors.\n" "\n" @@ -1932,7 +1841,6 @@ msgstr "" " 1. Premi 'Firma' per firmare la nuova transazione.\n" " 2. Premi 'Trasmetti il piano' per inviarla ai Will-Executor." -#. from Electrum it_IT #: bal/gui/qt/window.py:1038 msgid "Electrum was unable to deserialize the transaction:" msgstr "Electrum non ha potuto deserializzare la transazione:" @@ -1941,7 +1849,7 @@ msgstr "Electrum non ha potuto deserializzare la transazione:" msgid "no tx" msgstr "nessuna transazione" -#: bal/gui/qt/window.py:1067 +#: bal/gui/qt/window.py:1066 msgid "Please sign and broadcast this transaction to invalidate current will" msgstr "Firma questa transazione e trasmetti il piano per invalidare il piano del testamento attuale" @@ -2019,7 +1927,7 @@ msgstr "Controllo delle transazioni" msgid "Check Transaction" msgstr "Controlla la transazione" -#: bal/gui/qt/window.py:2169 +#: bal/gui/qt/window.py:2168 msgid "" "Could not download the will-executors list.\n" "\n" @@ -2029,7 +1937,7 @@ msgstr "" "\n" "Di solito la causa è la connessione a internet o un firewall, non il plugin. Controlla la connessione (spesso una VPN aiuta) e riprova." -#: bal/gui/qt/window.py:2179 +#: bal/gui/qt/window.py:2178 msgid "" "Could not download the will-executors list over Tor.\n" "\n" @@ -2043,7 +1951,7 @@ msgstr "" msgid "Downloading will-executors list..." msgstr "Download dell'elenco dei Will-Executor..." -#: bal/gui/qt/window.py:2251 +#: bal/gui/qt/window.py:2250 #, python-brace-format msgid "" "No active will-executor servers found for the {chain} network.\n" @@ -2054,7 +1962,7 @@ msgstr "" "\n" "Il server welist {url} ha risposto, ma non ha restituito Will-Executor per questa rete." -#: bal/gui/qt/window.py:2259 +#: bal/gui/qt/window.py:2258 #, python-brace-format msgid "" "Could not reach the configured welist server.\n" @@ -2071,7 +1979,7 @@ msgstr "" "\n" "Controlla l'URL del server welist nelle impostazioni del plugin." -#: bal/gui/qt/window.py:2269 +#: bal/gui/qt/window.py:2268 #, python-brace-format msgid "No active will-executor found for the {chain} network." msgstr "Nessun Will-Executor attivo trovato per la rete {chain}." @@ -2084,3 +1992,42 @@ msgstr "Ping ai Will-Executor:" msgid "Ping Will-Executors" msgstr "Ping ai Will-Executor" +#~ msgid "Format:" +#~ msgstr "Formato:" + +#~ msgid "(default)" +#~ msgstr "(predefinito)" + +#~ msgid "Proprietary format; every other BAL wallet understands it." +#~ msgstr "Formato proprietario; ogni altro portafogli BAL lo capisce." + +#~ msgid "Legacy BC-UR v1 (ur:bytes), compatible with older Blockchain Commons tools." +#~ msgstr "BC-UR v1 legacy (ur:bytes), compatibile con i vecchi strumenti Blockchain Commons." + +#~ msgid "Standard BC-UR v2 fountain codes (ur:bytes)." +#~ msgstr "Codici fountain standard BC-UR v2 (ur:bytes)." + +#~ msgid "Coinkite BBQR (B$ frames) for BitKit & friends." +#~ msgstr "Coinkite BBQR (frame B$) per BitKit e simili." + +#~ msgid "" +#~ "Scan the QR codes below, in order, with the will-opening device.\n" +#~ "Frame 1 of {} carries the total number of codes." +#~ msgstr "" +#~ "Scansiona i codici QR qui sotto, in ordine, con il dispositivo che apre il testamento.\n" +#~ "Il frame 1 di {} contiene il numero totale di codici." + +#~ msgid "QR Code Size" +#~ msgstr "Dimensione del codice QR" + +#~ msgid "" +#~ "Payload size of a single QR code when exporting a will via QR.\n" +#~ "\n" +#~ "Larger QR codes hold more data (fewer shots) but are easier to scan with a high-resolution camera; smaller QR codes scan fine even with low-resolution cameras but require more shots.\n" +#~ "The same selector is available inside the export dialog." +#~ msgstr "" +#~ "Quantità di dati in un singolo codice QR quando si esporta un testamento via QR.\n" +#~ "\n" +#~ "I codici QR più grandi contengono più dati (meno scatti) ma si leggono meglio con una fotocamera ad alta risoluzione; quelli più piccoli si leggono bene anche con fotocamere a bassa risoluzione ma richiedono più scatti.\n" +#~ "Lo stesso selettore è disponibile anche nella finestra di esportazione." + diff --git a/tests/test_core_animated_qr.py b/tests/test_core_animated_qr.py index 25438d7..785145e 100644 --- a/tests/test_core_animated_qr.py +++ b/tests/test_core_animated_qr.py @@ -1,11 +1,9 @@ """ -Tests for ``bal.core.animated_qr`` (BC-UR v1, BC-UR v2, BBQR interop). +Tests for ``bal.core.animated_qr`` (BBQR interop). -Validates the self-contained codecs against the published spec vectors -(BCR-2020-004/005 BC32, BCR-2020-012 bytewords) and against byte-exact -output captured from the reference C++ bc-ur encoder (fountain/xoshiro/ -alias-sampler parity), plus round trips, out-of-order assembly, missing-part -fountain solving and malformed-input rejection for all four formats. +Validates the self-contained BBQR codec against round trips, out-of-order +assembly, compression default/fallback, malformed-input rejection and the +format detection / parse_for_detection helpers. Run: source electrum/env/bin/activate @@ -22,328 +20,6 @@ import random from bal.core import animated_qr as aq -def _payload(plen: int) -> bytes: - """Deterministic payload matching the C++ reference driver (``(i*7)&0xff``).""" - return bytes((i * 7) & 0xFF for i in range(plen)) - - -# --------------------------------------------------------------------------- # -# BC32 (BCR-2020-004 / bcr-2020-005 rev1 reference implementation vectors) -# --------------------------------------------------------------------------- # - - -def test_bc32_official_vectors(): - cases = [ - (b"Hello, world", "fpjkcmr09ss8wmmjd3jq6ax7w9"), - (b"Hello world", "fpjkcmr0ypmk7unvvsh4ra4j"), - ( - bytes.fromhex("d934063e82001eec0585ee41ab5d8e4b703a4be1f73aec21e143912c56"), - "my6qv05zqq0wcpv9aeq6khvwfdcr5jlp7uawcg0pgwgjc4shjm6xu", - ), - ] - for payload, encoded in cases: - assert aq.bc32_encode(payload) == encoded - assert aq.bc32_decode(encoded) == payload - - -def test_bc32_checksum_rejected(): - good = aq.bc32_encode(b"Hello, world") - corrupted = good[:-1] + ("a" if good[-1] != "a" else "b") - try: - aq.bc32_decode(corrupted) - except aq.AnimatedQrError: - pass - else: - raise AssertionError("expected AnimatedQrError for corrupted BC32") - - -def test_bc32_bad_char_rejected(): - try: - aq.bc32_decode("1" * 26) - except aq.AnimatedQrError: - pass - else: - raise AssertionError("expected AnimatedQrError for '1' (not in alphabet)") - - -# --------------------------------------------------------------------------- # -# Bytewords (BCR-2020-012) -# --------------------------------------------------------------------------- # - - -def test_bytewords_minimal_roundtrip(): - samples = [bytes(range(256)), _payload(59), b"\x00"] + [ - os.urandom(64) for _ in range(4) - ] - for data in samples: - words = aq.bytewords_minimal_encode(data) - assert len(words) == (len(data) + 4) * 2 # 2 chars per byte incl. CRC - assert aq.bytewords_minimal_decode(words) == data - - -def test_bytewords_rejects_corrupted_crc(): - data = _payload(40) - words = aq.bytewords_minimal_encode(data) - flip = "a" if words[-1] != "a" else "b" - try: - aq.bytewords_minimal_decode(words[:-1] + flip) - except aq.AnimatedQrError: - pass - else: - raise AssertionError("expected AnimatedQrError for corrupted CRC") - - -def test_bytewords_rejects_odd_length(): - try: - aq.bytewords_minimal_decode("abc") - except aq.AnimatedQrError: - pass - else: - raise AssertionError("expected AnimatedQrError for odd-length bytewords") - - -# --------------------------------------------------------------------------- # -# BC-UR v2: byte-exact parity with the reference C++ encoder -# --------------------------------------------------------------------------- # - -# Reference frames from the bc-ur C++ fountain encoder -# (payload x=(i*7)&0xFF, cbor wrapped, single-part and multipart). -REF_V2_SINGLE_12 = "ur:bytes/gsaeatbabzcecndrehetfhfggtoeemhpmo" - -REF_V2_MULTI_59 = [ - "ur:bytes/2-2/lpaoaocsfscyrpdpjzbyhdctsbtdtavtvdwyykztaxbkbycsctdsdpeefrfwgagdhghyihjzjkknlylomymtaeeccasket", - "ur:bytes/3-2/lpaxaocsfscyrpdpjzbyhdcthdfraeatbabzcecndrehetfhfggtghhpidinjoktkblplkmunyoypdperpryssimryrldt", - "ur:bytes/4-2/lpaaaocsfscyrpdpjzbyhdctsbtdtavtvdwyykztaxbkbycsctdsdpeefrfwgagdhghyihjzjkknlylomymtaefeimteue", - "ur:bytes/5-2/lpahaocsfscyrpdpjzbyhdctmuwltavdwlzowlurdtfrdtdihkjekkjlhkdnesdidtuywlzmwluydtdiesdnssgdaontls", - "ur:bytes/6-2/lpamaocsfscyrpdpjzbyhdctmuwltavdwlzowlurdtfrdtdihkjekkjlhkdnesdidtuywlzmwluydtdiesdnssisescmwt", - "ur:bytes/7-2/lpataocsfscyrpdpjzbyhdcthdfraeatbabzcecndrehetfhfggtghhpidinjoktkblplkmunyoypdperprysslsspplgm", - "ur:bytes/8-2/lpayaocsfscyrpdpjzbyhdctsbtdtavtvdwyykztaxbkbycsctdsdpeefrfwgagdhghyihjzjkknlylomymtaeonlrzebg", - "ur:bytes/9-2/lpasaocsfscyrpdpjzbyhdctsbtdtavtvdwyykztaxbkbycsctdsdpeefrfwgagdhghyihjzjkknlylomymtaeaaryknzt", - "ur:bytes/10-2/lpbkaocsfscyrpdpjzbyhdctmuwltavdwlzowlurdtfrdtdihkjekkjlhkdnesdidtuywlzmwluydtdiesdnsslotsfrfn", - "ur:bytes/11-2/lpbdaocsfscyrpdpjzbyhdctmuwltavdwlzowlurdtfrdtdihkjekkjlhkdnesdidtuywlzmwluydtdiesdnssdtwyrstd", - "ur:bytes/12-2/lpbnaocsfscyrpdpjzbyhdctsbtdtavtvdwyykztaxbkbycsctdsdpeefrfwgagdhghyihjzjkknlylomymtaegswnvdin", - "ur:bytes/13-2/lpbtaocsfscyrpdpjzbyhdctmuwltavdwlzowlurdtfrdtdihkjekkjlhkdnesdidtuywlzmwluydtdiesdnsshknlptee", -] - -# Reference message for the 59-byte payload: byte-string head (0x58,0x3b) + data. -REF_V2_MULTI_59_MSG = bytes([0x58, 0x3B]) + _payload(59) - - -def test_v2_single_part_matches_reference(): - frames = aq.ur2_frames(_payload(12), len(REF_V2_SINGLE_12)) - assert frames == [REF_V2_SINGLE_12] - - -def test_v2_reference_frames_decode_and_reencode_exactly(): - message = REF_V2_MULTI_59_MSG - fragment_len = -(-len(message) // 2) - for frame in REF_V2_MULTI_59: - seq, seq_len, message_len, checksum, data = aq.ur2_parse_part(frame) - assert seq_len == 2 - assert message_len == len(message) - assert checksum == aq.crc32_int(message) - assert len(data) == fragment_len - # re-encoding the parsed values reproduces the reference line exactly - assert aq._ur2_part_string(seq, seq_len, message_len, checksum, data) == frame - # our choose_fragments + partition + xor reproduces the reference data - indexes = aq.choose_fragments(seq, seq_len, checksum) - assert seq_num_indexes_valid(seq, seq_len, indexes) - mixed = aq._mix_fragments(aq._partition_message(message, fragment_len), indexes, fragment_len) - assert mixed == data - - -def seq_num_indexes_valid(seq, seq_len, indexes): - # pure part for seq <= seq_len contains exactly fragment seq-1 - if seq <= seq_len: - return indexes == {seq - 1} - return set(indexes) <= set(range(seq_len)) and bool(indexes) - - -def test_v2_multipart_encoder_matches_reference_from_seq2(): - # Our frames start at seq 1 (spec-aligned); parts seq 2.. must equal the - # reference (which starts at seq 2 due to first_seq_num=1). - mine = aq.ur2_frames(_payload(59), 120) - assert mine[0].split("/", 1)[1].startswith("1-2") or "1-2" in mine[0].split("/")[1] - assert mine[1:4] == REF_V2_MULTI_59[:3] - - -def test_v2_reference_seq7_mix_parity(): - # Higher-degree mixed parts (seq_len=7) also match: message uses the - # reference head 0x58|0x00 for the 256-byte driver payload. - message = bytes([0x58, 0x00]) + _payload(256) - seq_len = 7 - fragment_len = -(-len(message) // seq_len) - frames = [ - "ur:bytes/9-7/lpasatcfadaocyfysnjlsrhddaykztaxbkbycsctdsdpeefrfwgagdhghyihjzjkknlylomymtntoxpyprrhrtsttotluovlwdwnsrfejzhd", - "ur:bytes/10-7/lpbkatcfadaocyfysnjlsrhddazeahbnbwcycldedlenfsfygrgmhkhniojtkpkelslememkneolpmqzrksasotitsuevwwpwfzswzpmdrvo", - "ur:bytes/11-7/lpbdatcfadaocyfysnjlsrhddawkwtbbbefnaefnbebbjojybebnaebndybbbewkwtceaecedyeebebbjobnaebnbeeedybbbeztwproyapd", - ] - for frame in frames: - seq, sl, mlen, checksum, data = aq.ur2_parse_part(frame) - assert sl == seq_len and mlen == len(message) - assert checksum == aq.crc32_int(message) - mixed = aq._mix_fragments( - aq._partition_message(message, fragment_len), - aq.choose_fragments(seq, seq_len, checksum), - fragment_len, - ) - assert mixed == data - - -# --------------------------------------------------------------------------- # -# BC-UR v2: sessions / fountain decoding -# --------------------------------------------------------------------------- # - - -def test_v2_roundtrip_in_order(): - payload = ("BAL transfer " * 9).encode() - frames = aq.ur2_frames(payload, 120) - seq_len = int(frames[0].split("/")[1].split("-")[1]) - assert len(frames) == 2 * seq_len # pure wave + redundant mixed wave - session = aq.AnimatedQrSession() - for frame in frames: - session.add_part(frame) - assert session.done - assert session.received == session.total - text, _ = session.resolve() - assert text == payload.decode() - - -def test_v2_out_of_order_and_duplicate(): - payload = ("BAL transfer " * 9).encode() - frames = aq.ur2_frames(payload, 120) - order = list(range(len(frames))) - random.Random(11).shuffle(order) - session = aq.AnimatedQrSession() - for i in order: - status = session.add_part(frames[i]) - assert status in ("ok", "dup") - session.add_part(frames[0]) # duplicate of an already-received part - assert session.done - assert session.resolve()[0] == payload.decode() - - -def test_v2_solves_without_a_pure_fragment(): - payload = ("BAL transfer " * 9).encode() - frames = aq.ur2_frames(payload, 120) - session = aq.AnimatedQrSession() - for frame in frames[1:]: # drop the first pure fragment - session.add_part(frame) - assert session.done - assert session.resolve()[0] == payload.decode() - - -def test_v2_single_part_import(): - session = aq.AnimatedQrSession() - session.add_part(REF_V2_SINGLE_12) - assert session.done and session.total == 1 - assert session.resolve()[0] == _payload(12).decode("latin-1") - - -def test_v2_conflicting_transfer_rejected(): - payload_a = b"AAAAAAAAAAAAAAAA" - payload_b = b"BBBBBBBBBBBBBBBB" - fa = aq.ur2_frames(payload_a, 500)[0] - fb = aq.ur2_frames(payload_b, 500)[0] - session = aq.AnimatedQrSession() - session.add_part(fa) - try: - session.add_part(fb) - except aq.TransferConflictError: - pass - else: - raise AssertionError("expected TransferConflictError for a different transfer") - - -def test_v2_corrupt_crc_rejected(): - frame = list(REF_V2_MULTI_59[0]) - idx = len(frame) - 1 - frame[idx] = "a" if frame[idx] != "a" else "b" - try: - aq.ur2_parse_part("".join(frame)) - except aq.AnimatedQrError: - pass - else: - raise AssertionError("expected AnimatedQrError for a corrupt v2 part") - - -def test_v2_session_cap_rejected(): - part = aq._ur2_part_string(1, 30000, 100, 1234, b"\x00" * 100) - session = aq._Ur2Session() - try: - session.add(part) - except aq.SessionLimitError: - pass - else: - raise AssertionError("expected SessionLimitError for oversized seq_len") - - -# --------------------------------------------------------------------------- # -# BC-UR v1 -# --------------------------------------------------------------------------- # - - -def test_v1_multipart_roundtrip(): - payload = ("v1 transfer payload " * 6).encode() - frames = aq.ur1_frames(payload, 120) - assert len(frames) > 1 - session = aq.AnimatedQrSession() - for frame in reversed(frames): - session.add_part(frame) - assert session.done - assert session.resolve()[0] == payload.decode() - - -def test_v1_single_part_roundtrip(): - payload = b"hello, bal" - frames = aq.ur1_frames(payload, 400) - assert len(frames) == 1 - session = aq.AnimatedQrSession() - session.add_part(frames[0]) - assert session.done and session.total == 1 - assert session.resolve()[0] == payload.decode() - - -def test_v1_headerless_single_part_import(): - # bcr-2020-005 rev1 allows omitting the sequence header + digest entirely. - payload = b"hello, bal" - message = aq.cbor_byte_string(payload) - single = "ur:bytes/" + aq.bc32_encode(message) - assert aq.detect_format(single) == "ur1" - session = aq.AnimatedQrSession() - session.add_part(single) - assert session.done - assert session.resolve()[0] == payload.decode() - - -def test_v1_digest_mismatch_rejected(): - frame = aq.ur1_frames(b"hello, bal", 400)[0] - tampered = frame[:-4] + "abcd" - session = aq.AnimatedQrSession() - session.add_part(tampered) - try: - session.resolve() - except aq.ChecksumError: - pass - else: - raise AssertionError("expected ChecksumError for a tampered v1 digest") - - -def test_v1_part_numbers_validated(): - for bad in ( - "ur:bytes/0of1/{}full".format("x" * 51), - "ur:bytes/2of1/{}full".format("x" * 51), - "ur:bytes/1of0/{}full".format("x" * 51), - "ur:bytes/1aof1/{}full".format("x" * 51), - ): - try: - aq.ur1_parse_part(bad) - except aq.AnimatedQrError: - pass - else: - raise AssertionError("expected AnimatedQrError for: {}".format(bad)) - - # --------------------------------------------------------------------------- # # BBQR # --------------------------------------------------------------------------- # @@ -424,41 +100,22 @@ def test_bbqr_part_number_limits(): # --------------------------------------------------------------------------- # -def test_detect_format_recognises_all_formats(): - assert aq.detect_format("BALQR1|1|1||payload") == "balqr" - assert aq.detect_format("BAL1" + "001" + "001" + "0" + "payload") == "balqr" - assert aq.detect_format(aq.ur1_frames(b"x", 400)[0]) == "ur1" - assert aq.detect_format(aq.ur2_frames(b"x", 400)[0]) == "ur2" +def test_detect_format_recognises_bbqr(): assert aq.detect_format(aq.bbqr_frames(b"x", 50)[0]) == "bbqr" - assert aq.detect_format(REF_V2_SINGLE_12) == "ur2" - assert aq.detect_format("ur:bytes/" + aq.bc32_encode(aq.cbor_byte_string(b"x"))) == "ur1" def test_detect_format_rejects_garbage(): - for text in ("", "hello world", "BALQ|1|1||a", "ur:", "ur:txn/xyz"): + for text in ("", "hello world", "BALQR1|1|1||payload", "ur:", "ur:bytes/xyz"): assert aq.detect_format(text) is None, text - # Lenient prefix probe: a string that merely *starts* with "balqr" is - # reported as balqr (the strict parse then rejects it downstream). - assert aq.detect_format("BALQRX|1|1||a") == "balqr" def test_parse_for_detection_keys(): - bal = aq.parse_for_detection("BALQR1|3|2||payload") - assert bal == ("balqr", "balqr:3", 3, 2) - # Compact v2 frame (fixed 11-char header) is detected too. - bal_v2 = aq.parse_for_detection("BAL1" + "007" + "004" + "0" + "payload") - assert bal_v2 == ("balqr", "balqr:7", 7, 4) - v2 = aq.parse_for_detection(aq.ur2_frames(b"x"*50, 400)[0]) - assert v2[0] == "ur2" and v2[2] == 1 and v2[3] == 1 - v1 = aq.parse_for_detection(aq.ur1_frames(b"x"*50, 120)[0]) - assert v1[0] == "ur1" and v1[2] > 1 and 1 <= v1[3] <= v1[2] - bb = aq.parse_for_detection(aq.bbqr_frames(b"x"*50, 40)[0]) + bb = aq.parse_for_detection(aq.bbqr_frames(b"x" * 50, 40)[0]) assert bb[0] == "bbqr" and bb[2] >= 1 and 0 <= bb[3] < bb[2] def test_format_names_exist(): - for fmt in ("balqr", "ur1", "ur2", "bbqr"): - assert aq.format_name(fmt) + assert aq.format_name("bbqr") assert aq.format_name("nope") == "nope" diff --git a/tests/test_core_qr_transfer.py b/tests/test_core_qr_transfer.py index b52aca5..64a46f2 100644 --- a/tests/test_core_qr_transfer.py +++ b/tests/test_core_qr_transfer.py @@ -1,10 +1,8 @@ """ Tests for ``bal.core.qrtransfer``. -Covers the BALQR frame encoding used for will transfer via QR codes / -audio modem: encoding, framing, reassembly, malformed input and the preset -list (optionally cross-checked against the ``qrcode`` library's EC-M -capacity when it is installed). +Covers the transfer-string encoding used for will transfer via BBQR QR codes +/ audio modem: encoding, decoding and the preset list. Run: source electrum/env/bin/activate @@ -18,34 +16,12 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) from bal.core.qrtransfer import ( CHUNK_PRESETS, - MIN_CHUNK_SIZE, - InconsistentTotalError, - MissingFramesError, - QrTransferError, - assemble, decode_transfer, encode_transfer, - parse_frame, preset_index_for_chunk_size, - split_frames, ) -def _frames(tx_strings, chunk_size, compress=False): - """Split a payload and return (payload, total, {index: payload}).""" - payload = encode_transfer(tx_strings, compress=compress) - parsed = {} - total = None - for frame in split_frames(payload, chunk_size, compressed=compress): - t, index, _compressed, p = parse_frame(frame) - if total is not None: - assert total == t - total = t - parsed[index] = p - assert total is not None - return payload, total, parsed - - # --------------------------------------------------------------------------- # # Round trips # --------------------------------------------------------------------------- # @@ -67,300 +43,6 @@ def test_empty_list_roundtrip(): assert decode_transfer(encode_transfer([]), compressed=False) == [] -# --------------------------------------------------------------------------- # -# Framing -# --------------------------------------------------------------------------- # - - -def test_single_frame(): - tx_strings = ["11" * 10] - payload = encode_transfer(tx_strings) - frames = split_frames(payload, 150) - assert len(frames) == 1 - total, index, compressed, p = parse_frame(frames[0]) - assert (total, index, compressed) == (1, 1, False) - assert p == payload - - -def test_multiple_frames_reassemble(): - tx_strings = ["ab" * 100, "cd" * 100] # 600 chars -> multiple frames - _payload, total, parsed = _frames(tx_strings, CHUNK_PRESETS[0][1]) - assert total > 1 - decoded = decode_transfer(assemble(parsed, total), compressed=False) - assert decoded == tx_strings - - -def test_size_greater_than_payload(): - tx_strings = ["12" * 5] - payload = encode_transfer(tx_strings) - frames = split_frames(payload, 1800) - assert len(frames) == 1 - _t, _i, _c, p = parse_frame(frames[0]) - assert p == payload - - -def test_exact_single_frame_boundary(): - # A 139-byte payload exactly fills the 150-byte preset budget (the 11-char - # compact header plus payload), so the encoded frame is exactly 150. - tx_strings = ["a" * 139] - payload = encode_transfer(tx_strings) - frames = split_frames(payload, 150) - assert len(frames) == 1 - assert len(frames[0]) == 150 - _t, _i, _c, p = parse_frame(frames[0]) - assert p == payload - - -def test_frames_fit_chunk_size(): - tx_strings = ["".join("{:02x}".format(i) * 2) for i in range(300)] - payload = encode_transfer(tx_strings) - for _label, size in CHUNK_PRESETS: - for frame in split_frames(payload, size): - assert len(frame) <= size, (size, len(frame)) - - -def test_single_tx_larger_than_chunk(): - # A huge serialized tx must be split over several frames and reassemble - # exactly (positional slicing is safe for hex/base64 text). - tx_strings = ["7b" * 1000] # 2000 chars - payload = encode_transfer(tx_strings) - frames = split_frames(payload, 150) - assert len(frames) > 1 - parsed = {parse_frame(f)[1]: parse_frame(f)[3] for f in frames} - total = parse_frame(frames[0])[0] - assert assemble(parsed, total) == payload - - -def test_compressed_frames_carry_flag(): - tx_strings = ["ab" * 40] - frames = split_frames(encode_transfer(tx_strings, compress=True), 150, compressed=True) - for frame in frames: - _t, _i, compressed, _p = parse_frame(frame) - assert compressed is True - # Plain frames do not. - frames_plain = split_frames(encode_transfer(tx_strings), 150) - _t, _i, compressed, _p = parse_frame(frames_plain[0]) - assert compressed is False - - -def test_compressed_roundtrip_through_frames(): - tx_strings = ["ab" * 50, "cd" * 50, "12" * 60] - payload = encode_transfer(tx_strings, compress=True) - parsed = {} - total = None - for frame in split_frames(payload, 400, compressed=True): - t, index, _c, p = parse_frame(frame) - total = t - parsed[index] = p - assert total is not None - decoded = decode_transfer(assemble(parsed, total), compressed=True) - assert decoded == tx_strings - - -# --------------------------------------------------------------------------- # -# Compact v2 wire format ("BAL1") -# --------------------------------------------------------------------------- # - - -def test_v2_frame_header_structure(): - frames = split_frames(encode_transfer(["11" * 10]), 150) - assert len(frames) == 1 - frame = frames[0] - assert frame.startswith("BAL1") - # Fixed 11-char header: magic + 3-char total + 3-char index + 1 flag. - assert len(frame) > 11 - magic, total_s, index_s, flag, payload = ( - frame[:4], - frame[4:7], - frame[7:10], - frame[10], - frame[11:], - ) - assert magic == "BAL1" - assert total_s == "001" - assert index_s == "001" - assert flag == "0" - assert payload == "11" * 10 - total, index, compressed, p = parse_frame(frame) - assert (total, index, compressed) == (1, 1, False) - assert p == payload - - -def test_v2_compressed_flag_is_z(): - frames = split_frames( - encode_transfer(["11" * 10], compress=True), 150, compressed=True - ) - assert frames[0][10] == "Z" - _t, _i, compressed, _p = parse_frame(frames[0]) - assert compressed is True - - -def test_v2_header_fixed_width_high_counts(): - # A long transfer needs multi-digit counts; the v2 header stays exactly - # 11 chars no matter how many frames (3-char base36 zero-padded counts). - tx_strings = ["ab" * 300] # 600 chars -> several frames at 150 - frames = split_frames(encode_transfer(tx_strings), 150) - assert len(frames) > 1 - for frame in frames: - # magic(4) + total(3) + index(3) + flag(1) = 11 chars, then payload. - assert len(frame) - len(frame[11:]) == 11 - - -def test_v2_max_frame_count(): - # A transfer needing more than 46655 frames must be rejected (3-char - # base36 count fields cannot represent larger totals). - from bal.core.qrtransfer import _MAX_TOTAL - - oversized = "A" * (_MAX_TOTAL * (150 - 11) + 1) - try: - split_frames(oversized, 150) - except QrTransferError: - pass - else: - raise AssertionError("expected QrTransferError above the frame cap") - - -def test_v2_boundary_at_max_count(): - from bal.core.qrtransfer import _MAX_TOTAL - - # Exactly at the cap: must still produce (bounded) frames with 3-char - # counts "VVV" (46655) for the highest serialised part. - payload = "B" * (_MAX_TOTAL * (150 - 11)) - frames = split_frames(payload, 150) - assert len(frames) == _MAX_TOTAL - total, index, _c, _p = parse_frame(frames[-1]) - assert total == _MAX_TOTAL - assert index == _MAX_TOTAL - assert frames[-1][:10] == "BAL1" + "ZZZ" + "ZZZ" - - -def test_encode_transfer_best(): - from bal.core.qrtransfer import encode_transfer_best - - # Redundant JSON-ish text compresses -> compressed (and longer source - # must round-trip unchanged). - txs = ['{"a": "%s"}' % ("x" * 300), '{"b": "%s"}' % ("y" * 300)] - transfer, compressed = encode_transfer_best(txs) - assert compressed is True - assert decode_transfer(transfer, compressed) == txs - - # Already-compact input stays plain (never larger than the source). - txs_small = ["ab", "cd"] - transfer, compressed = encode_transfer_best(txs_small) - assert compressed is False - assert decode_transfer(transfer, compressed) == txs_small - - -def test_v2_malformed_frames(): - bad = ( - "BAL1", # header only, no fields - "BAL1" + "001", # truncated - "BAL1" + "G-1" + "001" + "Z" + "p", # non-base36 total - "BAL1" + "001" + "G-1" + "Z" + "p", # non-base36 index - "BAL1" + "000" + "001" + "Z" + "p", # total 0 - "BAL1" + "001" + "000" + "Z" + "p", # index 0 - "BAL1" + "001" + "002" + "Z" + "p", # index beyond total - "BAL1" + "001" + "001" + "Q" + "p", # unknown flag - ) - for frame in bad: - try: - parse_frame(frame) - except QrTransferError: - continue - raise AssertionError("expected QrTransferError for: {!r}".format(frame)) - - -# --------------------------------------------------------------------------- # -# Malformed input -# --------------------------------------------------------------------------- # - - -def test_parse_bad_magic_and_version(): - for frame in ( - "BALQR|1|1||a", # missing version - "BALQR2|1|1||a", # unknown version - "XXXXX1|1|1||a", # unknown magic - ): - try: - parse_frame(frame) - except QrTransferError: - pass - else: - raise AssertionError("expected QrTransferError for: {}".format(frame)) - - -def test_parse_bad_arity(): - for frame in ("BALQR1", "BALQR1|1|1|"): - try: - parse_frame(frame) - except QrTransferError: - pass - else: - raise AssertionError("expected QrTransferError for: {}".format(frame)) - - -def test_parse_pipe_in_payload_is_folded(): - # maxsplit keeps the tail (including any inner '|') in the payload part. - frame = "BALQR1|1|1||a|b|c" - total, index, compressed, payload = parse_frame(frame) - assert (total, index, compressed) == (1, 1, False) - assert payload == "a|b|c" - - -def test_parse_bad_numbers(): - for frame in ( - "BALQR1|x|1||a", - "BALQR1|1|y||a", - "BALQR1|0|1||a", - "BALQR1|1|0||a", - "BALQR1|1|2||a", # index beyond total - "BALQR1|-1|1||a", - ): - try: - parse_frame(frame) - except QrTransferError: - pass - else: - raise AssertionError("expected QrTransferError for: {}".format(frame)) - - -def test_parse_bad_flags(): - try: - parse_frame("BALQR1|1|1|Q|payload") - except QrTransferError: - pass - else: - raise AssertionError("expected QrTransferError for unknown flags") - - -def test_assemble_missing_frames(): - try: - assemble({1: "a", 3: "c"}, total=3) - except MissingFramesError as e: - assert e.missing == [2] - else: - raise AssertionError("expected MissingFramesError") - - -def test_assemble_index_beyond_total(): - try: - assemble({1: "a", 2: "b"}, total=1) - except InconsistentTotalError: - pass - else: - raise AssertionError("expected InconsistentTotalError") - - -def test_assemble_order_and_total_validation(): - assert assemble({1: "a", 2: "b"}, total=2) == "ab" - try: - assemble({}, total=0) - except QrTransferError: - pass - else: - raise AssertionError("expected QrTransferError") - - # --------------------------------------------------------------------------- # # Constants / presets # --------------------------------------------------------------------------- # @@ -379,46 +61,6 @@ def test_preset_index_for_chunk_size(): assert preset_index_for_chunk_size(1800) == 3 -def test_min_chunk_size_guard(): - try: - split_frames("x" * 10, MIN_CHUNK_SIZE - 1) - except QrTransferError: - pass - else: - raise AssertionError("expected QrTransferError for tiny chunk size") - - -def test_split_frame_headers_consistent(): - tx_strings = ["ab" * 80] - payload = encode_transfer(tx_strings) - frames = split_frames(payload, 150) - totals = {parse_frame(frame)[0] for frame in frames} - assert len(totals) == 1 - assert totals.pop() == len(frames) - - -# --------------------------------------------------------------------------- # -# Optional: cross-check presets against the qrcode library (EC level M) -# --------------------------------------------------------------------------- # - - -def test_presets_fit_qrcode_ec_m(): - """Every preset budget must render inside a QR at EC level M.""" - try: - import qrcode - from qrcode.constants import ERROR_CORRECT_M - except ImportError: - print("qrcode not installed - skipping capacity check") - return - for _label, size in CHUNK_PRESETS: - # Worst-case frame: header with the largest plausible total/index plus - # a full payload of the preset budget. - frame = "BALQR1|9999|9999|Z|" + "a" * (size - 14) - qr = qrcode.QRCode(error_correction=ERROR_CORRECT_M, border=2) - qr.add_data(frame) - qr.get_matrix() # raises DataOverflowError if it does not fit - - # --------------------------------------------------------------------------- # if __name__ == "__main__": import traceback diff --git a/tests/test_gui_export_dialogs.py b/tests/test_gui_export_dialogs.py index bf48e16..b9ee382 100644 --- a/tests/test_gui_export_dialogs.py +++ b/tests/test_gui_export_dialogs.py @@ -52,9 +52,13 @@ class _Cfg: def get(self): return self._value + def set(self, value, save=True): + self._value = value + class FakePlugin: - QR_CHUNK_SIZE = _Cfg(CHUNK_PRESETS[0][1]) + def __init__(self): + self.QR_CHUNK_SIZE = _Cfg(CHUNK_PRESETS[0][1]) def read_file(self, path): return _PNG_BYTES @@ -301,6 +305,7 @@ def test_audio_export_page_send(): d = dialogs.WillExportDialog(bw, bal_plugin=bw.bal_plugin) assert d.audio_send_btn.text() == dialogs._("Send") assert d.audio_send_btn.isEnabled() + assert d.transport_audio.isEnabled() assert d.kbps_combo.count() > 0 kbps = int(d.kbps_combo.currentText()) @@ -334,6 +339,7 @@ def test_audio_export_page_plugin_missing(): bw.willitems = _make_willitems(2) d = dialogs.WillExportDialog(bw, bal_plugin=bw.bal_plugin) assert not d.audio_send_btn.isEnabled() + assert not d.transport_audio.isEnabled() assert "not available" in d.audio_warn_label.text() assert len(d.qr_page.frames) >= 1 # QR still usable assert d.file_export_btn.isEnabled() @@ -350,6 +356,7 @@ def test_audio_import_page_build(): assert d.kbps_combo.count() > 0 assert d.receive_btn.text() == dialogs._("Receive by audio…") assert d.receive_btn.isEnabled() + assert d.transport_audio.isEnabled() d.close() @@ -357,6 +364,7 @@ def test_audio_import_page_plugin_missing(): bw = FakeBalWindow(audio_plugin=None) d = dialogs.WillImportDialog(bw, bal_plugin=bw.bal_plugin) assert not d.receive_btn.isEnabled() + assert not d.transport_audio.isEnabled() assert "not available" in d.audio_warn_label.text() assert d.qr_page is not None # QR import still usable d.close() diff --git a/tests/test_gui_qr_transfer.py b/tests/test_gui_qr_transfer.py index 3d158ca..99b19d1 100644 --- a/tests/test_gui_qr_transfer.py +++ b/tests/test_gui_qr_transfer.py @@ -4,9 +4,10 @@ Tests for the QR / audio will-transfer dialogs (``bal.gui.qt.dialogs``). Covers the unified ``WillExportDialog`` (transport radios, stacked pages, QR build/navigation, chunk-size / autoplay, filter revert) and the unified ``WillImportDialog`` (QR frame capture, slot grid, complete-review enabling, -total mismatch reset, frame assembly -> decode). The wizard and the -camera/audio paths need a live wallet/hardware and are exercised only -through the shared frame-assembly path here. +total mismatch reset, frame assembly -> decode). The QR transport now uses +only the BBQR wire format. The wizard and the camera/audio paths need a live +wallet/hardware and are exercised only through the shared frame-assembly +path here. Run: QT_QPA_PLATFORM=offscreen python3 tests/test_gui_qr_transfer.py @@ -14,6 +15,7 @@ Run: import base64 import json +import os import sys from unittest.mock import MagicMock, patch @@ -23,7 +25,8 @@ from electrum.transaction import Transaction from PyQt6.QtWidgets import QApplication, QMainWindow import bal.gui.qt.dialogs as dialogs -from bal.core.qrtransfer import CHUNK_PRESETS, encode_transfer, split_frames +from bal.core.animated_qr import bbqr_frames +from bal.core.qrtransfer import CHUNK_PRESETS, encode_transfer from bal.core.will import WillItem _app = QApplication.instance() or QApplication(sys.argv) @@ -46,6 +49,15 @@ _VALID_TX_HEX = ( ) +def _rand_hex(n): + """Incompressible lowercase hex string of ``n`` random bytes.""" + return os.urandom(n).hex() + + +def _bbqr_frames(transfer, budget): + return bbqr_frames(transfer.encode("utf-8"), budget, encoding="Z") + + class _Cfg: def __init__(self, value): self._value = value @@ -53,10 +65,14 @@ class _Cfg: def get(self): return self._value + def set(self, value, save=True): + self._value = value + class FakePlugin: # Smallest QR preset: long transfers produce several frames. - QR_CHUNK_SIZE = _Cfg(CHUNK_PRESETS[0][1]) + def __init__(self): + self.QR_CHUNK_SIZE = _Cfg(CHUNK_PRESETS[0][1]) def read_file(self, path): return _PNG_BYTES @@ -109,7 +125,7 @@ class StubWillItem: def _make_willitems(n=3, payload_len=120, payloads=None): if payloads is None: - payloads = ["T{}".format(i) * payload_len for i in range(n)] + payloads = [_rand_hex(payload_len) for _ in range(n)] return { "item{}".format(i): StubWillItem(p) for i, p in enumerate(payloads) @@ -129,8 +145,9 @@ def test_export_dialog_builds(): assert page.tx_strings assert page.frames assert len(page.frames) >= 1 - # Frame 1 is shown. + # Frame 1 is shown, and it is a BBQR frame. assert page.qr_view.text == page.frames[0] + assert page.frames[0].startswith("B$") assert "1" in page.progress_label.text() d.close() @@ -196,6 +213,7 @@ def test_export_auto_scroll(): d = dialogs.WillExportDialog(bw, bal_plugin=bw.bal_plugin, initial_mode="qr") page = d.qr_page assert page.fps_spin is not None + assert len(page.frames) > 1 assert not page.auto_timer.isActive() page.fps_spin.setValue(2) @@ -222,6 +240,7 @@ def test_export_auto_scroll_loop(): d = dialogs.WillExportDialog(bw, bal_plugin=bw.bal_plugin, initial_mode="qr") page = d.qr_page assert page.loop_check is not None + assert len(page.frames) > 1 # Loop on: reaching the last code wraps back to the first and keeps going. page.loop_check.setChecked(True) @@ -244,9 +263,9 @@ def test_export_auto_scroll_loop(): def test_export_filter_valid_and_valid_nc(): bw = FakeBalWindow() - a = StubWillItem("A" * 120, statuses={"VALID": True, "COMPLETE": True}) - b = StubWillItem("B" * 120, statuses={"VALID": True}) - c = StubWillItem("C" * 120) + a = StubWillItem(_rand_hex(120), statuses={"VALID": True, "COMPLETE": True}) + b = StubWillItem(_rand_hex(120), statuses={"VALID": True}) + c = StubWillItem(_rand_hex(120)) bw.willitems = {"a": a, "b": b, "c": c} d = dialogs.WillExportDialog(bw, bal_plugin=bw.bal_plugin, initial_mode="qr") page = d.qr_page @@ -263,11 +282,11 @@ def test_export_filter_valid_and_valid_nc(): # "Valid" filter -> only the valid items (a, b). d._on_filter_change(1) - assert sorted(page.tx_strings) == ["A" * 120, "B" * 120] + assert sorted(page.tx_strings) == sorted([str(a.tx), str(b.tx)]) # "Valid NC" filter -> only the valid, not-complete item (b). d._on_filter_change(2) - assert sorted(page.tx_strings) == ["B" * 120] + assert sorted(page.tx_strings) == [str(b.tx)] assert page.qr_view.text == page.frames[0] d.close() @@ -276,7 +295,7 @@ def test_export_filter_empty_reverts(): bw = FakeBalWindow() # Only a COMPLETE valid item: "Valid NC" selects nothing -> revert. bw.willitems = { - "a": StubWillItem("A" * 120, statuses={"VALID": True, "COMPLETE": True}) + "a": StubWillItem(_rand_hex(120), statuses={"VALID": True, "COMPLETE": True}) } d = dialogs.WillExportDialog(bw, bal_plugin=bw.bal_plugin, initial_mode="qr") messages = [] @@ -290,17 +309,11 @@ def test_export_filter_empty_reverts(): def test_export_navigation_and_chunk_change(): - # Low-redundancy serialized transactions resist deflate, so even the - # compressed best-of transfer still needs several frames at the default - # chunk and navigation across frames is exercised. - def noisy(pad): - return "".join("{:02x}".format((pad * 31 + j * 101 + j * j) % 256) for j in range(200)) - + # Incompressible serialized transactions resist deflate, so the BBQR + # transfer still needs several frames at the default chunk and navigation + # across frames is exercised. bw = FakeBalWindow() - bw.willitems = _make_willitems( - n=6, payload_len=400, - payloads=["{}0{}".format(noisy(i), "T" * 50) for i in range(6)], - ) + bw.willitems = _make_willitems(n=6, payload_len=400) d = dialogs.WillExportDialog(bw, bal_plugin=bw.bal_plugin, initial_mode="qr") page = d.qr_page first_count = len(page.frames) @@ -322,6 +335,20 @@ def test_export_navigation_and_chunk_change(): d.close() +def test_export_size_change_is_remembered(): + # Changing the QR size inside the export dialog persists the choice so the + # next export starts from the last selected size. + bw = FakeBalWindow() + bw.willitems = _make_willitems(n=6, payload_len=400) + d = dialogs.WillExportDialog(bw, bal_plugin=bw.bal_plugin, initial_mode="qr") + page = d.qr_page + assert bw.bal_plugin.QR_CHUNK_SIZE.get() == dialogs.CHUNK_PRESETS[0][1] + + page._on_chunk_change(len(dialogs.CHUNK_PRESETS) - 1) + assert bw.bal_plugin.QR_CHUNK_SIZE.get() == dialogs.CHUNK_PRESETS[-1][1] + d.close() + + # ------------------------------------------------------------------ # # WillImportDialog (QR transport via d.qr_page) # ------------------------------------------------------------------ # @@ -333,8 +360,8 @@ def test_import_frame_flow(): assert not page.review_btn.isEnabled() assert not page.slot_area.isVisible() - transfer = encode_transfer(["A" * 120, "B" * 120, "C" * 120]) - frames = split_frames(transfer, 150) + transfer = encode_transfer([_rand_hex(120), _rand_hex(120), _rand_hex(120)]) + frames = _bbqr_frames(transfer, 150) assert len(frames) > 1 for frame in frames: @@ -361,12 +388,14 @@ def test_import_assembles_and_decodes(): captured["payload"] = payload page._finish_import = fake_finish - transfer = encode_transfer(["P" * 130, "Q" * 130]) - for frame in split_frames(transfer, 150): + tx_a = _rand_hex(130) + tx_b = _rand_hex(130) + transfer = encode_transfer([tx_a, tx_b]) + for frame in _bbqr_frames(transfer, 150): page._add_frame(frame) page._review_and_sign() # The payload is rejoined into an opaque string for autodetect. - assert captured["payload"].split("\n") == ["P" * 130, "Q" * 130] + assert captured["payload"].split("\n") == [tx_a, tx_b] d.close() @@ -407,7 +436,7 @@ def test_whole_will_qr_roundtrip(): transfer = encode_transfer(page.tx_strings) impl = dialogs.WillImportDialog(bw, bal_plugin=bw.bal_plugin) import_page = impl.qr_page - for frame in split_frames(transfer, dialogs.CHUNK_PRESETS[0][1]): + for frame in _bbqr_frames(transfer, dialogs.CHUNK_PRESETS[0][1]): import_page._add_frame(frame) assert import_page.review_btn.isEnabled() caught = {} @@ -431,8 +460,10 @@ def test_import_total_mismatch_resets(): warnings = [] page.show_warning = lambda msg: warnings.append(msg) # type: ignore[assignment] - frames_a = split_frames(encode_transfer(["A" * 120, "B" * 120]), 150) - frames_b = split_frames(encode_transfer(["A" * 120, "B" * 120, "C" * 120]), 150) + frames_a = _bbqr_frames(encode_transfer([_rand_hex(120), _rand_hex(120)]), 150) + frames_b = _bbqr_frames( + encode_transfer([_rand_hex(120), _rand_hex(120), _rand_hex(120)]), 150 + ) for frame in frames_a: page._add_frame(frame) assert page.total == len(frames_a) @@ -451,7 +482,7 @@ def test_import_manual_entry(): bw = FakeBalWindow() d = dialogs.WillImportDialog(bw, bal_plugin=bw.bal_plugin) page = d.qr_page - frames = split_frames(encode_transfer(["M" * 120]), 150) + frames = _bbqr_frames(encode_transfer(["M" * 20]), 150) page.manual_edit.setText(frames[0]) page._add_from_manual() assert page.manual_edit.text() == "" @@ -477,48 +508,48 @@ def _fresh_debounce(): def test_qr_import_debounce_pending_then_accept(): s = _fresh_debounce() # First sighting of a new identity: pending, not yet stored. - assert dialogs.qr_import_accept_frame(s, "balqr", "balqr:2", 2, 1, "PAYLOAD1") == "pending" + assert dialogs.qr_import_accept_frame(s, "bbqr", "bbqr:ZB:2", 2, 0, "PAYLOAD1") == "pending" assert s["pending_count"] == 1 assert s["last_index"] is None # A second stable read of the same identity: accepted. - assert dialogs.qr_import_accept_frame(s, "balqr", "balqr:2", 2, 1, "PAYLOAD1") == "accept" - assert s["last_index"] == 1 + assert dialogs.qr_import_accept_frame(s, "bbqr", "bbqr:ZB:2", 2, 0, "PAYLOAD1") == "accept" + assert s["last_index"] == 0 assert s["last_payload"] == "PAYLOAD1" assert s["pending_count"] == 0 def test_qr_import_debounce_re_reading_last_is_ignored(): s = _fresh_debounce() - dialogs.qr_import_accept_frame(s, "balqr", "balqr:2", 2, 1, "P1") - dialogs.qr_import_accept_frame(s, "balqr", "balqr:2", 2, 1, "P1") # accepted - # The exporter is still showing frame 1: must be ignored, not accepted. - assert dialogs.qr_import_accept_frame(s, "balqr", "balqr:2", 2, 1, "P1") == "ignore" - assert s["last_index"] == 1 + dialogs.qr_import_accept_frame(s, "bbqr", "bbqr:ZB:2", 2, 0, "P1") + dialogs.qr_import_accept_frame(s, "bbqr", "bbqr:ZB:2", 2, 0, "P1") # accepted + # The exporter is still showing frame 0: must be ignored, not accepted. + assert dialogs.qr_import_accept_frame(s, "bbqr", "bbqr:ZB:2", 2, 0, "P1") == "ignore" + assert s["last_index"] == 0 assert s["pending_count"] == 0 def test_qr_import_debounce_transition_pending_resets(): s = _fresh_debounce() - dialogs.qr_import_accept_frame(s, "balqr", "balqr:2", 2, 1, "P1") - dialogs.qr_import_accept_frame(s, "balqr", "balqr:2", 2, 1, "P1") # accept frame 1 + dialogs.qr_import_accept_frame(s, "bbqr", "bbqr:ZB:2", 2, 0, "P1") + dialogs.qr_import_accept_frame(s, "bbqr", "bbqr:ZB:2", 2, 0, "P1") # accept frame 0 # A new identity interrupts the pending accumulation. - assert dialogs.qr_import_accept_frame(s, "balqr", "balqr:2", 2, 2, "P2") == "pending" - assert dialogs.qr_import_accept_frame(s, "balqr", "balqr:2", 2, 2, "P2") == "accept" + assert dialogs.qr_import_accept_frame(s, "bbqr", "bbqr:ZB:2", 2, 1, "P2") == "pending" + assert dialogs.qr_import_accept_frame(s, "bbqr", "bbqr:ZB:2", 2, 1, "P2") == "accept" # Same-index duplicate with different payload is treated as new identity. - assert dialogs.qr_import_accept_frame(s, "balqr", "balqr:2", 2, 2, "P2") == "ignore" + assert dialogs.qr_import_accept_frame(s, "bbqr", "bbqr:ZB:2", 2, 1, "P2") == "ignore" def test_qr_import_debounce_total_mismatch_resets(): s = _fresh_debounce() # In-range frame is accepted even though its declared total is ignored. - assert dialogs.qr_import_accept_frame(s, "balqr", "balqr:3", 3, 2, "P2") == "pending" - assert dialogs.qr_import_accept_frame(s, "balqr", "balqr:3", 3, 2, "P2") == "accept" + assert dialogs.qr_import_accept_frame(s, "bbqr", "bbqr:ZB:3", 3, 1, "P2") == "pending" + assert dialogs.qr_import_accept_frame(s, "bbqr", "bbqr:ZB:3", 3, 1, "P2") == "accept" # A frame that belongs to a different transfer. - assert dialogs.qr_import_accept_frame(s, "balqr", "balqr:4", 4, 2, "P2B") == "reset" + assert dialogs.qr_import_accept_frame(s, "bbqr", "bbqr:ZB:4", 4, 1, "P2B") == "reset" # Once the policy is rebased on the new transfer, frames resume normally # (the widget clears the debounce while wiping the import). s["key"] = None - assert dialogs.qr_import_accept_frame(s, "balqr", "balqr:4", 4, 2, "P2B") == "pending" + assert dialogs.qr_import_accept_frame(s, "bbqr", "bbqr:ZB:4", 4, 1, "P2B") == "pending" def test_qr_import_handle_scanned_text_autofinish(): @@ -528,7 +559,7 @@ def test_qr_import_handle_scanned_text_autofinish(): reviewed = [] page._review_and_sign = lambda: reviewed.append(True) # type: ignore[assignment] - frames = split_frames(encode_transfer(["A" * 120, "B" * 120]), 150) + frames = _bbqr_frames(encode_transfer([_rand_hex(120), _rand_hex(120)]), 150) assert len(frames) > 1 # The camera session is running and every frame needs two stable reads. @@ -557,7 +588,7 @@ def test_qr_import_handle_scanned_text_manual_does_not_autofinish(): reviewed = [] page._review_and_sign = lambda: reviewed.append(True) # type: ignore[assignment] - frames = split_frames(encode_transfer(["A" * 120, "B" * 120]), 150) + frames = _bbqr_frames(encode_transfer([_rand_hex(120), _rand_hex(120)]), 150) assert len(frames) > 1 assert not page._scanning for frame in frames: @@ -570,54 +601,16 @@ def test_qr_import_handle_scanned_text_manual_does_not_autofinish(): # ------------------------------------------------------------------ # -# Animated-QR formats (BC-UR v1/v2, BBQR) via the export/import pages +# BBQR import/export round trips # ------------------------------------------------------------------ # -def test_export_format_combo_switches_codecs(): - bw = FakeBalWindow() - bw.willitems = _make_willitems(n=6, payload_len=400) - d = dialogs.WillExportDialog(bw, bal_plugin=bw.bal_plugin, initial_mode="qr") - page = d.qr_page - assert page.format == "balqr" - assert page.frames[0].startswith("BAL1") - assert page.format_combo.count() == 4 - - page._on_format_change(1) # BC-UR v1 - assert page.format == "ur1" - assert page.frames[0].startswith("ur:bytes/") - assert page.index == 0 - assert page.qr_view.text == page.frames[0] - - page._on_format_change(2) # BC-UR v2 - assert page.format == "ur2" - assert page.frames[0].startswith("ur:bytes/") - - page._on_format_change(3) # BBQR - assert page.format == "bbqr" - assert page.frames[0].startswith("B$") - d.close() - - -def test_export_animated_format_frames_fit_budget(): - bw = FakeBalWindow() - bw.willitems = _make_willitems(n=6, payload_len=400) - d = dialogs.WillExportDialog(bw, bal_plugin=bw.bal_plugin, initial_mode="qr") - page = d.qr_page - for index in range(1, 4): - page._on_format_change(index) - for frame in page.frames: - assert len(frame) <= dialogs.CHUNK_PRESETS[0][1] - d.close() - - -def _import_roundtrip_fmt(fmt_index): +def test_import_bbqr_roundtrip(): bw = FakeBalWindow() bw.willitems = _make_willitems(2) d = dialogs.WillExportDialog(bw, bal_plugin=bw.bal_plugin, initial_mode="qr") page = d.qr_page - page._on_format_change(fmt_index) frames = list(page.frames) - assert frames + assert frames and all(f.startswith("B$") for f in frames) d.close() impl = dialogs.WillImportDialog(bw, bal_plugin=bw.bal_plugin) @@ -634,24 +627,11 @@ def _import_roundtrip_fmt(fmt_index): impl.close() -def test_import_ur1_roundtrip(): - _import_roundtrip_fmt(1) - - -def test_import_ur2_roundtrip(): - _import_roundtrip_fmt(2) - - -def test_import_bbqr_roundtrip(): - _import_roundtrip_fmt(3) - - def test_import_animated_scan_debounce_autofinish(): bw = FakeBalWindow() bw.willitems = _make_willitems(2) d = dialogs.WillExportDialog(bw, bal_plugin=bw.bal_plugin, initial_mode="qr") page = d.qr_page - page._on_format_change(2) # BC-UR v2 fountain frames = list(page.frames) d.close() @@ -664,11 +644,8 @@ def test_import_animated_scan_debounce_autofinish(): for _rep in range(2): import_page._handle_scanned_text(frame) assert import_page.review_btn.isEnabled() - # The fountain transfer's part count is ``len(frames) // 2`` (pure + one - # redundant mixed wave). - assert import_page.total == len(frames) // 2 + assert import_page.total == len(frames) assert len(import_page.frames) >= import_page.total - assert import_page.review_btn.isEnabled() _app.processEvents() assert reviewed == [True] assert not import_page._scanning @@ -679,7 +656,7 @@ def test_import_garbage_scan_is_ignored(): bw = FakeBalWindow() d = dialogs.WillImportDialog(bw, bal_plugin=bw.bal_plugin) page = d.qr_page - for garbage in ("hello world", "12345", "B$ZZ", ""): + for garbage in ("hello world", "12345", "B$ZZ", "ur:bytes/xyz", ""): page._handle_scanned_text(garbage) assert not page.frames assert page.total == 0 @@ -694,8 +671,10 @@ def test_import_different_animated_transfer_resets(): warnings = [] page.show_warning = lambda msg: warnings.append(msg) # type: ignore[assignment] - frames_a = split_frames(encode_transfer(["A" * 120, "B" * 120]), 150) - frames_b = split_frames(encode_transfer(["A" * 120, "B" * 120, "C" * 120]), 150) + frames_a = _bbqr_frames(encode_transfer([_rand_hex(120), _rand_hex(120)]), 150) + frames_b = _bbqr_frames( + encode_transfer([_rand_hex(120), _rand_hex(120), _rand_hex(120)]), 150 + ) for frame in frames_a: page._add_frame(frame) assert page.total == len(frames_a)