4 Commits

Author SHA1 Message Date
346aeee62b Merge remote-tracking branch 'origin/main' 2026-10-08 19:01:23 -04:00
3b2eed37cc i18n: fix Babel format-string error on the heirs wizard text
The literal '%' in "100% of wallet balance" was detected as a Python
format specifier (%o in English vs %d in the translations), so
pybabel compile failed and the catalogs could not be built. Reword to
put '100%' at the end of the sentence and refresh es_ES/it_IT.
2026-10-08 19:00:51 -04:00
0c60462163 Merge pull request 'i18n review fixes (#63)' (#10) from fix/i18n-review into main
Reviewed-on: #10
2026-10-08 20:43:21 +00:00
a81443ff67 i18n review fixes (#63)
Truman's i18n review: release gate runs the i18n, CLI and import tests and checks Babel first; ruff known-first-party = bal; no translated text stored in the wallet (executor push records are copies, status history always saved as 'New', old 'Nuovo'/'Nuevo' mapped back on load).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01HxDDvLym1NEbFNYMc1rr7S
2026-10-07 15:03:25 +02:00
14 changed files with 626 additions and 1424 deletions

View File

@@ -3587,198 +3587,28 @@ delivery; when it is not, the plugin raises an explicit error (see below).
covers every catalog's translation of "New", the push helper does not covers every catalog's translation of "New", the push helper does not
modify the will item, old transient fields dropped on load. modify the will item, old transient fields dropped on load.
**Verification:** extended release test list (as above) plus the new test **Scope:** this entry contains ONLY the i18n review fixes. The other fixes
files: 489 passed, 1 failed (pre-existing: `test_group_e_mock_karen7::test_e5` prepared in the same session (invalidation fee, Check `KeyError`, cascade,
needs `tests/karen7`, not in the repository; `main` 827a1d1 gives 456 passed, deselected executor) are not part of it and will be proposed separately.
same failure). `ruff check bal/ tests/`: the same 3 pre-existing I001 as on
`main` (`dialogs.py`, `test_core_animated_qr.py`, `test_core_qr_transfer.py`), **Verification (on `main` 827a1d1):**
- New test file `tests/test_core_status_language_neutral.py`: 5 of 7 fail on
`main`, 7 of 7 pass with this change.
- Extended release test list (as above) plus the new test file: 446 passed,
1 failed (`main`: 439 passed, same failure). The failure is pre-existing:
`test_group_e_mock_karen7::test_e5` needs `tests/karen7`, a wallet file that
is not in the repository.
- Whole `tests/` directory: 609 passed, 26 failed, 3 collection errors
(`main`: 602 passed, and the SAME 26 failures and 3 errors, test by test).
All pre-existing and unrelated: 9 need `tests/karen7`; 21 in
`test_auto_rebuild_on_new_tx.py`, `test_cli_autorebuild.py` and
`test_cli_controller_offline.py` fail only when the whole directory runs
together (they pass when each file runs alone); 3 audio tests need an audio
device. `test_gui_export_dialogs.py::test_audio_import_receive_wiring` hangs
forever, also on `main`, so it was deselected.
- `ruff check bal/ tests/`: the same 3 pre-existing I001 as on `main`
(`dialogs.py`, `test_core_animated_qr.py`, `test_core_qr_transfer.py`),
none new. none new.
**Outcome:** DONE (pending the owner's test of the ZIP and Truman's review). **Outcome:** DONE (pending Truman's review).
---
## 64. Invalidation pays the current network fee (2 blocks) instead of `baltx_fees`
**Date:** 2026-09-26 (ported to `main` 827a1d1 on 2026-10-07)
**Goal (owner request):** the will's `baltx_fees` (default 20 sat/vbyte) is
deliberately high because the inheritance transactions may be broadcast by the
will-executors years from now, when network fees may be much higher. The
on-chain INVALIDATION, instead, is sent TODAY: it spends the will's coins back
to the wallet so the old pre-signed inheritance transactions become unusable.
It must therefore pay the current network fee, not the inheritance rate. After
the invalidation the plugin rebuilds the new inheritance with `baltx_fees` as
before (unchanged).
**What changed:**
- `bal/core/will.py`
- New constant `INVALIDATION_FEE_ETA_BLOCKS = 2` (owner decision: confirm
within 2 blocks, Electrum's own default target).
- New `Will.get_invalidation_fee_rate(wallet, fallback_fee_per_byte)`:
returns Electrum's live estimate for 2 blocks
(`network.fee_estimates.eta_target_to_fee`, sat/kvbyte → sat/vbyte), or
the fallback (the will's `baltx_fees`, i.e. the previous behaviour) when
there is no usable estimate: offline wallet, estimates not received yet,
network error or an invalid value. Never raises.
- `Will.invalidate_will`: uses that rate; the total fee is rounded UP to
whole satoshis (network rates can be fractional); logs the chosen rate and
its source (`network` / `fallback`). The `fees_per_byte` argument is now
only the fallback, so every caller (Tools → Invalidate, wizard
postpone/expired paths, automatic invalidation, CLI `bal_will_invalidate`)
gets the new behaviour without being changed. Docstrings added.
- `tests/test_core_invalidation_fee.py` (new): rate selection (2-block target,
network value used as is also above `baltx_fees`, fractional rates, fallback
when offline / no estimate / error / invalid values) and the fee actually
deducted by `invalidate_will` (network vs fallback, rounding up).
**Why no special case for an already-expired will:** discussed with the owner
and intentionally left out. Before its locktime the old inheritance tx cannot
enter a block or the mempool, so the invalidation has no competitor. RBF stays
enabled on the invalidation, so its fee can be raised from Electrum's History
("Increase fee") if it ever gets stuck.
**Verification:**
- `tests/test_core_invalidation_fee.py`: 12 passed (pytest and standalone).
- `make-release.sh` test list + `tests/test_cli_*.py`: 435 passed, 1 failed.
The failure is pre-existing and unrelated: `test_group_e_mock_karen7::test_e5`
needs `tests/karen7`, a wallet file that is not in the repository.
`tests/test_import_will_details.py`: 17 passed.
- `ruff check` clean on the touched files.
- Live, Electrum 4.8.2 testnet, offline daemon: `bal_will_invalidate` →
"invalidation fee: 20 sat/vbyte (fallback), total 2200 sat" (unchanged
behaviour when offline).
- Real Electrum wallet + real `FeeTimeEstimates` (1 input, 110 vbytes):
market 3 sat/vbyte → 330 sat; 1.5 → 165 sat; 0.4 → 110 sat (Electrum clamps
to its 1 sat/vbyte minimum relay fee); 45 → 4950 sat; no 2-block estimate →
fallback 20 sat/vbyte (2200 sat).
- API checked in Electrum 4.7.2, 4.8.0 and 4.8.2
(`FeeTimeEstimates.eta_target_to_fee`, `network.fee_estimates`).
**Notes:** on regtest Electrum returns a fixed 150 sat/vbyte when it has no
estimate, so regtest invalidations will pay that rate.
**Outcome:** DONE (pending the owner's test of the ZIP).
---
## 65. Check crashed with `KeyError: '<txid>'` (stale "father" link in the will tree)
**Date:** 2026-09-27
**Symptom (owner's mainnet wallet, 23 will items, Electrum 4.8.2):** the Check
wizard stopped at "Checking your will" with `Error: 'a65a682f…'`, and the
window refresh logged "error while updating window" with the same txid:
```
File "bal/core/will.py", line 657, in check_invalidated
or willtree[w.father].get_status("CONFIRMED")
KeyError: 'a65a682f2975f0dffd9eccce9392833f227badf617fc67815674736d594d4115'
```
**Root cause:** every will item stores its parent will tx in `father`, which is
persisted in the wallet (`"Father"` in `WillItem.to_dict`). `Will.add_willtree`
only ever SET it (`if not father: father = ...`) and never cleared it, so when
the parent tx was later removed from the will (rebuild, merge, replacement) the
child kept pointing to a txid that no longer exists. `check_invalidated` then
indexed the will with it. Reproduced on unmodified `main` (not related to #64).
**What changed:**
- `bal/core/will.py`
- `Will.add_willtree`: resets `father` on every item before rebuilding the
links, so the tree is always recomputed from the transactions themselves
(a stale value loaded from the wallet is discarded). Docstring added.
- `Will.check_invalidated`: defensive lookup - a father that is not (or no
longer) in the will is treated as "no father" instead of raising.
- `tests/test_core_willtree_father.py` (new): stale father discarded / replaced
by the real parent, no crash in `check_invalidated` (with and without the
tree rebuild), real parent/child link built, child of an unconfirmed parent
still skipped. 4 of the 6 tests fail on `main` and pass with the fix; the 2
"normal behaviour" tests pass on both.
**Verification:** make-release test list + `tests/test_cli_*.py` +
`tests/test_import_will_details.py`: 458 passed, 1 failed (pre-existing,
unrelated: `test_group_e_mock_karen7::test_e5` needs the missing `tests/karen7`
wallet file). `ruff check` clean on the touched files.
**Found while writing the tests, NOT fixed here (reported to the owner):**
`Will.set_invalidate` iterates `children.items()`, but `add_willtree` builds
`children` as a LIST, so invalidating an item that has child will txs raises
`AttributeError: 'list' object has no attribute 'items'` (reproduced on
`main`). The existing tests in `test_core_will_invalidate.py` set `children` to
dicts by hand, so they do not see it.
**Outcome:** DONE (pending the owner's test of the ZIP).
---
## 66. Invalidation cascade crashed on real will trees (`children` is a list)
**Date:** 2026-09-27
**Problem (found while fixing #65, reproduced on `main`):**
`Will.set_invalidate` iterated `children.items()`, but `Will.add_willtree`
builds `children` as a LIST of `[child_id, input_idx, prevout_idx]`, so
invalidating a will tx that has child will txs raised
`AttributeError: 'list' object has no attribute 'items'`. The existing tests
set `children` to dicts by hand, so they never saw it.
**What changed:** `bal/core/will.py` - `set_invalidate` accepts both shapes
(list from `add_willtree`, dict from older data/tests) and only recurses into
children that are still in the will. `tests/test_core_willtree_father.py`: new
test `test_set_invalidate_cascades_to_children_built_by_add_willtree` (fails
on `main`, passes with the fix).
**Verification:** 459 passed, 1 failed (pre-existing missing `tests/karen7`);
`ruff check` clean.
**Outcome:** DONE (included in the owner's test ZIP of 2026-09-27).
---
## 67. Deselected will-executor: its tx can be deleted and is no longer pushed
**Date:** 2026-09-27
**Problem (owner, mainnet wallet):** the will had a signed tx for
`we.divinaly.support` whose push had failed ("Push failed", "Not on server").
After deselecting that executor, Check still reported the will as valid, the
row stayed, and "delete" in the context menu was disabled (valid + signed
txs were never deletable). The broadcast also still targeted the deselected
executor, because each will item keeps its own build-time copy of the
executor record (`selected` = True).
**What changed (owner chose option "B"):**
- `bal/gui/qt/lists.py` - `_can_delete`: a valid, signed tx is now deletable
when its will-executor NEVER received it (not PUSHED and not CHECKED). It
stays protected when the executor holds it (deleting it locally would not
cancel it on the server) and when it has no executor (backup tx).
- `bal/core/willexecutors.py` - `get_willexecutor_transactions(...,
current_willexecutors=None)`: when the current executor list is given, an
executor that is no longer selected (or no longer listed) is skipped.
- `bal/gui/qt/window.py` (manual broadcast) and `bal/cli/controller.py`
(`bal_will_broadcast`) pass the current list read from the config. The
wizard already filtered by the current selection.
- Tests: `tests/test_gui_will_menu.py` (+3: deletable when never received,
protected when pushed/checked, protected backup tx) and new
`tests/test_core_deselected_executor.py` (4).
**Not changed (proposal for Truman, option "A"):** Check still does not treat
a tx of a deselected executor as "not coherent" (no automatic rebuild).
**Verification:** 483 passed, 1 failed (pre-existing missing `tests/karen7`);
`ruff check` clean on the touched files. Live (Electrum 4.8.2 testnet, CLI):
will built for 2 executors, one deselected → `bal_will_broadcast` targets only
the selected one (before the fix it targeted both).
**Outcome:** DONE (pending the owner's test of the ZIP).
**Porting note (entries 64-67):** first written on `main` 4a33116 as
58-61 (numbers then taken by the i18n and Check Alive work), re-applied on
`main` 827a1d1 on 2026-10-07 together with #63: extended release test list
489 passed, 1 pre-existing failure (missing `tests/karen7`); `ruff` no new
errors.

View File

@@ -767,11 +767,7 @@ class BalController:
if not willitems: if not willitems:
raise UserFacingException(_("No transaction matches the given txids")) raise UserFacingException(_("No transaction matches the given txids"))
# Skip executors the user deselected after building the will (the willexecutors = Willexecutors.get_willexecutor_transactions(willitems, force=force)
# controller's list is read from the config for every command).
willexecutors = Willexecutors.get_willexecutor_transactions(
willitems, force=force, current_willexecutors=self.willexecutors
)
if not willexecutors: if not willexecutors:
return {} return {}

View File

@@ -26,7 +26,6 @@ The status flags themselves (the source of truth) stay here; only the mapping
"status -> colour" now lives in the GUI layer. No behaviour changed. "status -> colour" now lives in the GUI layer. No behaviour changed.
""" """
import math
from datetime import datetime, timezone from datetime import datetime, timezone
from electrum.address_synchronizer import TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL from electrum.address_synchronizer import TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL
@@ -51,13 +50,6 @@ from .willexecutors import Willexecutors
MIN_LOCKTIME = 1 MIN_LOCKTIME = 1
MIN_BLOCK = 1 MIN_BLOCK = 1
# Confirmation target (in blocks) for the on-chain INVALIDATION transaction.
# WHY: the invalidation is sent TODAY (it spends the will's coins so the old
# pre-signed inheritance txs become unusable), so it must pay the CURRENT
# network fee for a quick confirmation - not ``baltx_fees``, which is a
# deliberately high rate reserved for inheritance txs that may be broadcast
# years from now. 2 blocks is Electrum's own default target (owner decision).
INVALIDATION_FEE_ETA_BLOCKS = 2
_logger = get_logger(__name__) _logger = get_logger(__name__)
@@ -76,22 +68,6 @@ class Will:
# build a tree with parent transactions # build a tree with parent transactions
@staticmethod @staticmethod
def add_willtree(will): def add_willtree(will):
"""(Re)build the parent/child links between the will transactions.
Every item gets ``children`` (the will txs spending its outputs) and,
when it spends the output of another will tx, a ``father``.
WHY the reset below: ``father`` is persisted in the wallet (the
``"Father"`` field of ``WillItem.to_dict``) and was only ever set,
never cleared. When the father tx was later removed from the will
(rebuild, merge, replacement) the child kept pointing to a txid that
no longer exists, and ``check_invalidated`` crashed with a
``KeyError`` on that txid (reported on a real wallet: the Check
wizard and the window refresh both failed). The tree is therefore
always recomputed from scratch from the transactions themselves.
"""
for willid in will:
will[willid].father = None
for willid in will: for willid in will:
will[willid].children = Will.get_children(will, willid) will[willid].children = Will.get_children(will, willid)
for child in will[willid].children: for child in will[willid].children:
@@ -462,68 +438,9 @@ class Will:
out[inp.prevout.to_str()] = inp out[inp.prevout.to_str()] = inp
return out return out
@staticmethod
def get_invalidation_fee_rate(wallet, fallback_fee_per_byte):
"""Return ``(fee_rate, source)`` for the on-chain invalidation tx.
``fee_rate`` is in sat/vbyte. ``source`` is ``"network"`` when the rate
comes from Electrum's live fee estimate, ``"fallback"`` otherwise.
WHY: the invalidation is broadcast NOW, so it pays the current network
fee for a confirmation within ``INVALIDATION_FEE_ETA_BLOCKS`` blocks.
The will's ``baltx_fees`` is kept only as a fallback, used when no
estimate is available (offline wallet, estimates not received yet,
server error), which is exactly the behaviour of previous versions.
The estimate is read from ``network.fee_estimates.eta_target_to_fee``
(Electrum 4.7.2 / 4.8.x), which returns sat/kvbyte and already clamps
the value between Electrum's conservative minimum relay fee (1
sat/vbyte) and its maximum dynamic fee. This method never raises.
Args:
wallet: The Electrum wallet (its ``network`` may be ``None``).
fallback_fee_per_byte: Rate (sat/vbyte) used when there is no
usable estimate - normally the will's ``baltx_fees``.
"""
network = getattr(wallet, "network", None) if wallet else None
fee_per_kb = None
if network is not None:
try:
fee_per_kb = network.fee_estimates.eta_target_to_fee(
INVALIDATION_FEE_ETA_BLOCKS
)
except Exception as e:
_logger.warning(f"invalidation fee: no network estimate ({e!r})")
fee_per_kb = None
# Accept only a real positive integer (Electrum's type for sat/kvbyte);
# ``bool`` is excluded explicitly because it is a subclass of ``int``.
if (
isinstance(fee_per_kb, int)
and not isinstance(fee_per_kb, bool)
and fee_per_kb > 0
):
return fee_per_kb / 1000, "network"
return fallback_fee_per_byte, "fallback"
@staticmethod @staticmethod
def invalidate_will(will, wallet, fees_per_byte, history_label=None, def invalidate_will(will, wallet, fees_per_byte, history_label=None,
will_locktime=None): will_locktime=None):
"""Build the (unsigned) on-chain invalidation transaction.
The transaction spends every coin used by the valid will items back to
one of the wallet's own change addresses, so the pre-signed
inheritance transactions held by the will-executors become unusable.
Fee: the CURRENT network fee for a confirmation within
``INVALIDATION_FEE_ETA_BLOCKS`` blocks (see
:meth:`get_invalidation_fee_rate`). ``fees_per_byte`` (the will's
``baltx_fees``) is only the FALLBACK used when no network estimate is
available. RBF stays enabled, so the fee can still be raised from
Electrum's History ("Increase fee") if the transaction gets stuck.
Returns the ``PartialTransaction``, or ``None`` when there is nothing
to invalidate or the balance does not cover the fee.
"""
_logger.debug("invalidate tx in will module") _logger.debug("invalidate tx in will module")
will_only_valid = Will.only_valid_list(will) will_only_valid = Will.only_valid_list(will)
inputs = Will.get_all_inputs(will_only_valid) inputs = Will.get_all_inputs(will_only_valid)
@@ -560,17 +477,7 @@ class Will:
utxo_to_spend, [out], locktime=locktime, version=2 utxo_to_spend, [out], locktime=locktime, version=2
) )
tx.set_rbf(True) tx.set_rbf(True)
fee_rate, fee_source = Will.get_invalidation_fee_rate( fee = tx.estimated_size() * fees_per_byte
wallet, fees_per_byte
)
# The network rate can be fractional (e.g. 1.5 sat/vbyte): round
# the total fee UP to whole satoshis so the output value is an
# integer and the rate is never below the one requested.
fee = int(math.ceil(tx.estimated_size() * fee_rate))
_logger.info(
f"invalidation fee: {fee_rate} sat/vbyte ({fee_source}), "
f"total {fee} sat"
)
if balance - fee > 0: if balance - fee > 0:
out = PartialTxOutput.from_address_and_value( out = PartialTxOutput.from_address_and_value(
change_addresses[0], balance - fee change_addresses[0], balance - fee
@@ -655,15 +562,9 @@ class Will:
def set_invalidate(wid, will=None): def set_invalidate(wid, will=None):
will = will if will is not None else {} will = will if will is not None else {}
will[wid].set_status("INVALIDATED", True) will[wid].set_status("INVALIDATED", True)
children = will[wid].children or [] if will[wid].children:
# add_willtree builds a LIST of [child_id, input_idx, prevout_idx]; for c in will[wid].children.items():
# a dict keyed by child id is still accepted (older data / tests). Will.set_invalidate(c[0], will)
child_ids = children.keys() if isinstance(children, dict) else [
c[0] for c in children
]
for child_id in child_ids:
if child_id in will:
Will.set_invalidate(child_id, will)
@staticmethod @staticmethod
def check_tx_height(tx, wallet): def check_tx_height(tx, wallet):
@@ -674,15 +575,10 @@ class Will:
@staticmethod @staticmethod
def check_invalidated(willtree, utxos_list, wallet): def check_invalidated(willtree, utxos_list, wallet):
for wid, w in willtree.items(): for wid, w in willtree.items():
# Defensive: a father that is not (or no longer) part of the will
# is treated as "no father" instead of raising KeyError, so the
# item's inputs are checked against the wallet UTXOs like any
# root transaction. See add_willtree for the root cause.
father = willtree.get(w.father) if w.father else None
if ( if (
father is None not w.father
or father.get_status("CONFIRMED") or willtree[w.father].get_status("CONFIRMED")
or father.get_status("MEMPOOL") or willtree[w.father].get_status("MEMPOOL")
): ):
for inp in w.tx.inputs(): for inp in w.tx.inputs():
inp_str = Util.utxo_to_str(inp) inp_str = Util.utxo_to_str(inp)

View File

@@ -230,19 +230,12 @@ class Willexecutors:
return True return True
@staticmethod @staticmethod
def get_willexecutor_transactions(will, force=False, current_willexecutors=None): def get_willexecutor_transactions(will, force=False):
"""Group the valid, signed, not-yet-pushed will txs by will-executor. """Group the valid, signed, not-yet-pushed will txs by will-executor.
Returns ``{url: executor_record}`` where each record gets ``txs`` (the Returns ``{url: executor_record}`` where each record gets ``txs`` (the
raw txs, one per line) and ``txsids``. raw txs, one per line) and ``txsids``.
``current_willexecutors`` is the CURRENT executor list from the config
(``{url: record}``). When given, an executor that is no longer in it or
no longer selected is skipped. WHY: every will item keeps its own copy
of the executor record taken at build time (``selected`` = True), so
without this check a deselected executor would still receive the will.
``None`` keeps the previous behaviour (only the item's own copy).
The returned records are COPIES: the will items are never modified. The returned records are COPIES: the will items are never modified.
""" """
willexecutors = {} willexecutors = {}
@@ -252,12 +245,6 @@ class Willexecutors:
if not willitem.get_status("PUSHED") or force: if not willitem.get_status("PUSHED") or force:
if willexecutor := willitem.we: if willexecutor := willitem.we:
url = willexecutor["url"] url = willexecutor["url"]
if current_willexecutors is not None and not (
Willexecutors.is_selected(
current_willexecutors.get(url)
)
):
continue
if willexecutor and Willexecutors.is_selected(willexecutor): if willexecutor and Willexecutors.is_selected(willexecutor):
if url not in willexecutors: if url not in willexecutors:
# Work on a COPY of the record: the item's # Work on a COPY of the record: the item's

View File

@@ -358,7 +358,8 @@ class BalWizardWidget(QWidget):
class BalWizardHeirsWidget(BalWizardWidget): class BalWizardHeirsWidget(BalWizardWidget):
title = N_("Bitcoin After Life Heirs") title = N_("Bitcoin After Life Heirs")
message = N_( message = N_(
"Please add your heirs\n remember that 100% of wallet balance will be spent" "Please add your heirs\n"
" remember that the wallet balance will be spent at 100%"
) )
def get_content(self): def get_content(self):

View File

@@ -93,27 +93,13 @@ def _can_broadcast(will_item):
def _can_delete(will_item): def _can_delete(will_item):
"""True if a will transaction may be deleted. """True if a will transaction may be deleted (invalid or unsigned).
Deletable: Valid AND fully-signed transactions are never deletable: removing them
* invalid or not fully signed transactions (unchanged rule); would silently drop a committed inheritance.
* valid AND fully signed transactions that their will-executor NEVER
received: not PUSHED and not CHECKED (e.g. "Push failed" /
"Not on server", or never sent).
Not deletable: a valid, signed transaction that the will-executor holds
(PUSHED or CHECKED). WHY: deleting it only on this PC would not remove it
from the executor's server, so the user would wrongly believe that
inheritance was cancelled; that case needs an on-chain invalidation.
Signed transactions without a will-executor (backup txs) also stay
protected, as before.
""" """
if not will_item: return bool(will_item) and (
return False not will_item.get_status("VALID") or not will_item.get_status("COMPLETE")
if not will_item.get_status("VALID") or not will_item.get_status("COMPLETE"):
return True
return bool(getattr(will_item, "we", None)) and not (
will_item.get_status("PUSHED") or will_item.get_status("CHECKED")
) )

View File

@@ -1632,15 +1632,7 @@ class BalWindow:
willitems = { willitems = {
t: willitems[t] for t in txids if t in willitems t: willitems[t] for t in txids if t in willitems
} }
# Read the CURRENT executor list from the config, so an executor the willexecutors = Willexecutors.get_willexecutor_transactions(willitems, force=force)
# user deselected after building the will is not contacted anymore.
willexecutors = Willexecutors.get_willexecutor_transactions(
willitems,
force=force,
current_willexecutors=Willexecutors.get_willexecutors(
self.bal_plugin, update=False, task=False
),
)
def getMsg(willexecutors): def getMsg(willexecutors):
msg = _("Broadcasting Transactions to Will-Executors:") + "\n" msg = _("Broadcasting Transactions to Will-Executors:") + "\n"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-10-03 12:24-0400\n" "POT-Creation-Date: 2026-10-07 14:25-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -60,7 +60,7 @@ msgstr ""
msgid "Error" msgid "Error"
msgstr "" msgstr ""
#: bal/core/will.py:1348 bal/gui/qt/dialogs.py:824 #: bal/core/will.py:1348 bal/gui/qt/dialogs.py:825
msgid "Expired" msgid "Expired"
msgstr "" msgstr ""
@@ -104,7 +104,7 @@ msgstr ""
msgid "Updated" msgid "Updated"
msgstr "" msgstr ""
#: bal/core/will.py:1359 bal/gui/qt/dialogs.py:2632 #: bal/core/will.py:1359 bal/gui/qt/dialogs.py:2633
msgid "Valid" msgid "Valid"
msgstr "" msgstr ""
@@ -195,15 +195,15 @@ msgstr ""
msgid "Bal Wizard Setup" msgid "Bal Wizard Setup"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:325 bal/gui/qt/dialogs.py:2760 #: bal/gui/qt/dialogs.py:325 bal/gui/qt/dialogs.py:2762
msgid "Previous" msgid "Previous"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:329 bal/gui/qt/dialogs.py:2763 #: bal/gui/qt/dialogs.py:329 bal/gui/qt/dialogs.py:2765
msgid "Next" msgid "Next"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:333 bal/gui/qt/dialogs.py:4019 #: bal/gui/qt/dialogs.py:333 bal/gui/qt/dialogs.py:4048
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
@@ -212,194 +212,193 @@ msgid "Bitcoin After Life Heirs"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:360 #: bal/gui/qt/dialogs.py:360
#, python-format
msgid "" msgid ""
"Please add your heirs\n" "Please add your heirs\n"
" remember that 100% of wallet balance will be spent" " remember that the wallet balance will be spent at 100%"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:369 bal/gui/qt/lists.py:1229 #: bal/gui/qt/dialogs.py:370 bal/gui/qt/lists.py:1229
msgid "Add" msgid "Add"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:371 bal/gui/qt/dialogs.py:2593 bal/gui/qt/lists.py:311 #: bal/gui/qt/dialogs.py:372 bal/gui/qt/dialogs.py:2594 bal/gui/qt/lists.py:311
#: bal/gui/qt/lists.py:673 bal/gui/qt/lists.py:1237 #: bal/gui/qt/lists.py:673 bal/gui/qt/lists.py:1237
msgid "Import" msgid "Import"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:373 bal/gui/qt/dialogs.py:2429 #: bal/gui/qt/dialogs.py:374 bal/gui/qt/dialogs.py:2430
#: bal/gui/qt/dialogs.py:2597 bal/gui/qt/lists.py:312 bal/gui/qt/lists.py:672 #: bal/gui/qt/dialogs.py:2598 bal/gui/qt/lists.py:312 bal/gui/qt/lists.py:672
#: bal/gui/qt/lists.py:1249 #: bal/gui/qt/lists.py:1249
msgid "Export" msgid "Export"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:398 bal/gui/qt/dialogs.py:506 #: bal/gui/qt/dialogs.py:399 bal/gui/qt/dialogs.py:507
msgid "Bitcoin After Life Will-Executors" msgid "Bitcoin After Life Will-Executors"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:399 #: bal/gui/qt/dialogs.py:400
msgid "Choose willexecutors download method" msgid "Choose willexecutors download method"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:406 #: bal/gui/qt/dialogs.py:407
msgid "Automatically download and select willexecutors" msgid "Automatically download and select willexecutors"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:407 #: bal/gui/qt/dialogs.py:408
msgid "Only download willexecutors list" msgid "Only download willexecutors list"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:408 #: bal/gui/qt/dialogs.py:409
msgid "Import willexecutor list from file" msgid "Import willexecutor list from file"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:409 #: bal/gui/qt/dialogs.py:410
msgid "Manual" msgid "Manual"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:438 bal/gui/qt/lists.py:1559 #: bal/gui/qt/dialogs.py:439 bal/gui/qt/lists.py:1559
msgid "willexecutors" msgid "willexecutors"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:507 #: bal/gui/qt/dialogs.py:508
msgid "Configure and select your willexecutors" msgid "Configure and select your willexecutors"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:526 #: bal/gui/qt/dialogs.py:527
msgid "Bitcoin After Life Will Settings" msgid "Bitcoin After Life Will Settings"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:561 bal/gui/qt/dialogs.py:614 #: bal/gui/qt/dialogs.py:562 bal/gui/qt/dialogs.py:615
msgid "Please wait" msgid "Please wait"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:644 #: bal/gui/qt/dialogs.py:645
msgid "Building Will" msgid "Building Will"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:649 #: bal/gui/qt/dialogs.py:650
msgid "Building Will:" msgid "Building Will:"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:730 bal/gui/qt/dialogs.py:755 #: bal/gui/qt/dialogs.py:731 bal/gui/qt/dialogs.py:756
#: bal/gui/qt/dialogs.py:765 bal/gui/qt/dialogs.py:788 #: bal/gui/qt/dialogs.py:766 bal/gui/qt/dialogs.py:789
msgid "Checking variables" msgid "Checking variables"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:756 #: bal/gui/qt/dialogs.py:757
msgid "Check Alive Threshold Passed: you have to Invalidate your old Will" msgid "Check Alive Threshold Passed: you have to Invalidate your old Will"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:765 bal/gui/qt/dialogs.py:847 #: bal/gui/qt/dialogs.py:766 bal/gui/qt/dialogs.py:848
msgid "No Heirs" msgid "No Heirs"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:788 bal/gui/qt/dialogs.py:1335 #: bal/gui/qt/dialogs.py:789 bal/gui/qt/dialogs.py:1336
#: bal/gui/qt/dialogs.py:2281 bal/gui/qt/window.py:2315 #: bal/gui/qt/dialogs.py:2282 bal/gui/qt/window.py:2315
msgid "Ok" msgid "Ok"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:792 #: bal/gui/qt/dialogs.py:793
msgid "" msgid ""
"In the inheritance process, the entire wallet will always be fully emptied. \n" "In the inheritance process, the entire wallet will always be fully emptied. \n"
"Your settings require an adjustment of the amounts" "Your settings require an adjustment of the amounts"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:801 bal/gui/qt/window.py:844 #: bal/gui/qt/dialogs.py:802 bal/gui/qt/window.py:844
msgid "Will-executor fee too high: {}" msgid "Will-executor fee too high: {}"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:835 #: bal/gui/qt/dialogs.py:836
msgid "Postponed: invalidating old will" msgid "Postponed: invalidating old will"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:870 bal/gui/qt/dialogs.py:879 #: bal/gui/qt/dialogs.py:871 bal/gui/qt/dialogs.py:880
#: bal/gui/qt/dialogs.py:2241 #: bal/gui/qt/dialogs.py:2242
msgid "Checking your will" msgid "Checking your will"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:872 #: bal/gui/qt/dialogs.py:873
msgid "An inheritance of this wallet is already executed (on blockchain)" msgid "An inheritance of this wallet is already executed (on blockchain)"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:881 #: bal/gui/qt/dialogs.py:882
msgid "Inheritance in mempool (waiting confirmation)" msgid "Inheritance in mempool (waiting confirmation)"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:914 #: bal/gui/qt/dialogs.py:915
msgid "Will-Executor excluded" msgid "Will-Executor excluded"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:914 bal/gui/qt/dialogs.py:1008 #: bal/gui/qt/dialogs.py:915 bal/gui/qt/dialogs.py:1009
#: bal/gui/qt/dialogs.py:2222 bal/gui/qt/dialogs.py:2233 #: bal/gui/qt/dialogs.py:2223 bal/gui/qt/dialogs.py:2234
msgid "Skipped" msgid "Skipped"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:920 bal/gui/qt/dialogs.py:1705 bal/gui/qt/lists.py:345 #: bal/gui/qt/dialogs.py:921 bal/gui/qt/dialogs.py:1706 bal/gui/qt/lists.py:345
#: bal/gui/qt/theme.py:116 #: bal/gui/qt/theme.py:116
msgid "Will-Executor" msgid "Will-Executor"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:921 #: bal/gui/qt/dialogs.py:922
msgid "Not present - select one or enable backup mode" msgid "Not present - select one or enable backup mode"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:982 #: bal/gui/qt/dialogs.py:983
msgid "All heirs' shares are below the dust limit: the inheritance cannot be created. Increase the amounts or reduce the number of heirs." msgid "All heirs' shares are below the dust limit: the inheritance cannot be created. Increase the amounts or reduce the number of heirs."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:998 #: bal/gui/qt/dialogs.py:999
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." 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 "" msgstr ""
#: bal/gui/qt/dialogs.py:1042 #: bal/gui/qt/dialogs.py:1043
msgid "Heir {}" msgid "Heir {}"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1044 #: bal/gui/qt/dialogs.py:1045
msgid "{} is DUST - excluded (amount below dust limit)" msgid "{} is DUST - excluded (amount below dust limit)"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1100 #: bal/gui/qt/dialogs.py:1101
msgid "Heir" msgid "Heir"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1217 bal/gui/qt/dialogs.py:2270 #: bal/gui/qt/dialogs.py:1218 bal/gui/qt/dialogs.py:2271
msgid "Please wait {}secs" msgid "Please wait {}secs"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1224 bal/gui/qt/dialogs.py:1276 #: bal/gui/qt/dialogs.py:1225 bal/gui/qt/dialogs.py:1277
#: bal/gui/qt/dialogs.py:1314 #: bal/gui/qt/dialogs.py:1315
msgid "Broadcasting" msgid "Broadcasting"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1335 bal/gui/qt/window.py:2317 #: bal/gui/qt/dialogs.py:1336 bal/gui/qt/window.py:2317
msgid "Ko" msgid "Ko"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1346 #: bal/gui/qt/dialogs.py:1347
msgid "Timeout - no answer" msgid "Timeout - no answer"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1388 #: bal/gui/qt/dialogs.py:1389
msgid "checking {} - {}" msgid "checking {} - {}"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1391 bal/gui/qt/dialogs.py:2239 #: bal/gui/qt/dialogs.py:1392 bal/gui/qt/dialogs.py:2240
#: bal/gui/qt/window.py:1691 #: bal/gui/qt/window.py:1691
msgid "Waiting" msgid "Waiting"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1405 #: bal/gui/qt/dialogs.py:1406
msgid "checked {} - {}" msgid "checked {} - {}"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1455 bal/gui/qt/dialogs.py:2025 #: bal/gui/qt/dialogs.py:1456 bal/gui/qt/dialogs.py:2026
#: bal/gui/qt/dialogs.py:2036 #: bal/gui/qt/dialogs.py:2037
msgid "Error: {}" msgid "Error: {}"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1516 #: bal/gui/qt/dialogs.py:1517
msgid "" msgid ""
"Your will has expired and must be invalidated before it can be rebuilt.\n" "Your will has expired and must be invalidated before it can be rebuilt.\n"
"A transaction window will now open:\n" "A transaction window will now open:\n"
@@ -407,159 +406,159 @@ msgid ""
"After the invalidation is confirmed, press the Check button to finish the will." "After the invalidation is confirmed, press the Check button to finish the will."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1545 bal/gui/qt/window.py:849 #: bal/gui/qt/dialogs.py:1546 bal/gui/qt/window.py:849
msgid "Check Alive must be after today and before the delivery date, please update it" msgid "Check Alive must be after today and before the delivery date, please update it"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1568 #: bal/gui/qt/dialogs.py:1569
msgid "" msgid ""
"Your old will has been invalidated and the transaction was broadcast.\n" "Your old will has been invalidated and the transaction was broadcast.\n"
"Electrum may need a little time to register it.\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." "Please wait until the invalidation transaction is confirmed, then press the Check button again to finish updating your will."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1584 #: bal/gui/qt/dialogs.py:1585
msgid "Invalidate your old will" msgid "Invalidate your old will"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1598 bal/gui/qt/dialogs.py:1643 #: bal/gui/qt/dialogs.py:1599 bal/gui/qt/dialogs.py:1644
msgid "Aborted" msgid "Aborted"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1617 #: bal/gui/qt/dialogs.py:1618
msgid "Auto-sign disabled — sign manually" msgid "Auto-sign disabled — sign manually"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1630 #: bal/gui/qt/dialogs.py:1631
msgid "" msgid ""
"The delivery date was automatically moved one day earlier so the updated will can correctly replace the previous one.\n" "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." "Please sign (and broadcast) to confirm the change."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1640 #: bal/gui/qt/dialogs.py:1641
msgid "Sign your will" msgid "Sign your will"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1645 bal/gui/qt/dialogs.py:1928 #: bal/gui/qt/dialogs.py:1646 bal/gui/qt/dialogs.py:1929
msgid "Nothing to do" msgid "Nothing to do"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1664 #: bal/gui/qt/dialogs.py:1665
msgid "Finished" msgid "Finished"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1681 #: bal/gui/qt/dialogs.py:1682
msgid "Calendar" msgid "Calendar"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1683 bal/gui/qt/dialogs.py:1713 #: bal/gui/qt/dialogs.py:1684 bal/gui/qt/dialogs.py:1714
#: bal/gui/qt/dialogs.py:1780 bal/gui/qt/dialogs.py:2028 #: bal/gui/qt/dialogs.py:1781 bal/gui/qt/dialogs.py:2029
#: bal/gui/qt/dialogs.py:2968 bal/gui/qt/dialogs.py:3207 #: bal/gui/qt/dialogs.py:2976 bal/gui/qt/dialogs.py:3231
#: bal/gui/qt/dialogs.py:4038 #: bal/gui/qt/dialogs.py:4067
msgid "Close" msgid "Close"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1709 bal/gui/qt/dialogs.py:1776 #: bal/gui/qt/dialogs.py:1710 bal/gui/qt/dialogs.py:1777
msgid "🔮 Wizard" msgid "🔮 Wizard"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1772 bal/gui/qt/dialogs.py:2580 #: bal/gui/qt/dialogs.py:1773 bal/gui/qt/dialogs.py:2581
msgid "Heirs" msgid "Heirs"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1881 bal/gui/qt/widgets.py:1150 #: bal/gui/qt/dialogs.py:1882 bal/gui/qt/widgets.py:1150
msgid "(reminder {idx}/{total})" msgid "(reminder {idx}/{total})"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1945 #: bal/gui/qt/dialogs.py:1946
msgid "All done" msgid "All done"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:1996 #: bal/gui/qt/dialogs.py:1997
msgid "Next steps (manual): press 'Sign' to sign your will, then 'Broadcast' to send it to the will-executors." msgid "Next steps (manual): press 'Sign' to sign your will, then 'Broadcast' to send it to the will-executors."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2001 #: bal/gui/qt/dialogs.py:2002
msgid "Next step (manual): press 'Sign' to sign your will." msgid "Next step (manual): press 'Sign' to sign your will."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2005 #: bal/gui/qt/dialogs.py:2006
msgid "Next step (manual): press 'Broadcast' to send your will to the will-executors." msgid "Next step (manual): press 'Broadcast' to send your will to the will-executors."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2100 #: bal/gui/qt/dialogs.py:2101
msgid "Miner fee rate changed (the will was built with {} sat/byte, now it is {}): a new will must be prepared." msgid "Miner fee rate changed (the will was built with {} sat/byte, now it is {}): a new will must be prepared."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2105 #: bal/gui/qt/dialogs.py:2106
msgid "The miner fee rate changed: a new will must be prepared." msgid "The miner fee rate changed: a new will must be prepared."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2109 #: bal/gui/qt/dialogs.py:2110
msgid "Will-executor \"{}\" is not covered by the current will: a new will must be prepared." msgid "Will-executor \"{}\" is not covered by the current will: a new will must be prepared."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2114 #: bal/gui/qt/dialogs.py:2115
msgid "Backup mode is enabled but the will has no backup transaction: a new will must be prepared." msgid "Backup mode is enabled but the will has no backup transaction: a new will must be prepared."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2123 #: bal/gui/qt/dialogs.py:2124
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." 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 "" msgstr ""
#: bal/gui/qt/dialogs.py:2132 #: bal/gui/qt/dialogs.py:2133
msgid "The heirs changed: a new will must be prepared." msgid "The heirs changed: a new will must be prepared."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2134 #: bal/gui/qt/dialogs.py:2135
msgid "A will-executor changed: a new will must be prepared." msgid "A will-executor changed: a new will must be prepared."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2143 #: bal/gui/qt/dialogs.py:2144
msgid "The wallet contains funds that the current will does not cover yet: a new will must be prepared." msgid "The wallet contains funds that the current will does not cover yet: a new will must be prepared."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2147 #: bal/gui/qt/dialogs.py:2148
msgid "The will contains no valid transaction: a new will must be prepared." msgid "The will contains no valid transaction: a new will must be prepared."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2153 #: bal/gui/qt/dialogs.py:2154
msgid "The will is no longer coherent and must be rebuilt; the exact reason could not be determined." msgid "The will is no longer coherent and must be rebuilt; the exact reason could not be determined."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2183 #: bal/gui/qt/dialogs.py:2184
msgid "No heirs: add at least one heir before building the will." msgid "No heirs: add at least one heir before building the will."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2186 #: bal/gui/qt/dialogs.py:2187
msgid "The wallet has no spendable funds, so no inheritance transaction can be created." msgid "The wallet has no spendable funds, so no inheritance transaction can be created."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2190 #: bal/gui/qt/dialogs.py:2191
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." 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 "" msgstr ""
#: bal/gui/qt/dialogs.py:2195 #: bal/gui/qt/dialogs.py:2196
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." 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 "" msgstr ""
#: bal/gui/qt/dialogs.py:2200 #: bal/gui/qt/dialogs.py:2201
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." 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 "" msgstr ""
#: bal/gui/qt/dialogs.py:2205 #: bal/gui/qt/dialogs.py:2206
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." 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 "" msgstr ""
#: bal/gui/qt/dialogs.py:2210 #: bal/gui/qt/dialogs.py:2211
msgid "The inheritance transactions could not be assembled from the available funds (the balance may not cover the miner fees)." msgid "The inheritance transactions could not be assembled from the available funds (the balance may not cover the miner fees)."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2214 #: bal/gui/qt/dialogs.py:2215
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." 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 "" msgstr ""
#: bal/gui/qt/dialogs.py:2225 #: bal/gui/qt/dialogs.py:2226
msgid "" msgid ""
"Could not build the will, and the exact cause could not be determined. Please check that:\n" "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" "- the wallet balance covers the miner and will-executor fees,\n"
@@ -568,403 +567,403 @@ msgid ""
"- at least one will-executor is selected and reachable." "- at least one will-executor is selected and reachable."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2246 #: bal/gui/qt/dialogs.py:2247
msgid "Invalidating old will" msgid "Invalidating old will"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2252 #: bal/gui/qt/dialogs.py:2253
msgid "Building your will" msgid "Building your will"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2258 #: bal/gui/qt/dialogs.py:2259
msgid "Signing your will" msgid "Signing your will"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2264 #: bal/gui/qt/dialogs.py:2265
msgid "Broadcasting your will to executors" msgid "Broadcasting your will to executors"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2312 #: bal/gui/qt/dialogs.py:2313
msgid "Wait" msgid "Wait"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2415 #: bal/gui/qt/dialogs.py:2416
msgid "Will Details" msgid "Will Details"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2421 bal/gui/qt/dialogs.py:4013 #: bal/gui/qt/dialogs.py:2422 bal/gui/qt/dialogs.py:4042
#: bal/gui/qt/lists.py:668 #: bal/gui/qt/lists.py:668
msgid "Sign" msgid "Sign"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2425 bal/gui/qt/lists.py:675 #: bal/gui/qt/dialogs.py:2426 bal/gui/qt/lists.py:675
msgid "Broadcast" msgid "Broadcast"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2432 bal/gui/qt/lists.py:677 #: bal/gui/qt/dialogs.py:2433 bal/gui/qt/lists.py:677
msgid "Invalidate" msgid "Invalidate"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2437 bal/gui/qt/lists.py:674 #: bal/gui/qt/dialogs.py:2438 bal/gui/qt/lists.py:674
msgid "Merge" msgid "Merge"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2445 #: bal/gui/qt/dialogs.py:2446
msgid "Expiration date: {}" msgid "Expiration date: {}"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2451 #: bal/gui/qt/dialogs.py:2452
msgid "Valid Txs:{}" msgid "Valid Txs:{}"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2453 #: bal/gui/qt/dialogs.py:2454
msgid "Total Txs:{}" msgid "Total Txs:{}"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2507 #: bal/gui/qt/dialogs.py:2508
msgid "Hide replaced" msgid "Hide replaced"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2509 #: bal/gui/qt/dialogs.py:2510
msgid "Unhide replaced" msgid "Unhide replaced"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2515 #: bal/gui/qt/dialogs.py:2516
msgid "Hide invalidated" msgid "Hide invalidated"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2517 #: bal/gui/qt/dialogs.py:2518
msgid "Unhide invalidated" msgid "Unhide invalidated"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2542 #: bal/gui/qt/dialogs.py:2543
msgid "Will-Executor Service List" msgid "Will-Executor Service List"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2589 bal/gui/qt/lists.py:314 #: bal/gui/qt/dialogs.py:2590 bal/gui/qt/lists.py:314
msgid "New Heir" msgid "New Heir"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2631 #: bal/gui/qt/dialogs.py:2632
msgid "All" msgid "All"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2634 #: bal/gui/qt/dialogs.py:2635
msgid "Valid NC" msgid "Valid NC"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2771 bal/gui/qt/dialogs.py:2847 #: bal/gui/qt/dialogs.py:2773 bal/gui/qt/dialogs.py:2851
msgid "Auto" msgid "Auto"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2773 #: bal/gui/qt/dialogs.py:2775
msgid "Automatically advance through the QR codes." msgid "Automatically advance through the QR codes."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2777 #: bal/gui/qt/dialogs.py:2779
msgid "QR codes per second:" msgid "QR codes per second:"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2781 #: bal/gui/qt/dialogs.py:2783
msgid " /s" msgid " /s"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2783 #: bal/gui/qt/dialogs.py:2785
msgid "Loop" msgid "Loop"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2785 #: bal/gui/qt/dialogs.py:2787
msgid "When the last QR code is reached, keep cycling from the first one instead of stopping." msgid "When the last QR code is reached, keep cycling from the first one instead of stopping."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2795 #: bal/gui/qt/dialogs.py:2797
msgid "QR code size:" msgid "QR code size:"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2841 #: bal/gui/qt/dialogs.py:2845
msgid "Stop" msgid "Stop"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2862 #: bal/gui/qt/dialogs.py:2866
msgid "" msgid ""
"Scan the QR codes below with the will-opening device.\n" "Scan the QR codes below with the will-opening device.\n"
"{} codes carry the whole transfer (any order works, duplicates are ignored)." "{} codes carry the whole transfer (any order works, duplicates are ignored)."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2884 #: bal/gui/qt/dialogs.py:2888
msgid "Frame {} of {}" msgid "Frame {} of {}"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2907 #: bal/gui/qt/dialogs.py:2911
msgid "Export will" msgid "Export will"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2917 #: bal/gui/qt/dialogs.py:2922
msgid "No will transaction to export." msgid "No will transaction to export."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2924 #: bal/gui/qt/dialogs.py:2929
msgid "Export:" msgid "Export:"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2931 #: bal/gui/qt/dialogs.py:2936
msgid "Whole will" msgid "Whole will"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2934 #: bal/gui/qt/dialogs.py:2939
msgid "Export the full will items (all details/statuses) as JSON, or only the serialized transactions." msgid "Export the full will items (all details/statuses) as JSON, or only the serialized transactions."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2945 #: bal/gui/qt/dialogs.py:2950
msgid "Send as:" msgid "Send as:"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2947 bal/gui/qt/dialogs.py:3184 #: bal/gui/qt/dialogs.py:2952 bal/gui/qt/dialogs.py:3208
msgid "File" msgid "File"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2948 bal/gui/qt/dialogs.py:3185 #: bal/gui/qt/dialogs.py:2953 bal/gui/qt/dialogs.py:3209
msgid "QR Code" msgid "QR Code"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2949 bal/gui/qt/dialogs.py:3186 #: bal/gui/qt/dialogs.py:2954 bal/gui/qt/dialogs.py:3210
msgid "Audio" msgid "Audio"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2990 #: bal/gui/qt/dialogs.py:3005
msgid "Export the will as a JSON file. Uncheck \"Whole will\" above to export only the serialized transactions (comma-separated)." msgid "Export the will as a JSON file. Uncheck \"Whole will\" above to export only the serialized transactions (comma-separated)."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:2999 #: bal/gui/qt/dialogs.py:3014
msgid "Export…" msgid "Export…"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3016 bal/gui/qt/dialogs.py:3242 #: bal/gui/qt/dialogs.py:3032 bal/gui/qt/dialogs.py:3271
#: bal/gui/qt/window.py:1787 bal/gui/qt/window.py:1800 #: bal/gui/qt/window.py:1787 bal/gui/qt/window.py:1800
msgid "Audio MODEM plugin is not available." msgid "Audio MODEM plugin is not available."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3020 bal/gui/qt/dialogs.py:3246 #: bal/gui/qt/dialogs.py:3036 bal/gui/qt/dialogs.py:3275
msgid "Speed (KB/sec):" msgid "Speed (KB/sec):"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3031 #: bal/gui/qt/dialogs.py:3047
msgid "Send" msgid "Send"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3076 bal/gui/qt/dialogs.py:3110 #: bal/gui/qt/dialogs.py:3100 bal/gui/qt/dialogs.py:3134
#: bal/gui/qt/dialogs.py:3135 #: bal/gui/qt/dialogs.py:3159
msgid "No will transaction matches the selected filter." msgid "No will transaction matches the selected filter."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3099 #: bal/gui/qt/dialogs.py:3123
msgid "item(s)" msgid "item(s)"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3099 #: bal/gui/qt/dialogs.py:3123
msgid "transaction(s)" msgid "transaction(s)"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3101 #: bal/gui/qt/dialogs.py:3125
msgid "{} {} will be exported." msgid "{} {} will be exported."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3104 #: bal/gui/qt/dialogs.py:3128
msgid "{} {} will be sent." msgid "{} {} will be sent."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3128 bal/gui/qt/dialogs.py:3131 #: bal/gui/qt/dialogs.py:3152 bal/gui/qt/dialogs.py:3155
#: bal/gui/qt/dialogs.py:3282 bal/gui/qt/dialogs.py:3285 #: bal/gui/qt/dialogs.py:3311 bal/gui/qt/dialogs.py:3314
msgid "Invalid audio speed." msgid "Invalid audio speed."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3167 #: bal/gui/qt/dialogs.py:3191
msgid "Import will" msgid "Import will"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3173 #: bal/gui/qt/dialogs.py:3197
msgid "" msgid ""
"Choose how the will was exported: from a file, QR codes or audio.\n" "Choose how the will was exported: from a file, QR codes or audio.\n"
"The import never touches the live will." "The import never touches the live will."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3182 #: bal/gui/qt/dialogs.py:3206
msgid "Import from:" msgid "Import from:"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3218 #: bal/gui/qt/dialogs.py:3246
msgid "Import the JSON will file written by the Export ▶ File option. The will opens in a read-only preview window." msgid "Import the JSON will file written by the Export ▶ File option. The will opens in a read-only preview window."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3225 #: bal/gui/qt/dialogs.py:3253
msgid "Choose will file…" msgid "Choose will file…"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3255 #: bal/gui/qt/dialogs.py:3284
msgid "Waiting for the audio transfer…" msgid "Waiting for the audio transfer…"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3258 #: bal/gui/qt/dialogs.py:3287
msgid "Receive by audio…" msgid "Receive by audio…"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3299 #: bal/gui/qt/dialogs.py:3328
msgid "Receiving…" msgid "Receiving…"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3319 #: bal/gui/qt/dialogs.py:3348
msgid "No transaction data received." msgid "No transaction data received."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3337 #: bal/gui/qt/dialogs.py:3366
msgid "Waiting for audio ({:.1f} kbps)…" msgid "Waiting for audio ({:.1f} kbps)…"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3410 #: bal/gui/qt/dialogs.py:3439
msgid "Could not parse a transferred will item: {}" msgid "Could not parse a transferred will item: {}"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3430 #: bal/gui/qt/dialogs.py:3459
msgid "Could not parse a transferred transaction: {}" msgid "Could not parse a transferred transaction: {}"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3441 #: bal/gui/qt/dialogs.py:3470
msgid "The imported will contains no valid transaction in this wallet." msgid "The imported will contains no valid transaction in this wallet."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3452 #: bal/gui/qt/dialogs.py:3481
msgid "{} imported transaction(s) are not valid in this wallet and were skipped." msgid "{} imported transaction(s) are not valid in this wallet and were skipped."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3552 #: bal/gui/qt/dialogs.py:3581
msgid "" msgid ""
"Show the will QR codes to the camera, one after another.\n" "Show the will QR codes to the camera, one after another.\n"
"After the first code, every new code is captured automatically.\n" "After the first code, every new code is captured automatically.\n"
"The first frame sets the total number of codes; duplicates are ignored." "The first frame sets the total number of codes; duplicates are ignored."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3562 bal/gui/qt/dialogs.py:3740 #: bal/gui/qt/dialogs.py:3591 bal/gui/qt/dialogs.py:3769
msgid "Waiting for the first frame…" msgid "Waiting for the first frame…"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3579 #: bal/gui/qt/dialogs.py:3608
msgid "…or paste/type the frame text here" msgid "…or paste/type the frame text here"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3583 #: bal/gui/qt/dialogs.py:3612
msgid "Add frame" msgid "Add frame"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3589 bal/gui/qt/dialogs.py:3832 #: bal/gui/qt/dialogs.py:3618 bal/gui/qt/dialogs.py:3861
msgid "Scan with camera…" msgid "Scan with camera…"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3591 #: bal/gui/qt/dialogs.py:3620
msgid "Start the live camera. While it runs, every new QR code shown to the camera is captured automatically." msgid "Start the live camera. While it runs, every new QR code shown to the camera is captured automatically."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3598 #: bal/gui/qt/dialogs.py:3627
msgid "Reset" msgid "Reset"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3605 #: bal/gui/qt/dialogs.py:3634
msgid "Review and Sign…" msgid "Review and Sign…"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3624 #: bal/gui/qt/dialogs.py:3653
msgid "The scanned code belongs to a different transfer ({} frames, {}). The import was reset; scan the first code again." msgid "The scanned code belongs to a different transfer ({} frames, {}). The import was reset; scan the first code again."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3715 #: bal/gui/qt/dialogs.py:3744
msgid "All {} frames stored." msgid "All {} frames stored."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3721 #: bal/gui/qt/dialogs.py:3750
msgid "Stored {} of {} frames." msgid "Stored {} of {} frames."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3786 #: bal/gui/qt/dialogs.py:3815
msgid "Cannot start QR scanner, no usable camera found." msgid "Cannot start QR scanner, no usable camera found."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3791 #: bal/gui/qt/dialogs.py:3820
msgid "Stop scanning" msgid "Stop scanning"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3838 #: bal/gui/qt/dialogs.py:3867
msgid "Camera error: {}" msgid "Camera error: {}"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3884 #: bal/gui/qt/dialogs.py:3913
msgid "The scanned code belongs to a different transfer ({} frames, {}). The import was reset; show the first code again." msgid "The scanned code belongs to a different transfer ({} frames, {}). The import was reset; show the first code again."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3928 #: bal/gui/qt/dialogs.py:3957
msgid "The transferred will contains no transactions." msgid "The transferred will contains no transactions."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3957 #: bal/gui/qt/dialogs.py:3986
msgid "Review and sign imported will" msgid "Review and sign imported will"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:3969 #: bal/gui/qt/dialogs.py:3998
msgid "Enter your wallet password to sign the imported transactions." msgid "Enter your wallet password to sign the imported transactions."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:4016 #: bal/gui/qt/dialogs.py:4045
msgid "Skip" msgid "Skip"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:4032 #: bal/gui/qt/dialogs.py:4061
msgid "Save signed file…" msgid "Save signed file…"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:4035 #: bal/gui/qt/dialogs.py:4064
msgid "Show signed QR…" msgid "Show signed QR…"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:4059 #: bal/gui/qt/dialogs.py:4088
msgid "Transaction {} of {}" msgid "Transaction {} of {}"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:4061 #: bal/gui/qt/dialogs.py:4090
msgid "TXID: {}" msgid "TXID: {}"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:4064 #: bal/gui/qt/dialogs.py:4093
msgid "unknown" msgid "unknown"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:4070 #: bal/gui/qt/dialogs.py:4099
msgid "(no outputs)" msgid "(no outputs)"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:4084 #: bal/gui/qt/dialogs.py:4113
msgid "unknown (partial transaction)" msgid "unknown (partial transaction)"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:4086 #: bal/gui/qt/dialogs.py:4115
msgid "" msgid ""
"Total outputs: {}\n" "Total outputs: {}\n"
"Fee: {}" "Fee: {}"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:4097 #: bal/gui/qt/dialogs.py:4126
msgid "" msgid ""
"Signed {} of {} transactions.\n" "Signed {} of {} transactions.\n"
"\n" "\n"
"Save a signed file to carry to the broadcast device, or show the signed transactions as QR codes." "Save a signed file to carry to the broadcast device, or show the signed transactions as QR codes."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:4116 #: bal/gui/qt/dialogs.py:4145
msgid "Could not sign the transaction: {}" msgid "Could not sign the transaction: {}"
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:4135 #: bal/gui/qt/dialogs.py:4164
msgid "Signed will saved." msgid "Signed will saved."
msgstr "" msgstr ""
#: bal/gui/qt/dialogs.py:4146 #: bal/gui/qt/dialogs.py:4175
msgid "No signed transaction to show." msgid "No signed transaction to show."
msgstr "" msgstr ""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,82 +0,0 @@
"""
Tests for ``Willexecutors.get_willexecutor_transactions`` with the CURRENT
executor selection (``current_willexecutors``).
Every will item keeps its own copy of the executor record taken at build time
(``selected`` = True). Without the current selection, an executor the user
deselected after building the will would still receive the will.
Run (standalone or with pytest):
QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src \
python3 tests/test_core_deselected_executor.py
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.core.willexecutors import Willexecutors
class FakeItem:
"""Minimal will item: statuses, a tx and the build-time executor copy."""
def __init__(self, url, **flags):
self._status = {"VALID": True, "COMPLETE": True}
self._status.update(flags)
self.we = {"url": url, "selected": True}
self.tx = f"rawtx-{url}"
def get_status(self, name):
return self._status.get(name, False)
A = "https://we.a.example"
B = "https://we.b.example"
def _will():
return {"tx_a": FakeItem(A), "tx_b": FakeItem(B)}
def test_without_current_list_previous_behaviour():
assert set(Willexecutors.get_willexecutor_transactions(_will())) == {A, B}
def test_deselected_executor_is_skipped():
current = {A: {"selected": True}, B: {"selected": False}}
out = Willexecutors.get_willexecutor_transactions(
_will(), current_willexecutors=current
)
assert set(out) == {A}
assert out[A]["txsids"] == ["tx_a"]
def test_executor_removed_from_list_is_skipped():
current = {A: {"selected": True}}
out = Willexecutors.get_willexecutor_transactions(
_will(), current_willexecutors=current
)
assert set(out) == {A}
def test_already_pushed_still_excluded_unless_forced():
will = {"tx_a": FakeItem(A, PUSHED=True)}
current = {A: {"selected": True}}
assert Willexecutors.get_willexecutor_transactions(
will, current_willexecutors=current
) == {}
assert set(
Willexecutors.get_willexecutor_transactions(
will, force=True, current_willexecutors=current
)
) == {A}
if __name__ == "__main__":
for name in sorted(dir()):
if name.startswith("test_"):
globals()[name]()
print(f" [OK] {name}")
print("[OK] All deselected-executor tests passed")

View File

@@ -1,231 +0,0 @@
"""
Tests for the fee of the on-chain INVALIDATION transaction.
The invalidation is broadcast TODAY (it spends the will's coins so the old
pre-signed inheritance transactions become unusable), therefore it must pay the
CURRENT network fee for a confirmation within
``INVALIDATION_FEE_ETA_BLOCKS`` (2) blocks. The will's ``baltx_fees`` (a
deliberately high rate meant for inheritance transactions broadcast years from
now) is only the FALLBACK used when no network estimate is available.
Covers:
* Will.get_invalidation_fee_rate() - rate selection and fallback rules
* Will.invalidate_will() - the chosen rate is really used for the fee
Run (standalone or with pytest):
QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src \
python3 tests/test_core_invalidation_fee.py
QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src \
python3 -m pytest tests/test_core_invalidation_fee.py -q
"""
import os
import sys
from unittest.mock import MagicMock, patch
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from electrum.transaction import Transaction
from bal.core.will import INVALIDATION_FEE_ETA_BLOCKS, Will, WillItem
# WillItem parses a raw tx and calls add_info_from_wallet; no live wallet here.
_patcher = patch.object(Transaction, "add_info_from_wallet")
_patcher.start()
# A valid serialized transaction (1 input, 1 output) reused by the core tests.
_VALID_TX_HEX = (
"01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65b"
"f38633b424eb4031000000006c493046022100a82bbc57a0136751e543"
"3f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7d"
"e89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d98501"
"2102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae3"
"5cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a"
"42146f11ef8414ae929feaafc388ac00000000"
)
# The outpoint spent by _VALID_TX_HEX (input 0).
_PREVOUT_STR = "3140eb24b43386f35ba69e3875eb6c93130ac66201d01c58f598defc949a5c2a:0"
_CHANGE_ADDR = "14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs"
_BALANCE = 1000000
# The will's baltx_fees used as the fallback rate in every test (sat/vbyte).
_BALTX_FEES = 20
# ------------------------------------------------------------------ #
# Helpers
# ------------------------------------------------------------------ #
def _wallet_with_estimate(fee_per_kb):
"""Wallet whose network returns ``fee_per_kb`` (sat/kvbyte) for any ETA.
``fee_per_kb`` may also be an Exception instance: the estimate call then
raises it, to simulate a broken network object.
"""
wallet = MagicMock()
wallet.get_change_addresses_for_new_transaction.return_value = [_CHANGE_ADDR]
network = MagicMock()
if isinstance(fee_per_kb, Exception):
network.fee_estimates.eta_target_to_fee.side_effect = fee_per_kb
else:
network.fee_estimates.eta_target_to_fee.return_value = fee_per_kb
wallet.network = network
return wallet
def _make_will():
"""One VALID will item spending _PREVOUT_STR with a known input value."""
item = WillItem({
"tx": _VALID_TX_HEX,
"heirs": {"alice": ["addr_alice", 5000, "30d"]},
"willexecutor": None,
"status": "",
"description": "",
"time": 0,
"change": "",
"baltx_fees": _BALTX_FEES,
})
item.STATUS = WillItem.copy_status_table(WillItem.STATUS_DEFAULT)
item.tx.inputs()[0]._TxInput__value_sats = _BALANCE
return {"willtxid1": item}
def _make_utxo():
"""Wallet-side UTXO matching the will input."""
utxo = MagicMock()
utxo.prevout.to_str.return_value = _PREVOUT_STR
utxo.is_coinbase_output.return_value = False
utxo.block_height = 1
utxo.value_sats.return_value = _BALANCE
return utxo
def _invalidation_output_value(wallet, estimated_size=200):
"""Run Will.invalidate_will and return the value of the final output.
Electrum's tx building is mocked (as in test_core_will_invalidate.py):
``estimated_size`` is the vsize the fee is computed from.
"""
wallet.get_utxos.return_value = [_make_utxo()]
mock_output = MagicMock()
mock_tx = MagicMock()
mock_tx.estimated_size.return_value = estimated_size
with patch("bal.core.will.Util.get_current_height", return_value=800000), \
patch("electrum.transaction.PartialTxOutput.from_address_and_value",
return_value=mock_output) as mock_out, \
patch("electrum.transaction.PartialTransaction.from_io",
return_value=mock_tx):
result = Will.invalidate_will(_make_will(), wallet, _BALTX_FEES)
assert result is mock_tx
# 1st call: full balance (size estimation); 2nd call: balance - fee.
return mock_out.call_args_list[1][0][1]
# ------------------------------------------------------------------ #
# Will.get_invalidation_fee_rate
# ------------------------------------------------------------------ #
def test_target_is_two_blocks():
"""The owner's decision: confirm within 2 blocks."""
assert INVALIDATION_FEE_ETA_BLOCKS == 2
wallet = _wallet_with_estimate(3000)
Will.get_invalidation_fee_rate(wallet, _BALTX_FEES)
wallet.network.fee_estimates.eta_target_to_fee.assert_called_once_with(2)
def test_network_estimate_is_used():
"""A live estimate (sat/kvbyte) becomes the rate in sat/vbyte."""
wallet = _wallet_with_estimate(3000)
assert Will.get_invalidation_fee_rate(wallet, _BALTX_FEES) == (3.0, "network")
def test_network_estimate_higher_than_baltx_fees_is_used_as_is():
"""No cap: if the market is above baltx_fees, the market rate is used."""
wallet = _wallet_with_estimate(45000)
assert Will.get_invalidation_fee_rate(wallet, _BALTX_FEES) == (45.0, "network")
def test_fractional_estimate_is_kept():
"""Electrum estimates may be fractional (e.g. 1.5 sat/vbyte)."""
wallet = _wallet_with_estimate(1500)
assert Will.get_invalidation_fee_rate(wallet, _BALTX_FEES) == (1.5, "network")
def test_offline_wallet_falls_back_to_baltx_fees():
"""No network object (offline): previous behaviour, baltx_fees."""
wallet = MagicMock()
wallet.network = None
assert Will.get_invalidation_fee_rate(wallet, _BALTX_FEES) == (
_BALTX_FEES,
"fallback",
)
def test_missing_estimate_falls_back():
"""Estimates not received yet: Electrum returns None."""
wallet = _wallet_with_estimate(None)
assert Will.get_invalidation_fee_rate(wallet, _BALTX_FEES) == (
_BALTX_FEES,
"fallback",
)
def test_estimate_error_falls_back_and_does_not_raise():
"""A broken network object must never break the invalidation."""
wallet = _wallet_with_estimate(RuntimeError("server gone"))
assert Will.get_invalidation_fee_rate(wallet, _BALTX_FEES) == (
_BALTX_FEES,
"fallback",
)
def test_non_integer_or_invalid_estimates_fall_back():
"""Only a positive int (Electrum's sat/kvbyte type) is trusted."""
for bad in (0, -1000, True, "3000", 3.5, MagicMock()):
wallet = _wallet_with_estimate(bad)
assert Will.get_invalidation_fee_rate(wallet, _BALTX_FEES) == (
_BALTX_FEES,
"fallback",
), bad
def test_no_wallet_falls_back():
assert Will.get_invalidation_fee_rate(None, _BALTX_FEES) == (
_BALTX_FEES,
"fallback",
)
# ------------------------------------------------------------------ #
# Will.invalidate_will uses the chosen rate
# ------------------------------------------------------------------ #
def test_invalidation_pays_network_fee_not_baltx_fees():
"""3 sat/vbyte on 200 vbytes = 600 sat (not 200 * 20 = 4000 sat)."""
wallet = _wallet_with_estimate(3000)
assert _invalidation_output_value(wallet) == _BALANCE - 600
def test_invalidation_offline_pays_baltx_fees():
"""Offline: unchanged behaviour, 200 * 20 = 4000 sat."""
wallet = _wallet_with_estimate(None)
assert _invalidation_output_value(wallet) == _BALANCE - 4000
def test_fractional_fee_is_rounded_up_to_whole_satoshis():
"""1.5 sat/vbyte on 201 vbytes = 301.5 -> 302 sat (never below the rate)."""
wallet = _wallet_with_estimate(1500)
value = _invalidation_output_value(wallet, estimated_size=201)
assert value == _BALANCE - 302
assert isinstance(value, int)
# ------------------------------------------------------------------ #
# Main
# ------------------------------------------------------------------ #
if __name__ == "__main__":
for name in sorted(dir()):
if name.startswith("test_"):
globals()[name]()
print(f" [OK] {name}")
print("[OK] All invalidation fee tests passed")

View File

@@ -1,145 +0,0 @@
"""
Tests for the parent/child tree of the will transactions (``father`` links).
Bug reproduced from a real wallet (Electrum log, Check wizard):
File "bal/core/will.py", in check_invalidated
or willtree[w.father].get_status("CONFIRMED")
KeyError: 'a65a682f…'
``father`` is persisted in the wallet (``"Father"``) and was never cleared, so
after the father tx was removed from the will the child pointed to a txid that
no longer exists. ``Will.add_willtree`` now rebuilds the links from scratch and
``Will.check_invalidated`` treats a missing father as "no father".
Run (standalone or with pytest):
QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src \
python3 tests/test_core_willtree_father.py
"""
import os
import sys
from unittest.mock import MagicMock, patch
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from electrum.transaction import Transaction
from bal.core.will import Will, WillItem
# WillItem parses a raw tx and calls add_info_from_wallet; no live wallet here.
_patcher = patch.object(Transaction, "add_info_from_wallet")
_patcher.start()
# A valid serialized transaction (1 input, 1 output) reused by the core tests.
_VALID_TX_HEX = (
"01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65b"
"f38633b424eb4031000000006c493046022100a82bbc57a0136751e543"
"3f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7d"
"e89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d98501"
"2102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae3"
"5cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a"
"42146f11ef8414ae929feaafc388ac00000000"
)
# The input of _VALID_TX_HEX spends this outpoint (little-endian in the hex).
_FUNDING_ID = "3140eb24b43386f35ba69e3875eb6c93130ac66201d01c58f598defc949a5c2a"
# PARENT: _VALID_TX_HEX itself. CHILD: the same tx with its input re-pointed to
# the parent's txid, so the child spends the parent's output 0.
_PARENT_ID = Transaction(_VALID_TX_HEX).txid()
_CHILD_TX_HEX = _VALID_TX_HEX.replace(
bytes.fromhex(_FUNDING_ID)[::-1].hex(), bytes.fromhex(_PARENT_ID)[::-1].hex()
)
# A txid that is not in the will (the "removed father" of the bug report).
_MISSING_ID = "a65a682f2975f0dffd9eccce9392833f227badf617fc67815674736d594d4115"
def _item(tx_hex=_VALID_TX_HEX, father=None):
"""WillItem built from ``tx_hex``, optionally with a stored father."""
data = {"tx": tx_hex, "heirs": {}, "willexecutor": None}
if father is not None:
data["Father"] = father
return WillItem(data)
def _wallet_without_adb():
"""Wallet whose check_invalidated path skips on-chain lookups."""
wallet = MagicMock()
wallet.adb = None
return wallet
def test_stale_father_is_discarded_by_add_willtree():
child = _item(father=_MISSING_ID)
will = {child._id: child}
Will.add_willtree(will)
assert child.father is None
def test_check_invalidated_does_not_crash_on_stale_father():
"""The exact failure of the bug report, now without KeyError."""
child = _item(father=_MISSING_ID)
will = {child._id: child}
Will.add_willtree(will)
Will.check_invalidated(will, [], _wallet_without_adb())
def test_check_invalidated_tolerates_missing_father_without_add_willtree():
"""Defensive path: even if the tree was not rebuilt, no KeyError."""
child = _item(father=_MISSING_ID)
will = {child._id: child}
Will.check_invalidated(will, [], _wallet_without_adb())
assert child.father == _MISSING_ID
def test_real_parent_child_link_is_built():
parent = _item()
child = _item(_CHILD_TX_HEX)
will = {_PARENT_ID: parent, child._id: child}
Will.add_willtree(will)
assert child.father == _PARENT_ID
assert [c[0] for c in parent.children] == [child._id]
assert parent.father is None
def test_stale_father_is_replaced_by_the_real_parent():
parent = _item()
child = _item(_CHILD_TX_HEX, father=_MISSING_ID)
will = {_PARENT_ID: parent, child._id: child}
Will.add_willtree(will)
assert child.father == _PARENT_ID
def test_child_of_unconfirmed_parent_is_not_checked_against_utxos():
"""Existing rule kept: a child of a VALID (not mined) parent is skipped."""
parent = _item()
child = _item(_CHILD_TX_HEX)
will = {_PARENT_ID: parent, child._id: child}
Will.add_willtree(will)
wallet = MagicMock()
# Funding not spent on-chain: nothing gets invalidated, we only observe
# which items had their inputs checked.
with patch.object(Will, "check_tx_height", return_value=-2) as height, \
patch.object(Will, "_funding_really_spent", return_value=False):
Will.check_invalidated({child._id: child, _PARENT_ID: parent}, [], wallet)
# Only the parent (a root) looked up its height; the child was skipped.
checked = [call.args[0] for call in height.call_args_list]
assert checked == [parent.tx]
def test_set_invalidate_cascades_to_children_built_by_add_willtree():
"""add_willtree builds children as a LIST: invalidation must cascade."""
parent = _item()
child = _item(_CHILD_TX_HEX)
will = {parent._id: parent, child._id: child}
Will.add_willtree(will)
Will.set_invalidate(parent._id, will)
assert parent.get_status("INVALIDATED") is True
assert child.get_status("INVALIDATED") is True
if __name__ == "__main__":
for name in sorted(dir()):
if name.startswith("test_"):
globals()[name]()
print(f" [OK] {name}")
print("[OK] All will-tree father tests passed")

View File

@@ -92,31 +92,6 @@ def test_can_delete_none_false():
assert _can_delete(None) is False assert _can_delete(None) is False
def _signed_valid(we="https://we.example", **flags):
"""A VALID + COMPLETE (signed) item, optionally with a will-executor."""
item = FakeWillItem(VALID=True, COMPLETE=True, **flags)
item.we = {"url": we, "selected": True} if we else None
return item
def test_can_delete_signed_but_never_received_by_executor():
"""Push failed / "Not on server": the executor does not hold it."""
assert _can_delete(_signed_valid(PUSH_FAIL=True, CHECK_FAIL=True)) is True
assert _can_delete(_signed_valid()) is True # signed, never sent
def test_can_delete_signed_and_pushed_false():
"""The executor holds it: deleting locally would not cancel it."""
assert _can_delete(_signed_valid(PUSHED=True)) is False
assert _can_delete(_signed_valid(PUSHED=True, CHECKED=True)) is False
assert _can_delete(_signed_valid(CHECKED=True)) is False
def test_can_delete_signed_backup_tx_without_executor_false():
"""Signed txs without a will-executor (backup) stay protected."""
assert _can_delete(_signed_valid(we=None)) is False
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# _apply_select_all # _apply_select_all
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #