Compare commits
1 Commits
346aeee62b
...
fix/i18n-r
| Author | SHA1 | Date | |
|---|---|---|---|
| 2bb9fe812c |
214
CHANGELOG.md
214
CHANGELOG.md
@@ -3587,28 +3587,198 @@ 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
|
||||
modify the will item, old transient fields dropped on load.
|
||||
|
||||
**Scope:** this entry contains ONLY the i18n review fixes. The other fixes
|
||||
prepared in the same session (invalidation fee, Check `KeyError`, cascade,
|
||||
deselected executor) are not part of it and will be proposed separately.
|
||||
**Verification:** extended release test list (as above) plus the new test
|
||||
files: 489 passed, 1 failed (pre-existing: `test_group_e_mock_karen7::test_e5`
|
||||
needs `tests/karen7`, not in the repository; `main` 827a1d1 gives 456 passed,
|
||||
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`),
|
||||
none new.
|
||||
|
||||
**Verification (on `main` 827a1d1):**
|
||||
**Outcome:** DONE (pending the owner's test of the ZIP and Truman's review).
|
||||
|
||||
- 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.
|
||||
---
|
||||
|
||||
**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.
|
||||
|
||||
@@ -767,7 +767,11 @@ class BalController:
|
||||
if not willitems:
|
||||
raise UserFacingException(_("No transaction matches the given txids"))
|
||||
|
||||
willexecutors = Willexecutors.get_willexecutor_transactions(willitems, force=force)
|
||||
# Skip executors the user deselected after building the will (the
|
||||
# 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:
|
||||
return {}
|
||||
|
||||
|
||||
118
bal/core/will.py
118
bal/core/will.py
@@ -26,6 +26,7 @@ The status flags themselves (the source of truth) stay here; only the mapping
|
||||
"status -> colour" now lives in the GUI layer. No behaviour changed.
|
||||
"""
|
||||
|
||||
import math
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from electrum.address_synchronizer import TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL
|
||||
@@ -50,6 +51,13 @@ from .willexecutors import Willexecutors
|
||||
|
||||
MIN_LOCKTIME = 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__)
|
||||
|
||||
|
||||
@@ -68,6 +76,22 @@ class Will:
|
||||
# build a tree with parent transactions
|
||||
@staticmethod
|
||||
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:
|
||||
will[willid].children = Will.get_children(will, willid)
|
||||
for child in will[willid].children:
|
||||
@@ -438,9 +462,68 @@ class Will:
|
||||
out[inp.prevout.to_str()] = inp
|
||||
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
|
||||
def invalidate_will(will, wallet, fees_per_byte, history_label=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")
|
||||
will_only_valid = Will.only_valid_list(will)
|
||||
inputs = Will.get_all_inputs(will_only_valid)
|
||||
@@ -477,7 +560,17 @@ class Will:
|
||||
utxo_to_spend, [out], locktime=locktime, version=2
|
||||
)
|
||||
tx.set_rbf(True)
|
||||
fee = tx.estimated_size() * fees_per_byte
|
||||
fee_rate, fee_source = Will.get_invalidation_fee_rate(
|
||||
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:
|
||||
out = PartialTxOutput.from_address_and_value(
|
||||
change_addresses[0], balance - fee
|
||||
@@ -562,9 +655,15 @@ class Will:
|
||||
def set_invalidate(wid, will=None):
|
||||
will = will if will is not None else {}
|
||||
will[wid].set_status("INVALIDATED", True)
|
||||
if will[wid].children:
|
||||
for c in will[wid].children.items():
|
||||
Will.set_invalidate(c[0], will)
|
||||
children = will[wid].children or []
|
||||
# add_willtree builds a LIST of [child_id, input_idx, prevout_idx];
|
||||
# a dict keyed by child id is still accepted (older data / tests).
|
||||
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
|
||||
def check_tx_height(tx, wallet):
|
||||
@@ -575,10 +674,15 @@ class Will:
|
||||
@staticmethod
|
||||
def check_invalidated(willtree, utxos_list, wallet):
|
||||
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 (
|
||||
not w.father
|
||||
or willtree[w.father].get_status("CONFIRMED")
|
||||
or willtree[w.father].get_status("MEMPOOL")
|
||||
father is None
|
||||
or father.get_status("CONFIRMED")
|
||||
or father.get_status("MEMPOOL")
|
||||
):
|
||||
for inp in w.tx.inputs():
|
||||
inp_str = Util.utxo_to_str(inp)
|
||||
|
||||
@@ -230,12 +230,19 @@ class Willexecutors:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_willexecutor_transactions(will, force=False):
|
||||
def get_willexecutor_transactions(will, force=False, current_willexecutors=None):
|
||||
"""Group the valid, signed, not-yet-pushed will txs by will-executor.
|
||||
|
||||
Returns ``{url: executor_record}`` where each record gets ``txs`` (the
|
||||
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.
|
||||
"""
|
||||
willexecutors = {}
|
||||
@@ -245,6 +252,12 @@ class Willexecutors:
|
||||
if not willitem.get_status("PUSHED") or force:
|
||||
if willexecutor := willitem.we:
|
||||
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 url not in willexecutors:
|
||||
# Work on a COPY of the record: the item's
|
||||
|
||||
@@ -358,8 +358,7 @@ class BalWizardWidget(QWidget):
|
||||
class BalWizardHeirsWidget(BalWizardWidget):
|
||||
title = N_("Bitcoin After Life Heirs")
|
||||
message = N_(
|
||||
"Please add your heirs\n"
|
||||
" remember that the wallet balance will be spent at 100%"
|
||||
"Please add your heirs\n remember that 100% of wallet balance will be spent"
|
||||
)
|
||||
|
||||
def get_content(self):
|
||||
|
||||
@@ -93,13 +93,27 @@ def _can_broadcast(will_item):
|
||||
|
||||
|
||||
def _can_delete(will_item):
|
||||
"""True if a will transaction may be deleted (invalid or unsigned).
|
||||
"""True if a will transaction may be deleted.
|
||||
|
||||
Valid AND fully-signed transactions are never deletable: removing them
|
||||
would silently drop a committed inheritance.
|
||||
Deletable:
|
||||
* invalid or not fully signed transactions (unchanged rule);
|
||||
* 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.
|
||||
"""
|
||||
return bool(will_item) and (
|
||||
not will_item.get_status("VALID") or not will_item.get_status("COMPLETE")
|
||||
if not will_item:
|
||||
return False
|
||||
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")
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1632,7 +1632,15 @@ class BalWindow:
|
||||
willitems = {
|
||||
t: willitems[t] for t in txids if t in willitems
|
||||
}
|
||||
willexecutors = Willexecutors.get_willexecutor_transactions(willitems, force=force)
|
||||
# Read the CURRENT executor list from the config, so an executor the
|
||||
# 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):
|
||||
msg = _("Broadcasting Transactions to Will-Executors:") + "\n"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-10-07 14:25-0400\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 <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -60,7 +60,7 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
#: bal/core/will.py:1348 bal/gui/qt/dialogs.py:825
|
||||
#: bal/core/will.py:1348 bal/gui/qt/dialogs.py:824
|
||||
msgid "Expired"
|
||||
msgstr ""
|
||||
|
||||
@@ -104,7 +104,7 @@ msgstr ""
|
||||
msgid "Updated"
|
||||
msgstr ""
|
||||
|
||||
#: bal/core/will.py:1359 bal/gui/qt/dialogs.py:2633
|
||||
#: bal/core/will.py:1359 bal/gui/qt/dialogs.py:2632
|
||||
msgid "Valid"
|
||||
msgstr ""
|
||||
|
||||
@@ -195,15 +195,15 @@ msgstr ""
|
||||
msgid "Bal Wizard Setup"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:325 bal/gui/qt/dialogs.py:2762
|
||||
#: bal/gui/qt/dialogs.py:325 bal/gui/qt/dialogs.py:2760
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:329 bal/gui/qt/dialogs.py:2765
|
||||
#: bal/gui/qt/dialogs.py:329 bal/gui/qt/dialogs.py:2763
|
||||
msgid "Next"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:333 bal/gui/qt/dialogs.py:4048
|
||||
#: bal/gui/qt/dialogs.py:333 bal/gui/qt/dialogs.py:4019
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
@@ -212,193 +212,194 @@ msgid "Bitcoin After Life Heirs"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:360
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Please add your heirs\n"
|
||||
" remember that the wallet balance will be spent at 100%"
|
||||
" remember that 100% of wallet balance will be spent"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:370 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:372 bal/gui/qt/dialogs.py:2594 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:374 bal/gui/qt/dialogs.py:2430
|
||||
#: bal/gui/qt/dialogs.py:2598 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:399 bal/gui/qt/dialogs.py:507
|
||||
#: bal/gui/qt/dialogs.py:398 bal/gui/qt/dialogs.py:506
|
||||
msgid "Bitcoin After Life Will-Executors"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:400
|
||||
#: bal/gui/qt/dialogs.py:399
|
||||
msgid "Choose willexecutors download method"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:407
|
||||
#: bal/gui/qt/dialogs.py:406
|
||||
msgid "Automatically download and select willexecutors"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:408
|
||||
#: bal/gui/qt/dialogs.py:407
|
||||
msgid "Only download willexecutors list"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:409
|
||||
#: bal/gui/qt/dialogs.py:408
|
||||
msgid "Import willexecutor list from file"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:410
|
||||
#: bal/gui/qt/dialogs.py:409
|
||||
msgid "Manual"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:439 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:508
|
||||
#: bal/gui/qt/dialogs.py:507
|
||||
msgid "Configure and select your willexecutors"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:527
|
||||
#: bal/gui/qt/dialogs.py:526
|
||||
msgid "Bitcoin After Life Will Settings"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:562 bal/gui/qt/dialogs.py:615
|
||||
#: bal/gui/qt/dialogs.py:561 bal/gui/qt/dialogs.py:614
|
||||
msgid "Please wait"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:645
|
||||
#: bal/gui/qt/dialogs.py:644
|
||||
msgid "Building Will"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:650
|
||||
#: bal/gui/qt/dialogs.py:649
|
||||
msgid "Building Will:"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:731 bal/gui/qt/dialogs.py:756
|
||||
#: bal/gui/qt/dialogs.py:766 bal/gui/qt/dialogs.py:789
|
||||
#: 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:757
|
||||
#: bal/gui/qt/dialogs.py:756
|
||||
msgid "Check Alive Threshold Passed: you have to Invalidate your old Will"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:766 bal/gui/qt/dialogs.py:848
|
||||
#: bal/gui/qt/dialogs.py:765 bal/gui/qt/dialogs.py:847
|
||||
msgid "No Heirs"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:789 bal/gui/qt/dialogs.py:1336
|
||||
#: bal/gui/qt/dialogs.py:2282 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:793
|
||||
#: 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:802 bal/gui/qt/window.py:844
|
||||
#: bal/gui/qt/dialogs.py:801 bal/gui/qt/window.py:844
|
||||
msgid "Will-executor fee too high: {}"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:836
|
||||
#: bal/gui/qt/dialogs.py:835
|
||||
msgid "Postponed: invalidating old will"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:871 bal/gui/qt/dialogs.py:880
|
||||
#: bal/gui/qt/dialogs.py:2242
|
||||
#: 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:873
|
||||
#: bal/gui/qt/dialogs.py:872
|
||||
msgid "An inheritance of this wallet is already executed (on blockchain)"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:882
|
||||
#: bal/gui/qt/dialogs.py:881
|
||||
msgid "Inheritance in mempool (waiting confirmation)"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:915
|
||||
#: bal/gui/qt/dialogs.py:914
|
||||
msgid "Will-Executor excluded"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:915 bal/gui/qt/dialogs.py:1009
|
||||
#: bal/gui/qt/dialogs.py:2223 bal/gui/qt/dialogs.py:2234
|
||||
#: 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:921 bal/gui/qt/dialogs.py:1706 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:922
|
||||
#: bal/gui/qt/dialogs.py:921
|
||||
msgid "Not present - select one or enable backup mode"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:983
|
||||
#: 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:999
|
||||
#: 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:1043
|
||||
#: bal/gui/qt/dialogs.py:1042
|
||||
msgid "Heir {}"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:1045
|
||||
#: bal/gui/qt/dialogs.py:1044
|
||||
msgid "{} is DUST - excluded (amount below dust limit)"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:1101
|
||||
#: bal/gui/qt/dialogs.py:1100
|
||||
msgid "Heir"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:1218 bal/gui/qt/dialogs.py:2271
|
||||
#: bal/gui/qt/dialogs.py:1217 bal/gui/qt/dialogs.py:2270
|
||||
msgid "Please wait {}secs"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:1225 bal/gui/qt/dialogs.py:1277
|
||||
#: bal/gui/qt/dialogs.py:1315
|
||||
#: 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:1336 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:1347
|
||||
#: bal/gui/qt/dialogs.py:1346
|
||||
msgid "Timeout - no answer"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:1389
|
||||
#: bal/gui/qt/dialogs.py:1388
|
||||
msgid "checking {} - {}"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:1392 bal/gui/qt/dialogs.py:2240
|
||||
#: 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:1406
|
||||
#: bal/gui/qt/dialogs.py:1405
|
||||
msgid "checked {} - {}"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:1456 bal/gui/qt/dialogs.py:2026
|
||||
#: bal/gui/qt/dialogs.py:2037
|
||||
#: 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:1517
|
||||
#: 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"
|
||||
@@ -406,159 +407,159 @@ msgid ""
|
||||
"After the invalidation is confirmed, press the Check button to finish the will."
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:1546 bal/gui/qt/window.py:849
|
||||
#: 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:1569
|
||||
#: 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:1585
|
||||
#: bal/gui/qt/dialogs.py:1584
|
||||
msgid "Invalidate your old will"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:1599 bal/gui/qt/dialogs.py:1644
|
||||
#: bal/gui/qt/dialogs.py:1598 bal/gui/qt/dialogs.py:1643
|
||||
msgid "Aborted"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:1618
|
||||
#: bal/gui/qt/dialogs.py:1617
|
||||
msgid "Auto-sign disabled — sign manually"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:1631
|
||||
#: 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:1641
|
||||
#: bal/gui/qt/dialogs.py:1640
|
||||
msgid "Sign your will"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:1646 bal/gui/qt/dialogs.py:1929
|
||||
#: bal/gui/qt/dialogs.py:1645 bal/gui/qt/dialogs.py:1928
|
||||
msgid "Nothing to do"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:1665
|
||||
#: bal/gui/qt/dialogs.py:1664
|
||||
msgid "Finished"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:1682
|
||||
#: bal/gui/qt/dialogs.py:1681
|
||||
msgid "Calendar"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:1684 bal/gui/qt/dialogs.py:1714
|
||||
#: bal/gui/qt/dialogs.py:1781 bal/gui/qt/dialogs.py:2029
|
||||
#: bal/gui/qt/dialogs.py:2976 bal/gui/qt/dialogs.py:3231
|
||||
#: bal/gui/qt/dialogs.py:4067
|
||||
#: 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:1710 bal/gui/qt/dialogs.py:1777
|
||||
#: bal/gui/qt/dialogs.py:1709 bal/gui/qt/dialogs.py:1776
|
||||
msgid "🔮 Wizard"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:1773 bal/gui/qt/dialogs.py:2581
|
||||
#: bal/gui/qt/dialogs.py:1772 bal/gui/qt/dialogs.py:2580
|
||||
msgid "Heirs"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:1882 bal/gui/qt/widgets.py:1150
|
||||
#: bal/gui/qt/dialogs.py:1881 bal/gui/qt/widgets.py:1150
|
||||
msgid "(reminder {idx}/{total})"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:1946
|
||||
#: bal/gui/qt/dialogs.py:1945
|
||||
msgid "All done"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:1997
|
||||
#: 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:2002
|
||||
#: bal/gui/qt/dialogs.py:2001
|
||||
msgid "Next step (manual): press 'Sign' to sign your will."
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2006
|
||||
#: 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:2101
|
||||
#: 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:2106
|
||||
#: bal/gui/qt/dialogs.py:2105
|
||||
msgid "The miner fee rate changed: a new will must be prepared."
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2110
|
||||
#: 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:2115
|
||||
#: 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:2124
|
||||
#: 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:2133
|
||||
#: bal/gui/qt/dialogs.py:2132
|
||||
msgid "The heirs changed: a new will must be prepared."
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2135
|
||||
#: bal/gui/qt/dialogs.py:2134
|
||||
msgid "A will-executor changed: a new will must be prepared."
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2144
|
||||
#: 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:2148
|
||||
#: 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:2154
|
||||
#: 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:2184
|
||||
#: bal/gui/qt/dialogs.py:2183
|
||||
msgid "No heirs: add at least one heir before building the will."
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2187
|
||||
#: 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:2191
|
||||
#: 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:2196
|
||||
#: 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:2201
|
||||
#: 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:2206
|
||||
#: 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:2211
|
||||
#: 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:2215
|
||||
#: 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:2226
|
||||
#: 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"
|
||||
@@ -567,403 +568,403 @@ msgid ""
|
||||
"- at least one will-executor is selected and reachable."
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2247
|
||||
#: bal/gui/qt/dialogs.py:2246
|
||||
msgid "Invalidating old will"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2253
|
||||
#: bal/gui/qt/dialogs.py:2252
|
||||
msgid "Building your will"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2259
|
||||
#: bal/gui/qt/dialogs.py:2258
|
||||
msgid "Signing your will"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2265
|
||||
#: bal/gui/qt/dialogs.py:2264
|
||||
msgid "Broadcasting your will to executors"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2313
|
||||
#: bal/gui/qt/dialogs.py:2312
|
||||
msgid "Wait"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2416
|
||||
#: bal/gui/qt/dialogs.py:2415
|
||||
msgid "Will Details"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2422 bal/gui/qt/dialogs.py:4042
|
||||
#: 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:2426 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:2433 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:2438 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:2446
|
||||
#: bal/gui/qt/dialogs.py:2445
|
||||
msgid "Expiration date: {}"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2452
|
||||
#: bal/gui/qt/dialogs.py:2451
|
||||
msgid "Valid Txs:{}"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2454
|
||||
#: bal/gui/qt/dialogs.py:2453
|
||||
msgid "Total Txs:{}"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2508
|
||||
#: bal/gui/qt/dialogs.py:2507
|
||||
msgid "Hide replaced"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2510
|
||||
#: bal/gui/qt/dialogs.py:2509
|
||||
msgid "Unhide replaced"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2516
|
||||
#: bal/gui/qt/dialogs.py:2515
|
||||
msgid "Hide invalidated"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2518
|
||||
#: bal/gui/qt/dialogs.py:2517
|
||||
msgid "Unhide invalidated"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2543
|
||||
#: bal/gui/qt/dialogs.py:2542
|
||||
msgid "Will-Executor Service List"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2590 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:2632
|
||||
#: bal/gui/qt/dialogs.py:2631
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2635
|
||||
#: bal/gui/qt/dialogs.py:2634
|
||||
msgid "Valid NC"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2773 bal/gui/qt/dialogs.py:2851
|
||||
#: bal/gui/qt/dialogs.py:2771 bal/gui/qt/dialogs.py:2847
|
||||
msgid "Auto"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2775
|
||||
#: bal/gui/qt/dialogs.py:2773
|
||||
msgid "Automatically advance through the QR codes."
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2779
|
||||
#: bal/gui/qt/dialogs.py:2777
|
||||
msgid "QR codes per second:"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2783
|
||||
#: bal/gui/qt/dialogs.py:2781
|
||||
msgid " /s"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2785
|
||||
#: bal/gui/qt/dialogs.py:2783
|
||||
msgid "Loop"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2787
|
||||
#: 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:2797
|
||||
#: bal/gui/qt/dialogs.py:2795
|
||||
msgid "QR code size:"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2845
|
||||
#: bal/gui/qt/dialogs.py:2841
|
||||
msgid "Stop"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2866
|
||||
#: 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:2888
|
||||
#: bal/gui/qt/dialogs.py:2884
|
||||
msgid "Frame {} of {}"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2911
|
||||
#: bal/gui/qt/dialogs.py:2907
|
||||
msgid "Export will"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2922
|
||||
#: bal/gui/qt/dialogs.py:2917
|
||||
msgid "No will transaction to export."
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2929
|
||||
#: bal/gui/qt/dialogs.py:2924
|
||||
msgid "Export:"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2936
|
||||
#: bal/gui/qt/dialogs.py:2931
|
||||
msgid "Whole will"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2939
|
||||
#: 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:2950
|
||||
#: bal/gui/qt/dialogs.py:2945
|
||||
msgid "Send as:"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2952 bal/gui/qt/dialogs.py:3208
|
||||
#: bal/gui/qt/dialogs.py:2947 bal/gui/qt/dialogs.py:3184
|
||||
msgid "File"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2953 bal/gui/qt/dialogs.py:3209
|
||||
#: bal/gui/qt/dialogs.py:2948 bal/gui/qt/dialogs.py:3185
|
||||
msgid "QR Code"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:2954 bal/gui/qt/dialogs.py:3210
|
||||
#: bal/gui/qt/dialogs.py:2949 bal/gui/qt/dialogs.py:3186
|
||||
msgid "Audio"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3005
|
||||
#: 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:3014
|
||||
#: bal/gui/qt/dialogs.py:2999
|
||||
msgid "Export…"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3032 bal/gui/qt/dialogs.py:3271
|
||||
#: 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:3036 bal/gui/qt/dialogs.py:3275
|
||||
#: bal/gui/qt/dialogs.py:3020 bal/gui/qt/dialogs.py:3246
|
||||
msgid "Speed (KB/sec):"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3047
|
||||
#: bal/gui/qt/dialogs.py:3031
|
||||
msgid "Send"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3100 bal/gui/qt/dialogs.py:3134
|
||||
#: bal/gui/qt/dialogs.py:3159
|
||||
#: 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:3123
|
||||
#: bal/gui/qt/dialogs.py:3099
|
||||
msgid "item(s)"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3123
|
||||
#: bal/gui/qt/dialogs.py:3099
|
||||
msgid "transaction(s)"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3125
|
||||
#: bal/gui/qt/dialogs.py:3101
|
||||
msgid "{} {} will be exported."
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3128
|
||||
#: bal/gui/qt/dialogs.py:3104
|
||||
msgid "{} {} will be sent."
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3152 bal/gui/qt/dialogs.py:3155
|
||||
#: bal/gui/qt/dialogs.py:3311 bal/gui/qt/dialogs.py:3314
|
||||
#: 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:3191
|
||||
#: bal/gui/qt/dialogs.py:3167
|
||||
msgid "Import will"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3197
|
||||
#: 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:3206
|
||||
#: bal/gui/qt/dialogs.py:3182
|
||||
msgid "Import from:"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3246
|
||||
#: 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:3253
|
||||
#: bal/gui/qt/dialogs.py:3225
|
||||
msgid "Choose will file…"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3284
|
||||
#: bal/gui/qt/dialogs.py:3255
|
||||
msgid "Waiting for the audio transfer…"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3287
|
||||
#: bal/gui/qt/dialogs.py:3258
|
||||
msgid "Receive by audio…"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3328
|
||||
#: bal/gui/qt/dialogs.py:3299
|
||||
msgid "Receiving…"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3348
|
||||
#: bal/gui/qt/dialogs.py:3319
|
||||
msgid "No transaction data received."
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3366
|
||||
#: bal/gui/qt/dialogs.py:3337
|
||||
msgid "Waiting for audio ({:.1f} kbps)…"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3439
|
||||
#: bal/gui/qt/dialogs.py:3410
|
||||
msgid "Could not parse a transferred will item: {}"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3459
|
||||
#: bal/gui/qt/dialogs.py:3430
|
||||
msgid "Could not parse a transferred transaction: {}"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3470
|
||||
#: bal/gui/qt/dialogs.py:3441
|
||||
msgid "The imported will contains no valid transaction in this wallet."
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3481
|
||||
#: 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:3581
|
||||
#: 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:3591 bal/gui/qt/dialogs.py:3769
|
||||
#: bal/gui/qt/dialogs.py:3562 bal/gui/qt/dialogs.py:3740
|
||||
msgid "Waiting for the first frame…"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3608
|
||||
#: bal/gui/qt/dialogs.py:3579
|
||||
msgid "…or paste/type the frame text here"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3612
|
||||
#: bal/gui/qt/dialogs.py:3583
|
||||
msgid "Add frame"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3618 bal/gui/qt/dialogs.py:3861
|
||||
#: bal/gui/qt/dialogs.py:3589 bal/gui/qt/dialogs.py:3832
|
||||
msgid "Scan with camera…"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3620
|
||||
#: 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:3627
|
||||
#: bal/gui/qt/dialogs.py:3598
|
||||
msgid "Reset"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3634
|
||||
#: bal/gui/qt/dialogs.py:3605
|
||||
msgid "Review and Sign…"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3653
|
||||
#: 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:3744
|
||||
#: bal/gui/qt/dialogs.py:3715
|
||||
msgid "All {} frames stored."
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3750
|
||||
#: bal/gui/qt/dialogs.py:3721
|
||||
msgid "Stored {} of {} frames."
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3815
|
||||
#: bal/gui/qt/dialogs.py:3786
|
||||
msgid "Cannot start QR scanner, no usable camera found."
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3820
|
||||
#: bal/gui/qt/dialogs.py:3791
|
||||
msgid "Stop scanning"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3867
|
||||
#: bal/gui/qt/dialogs.py:3838
|
||||
msgid "Camera error: {}"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3913
|
||||
#: 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:3957
|
||||
#: bal/gui/qt/dialogs.py:3928
|
||||
msgid "The transferred will contains no transactions."
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3986
|
||||
#: bal/gui/qt/dialogs.py:3957
|
||||
msgid "Review and sign imported will"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:3998
|
||||
#: bal/gui/qt/dialogs.py:3969
|
||||
msgid "Enter your wallet password to sign the imported transactions."
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:4045
|
||||
#: bal/gui/qt/dialogs.py:4016
|
||||
msgid "Skip"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:4061
|
||||
#: bal/gui/qt/dialogs.py:4032
|
||||
msgid "Save signed file…"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:4064
|
||||
#: bal/gui/qt/dialogs.py:4035
|
||||
msgid "Show signed QR…"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:4088
|
||||
#: bal/gui/qt/dialogs.py:4059
|
||||
msgid "Transaction {} of {}"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:4090
|
||||
#: bal/gui/qt/dialogs.py:4061
|
||||
msgid "TXID: {}"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:4093
|
||||
#: bal/gui/qt/dialogs.py:4064
|
||||
msgid "unknown"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:4099
|
||||
#: bal/gui/qt/dialogs.py:4070
|
||||
msgid "(no outputs)"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:4113
|
||||
#: bal/gui/qt/dialogs.py:4084
|
||||
msgid "unknown (partial transaction)"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:4115
|
||||
#: bal/gui/qt/dialogs.py:4086
|
||||
msgid ""
|
||||
"Total outputs: {}\n"
|
||||
"Fee: {}"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:4126
|
||||
#: 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:4145
|
||||
#: bal/gui/qt/dialogs.py:4116
|
||||
msgid "Could not sign the transaction: {}"
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:4164
|
||||
#: bal/gui/qt/dialogs.py:4135
|
||||
msgid "Signed will saved."
|
||||
msgstr ""
|
||||
|
||||
#: bal/gui/qt/dialogs.py:4175
|
||||
#: bal/gui/qt/dialogs.py:4146
|
||||
msgid "No signed transaction to show."
|
||||
msgstr ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
82
tests/test_core_deselected_executor.py
Normal file
82
tests/test_core_deselected_executor.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
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")
|
||||
231
tests/test_core_invalidation_fee.py
Normal file
231
tests/test_core_invalidation_fee.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
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")
|
||||
145
tests/test_core_willtree_father.py
Normal file
145
tests/test_core_willtree_father.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
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")
|
||||
@@ -92,6 +92,31 @@ def test_can_delete_none_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
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
Reference in New Issue
Block a user