lint: ruff cleanup pass across bal/ and tests/
- Sort imports and fix pyproject ruff config (per-file ignores for intentional Qt/core exceptions) - Mark Heirs.validate_* helpers as @staticmethod - Clean up dead code, rename shadowing vars, use raise ... from - Add AGENTS.md with env/lint/test/release guidance
This commit is contained in:
76
AGENTS.md
Normal file
76
AGENTS.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
BAL — Bitcoin After Life, an Electrum plugin (inheritance / dead-man's-switch).
|
||||||
|
Source-of-truth docs: `README.md`, `HANDOFF.md`, `COMPATIBILITY.md`.
|
||||||
|
|
||||||
|
## Environments (critical)
|
||||||
|
|
||||||
|
Two separate venvs; using the wrong one is the #1 mistake.
|
||||||
|
|
||||||
|
- **Runtime env** (Electrum + PyQt6, has `electrum` importable):
|
||||||
|
`source /home/steal/devel/bal/electrum/env/bin/activate`
|
||||||
|
This is an editable install of the Electrum 4.8.0 checkout at
|
||||||
|
`/home/steal/devel/bal/electrum`. Use it for anything that imports
|
||||||
|
`electrum`, runs GUI code, or runs tests.
|
||||||
|
- **Lint venv** (repo-local `venv/`): ruff, black, flake8 only. It cannot
|
||||||
|
import `electrum` or `PyQt6`. Do NOT use it to run tests.
|
||||||
|
|
||||||
|
The plugin's `bal/` directory is symlinked into
|
||||||
|
`electrum/electrum/plugins/bal` (internal-plugin install used during dev).
|
||||||
|
|
||||||
|
## Test & verify
|
||||||
|
|
||||||
|
Tests are **standalone scripts**, not pytest. Each `tests/test_*.py` file runs
|
||||||
|
its `test_*` functions from `if __name__ == "__main__"`. Run a file directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source /home/steal/devel/bal/electrum/env/bin/activate
|
||||||
|
python3 tests/test_core_heirs.py # core, no Qt needed
|
||||||
|
QT_QPA_PLATFORM=offscreen python3 tests/test_gui_common.py # GUI tests need offscreen
|
||||||
|
```
|
||||||
|
|
||||||
|
- Most core tests run offline (no wallet/network). Some files
|
||||||
|
(`test_group_*.py`, `test_no_willexecutor_karen7.py`, `parallel_ping_test.py`)
|
||||||
|
exercise will-executor/network flows and need the live servers — don't rely on
|
||||||
|
them for quick verification.
|
||||||
|
- `tests/smoke_test.py` proves clean import under real Electrum:
|
||||||
|
`QT_QPA_PLATFORM=offscreen python3 tests/smoke_test.py electrum.plugins.bal`
|
||||||
|
- `tests/external_zip_test.py` loads the built zip the way Electrum's plugin
|
||||||
|
dialog does (`electrum_external_plugins.bal`); run it after `build_zip.py`.
|
||||||
|
|
||||||
|
## Lint / typecheck
|
||||||
|
|
||||||
|
- **Ruff is NOT clean** (hundreds of pre-existing errors in `bal/` and
|
||||||
|
`tests/`). Do not run `--fix` wholesale and do not try to silence everything;
|
||||||
|
just avoid adding new violations. Config: `pyproject.toml` (line-length 88,
|
||||||
|
E501 ignored).
|
||||||
|
- Lint via the repo venv: `/home/steal/devel/bal/bal-electrum-plugin/venv/bin/ruff`
|
||||||
|
- Typecheck: `pyright` (npm, `node_modules/`), config `pyrightconfig.json`
|
||||||
|
(`extraPaths: ["../electrum"]`). Pyright reports many false positives on
|
||||||
|
dynamically-attached attrs (e.g. `self.window`, `BalPlugin.*`); don't chase
|
||||||
|
them.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- `bal/core/` = GUI-free logic (`heirs.py`, `will.py`, `willexecutors.py`,
|
||||||
|
`plugin_base.py`, `util.py`). Must never import Qt.
|
||||||
|
- `bal/gui/qt/` = PyQt6 layer. `window.py` is the per-wallet controller,
|
||||||
|
`plugin.py` is the Electrum `@hooks` entry, `qt.py` is a zipimport shim.
|
||||||
|
- `bal/manifest.json` = version source of truth (Electrum reads it; also read by
|
||||||
|
`make-release.sh`).
|
||||||
|
- Compatibility constraint: must support Electrum **4.7.2 and 4.8.0**; the DB
|
||||||
|
registration API differs between them (`json_db.register_dict` vs
|
||||||
|
`stored_dict.register_name`).
|
||||||
|
|
||||||
|
## Build / release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 build_zip.py # -> bal-electrum-plugin.zip (deterministic, prints sha256)
|
||||||
|
./make-release.sh [v0.x.y] # bump manifest version, tag, sign, push Gitea release
|
||||||
|
```
|
||||||
|
|
||||||
|
- `make-release.sh` requires gpg and Gitea credentials (`~/.git-credentials`
|
||||||
|
or `GITEA_USER`/`GITEA_TOKEN`). It bumps `bal/manifest.json` — bump the
|
||||||
|
version there, never invent a new source of truth.
|
||||||
|
- Remote is Gitea (`origin` = bitcoin-after.life). `.env` holds a Gitea token
|
||||||
|
(gitignored, never commit it).
|
||||||
@@ -102,7 +102,7 @@ def validate_op_return_hex(data_hex: str) -> None:
|
|||||||
try:
|
try:
|
||||||
data = bytes.fromhex(data_hex)
|
data = bytes.fromhex(data_hex)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise NotAnAddress(f"OP_RETURN data is not valid hex: {data_hex}")
|
raise NotAnAddress(f"OP_RETURN data is not valid hex: {data_hex}") from None
|
||||||
if len(data) > 80:
|
if len(data) > 80:
|
||||||
raise NotAnAddress(
|
raise NotAnAddress(
|
||||||
f"OP_RETURN data too long ({len(data)} bytes, max 80)"
|
f"OP_RETURN data too long ({len(data)} bytes, max 80)"
|
||||||
@@ -205,7 +205,7 @@ def prepare_transactions(locktimes, available_utxos, fees, wallet):
|
|||||||
change = get_change_output(wallet, in_amount, out_amount, fee)
|
change = get_change_output(wallet, in_amount, out_amount, fee)
|
||||||
if change:
|
if change:
|
||||||
outputs.append(change)
|
outputs.append(change)
|
||||||
for i in range(0, 100):
|
for _ in range(0, 100):
|
||||||
random.shuffle(outputs)
|
random.shuffle(outputs)
|
||||||
|
|
||||||
#op_return_text = "Hello Bal!"
|
#op_return_text = "Hello Bal!"
|
||||||
@@ -281,7 +281,7 @@ def invalidate_inheritance_transactions(wallet):
|
|||||||
del dtxs[txid]
|
del dtxs[txid]
|
||||||
|
|
||||||
utxos = {}
|
utxos = {}
|
||||||
for txid, tx in dtxs.items():
|
for _, tx in dtxs.items():
|
||||||
get_utxos_from_inputs(tx.inputs(), tx, utxos)
|
get_utxos_from_inputs(tx.inputs(), tx, utxos)
|
||||||
|
|
||||||
utxos = sorted(utxos.items(), key=lambda item: len(item[1]))
|
utxos = sorted(utxos.items(), key=lambda item: len(item[1]))
|
||||||
@@ -727,7 +727,7 @@ class Heirs(dict, Logger):
|
|||||||
)
|
)
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
raise e
|
raise
|
||||||
total_fees = 0
|
total_fees = 0
|
||||||
total_fees_real = 0
|
total_fees_real = 0
|
||||||
total_in = 0
|
total_in = 0
|
||||||
@@ -853,6 +853,7 @@ class Heirs(dict, Logger):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def validate_address(address):
|
def validate_address(address):
|
||||||
if is_op_return_address(address):
|
if is_op_return_address(address):
|
||||||
data_hex = address[len(OP_RETURN_PREFIX):]
|
data_hex = address[len(OP_RETURN_PREFIX):]
|
||||||
@@ -862,24 +863,27 @@ class Heirs(dict, Logger):
|
|||||||
raise NotAnAddress(f"not an address,{address}")
|
raise NotAnAddress(f"not an address,{address}")
|
||||||
return address
|
return address
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def validate_amount(amount):
|
def validate_amount(amount):
|
||||||
try:
|
try:
|
||||||
famount = float(amount[:-1]) if Util.is_perc(amount) else float(amount)
|
famount = float(amount[:-1]) if Util.is_perc(amount) else float(amount)
|
||||||
if famount <= 0.00000001:
|
if famount <= 0.00000001:
|
||||||
raise AmountNotValid(f"amount have to be positive {famount} < 0")
|
raise AmountNotValid(f"amount have to be positive {famount} < 0")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise AmountNotValid(f"amount not properly formatted, {e}")
|
raise AmountNotValid(f"amount not properly formatted, {e}") from e
|
||||||
return amount
|
return amount
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def validate_locktime(locktime, timestamp_to_check=False):
|
def validate_locktime(locktime, timestamp_to_check=False):
|
||||||
try:
|
try:
|
||||||
if timestamp_to_check:
|
if timestamp_to_check:
|
||||||
if Util.parse_locktime_string(locktime, None) < timestamp_to_check:
|
if Util.parse_locktime_string(locktime, None) < timestamp_to_check:
|
||||||
raise HeirExpiredException()
|
raise HeirExpiredException()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise LocktimeNotValid(f"locktime string not properly formatted, {e}")
|
raise LocktimeNotValid(f"locktime string not properly formatted, {e}") from e
|
||||||
return locktime
|
return locktime
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def validate_heir(k, v, timestamp_to_check=False):
|
def validate_heir(k, v, timestamp_to_check=False):
|
||||||
address = Heirs.validate_address(v[HEIR_ADDRESS])
|
address = Heirs.validate_address(v[HEIR_ADDRESS])
|
||||||
if is_op_return_address(v[HEIR_ADDRESS]):
|
if is_op_return_address(v[HEIR_ADDRESS]):
|
||||||
@@ -889,6 +893,7 @@ class Heirs(dict, Logger):
|
|||||||
locktime = Heirs.validate_locktime(v[HEIR_LOCKTIME], timestamp_to_check)
|
locktime = Heirs.validate_locktime(v[HEIR_LOCKTIME], timestamp_to_check)
|
||||||
return (address, amount, locktime)
|
return (address, amount, locktime)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def _validate(data, timestamp_to_check=False):
|
def _validate(data, timestamp_to_check=False):
|
||||||
|
|
||||||
for k, v in list(data.items()):
|
for k, v in list(data.items()):
|
||||||
|
|||||||
@@ -477,14 +477,14 @@ class BalTimestamp:
|
|||||||
We clamp out-of-range timestamps to INT32_MAX, mirroring Electrum's own
|
We clamp out-of-range timestamps to INT32_MAX, mirroring Electrum's own
|
||||||
``get_max_allowed_timestamp`` workaround (see Electrum issue #6170).
|
``get_max_allowed_timestamp`` workaround (see Electrum issue #6170).
|
||||||
"""
|
"""
|
||||||
INT32_MAX = 2 ** 31 - 1
|
int32_max = 2 ** 31 - 1
|
||||||
try:
|
try:
|
||||||
return datetime.fromtimestamp(ts)
|
return datetime.fromtimestamp(ts)
|
||||||
except (OSError, OverflowError, ValueError):
|
except (OSError, OverflowError, ValueError):
|
||||||
try:
|
try:
|
||||||
return datetime.fromtimestamp(min(int(ts), INT32_MAX))
|
return datetime.fromtimestamp(min(int(ts), int32_max))
|
||||||
except (OSError, OverflowError, ValueError):
|
except (OSError, OverflowError, ValueError):
|
||||||
return datetime.fromtimestamp(INT32_MAX)
|
return datetime.fromtimestamp(int32_max)
|
||||||
|
|
||||||
def to_date(self, from_date=None, reverse=False):
|
def to_date(self, from_date=None, reverse=False):
|
||||||
"""Resolve to a ``datetime``.
|
"""Resolve to a ``datetime``.
|
||||||
|
|||||||
@@ -400,14 +400,14 @@ class Util:
|
|||||||
def get_lowest_valid_tx(available_utxos, will):
|
def get_lowest_valid_tx(available_utxos, will):
|
||||||
"""Placeholder kept from the original code (sorts the will by locktime)."""
|
"""Placeholder kept from the original code (sorts the will by locktime)."""
|
||||||
will = sorted(will.items(), key=lambda x: x[1]["tx"].locktime)
|
will = sorted(will.items(), key=lambda x: x[1]["tx"].locktime)
|
||||||
for txid, willitem in will.items():
|
for _txid, _willitem in will.items():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_locktimes(will):
|
def get_locktimes(will):
|
||||||
"""Return the distinct locktimes used by the transactions in ``will``."""
|
"""Return the distinct locktimes used by the transactions in ``will``."""
|
||||||
locktimes = {}
|
locktimes = {}
|
||||||
for txid, willitem in will.items():
|
for _, willitem in will.items():
|
||||||
locktimes[willitem["tx"].locktime] = True
|
locktimes[willitem["tx"].locktime] = True
|
||||||
return locktimes.keys()
|
return locktimes.keys()
|
||||||
|
|
||||||
@@ -446,7 +446,7 @@ class Util:
|
|||||||
def get_will_spent_utxos(will):
|
def get_will_spent_utxos(will):
|
||||||
"""Collect every input spent by any transaction in ``will``."""
|
"""Collect every input spent by any transaction in ``will``."""
|
||||||
utxos = []
|
utxos = []
|
||||||
for txid, willitem in will.items():
|
for _, willitem in will.items():
|
||||||
utxos += willitem["tx"].inputs()
|
utxos += willitem["tx"].inputs()
|
||||||
|
|
||||||
return utxos
|
return utxos
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ from electrum.util import (
|
|||||||
bfh,
|
bfh,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .heirs import WillExecutorFeeTooHighException
|
||||||
from .util import Util
|
from .util import Util
|
||||||
from .willexecutors import Willexecutors
|
from .willexecutors import Willexecutors
|
||||||
from .heirs import WillExecutorFeeTooHighException
|
|
||||||
|
|
||||||
MIN_LOCKTIME = 1
|
MIN_LOCKTIME = 1
|
||||||
MIN_BLOCK = 1
|
MIN_BLOCK = 1
|
||||||
@@ -220,13 +220,8 @@ class Will:
|
|||||||
if ow.we["url"] == nw.we["url"]:
|
if ow.we["url"] == nw.we["url"]:
|
||||||
if int(ow.we["base_fee"]) > int(nw.we["base_fee"]):
|
if int(ow.we["base_fee"]) > int(nw.we["base_fee"]):
|
||||||
return anticipate
|
return anticipate
|
||||||
else:
|
elif int(ow.tx_fees) != int(nw.tx_fees):
|
||||||
if int(ow.tx_fees) != int(nw.tx_fees):
|
return anticipate
|
||||||
return anticipate
|
|
||||||
else:
|
|
||||||
ow.tx.locktime
|
|
||||||
else:
|
|
||||||
ow.tx.locktime
|
|
||||||
else:
|
else:
|
||||||
if nw.we == ow.we:
|
if nw.we == ow.we:
|
||||||
if not Util.cmp_heirs_by_values(ow.heirs, nw.heirs, [0, 3]):
|
if not Util.cmp_heirs_by_values(ow.heirs, nw.heirs, [0, 3]):
|
||||||
@@ -512,7 +507,7 @@ class Will:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_new(will):
|
def is_new(will):
|
||||||
for wid, w in will.items():
|
for _wid, w in will.items():
|
||||||
if w.get_status("VALID") and not w.get_status("COMPLETE"):
|
if w.get_status("VALID") and not w.get_status("COMPLETE"):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -784,7 +779,7 @@ class Will:
|
|||||||
timestamp_to_check: Reference UNIX timestamp (usually "now").
|
timestamp_to_check: Reference UNIX timestamp (usually "now").
|
||||||
"""
|
"""
|
||||||
_logger.info("check if some transaction is expired")
|
_logger.info("check if some transaction is expired")
|
||||||
for prevout_str, wid in all_inputs_min_locktime.items():
|
for _inputs, wid in all_inputs_min_locktime.items():
|
||||||
for w in wid:
|
for w in wid:
|
||||||
if w[1].get_status("VALID"):
|
if w[1].get_status("VALID"):
|
||||||
locktime = int(wid[0][1].tx.locktime)
|
locktime = int(wid[0][1].tx.locktime)
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ class Willexecutors:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
_logger.debug(f"error:{e}")
|
_logger.debug(f"error:{e}")
|
||||||
if str(e) == "already present":
|
if str(e) == "already present":
|
||||||
raise Willexecutors.AlreadyPresentException()
|
raise Willexecutors.AlreadyPresentException() from None
|
||||||
out = False
|
out = False
|
||||||
willexecutor["broadcast_status"] = _("Failed")
|
willexecutor["broadcast_status"] = _("Failed")
|
||||||
|
|
||||||
@@ -485,8 +485,7 @@ class Willexecutors:
|
|||||||
Returns:
|
Returns:
|
||||||
The same ``willexecutors`` mapping, updated in place.
|
The same ``willexecutors`` mapping, updated in place.
|
||||||
"""
|
"""
|
||||||
from concurrent.futures import ThreadPoolExecutor, wait
|
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
|
||||||
from concurrent.futures import FIRST_COMPLETED
|
|
||||||
|
|
||||||
items = list(willexecutors.items())
|
items = list(willexecutors.items())
|
||||||
if not items:
|
if not items:
|
||||||
@@ -566,8 +565,7 @@ class Willexecutors:
|
|||||||
Returns ``{url: (ok, exception_or_None)}`` for the servers that
|
Returns ``{url: (ok, exception_or_None)}`` for the servers that
|
||||||
answered in time (timed-out servers are reported via ``on_timeout``).
|
answered in time (timed-out servers are reported via ``on_timeout``).
|
||||||
"""
|
"""
|
||||||
from concurrent.futures import ThreadPoolExecutor, wait
|
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
|
||||||
from concurrent.futures import FIRST_COMPLETED
|
|
||||||
|
|
||||||
targets = [(url, we) for url, we in willexecutors.items() if "txs" in we]
|
targets = [(url, we) for url, we in willexecutors.items() if "txs" in we]
|
||||||
results = {}
|
results = {}
|
||||||
@@ -684,8 +682,7 @@ class Willexecutors:
|
|||||||
Returns ``{wid: (result_or_None, exception_or_None)}`` for the servers
|
Returns ``{wid: (result_or_None, exception_or_None)}`` for the servers
|
||||||
that answered in time.
|
that answered in time.
|
||||||
"""
|
"""
|
||||||
from concurrent.futures import ThreadPoolExecutor, wait
|
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
|
||||||
from concurrent.futures import FIRST_COMPLETED
|
|
||||||
|
|
||||||
targets = [(wid, url) for wid, url in items if url]
|
targets = [(wid, url) for wid, url in items if url]
|
||||||
results = {}
|
results = {}
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ to "check in" before the locktime expires. This module turns the event data
|
|||||||
into an RFC-5545 .ics file and opens it with the OS default application.
|
into an RFC-5545 .ics file and opens it with the OS default application.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .common import *
|
|
||||||
from .common import _, _logger # underscore names are not re-exported by "import *"
|
|
||||||
from PyQt6.QtGui import QAction
|
from PyQt6.QtGui import QAction
|
||||||
from PyQt6.QtWidgets import QToolButton
|
from PyQt6.QtWidgets import QToolButton
|
||||||
|
|
||||||
|
from .common import *
|
||||||
|
from .common import _, _logger # underscore names are not re-exported by "import *"
|
||||||
|
|
||||||
|
|
||||||
class BalCalendarButton(QToolButton):
|
class BalCalendarButton(QToolButton):
|
||||||
"""A QToolButton with a dropdown menu for .ics calendar file actions.
|
"""A QToolButton with a dropdown menu for .ics calendar file actions.
|
||||||
@@ -79,7 +80,8 @@ class BalCalendarButton(QToolButton):
|
|||||||
path = self._ensure_ics()
|
path = self._ensure_ics()
|
||||||
if not path:
|
if not path:
|
||||||
return
|
return
|
||||||
import shlex, subprocess
|
import shlex
|
||||||
|
import subprocess
|
||||||
if self._bal_window.bal_plugin.is_basic_mode():
|
if self._bal_window.bal_plugin.is_basic_mode():
|
||||||
app = self._bal_window.bal_plugin.CALENDAR_APP.default
|
app = self._bal_window.bal_plugin.CALENDAR_APP.default
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -27,65 +27,129 @@ from decimal import Decimal
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import Any, Callable, Mapping, Optional, Union
|
from typing import Any, Callable, Mapping, Optional, Union
|
||||||
|
|
||||||
from electrum.bitcoin import (NLOCKTIME_BLOCKHEIGHT_MAX, NLOCKTIME_MAX,
|
from electrum.bitcoin import NLOCKTIME_BLOCKHEIGHT_MAX, NLOCKTIME_MAX, NLOCKTIME_MIN
|
||||||
NLOCKTIME_MIN)
|
|
||||||
from electrum.gui.qt.amountedit import BTCAmountEdit
|
from electrum.gui.qt.amountedit import BTCAmountEdit
|
||||||
from electrum.gui.qt.main_window import ElectrumWindow, StatusBarButton
|
from electrum.gui.qt.main_window import ElectrumWindow, StatusBarButton
|
||||||
from electrum.gui.qt.my_treeview import MyTreeView
|
from electrum.gui.qt.my_treeview import MyTreeView
|
||||||
from electrum.gui.qt.password_dialog import PasswordDialog
|
from electrum.gui.qt.password_dialog import PasswordDialog
|
||||||
from electrum.gui.qt.transaction_dialog import TxDialog
|
from electrum.gui.qt.transaction_dialog import TxDialog
|
||||||
from electrum.gui.qt.util import (Buttons, CancelButton, ColorScheme,
|
from electrum.gui.qt.util import (
|
||||||
EnterButton, HelpButton, MessageBoxMixin,
|
Buttons,
|
||||||
OkButton, TaskThread, WindowModalDialog,
|
CancelButton,
|
||||||
char_width_in_lineedit, getSaveFileName,
|
ColorScheme,
|
||||||
import_meta_gui, read_QIcon_from_bytes,
|
EnterButton,
|
||||||
read_QPixmap_from_bytes, webopen)
|
HelpButton,
|
||||||
|
MessageBoxMixin,
|
||||||
|
OkButton,
|
||||||
|
TaskThread,
|
||||||
|
WindowModalDialog,
|
||||||
|
char_width_in_lineedit,
|
||||||
|
getSaveFileName,
|
||||||
|
import_meta_gui,
|
||||||
|
read_QIcon_from_bytes,
|
||||||
|
read_QPixmap_from_bytes,
|
||||||
|
webopen,
|
||||||
|
)
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum.logging import get_logger
|
from electrum.logging import get_logger
|
||||||
from electrum.network import BestEffortRequestFailed, Network, TxBroadcastError
|
from electrum.network import BestEffortRequestFailed, Network, TxBroadcastError
|
||||||
from electrum.payment_identifier import PaymentIdentifier
|
from electrum.payment_identifier import PaymentIdentifier
|
||||||
from electrum.plugin import hook
|
from electrum.plugin import hook
|
||||||
from electrum.transaction import SerializationError, Transaction, tx_from_any
|
from electrum.transaction import SerializationError, Transaction, tx_from_any
|
||||||
from electrum.util import (DECIMAL_POINT, FileExportFailed, UserCancelled,
|
from electrum.util import (
|
||||||
decimal_point_to_base_unit_name, read_json_file,
|
DECIMAL_POINT,
|
||||||
write_json_file)
|
FileExportFailed,
|
||||||
from PyQt6.QtCore import (QDateTime, QModelIndex, QPersistentModelIndex, QSize,
|
UserCancelled,
|
||||||
Qt, QTimer, pyqtSignal)
|
decimal_point_to_base_unit_name,
|
||||||
from PyQt6.QtGui import (QColor, QPainter, QPalette, QStandardItem,
|
read_json_file,
|
||||||
QStandardItemModel)
|
write_json_file,
|
||||||
from PyQt6.QtWidgets import (QAbstractItemView, QAbstractSpinBox, QCheckBox,
|
)
|
||||||
QComboBox, QDateTimeEdit, QGridLayout, QHBoxLayout,
|
from PyQt6.QtCore import (
|
||||||
QInputDialog, QLabel, QLineEdit, QTextEdit, QMenu,
|
QDateTime,
|
||||||
QMenuBar, QPushButton, QScrollArea, QSizePolicy,
|
QModelIndex,
|
||||||
QSpinBox, QStackedWidget, QStyle, QStyleOptionFrame,
|
QPersistentModelIndex,
|
||||||
QVBoxLayout, QWidget, QDialog)
|
QSize,
|
||||||
|
Qt,
|
||||||
|
QTimer,
|
||||||
|
pyqtSignal,
|
||||||
|
)
|
||||||
|
from PyQt6.QtGui import QColor, QPainter, QPalette, QStandardItem, QStandardItemModel
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QAbstractItemView,
|
||||||
|
QAbstractSpinBox,
|
||||||
|
QCheckBox,
|
||||||
|
QComboBox,
|
||||||
|
QDateTimeEdit,
|
||||||
|
QDialog,
|
||||||
|
QGridLayout,
|
||||||
|
QHBoxLayout,
|
||||||
|
QInputDialog,
|
||||||
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
|
QMenu,
|
||||||
|
QMenuBar,
|
||||||
|
QPushButton,
|
||||||
|
QScrollArea,
|
||||||
|
QSizePolicy,
|
||||||
|
QSpinBox,
|
||||||
|
QStackedWidget,
|
||||||
|
QStyle,
|
||||||
|
QStyleOptionFrame,
|
||||||
|
QTextEdit,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ...core.heirs import (
|
||||||
|
HEIR_DUST_AMOUNT,
|
||||||
|
HEIR_REAL_AMOUNT,
|
||||||
|
OP_RETURN_PREFIX,
|
||||||
|
HeirAmountIsDustException,
|
||||||
|
Heirs,
|
||||||
|
WillExecutorFeeTooHighException,
|
||||||
|
get_op_return_hex,
|
||||||
|
is_op_return_address,
|
||||||
|
validate_op_return_hex,
|
||||||
|
)
|
||||||
|
|
||||||
# --- Core (GUI-free) logic layer ---
|
# --- Core (GUI-free) logic layer ---
|
||||||
from ...core.plugin_base import BalPlugin, BalTimestamp
|
from ...core.plugin_base import BalPlugin, BalTimestamp
|
||||||
from ...core.heirs import (HEIR_DUST_AMOUNT, HEIR_REAL_AMOUNT,
|
|
||||||
HeirAmountIsDustException, Heirs,
|
|
||||||
OP_RETURN_PREFIX, is_op_return_address,
|
|
||||||
get_op_return_hex, validate_op_return_hex,
|
|
||||||
WillExecutorFeeTooHighException)
|
|
||||||
from ...core.util import Util
|
from ...core.util import Util
|
||||||
from ...core.will import (AmountException, HeirChangeException,
|
from ...core.will import (
|
||||||
HeirNotFoundException, NoHeirsException,
|
AmountException,
|
||||||
NotCompleteWillException, NoWillExecutorNotPresent,
|
HeirChangeException,
|
||||||
TxFeesChangedException, Will,
|
HeirNotFoundException,
|
||||||
WillexecutorChangeException, WillExecutorNotPresent,
|
NoHeirsException,
|
||||||
WillExpiredException, WillItem, WillPostponedException)
|
NotCompleteWillException,
|
||||||
from ...core.willexecutors import Willexecutors
|
NoWillExecutorNotPresent,
|
||||||
from ...core.willexecutors import is_onion_url, is_tor_active # noqa: F401
|
TxFeesChangedException,
|
||||||
|
Will,
|
||||||
|
WillexecutorChangeException,
|
||||||
|
WillExecutorNotPresent,
|
||||||
|
WillExpiredException,
|
||||||
|
WillItem,
|
||||||
|
WillPostponedException,
|
||||||
|
)
|
||||||
|
from ...core.willexecutors import ( # noqa: F401
|
||||||
|
Willexecutors,
|
||||||
|
is_onion_url,
|
||||||
|
is_tor_active,
|
||||||
|
)
|
||||||
|
|
||||||
# --- 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
|
||||||
from .window_utils import (bring_to_front, show_modal, show_on_top,
|
from .window_utils import (
|
||||||
stop_thread, top_level_of)
|
bring_to_front,
|
||||||
|
show_modal,
|
||||||
|
show_on_top,
|
||||||
|
stop_thread,
|
||||||
|
top_level_of,
|
||||||
|
)
|
||||||
|
|
||||||
_logger = get_logger(__name__)
|
_logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class shown_cv:
|
class shown_cv: # noqa: N801 (intentionally lowercase: mirrors Qt signal naming)
|
||||||
_type = bool
|
_type = bool
|
||||||
|
|
||||||
def __init__(self, value):
|
def __init__(self, value):
|
||||||
|
|||||||
@@ -17,13 +17,16 @@ the few list classes they reference are imported lazily inside the methods that
|
|||||||
use them (see ``lists`` imports below).
|
use them (see ``lists`` imports below).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from .calendar import BalCalendar, BalCalendarButton
|
||||||
from .common import *
|
from .common import *
|
||||||
from .common import _, _logger # underscore names are not re-exported by "import *"
|
from .common import _, _logger # underscore names are not re-exported by "import *"
|
||||||
from .widgets import (BalCheckBox, BalLineEdit, BalTextEdit, BalTxFeesWidget,
|
from .widgets import (
|
||||||
LockTimeWidget, PercAmountEdit, ThresholdTimeWidget,
|
WillSettingsWidget,
|
||||||
WillSettingsWidget, WillWidget, basic_reminder_offsets,
|
WillWidget,
|
||||||
compute_reminder_offsets)
|
basic_reminder_offsets,
|
||||||
from .calendar import BalCalendar, BalCalendarButton
|
compute_reminder_offsets,
|
||||||
|
)
|
||||||
|
|
||||||
# NOTE: list views (HeirListWidget, PreviewList, WillExecutorWidget) are
|
# NOTE: list views (HeirListWidget, PreviewList, WillExecutorWidget) are
|
||||||
# imported lazily where needed to avoid a dialogs<->lists import cycle.
|
# imported lazily where needed to avoid a dialogs<->lists import cycle.
|
||||||
|
|
||||||
@@ -31,9 +34,7 @@ from .calendar import BalCalendar, BalCalendarButton
|
|||||||
class BalDialog(QDialog,MessageBoxMixin):
|
class BalDialog(QDialog,MessageBoxMixin):
|
||||||
_stopping = False
|
_stopping = False
|
||||||
def __init__(self, parent, bal_plugin, title=None, icon="icons/bal16x16.png"):
|
def __init__(self, parent, bal_plugin, title=None, icon="icons/bal16x16.png"):
|
||||||
import signal
|
|
||||||
from PyQt6.QtCore import QMetaObject, Qt
|
from PyQt6.QtCore import QMetaObject, Qt
|
||||||
from PyQt6.QtWidgets import QApplication
|
|
||||||
def handler(signum, frame):
|
def handler(signum, frame):
|
||||||
QMetaObject.invokeMethod(self, "close", Qt.ConnectionType.QueuedConnection)
|
QMetaObject.invokeMethod(self, "close", Qt.ConnectionType.QueuedConnection)
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ class BalDialog(QDialog,MessageBoxMixin):
|
|||||||
self.setWindowTitle(title)
|
self.setWindowTitle(title)
|
||||||
# WindowModalDialog.__init__(self,parent)
|
# WindowModalDialog.__init__(self,parent)
|
||||||
self.setWindowIcon(read_QIcon_from_bytes(bal_plugin.read_file(icon)))
|
self.setWindowIcon(read_QIcon_from_bytes(bal_plugin.read_file(icon)))
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
self._stopping = True
|
self._stopping = True
|
||||||
# NOTE: we deliberately do NOT stop ``self.thread`` here.
|
# NOTE: we deliberately do NOT stop ``self.thread`` here.
|
||||||
@@ -701,7 +702,7 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
return None, Will.invalidate_will(
|
return None, Will.invalidate_will(
|
||||||
self.bal_window.willitems, self.bal_window.wallet, fee_per_byte
|
self.bal_window.willitems, self.bal_window.wallet, fee_per_byte
|
||||||
)
|
)
|
||||||
except NoHeirsException as e:
|
except NoHeirsException:
|
||||||
_logger.debug("no heirs")
|
_logger.debug("no heirs")
|
||||||
self.msg_set_checking("No Heirs")
|
self.msg_set_checking("No Heirs")
|
||||||
except NotCompleteWillException as e:
|
except NotCompleteWillException as e:
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ construction) for all business actions, so the heavy logic stays in ``window``
|
|||||||
and ``dialogs``.
|
and ``dialogs``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import QLineEdit as _QLineEdit
|
||||||
|
from PyQt6.QtWidgets import QMessageBox, QStyledItemDelegate
|
||||||
|
|
||||||
from .common import *
|
from .common import *
|
||||||
from .common import _, _logger # underscore names are not re-exported by "import *"
|
from .common import _, _logger # underscore names are not re-exported by "import *"
|
||||||
from .widgets import BalCheckBox, PercAmountEdit, WillSettingsWidget
|
|
||||||
from PyQt6.QtWidgets import QMessageBox
|
|
||||||
from PyQt6.QtWidgets import QStyledItemDelegate, QLineEdit as _QLineEdit
|
|
||||||
from .dialogs import BalBuildWillDialog, BalDialog
|
from .dialogs import BalBuildWillDialog, BalDialog
|
||||||
|
from .widgets import BalCheckBox, WillSettingsWidget
|
||||||
|
|
||||||
|
|
||||||
class HeirListWidget(MyTreeView, MessageBoxMixin):
|
class HeirListWidget(MyTreeView, MessageBoxMixin):
|
||||||
@@ -194,7 +195,7 @@ class HeirListWidget(MyTreeView, MessageBoxMixin):
|
|||||||
set_current = QPersistentModelIndex(idx)
|
set_current = QPersistentModelIndex(idx)
|
||||||
try:
|
try:
|
||||||
self.will_settings_widget.on_locktime_change()
|
self.will_settings_widget.on_locktime_change()
|
||||||
except Exception as e:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
self.set_current_idx(set_current)
|
self.set_current_idx(set_current)
|
||||||
# FIXME refresh loses sort order; so set "default" here:
|
# FIXME refresh loses sort order; so set "default" here:
|
||||||
@@ -214,15 +215,15 @@ class HeirListWidget(MyTreeView, MessageBoxMixin):
|
|||||||
menu.addAction(_("Import"), self.bal_window.import_heirs)
|
menu.addAction(_("Import"), self.bal_window.import_heirs)
|
||||||
menu.addAction(_("Export"), lambda: self.bal_window.export_heirs())
|
menu.addAction(_("Export"), lambda: self.bal_window.export_heirs())
|
||||||
|
|
||||||
newHeirButton = QPushButton(_("New Heir"))
|
new_heir_button = QPushButton(_("New Heir"))
|
||||||
newHeirButton.clicked.connect(self.bal_window.new_heir_dialog)
|
new_heir_button.clicked.connect(self.bal_window.new_heir_dialog)
|
||||||
|
|
||||||
widget = QWidget(self)
|
widget = QWidget(self)
|
||||||
layout = QHBoxLayout(widget)
|
layout = QHBoxLayout(widget)
|
||||||
self.will_settings_widget = WillSettingsWidget(self.bal_window, self)
|
self.will_settings_widget = WillSettingsWidget(self.bal_window, self)
|
||||||
|
|
||||||
layout.addWidget(self.will_settings_widget)
|
layout.addWidget(self.will_settings_widget)
|
||||||
layout.addWidget(newHeirButton)
|
layout.addWidget(new_heir_button)
|
||||||
|
|
||||||
toolbar.insertWidget(2, widget)
|
toolbar.insertWidget(2, widget)
|
||||||
|
|
||||||
@@ -281,7 +282,7 @@ class PreviewList(MyTreeView, MessageBoxMixin):
|
|||||||
self.setModel(QStandardItemModel(self))
|
self.setModel(QStandardItemModel(self))
|
||||||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||||
self.sortByColumn(self.Columns.NAME, Qt.SortOrder.AscendingOrder)
|
self.sortByColumn(self.Columns.NAME, Qt.SortOrder.AscendingOrder)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.setSortingEnabled(True)
|
self.setSortingEnabled(True)
|
||||||
|
|||||||
@@ -15,14 +15,17 @@ and cached in ``self.bal_windows``.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from electrum.gui.qt.main_window import StatusBarButton
|
from electrum.gui.qt.main_window import StatusBarButton
|
||||||
|
from PyQt6.QtWidgets import QLayout
|
||||||
|
|
||||||
from .common import *
|
from .common import *
|
||||||
from .common import _, _logger # underscore names are not re-exported by "import *"
|
from .common import ( # underscore names are not re-exported by "import *"
|
||||||
from .common import read_QIcon_from_bytes
|
_,
|
||||||
|
_logger,
|
||||||
|
read_QIcon_from_bytes,
|
||||||
|
)
|
||||||
|
from .dialogs import BalDialog
|
||||||
from .widgets import BalCheckBox, BalLineEdit, BalSpinBox, BalTextEdit
|
from .widgets import BalCheckBox, BalLineEdit, BalSpinBox, BalTextEdit
|
||||||
from .window import BalWindow
|
from .window import BalWindow
|
||||||
from .dialogs import BalDialog
|
|
||||||
from PyQt6.QtWidgets import QLayout
|
|
||||||
|
|
||||||
|
|
||||||
def _window_key(window):
|
def _window_key(window):
|
||||||
@@ -88,9 +91,11 @@ class Plugin(BalPlugin):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
from electrum.gui.qt.plugins_dialog import PluginsDialog
|
from electrum.gui.qt.plugins_dialog import (
|
||||||
|
PluginsDialog as plugins_dialog, # noqa: N813
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
PluginsDialog = None
|
plugins_dialog = None
|
||||||
app = QApplication.instance()
|
app = QApplication.instance()
|
||||||
if app is None:
|
if app is None:
|
||||||
return []
|
return []
|
||||||
@@ -106,7 +111,7 @@ class Plugin(BalPlugin):
|
|||||||
for w in app.topLevelWidgets():
|
for w in app.topLevelWidgets():
|
||||||
try:
|
try:
|
||||||
is_match = False
|
is_match = False
|
||||||
if PluginsDialog is not None and isinstance(w, PluginsDialog):
|
if plugins_dialog is not None and isinstance(w, plugins_dialog):
|
||||||
is_match = True
|
is_match = True
|
||||||
elif type(w).__name__ == "PluginsDialog":
|
elif type(w).__name__ == "PluginsDialog":
|
||||||
is_match = True
|
is_match = True
|
||||||
@@ -142,17 +147,17 @@ class Plugin(BalPlugin):
|
|||||||
each is guarded independently.
|
each is guarded independently.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
from PyQt6.QtWidgets import QDialog
|
from PyQt6.QtWidgets import QDialog as qdialog # noqa: N813
|
||||||
except Exception:
|
except Exception:
|
||||||
QDialog = None
|
qdialog = None
|
||||||
# 1) reject() / done(): the reliable way to end an exec() modal loop.
|
# 1) reject() / done(): the reliable way to end an exec() modal loop.
|
||||||
if QDialog is not None and isinstance(d, QDialog):
|
if qdialog is not None and isinstance(d, qdialog):
|
||||||
try:
|
try:
|
||||||
d.reject()
|
d.reject()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_logger.debug("reject() failed: {}".format(e))
|
_logger.debug("reject() failed: {}".format(e))
|
||||||
try:
|
try:
|
||||||
d.done(QDialog.DialogCode.Rejected)
|
d.done(qdialog.DialogCode.Rejected)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_logger.debug("done() failed: {}".format(e))
|
_logger.debug("done() failed: {}".format(e))
|
||||||
# 2) close(): covers non-QDialog top-levels and is a harmless extra.
|
# 2) close(): covers non-QDialog top-levels and is a harmless extra.
|
||||||
@@ -172,9 +177,9 @@ class Plugin(BalPlugin):
|
|||||||
it and closes it themselves (it must not linger in the background).
|
it and closes it themselves (it must not linger in the background).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
from PyQt6.QtCore import QTimer
|
from PyQt6.QtCore import QTimer as qtimer # noqa: N813
|
||||||
except Exception:
|
except Exception:
|
||||||
QTimer = None
|
qtimer = None
|
||||||
# Schedule of retry delays (ms) measured from each call.
|
# Schedule of retry delays (ms) measured from each call.
|
||||||
retry_delays = [400, 800, 1500]
|
retry_delays = [400, 800, 1500]
|
||||||
dialogs = Plugin._find_plugins_manager_dialogs()
|
dialogs = Plugin._find_plugins_manager_dialogs()
|
||||||
@@ -190,8 +195,8 @@ class Plugin(BalPlugin):
|
|||||||
if not still_open:
|
if not still_open:
|
||||||
_logger.info("plugins dialog closed successfully")
|
_logger.info("plugins dialog closed successfully")
|
||||||
return
|
return
|
||||||
if attempt < len(retry_delays) and QTimer is not None:
|
if attempt < len(retry_delays) and qtimer is not None:
|
||||||
QTimer.singleShot(
|
qtimer.singleShot(
|
||||||
retry_delays[attempt],
|
retry_delays[attempt],
|
||||||
lambda: Plugin._handle_plugins_manager_dialog(attempt + 1),
|
lambda: Plugin._handle_plugins_manager_dialog(attempt + 1),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ Contents:
|
|||||||
* WillWidget - single will-tx box
|
* WillWidget - single will-tx box
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from .calendar import BalCalendar, BalCalendarButton
|
||||||
from .common import *
|
from .common import *
|
||||||
from .common import _, _logger # underscore names are not re-exported by "import *"
|
from .common import _, _logger # underscore names are not re-exported by "import *"
|
||||||
from .calendar import BalCalendar, BalCalendarButton
|
|
||||||
|
|
||||||
|
|
||||||
def compute_reminder_offsets(days, count):
|
def compute_reminder_offsets(days, count):
|
||||||
@@ -690,7 +690,7 @@ class LockTimeDateEdit(QDateTimeEdit, _LockTimeEditor):
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
x = int(x)
|
x = int(x)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
x = QDateTime.currentDateTime().timestamp()
|
x = QDateTime.currentDateTime().timestamp()
|
||||||
finally:
|
finally:
|
||||||
# Use the overflow-safe converter: on Windows datetime.fromtimestamp
|
# Use the overflow-safe converter: on Windows datetime.fromtimestamp
|
||||||
@@ -1395,15 +1395,15 @@ class PercAmountEdit(BTCAmountEdit):
|
|||||||
if self.base_unit:
|
if self.base_unit:
|
||||||
panel = QStyleOptionFrame()
|
panel = QStyleOptionFrame()
|
||||||
self.initStyleOption(panel)
|
self.initStyleOption(panel)
|
||||||
textRect = self.style().subElementRect(
|
text_rect = self.style().subElementRect(
|
||||||
QStyle.SubElement.SE_LineEditContents, panel, self
|
QStyle.SubElement.SE_LineEditContents, panel, self
|
||||||
)
|
)
|
||||||
textRect.adjust(2, 0, -10, 0)
|
text_rect.adjust(2, 0, -10, 0)
|
||||||
painter = QPainter(self)
|
painter = QPainter(self)
|
||||||
painter.setPen(ColorScheme.GRAY.as_color())
|
painter.setPen(ColorScheme.GRAY.as_color())
|
||||||
if len(self.text()) == 0:
|
if len(self.text()) == 0:
|
||||||
painter.drawText(
|
painter.drawText(
|
||||||
textRect,
|
text_rect,
|
||||||
int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter),
|
int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter),
|
||||||
self.base_unit() + " or perc value",
|
self.base_unit() + " or perc value",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,11 +18,16 @@ import threading
|
|||||||
|
|
||||||
from .common import *
|
from .common import *
|
||||||
from .common import _, _logger # underscore names are not re-exported by "import *"
|
from .common import _, _logger # underscore names are not re-exported by "import *"
|
||||||
from .widgets import LockTimeWidget, PercAmountEdit, WillSettingsWidget
|
from .dialogs import (
|
||||||
from .dialogs import (BalBlockingWaitingDialog, BalBuildWillDialog, BalDialog,
|
BalBuildWillDialog,
|
||||||
BalWaitingDialog, BalWizardDialog, WillDetailDialog,
|
BalDialog,
|
||||||
WillExecutorDialog)
|
BalWaitingDialog,
|
||||||
from .lists import HeirListWidget, PreviewList, WillExecutorWidget
|
BalWizardDialog,
|
||||||
|
WillDetailDialog,
|
||||||
|
WillExecutorDialog,
|
||||||
|
)
|
||||||
|
from .lists import HeirListWidget, PreviewList
|
||||||
|
from .widgets import LockTimeWidget, PercAmountEdit
|
||||||
|
|
||||||
|
|
||||||
class BalWindow:
|
class BalWindow:
|
||||||
|
|||||||
@@ -5,3 +5,15 @@ target-version = "py312"
|
|||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
select =["E", "W", "F", "I", "N", "B"]
|
select =["E", "W", "F", "I", "N", "B"]
|
||||||
ignore = ["E501"]
|
ignore = ["E501"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
"bal/gui/qt/*.py" = ["F403", "F405"] # intentional `from .common import *` hub
|
||||||
|
"bal/gui/qt/common.py" = ["F401"] # re-exports consumed via `import *`
|
||||||
|
"bal/gui/qt/dialogs.py" = ["N802"] # Qt overrides: closeEvent/hideEvent/getText
|
||||||
|
"bal/gui/qt/lists.py" = ["N802"] # Qt overrides: createEditor/setEditorData/setModelData
|
||||||
|
"bal/gui/qt/widgets.py" = ["N802", "N815"] # Qt overrides + Qt signal attrs (valueChanged, ...)
|
||||||
|
"bal/gui/qt/window.py" = ["N802"] # getMsg
|
||||||
|
"bal/core/heirs.py" = ["N818", "N802"] # public exception names + buildTransactions API
|
||||||
|
"bal/core/will.py" = ["N818"] # public exception names
|
||||||
|
"bal/core/willexecutors.py" = ["N818"] # public exception names
|
||||||
|
"tests/*.py" = ["N802", "E402"] # deliberate UPPER_CASE helpers + sys.path-before-import
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ def _active_source_without_strings(module) -> str:
|
|||||||
if isinstance(node.value, str) and hasattr(node, "end_lineno"):
|
if isinstance(node.value, str) and hasattr(node, "end_lineno"):
|
||||||
self.spans.append((node.lineno, node.end_lineno))
|
self.spans.append((node.lineno, node.end_lineno))
|
||||||
self.generic_visit(node)
|
self.generic_visit(node)
|
||||||
s = _S(); s.visit(tree)
|
s = _S()
|
||||||
|
s.visit(tree)
|
||||||
drop = set()
|
drop = set()
|
||||||
for a, b in s.spans:
|
for a, b in s.spans:
|
||||||
drop.update(range(a, b + 1))
|
drop.update(range(a, b + 1))
|
||||||
@@ -45,12 +46,13 @@ def _active_source_without_strings(module) -> str:
|
|||||||
|
|
||||||
def main(pkg: str) -> int:
|
def main(pkg: str) -> int:
|
||||||
from PyQt6.QtWidgets import QApplication, QDialog, QWidget
|
from PyQt6.QtWidgets import QApplication, QDialog, QWidget
|
||||||
app = QApplication.instance() or QApplication(sys.argv)
|
_app = QApplication.instance() or QApplication(sys.argv)
|
||||||
|
|
||||||
wu = importlib.import_module(pkg + ".gui.qt.window_utils")
|
wu = importlib.import_module(pkg + ".gui.qt.window_utils")
|
||||||
|
|
||||||
# top_level_of: returns the top-level container of a child widget
|
# top_level_of: returns the top-level container of a child widget
|
||||||
w = QWidget(); child = QWidget(w)
|
w = QWidget()
|
||||||
|
child = QWidget(w)
|
||||||
assert wu.top_level_of(child) is w
|
assert wu.top_level_of(child) is w
|
||||||
assert wu.top_level_of(None) is None
|
assert wu.top_level_of(None) is None
|
||||||
print("[OK] top_level_of")
|
print("[OK] top_level_of")
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ N = 8 # number of servers
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
we_mod = importlib.import_module(f"{PKG}.core.willexecutors")
|
we_mod = importlib.import_module(f"{PKG}.core.willexecutors")
|
||||||
W = we_mod.Willexecutors
|
we_cls = we_mod.Willexecutors
|
||||||
|
|
||||||
# ---- 1) ping_servers_parallel: time ~= slowest, not sum ----
|
# ---- 1) ping_servers_parallel: time ~= slowest, not sum ----
|
||||||
def slow_get_info(url, we, **kwargs):
|
def slow_get_info(url, we, **kwargs):
|
||||||
@@ -43,8 +43,8 @@ def main():
|
|||||||
we["status"] = 200
|
we["status"] = 200
|
||||||
return we
|
return we
|
||||||
|
|
||||||
orig_get_info = W.get_info_task
|
orig_get_info = we_cls.get_info_task
|
||||||
W.get_info_task = staticmethod(slow_get_info)
|
we_cls.get_info_task = staticmethod(slow_get_info)
|
||||||
try:
|
try:
|
||||||
wes = {}
|
wes = {}
|
||||||
for i in range(N):
|
for i in range(N):
|
||||||
@@ -57,7 +57,7 @@ def main():
|
|||||||
seen.append((url, ok))
|
seen.append((url, ok))
|
||||||
|
|
||||||
start = time.time()
|
start = time.time()
|
||||||
W.ping_servers_parallel(wes, on_each=on_each, max_workers=N)
|
we_cls.ping_servers_parallel(wes, on_each=on_each, max_workers=N)
|
||||||
elapsed = time.time() - start
|
elapsed = time.time() - start
|
||||||
|
|
||||||
# Sequential would take ~ N * SLOW. Parallel must be far less.
|
# Sequential would take ~ N * SLOW. Parallel must be far less.
|
||||||
@@ -81,15 +81,15 @@ def main():
|
|||||||
assert we["status"] == "KO", (url, we)
|
assert we["status"] == "KO", (url, we)
|
||||||
print("[OK] ping results written back into the willexecutors mapping")
|
print("[OK] ping results written back into the willexecutors mapping")
|
||||||
finally:
|
finally:
|
||||||
W.get_info_task = orig_get_info
|
we_cls.get_info_task = orig_get_info
|
||||||
|
|
||||||
# ---- 2) push_transactions_parallel: time ~= slowest, not sum ----
|
# ---- 2) push_transactions_parallel: time ~= slowest, not sum ----
|
||||||
def slow_push(we, **kwargs):
|
def slow_push(we, **kwargs):
|
||||||
time.sleep(SLOW)
|
time.sleep(SLOW)
|
||||||
return "fail" not in we["url"]
|
return "fail" not in we["url"]
|
||||||
|
|
||||||
orig_push = W.push_transactions_to_willexecutor
|
orig_push = we_cls.push_transactions_to_willexecutor
|
||||||
W.push_transactions_to_willexecutor = staticmethod(slow_push)
|
we_cls.push_transactions_to_willexecutor = staticmethod(slow_push)
|
||||||
try:
|
try:
|
||||||
wes = {}
|
wes = {}
|
||||||
for i in range(N):
|
for i in range(N):
|
||||||
@@ -106,7 +106,7 @@ def main():
|
|||||||
pushed.append((url, ok))
|
pushed.append((url, ok))
|
||||||
|
|
||||||
start = time.time()
|
start = time.time()
|
||||||
results = W.push_transactions_parallel(wes, on_each=on_each_push,
|
results = we_cls.push_transactions_parallel(wes, on_each=on_each_push,
|
||||||
max_workers=N)
|
max_workers=N)
|
||||||
elapsed = time.time() - start
|
elapsed = time.time() - start
|
||||||
|
|
||||||
@@ -117,11 +117,11 @@ def main():
|
|||||||
f"(sequential would be ~{sequential:.2f}s)")
|
f"(sequential would be ~{sequential:.2f}s)")
|
||||||
|
|
||||||
assert len(results) == N, results
|
assert len(results) == N, results
|
||||||
for url, (ok, exc) in results.items():
|
for url, (ok, _exc) in results.items():
|
||||||
assert ok == ("good" in url), (url, ok)
|
assert ok == ("good" in url), (url, ok)
|
||||||
print("[OK] push results correct for every server")
|
print("[OK] push results correct for every server")
|
||||||
finally:
|
finally:
|
||||||
W.push_transactions_to_willexecutor = orig_push
|
we_cls.push_transactions_to_willexecutor = orig_push
|
||||||
|
|
||||||
# ---- 2b) global deadline: a hung server must not block past `deadline` ----
|
# ---- 2b) global deadline: a hung server must not block past `deadline` ----
|
||||||
def hanging_push(we, **kwargs):
|
def hanging_push(we, **kwargs):
|
||||||
@@ -129,8 +129,8 @@ def main():
|
|||||||
time.sleep(10)
|
time.sleep(10)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
orig_push2 = W.push_transactions_to_willexecutor
|
orig_push2 = we_cls.push_transactions_to_willexecutor
|
||||||
W.push_transactions_to_willexecutor = staticmethod(hanging_push)
|
we_cls.push_transactions_to_willexecutor = staticmethod(hanging_push)
|
||||||
try:
|
try:
|
||||||
wes = {
|
wes = {
|
||||||
"https://fast.example": {
|
"https://fast.example": {
|
||||||
@@ -146,7 +146,7 @@ def main():
|
|||||||
return True
|
return True
|
||||||
time.sleep(10)
|
time.sleep(10)
|
||||||
return True
|
return True
|
||||||
W.push_transactions_to_willexecutor = staticmethod(fast_or_hang)
|
we_cls.push_transactions_to_willexecutor = staticmethod(fast_or_hang)
|
||||||
|
|
||||||
timed_out = []
|
timed_out = []
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@ def main():
|
|||||||
timed_out.append(url)
|
timed_out.append(url)
|
||||||
|
|
||||||
start = time.time()
|
start = time.time()
|
||||||
W.push_transactions_parallel(
|
we_cls.push_transactions_parallel(
|
||||||
wes, max_workers=2, deadline=1.0, on_timeout=on_timeout
|
wes, max_workers=2, deadline=1.0, on_timeout=on_timeout
|
||||||
)
|
)
|
||||||
elapsed = time.time() - start
|
elapsed = time.time() - start
|
||||||
@@ -163,7 +163,7 @@ def main():
|
|||||||
print(f"[OK] global deadline enforced: returned in {elapsed:.1f}s, "
|
print(f"[OK] global deadline enforced: returned in {elapsed:.1f}s, "
|
||||||
f"hung server reported via on_timeout")
|
f"hung server reported via on_timeout")
|
||||||
finally:
|
finally:
|
||||||
W.push_transactions_to_willexecutor = orig_push2
|
we_cls.push_transactions_to_willexecutor = orig_push2
|
||||||
|
|
||||||
# ---- 2c) on_tick is fired periodically from the CALLING thread ----
|
# ---- 2c) on_tick is fired periodically from the CALLING thread ----
|
||||||
# The elapsed-time counter is driven by an on_tick callback called from the
|
# The elapsed-time counter is driven by an on_tick callback called from the
|
||||||
@@ -175,8 +175,8 @@ def main():
|
|||||||
time.sleep(SLOW * 6) # ~3s, long enough for several ticks
|
time.sleep(SLOW * 6) # ~3s, long enough for several ticks
|
||||||
return True
|
return True
|
||||||
|
|
||||||
orig_push3 = W.push_transactions_to_willexecutor
|
orig_push3 = we_cls.push_transactions_to_willexecutor
|
||||||
W.push_transactions_to_willexecutor = staticmethod(slow_push2)
|
we_cls.push_transactions_to_willexecutor = staticmethod(slow_push2)
|
||||||
try:
|
try:
|
||||||
wes = {
|
wes = {
|
||||||
"https://tick.example": {
|
"https://tick.example": {
|
||||||
@@ -191,7 +191,7 @@ def main():
|
|||||||
ticks.append(time.time())
|
ticks.append(time.time())
|
||||||
tick_threads.add(threading.current_thread())
|
tick_threads.add(threading.current_thread())
|
||||||
|
|
||||||
W.push_transactions_parallel(
|
we_cls.push_transactions_parallel(
|
||||||
wes, max_workers=1, on_tick=on_tick, tick_interval=0.5
|
wes, max_workers=1, on_tick=on_tick, tick_interval=0.5
|
||||||
)
|
)
|
||||||
# ~3s push with 0.5s ticks => at least a few ticks.
|
# ~3s push with 0.5s ticks => at least a few ticks.
|
||||||
@@ -202,7 +202,7 @@ def main():
|
|||||||
)
|
)
|
||||||
print(f"[OK] on_tick fired {len(ticks)} times from the calling thread")
|
print(f"[OK] on_tick fired {len(ticks)} times from the calling thread")
|
||||||
finally:
|
finally:
|
||||||
W.push_transactions_to_willexecutor = orig_push3
|
we_cls.push_transactions_to_willexecutor = orig_push3
|
||||||
|
|
||||||
# ---- 2d) check_transactions_parallel: parallel + deadline + on_tick ----
|
# ---- 2d) check_transactions_parallel: parallel + deadline + on_tick ----
|
||||||
# Pressing "Check" verifies each will-executor still holds its tx. This used
|
# Pressing "Check" verifies each will-executor still holds its tx. This used
|
||||||
@@ -214,8 +214,8 @@ def main():
|
|||||||
time.sleep(SLOW)
|
time.sleep(SLOW)
|
||||||
return {"tx": "ok"} if "good" in url else None
|
return {"tx": "ok"} if "good" in url else None
|
||||||
|
|
||||||
orig_check = W.check_transaction
|
orig_check = we_cls.check_transaction
|
||||||
W.check_transaction = staticmethod(slow_check)
|
we_cls.check_transaction = staticmethod(slow_check)
|
||||||
try:
|
try:
|
||||||
targets = []
|
targets = []
|
||||||
for i in range(N):
|
for i in range(N):
|
||||||
@@ -228,7 +228,7 @@ def main():
|
|||||||
checked.append((wid, res))
|
checked.append((wid, res))
|
||||||
|
|
||||||
start = time.time()
|
start = time.time()
|
||||||
results = W.check_transactions_parallel(
|
results = we_cls.check_transactions_parallel(
|
||||||
targets, on_each=on_each_check, max_workers=N
|
targets, on_each=on_each_check, max_workers=N
|
||||||
)
|
)
|
||||||
elapsed = time.time() - start
|
elapsed = time.time() - start
|
||||||
@@ -239,7 +239,7 @@ def main():
|
|||||||
print(f"[OK] check parallel: {elapsed:.2f}s for {N} servers "
|
print(f"[OK] check parallel: {elapsed:.2f}s for {N} servers "
|
||||||
f"(sequential would be ~{sequential:.2f}s)")
|
f"(sequential would be ~{sequential:.2f}s)")
|
||||||
finally:
|
finally:
|
||||||
W.check_transaction = orig_check
|
we_cls.check_transaction = orig_check
|
||||||
|
|
||||||
# 2d-bis) global deadline + on_tick from the calling thread
|
# 2d-bis) global deadline + on_tick from the calling thread
|
||||||
def hanging_check(txid, url, **kwargs):
|
def hanging_check(txid, url, **kwargs):
|
||||||
@@ -248,8 +248,8 @@ def main():
|
|||||||
time.sleep(10)
|
time.sleep(10)
|
||||||
return {"tx": "ok"}
|
return {"tx": "ok"}
|
||||||
|
|
||||||
orig_check2 = W.check_transaction
|
orig_check2 = we_cls.check_transaction
|
||||||
W.check_transaction = staticmethod(hanging_check)
|
we_cls.check_transaction = staticmethod(hanging_check)
|
||||||
try:
|
try:
|
||||||
targets = [
|
targets = [
|
||||||
("idf", "https://fast.example"),
|
("idf", "https://fast.example"),
|
||||||
@@ -268,7 +268,7 @@ def main():
|
|||||||
tick_threads.add(threading.current_thread())
|
tick_threads.add(threading.current_thread())
|
||||||
|
|
||||||
start = time.time()
|
start = time.time()
|
||||||
W.check_transactions_parallel(
|
we_cls.check_transactions_parallel(
|
||||||
targets, max_workers=2, deadline=2.0,
|
targets, max_workers=2, deadline=2.0,
|
||||||
on_timeout=on_timeout_check, on_tick=on_tick_check,
|
on_timeout=on_timeout_check, on_tick=on_tick_check,
|
||||||
tick_interval=0.5,
|
tick_interval=0.5,
|
||||||
@@ -282,7 +282,7 @@ def main():
|
|||||||
print(f"[OK] check global deadline enforced ({elapsed:.1f}s), on_tick "
|
print(f"[OK] check global deadline enforced ({elapsed:.1f}s), on_tick "
|
||||||
f"fired {len(ticks)}x from the calling thread")
|
f"fired {len(ticks)}x from the calling thread")
|
||||||
finally:
|
finally:
|
||||||
W.check_transaction = orig_check2
|
we_cls.check_transaction = orig_check2
|
||||||
|
|
||||||
# ---- 3) the wizard's loop_push must use the parallel helper ----
|
# ---- 3) the wizard's loop_push must use the parallel helper ----
|
||||||
# The "Building Will" wizard broadcasts via BalBuildWillDialog.loop_push.
|
# The "Building Will" wizard broadcasts via BalBuildWillDialog.loop_push.
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import sys
|
|||||||
|
|
||||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
from PyQt6.QtWidgets import QApplication, QLabel, QWidget, QVBoxLayout
|
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
|
from PyQt6.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget
|
||||||
|
|
||||||
# Same colors as BalBuildWillDialog
|
# Same colors as BalBuildWillDialog
|
||||||
COLOR_WARNING = "#cfa808"
|
COLOR_WARNING = "#cfa808"
|
||||||
|
|||||||
@@ -17,10 +17,15 @@ import os
|
|||||||
|
|
||||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
from PyQt6.QtWidgets import ( # noqa: E402
|
|
||||||
QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
|
||||||
)
|
|
||||||
from PyQt6.QtCore import Qt # noqa: E402
|
from PyQt6.QtCore import Qt # noqa: E402
|
||||||
|
from PyQt6.QtWidgets import ( # noqa: E402
|
||||||
|
QApplication,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QPushButton,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
COLOR_OK = "#05ad05"
|
COLOR_OK = "#05ad05"
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import sys
|
|||||||
|
|
||||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
from PyQt6.QtWidgets import QApplication, QLabel, QWidget, QVBoxLayout
|
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
|
from PyQt6.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget
|
||||||
|
|
||||||
COLOR_ERROR = "#ff0000"
|
COLOR_ERROR = "#ff0000"
|
||||||
COLOR_OK = "#05ad05"
|
COLOR_OK = "#05ad05"
|
||||||
|
|||||||
@@ -20,12 +20,17 @@ import os
|
|||||||
|
|
||||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
from PyQt6.QtWidgets import ( # noqa: E402
|
from PyQt6.QtCore import QSize # noqa: E402
|
||||||
QApplication, QWidget, QHBoxLayout, QPushButton, QComboBox, QLineEdit,
|
|
||||||
QLabel,
|
|
||||||
)
|
|
||||||
from PyQt6.QtGui import QIcon, QPixmap # noqa: E402
|
from PyQt6.QtGui import QIcon, QPixmap # noqa: E402
|
||||||
from PyQt6.QtCore import QSize, Qt # noqa: E402
|
from PyQt6.QtWidgets import ( # noqa: E402
|
||||||
|
QApplication,
|
||||||
|
QComboBox,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
|
QPushButton,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
ICON_PATH = os.path.join(os.path.dirname(__file__), "..", "bal", "icons",
|
ICON_PATH = os.path.join(os.path.dirname(__file__), "..", "bal", "icons",
|
||||||
"wizard.png")
|
"wizard.png")
|
||||||
|
|||||||
@@ -27,12 +27,19 @@ import os
|
|||||||
|
|
||||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
from PyQt6.QtWidgets import ( # noqa: E402
|
|
||||||
QApplication, QWidget, QVBoxLayout, QHBoxLayout, QToolButton, QComboBox,
|
|
||||||
QLineEdit, QSpinBox, QLabel,
|
|
||||||
)
|
|
||||||
from PyQt6.QtGui import QFontMetrics # noqa: E402
|
|
||||||
from PyQt6.QtCore import Qt # noqa: E402
|
from PyQt6.QtCore import Qt # noqa: E402
|
||||||
|
from PyQt6.QtGui import QFontMetrics # noqa: E402
|
||||||
|
from PyQt6.QtWidgets import ( # noqa: E402
|
||||||
|
QApplication,
|
||||||
|
QComboBox,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
|
QSpinBox,
|
||||||
|
QToolButton,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _char_w():
|
def _char_w():
|
||||||
|
|||||||
@@ -21,18 +21,21 @@ Run:
|
|||||||
QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src python3 tests/sim_update_flows.py
|
QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src python3 tests/sim_update_flows.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import copy
|
import copy
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||||
|
|
||||||
from bal.core.will import (
|
from bal.core.will import (
|
||||||
WillItem, Will,
|
HeirNotFoundException,
|
||||||
NotCompleteWillException, HeirNotFoundException, NoHeirsException,
|
NoHeirsException,
|
||||||
TxFeesChangedException, WillExpiredException,
|
NotCompleteWillException,
|
||||||
|
TxFeesChangedException,
|
||||||
|
Will,
|
||||||
|
WillExpiredException,
|
||||||
|
WillItem,
|
||||||
)
|
)
|
||||||
from bal.core.util import Util
|
|
||||||
|
|
||||||
# A valid serialized tx (1 input + 1 output, version 2). Its nLockTime is 0.
|
# A valid serialized tx (1 input + 1 output, version 2). Its nLockTime is 0.
|
||||||
_VALID_TX_HEX = (
|
_VALID_TX_HEX = (
|
||||||
|
|||||||
@@ -26,30 +26,28 @@ def main():
|
|||||||
from PyQt6.QtWidgets import QApplication # noqa
|
from PyQt6.QtWidgets import QApplication # noqa
|
||||||
_app = QApplication.instance() or QApplication([])
|
_app = QApplication.instance() or QApplication([])
|
||||||
|
|
||||||
results = {}
|
|
||||||
|
|
||||||
# 1) Core modules import (these must be GUI-free).
|
# 1) Core modules import (these must be GUI-free).
|
||||||
bal = imp_core("bal", "core.plugin_base")
|
bal = imp_core("bal", "core.plugin_base")
|
||||||
util = imp_core("util", "core.util")
|
util = imp_core("util", "core.util")
|
||||||
heirs = imp_core("heirs", "core.heirs")
|
heirs = imp_core("heirs", "core.heirs")
|
||||||
will = imp_core("will", "core.will")
|
will = imp_core("will", "core.will")
|
||||||
we = imp_core("willexecutors", "core.willexecutors")
|
_we = imp_core("willexecutors", "core.willexecutors")
|
||||||
|
|
||||||
# 2) GUI module imports.
|
# 2) GUI module imports.
|
||||||
qt = imp_gui()
|
qt = imp_gui()
|
||||||
|
|
||||||
# 3) Behaviour checks (pure logic, must be identical across versions).
|
# 3) Behaviour checks (pure logic, must be identical across versions).
|
||||||
BalTimestamp = bal.BalTimestamp
|
bal_timestamp = bal.BalTimestamp
|
||||||
assert BalTimestamp("30d").duration_to_days() == 30, "BalTimestamp 30d"
|
assert bal_timestamp("30d").duration_to_days() == 30, "BalTimestamp 30d"
|
||||||
assert BalTimestamp("1y").duration_to_days() == 365, "BalTimestamp 1y"
|
assert bal_timestamp("1y").duration_to_days() == 365, "BalTimestamp 1y"
|
||||||
assert str(BalTimestamp("7d")) == "7d", "BalTimestamp str"
|
assert str(bal_timestamp("7d")) == "7d", "BalTimestamp str"
|
||||||
|
|
||||||
Util = util.Util
|
util_cls = util.Util
|
||||||
assert Util.is_perc("50%") is True
|
assert util_cls.is_perc("50%") is True
|
||||||
assert Util.is_perc("100") is False
|
assert util_cls.is_perc("100") is False
|
||||||
assert Util.text_to_hex("BAL") == "42414c"
|
assert util_cls.text_to_hex("BAL") == "42414c"
|
||||||
assert Util.hex_to_text("42414c") == "BAL"
|
assert util_cls.hex_to_text("42414c") == "BAL"
|
||||||
assert Util.int_locktime(days=1) == 86400
|
assert util_cls.int_locktime(days=1) == 86400
|
||||||
|
|
||||||
# heirs constants must keep the same column layout (very delicate!)
|
# heirs constants must keep the same column layout (very delicate!)
|
||||||
assert heirs.HEIR_ADDRESS == 0
|
assert heirs.HEIR_ADDRESS == 0
|
||||||
|
|||||||
@@ -27,19 +27,19 @@ Run:
|
|||||||
tests/test_anticipate_manual_locktime.py -q
|
tests/test_anticipate_manual_locktime.py -q
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import copy
|
import copy
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||||
|
|
||||||
import pytest # noqa: E402
|
import pytest # noqa: E402
|
||||||
|
|
||||||
from bal.core.will import ( # noqa: E402
|
from bal.core.will import ( # noqa: E402
|
||||||
WillItem,
|
|
||||||
Will,
|
|
||||||
NotCompleteWillException,
|
NotCompleteWillException,
|
||||||
|
Will,
|
||||||
WillExpiredException,
|
WillExpiredException,
|
||||||
|
WillItem,
|
||||||
)
|
)
|
||||||
|
|
||||||
# A valid serialized tx (1 input + 1 output, version 2).
|
# A valid serialized tx (1 input + 1 output, version 2).
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ whether a fix is needed. Run:
|
|||||||
python3 -m pytest tests/test_anticipate_past_locktime.py -q
|
python3 -m pytest tests/test_anticipate_past_locktime.py -q
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||||
|
|
||||||
from bal.core.util import Util, LOCKTIME_THRESHOLD
|
from bal.core.util import LOCKTIME_THRESHOLD, Util
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -9,25 +9,34 @@ Run:
|
|||||||
python3 tests/test_core_heirs.py
|
python3 tests/test_core_heirs.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||||
|
|
||||||
from bal.core.heirs import (
|
from bal.core.heirs import (
|
||||||
HEIR_ADDRESS, HEIR_AMOUNT, HEIR_LOCKTIME, HEIR_REAL_AMOUNT,
|
HEIR_ADDRESS,
|
||||||
HEIR_DUST_AMOUNT, TRANSACTION_LABEL,
|
HEIR_AMOUNT,
|
||||||
|
HEIR_DUST_AMOUNT,
|
||||||
|
HEIR_LOCKTIME,
|
||||||
|
HEIR_REAL_AMOUNT,
|
||||||
OP_RETURN_PREFIX,
|
OP_RETURN_PREFIX,
|
||||||
create_op_return_script,
|
TRANSACTION_LABEL,
|
||||||
is_op_return_address, get_op_return_hex, validate_op_return_hex,
|
|
||||||
AliasNotFoundException,
|
AliasNotFoundException,
|
||||||
NotAnAddress, AmountNotValid, LocktimeNotValid,
|
AmountNotValid,
|
||||||
HeirExpiredException, HeirAmountIsDustException,
|
|
||||||
NoHeirsException, WillExecutorFeeException,
|
|
||||||
BalanceTooLowException,
|
BalanceTooLowException,
|
||||||
|
HeirAmountIsDustException,
|
||||||
Heirs,
|
Heirs,
|
||||||
|
LocktimeNotValid,
|
||||||
|
NoHeirsException,
|
||||||
|
NotAnAddress,
|
||||||
|
WillExecutorFeeException,
|
||||||
|
create_op_return_script,
|
||||||
|
get_op_return_hex,
|
||||||
|
is_op_return_address,
|
||||||
|
validate_op_return_hex,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Constants
|
# Constants
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
@@ -70,7 +79,7 @@ def test_op_return_empty():
|
|||||||
def test_op_return_too_big():
|
def test_op_return_too_big():
|
||||||
try:
|
try:
|
||||||
create_op_return_script("ab" * 81) # 81 bytes > max 80
|
create_op_return_script("ab" * 81) # 81 bytes > max 80
|
||||||
assert False, "expected ValueError"
|
raise AssertionError("expected ValueError")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -179,13 +188,13 @@ def test_validate_amount():
|
|||||||
# Invalid
|
# Invalid
|
||||||
try:
|
try:
|
||||||
Heirs.validate_amount("0.000000001")
|
Heirs.validate_amount("0.000000001")
|
||||||
assert False, "expected AmountNotValid"
|
raise AssertionError("expected AmountNotValid")
|
||||||
except AmountNotValid:
|
except AmountNotValid:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
Heirs.validate_amount("-1")
|
Heirs.validate_amount("-1")
|
||||||
assert False, "expected AmountNotValid"
|
raise AssertionError("expected AmountNotValid")
|
||||||
except AmountNotValid:
|
except AmountNotValid:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -209,7 +218,7 @@ def test_validate_locktime_expired():
|
|||||||
past = int(time.time()) - 86400 # yesterday
|
past = int(time.time()) - 86400 # yesterday
|
||||||
try:
|
try:
|
||||||
Heirs.validate_locktime(str(past), timestamp_to_check=past + 1)
|
Heirs.validate_locktime(str(past), timestamp_to_check=past + 1)
|
||||||
assert False, "expected LocktimeNotValid"
|
raise AssertionError("expected LocktimeNotValid")
|
||||||
except LocktimeNotValid:
|
except LocktimeNotValid:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -289,7 +298,7 @@ def test_validate_op_return_hex_valid():
|
|||||||
def test_validate_op_return_hex_invalid():
|
def test_validate_op_return_hex_invalid():
|
||||||
try:
|
try:
|
||||||
validate_op_return_hex("nothex!!")
|
validate_op_return_hex("nothex!!")
|
||||||
assert False, "expected NotAnAddress"
|
raise AssertionError("expected NotAnAddress")
|
||||||
except NotAnAddress:
|
except NotAnAddress:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -297,7 +306,7 @@ def test_validate_op_return_hex_invalid():
|
|||||||
def test_validate_op_return_hex_too_long():
|
def test_validate_op_return_hex_too_long():
|
||||||
try:
|
try:
|
||||||
validate_op_return_hex("ab" * 81)
|
validate_op_return_hex("ab" * 81)
|
||||||
assert False, "expected NotAnAddress"
|
raise AssertionError("expected NotAnAddress")
|
||||||
except NotAnAddress:
|
except NotAnAddress:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -355,4 +364,4 @@ if __name__ == "__main__":
|
|||||||
if name.startswith("test_"):
|
if name.startswith("test_"):
|
||||||
globals()[name]()
|
globals()[name]()
|
||||||
print(f" [OK] {name}")
|
print(f" [OK] {name}")
|
||||||
print(f"[OK] All heirs tests passed")
|
print("[OK] All heirs tests passed")
|
||||||
|
|||||||
@@ -8,19 +8,20 @@ Run:
|
|||||||
QT_QPA_PLATFORM=offscreen python3 tests/test_core_heirs_extra.py
|
QT_QPA_PLATFORM=offscreen python3 tests/test_core_heirs_extra.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||||
|
|
||||||
from bal.core.heirs import (
|
from bal.core.heirs import (
|
||||||
Heirs, create_op_return_script, reduce_outputs,
|
HEIR_AMOUNT,
|
||||||
HEIR_ADDRESS, HEIR_AMOUNT, HEIR_LOCKTIME, HEIR_REAL_AMOUNT,
|
Heirs,
|
||||||
|
create_op_return_script,
|
||||||
|
reduce_outputs,
|
||||||
)
|
)
|
||||||
from bal.core.willexecutors import Willexecutors
|
from bal.core.willexecutors import Willexecutors
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Heirs db-dependent methods
|
# Heirs db-dependent methods
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
@@ -188,7 +189,7 @@ def test_validate_address_invalid():
|
|||||||
from bal.core.heirs import NotAnAddress
|
from bal.core.heirs import NotAnAddress
|
||||||
try:
|
try:
|
||||||
Heirs.validate_address("bad")
|
Heirs.validate_address("bad")
|
||||||
assert False, "should have raised"
|
raise AssertionError("should have raised")
|
||||||
except NotAnAddress:
|
except NotAnAddress:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,15 @@ Run:
|
|||||||
python3 tests/test_core_plugin_base.py
|
python3 tests/test_core_plugin_base.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||||
|
|
||||||
from datetime import datetime, date, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from bal.core.plugin_base import BalTimestamp, BalPlugin, BalConfig
|
|
||||||
|
|
||||||
|
from bal.core.plugin_base import BalConfig, BalPlugin, BalTimestamp
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# BalTimestamp
|
# BalTimestamp
|
||||||
|
|||||||
@@ -9,13 +9,14 @@ Run:
|
|||||||
python3 tests/test_core_util.py
|
python3 tests/test_core_util.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from bal.core.util import Util, LOCKTIME_THRESHOLD
|
from bal.core.util import Util
|
||||||
|
|
||||||
|
|
||||||
def test_locktime_to_str():
|
def test_locktime_to_str():
|
||||||
@@ -40,7 +41,7 @@ def test_str_to_locktime():
|
|||||||
|
|
||||||
# the block-height suffix "b" was removed (A1): "144b" is no longer a valid
|
# the block-height suffix "b" was removed (A1): "144b" is no longer a valid
|
||||||
# relative locktime, so it is NOT passed through unchanged.
|
# relative locktime, so it is NOT passed through unchanged.
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(ValueError):
|
||||||
Util.str_to_locktime("144b")
|
Util.str_to_locktime("144b")
|
||||||
|
|
||||||
# integer string -> int
|
# integer string -> int
|
||||||
@@ -347,44 +348,44 @@ def test_in_utxo():
|
|||||||
|
|
||||||
|
|
||||||
def test_cmp_output():
|
def test_cmp_output():
|
||||||
class O:
|
class Obj:
|
||||||
def __init__(self, addr, val):
|
def __init__(self, addr, val):
|
||||||
self.address = addr
|
self.address = addr
|
||||||
self.value = val
|
self.value = val
|
||||||
assert Util.cmp_output(O("a", 100), O("a", 100)) is True
|
assert Util.cmp_output(Obj("a", 100), Obj("a", 100)) is True
|
||||||
assert Util.cmp_output(O("a", 100), O("b", 100)) is False
|
assert Util.cmp_output(Obj("a", 100), Obj("b", 100)) is False
|
||||||
assert Util.cmp_output(O("a", 100), O("a", 200)) is False
|
assert Util.cmp_output(Obj("a", 100), Obj("a", 200)) is False
|
||||||
|
|
||||||
|
|
||||||
def test_in_output():
|
def test_in_output():
|
||||||
class O:
|
class Obj:
|
||||||
def __init__(self, addr, val):
|
def __init__(self, addr, val):
|
||||||
self.address = addr
|
self.address = addr
|
||||||
self.value = val
|
self.value = val
|
||||||
outputs = [O("a", 100), O("b", 200)]
|
outputs = [Obj("a", 100), Obj("b", 200)]
|
||||||
assert Util.in_output(O("a", 100), outputs) is True
|
assert Util.in_output(Obj("a", 100), outputs) is True
|
||||||
assert Util.in_output(O("z", 999), outputs) is False
|
assert Util.in_output(Obj("z", 999), outputs) is False
|
||||||
assert Util.in_output(O("a", 100), []) is False
|
assert Util.in_output(Obj("a", 100), []) is False
|
||||||
|
|
||||||
|
|
||||||
def test_din_output():
|
def test_din_output():
|
||||||
class O:
|
class Obj:
|
||||||
def __init__(self, addr, val):
|
def __init__(self, addr, val):
|
||||||
self.address = addr
|
self.address = addr
|
||||||
self.value = val
|
self.value = val
|
||||||
|
|
||||||
outputs = [O("a", 100), O("b", 200)]
|
outputs = [Obj("a", 100), Obj("b", 200)]
|
||||||
|
|
||||||
# same amount AND same address
|
# same amount AND same address
|
||||||
same_amt, same_addr = Util.din_output(O("a", 100), outputs)
|
same_amt, same_addr = Util.din_output(Obj("a", 100), outputs)
|
||||||
assert same_amt is True and same_addr is True
|
assert same_amt is True and same_addr is True
|
||||||
|
|
||||||
# same amount but different address
|
# same amount but different address
|
||||||
same_amt, same_addr = Util.din_output(O("c", 100), outputs)
|
same_amt, same_addr = Util.din_output(Obj("c", 100), outputs)
|
||||||
assert same_amt is True and same_addr is False
|
assert same_amt is True and same_addr is False
|
||||||
|
|
||||||
# different amount
|
# different amount
|
||||||
same_amt, same_addr = Util.din_output(O("z", 999), outputs)
|
same_amt, same_addr = Util.din_output(Obj("z", 999), outputs)
|
||||||
assert same_amt is False and same_addr is False
|
assert same_amt is False and same_addr is False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ Run:
|
|||||||
python3 tests/test_core_will.py
|
python3 tests/test_core_will.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import copy
|
import copy
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||||
|
|
||||||
from bal.core.will import WillItem, Will
|
from bal.core.will import Will, WillItem
|
||||||
from bal.core.willexecutors import Willexecutors
|
|
||||||
|
|
||||||
# A valid serialized Bitcoin transaction hex (1 input + 1 P2PKH output, version 2)
|
# A valid serialized Bitcoin transaction hex (1 input + 1 P2PKH output, version 2)
|
||||||
_VALID_TX_HEX = (
|
_VALID_TX_HEX = (
|
||||||
@@ -332,11 +332,19 @@ def test_will_check_tx_height():
|
|||||||
|
|
||||||
def test_exceptions():
|
def test_exceptions():
|
||||||
from bal.core.will import (
|
from bal.core.will import (
|
||||||
WillException, WillExpiredException, NotCompleteWillException,
|
AmountException,
|
||||||
HeirChangeException, TxFeesChangedException, HeirNotFoundException,
|
FixedAmountException,
|
||||||
WillexecutorChangeException, NoWillExecutorNotPresent,
|
HeirChangeException,
|
||||||
WillExecutorNotPresent, NoHeirsException,
|
HeirNotFoundException,
|
||||||
AmountException, PercAmountException, FixedAmountException,
|
NoHeirsException,
|
||||||
|
NotCompleteWillException,
|
||||||
|
NoWillExecutorNotPresent,
|
||||||
|
PercAmountException,
|
||||||
|
TxFeesChangedException,
|
||||||
|
WillException,
|
||||||
|
WillexecutorChangeException,
|
||||||
|
WillExecutorNotPresent,
|
||||||
|
WillExpiredException,
|
||||||
WillPostponedException,
|
WillPostponedException,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -375,4 +383,4 @@ if __name__ == "__main__":
|
|||||||
if name.startswith("test_"):
|
if name.startswith("test_"):
|
||||||
globals()[name]()
|
globals()[name]()
|
||||||
print(f" [OK] {name}")
|
print(f" [OK] {name}")
|
||||||
print(f"[OK] All Will tests passed")
|
print("[OK] All Will tests passed")
|
||||||
|
|||||||
@@ -8,15 +8,16 @@ Run:
|
|||||||
QT_QPA_PLATFORM=offscreen python3 tests/test_core_will_extra.py
|
QT_QPA_PLATFORM=offscreen python3 tests/test_core_will_extra.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
from unittest.mock import MagicMock, patch, PropertyMock, call
|
import sys
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||||
|
|
||||||
from bal.core.will import Will, WillItem
|
|
||||||
from electrum.transaction import Transaction
|
from electrum.transaction import Transaction
|
||||||
|
|
||||||
|
from bal.core.will import Will, WillItem
|
||||||
|
|
||||||
_VALID_TX_HEX = (
|
_VALID_TX_HEX = (
|
||||||
"01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65b"
|
"01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65b"
|
||||||
"f38633b424eb4031000000006c493046022100a82bbc57a0136751e543"
|
"f38633b424eb4031000000006c493046022100a82bbc57a0136751e543"
|
||||||
|
|||||||
@@ -22,15 +22,14 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||||
|
|
||||||
from bal.core.will import Will, WillItem
|
|
||||||
|
|
||||||
# Patch Transaction.add_info_from_wallet so WillItem can parse the tx hex
|
# Patch Transaction.add_info_from_wallet so WillItem can parse the tx hex
|
||||||
# without a live Electrum wallet connection.
|
# without a live Electrum wallet connection.
|
||||||
from electrum.transaction import Transaction
|
from electrum.transaction import Transaction
|
||||||
|
|
||||||
|
from bal.core.will import Will, WillItem
|
||||||
|
|
||||||
_patcher = patch.object(Transaction, "add_info_from_wallet")
|
_patcher = patch.object(Transaction, "add_info_from_wallet")
|
||||||
_patcher.start()
|
_patcher.start()
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
|||||||
from bal.core.plugin_base import BalConfig
|
from bal.core.plugin_base import BalConfig
|
||||||
from bal.core.willexecutors import Willexecutors
|
from bal.core.willexecutors import Willexecutors
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Mocks
|
# Mocks
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
|||||||
|
|
||||||
from bal.core.plugin_base import BalConfig
|
from bal.core.plugin_base import BalConfig
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Mocks
|
# Mocks
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
|||||||
from bal.core.plugin_base import BalConfig
|
from bal.core.plugin_base import BalConfig
|
||||||
from bal.gui.qt.widgets import compute_reminder_offsets
|
from bal.gui.qt.widgets import compute_reminder_offsets
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Mocks
|
# Mocks
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import copy
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -42,15 +41,12 @@ from electrum import bitcoin
|
|||||||
from electrum.transaction import (
|
from electrum.transaction import (
|
||||||
PartialTransaction,
|
PartialTransaction,
|
||||||
PartialTxInput,
|
PartialTxInput,
|
||||||
PartialTxOutput,
|
|
||||||
TxOutpoint,
|
TxOutpoint,
|
||||||
)
|
)
|
||||||
from electrum.util import bfh
|
from electrum.util import bfh
|
||||||
|
|
||||||
from bal.core.heirs import Heirs
|
from bal.core.heirs import Heirs
|
||||||
from bal.core.will import Will, WillItem
|
from bal.core.will import Will, WillItem
|
||||||
from bal.core.willexecutors import Willexecutors
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Load karen7 wallet data
|
# Load karen7 wallet data
|
||||||
@@ -166,7 +162,7 @@ def _build_utxo_value_map(data):
|
|||||||
for txid, outputs in txo.items():
|
for txid, outputs in txo.items():
|
||||||
if not isinstance(outputs, dict):
|
if not isinstance(outputs, dict):
|
||||||
continue
|
continue
|
||||||
for addr, out_map in outputs.items():
|
for _, out_map in outputs.items():
|
||||||
if not isinstance(out_map, dict):
|
if not isinstance(out_map, dict):
|
||||||
continue
|
continue
|
||||||
for idx, info in out_map.items():
|
for idx, info in out_map.items():
|
||||||
@@ -188,7 +184,7 @@ def _populate_input_values(will, utxo_value_map):
|
|||||||
This is the equivalent of what ``add_info_from_wallet`` does in the
|
This is the equivalent of what ``add_info_from_wallet`` does in the
|
||||||
real flow: looking up the UTXO value and attaching it to the input.
|
real flow: looking up the UTXO value and attaching it to the input.
|
||||||
"""
|
"""
|
||||||
for wid, wi in will.items():
|
for _, wi in will.items():
|
||||||
for txin in wi.tx.inputs():
|
for txin in wi.tx.inputs():
|
||||||
prevout_str = txin.prevout.to_str()
|
prevout_str = txin.prevout.to_str()
|
||||||
if txin._trusted_value_sats is None and prevout_str in utxo_value_map:
|
if txin._trusted_value_sats is None and prevout_str in utxo_value_map:
|
||||||
|
|||||||
@@ -36,13 +36,12 @@ import pytest
|
|||||||
# below the repo root that contains the ``bal`` package).
|
# below the repo root that contains the ``bal`` package).
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||||
|
|
||||||
from bal.core.will import WillItem, Will, HeirNotFoundException
|
|
||||||
from bal.core.heirs import Heirs
|
from bal.core.heirs import Heirs
|
||||||
|
from bal.core.will import HeirNotFoundException, Will, WillItem
|
||||||
from bal.core.willexecutors import Willexecutors
|
from bal.core.willexecutors import Willexecutors
|
||||||
from bal.gui.qt.calendar import BalCalendar
|
from bal.gui.qt.calendar import BalCalendar
|
||||||
from bal.gui.qt.widgets import compute_reminder_offsets
|
from bal.gui.qt.widgets import compute_reminder_offsets
|
||||||
|
|
||||||
|
|
||||||
# A valid serialized Bitcoin transaction hex (1 input + 1 P2PKH output,
|
# A valid serialized Bitcoin transaction hex (1 input + 1 P2PKH output,
|
||||||
# version 2). Reused from test_core_will.py so WillItem can parse a real tx.
|
# version 2). Reused from test_core_will.py so WillItem can parse a real tx.
|
||||||
_VALID_TX_HEX = (
|
_VALID_TX_HEX = (
|
||||||
@@ -550,7 +549,7 @@ def test_e5_build_with_real_wallet_heirs_and_utxos():
|
|||||||
for txid, outputs in txo.items():
|
for txid, outputs in txo.items():
|
||||||
if not isinstance(outputs, dict):
|
if not isinstance(outputs, dict):
|
||||||
continue
|
continue
|
||||||
for addr, out_map in outputs.items():
|
for _, out_map in outputs.items():
|
||||||
if not isinstance(out_map, dict):
|
if not isinstance(out_map, dict):
|
||||||
continue
|
continue
|
||||||
for idx, info in out_map.items():
|
for idx, info in out_map.items():
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ Run:
|
|||||||
python3 tests/test_group_f_heir_change_rebuild.py
|
python3 tests/test_group_f_heir_change_rebuild.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||||
|
|
||||||
from bal.core.will import Will
|
from bal.core.will import Will
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ import sys
|
|||||||
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||||
|
|
||||||
from bal.gui.qt.widgets import (BASIC_REMINDER_OFFSETS,
|
from bal.gui.qt.widgets import BASIC_REMINDER_OFFSETS, basic_reminder_offsets
|
||||||
basic_reminder_offsets)
|
|
||||||
|
|
||||||
|
|
||||||
def test_basic_offsets_all_future():
|
def test_basic_offsets_all_future():
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
|||||||
|
|
||||||
from bal.gui.qt.window import BalWindow # noqa: E402 (path insert above)
|
from bal.gui.qt.window import BalWindow # noqa: E402 (path insert above)
|
||||||
|
|
||||||
|
|
||||||
OFFSET = BalWindow.BASIC_MODE_CHECK_ALIVE_OFFSET_SECONDS
|
OFFSET = BalWindow.BASIC_MODE_CHECK_ALIVE_OFFSET_SECONDS
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,12 @@ Run:
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
sys.path.insert(0, __file__.rsplit("/", 2)[0])
|
sys.path.insert(0, __file__.rsplit("/", 2)[0])
|
||||||
|
|
||||||
from bal.gui.qt.calendar import BalCalendar
|
from bal.gui.qt.calendar import BalCalendar
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# format_time
|
# format_time
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ Run:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, __file__.rsplit("/", 2)[0])
|
sys.path.insert(0, __file__.rsplit("/", 2)[0])
|
||||||
|
|
||||||
from PyQt6.QtWidgets import QApplication, QGridLayout, QLabel, QWidget
|
from PyQt6.QtWidgets import QApplication, QGridLayout, QLabel
|
||||||
|
|
||||||
# Import the module itself, not via "from .common import *"
|
# Import the module itself, not via "from .common import *"
|
||||||
import bal.gui.qt.common as C
|
import bal.gui.qt.common as common
|
||||||
|
|
||||||
_app = QApplication.instance() or QApplication(sys.argv)
|
_app = QApplication.instance() or QApplication(sys.argv)
|
||||||
|
|
||||||
@@ -23,18 +24,18 @@ _app = QApplication.instance() or QApplication(sys.argv)
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
def test_shown_cv_default():
|
def test_shown_cv_default():
|
||||||
cv = C.shown_cv(True)
|
cv = common.shown_cv(True)
|
||||||
assert cv.get() is True
|
assert cv.get() is True
|
||||||
|
|
||||||
|
|
||||||
def test_shown_cv_set():
|
def test_shown_cv_set():
|
||||||
cv = C.shown_cv(True)
|
cv = common.shown_cv(True)
|
||||||
cv.set(False)
|
cv.set(False)
|
||||||
assert cv.get() is False
|
assert cv.get() is False
|
||||||
|
|
||||||
|
|
||||||
def test_shown_cv_roundtrip():
|
def test_shown_cv_roundtrip():
|
||||||
cv = C.shown_cv(False)
|
cv = common.shown_cv(False)
|
||||||
assert cv.get() is False
|
assert cv.get() is False
|
||||||
cv.set(True)
|
cv.set(True)
|
||||||
assert cv.get() is True
|
assert cv.get() is True
|
||||||
@@ -47,19 +48,19 @@ def test_shown_cv_roundtrip():
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
def test_check_alive_error_default():
|
def test_check_alive_error_default():
|
||||||
err = C.CheckAliveError(1000000)
|
err = common.CheckAliveError(1000000)
|
||||||
assert err.timestamp_to_check == 1000000
|
assert err.timestamp_to_check == 1000000
|
||||||
|
|
||||||
|
|
||||||
def test_check_alive_error_str():
|
def test_check_alive_error_str():
|
||||||
err = C.CheckAliveError(1000000)
|
err = common.CheckAliveError(1000000)
|
||||||
s = str(err)
|
s = str(err)
|
||||||
assert "Check alive expired" in s
|
assert "Check alive expired" in s
|
||||||
assert "1970" in s
|
assert "1970" in s
|
||||||
|
|
||||||
|
|
||||||
def test_check_alive_error_subclass():
|
def test_check_alive_error_subclass():
|
||||||
assert issubclass(C.CheckAliveError, Exception)
|
assert issubclass(common.CheckAliveError, Exception)
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
@@ -68,17 +69,15 @@ def test_check_alive_error_subclass():
|
|||||||
|
|
||||||
def test_add_widget():
|
def test_add_widget():
|
||||||
grid = QGridLayout()
|
grid = QGridLayout()
|
||||||
parent = QWidget()
|
|
||||||
label = QLabel("test")
|
label = QLabel("test")
|
||||||
C.add_widget(grid, "Label", label, 0, "Help text")
|
common.add_widget(grid, "Label", label, 0, "Help text")
|
||||||
assert grid.count() == 3 # label + widget + help button
|
assert grid.count() == 3 # label + widget + help button
|
||||||
|
|
||||||
|
|
||||||
def test_add_widget_multiple_rows():
|
def test_add_widget_multiple_rows():
|
||||||
grid = QGridLayout()
|
grid = QGridLayout()
|
||||||
parent = QWidget()
|
common.add_widget(grid, "A", QLabel("a"), 0, "help_a")
|
||||||
C.add_widget(grid, "A", QLabel("a"), 0, "help_a")
|
common.add_widget(grid, "B", QLabel("b"), 1, "help_b")
|
||||||
C.add_widget(grid, "B", QLabel("b"), 1, "help_b")
|
|
||||||
assert grid.count() == 6
|
assert grid.count() == 6
|
||||||
|
|
||||||
|
|
||||||
@@ -87,7 +86,7 @@ def test_add_widget_multiple_rows():
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
def test_log_error_no_window():
|
def test_log_error_no_window():
|
||||||
C.log_error((Exception, Exception("test"), None))
|
common.log_error((Exception, Exception("test"), None))
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ Run:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, __file__.rsplit("/", 2)[0])
|
sys.path.insert(0, __file__.rsplit("/", 2)[0])
|
||||||
|
|
||||||
from bal.gui.qt.theme import status_color
|
from bal.gui.qt.theme import status_color
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Helpers
|
# Helpers
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|||||||
@@ -13,13 +13,11 @@ Run:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, __file__.rsplit("/", 2)[0])
|
sys.path.insert(0, __file__.rsplit("/", 2)[0])
|
||||||
|
|
||||||
from PyQt6.QtCore import QTimer
|
|
||||||
from PyQt6.QtWidgets import QApplication, QWidget
|
from PyQt6.QtWidgets import QApplication, QWidget
|
||||||
|
|
||||||
from electrum.util import DECIMAL_POINT, decimal_point_to_base_unit_name
|
|
||||||
|
|
||||||
_app = QApplication.instance() or QApplication(sys.argv)
|
_app = QApplication.instance() or QApplication(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,18 @@ Run:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, __file__.rsplit("/", 2)[0])
|
sys.path.insert(0, __file__.rsplit("/", 2)[0])
|
||||||
|
|
||||||
from PyQt6.QtCore import QTimer
|
from PyQt6.QtCore import QTimer
|
||||||
from PyQt6.QtWidgets import QApplication, QDialog, QWidget
|
from PyQt6.QtWidgets import QApplication, QDialog, QWidget
|
||||||
|
|
||||||
from bal.gui.qt.window_utils import (
|
from bal.gui.qt.window_utils import (
|
||||||
bring_to_front, show_modal, show_on_top, stop_thread, top_level_of,
|
bring_to_front,
|
||||||
|
show_modal,
|
||||||
|
show_on_top,
|
||||||
|
stop_thread,
|
||||||
|
top_level_of,
|
||||||
)
|
)
|
||||||
|
|
||||||
_app = QApplication.instance() or QApplication(sys.argv)
|
_app = QApplication.instance() or QApplication(sys.argv)
|
||||||
|
|||||||
@@ -33,10 +33,9 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from electrum import constants
|
from electrum import constants
|
||||||
|
|
||||||
constants.net = constants.BitcoinRegtest
|
constants.net = constants.BitcoinRegtest
|
||||||
@@ -48,10 +47,14 @@ from electrum.transaction import PartialTxInput, TxOutpoint
|
|||||||
from electrum.util import bfh
|
from electrum.util import bfh
|
||||||
|
|
||||||
from bal.core.heirs import Heirs
|
from bal.core.heirs import Heirs
|
||||||
from bal.core.will import Will, WillItem, NoWillExecutorNotPresent, NotCompleteWillException
|
from bal.core.will import (
|
||||||
|
NotCompleteWillException,
|
||||||
|
NoWillExecutorNotPresent,
|
||||||
|
Will,
|
||||||
|
WillItem,
|
||||||
|
)
|
||||||
from bal.core.willexecutors import Willexecutors
|
from bal.core.willexecutors import Willexecutors
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Load karen7 wallet data
|
# Load karen7 wallet data
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
@@ -479,11 +482,11 @@ class TestNoWillexecutorKaren7:
|
|||||||
dialog = FakeBuildWillDialog(self.bal_window)
|
dialog = FakeBuildWillDialog(self.bal_window)
|
||||||
dialog.task_phase1()
|
dialog.task_phase1()
|
||||||
assert any(
|
assert any(
|
||||||
"Not present - select one or enable backup mode" in l
|
"Not present - select one or enable backup mode" in label
|
||||||
for l in dialog.labels
|
for label in dialog.labels
|
||||||
), "dialog labels must contain the 'not present' message"
|
), "dialog labels must contain the 'not present' message"
|
||||||
assert any(
|
assert any(
|
||||||
"#ff0000" in l for l in dialog.labels
|
"#ff0000" in label for label in dialog.labels
|
||||||
), "dialog labels must use red (COLOR_ERROR)"
|
), "dialog labels must use red (COLOR_ERROR)"
|
||||||
|
|
||||||
def test_task_phase1_adds_action_buttons(self):
|
def test_task_phase1_adds_action_buttons(self):
|
||||||
|
|||||||
@@ -23,9 +23,15 @@ if os.path.isdir(ELECTRUM_DIR):
|
|||||||
sys.path.insert(0, ELECTRUM_DIR)
|
sys.path.insert(0, ELECTRUM_DIR)
|
||||||
|
|
||||||
from bal.core.heirs import Heirs
|
from bal.core.heirs import Heirs
|
||||||
from bal.core.willexecutors import Willexecutors
|
|
||||||
from bal.core.will import Will, WillItem, NotCompleteWillException, NoHeirsException, NoWillExecutorNotPresent
|
|
||||||
from bal.core.plugin_base import BalPlugin, BalTimestamp
|
from bal.core.plugin_base import BalPlugin, BalTimestamp
|
||||||
|
from bal.core.will import (
|
||||||
|
NoHeirsException,
|
||||||
|
NotCompleteWillException,
|
||||||
|
NoWillExecutorNotPresent,
|
||||||
|
Will,
|
||||||
|
WillItem,
|
||||||
|
)
|
||||||
|
from bal.core.willexecutors import Willexecutors
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Load karen7 wallet data
|
# Load karen7 wallet data
|
||||||
@@ -53,7 +59,7 @@ def build_utxos(data):
|
|||||||
for txid, outputs in txo.items():
|
for txid, outputs in txo.items():
|
||||||
if not isinstance(outputs, dict):
|
if not isinstance(outputs, dict):
|
||||||
continue
|
continue
|
||||||
for addr, out_map in outputs.items():
|
for _, out_map in outputs.items():
|
||||||
if not isinstance(out_map, dict):
|
if not isinstance(out_map, dict):
|
||||||
continue
|
continue
|
||||||
for idx, info in out_map.items():
|
for idx, info in out_map.items():
|
||||||
@@ -85,7 +91,6 @@ class FakeBalWindow:
|
|||||||
def init_class_variables(self):
|
def init_class_variables(self):
|
||||||
if not self.heirs:
|
if not self.heirs:
|
||||||
raise NoHeirsException("Heirs are not defined")
|
raise NoHeirsException("Heirs are not defined")
|
||||||
from bal.core.plugin_base import BalTimestamp
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
self.date_to_check = BalTimestamp(self.will_settings['threshold']).to_timestamp()
|
self.date_to_check = BalTimestamp(self.will_settings['threshold']).to_timestamp()
|
||||||
self.no_willexecutor = self.bal_plugin.NO_WILLEXECUTOR.get()
|
self.no_willexecutor = self.bal_plugin.NO_WILLEXECUTOR.get()
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ class _WindowsLikeDatetime(_real_datetime):
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
plugin_base = importlib.import_module(f"{PKG}.core.plugin_base")
|
plugin_base = importlib.import_module(f"{PKG}.core.plugin_base")
|
||||||
BalTimestamp = plugin_base.BalTimestamp
|
bt_class = plugin_base.BalTimestamp
|
||||||
|
|
||||||
# 1) Sanity: with the real (Linux 64-bit) datetime, large ts works already.
|
# 1) Sanity: with the real (Linux 64-bit) datetime, large ts works already.
|
||||||
bt = BalTimestamp(NLOCKTIME_MAX)
|
bt = bt_class(NLOCKTIME_MAX)
|
||||||
d = bt.to_date()
|
d = bt.to_date()
|
||||||
assert isinstance(d, _real_datetime), d
|
assert isinstance(d, _real_datetime), d
|
||||||
print("[OK] BalTimestamp(NLOCKTIME_MAX).to_date() works on this platform")
|
print("[OK] BalTimestamp(NLOCKTIME_MAX).to_date() works on this platform")
|
||||||
@@ -59,7 +59,7 @@ def main():
|
|||||||
plugin_base.datetime = _WindowsLikeDatetime
|
plugin_base.datetime = _WindowsLikeDatetime
|
||||||
try:
|
try:
|
||||||
# 2a) Absolute sentinel timestamp (the exact crash path from the log).
|
# 2a) Absolute sentinel timestamp (the exact crash path from the log).
|
||||||
bt = BalTimestamp(NLOCKTIME_MAX)
|
bt = bt_class(NLOCKTIME_MAX)
|
||||||
d = bt.to_date() # must NOT raise OverflowError anymore
|
d = bt.to_date() # must NOT raise OverflowError anymore
|
||||||
assert d.year <= 2038, f"expected clamp to <=2038, got {d!r}"
|
assert d.year <= 2038, f"expected clamp to <=2038, got {d!r}"
|
||||||
print("[OK] to_date(NLOCKTIME_MAX) no longer raises (clamped to INT32_MAX)")
|
print("[OK] to_date(NLOCKTIME_MAX) no longer raises (clamped to INT32_MAX)")
|
||||||
@@ -75,13 +75,13 @@ def main():
|
|||||||
print("[OK] str()/repr() on out-of-range timestamp are safe")
|
print("[OK] str()/repr() on out-of-range timestamp are safe")
|
||||||
|
|
||||||
# 2d) Relative durations that overflow when added (e.g. huge 'd').
|
# 2d) Relative durations that overflow when added (e.g. huge 'd').
|
||||||
bt_rel = BalTimestamp(f"{10 ** 9}d") # ~2.7M years -> overflow
|
bt_rel = bt_class(f"{10 ** 9}d") # ~2.7M years -> overflow
|
||||||
d2 = bt_rel.to_date()
|
d2 = bt_rel.to_date()
|
||||||
assert d2 is not None
|
assert d2 is not None
|
||||||
print("[OK] huge relative duration no longer raises")
|
print("[OK] huge relative duration no longer raises")
|
||||||
|
|
||||||
# 2e) Normal values are unchanged (behaviour-preserving check).
|
# 2e) Normal values are unchanged (behaviour-preserving check).
|
||||||
bt_norm = BalTimestamp("90d")
|
bt_norm = bt_class("90d")
|
||||||
d3 = bt_norm.to_date()
|
d3 = bt_norm.to_date()
|
||||||
# 90 days from now, normalised to midnight
|
# 90 days from now, normalised to midnight
|
||||||
assert d3.hour == 0 and d3.minute == 0 and d3.second == 0
|
assert d3.hour == 0 and d3.minute == 0 and d3.second == 0
|
||||||
|
|||||||
Reference in New Issue
Block a user