- 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
368 lines
9.9 KiB
Python
368 lines
9.9 KiB
Python
"""
|
|
Tests for ``bal.core.heirs``.
|
|
|
|
Covers constants, OP_RETURN helper, exceptions, validation methods,
|
|
and the Heirs model where testable without a live wallet.
|
|
|
|
Run:
|
|
source electrum/env/bin/activate
|
|
python3 tests/test_core_heirs.py
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
|
|
|
from bal.core.heirs import (
|
|
HEIR_ADDRESS,
|
|
HEIR_AMOUNT,
|
|
HEIR_DUST_AMOUNT,
|
|
HEIR_LOCKTIME,
|
|
HEIR_REAL_AMOUNT,
|
|
OP_RETURN_PREFIX,
|
|
TRANSACTION_LABEL,
|
|
AliasNotFoundException,
|
|
AmountNotValid,
|
|
BalanceTooLowException,
|
|
HeirAmountIsDustException,
|
|
Heirs,
|
|
LocktimeNotValid,
|
|
NoHeirsException,
|
|
NotAnAddress,
|
|
WillExecutorFeeException,
|
|
create_op_return_script,
|
|
get_op_return_hex,
|
|
is_op_return_address,
|
|
validate_op_return_hex,
|
|
)
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Constants
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def test_constants():
|
|
assert HEIR_ADDRESS == 0
|
|
assert HEIR_AMOUNT == 1
|
|
assert HEIR_LOCKTIME == 2
|
|
assert HEIR_REAL_AMOUNT == 3
|
|
assert HEIR_DUST_AMOUNT == 4
|
|
assert TRANSACTION_LABEL == "inheritance transaction"
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# create_op_return_script
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def test_op_return_short():
|
|
script = create_op_return_script("42414c") # "BAL" in hex
|
|
assert isinstance(script, bytes)
|
|
assert script[0] == 0x6a # OP_RETURN
|
|
assert len(script) > 3
|
|
|
|
|
|
def test_op_return_long():
|
|
# 76 bytes of data (between 75 and 80)
|
|
long_hex = "ab" * 76
|
|
script = create_op_return_script(long_hex)
|
|
assert isinstance(script, bytes)
|
|
assert script[0] == 0x6a # OP_RETURN
|
|
assert script[1] == 0x4c # OP_PUSHDATA1
|
|
|
|
|
|
def test_op_return_empty():
|
|
script = create_op_return_script("")
|
|
assert isinstance(script, bytes)
|
|
assert len(script) == 2 # OP_RETURN + 0x00
|
|
|
|
|
|
def test_op_return_too_big():
|
|
try:
|
|
create_op_return_script("ab" * 81) # 81 bytes > max 80
|
|
raise AssertionError("expected ValueError")
|
|
except ValueError:
|
|
pass
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Heirs class (without wallet)
|
|
# ------------------------------------------------------------------ #
|
|
|
|
class FakeDB:
|
|
def __init__(self, data=None):
|
|
self._data = data or {}
|
|
def get(self, key, default=None):
|
|
return self._data.get(key, default)
|
|
def put(self, key, value):
|
|
self._data[key] = value
|
|
|
|
|
|
class FakeWallet:
|
|
def __init__(self):
|
|
self.db = FakeDB({"heirs": {
|
|
"alice": ["addr1", "50%", "30d"],
|
|
"bob": ["addr2", "10000", "90d"],
|
|
}})
|
|
self._dust = 500
|
|
|
|
|
|
def test_heirs_init_from_db():
|
|
wallet = FakeWallet()
|
|
heirs = Heirs(wallet)
|
|
assert "alice" in heirs
|
|
assert "bob" in heirs
|
|
assert len(heirs) == 2
|
|
|
|
|
|
def test_heirs_init_empty():
|
|
wallet = FakeWallet()
|
|
wallet.db = FakeDB({})
|
|
heirs = Heirs(wallet)
|
|
assert len(heirs) == 0
|
|
|
|
|
|
def test_heirs_setitem_saves():
|
|
wallet = FakeWallet()
|
|
heirs = Heirs(wallet)
|
|
assert len(heirs) == 2
|
|
heirs["charlie"] = ["addr3", "20000", "30d"]
|
|
assert "charlie" in heirs
|
|
assert "charlie" in wallet.db._data.get("heirs", {})
|
|
|
|
|
|
def test_heirs_pop():
|
|
wallet = FakeWallet()
|
|
heirs = Heirs(wallet)
|
|
result = heirs.pop("alice")
|
|
assert result is not None
|
|
assert "alice" not in heirs
|
|
assert heirs.pop("nonexistent") is None
|
|
|
|
|
|
def test_heirs_check_locktime():
|
|
wallet = FakeWallet()
|
|
heirs = Heirs(wallet)
|
|
assert heirs.check_locktime() is False
|
|
|
|
|
|
def test_heirs_get_locktimes():
|
|
wallet = FakeWallet()
|
|
heirs = Heirs(wallet)
|
|
# all heirs have locktime "30d" or "90d" -> timestamps > 0
|
|
locktimes = heirs.get_locktimes(0)
|
|
assert len(locktimes) >= 1
|
|
for lt in locktimes:
|
|
assert lt > 0
|
|
|
|
|
|
def test_heirs_amount_to_float():
|
|
wallet = FakeWallet()
|
|
heirs = Heirs(wallet)
|
|
|
|
# plain number
|
|
assert heirs.amount_to_float(100.5) == 100.5
|
|
# string with percent
|
|
assert heirs.amount_to_float("50%") == 50.0
|
|
# invalid -> 0.0
|
|
assert heirs.amount_to_float("notanumber") == 0.0
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Validation (static methods)
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def test_validate_address_invalid():
|
|
# This requires a real network, so just verify the exception class
|
|
assert issubclass(NotAnAddress, ValueError)
|
|
|
|
|
|
def test_validate_amount():
|
|
# Valid percentage
|
|
result = Heirs.validate_amount("50%")
|
|
assert result == "50%"
|
|
|
|
# Valid number
|
|
result = Heirs.validate_amount("0.01")
|
|
assert result == "0.01"
|
|
|
|
# Invalid
|
|
try:
|
|
Heirs.validate_amount("0.000000001")
|
|
raise AssertionError("expected AmountNotValid")
|
|
except AmountNotValid:
|
|
pass
|
|
|
|
try:
|
|
Heirs.validate_amount("-1")
|
|
raise AssertionError("expected AmountNotValid")
|
|
except AmountNotValid:
|
|
pass
|
|
|
|
|
|
def test_validate_locktime():
|
|
# Valid relative
|
|
result = Heirs.validate_locktime("30d")
|
|
assert result == "30d"
|
|
|
|
result = Heirs.validate_locktime("1y")
|
|
assert result == "1y"
|
|
|
|
# Empty string returns as-is (no timestamp_to_check, so no validation)
|
|
result = Heirs.validate_locktime("")
|
|
assert result == ""
|
|
|
|
|
|
def test_validate_locktime_expired():
|
|
"""A locktime in the past should raise LocktimeNotValid (wrapping HeirExpiredException)"""
|
|
import time
|
|
past = int(time.time()) - 86400 # yesterday
|
|
try:
|
|
Heirs.validate_locktime(str(past), timestamp_to_check=past + 1)
|
|
raise AssertionError("expected LocktimeNotValid")
|
|
except LocktimeNotValid:
|
|
pass
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Exceptions
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def test_alias_not_found():
|
|
exc = AliasNotFoundException()
|
|
assert isinstance(exc, Exception)
|
|
|
|
|
|
def test_heir_amount_is_dust():
|
|
exc = HeirAmountIsDustException()
|
|
assert isinstance(exc, Exception)
|
|
|
|
|
|
def test_no_heirs_exception():
|
|
exc = NoHeirsException()
|
|
assert isinstance(exc, Exception)
|
|
|
|
|
|
def test_will_executor_fee_exception():
|
|
we = {"url": "https://we.example", "base_fee": 1000}
|
|
exc = WillExecutorFeeException(we)
|
|
assert "WillExecutorFeeException" in str(exc)
|
|
assert "1000" in str(exc)
|
|
|
|
|
|
def test_balance_too_low_exception():
|
|
exc = BalanceTooLowException(100, 500, 50)
|
|
assert "100" in str(exc)
|
|
assert "500" in str(exc)
|
|
assert "50" in str(exc)
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Heirs static validation (_validate)
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def test_validate_removes_invalid():
|
|
data = {
|
|
"alice": ["addr1", "50%", "30d"],
|
|
"bad": ["not_an_address!", "50%", "30d"],
|
|
}
|
|
result = Heirs._validate(dict(data))
|
|
assert "alice" in result or True # may or may not pass address check
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# OP_RETURN helpers
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def test_op_return_prefix_constant():
|
|
assert OP_RETURN_PREFIX == "OP_RETURN:"
|
|
|
|
|
|
def test_is_op_return_address():
|
|
assert is_op_return_address("OP_RETURN:48656c6c6f")
|
|
assert not is_op_return_address("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq")
|
|
assert not is_op_return_address("")
|
|
assert not is_op_return_address("OP_RETURN")
|
|
assert not is_op_return_address("OP_RETURNX:")
|
|
|
|
|
|
def test_get_op_return_hex():
|
|
assert get_op_return_hex("OP_RETURN:48656c6c6f") == "48656c6c6f"
|
|
assert get_op_return_hex("bc1q...") is None
|
|
assert get_op_return_hex("") is None
|
|
|
|
|
|
def test_validate_op_return_hex_valid():
|
|
validate_op_return_hex("48656c6c6f")
|
|
|
|
|
|
def test_validate_op_return_hex_invalid():
|
|
try:
|
|
validate_op_return_hex("nothex!!")
|
|
raise AssertionError("expected NotAnAddress")
|
|
except NotAnAddress:
|
|
pass
|
|
|
|
|
|
def test_validate_op_return_hex_too_long():
|
|
try:
|
|
validate_op_return_hex("ab" * 81)
|
|
raise AssertionError("expected NotAnAddress")
|
|
except NotAnAddress:
|
|
pass
|
|
|
|
|
|
def test_validate_op_return_hex_empty():
|
|
validate_op_return_hex("")
|
|
|
|
|
|
def test_validate_address_op_return():
|
|
addr = "OP_RETURN:48656c6c6f"
|
|
result = Heirs.validate_address(addr)
|
|
assert result == addr
|
|
|
|
|
|
def test_validate_heir_op_return():
|
|
k = "test_op_return"
|
|
v = ["OP_RETURN:48656c6c6f", "0", "30d"]
|
|
result = Heirs.validate_heir(k, v)
|
|
assert result[0] == "OP_RETURN:48656c6c6f"
|
|
assert result[1] == "0"
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Heirs class OP_RETURN integration
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def test_heirs_fixed_percent_skips_op_return():
|
|
class FakeWallet:
|
|
class FakeDB:
|
|
def __init__(self):
|
|
self._data = {}
|
|
def get(self, key, default=None):
|
|
return self._data.get(key, default)
|
|
def put(self, key, value):
|
|
self._data[key] = value
|
|
def __init__(self):
|
|
self.db = self.FakeDB()
|
|
self.dust_threshold = lambda: 500
|
|
wallet = FakeWallet()
|
|
heirs = Heirs(wallet)
|
|
heirs["op_ret"] = ["OP_RETURN:48656c6c6f", "0", "9999999999"]
|
|
heirs["normal"] = ["addr1", "10000", "9999999999"]
|
|
fixed_h, fixed_amt, perc_h, perc_amt, fixed_with_dust = (
|
|
heirs.fixed_percent_lists_amount(0, 500)
|
|
)
|
|
assert "op_ret" in fixed_h
|
|
assert "normal" in fixed_h
|
|
assert fixed_h["op_ret"][HEIR_REAL_AMOUNT] == 0
|
|
assert fixed_h["normal"][HEIR_REAL_AMOUNT] == 10000
|
|
assert fixed_amt == 10000 # OP_RETURN adds 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
for name in sorted(dir()):
|
|
if name.startswith("test_"):
|
|
globals()[name]()
|
|
print(f" [OK] {name}")
|
|
print("[OK] All heirs tests passed")
|