10 Commits

8 changed files with 285 additions and 13 deletions

View File

@@ -2149,3 +2149,183 @@ earlier)" as one of the three possible reasons (v0.5.9).
- `ruff`: no new errors. - `ruff`: no new errors.
**Outcome:** DONE (delivered as test ZIP v0.5.13; commit only after confirmation). **Outcome:** DONE (delivered as test ZIP v0.5.13; commit only after confirmation).
---
## 38. v0.5.14 - Shorten long Tor (.onion) will-executor URLs in the list
**Date:** 2026-07-15
**Goal (owner request):** In the will-executor list, a Tor (.onion) URL is very
long and overflows the URL column. Show it shortened (like the welist site at
welist.bitcoin-after.life), keeping the full address available.
**What changed (`bal/gui/qt/lists.py`, display only):**
- `WillExecutorListWidget.update()`: the URL column now displays a shortened
form for long addresses - the first 37 characters + an ellipsis (matching the
welist site) - while the FULL url is still stored as the row's key data and is
set as the cell tooltip (so hovering shows the complete address). URLs of 40
characters or fewer are shown unchanged.
- New `_FullUrlEditDelegate` installed on the URL column: when the user
double-clicks the URL cell to edit it, the editor is preloaded with the FULL
url (from the key role) instead of the shortened display text. Without this,
editing a shortened URL would have saved the truncated text and corrupted the
will-executor key. Verified against Electrum's edit-commit path
(my_treeview on_commitData -> on_edited): the committed text is the editor's
text (now the full URL) and the edit key is read from the role, so an
unmodified URL round-trips intact.
- Only the URL column's presentation/editor changed; download, ping and
connection logic are untouched.
**Verification:**
- Full test suite against **Electrum 4.7.2**: `266 passed`; against **Electrum
4.8.0**: `266 passed` (same 2 pre-existing, unrelated failures in both).
- `ruff`: no new errors.
- Shortening check: the 69-char sample .onion becomes a 38-char display
("http://gxvdgcqdy2s7x5cujmkaqua72r2aqo..."), while short https URLs are shown
unchanged.
**Note:** the separate ~5s delay observed when contacting the .onion server is
inherent to Tor building a circuit to the hidden service, not a plugin bug; it
was analysed but intentionally NOT changed here.
**Outcome:** DONE (delivered as test ZIP v0.5.14; commit only after confirmation).
---
## 39. v0.5.15 - Fix KeyError on delete/select/ping of shortened .onion will-executors
**Date:** 2026-07-15
**Problem (owner-reported):** After v0.5.14 shortened long .onion URLs in the
will-executor list, deleting (or selecting/deselecting/pinging) a server raised:
KeyError: 'http://gxvdgcqdy2s7x5cujmkaqua72r2aqo...'
**Cause:** `WillExecutorListWidget.create_menu` collected the selected key with
`item.data(0)`, which returns the DISPLAY text - now the SHORTENED URL (with the
ellipsis). That truncated string was then used as a dict key in
delete/select/deselect/ping, none of which exist in `willexecutors_list` (keyed
by the full URL) -> KeyError. This was a regression introduced by v0.5.14.
**Fix (`bal/gui/qt/lists.py`, `create_menu`):** the selected key is now read from
the real key role (`ROLE_HEIR_KEY + Columns.URL`), which always holds the full
URL - the same mechanism already used by `get_edit_key_from_coordinate` and
`on_double_click` - with a fallback to `item.data(0)` only if the role is
missing. So the context-menu actions operate on the full URL again.
**Verification:**
- Full test suite against **Electrum 4.7.2**: `266 passed`; against **Electrum
4.8.0**: `266 passed` (same 2 pre-existing, unrelated failures in both).
- `ruff`: no new errors.
- Checked there are no other `.data(0)` key usages left in the will-executor
list (the remaining ones are in the heir/will lists, which do not shorten
URLs).
**Outcome:** DONE (delivered as test ZIP v0.5.15; commit only after confirmation).
---
## 40. v0.5.16 - Skip Tor (.onion) will-executors from download when Electrum is not on Tor
**Date:** 2026-07-16
**Goal (owner request):** A .onion will-executor is only reachable through Tor.
Users connected via Tor should get the .onion servers in the welist download;
users NOT on Tor should NOT download them at all - contacting them would only
waste time (they are unreachable). No user-facing option: the behaviour is
driven solely by whether Electrum is connected through Tor.
**What changed:**
- `bal/core/willexecutors.py`: new helpers `is_onion_url(url)` (detects .onion
hosts) and `is_tor_active()` (True only when `Network.is_proxy_tor is True`;
False/None => not on Tor; read defensively for offline mode). Present in both
Electrum 4.7.2 and 4.8.0.
- `bal/gui/qt/window.py` (`fetch_will_executors_list`): when Tor is NOT active,
.onion servers are dropped from the downloaded list (not initialised, not
shown). When Tor is active, all servers are kept as before.
- `bal/gui/qt/common.py`: re-export the two helpers for the Qt layer.
**Note:** this replaces an earlier, more complex experiment (a "Force onion"
setting plus a non-selectable "Tor off" row) that was never committed to Gitea;
the final behaviour is the simple Tor-driven filter described above.
**Verification:**
- Full test suite against **Electrum 4.7.2**: `266 passed`; against **Electrum
4.8.0**: `266 passed` (same 2 pre-existing, unrelated failures in both).
- `ruff`: no new errors.
- Filter check: with Tor off the .onion server is removed from the list and only
the https servers remain; with Tor on the .onion server is kept.
**Caveat:** the test suite does not simulate a real Tor connection, so the
decisive confirmation (with Electrum on Tor = celeste dot vs off = green dot)
must be done by the owner on Windows.
**Outcome:** DONE (delivered as test ZIP v0.5.16; commit only after confirmation).
---
## 41. v0.5.17 - Fix crash when the welist server returns a non-dict response
**Date:** 2026-07-16
**Problem (owner-reported, Electrum 4.8.0, Tor active):** pressing Download List
could crash with:
ValueError: dictionary update sequence element #0 has length 1; 2 is required
at window.py on_success -> self.willexecutors.update(result)
**Root cause:** the will-executor list download assumed the server always
returns a dict of {url: info}. `handle_response` does `json.loads(...)` and, when
the body is NOT valid JSON (e.g. an error/HTML/plain-text page - more likely over
Tor when a server misbehaves), it returns the RAW STRING instead. That string
then reached `self.willexecutors.update(result)`, and `dict.update("...")`
iterates the string characterwise, raising the ValueError. The bug was latent
(not introduced by the onion filter) and surfaced with Tor active.
**Fix (`bal/gui/qt/window.py`):**
- `fetch_will_executors_list`: only accept the response if it is a `dict`;
otherwise log it, record "invalid response format" and try the next candidate
(so the normal "download failed" message is shown instead of crashing). Also,
each entry is now checked to be a dict before use (malformed entries skipped).
- `on_success`: second line of defense - only `self.willexecutors.update(result)`
when `result` is a non-empty dict; otherwise show the clean download-failed
warning. A non-dict can no longer crash the callback.
**Verification:**
- Anti-crash check: on_success with a string, a list or an empty dict shows the
warning without crashing; a valid dict updates normally.
- Full test suite against **Electrum 4.7.2**: `266 passed`; against **Electrum
4.8.0**: `266 passed` (same 2 pre-existing, unrelated failures in both).
- `ruff`: no new errors.
**Outcome:** DONE (delivered as test ZIP v0.5.17; commit only after confirmation).
---
## 42. v0.5.18 - Clearer message when the list download fails/times out over Tor
**Date:** 2026-07-16
**Context (owner-reported):** With Electrum connected through Tor, "Download
List" could sit until the timeout and download nothing, while the exact same
build worked fine without Tor (or when the user's exit IP changed via VPN). This
is not a plugin bug: it is a slow/obstructed Tor path on the user's connection.
The request was to make the failure message clearer (no timeout change).
**What changed (`bal/gui/qt/window.py`, text/UX only):**
- New `DOWNLOAD_FAILED_TOR_MESSAGE`: "Could not download the will-executors list
over Tor. ... Your Tor connection may be slow. Please try again, or use a VPN
(or temporarily disable Tor) for a faster connection."
- Both failure paths (`on_success` with an empty/invalid result, and the generic
branch of `on_failure`) now show the Tor-specific message when
`is_tor_active()` is true, and the existing generic message otherwise.
- The download timeout itself is unchanged.
**Verification:**
- Full test suite against **Electrum 4.7.2**: `266 passed`; against **Electrum
4.8.0**: `266 passed` (same 2 pre-existing, unrelated failures in both).
- `ruff`: no new errors.
**Outcome:** DONE (delivered as test ZIP v0.5.18; commit only after confirmation).

View File

@@ -1 +1 @@
0.5.15 0.5.18

View File

@@ -36,4 +36,4 @@ The plugin supports Electrum 4.7.2 and 4.8.0 with PyQt6. Electrum 4.8.0 removed
available and adapts, so both releases keep working. available and adapts, so both releases keep working.
""" """
__version__ = "0.5.15" __version__ = "0.5.18"

View File

@@ -121,7 +121,7 @@ class BalPlugin(BasePlugin):
""" """
_version = None _version = None
__version__ = "0.5.13" # AUTOMATICALLY GENERATED DO NOT EDIT __version__ = "0.5.18" # AUTOMATICALLY GENERATED DO NOT EDIT
# Command used to open an .ics calendar file, per operating system. # Command used to open an .ics calendar file, per operating system.
default_app = { default_app = {

View File

@@ -68,6 +68,47 @@ CHECK_GLOBAL_DEADLINE = NETWORK_DEADLINE
_logger = get_logger(__name__) _logger = get_logger(__name__)
# --------------------------------------------------------------------------- #
# Tor / .onion helpers
# --------------------------------------------------------------------------- #
# A .onion will-executor is only reachable when Electrum routes traffic through
# Tor. There is no point downloading .onion servers from welist when Electrum is
# not on Tor: they would only waste time (unreachable). We therefore drop them
# from the downloaded list unless Tor is confirmed active. Electrum itself gates
# .onion the same way (`if host.endswith('.onion') and not self.is_proxy_tor`).
def is_onion_url(url):
"""True if the URL points to a Tor hidden service (.onion host)."""
if not url:
return False
try:
u = url.lower()
if "://" in u:
u = u.split("://", 1)[1]
host = u.split("/", 1)[0].split(":", 1)[0]
return host.endswith(".onion")
except Exception:
return False
def is_tor_active():
"""True only if Electrum is CONFIRMED connected through Tor.
Reads Network.is_proxy_tor defensively (network may be None in offline
mode). It is TRI-STATE (True / False / None); we require True, so False and
None (undetermined) both mean "Tor not active". Available in Electrum 4.7.2
and 4.8.0.
"""
try:
network = Network.get_instance()
if network is None:
return False
return network.is_proxy_tor is True
except Exception:
return False
chainname = BalPlugin.chainname chainname = BalPlugin.chainname

View File

@@ -72,6 +72,7 @@ from ...core.will import (AmountException, HeirChangeException,
WillexecutorChangeException, WillExecutorNotPresent, WillexecutorChangeException, WillExecutorNotPresent,
WillExpiredException, WillItem, WillPostponedException) WillExpiredException, WillItem, WillPostponedException)
from ...core.willexecutors import Willexecutors from ...core.willexecutors import Willexecutors
from ...core.willexecutors import is_onion_url, is_tor_active # noqa: F401
# --- Presentation helpers --- # --- Presentation helpers ---
from .theme import server_status_text, server_status_tooltip, status_color from .theme import server_status_text, server_status_tooltip, status_color

View File

@@ -1156,9 +1156,35 @@ class BalWindow:
f"len={len(resp) if hasattr(resp, '__len__') else 'n/a'}" f"len={len(resp) if hasattr(resp, '__len__') else 'n/a'}"
) )
if resp: if resp:
# Robustness: the server is expected to return a dict of
# {url: info}. If it returns anything else (a raw string
# error page, a list, etc. - more likely over Tor when a
# server misbehaves), do NOT use it: that would later crash
# on `self.willexecutors.update(result)`. Treat it as an
# empty/failed response and try the next candidate.
if not isinstance(resp, dict):
_logger.warning(
f"fetch_will_executors_list: {url} -> unexpected "
f"response type {type(resp).__name__}, ignoring"
)
last_error = "invalid response format"
continue
result = resp result = resp
for w in result: # Tor gating: if Electrum is NOT connected through Tor, drop
if w not in ("status", "url"): # the .onion servers entirely - they are unreachable without
# Tor and would only waste time. When on Tor, keep them all.
tor_on = is_tor_active()
for w in list(result.keys()):
if w in ("status", "url"):
continue
# Defensive: each entry must be a dict too; skip anything
# malformed rather than crashing downstream.
if not isinstance(result.get(w), dict):
del result[w]
continue
if not tor_on and is_onion_url(w):
del result[w]
continue
Willexecutors.initialize_willexecutor( Willexecutors.initialize_willexecutor(
result[w], w, None, result[w], w, None,
old_willexecutors.get(w, None), old_willexecutors.get(w, None),
@@ -1191,6 +1217,16 @@ class BalWindow:
"and try again." "and try again."
) )
# Shown when the download fails while Electrum is connected through Tor:
# the most common cause is a slow Tor connection, so guide the user
# accordingly instead of the generic message above.
DOWNLOAD_FAILED_TOR_MESSAGE = (
"Could not download the will-executors list over Tor.\n\n"
"Electrum is connected through Tor and the connection is taking too "
"long. Your Tor connection may be slow. Please try again, or use a VPN "
"(or temporarily disable Tor) for a faster connection."
)
def download_list(self, willexecutors, fn_on_success, fn_on_failure=None): def download_list(self, willexecutors, fn_on_success, fn_on_failure=None):
if fn_on_failure is None: if fn_on_failure is None:
fn_on_failure = log_error fn_on_failure = log_error
@@ -1235,9 +1271,18 @@ class BalWindow:
stop_heartbeat.set() stop_heartbeat.set()
def on_success(result): def on_success(result):
if result: # Defensive: only merge a proper dict (see fetch_will_executors_list).
# A non-dict here would raise "dictionary update sequence element..."
if isinstance(result, dict) and result:
self.willexecutors.update(result) self.willexecutors.update(result)
fn_on_success(result) fn_on_success(result)
else:
# Tor-aware failure message: a download failure/timeout while
# Electrum is connected through Tor is most often a slow Tor
# connection, so point the user to that (try again / VPN /
# disable Tor). Otherwise keep the generic connection message.
if is_tor_active():
self.show_warning(_(self.DOWNLOAD_FAILED_TOR_MESSAGE))
else: else:
self.show_warning(_(self.DOWNLOAD_FAILED_MESSAGE)) self.show_warning(_(self.DOWNLOAD_FAILED_MESSAGE))
@@ -1269,6 +1314,11 @@ class BalWindow:
f"No active will-executor found for the " f"No active will-executor found for the "
f"{err.chain} network." f"{err.chain} network."
)) ))
else:
# Tor-aware: a generic failure/timeout while on Tor is most
# likely a slow Tor connection.
if is_tor_active():
self.show_warning(_(self.DOWNLOAD_FAILED_TOR_MESSAGE))
else: else:
self.show_warning(_(self.DOWNLOAD_FAILED_MESSAGE)) self.show_warning(_(self.DOWNLOAD_FAILED_MESSAGE))

View File

@@ -1,7 +1,7 @@
{ {
"name": "bal", "name": "bal",
"fullname": "Bitcoin After Life", "fullname": "Bitcoin After Life",
"version": "0.5.15", "version": "0.5.18",
"description": "Provides free and decentralized Bitcoin inheritance support. Build time-locked 'will' transactions that transfer funds to your heirs if you stop refreshing them (dead-man's switch), optionally relayed by will-executor servers.", "description": "Provides free and decentralized Bitcoin inheritance support. Build time-locked 'will' transactions that transfer funds to your heirs if you stop refreshing them (dead-man's switch), optionally relayed by will-executor servers.",
"author": "Svatantrya", "author": "Svatantrya",
"licence": "MIT", "licence": "MIT",