forked from bitcoinafterlife/bal-electrum-plugin
Merge pull request #5 from Bitcoin-after-life/genspark_ai_developer
v0.3.2: missed-update fixes, server re-check, bold Building Will results (reverts broken v0.3.1)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@
|
|||||||
*.zip
|
*.zip
|
||||||
bal-electrum-plugin.zip
|
bal-electrum-plugin.zip
|
||||||
electrum-src/
|
electrum-src/
|
||||||
|
preview_*.png
|
||||||
|
|||||||
@@ -517,43 +517,95 @@ stato. Le funzioni `server_status_text()` e `server_status_tooltip()` sono in
|
|||||||
|
|
||||||
Confermato dall'utente: **"mi pare che funziona"**.
|
Confermato dall'utente: **"mi pare che funziona"**.
|
||||||
|
|
||||||
## 15. BUGFIX: doppia invalidazione al posticipo dell'eredita (v0.3.1)
|
## 15. TENTATIVO E REVERT: fix doppia invalidazione al posticipo (v0.3.1 -> v0.3.2)
|
||||||
|
|
||||||
### Sintomo segnalato dall'utente
|
### v0.3.1 (RITIRATA)
|
||||||
Posticipando il delivery time, il plugin chiedeva di firmare **due volte** la
|
Per risolvere la doppia firma dell'invalidazione al posticipo, era stato
|
||||||
transazione di invalidazione ("Invalidate your old will"), e solo dopo faceva
|
introdotto `Will.mark_invalidated_by_tx()`, chiamato in
|
||||||
firmare la nuova eredita ("Prepare new will").
|
`loop_broadcast_invalidating` dopo il broadcast dell'invalidazione, per marcare
|
||||||
|
`INVALIDATED` le will che spendevano gli stessi UTXO della tx di invalidazione
|
||||||
|
e persistere lo stato con `save_willitems`.
|
||||||
|
|
||||||
### Causa
|
### Perche e stata ritirata
|
||||||
Il percorso di posticipo era:
|
La modifica ha introdotto una regressione grave segnalata dall'utente:
|
||||||
1. `task_phase1` rileva il posticipo -> `WillPostponedException` -> costruisce
|
**la lista eredita mostrava ancora le vecchie eredita e l'aggiornamento di
|
||||||
la tx di invalidazione -> 1ª firma.
|
eredi/date risultava incoerente**.
|
||||||
2. Dopo il broadcast, `on_success_invalidate` **riavvia** `task_phase1` per
|
|
||||||
ricostruire la nuova eredita.
|
|
||||||
3. **Ma** le will item vecchie erano ancora marcate `COMPLETE`/`PUSHED` con la
|
|
||||||
stessa `tx.locktime` di prima (l'invalidazione on-chain non aggiornava lo
|
|
||||||
stato in memoria), quindi la condizione del posticipo scattava di **nuovo**
|
|
||||||
-> `WillPostponedException` -> **2ª** firma di invalidazione.
|
|
||||||
4. Solo al terzo giro la will veniva finalmente ricostruita.
|
|
||||||
|
|
||||||
### Correzione
|
Causa: `loop_broadcast_invalidating` e il punto di broadcast usato per **TUTTI**
|
||||||
- **`core/will.py`**: nuovo metodo statico `Will.mark_invalidated_by_tx(will,
|
i tipi di invalidazione (posticipo, CheckAlive, will scaduto/anticipato), non
|
||||||
tx)` che marca come `INVALIDATED` ogni will valida che spende almeno uno dei
|
solo per il posticipo. Inoltre il metodo marcava e **persisteva** lo stato
|
||||||
prevout consumati dalla tx di invalidazione appena trasmessa. Settare
|
`INVALIDATED` su tutte le will item che condividevano gli UTXO del wallet
|
||||||
`INVALIDATED` azzera automaticamente il flag `VALID` (logica gia esistente in
|
(tipicamente tutte). Queste will item invalidate restavano poi in memoria e su
|
||||||
`WillItem.set_status`), cosi quelle will escono da `only_valid_list`.
|
disco, inquinando la ricostruzione di eredi/date e lasciando vecchie voci nella
|
||||||
- **`gui/qt/dialogs.py`** (`loop_broadcast_invalidating`): dopo il broadcast
|
lista.
|
||||||
**riuscito** (quando si ottiene il `txid`), si chiama `mark_invalidated_by_tx`
|
|
||||||
e si salva. Al riavvio di `task_phase1` le vecchie will non sono piu `VALID`,
|
### v0.3.2 (questa versione): REVERT completo
|
||||||
quindi `WillPostponedException` non viene piu sollevata e la will viene
|
- Rimosso `Will.mark_invalidated_by_tx()` da `core/will.py`.
|
||||||
ricostruita direttamente. Risultato: **una sola** firma di invalidazione,
|
- Rimossa la chiamata in `gui/qt/dialogs.py` (`loop_broadcast_invalidating`):
|
||||||
poi la new will.
|
il metodo torna **identico** alla v0.3.0.
|
||||||
|
- Rimossi i due test relativi; mantenuto solo l'assert di gerarchia su
|
||||||
|
`WillPostponedException` (corretto e indipendente).
|
||||||
|
- `core/will.py` e `gui/qt/dialogs.py` sono ora **byte-identici** alla v0.3.0
|
||||||
|
funzionante (verificato con `git diff a394cde`).
|
||||||
|
|
||||||
|
Il bug della doppia invalidazione al posticipo resta quindi **aperto** e andra
|
||||||
|
riaffrontato in modo piu mirato (senza toccare il percorso di broadcast comune e
|
||||||
|
senza persistere stati su will che condividono gli UTXO), previa conferma
|
||||||
|
dell'utente. La priorita era ripristinare il comportamento corretto di
|
||||||
|
lista/eredi/date.
|
||||||
|
|
||||||
|
## 16. Aggiornamenti mancati, Check/Close coerenti, e rifinitura UI (v0.3.2)
|
||||||
|
|
||||||
|
### FIX 1 - Rimozione di un erede rilevata su Check / chiusura Electrum
|
||||||
|
`core/will.py` (`check_willexecutors_and_heirs`): prima il plugin
|
||||||
|
rilevava solo l'**aggiunta** di un erede (raise `HeirNotFoundException` quando un
|
||||||
|
erede corrente non era piu nella will). Mancava il caso inverso: la
|
||||||
|
**rimozione** di un erede. Aggiunto il ramo `else` che lancia
|
||||||
|
`HeirNotFoundException` anche quando la will porta ancora un erede che non e piu
|
||||||
|
presente nel set di eredi corrente. Cosi la ricostruzione dell'eredita scatta su
|
||||||
|
**Check** e alla **chiusura di Electrum** (entrambi usano lo stesso percorso
|
||||||
|
`BalBuildWillDialog.build_will_task()`), come deciso dall'utente: nessun
|
||||||
|
aggiornamento automatico dopo la modifica, solo manuale con Check / alla
|
||||||
|
chiusura.
|
||||||
|
|
||||||
|
### FIX 2 - Check interroga i server anche per le will gia inviate
|
||||||
|
`core/will.py` (nuovo `Will.needs_server_check(w)`) e
|
||||||
|
`gui/qt/lists.py` (`PreviewList.check`): prima il Check interrogava i server solo
|
||||||
|
per le will in stato `PUSHED`. Le will gia inviate ma rimaste su "New / Not sent"
|
||||||
|
non venivano ricontrollate ("nothing to do"). Ora `needs_server_check` include
|
||||||
|
ogni will **VALID** con un will-executor e **non ancora CHECKED**, anche se non
|
||||||
|
in stato `PUSHED`. Stesso controllo usato sia dal pulsante Check sia da
|
||||||
|
`on_close`.
|
||||||
|
|
||||||
|
### FIX 3 - Hide invalidated/replaced da finestra Impostazioni aggiornava la lista
|
||||||
|
`core/plugin_base.py` (nuovo `sync_hide_filters()`) e
|
||||||
|
`gui/qt/window.py` (`update_all`): le checkbox "Hide Replaced" / "Hide
|
||||||
|
Invalidated" nella finestra Impostazioni scrivono direttamente la config
|
||||||
|
(`BalConfig.set`) senza toccare i flag in cache `_hide_invalidated` /
|
||||||
|
`_hide_replaced` usati dalla lista per filtrare. Risultato: la lista continuava
|
||||||
|
a filtrare col valore vecchio finche non si riavviava Electrum. Ora
|
||||||
|
`update_all()` chiama `sync_hide_filters()` che ri-legge i flag dalla config,
|
||||||
|
quindi qualunque sorgente del cambiamento (toolbar o finestra Impostazioni)
|
||||||
|
aggiorna subito la lista.
|
||||||
|
|
||||||
|
### Rifinitura UI - Risultati in grassetto nel dialog "Building Will"
|
||||||
|
`gui/qt/dialogs.py` (`BalBuildWillDialog`): i **risultati** mostrati a destra di
|
||||||
|
ogni riga di stato (es. `Ok`, `Ko`, `Nothing to do`, `Skipped`, `Wait`,
|
||||||
|
`Timeout`) sono ora resi in **grassetto**, mantenendo i loro colori
|
||||||
|
(verde/rosso/giallo). Le etichette di stato a sinistra restano in peso normale.
|
||||||
|
Modifica centralizzata negli helper `msg_ok`, `msg_error`, `msg_warning`,
|
||||||
|
`msg_set_status`, piu le righe dei will-executor (push e check) che ora mostrano
|
||||||
|
`Ok/Ko` e `True/False` in grassetto + colore (verde/rosso).
|
||||||
|
|
||||||
### Test
|
### Test
|
||||||
- Aggiunti 2 test in `tests/test_core_will.py`
|
- 186 test ufficiali passano; smoke test, external-zip test e simulazione dei
|
||||||
(`test_will_mark_invalidated_by_tx`, `test_will_mark_invalidated_by_tx_no_match`)
|
flussi di aggiornamento (`tests/sim_update_flows.py`) OK; `ruff` senza nuove
|
||||||
e l'assert di gerarchia per `WillPostponedException`.
|
segnalazioni reali (solo falsi positivi pre-esistenti da star-import).
|
||||||
- 184 test ufficiali passati; smoke test ed external-zip test OK; `ruff` senza
|
- Aggiunti test in `tests/test_core_will.py`:
|
||||||
nuove segnalazioni.
|
`test_check_heirs_unchanged_is_coherent`,
|
||||||
|
`test_check_heir_removed_triggers_rebuild`,
|
||||||
|
`test_check_heir_added_triggers_rebuild`, `test_needs_server_check`.
|
||||||
|
|
||||||
Confermato dall'utente: **"confermo che funziona"**.
|
Confermato dall'utente sui dati reali: dopo Sign -> Broadcast -> Check le
|
||||||
|
transazioni gia inviate sono tornate verdi ("confirmed on server"); la lista
|
||||||
|
torna pulita; il grassetto e l'aggiornamento delle hide-flag funzionano.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
0.3.1
|
0.3.2
|
||||||
@@ -34,4 +34,4 @@ The plugin targets Electrum 4.7.2 (the last stable release exposing
|
|||||||
``json_db.register_dict``) and PyQt6.
|
``json_db.register_dict``) and PyQt6.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.3.1"
|
__version__ = "0.3.2"
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class BalPlugin(BasePlugin):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_version = None
|
_version = None
|
||||||
__version__ = "0.3.1" # AUTOMATICALLY GENERATED DO NOT EDIT
|
__version__ = "0.3.2" # 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 = {
|
||||||
@@ -237,6 +237,29 @@ class BalPlugin(BasePlugin):
|
|||||||
"""Absolute path to a file bundled inside the plugin directory."""
|
"""Absolute path to a file bundled inside the plugin directory."""
|
||||||
return os.path.join(self.plugin_dir, *parts)
|
return os.path.join(self.plugin_dir, *parts)
|
||||||
|
|
||||||
|
def sync_hide_filters(self):
|
||||||
|
"""Re-read the "hide" filter flags from the persisted config.
|
||||||
|
|
||||||
|
The cached ``_hide_invalidated`` / ``_hide_replaced`` flags are used by
|
||||||
|
the GUI list to decide which rows to skip. They can be changed from two
|
||||||
|
different places:
|
||||||
|
|
||||||
|
* the list toolbar buttons, which call :meth:`hide_invalidated` /
|
||||||
|
:meth:`hide_replaced` (a toggle that updates both the cache and the
|
||||||
|
config), and
|
||||||
|
* the Settings dialog checkboxes, which write the config directly
|
||||||
|
(``BalConfig.set``) without touching the cached flags.
|
||||||
|
|
||||||
|
In the second case the cache and the config would drift apart and the
|
||||||
|
transaction list would keep filtering with the *old* value, so the
|
||||||
|
toggled rows never appear/disappear until Electrum is restarted.
|
||||||
|
Re-syncing the cache from the config here (called by ``update_all``)
|
||||||
|
keeps every code path coherent regardless of where the change came
|
||||||
|
from.
|
||||||
|
"""
|
||||||
|
self._hide_invalidated = self.HIDE_INVALIDATED.get()
|
||||||
|
self._hide_replaced = self.HIDE_REPLACED.get()
|
||||||
|
|
||||||
def hide_invalidated(self):
|
def hide_invalidated(self):
|
||||||
"""Toggle (and persist) the "hide invalidated transactions" filter."""
|
"""Toggle (and persist) the "hide invalidated transactions" filter."""
|
||||||
self._hide_invalidated = not self._hide_invalidated
|
self._hide_invalidated = not self._hide_invalidated
|
||||||
|
|||||||
@@ -83,6 +83,25 @@ class Will:
|
|||||||
if v.get_status("VALID"):
|
if v.get_status("VALID"):
|
||||||
yield k
|
yield k
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def needs_server_check(w):
|
||||||
|
"""Return True if ``w`` should be queried on its will-executor server
|
||||||
|
when the user presses Check (or on Electrum close).
|
||||||
|
|
||||||
|
A will needs a server check when it is VALID, has a will-executor
|
||||||
|
assigned, and is not yet CHECKED. This intentionally includes wills
|
||||||
|
that are not (yet) marked PUSHED: a will that was actually sent in the
|
||||||
|
past but whose saved status still reads "New" would otherwise be
|
||||||
|
skipped, leaving the Server column stuck on "Not sent". The server
|
||||||
|
response (see WillItem.set_check_willexecutor) then corrects the status
|
||||||
|
to PUSHED/CHECKED if the transaction is present, or CHECK_FAIL if not.
|
||||||
|
"""
|
||||||
|
return bool(
|
||||||
|
w.get_status("VALID")
|
||||||
|
and w.we
|
||||||
|
and not w.get_status("CHECKED")
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def search_equal_tx(will, tx, wid):
|
def search_equal_tx(will, tx, wid):
|
||||||
for w in will:
|
for w in will:
|
||||||
@@ -420,31 +439,6 @@ class Will:
|
|||||||
_logger.debug("len utxo_to_spend <=0")
|
_logger.debug("len utxo_to_spend <=0")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def mark_invalidated_by_tx(will, tx):
|
|
||||||
"""Mark as INVALIDATED every valid will item that spends at least one
|
|
||||||
of the prevouts consumed by ``tx`` (the on-chain invalidation tx that
|
|
||||||
was just broadcast).
|
|
||||||
|
|
||||||
Once the invalidation tx is broadcast, the previously signed/sent will
|
|
||||||
transactions that relied on those same UTXOs can no longer be mined, so
|
|
||||||
their will items must stop being VALID. Setting INVALIDATED clears the
|
|
||||||
VALID flag (see WillItem.set_status), which removes them from
|
|
||||||
only_valid_list and therefore prevents the postpone/expire check from
|
|
||||||
firing a *second* invalidation on the next pass.
|
|
||||||
|
|
||||||
Returns the list of will ids that were marked.
|
|
||||||
"""
|
|
||||||
spent_prevouts = {i.prevout.to_str() for i in tx.inputs()}
|
|
||||||
invalidated = []
|
|
||||||
for wid in Will.only_valid_list(will):
|
|
||||||
w = will[wid]
|
|
||||||
wi_prevouts = {i.prevout.to_str() for i in w.tx.inputs()}
|
|
||||||
if spent_prevouts & wi_prevouts:
|
|
||||||
Will.set_invalidate(wid, will)
|
|
||||||
invalidated.append(wid)
|
|
||||||
return invalidated
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_new(will):
|
def is_new(will):
|
||||||
for wid, w in will.items():
|
for wid, w in will.items():
|
||||||
@@ -732,9 +726,18 @@ class Will:
|
|||||||
# signed/sent falls through here -> a plain rebuild via
|
# signed/sent falls through here -> a plain rebuild via
|
||||||
# HeirNotFoundException (no on-chain fee needed).
|
# HeirNotFoundException (no on-chain fee needed).
|
||||||
else:
|
else:
|
||||||
|
# The will still carries this heir, but the heir is no
|
||||||
|
# longer present in the current heirs set: the user
|
||||||
|
# removed it. This must trigger a rebuild exactly like
|
||||||
|
# "heir added" does, otherwise the removed heir would
|
||||||
|
# silently stay in the inheritance transaction. Raising
|
||||||
|
# HeirNotFoundException reuses the same rebuild path used
|
||||||
|
# by the Check button and by on_close (Electrum quit).
|
||||||
_logger.debug(
|
_logger.debug(
|
||||||
f"heir not present transaction is not valid:{wheir} {wid}, {w}"
|
f"heir removed, transaction is not valid:"
|
||||||
|
f"{wheir} {wid}, {w}"
|
||||||
)
|
)
|
||||||
|
raise HeirNotFoundException(wheir)
|
||||||
|
|
||||||
if willexecutor := w.we:
|
if willexecutor := w.we:
|
||||||
count = willexecutors_found.get(willexecutor["url"], 0)
|
count = willexecutors_found.get(willexecutor["url"], 0)
|
||||||
|
|||||||
@@ -708,19 +708,6 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
self.msg_set_invalidating(self.msg_ok())
|
self.msg_set_invalidating(self.msg_ok())
|
||||||
if not txid:
|
if not txid:
|
||||||
_logger.debug(f"should not be none txid: {txid}")
|
_logger.debug(f"should not be none txid: {txid}")
|
||||||
else:
|
|
||||||
# The invalidation tx is now broadcast, so the old signed/sent
|
|
||||||
# will transactions spending those same UTXOs can no longer be
|
|
||||||
# mined. Mark them INVALIDATED (which clears their VALID flag)
|
|
||||||
# so the postpone/expire check does NOT fire a second
|
|
||||||
# invalidation when phase 1 is restarted to rebuild the new
|
|
||||||
# (postponed) will.
|
|
||||||
invalidated = Will.mark_invalidated_by_tx(
|
|
||||||
self.bal_window.willitems, tx
|
|
||||||
)
|
|
||||||
if invalidated:
|
|
||||||
_logger.debug(f"invalidated will items: {invalidated}")
|
|
||||||
self.bal_window.save_willitems()
|
|
||||||
|
|
||||||
except TxBroadcastError as e:
|
except TxBroadcastError as e:
|
||||||
_logger.error(f"fail to broadcast transaction:{e}")
|
_logger.error(f"fail to broadcast transaction:{e}")
|
||||||
@@ -784,7 +771,10 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
self.bal_window.willitems[wid].set_status("PUSH_FAIL", True)
|
self.bal_window.willitems[wid].set_status("PUSH_FAIL", True)
|
||||||
retry_flag["value"] = True
|
retry_flag["value"] = True
|
||||||
done["count"] += 1
|
done["count"] += 1
|
||||||
self.msg_edit_row("{} : {}".format(url, "Ok" if ok else "Ko"))
|
# Show the per-server result (Ok/Ko) in bold + color so the
|
||||||
|
# outcome stands out, keeping the server URL in normal weight.
|
||||||
|
result = self.msg_ok("Ok") if ok else self.msg_error("Ko")
|
||||||
|
self.msg_edit_row("{} : {}".format(url, result))
|
||||||
self.msg_set_pushing(_status_line())
|
self.msg_set_pushing(_status_line())
|
||||||
|
|
||||||
def on_timeout(url, willexecutor):
|
def on_timeout(url, willexecutor):
|
||||||
@@ -836,7 +826,7 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
if self._stopping:
|
if self._stopping:
|
||||||
return
|
return
|
||||||
row = self.msg_edit_row(
|
row = self.msg_edit_row(
|
||||||
"checking {} - {} : {}".format(
|
"checking {} - {} : <b>{}</b>".format(
|
||||||
self.bal_window.willitems[wid].we["url"], wid, "Waiting"
|
self.bal_window.willitems[wid].we["url"], wid, "Waiting"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -844,11 +834,16 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
w.set_check_willexecutor(
|
w.set_check_willexecutor(
|
||||||
Willexecutors.check_transaction(wid, w.we["url"])
|
Willexecutors.check_transaction(wid, w.we["url"])
|
||||||
)
|
)
|
||||||
|
# Show the CHECKED result in bold + color (green True /
|
||||||
|
# red False) so the outcome stands out, keeping the server
|
||||||
|
# URL and tx id in normal weight.
|
||||||
|
checked = self.bal_window.willitems[wid].get_status("CHECKED")
|
||||||
|
result = self.msg_ok(checked) if checked else self.msg_error(checked)
|
||||||
row = self.msg_edit_row(
|
row = self.msg_edit_row(
|
||||||
"checked {} - {} : {}".format(
|
"checked {} - {} : {}".format(
|
||||||
self.bal_window.willitems[wid].we["url"],
|
self.bal_window.willitems[wid].we["url"],
|
||||||
wid,
|
wid,
|
||||||
self.bal_window.willitems[wid].get_status("CHECKED"),
|
result,
|
||||||
),
|
),
|
||||||
row,
|
row,
|
||||||
)
|
)
|
||||||
@@ -1034,20 +1029,31 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
self.wait_row = self.msg_edit_row(f"Please wait {status}secs", self.wait_row)
|
self.wait_row = self.msg_edit_row(f"Please wait {status}secs", self.wait_row)
|
||||||
|
|
||||||
def msg_error(self, e):
|
def msg_error(self, e):
|
||||||
return "<font color='{}'>{}</font>".format(self.COLOR_ERROR, e)
|
# Results are shown in bold so the outcome stands out from the
|
||||||
|
# left-side state label (which stays in normal weight).
|
||||||
|
return "<font color='{}'><b>{}</b></font>".format(self.COLOR_ERROR, e)
|
||||||
|
|
||||||
def msg_ok(self, e="Ok"):
|
def msg_ok(self, e="Ok"):
|
||||||
return "<font color='{}'>{}</font>".format(self.COLOR_OK, e)
|
# Results are shown in bold (see msg_error).
|
||||||
|
return "<font color='{}'><b>{}</b></font>".format(self.COLOR_OK, e)
|
||||||
|
|
||||||
def msg_warning(self, e):
|
def msg_warning(self, e):
|
||||||
return "<font color='{}'>{}</font".format(self.COLOR_WARNING, e)
|
# Results are shown in bold (see msg_error).
|
||||||
|
return "<font color='{}'><b>{}</b></font>".format(self.COLOR_WARNING, e)
|
||||||
|
|
||||||
def msg_set_status(self, msg, row=None, status=None, color=None):
|
def msg_set_status(self, msg, row=None, status=None, color=None):
|
||||||
|
# The left "state" label keeps its normal weight; only the right-side
|
||||||
|
# result (``status``) is rendered in bold so it is easy to read at a
|
||||||
|
# glance. ``status`` may already contain rich-text emitted by
|
||||||
|
# msg_ok/msg_error/msg_warning (which add their own <b>...</b>); wrapping
|
||||||
|
# it again in <b> is harmless for those cases.
|
||||||
status = "Wait" if status is None else status
|
status = "Wait" if status is None else status
|
||||||
if color is None:
|
if color is None:
|
||||||
line = f"{_(msg)}:\t{status}"
|
line = "{}:\t<b>{}</b>".format(_(msg), status)
|
||||||
else:
|
else:
|
||||||
line = "<font color={}>{}:\t{}</font>".format(color, _(msg), status)
|
line = "{}:\t<font color={}><b>{}</b></font>".format(
|
||||||
|
_(msg), color, status
|
||||||
|
)
|
||||||
return self.msg_edit_row(line, row)
|
return self.msg_edit_row(line, row)
|
||||||
|
|
||||||
def ask_password(self, msg=None):
|
def ask_password(self, msg=None):
|
||||||
|
|||||||
@@ -532,11 +532,15 @@ class PreviewList(MyTreeView, MessageBoxMixin):
|
|||||||
|
|
||||||
will = {}
|
will = {}
|
||||||
for wid, w in self.bal_window.willitems.items():
|
for wid, w in self.bal_window.willitems.items():
|
||||||
if (
|
# Query the will-executor server for every valid will that HAS a
|
||||||
w.get_status("VALID")
|
# will-executor assigned and is not yet CHECKED. Previously only
|
||||||
and w.get_status("PUSHED")
|
# transactions already marked PUSHED were checked, so a will that
|
||||||
and not w.get_status("CHECKED")
|
# had actually been sent in the past but whose saved status still
|
||||||
):
|
# read "New" (not PUSHED) was skipped and the Check button reported
|
||||||
|
# "nothing to do". Will.needs_server_check now also includes such
|
||||||
|
# non-PUSHED wills, so the server can confirm the transaction is
|
||||||
|
# present and correct the status (see set_check_willexecutor).
|
||||||
|
if Will.needs_server_check(w):
|
||||||
will[wid] = w
|
will[wid] = w
|
||||||
if will:
|
if will:
|
||||||
self.bal_window.check_transactions(will)
|
self.bal_window.check_transactions(will)
|
||||||
|
|||||||
@@ -1227,6 +1227,13 @@ class BalWindow:
|
|||||||
|
|
||||||
def update_all(self):
|
def update_all(self):
|
||||||
try:
|
try:
|
||||||
|
# Re-sync the cached "hide invalidated/replaced" flags from the
|
||||||
|
# persisted config before refreshing the list. The Settings dialog
|
||||||
|
# checkboxes write the config directly (without touching the cached
|
||||||
|
# flags), so without this the list would keep filtering with the old
|
||||||
|
# value and the invalidated/replaced rows would not appear/disappear
|
||||||
|
# until Electrum was restarted.
|
||||||
|
self.bal_plugin.sync_hide_filters()
|
||||||
Will.add_willtree(self.willitems)
|
Will.add_willtree(self.willitems)
|
||||||
all_utxos = self.wallet.get_utxos()
|
all_utxos = self.wallet.get_utxos()
|
||||||
utxos_list = Will.utxos_strs(all_utxos)
|
utxos_list = Will.utxos_strs(all_utxos)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "bal",
|
"name": "bal",
|
||||||
"fullname": "Bitcoin After Life",
|
"fullname": "Bitcoin After Life",
|
||||||
"version": "0.3.1",
|
"version": "0.3.2",
|
||||||
"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",
|
||||||
|
|||||||
110
tests/preview_build_will_dialog.py
Normal file
110
tests/preview_build_will_dialog.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Render a visual PREVIEW (before/after) of the "Building Will" dialog text.
|
||||||
|
|
||||||
|
This is a throwaway, GUI-only helper used to show the user how the proposed
|
||||||
|
"bold results" formatting looks compared to the current rendering, BEFORE any
|
||||||
|
production code is changed. It does NOT import the plugin; it just reproduces
|
||||||
|
the exact rich-text the dialog builds via ``msg_set_status`` / ``msg_ok`` /
|
||||||
|
``msg_error`` so the preview is faithful.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
QT_QPA_PLATFORM=offscreen python3 tests/preview_build_will_dialog.py
|
||||||
|
It writes two PNGs in the repo root: preview_before.png and preview_after.png.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import QApplication, QLabel, QWidget, QVBoxLayout
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
|
||||||
|
# Same colors as BalBuildWillDialog
|
||||||
|
COLOR_WARNING = "#cfa808"
|
||||||
|
COLOR_ERROR = "#ff0000"
|
||||||
|
COLOR_OK = "#05ad05"
|
||||||
|
|
||||||
|
|
||||||
|
# ---- current rendering (BEFORE) -------------------------------------------
|
||||||
|
def ok_before(e="Ok"):
|
||||||
|
return "<font color='{}'>{}</font>".format(COLOR_OK, e)
|
||||||
|
|
||||||
|
|
||||||
|
def error_before(e):
|
||||||
|
return "<font color='{}'>{}</font>".format(COLOR_ERROR, e)
|
||||||
|
|
||||||
|
|
||||||
|
def row_before(msg, status, color=None):
|
||||||
|
if color is None:
|
||||||
|
return f"{msg}:\t{status}"
|
||||||
|
return "<font color={}>{}:\t{}</font>".format(color, msg, status)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- proposed rendering (AFTER): results in bold --------------------------
|
||||||
|
def ok_after(e="Ok"):
|
||||||
|
return "<font color='{}'><b>{}</b></font>".format(COLOR_OK, e)
|
||||||
|
|
||||||
|
|
||||||
|
def error_after(e):
|
||||||
|
return "<font color='{}'><b>{}</b></font>".format(COLOR_ERROR, e)
|
||||||
|
|
||||||
|
|
||||||
|
def row_after(msg, status, color=None):
|
||||||
|
# Left state label stays normal; only the result (status) becomes bold.
|
||||||
|
if color is None:
|
||||||
|
return f"{msg}:\t<b>{status}</b>"
|
||||||
|
# When a color is given for the whole line, keep the label normal and bold
|
||||||
|
# only the status portion.
|
||||||
|
return "{}:\t<font color={}><b>{}</b></font>".format(msg, color, status)
|
||||||
|
|
||||||
|
|
||||||
|
def build_rows(mode):
|
||||||
|
if mode == "before":
|
||||||
|
ok, err, row = ok_before, error_before, row_before
|
||||||
|
else:
|
||||||
|
ok, err, row = ok_after, error_after, row_after
|
||||||
|
rows = [
|
||||||
|
row("checking variables", "Wait"),
|
||||||
|
row("Checking your will", ok()),
|
||||||
|
row("Signing your will", "Nothing to do"),
|
||||||
|
row("Broadcasting your will to executors", "Nothing to do"),
|
||||||
|
ok(),
|
||||||
|
row("Invalidating old will", err("Ko")),
|
||||||
|
"https://executor.example.org : " + ok(),
|
||||||
|
"https://other.example.org : " + err("Ko"),
|
||||||
|
"Please wait 2secs",
|
||||||
|
row("Will-Executor excluded", "Skipped", COLOR_ERROR),
|
||||||
|
]
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def render(mode, path):
|
||||||
|
rows = build_rows(mode)
|
||||||
|
full_text = "<br><br>".join(rows).replace("\n", "<br>")
|
||||||
|
w = QWidget()
|
||||||
|
w.setStyleSheet("background:#2b2b2b;")
|
||||||
|
lay = QVBoxLayout(w)
|
||||||
|
title = QLabel(f"Building Will — {mode.upper()}")
|
||||||
|
title.setStyleSheet("color:#ffffff; font-size:15px; font-weight:bold;")
|
||||||
|
lbl = QLabel(full_text)
|
||||||
|
lbl.setTextFormat(Qt.TextFormat.RichText)
|
||||||
|
lbl.setStyleSheet("color:#dddddd; font-size:13px;")
|
||||||
|
lbl_font = lbl.font()
|
||||||
|
lbl_font.setPointSize(11)
|
||||||
|
lbl.setFont(lbl_font)
|
||||||
|
lay.addWidget(title)
|
||||||
|
lay.addWidget(lbl)
|
||||||
|
w.resize(560, 420)
|
||||||
|
w.show()
|
||||||
|
app.processEvents()
|
||||||
|
pix = w.grab()
|
||||||
|
pix.save(path)
|
||||||
|
print(f"[{mode}] saved -> {path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
render("before", os.path.join(here, "preview_before.png"))
|
||||||
|
render("after", os.path.join(here, "preview_after.png"))
|
||||||
105
tests/preview_we_rows.py
Normal file
105
tests/preview_we_rows.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Visual PREVIEW focused on the WILL-EXECUTOR rows of the Building Will dialog.
|
||||||
|
|
||||||
|
Reproduces faithfully the three real variants built in dialogs.py:
|
||||||
|
|
||||||
|
1. Broadcasting (push) result -> line 774: "{url} : {Ok|Ko}" (plain, no color today)
|
||||||
|
2. Timeout -> line 783: "{url} : <font red>Timeout - no answer</font>"
|
||||||
|
3. Checking already-present -> line 825/834:
|
||||||
|
"checking {url} - {wid} : Waiting"
|
||||||
|
"checked {url} - {wid} : True/False" (plain, no color today)
|
||||||
|
|
||||||
|
Shows BEFORE (current) vs AFTER (proposed: result in bold, keeping label as-is).
|
||||||
|
|
||||||
|
Run:
|
||||||
|
QT_QPA_PLATFORM=offscreen python3 tests/preview_we_rows.py
|
||||||
|
Writes preview_we_before.png / preview_we_after.png in the repo root.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import QApplication, QLabel, QWidget, QVBoxLayout
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
|
||||||
|
COLOR_ERROR = "#ff0000"
|
||||||
|
COLOR_OK = "#05ad05"
|
||||||
|
|
||||||
|
URL1 = "https://executor.example.org"
|
||||||
|
URL2 = "https://other-executor.net"
|
||||||
|
WID = "a1b2c3"
|
||||||
|
|
||||||
|
|
||||||
|
def err(e):
|
||||||
|
return "<font color='{}'>{}</font>".format(COLOR_ERROR, e)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- BEFORE: exactly as the code builds today -----------------
|
||||||
|
def rows_before():
|
||||||
|
return [
|
||||||
|
# 1. push results (plain text, no color/bold today)
|
||||||
|
"{} : {}".format(URL1, "Ok"),
|
||||||
|
"{} : {}".format(URL2, "Ko"),
|
||||||
|
# 2. timeout (already red, not bold)
|
||||||
|
"{} : {}".format(URL1, err("Timeout - no answer")),
|
||||||
|
# 3. already-present check
|
||||||
|
"checking {} - {} : {}".format(URL1, WID, "Waiting"),
|
||||||
|
"checked {} - {} : {}".format(URL1, WID, "True"),
|
||||||
|
"checked {} - {} : {}".format(URL2, WID, "False"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- AFTER: result portion in bold, label unchanged -----------
|
||||||
|
def err_after(e):
|
||||||
|
return "<font color='{}'><b>{}</b></font>".format(COLOR_ERROR, e)
|
||||||
|
|
||||||
|
|
||||||
|
def rows_after():
|
||||||
|
return [
|
||||||
|
# 1. push results: color + bold the Ok / Ko outcome
|
||||||
|
"{} : <font color='{}'><b>{}</b></font>".format(URL1, COLOR_OK, "Ok"),
|
||||||
|
"{} : <font color='{}'><b>{}</b></font>".format(URL2, COLOR_ERROR, "Ko"),
|
||||||
|
# 2. timeout: bold the red message
|
||||||
|
"{} : {}".format(URL1, err_after("Timeout - no answer")),
|
||||||
|
# 3. already-present check: bold the result
|
||||||
|
"checking {} - {} : <b>{}</b>".format(URL1, WID, "Waiting"),
|
||||||
|
"checked {} - {} : <font color='{}'><b>{}</b></font>".format(
|
||||||
|
URL1, WID, COLOR_OK, "True"
|
||||||
|
),
|
||||||
|
"checked {} - {} : <font color='{}'><b>{}</b></font>".format(
|
||||||
|
URL2, WID, COLOR_ERROR, "False"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def render(rows, title, path):
|
||||||
|
full_text = "<br><br>".join(rows).replace("\n", "<br>")
|
||||||
|
w = QWidget()
|
||||||
|
w.setStyleSheet("background:#2b2b2b;")
|
||||||
|
lay = QVBoxLayout(w)
|
||||||
|
t = QLabel(title)
|
||||||
|
t.setStyleSheet("color:#ffffff; font-size:15px; font-weight:bold;")
|
||||||
|
lbl = QLabel(full_text)
|
||||||
|
lbl.setTextFormat(Qt.TextFormat.RichText)
|
||||||
|
lbl.setStyleSheet("color:#dddddd;")
|
||||||
|
f = lbl.font()
|
||||||
|
f.setPointSize(11)
|
||||||
|
lbl.setFont(f)
|
||||||
|
lay.addWidget(t)
|
||||||
|
lay.addWidget(lbl)
|
||||||
|
w.resize(560, 320)
|
||||||
|
w.show()
|
||||||
|
app.processEvents()
|
||||||
|
w.grab().save(path)
|
||||||
|
print(f"saved -> {path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
render(rows_before(), "Will-Executor rows — BEFORE",
|
||||||
|
os.path.join(here, "preview_we_before.png"))
|
||||||
|
render(rows_after(), "Will-Executor rows — AFTER",
|
||||||
|
os.path.join(here, "preview_we_after.png"))
|
||||||
171
tests/sim_update_flows.py
Normal file
171
tests/sim_update_flows.py
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
"""
|
||||||
|
Real-world simulation of the inheritance update flows.
|
||||||
|
|
||||||
|
This script does NOT touch the GUI. It drives the core decision function
|
||||||
|
``Will.check_willexecutors_and_heirs`` (the one that decides whether a will is
|
||||||
|
still coherent or must be rebuilt) through the scenarios the user reported:
|
||||||
|
|
||||||
|
1. delivery date moved forward (postpone) -> must NOT stay "coherent"
|
||||||
|
2. an heir is added -> must trigger rebuild
|
||||||
|
3. an heir is removed -> must trigger rebuild
|
||||||
|
4. a single heir percentage / amount is changed -> must trigger rebuild
|
||||||
|
5. nothing changed -> stays coherent
|
||||||
|
|
||||||
|
For each scenario we report which exception (if any) is raised, because that is
|
||||||
|
exactly what the GUI relies on to decide whether to rebuild the inheritance
|
||||||
|
transactions. If the function returns True (coherent) when something DID
|
||||||
|
change, the GUI will (correctly) show no update -- which is the symptom the
|
||||||
|
user described.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src python3 tests/sim_update_flows.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import copy
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||||
|
|
||||||
|
from bal.core.will import (
|
||||||
|
WillItem, Will,
|
||||||
|
NotCompleteWillException, HeirNotFoundException, NoHeirsException,
|
||||||
|
TxFeesChangedException, WillExpiredException,
|
||||||
|
)
|
||||||
|
from bal.core.util import Util
|
||||||
|
|
||||||
|
# A valid serialized tx (1 input + 1 output, version 2). Its nLockTime is 0.
|
||||||
|
_VALID_TX_HEX = (
|
||||||
|
"01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65b"
|
||||||
|
"f38633b424eb4031000000006c493046022100a82bbc57a0136751e543"
|
||||||
|
"3f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7d"
|
||||||
|
"e89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d98501"
|
||||||
|
"2102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae3"
|
||||||
|
"5cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a"
|
||||||
|
"42146f11ef8414ae929feaafc388ac00000000"
|
||||||
|
)
|
||||||
|
|
||||||
|
# A locktime far in the past (so the frozen tx.locktime is a fixed integer we
|
||||||
|
# control via monkey-patching below). We will override w.tx.locktime per test.
|
||||||
|
TX_FEES = 100
|
||||||
|
|
||||||
|
|
||||||
|
def _make_will_item(heirs, tx_locktime, status_complete=False):
|
||||||
|
"""Build a WillItem whose stored heirs == ``heirs`` and whose tx.locktime
|
||||||
|
is forced to ``tx_locktime`` (the value frozen in the signed Bitcoin tx)."""
|
||||||
|
d = {
|
||||||
|
"tx": _VALID_TX_HEX,
|
||||||
|
"heirs": copy.deepcopy(heirs),
|
||||||
|
"willexecutor": None,
|
||||||
|
"status": "",
|
||||||
|
"description": "",
|
||||||
|
"time": 0,
|
||||||
|
"change": "",
|
||||||
|
"baltx_fees": TX_FEES,
|
||||||
|
}
|
||||||
|
item = WillItem(d, _id="willid_1")
|
||||||
|
item.STATUS = copy.deepcopy(WillItem.STATUS_DEFAULT)
|
||||||
|
# Force the locktime frozen "inside" the signed tx.
|
||||||
|
item.tx.locktime = tx_locktime
|
||||||
|
if status_complete:
|
||||||
|
item.set_status("COMPLETE", True)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def _run(label, will_heirs, current_heirs, tx_locktime,
|
||||||
|
status_complete=False, check_date=0):
|
||||||
|
"""Run check_willexecutors_and_heirs and report the outcome."""
|
||||||
|
item = _make_will_item(will_heirs, tx_locktime, status_complete)
|
||||||
|
will = {"willid_1": item}
|
||||||
|
outcome = None
|
||||||
|
try:
|
||||||
|
result = Will.check_willexecutors_and_heirs(
|
||||||
|
will,
|
||||||
|
current_heirs, # the (possibly edited) heirs dict
|
||||||
|
{}, # willexecutors
|
||||||
|
False, # self_willexecutor
|
||||||
|
check_date, # check_date (timestamp)
|
||||||
|
TX_FEES, # tx_fees
|
||||||
|
)
|
||||||
|
outcome = f"coherent (returned {result})"
|
||||||
|
except HeirNotFoundException as e:
|
||||||
|
outcome = f"HeirNotFoundException: {e}"
|
||||||
|
except NoHeirsException as e:
|
||||||
|
outcome = f"NoHeirsException: {e}"
|
||||||
|
except TxFeesChangedException as e:
|
||||||
|
outcome = f"TxFeesChangedException: {e}"
|
||||||
|
except WillExpiredException as e:
|
||||||
|
outcome = f"WillExpiredException: {e}"
|
||||||
|
except NotCompleteWillException as e:
|
||||||
|
outcome = f"{type(e).__name__}: {e}"
|
||||||
|
except Exception as e:
|
||||||
|
outcome = f"!! UNEXPECTED {type(e).__name__}: {e}"
|
||||||
|
print(f"[{label}]")
|
||||||
|
print(f" -> {outcome}")
|
||||||
|
return outcome
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# locktime string "0d" -> Util.parse_locktime_string returns a timestamp
|
||||||
|
# ~ now. We use explicit integer timestamps to keep things deterministic.
|
||||||
|
base_lt = 1900000000 # frozen tx.locktime (year ~2030)
|
||||||
|
later_lt = "2000000000" # a later locktime string (postpone)
|
||||||
|
same_lt = str(base_lt)
|
||||||
|
|
||||||
|
# Scenario 0: nothing changed -> should be coherent.
|
||||||
|
heirs = {"alice": ["addr_alice", 5000, same_lt]}
|
||||||
|
_run("0. nothing changed",
|
||||||
|
will_heirs=heirs, current_heirs=copy.deepcopy(heirs),
|
||||||
|
tx_locktime=base_lt, check_date=0)
|
||||||
|
|
||||||
|
# Scenario 1: delivery date moved forward (postpone), will NOT yet signed.
|
||||||
|
heirs_will = {"alice": ["addr_alice", 5000, same_lt]}
|
||||||
|
heirs_now = {"alice": ["addr_alice", 5000, later_lt]}
|
||||||
|
_run("1. date postponed (unsigned will)",
|
||||||
|
will_heirs=heirs_will, current_heirs=heirs_now,
|
||||||
|
tx_locktime=base_lt, check_date=0)
|
||||||
|
|
||||||
|
# Scenario 1b: postpone on a SIGNED will (status COMPLETE).
|
||||||
|
_run("1b. date postponed (SIGNED will)",
|
||||||
|
will_heirs=heirs_will, current_heirs=heirs_now,
|
||||||
|
tx_locktime=base_lt, status_complete=True, check_date=0)
|
||||||
|
|
||||||
|
# Scenario 2: an heir is ADDED.
|
||||||
|
heirs_will = {"alice": ["addr_alice", 5000, same_lt]}
|
||||||
|
heirs_now = {
|
||||||
|
"alice": ["addr_alice", 5000, same_lt],
|
||||||
|
"bob": ["addr_bob", 3000, same_lt],
|
||||||
|
}
|
||||||
|
_run("2. heir added (bob)",
|
||||||
|
will_heirs=heirs_will, current_heirs=heirs_now,
|
||||||
|
tx_locktime=base_lt, check_date=0)
|
||||||
|
|
||||||
|
# Scenario 3: an heir is REMOVED.
|
||||||
|
heirs_will = {
|
||||||
|
"alice": ["addr_alice", 5000, same_lt],
|
||||||
|
"bob": ["addr_bob", 3000, same_lt],
|
||||||
|
}
|
||||||
|
heirs_now = {"alice": ["addr_alice", 5000, same_lt]}
|
||||||
|
_run("3. heir removed (bob)",
|
||||||
|
will_heirs=heirs_will, current_heirs=heirs_now,
|
||||||
|
tx_locktime=base_lt, check_date=0)
|
||||||
|
|
||||||
|
# Scenario 4: a single heir AMOUNT/percentage changed.
|
||||||
|
heirs_will = {"alice": ["addr_alice", 5000, same_lt]}
|
||||||
|
heirs_now = {"alice": ["addr_alice", 9999, same_lt]}
|
||||||
|
_run("4. heir amount changed (5000 -> 9999)",
|
||||||
|
will_heirs=heirs_will, current_heirs=heirs_now,
|
||||||
|
tx_locktime=base_lt, check_date=0)
|
||||||
|
|
||||||
|
# Scenario 5: heir ADDRESS changed.
|
||||||
|
heirs_will = {"alice": ["addr_alice", 5000, same_lt]}
|
||||||
|
heirs_now = {"alice": ["addr_NEW", 5000, same_lt]}
|
||||||
|
_run("5. heir address changed",
|
||||||
|
will_heirs=heirs_will, current_heirs=heirs_now,
|
||||||
|
tx_locktime=base_lt, check_date=0)
|
||||||
|
|
||||||
|
print("\n[done] simulation finished")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -164,6 +164,103 @@ def test_will_only_valid_list():
|
|||||||
assert "b" not in result
|
assert "b" not in result
|
||||||
|
|
||||||
|
|
||||||
|
def _make_will_with_heirs(heirs, tx_locktime):
|
||||||
|
"""Build a single-item will whose stored heirs == ``heirs`` and whose
|
||||||
|
frozen tx.locktime == ``tx_locktime`` (what the will-executors hold)."""
|
||||||
|
item = WillItem(_make_minimal_willitem_dict(heirs=copy.deepcopy(heirs)))
|
||||||
|
item.STATUS = copy.deepcopy(WillItem.STATUS_DEFAULT)
|
||||||
|
item.tx.locktime = tx_locktime
|
||||||
|
return {"willid_1": item}
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_heirs_unchanged_is_coherent():
|
||||||
|
"""No heir change -> the will stays coherent (no rebuild)."""
|
||||||
|
lt = 1900000000
|
||||||
|
heirs = {"alice": ["addr_alice", 5000, str(lt)]}
|
||||||
|
will = _make_will_with_heirs(heirs, lt)
|
||||||
|
result = Will.check_willexecutors_and_heirs(
|
||||||
|
will, copy.deepcopy(heirs), {}, False, 0, 100
|
||||||
|
)
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_heir_removed_triggers_rebuild():
|
||||||
|
"""Removing an heir MUST be detected (HeirNotFoundException), so the Check
|
||||||
|
button and on_close rebuild the inheritance. Regression test for the bug
|
||||||
|
where a removed heir silently stayed in the transaction."""
|
||||||
|
from bal.core.will import HeirNotFoundException
|
||||||
|
lt = 1900000000
|
||||||
|
will_heirs = {
|
||||||
|
"alice": ["addr_alice", 5000, str(lt)],
|
||||||
|
"bob": ["addr_bob", 3000, str(lt)],
|
||||||
|
}
|
||||||
|
current_heirs = {"alice": ["addr_alice", 5000, str(lt)]} # bob removed
|
||||||
|
will = _make_will_with_heirs(will_heirs, lt)
|
||||||
|
raised = False
|
||||||
|
try:
|
||||||
|
Will.check_willexecutors_and_heirs(
|
||||||
|
will, current_heirs, {}, False, 0, 100
|
||||||
|
)
|
||||||
|
except HeirNotFoundException:
|
||||||
|
raised = True
|
||||||
|
assert raised, "removing an heir must raise HeirNotFoundException"
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_heir_added_triggers_rebuild():
|
||||||
|
"""Adding an heir must be detected (HeirNotFoundException)."""
|
||||||
|
from bal.core.will import HeirNotFoundException
|
||||||
|
lt = 1900000000
|
||||||
|
will_heirs = {"alice": ["addr_alice", 5000, str(lt)]}
|
||||||
|
current_heirs = {
|
||||||
|
"alice": ["addr_alice", 5000, str(lt)],
|
||||||
|
"bob": ["addr_bob", 3000, str(lt)], # added
|
||||||
|
}
|
||||||
|
will = _make_will_with_heirs(will_heirs, lt)
|
||||||
|
raised = False
|
||||||
|
try:
|
||||||
|
Will.check_willexecutors_and_heirs(
|
||||||
|
will, current_heirs, {}, False, 0, 100
|
||||||
|
)
|
||||||
|
except HeirNotFoundException:
|
||||||
|
raised = True
|
||||||
|
assert raised, "adding an heir must raise HeirNotFoundException"
|
||||||
|
|
||||||
|
|
||||||
|
def test_needs_server_check():
|
||||||
|
"""Check button selection logic: a VALID will with a will-executor that is
|
||||||
|
not yet CHECKED must be queried on the server, even if it is not PUSHED
|
||||||
|
(regression for the 'New / Not sent' wills that Check ignored)."""
|
||||||
|
we = {"url": "https://we.example.com"}
|
||||||
|
|
||||||
|
# New (not PUSHED) but has a will-executor -> must be checked.
|
||||||
|
item_new = _make_willitem_blank()
|
||||||
|
item_new.we = we
|
||||||
|
assert Will.needs_server_check(item_new) is True
|
||||||
|
|
||||||
|
# PUSHED but not CHECKED -> must be checked (previous behaviour).
|
||||||
|
item_pushed = _make_willitem_blank()
|
||||||
|
item_pushed.we = we
|
||||||
|
item_pushed.set_status("PUSHED", True)
|
||||||
|
assert Will.needs_server_check(item_pushed) is True
|
||||||
|
|
||||||
|
# Already CHECKED -> no need to check again.
|
||||||
|
item_checked = _make_willitem_blank()
|
||||||
|
item_checked.we = we
|
||||||
|
item_checked.set_status("CHECKED", True)
|
||||||
|
assert Will.needs_server_check(item_checked) is False
|
||||||
|
|
||||||
|
# No will-executor assigned -> nothing to check on a server.
|
||||||
|
item_no_we = _make_willitem_blank()
|
||||||
|
item_no_we.we = None
|
||||||
|
assert Will.needs_server_check(item_no_we) is False
|
||||||
|
|
||||||
|
# Not VALID (e.g. invalidated) -> not checked.
|
||||||
|
item_invalid = _make_willitem_blank()
|
||||||
|
item_invalid.we = we
|
||||||
|
item_invalid.set_status("INVALIDATED", True)
|
||||||
|
assert Will.needs_server_check(item_invalid) is False
|
||||||
|
|
||||||
|
|
||||||
def test_will_is_new():
|
def test_will_is_new():
|
||||||
item1 = _make_willitem_blank()
|
item1 = _make_willitem_blank()
|
||||||
item1.set_status("COMPLETE", True)
|
item1.set_status("COMPLETE", True)
|
||||||
@@ -228,79 +325,6 @@ def test_will_check_tx_height():
|
|||||||
# Exception classes
|
# Exception classes
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
def test_will_mark_invalidated_by_tx():
|
|
||||||
"""A valid will spending the same prevout as the invalidation tx must be
|
|
||||||
marked INVALIDATED (and therefore lose its VALID flag). This is what
|
|
||||||
prevents the postpone/expire check from firing a *second* invalidation
|
|
||||||
when phase 1 is restarted after a successful on-chain invalidation."""
|
|
||||||
class FakePrevout:
|
|
||||||
def __init__(self, s):
|
|
||||||
self._s = s
|
|
||||||
def to_str(self):
|
|
||||||
return self._s
|
|
||||||
|
|
||||||
class FakeInput:
|
|
||||||
def __init__(self, s):
|
|
||||||
self.prevout = FakePrevout(s)
|
|
||||||
|
|
||||||
class FakeTx:
|
|
||||||
def __init__(self, prevouts):
|
|
||||||
self._inputs = [FakeInput(p) for p in prevouts]
|
|
||||||
def inputs(self):
|
|
||||||
return self._inputs
|
|
||||||
|
|
||||||
# The real test will item spends this prevout (from _VALID_TX_HEX).
|
|
||||||
spent = "3140eb24b43386f35ba69e3875eb6c93130ac66201d01c58f598defc949a5c2a:0"
|
|
||||||
|
|
||||||
# Will item that spends the same UTXO -> must be invalidated.
|
|
||||||
item_match = _make_willitem_blank()
|
|
||||||
item_match.set_status("COMPLETE", True)
|
|
||||||
# Will item that spends an unrelated UTXO -> must stay VALID.
|
|
||||||
item_other = _make_willitem_blank()
|
|
||||||
item_other.tx = FakeTx(["deadbeef:1"])
|
|
||||||
item_other.children = {}
|
|
||||||
|
|
||||||
will = {"match": item_match, "other": item_other}
|
|
||||||
inval_tx = FakeTx([spent])
|
|
||||||
|
|
||||||
invalidated = Will.mark_invalidated_by_tx(will, inval_tx)
|
|
||||||
|
|
||||||
assert "match" in invalidated
|
|
||||||
assert "other" not in invalidated
|
|
||||||
assert will["match"].get_status("INVALIDATED") is True
|
|
||||||
assert will["match"].get_status("VALID") is False
|
|
||||||
assert will["other"].get_status("VALID") is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_will_mark_invalidated_by_tx_no_match():
|
|
||||||
"""If no valid will spends any of the invalidation tx's prevouts, nothing
|
|
||||||
is marked."""
|
|
||||||
class FakePrevout:
|
|
||||||
def __init__(self, s):
|
|
||||||
self._s = s
|
|
||||||
def to_str(self):
|
|
||||||
return self._s
|
|
||||||
|
|
||||||
class FakeInput:
|
|
||||||
def __init__(self, s):
|
|
||||||
self.prevout = FakePrevout(s)
|
|
||||||
|
|
||||||
class FakeTx:
|
|
||||||
def __init__(self, prevouts):
|
|
||||||
self._inputs = [FakeInput(p) for p in prevouts]
|
|
||||||
def inputs(self):
|
|
||||||
return self._inputs
|
|
||||||
|
|
||||||
item = _make_willitem_blank()
|
|
||||||
will = {"a": item}
|
|
||||||
inval_tx = FakeTx(["unrelated:9"])
|
|
||||||
|
|
||||||
invalidated = Will.mark_invalidated_by_tx(will, inval_tx)
|
|
||||||
|
|
||||||
assert invalidated == []
|
|
||||||
assert will["a"].get_status("VALID") is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_exceptions():
|
def test_exceptions():
|
||||||
from bal.core.will import (
|
from bal.core.will import (
|
||||||
WillException, WillExpiredException, NotCompleteWillException,
|
WillException, WillExpiredException, NotCompleteWillException,
|
||||||
|
|||||||
Reference in New Issue
Block a user