forked from bitcoinafterlife/bal-electrum-plugin
remove bal with pycache
This commit is contained in:
@@ -1,400 +0,0 @@
|
||||
"""
|
||||
bal.core.plugin_base
|
||||
=====================
|
||||
|
||||
GUI-agnostic foundation of the plugin.
|
||||
|
||||
It contains:
|
||||
* :class:`BalConfig` - a thin typed wrapper around an Electrum config key
|
||||
with a default value.
|
||||
* :class:`BalPlugin` - the base plugin class (extends Electrum's
|
||||
``BasePlugin``) holding every configuration option
|
||||
and the default "will settings". The Qt-specific
|
||||
``Plugin`` subclass lives in ``bal.gui.qt.plugin``.
|
||||
* :class:`BalTimestamp`- helper to convert between relative durations
|
||||
(``"30d"``, ``"1y"``) and absolute timestamps.
|
||||
|
||||
It also registers the three custom persisted dictionaries (``heirs``,
|
||||
``will`` and ``will_settings``) with Electrum's JSON database so they are
|
||||
serialised together with the wallet file.
|
||||
|
||||
This module performs **no** GUI work and imports nothing from PyQt / electrum.gui.
|
||||
"""
|
||||
|
||||
import os
|
||||
import platform
|
||||
from datetime import date, datetime, timedelta
|
||||
|
||||
from electrum import constants, json_db
|
||||
from electrum.logging import get_logger
|
||||
from electrum.plugin import BasePlugin
|
||||
from electrum.transaction import tx_from_any
|
||||
|
||||
_logger = get_logger(__name__)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Wallet-DB registration
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Electrum needs to know how to (de)serialise the custom dictionaries the
|
||||
# plugin stores inside the wallet file. ``register_dict`` associates a key
|
||||
# name with a conversion callable applied to each value when the wallet is
|
||||
# loaded. ``will`` values run through ``get_will`` so the stored transaction
|
||||
# hex is turned back into a ``Transaction`` object.
|
||||
def get_will(x):
|
||||
"""Deserialise a stored will entry, rebuilding its ``tx`` object."""
|
||||
try:
|
||||
x["tx"] = tx_from_any(x["tx"])
|
||||
except Exception as e:
|
||||
raise e
|
||||
return x
|
||||
|
||||
|
||||
json_db.register_dict("heirs", tuple, None)
|
||||
json_db.register_dict("will", dict, None)
|
||||
json_db.register_dict("will_settings", lambda x: x, None)
|
||||
|
||||
|
||||
class BalConfig:
|
||||
"""Typed accessor for a single Electrum configuration key.
|
||||
|
||||
Wraps ``config.get`` / ``config.set_key`` and supplies a default value
|
||||
when the key is missing.
|
||||
"""
|
||||
|
||||
def __init__(self, config, name, default):
|
||||
self.config = config
|
||||
self.name = name
|
||||
self.default = default
|
||||
|
||||
def get(self, default=None):
|
||||
"""Return the stored value, falling back to ``default`` then ``self.default``."""
|
||||
v = self.config.get(self.name, default)
|
||||
if v is None:
|
||||
if default is not None:
|
||||
v = default
|
||||
else:
|
||||
v = self.default
|
||||
return v
|
||||
|
||||
def set(self, value, save=True):
|
||||
"""Persist ``value`` for this key."""
|
||||
self.config.set_key(self.name, value, save=save)
|
||||
|
||||
|
||||
class BalPlugin(BasePlugin):
|
||||
"""Base plugin: holds configuration and default inheritance settings.
|
||||
|
||||
The GUI layer subclasses this in ``bal.gui.qt.plugin.Plugin`` and adds the
|
||||
Electrum ``@hook`` methods. Keeping the configuration here means the CLI
|
||||
layer (or unit tests) can use the plugin logic without importing Qt.
|
||||
"""
|
||||
|
||||
_version = None
|
||||
__version__ = "0.3.3" # AUTOMATICALLY GENERATED DO NOT EDIT
|
||||
|
||||
# Command used to open an .ics calendar file, per operating system.
|
||||
default_app = {
|
||||
"Linux": "xdg-open",
|
||||
"Windows": "cmd /c start",
|
||||
"Darwin": "open",
|
||||
}
|
||||
|
||||
# Human-readable chain name ("bitcoin", "testnet", "regtest", ...).
|
||||
chainname = (
|
||||
constants.net.NET_NAME if constants.net.NET_NAME != "mainnet" else "bitcoin"
|
||||
)
|
||||
|
||||
# Default geometry hint for some dialogs (kept from the original code).
|
||||
SIZE = (159, 97)
|
||||
|
||||
def version(self):
|
||||
"""Return the plugin version, read once from the ``VERSION`` file."""
|
||||
if not self._version:
|
||||
try:
|
||||
f = ""
|
||||
with open("{}/VERSION".format(self.plugin_dir), "r") as fi:
|
||||
f = str(fi.read())
|
||||
self._version = f.strip()
|
||||
except Exception as e:
|
||||
_logger.error(f"failed to get version: {e}")
|
||||
self._version = "unknown"
|
||||
return self._version
|
||||
|
||||
def __init__(self, parent, config, name):
|
||||
self.logger = get_logger(__name__)
|
||||
BasePlugin.__init__(self, parent, config, name)
|
||||
|
||||
# Base directory for plugin data inside the Electrum data dir.
|
||||
self.base_dir = os.path.join(config.electrum_path(), "bal")
|
||||
self.plugin_dir = os.path.split(os.path.realpath(__file__))[0]
|
||||
|
||||
# Make the plugin importable when loaded from a zip (legacy behaviour:
|
||||
# the parent directory of this file is added to ``sys.path``).
|
||||
zipfile = "/".join(self.plugin_dir.split("/")[:-1])
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, zipfile)
|
||||
|
||||
self.parent = parent
|
||||
self.config = config
|
||||
self.name = name
|
||||
|
||||
# ---------------------------------------------------------------- #
|
||||
# Configuration options (all persisted via Electrum's config).
|
||||
# ---------------------------------------------------------------- #
|
||||
self.ASK_BROADCAST = BalConfig(config, "bal_ask_broadcast", True)
|
||||
self.BROADCAST = BalConfig(config, "bal_broadcast", True)
|
||||
self.LOCKTIME_TIME = BalConfig(config, "bal_locktime_time", 90)
|
||||
self.LOCKTIMEDELTA_TIME = BalConfig(config, "bal_locktimedelta_time", 7)
|
||||
self.ENABLE_MULTIVERSE = BalConfig(config, "bal_enable_multiverse", False)
|
||||
self.TX_FEES = BalConfig(config, "bal_tx_fees", 100)
|
||||
self.INVALIDATE = BalConfig(config, "bal_invalidate", True)
|
||||
self.ASK_INVALIDATE = BalConfig(config, "bal_ask_invalidate", True)
|
||||
self.PREVIEW = BalConfig(config, "bal_preview", True)
|
||||
self.SAVE_TXS = BalConfig(config, "bal_save_txs", True)
|
||||
|
||||
self.NO_WILLEXECUTOR = BalConfig(config, "bal_no_willexecutor", True)
|
||||
self.HIDE_REPLACED = BalConfig(config, "bal_hide_replaced", True)
|
||||
self.HIDE_INVALIDATED = BalConfig(config, "bal_hide_invalidated", True)
|
||||
self.ALLOW_REPUSH = BalConfig(config, "bal_allow_repush", True)
|
||||
self.FIRST_EXECUTION = BalConfig(config, "bal_first_execution", True)
|
||||
self.AUTO_SIGN = BalConfig(config, "bal_auto_sign", True)
|
||||
self.ALARM_NUMBER = BalConfig(config, "bal_alarm_number", 3)
|
||||
self.WELIST_SERVER = BalConfig(
|
||||
config, "bal_welist_server", "https://welist.bitcoin-after.life/"
|
||||
)
|
||||
self.EVENT_DESCRIPTION = BalConfig(
|
||||
config,
|
||||
"bal_event_description",
|
||||
"BAL will execution of $wallet_name\r\n heirs list: \r\n$heirs_complete",
|
||||
)
|
||||
self.EVENT_SUMMARY = BalConfig(
|
||||
config, "bal_event_summary", "BAL -Will execution of $wallet_name"
|
||||
)
|
||||
|
||||
# Default will-executor servers, keyed by network.
|
||||
self.WILLEXECUTORS = BalConfig(
|
||||
config,
|
||||
"bal_willexecutors",
|
||||
{
|
||||
"mainnet": {
|
||||
"https://we.bitcoin-after.life": {
|
||||
"base_fee": 100000,
|
||||
"status": "New",
|
||||
"info": "Bitcoin After Life Will Executor",
|
||||
"address": "bc1qusymuetsz2psaqzqxv8qmzcy64d9meckj3lxxf",
|
||||
"selected": True,
|
||||
}
|
||||
},
|
||||
"testnet": {
|
||||
"https://we.bitcoin-after.life": {
|
||||
"base_fee": 100000,
|
||||
"status": "New",
|
||||
"info": "Bitcoin After Life Will Executor",
|
||||
"address": "bcrt1qa5cntu4hgadw8zd3n6sq2nzjy34sxdtd9u0gp7",
|
||||
"selected": True,
|
||||
}
|
||||
},
|
||||
"testnet4": {
|
||||
"https://we.bitcoin-after.life": {
|
||||
"base_fee": 100000,
|
||||
"status": "New",
|
||||
"info": "Bitcoin After Life Will Executor",
|
||||
"address": "bcrt1qa5cntu4hgadw8zd3n6sq2nzjy34sxdtd9u0gp7",
|
||||
"selected": True,
|
||||
}
|
||||
},
|
||||
"regtest": {
|
||||
"https://we.bitcoin-after.life": {
|
||||
"base_fee": 100000,
|
||||
"status": "New",
|
||||
"info": "Bitcoin After Life Will Executor",
|
||||
"address": "bcrt1qa5cntu4hgadw8zd3n6sq2nzjy34sxdtd9u0gp7",
|
||||
"selected": True,
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
self.WILL_SETTINGS = BalConfig(
|
||||
config,
|
||||
"bal_will_settings",
|
||||
BalPlugin.default_will_settings(),
|
||||
)
|
||||
|
||||
self.system = platform.system()
|
||||
self.CALENDAR_APP = BalConfig(
|
||||
config, "bal_open_app", self.default_app.get(self.system, "")
|
||||
)
|
||||
|
||||
# Cached toggles used by the GUI list filters.
|
||||
self._hide_invalidated = self.HIDE_INVALIDATED.get()
|
||||
self._hide_replaced = self.HIDE_REPLACED.get()
|
||||
|
||||
def resource_path(self, *parts):
|
||||
"""Absolute path to a file bundled inside the plugin directory."""
|
||||
return os.path.join(self.plugin_dir, *parts)
|
||||
|
||||
def sync_hide_filters(self):
|
||||
"""Re-read the "hide" filter flags from the persisted config.
|
||||
|
||||
The cached ``_hide_invalidated`` / ``_hide_replaced`` flags are used by
|
||||
the GUI list to decide which rows to skip. They can be changed from two
|
||||
different places:
|
||||
|
||||
* the list toolbar buttons, which call :meth:`hide_invalidated` /
|
||||
:meth:`hide_replaced` (a toggle that updates both the cache and the
|
||||
config), and
|
||||
* the Settings dialog checkboxes, which write the config directly
|
||||
(``BalConfig.set``) without touching the cached flags.
|
||||
|
||||
In the second case the cache and the config would drift apart and the
|
||||
transaction list would keep filtering with the *old* value, so the
|
||||
toggled rows never appear/disappear until Electrum is restarted.
|
||||
Re-syncing the cache from the config here (called by ``update_all``)
|
||||
keeps every code path coherent regardless of where the change came
|
||||
from.
|
||||
"""
|
||||
self._hide_invalidated = self.HIDE_INVALIDATED.get()
|
||||
self._hide_replaced = self.HIDE_REPLACED.get()
|
||||
|
||||
def hide_invalidated(self):
|
||||
"""Toggle (and persist) the "hide invalidated transactions" filter."""
|
||||
self._hide_invalidated = not self._hide_invalidated
|
||||
self.HIDE_INVALIDATED.set(self._hide_invalidated)
|
||||
|
||||
def hide_replaced(self):
|
||||
"""Toggle (and persist) the "hide replaced transactions" filter."""
|
||||
self._hide_replaced = not self._hide_replaced
|
||||
self.HIDE_REPLACED.set(self._hide_replaced)
|
||||
|
||||
def validate_will_settings(self, will_settings):
|
||||
"""Fill in any missing will-setting with its default value."""
|
||||
defaults = BalPlugin.default_will_settings()
|
||||
if not will_settings:
|
||||
will_settings = []
|
||||
if int(will_settings.get("baltx_fees", 0)) < 1:
|
||||
will_settings["baltx_fees"] = defaults['baltx_fees']
|
||||
if not will_settings.get("threshold"):
|
||||
will_settings["threshold"] = defaults['threshold']
|
||||
if not will_settings.get("locktime"):
|
||||
will_settings["locktime"] = defaults['locktime']
|
||||
return will_settings
|
||||
|
||||
@staticmethod
|
||||
def default_will_settings():
|
||||
"""Default will settings: a fee rate plus absolute threshold/locktime."""
|
||||
will_settings = {"baltx_fees": 100}
|
||||
will_settings.update(BalPlugin.default_will_settings_absolute())
|
||||
return will_settings
|
||||
|
||||
@staticmethod
|
||||
def default_will_settings_absolute():
|
||||
"""Convert the default relative dates into absolute timestamps (from today)."""
|
||||
relative_dates = BalPlugin.default_will_settings_relative()
|
||||
today = date.today()
|
||||
dt = datetime(today.year, today.month, today.day, 0, 0, 0)
|
||||
threshold = (
|
||||
dt + timedelta(days=BalTimestamp(relative_dates["threshold"]).duration_to_days())
|
||||
).timestamp()
|
||||
locktime = (
|
||||
dt + timedelta(days=BalTimestamp(relative_dates["locktime"]).duration_to_days())
|
||||
).timestamp()
|
||||
return {"threshold": threshold, "locktime": locktime}
|
||||
|
||||
@staticmethod
|
||||
def default_will_settings_relative():
|
||||
"""Default relative dates: 30 days threshold, 1 year locktime."""
|
||||
return {"threshold": "30d", "locktime": "1y"}
|
||||
|
||||
|
||||
class BalTimestamp:
|
||||
"""Parse and convert relative durations / absolute timestamps.
|
||||
|
||||
A value may be:
|
||||
* ``"<n>y"`` -> ``n`` years (unit ``"y"``)
|
||||
* ``"<n>d"`` -> ``n`` days (unit ``"d"``)
|
||||
* an integer -> an absolute UNIX timestamp (``unit is None``)
|
||||
"""
|
||||
|
||||
value = None
|
||||
unit = None
|
||||
|
||||
def __init__(self, value):
|
||||
str_value = str(value)
|
||||
if str_value and str_value[-1].lower() in ("y", "d"):
|
||||
self.value = int(str_value[:-1])
|
||||
self.unit = str_value[-1]
|
||||
else:
|
||||
try:
|
||||
self.value = int(value)
|
||||
except Exception as _e:
|
||||
self.value = 1
|
||||
self.unit = None
|
||||
|
||||
def duration_to_days(self):
|
||||
"""Return the duration expressed in days (years are ``*365``)."""
|
||||
return self.value * 365 if self.unit == 'y' else self.value
|
||||
|
||||
@staticmethod
|
||||
def _safe_fromtimestamp(ts):
|
||||
"""``datetime.fromtimestamp`` that never raises ``OverflowError``.
|
||||
|
||||
On Windows ``time_t`` is 32-bit, so ``datetime.fromtimestamp`` raises
|
||||
``OverflowError: Python int too large to convert to C int`` for any
|
||||
timestamp past the year-2038 limit (e.g. ``NLOCKTIME_MAX = 2**32 - 1``,
|
||||
used as the default/sentinel locktime). On 64-bit Linux the same call
|
||||
succeeds, which is why this only crashed on the user's Windows build.
|
||||
|
||||
We clamp out-of-range timestamps to INT32_MAX, mirroring Electrum's own
|
||||
``get_max_allowed_timestamp`` workaround (see Electrum issue #6170).
|
||||
"""
|
||||
INT32_MAX = 2 ** 31 - 1
|
||||
try:
|
||||
return datetime.fromtimestamp(ts)
|
||||
except (OSError, OverflowError, ValueError):
|
||||
try:
|
||||
return datetime.fromtimestamp(min(int(ts), INT32_MAX))
|
||||
except (OSError, OverflowError, ValueError):
|
||||
return datetime.fromtimestamp(INT32_MAX)
|
||||
|
||||
def to_date(self, from_date=None, reverse=False):
|
||||
"""Resolve to a ``datetime``.
|
||||
|
||||
For absolute values the stored timestamp is returned; for relative ones
|
||||
the duration is added to (or, if ``reverse``, subtracted from)
|
||||
``from_date`` (defaulting to *now*), normalised to midnight.
|
||||
"""
|
||||
if self.unit is None:
|
||||
return self._safe_fromtimestamp(self.value)
|
||||
else:
|
||||
if from_date is None:
|
||||
from_date = datetime.now()
|
||||
if isinstance(from_date, (int, float)):
|
||||
from_date = self._safe_fromtimestamp(from_date)
|
||||
reverse = 1 if not reverse else -1
|
||||
try:
|
||||
return (
|
||||
from_date + (reverse * timedelta(days=self.duration_to_days()))
|
||||
).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
except (OverflowError, OSError, ValueError):
|
||||
# Duration overflowed datetime's range; clamp to INT32_MAX.
|
||||
return self._safe_fromtimestamp(2 ** 31 - 1).replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
|
||||
def to_timestamp(self, from_date=None, reverse=False):
|
||||
"""Same as :meth:`to_date` but returns a UNIX timestamp."""
|
||||
return self.to_date(from_date, reverse).timestamp()
|
||||
|
||||
def __str__(self):
|
||||
if self.unit is None:
|
||||
return self._safe_fromtimestamp(self.value).isoformat()
|
||||
else:
|
||||
return f"{self.value}{self.unit}"
|
||||
|
||||
def __repr__(self):
|
||||
if self.unit is None:
|
||||
return self._safe_fromtimestamp(self.value).isoformat()
|
||||
else:
|
||||
return f"{self.value}{self.unit}"
|
||||
Reference in New Issue
Block a user