Files
bal-electrum-plugin/bal/core/willexecutors.py

909 lines
37 KiB
Python

"""
bal.core.willexecutors
=======================
Client logic for talking to *will-executor* servers.
A will-executor is an optional third-party service that, for a small fee,
stores the signed inheritance transactions off-line and broadcasts them once
their locktime expires (acting as a dead-man's switch backup).
This module only contains the networking / data-shaping logic (downloading the
server list, pinging servers for their fee and address, pushing transactions,
checking whether a tx is already stored). It is GUI-free: all user
interaction is handled by the Qt layer.
"""
import json
import time
from datetime import datetime
from aiohttp import ClientResponse
from electrum import bitcoin, constants
from electrum.bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC
from electrum.i18n import _
from electrum.logging import get_logger
from electrum.network import Network
from .plugin_base import BalPlugin, get_version
# Per-request timeout (seconds) for interactive operations (ping / info /
# list download). These fail fast (no retries) so a dead server does not
# block the UI.
DEFAULT_TIMEOUT = 5
# Single, shared wall-clock deadline (seconds) for ALL network waits the user
# can watch in the GUI: the parallel broadcast (pushtxs), the parallel check
# (searchtx), the will-executor ping and the will-executor list download.
# Having ONE constant (instead of several scattered 30s/45s values) keeps the
# experience consistent and makes it trivial to tune. Requested by the user
# (reduced from 30s/45s to 20s, unified into one variable).
NETWORK_DEADLINE = 20
# Broadcast (pushtxs) timeouts. Broadcasting a will is important, so we keep a
# couple of quick retries to survive a transient hiccup -- but far from the old
# 10s x 10 retries + 30s sleeps (~140s) that froze the wizard on a dead server.
# Worst case per server is now ~ PUSH_TIMEOUT * (1 + PUSH_MAX_RETRIES)
# + PUSH_RETRY_SLEEP * PUSH_MAX_RETRIES = 8 * 3 + 1 * 2 = ~26s, and the wizard
# also enforces a global deadline on top of this (see push_transactions_parallel).
PUSH_TIMEOUT = 8
PUSH_MAX_RETRIES = 2
PUSH_RETRY_SLEEP = 1
# Global wall-clock deadline (seconds) for the whole parallel broadcast. Once
# it elapses we stop waiting for the still-pending servers, mark them as
# "Timeout" and let the wizard proceed instead of appearing stuck.
# Derived from the single shared NETWORK_DEADLINE constant above.
PUSH_GLOBAL_DEADLINE = NETWORK_DEADLINE
# Check (searchtx) timeouts. Used when the user presses "Check" to verify that
# each will-executor still holds the transaction. Like the broadcast path, the
# old defaults (10s x 10 retries + 30s sleeps ~= 140s per server) froze the
# "checking transaction" dialog on a single dead server. Fail fast with one
# quick retry, and cap the whole batch with a global deadline.
CHECK_TIMEOUT = 8
CHECK_MAX_RETRIES = 1
CHECK_RETRY_SLEEP = 1
# Derived from the single shared NETWORK_DEADLINE constant above.
CHECK_GLOBAL_DEADLINE = NETWORK_DEADLINE
_logger = get_logger(__name__)
# --------------------------------------------------------------------------- #
# Tor / .onion helpers
# --------------------------------------------------------------------------- #
# A .onion will-executor is only reachable when Electrum routes traffic through
# Tor. There is no point downloading .onion servers from welist when Electrum is
# not on Tor: they would only waste time (unreachable). We therefore drop them
# from the downloaded list unless Tor is confirmed active. Electrum itself gates
# .onion the same way (`if host.endswith('.onion') and not self.is_proxy_tor`).
def is_onion_url(url):
"""True if the URL points to a Tor hidden service (.onion host)."""
if not url:
return False
try:
u = url.lower()
if "://" in u:
u = u.split("://", 1)[1]
host = u.split("/", 1)[0].split(":", 1)[0]
return host.endswith(".onion")
except Exception:
return False
def is_tor_active():
"""True only if Electrum is CONFIRMED connected through Tor.
Reads Network.is_proxy_tor defensively (network may be None in offline
mode). It is TRI-STATE (True / False / None); we require True, so False and
None (undetermined) both mean "Tor not active". Available in Electrum 4.7.2
and 4.8.0.
"""
try:
network = Network.get_instance()
if network is None:
return False
return network.is_proxy_tor is True
except Exception:
return False
chainname = BalPlugin.chainname
class Willexecutors:
class NoServersForChainError(Exception):
"""Raised when the welist server responds but returns no data for the
requested chain, indicating no active servers for that network."""
def __init__(self, chain, url=None, reason=None):
self.chain = chain
self.url = url
self.reason = reason
super().__init__(
f"NoServersForChainError: chain={chain} url={url} reason={reason}"
)
# Expose the networking constants as class attributes so the GUI layer can
# reference them (e.g. to show the "Xs / DEADLINEs" countdown) without
# importing module-level names. Single source of truth: the module
# constants defined above.
DEFAULT_TIMEOUT = DEFAULT_TIMEOUT
NETWORK_DEADLINE = NETWORK_DEADLINE
PUSH_TIMEOUT = PUSH_TIMEOUT
PUSH_MAX_RETRIES = PUSH_MAX_RETRIES
PUSH_RETRY_SLEEP = PUSH_RETRY_SLEEP
PUSH_GLOBAL_DEADLINE = PUSH_GLOBAL_DEADLINE
CHECK_TIMEOUT = CHECK_TIMEOUT
CHECK_MAX_RETRIES = CHECK_MAX_RETRIES
CHECK_RETRY_SLEEP = CHECK_RETRY_SLEEP
CHECK_GLOBAL_DEADLINE = CHECK_GLOBAL_DEADLINE
@staticmethod
def save(bal_plugin, willexecutors):
_logger.debug(f"save {willexecutors},{chainname}")
aw = bal_plugin.WILLEXECUTORS.get()
aw[chainname] = willexecutors
bal_plugin.WILLEXECUTORS.set(aw)
_logger.debug(f"saved: {aw}")
# bal_plugin.WILLEXECUTORS.set(willexecutors)
@staticmethod
def get_willexecutors(
bal_plugin, update=False, bal_window=False, force=False, task=True
):
willexecutors = bal_plugin.WILLEXECUTORS.get()
willexecutors = willexecutors.get(chainname, {})
to_del = []
for w in willexecutors:
if not isinstance(willexecutors[w], dict):
to_del.append(w)
continue
Willexecutors.initialize_willexecutor(willexecutors[w], w)
for w in to_del:
_logger.error(
"error Willexecutor to delete type:{} {}".format(
type(willexecutors[w]), w
)
)
del willexecutors[w]
bal = bal_plugin.WILLEXECUTORS.default.get(chainname, {})
for bal_url, bal_executor in bal.items():
if bal_url not in willexecutors:
_logger.debug(f"force add {bal_url} willexecutor")
willexecutors[bal_url] = bal_executor
# if update:
# found = False
# for url, we in willexecutors.items():
# if Willexecutors.is_selected(we):
# found = True
# if found or force:
# if bal_plugin.PING_WILLEXECUTORS.get() or force:
# ping_willexecutors = True
# if bal_plugin.ASK_PING_WILLEXECUTORS.get() and not force:
# if bal_window:
# ping_willexecutors = bal_window.window.question(
# _(
# "Contact willexecutors servers to update payment informations?"
# )
# )
# if ping_willexecutors:
# if task:
# bal_window.ping_willexecutors(willexecutors, task)
# else:
# bal_window.ping_willexecutors_task(willexecutors)
w_sorted = dict(
sorted(
willexecutors.items(), key=lambda w: w[1].get("sort", 0), reverse=True
)
)
return w_sorted
@staticmethod
def is_selected(willexecutor, value=None, max_fee=None):
if not willexecutor:
return False
if value is not None:
willexecutor["selected"] = value
if max_fee is not None:
base_fee = willexecutor.get("base_fee", 0)
if int(base_fee) >= max_fee:
return False
try:
return willexecutor["selected"]
except Exception:
willexecutor["selected"] = False
return False
@staticmethod
def is_valid(willexecutor, max_fee=None, dust=None):
if not willexecutor:
return False
address = willexecutor.get("address", "")
if not address or not bitcoin.is_address(address, net=constants.net):
return False
base_fee = int(willexecutor.get("base_fee", 0))
if dust is not None and base_fee <= dust:
return False
if max_fee is not None and base_fee >= max_fee:
return False
return True
@staticmethod
def get_willexecutor_transactions(will, force=False):
willexecutors = {}
for wid, willitem in will.items():
if willitem.get_status("VALID"):
if willitem.get_status("COMPLETE"):
if not willitem.get_status("PUSHED") or force:
if willexecutor := willitem.we:
url = willexecutor["url"]
if willexecutor and Willexecutors.is_selected(willexecutor):
if url not in willexecutors:
willexecutor["txs"] = ""
willexecutor["txsids"] = []
willexecutor["broadcast_status"] = _("Waiting...")
willexecutors[url] = willexecutor
willexecutors[url]["txs"] += str(willitem.tx) + "\n"
willexecutors[url]["txsids"].append(wid)
return willexecutors
# def only_selected_list(willexecutors):
# out = {}
# for url, v in willexecutors.items():
# if Willexecutors.is_selected(url):
# out[url] = v
# def push_transactions_to_willexecutors(will):
# willexecutors = Willexecutors.get_transactions_to_be_pushed()
# for url in willexecutors:
# willexecutor = willexecutors[url]
# if Willexecutors.is_selected(willexecutor):
# if "txs" in willexecutor:
# Willexecutors.push_transactions_to_willexecutor(
# willexecutors[url]["txs"], url
# )
@staticmethod
def send_request(
method, url, data=None, *, timeout=10, handle_response=None, count_reply=0,
max_retries=10, retry_sleep=3,
):
"""Send an HTTP request to a will-executor server.
``max_retries`` / ``retry_sleep`` control the timeout-retry behaviour:
* For *critical* operations (pushing inheritance transactions) the
historical default of up to 10 retries with a 3s back-off is kept, so
a transient network hiccup does not lose a transaction.
* For *interactive* operations (ping / info / list download) callers
should pass ``max_retries=0`` so a dead server fails fast (one short
timeout) instead of blocking the UI for minutes. See
:meth:`ping_servers_parallel`.
"""
network = Network.get_instance()
if not network:
raise Exception("You are offline.")
_logger.debug(f"<-- {method} {url} {data}")
headers = {}
headers["user-agent"] = f"BalPlugin v:{get_version()}"
headers["Content-Type"] = "text/plain"
if not handle_response:
handle_response = Willexecutors.handle_response
attempts = max_retries + 1
for attempt in range(attempts):
try:
if method == "get":
response = Network.send_http_on_proxy(
method,
url,
params=data,
headers=headers,
on_finish=handle_response,
timeout=timeout,
)
elif method == "post":
response = Network.send_http_on_proxy(
method,
url,
body=data,
headers=headers,
on_finish=handle_response,
timeout=timeout,
)
else:
raise Exception(f"unexpected {method=!r}")
_logger.debug(f"--> {response}")
return response
except TimeoutError:
if attempt < max_retries:
_logger.debug(
f"timeout({attempt}) error: "
f"retry in {retry_sleep} sec..."
)
if retry_sleep:
time.sleep(retry_sleep)
else:
_logger.debug(f"Too many timeouts: {attempt}")
except Exception as e:
raise e
return None
@staticmethod
def get_we_url_from_response(resp):
url_slices = str(resp.url).split("/")
if len(url_slices) > 2:
url_slices = url_slices[:-2]
return "/".join(url_slices)
@staticmethod
async def handle_response(resp: ClientResponse):
resp.raise_for_status()
r = await resp.text()
try:
r = json.loads(r)
except Exception as e:
_logger.debug(f"error handling response:{e}")
return r
@staticmethod
class AlreadyPresentException(Exception):
pass
@staticmethod
def push_transactions_to_willexecutor(
willexecutor, *, timeout=PUSH_TIMEOUT, max_retries=PUSH_MAX_RETRIES,
retry_sleep=PUSH_RETRY_SLEEP,
):
# ``timeout`` / ``max_retries`` / ``retry_sleep`` are forwarded to
# send_request so the broadcast fails fast on a dead/slow server instead
# of hanging for ~140s (the old default was 10s timeout x 10 retries +
# 30s of sleeps). A small number of quick retries still protects
# against a transient hiccup without freezing the wizard.
out = True
try:
_logger.debug(f"{willexecutor['url']}: {willexecutor['txs']}")
if w := Willexecutors.send_request(
"post",
willexecutor["url"] + "/" + chainname + "/pushtxs",
data=willexecutor["txs"].encode("ascii"),
timeout=timeout,
max_retries=max_retries,
retry_sleep=retry_sleep,
):
_logger.debug(f"pushed: {w}")
if w != "thx":
_logger.debug(f"error: {w}")
raise Exception(w)
willexecutor["broadcast_status"] = _("Success")
else:
raise Exception(
f"empty reply from:{willexecutor['url']}"
)
except Exception as e:
_logger.debug(f"error:{e}")
if str(e) == "already present":
raise Willexecutors.AlreadyPresentException()
out = False
willexecutor["broadcast_status"] = _("Failed")
return out
@staticmethod
def ping_servers(willexecutors):
for url, we in willexecutors.items():
Willexecutors.get_info_task(url, we)
@staticmethod
def get_info_task(url, willexecutor, *, timeout=DEFAULT_TIMEOUT,
max_retries=0, retry_sleep=0):
w = None
try:
_logger.info("GETINFO_WILLEXECUTOR")
_logger.debug(url)
# Fast-fail by default (max_retries=0): a dead server returns after a
# single short timeout instead of retrying 10x with sleeps, which
# used to freeze the UI for minutes per unreachable server.
w = Willexecutors.send_request(
"get", url + "/" + chainname + "/info",
timeout=timeout, max_retries=max_retries, retry_sleep=retry_sleep,
)
if isinstance(w, dict):
address = w.get("address")
if not isinstance(address, str) or not bitcoin.is_address(
address, net=constants.net
):
_logger.warning(
f"invalid address from {url}: {address!r}"
)
willexecutor["status"] = "KO"
else:
base_fee = w.get("base_fee")
try:
base_fee = int(base_fee)
if base_fee < 0:
raise ValueError("negative fee")
if base_fee > TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN:
raise ValueError("fee exceeds total coin supply")
except (TypeError, ValueError) as e:
_logger.warning(
f"invalid base_fee from {url}: "
f"{w.get('base_fee')!r} ({e})"
)
willexecutor["status"] = "KO"
else:
willexecutor["url"] = url
willexecutor["status"] = 200
willexecutor["base_fee"] = base_fee
willexecutor["address"] = address
willexecutor["info"] = w.get("info", "")
else:
# No dict reply (timeout / empty) -> mark as unreachable.
willexecutor["status"] = "KO"
_logger.debug(f"response_data {w}")
except Exception as e:
_logger.error(f"error {e} contacting {url}: {w}")
willexecutor["status"] = "KO"
willexecutor["last_update"] = datetime.now().timestamp()
return willexecutor
@staticmethod
def ping_servers_parallel(willexecutors, *, on_each=None, max_workers=8,
timeout=DEFAULT_TIMEOUT, on_tick=None,
tick_interval=1.0):
"""Ping every will-executor concurrently and report results as they
arrive.
Network requests run in a thread pool: each ``send_http_on_proxy`` call
schedules its coroutine on Electrum's shared asyncio loop and blocks
only its *own* worker thread, so the total wall-clock time is roughly
that of the slowest server rather than the *sum* of all of them. A
single dead server can no longer stall the whole batch.
Args:
willexecutors: ``{url: we_dict}`` mapping (mutated in place with the
ping result, exactly like the old sequential ``ping_servers``).
on_each: optional ``callback(url, we_dict, ok: bool)`` invoked from a
worker thread each time a server answers (or fails), so the GUI
can update its list live. Must be thread-safe / marshalled to
the GUI thread by the caller.
max_workers: maximum number of concurrent pings.
timeout: per-request timeout in seconds (fast-fail, no retries).
on_tick: optional ``callback()`` invoked periodically (every
``tick_interval`` seconds) **from the calling thread** while
waiting for servers, so a Qt caller can refresh an elapsed-time
counter from the same thread that drives ``on_each``.
Returns:
The same ``willexecutors`` mapping, updated in place.
"""
from concurrent.futures import ThreadPoolExecutor, wait
from concurrent.futures import FIRST_COMPLETED
items = list(willexecutors.items())
if not items:
return willexecutors
def _ping_one(url, we):
we = Willexecutors.get_info_task(
url, we, timeout=timeout, max_retries=0, retry_sleep=0
)
ok = we.get("status") == 200
return url, we, ok
def _fire_tick():
if on_tick is not None:
try:
on_tick()
except Exception as cb_err:
_logger.error(f"ping on_tick callback error: {cb_err}")
workers = max(1, min(max_workers, len(items)))
# Manual pool (no ``with``) so we can poll futures in short slices and
# drive ``on_tick`` from THIS thread between waits (reliable Qt repaint).
pool = ThreadPoolExecutor(max_workers=workers, thread_name_prefix="bal-ping")
futures = {pool.submit(_ping_one, url, we) for url, we in items}
try:
pending = set(futures)
while pending:
done, pending = wait(
pending, timeout=tick_interval, return_when=FIRST_COMPLETED
)
for fut in done:
try:
url, we, ok = fut.result()
except Exception as e: # defensive: one server never crashes all
_logger.error(f"ping_servers_parallel worker error: {e}")
continue
willexecutors[url] = we
if on_each is not None:
try:
on_each(url, we, ok)
except Exception as cb_err:
_logger.error(f"ping on_each callback error: {cb_err}")
# Drive the elapsed-time counter from the calling thread.
_fire_tick()
finally:
try:
pool.shutdown(wait=False, cancel_futures=True)
except TypeError:
pool.shutdown(wait=False)
return willexecutors
@staticmethod
def push_transactions_parallel(willexecutors, *, on_each=None, max_workers=8,
deadline=PUSH_GLOBAL_DEADLINE, on_timeout=None,
on_tick=None, tick_interval=1.0):
"""Push transactions to multiple will-executors concurrently.
Like :meth:`ping_servers_parallel` but for the ``pushtxs`` operation.
Each server keeps a short retry behaviour
(:meth:`push_transactions_to_willexecutor`) so a real transaction is not
lost to a transient hiccup, but servers are contacted in parallel and
results are reported via ``on_each(url, we_dict, ok, exc)`` as they
complete.
A global wall-clock ``deadline`` (seconds) caps the whole operation: if
some servers are still pending when it elapses, we stop waiting, mark
them via ``on_timeout(url, we_dict)`` and return, so the caller (the
wizard) is never stuck behind one unresponsive server. Pass
``deadline=None`` to wait indefinitely (old behaviour).
``on_tick()`` is invoked periodically (every ``tick_interval`` seconds)
**from the calling thread** while waiting for workers. This lets a Qt
caller refresh an elapsed-time counter from the same thread that drives
``on_each`` (so its pyqtSignal repaints reliably), instead of relying on
a separate heartbeat thread whose signal emissions are not marshalled.
Returns ``{url: (ok, exception_or_None)}`` for the servers that
answered in time (timed-out servers are reported via ``on_timeout``).
"""
from concurrent.futures import ThreadPoolExecutor, wait
from concurrent.futures import FIRST_COMPLETED
targets = [(url, we) for url, we in willexecutors.items() if "txs" in we]
results = {}
if not targets:
return results
def _push_one(url, we):
try:
ok = Willexecutors.push_transactions_to_willexecutor(we)
return url, we, ok, None
except Willexecutors.AlreadyPresentException as ape:
return url, we, False, ape
except Exception as e:
return url, we, False, e
def _fire_tick():
if on_tick is not None:
try:
on_tick()
except Exception as cb_err:
_logger.error(f"push on_tick callback error: {cb_err}")
workers = max(1, min(max_workers, len(targets)))
# NOTE: we do not use ``with ThreadPoolExecutor(...)`` here because its
# __exit__ calls shutdown(wait=True), which would block on a hung worker
# and defeat the whole point of the global deadline. We shut the pool
# down without waiting once the deadline elapses; the daemon worker(s)
# stuck on a dead socket will be torn down when their request finally
# times out (PUSH_TIMEOUT), without holding up the wizard.
pool = ThreadPoolExecutor(max_workers=workers, thread_name_prefix="bal-push")
fut_to_url = {pool.submit(_push_one, url, we): (url, we)
for url, we in targets}
start = time.time()
try:
# Poll the futures in short slices so we can call ``on_tick`` from
# THIS thread between waits. ``wait(..., timeout=tick_interval)``
# returns as soon as a future completes OR the slice elapses,
# whichever comes first, so the counter advances ~once per second
# while the parallel push runs.
pending = set(fut_to_url.keys())
while pending:
if deadline is not None and (time.time() - start) >= deadline:
break
slice_timeout = tick_interval
if deadline is not None:
remaining = deadline - (time.time() - start)
slice_timeout = max(0.0, min(tick_interval, remaining))
done, pending = wait(
pending, timeout=slice_timeout, return_when=FIRST_COMPLETED
)
for fut in done:
try:
url, we, ok, exc = fut.result()
except Exception as e:
_logger.error(
f"push_transactions_parallel worker error: {e}"
)
continue
results[url] = (ok, exc)
if on_each is not None:
try:
on_each(url, we, ok, exc)
except Exception as cb_err:
_logger.error(f"push on_each callback error: {cb_err}")
# Drive the elapsed-time counter from the calling thread.
_fire_tick()
# Any server still pending here hit the global deadline.
if pending:
elapsed = time.time() - start
_logger.warning(
f"push global deadline ({deadline}s) reached after "
f"{elapsed:.1f}s; {len(pending)} server(s) "
f"did not answer in time"
)
for fut in pending:
url, we = fut_to_url[fut]
if url in results:
continue
if on_timeout is not None:
try:
on_timeout(url, we)
except Exception as cb_err:
_logger.error(
f"push on_timeout callback error: {cb_err}"
)
finally:
# Do not block on still-running workers (Python 3.9+: cancel queued).
try:
pool.shutdown(wait=False, cancel_futures=True)
except TypeError:
pool.shutdown(wait=False)
return results
@staticmethod
def check_transactions_parallel(items, *, on_each=None, max_workers=8,
deadline=CHECK_GLOBAL_DEADLINE,
on_timeout=None, on_tick=None,
tick_interval=1.0):
"""Check (searchtx) several will-executors concurrently.
Same design as :meth:`push_transactions_parallel`, but for the "Check"
operation: it verifies that each will-executor still holds its
transaction. ``items`` is an iterable of ``(wid, url)`` pairs (one per
will-item that has a will-executor).
Each server is contacted in parallel with a short fail-fast retry
(:meth:`check_transaction`), results are reported via
``on_each(wid, url, result_or_None, exc)`` as they arrive, ``on_tick()``
is called periodically from the calling thread to refresh a counter, and
a global ``deadline`` guarantees the dialog never freezes behind one
unresponsive server (pending servers are reported via
``on_timeout(wid, url)``).
Returns ``{wid: (result_or_None, exception_or_None)}`` for the servers
that answered in time.
"""
from concurrent.futures import ThreadPoolExecutor, wait
from concurrent.futures import FIRST_COMPLETED
targets = [(wid, url) for wid, url in items if url]
results = {}
if not targets:
return results
def _check_one(wid, url):
try:
res = Willexecutors.check_transaction(wid, url)
return wid, url, res, None
except Exception as e:
return wid, url, None, e
def _fire_tick():
if on_tick is not None:
try:
on_tick()
except Exception as cb_err:
_logger.error(f"check on_tick callback error: {cb_err}")
workers = max(1, min(max_workers, len(targets)))
# Manual pool (no ``with``): we must not block on a hung worker when the
# global deadline elapses (see push_transactions_parallel for details).
pool = ThreadPoolExecutor(max_workers=workers, thread_name_prefix="bal-check")
fut_to_target = {pool.submit(_check_one, wid, url): (wid, url)
for wid, url in targets}
start = time.time()
try:
pending = set(fut_to_target.keys())
while pending:
if deadline is not None and (time.time() - start) >= deadline:
break
slice_timeout = tick_interval
if deadline is not None:
remaining = deadline - (time.time() - start)
slice_timeout = max(0.0, min(tick_interval, remaining))
done, pending = wait(
pending, timeout=slice_timeout, return_when=FIRST_COMPLETED
)
for fut in done:
try:
wid, url, res, exc = fut.result()
except Exception as e:
_logger.error(
f"check_transactions_parallel worker error: {e}"
)
continue
results[wid] = (res, exc)
if on_each is not None:
try:
on_each(wid, url, res, exc)
except Exception as cb_err:
_logger.error(f"check on_each callback error: {cb_err}")
# Drive the elapsed-time counter from the calling thread.
_fire_tick()
# Any server still pending here hit the global deadline.
if pending:
elapsed = time.time() - start
_logger.warning(
f"check global deadline ({deadline}s) reached after "
f"{elapsed:.1f}s; {len(pending)} server(s) "
f"did not answer in time"
)
for fut in pending:
wid, url = fut_to_target[fut]
if wid in results:
continue
if on_timeout is not None:
try:
on_timeout(wid, url)
except Exception as cb_err:
_logger.error(
f"check on_timeout callback error: {cb_err}"
)
finally:
try:
pool.shutdown(wait=False, cancel_futures=True)
except TypeError:
pool.shutdown(wait=False)
return results
@staticmethod
def initialize_willexecutor(willexecutor, url, status=None, old_willexecutor=None):
old_willexecutor=old_willexecutor if old_willexecutor is not None else {}
willexecutor["url"] = url
if status is not None:
willexecutor["status"] = status
else:
willexecutor["status"] = old_willexecutor.get("status",willexecutor.get("status","Ko"))
willexecutor["selected"]=Willexecutors.is_selected(old_willexecutor) or willexecutor.get("selected",False)
address = old_willexecutor.get("address", willexecutor.get("address", ""))
if address and not bitcoin.is_address(address, net=constants.net):
_logger.warning(
f"invalid address {address!r} for executor {url}, "
f"falling back to empty"
)
address = ""
willexecutor["address"] = address
willexecutor["promo_code"]=old_willexecutor.get("promo_code",willexecutor.get("promo_code"))
@staticmethod
def download_list(old_willexecutors,welist_server):
try:
welist_server = welist_server if welist_server[-1] == '/' else welist_server+'/'
willexecutors = Willexecutors.send_request(
"get",
f"{welist_server}data/{chainname}?page=0&limit=100",
)
if not isinstance(willexecutors, dict):
_logger.warning(
f"unexpected download_list response type: "
f"{type(willexecutors).__name__}"
)
return {}
for w in willexecutors:
if w not in ("status", "url"):
if not isinstance(willexecutors.get(w), dict):
_logger.warning(
f"malformed entry {w!r} in executor list, "
f"type={type(willexecutors.get(w)).__name__}"
)
continue
Willexecutors.initialize_willexecutor(
willexecutors[w], w, None, old_willexecutors.get(w,None)
)
return willexecutors
except Exception as e:
_logger.error(f"Failed to download willexecutors list: {e}")
return {}
@staticmethod
def get_willexecutors_list_from_json():
try:
with open("willexecutors.json") as f:
willexecutors = json.load(f)
for w in willexecutors:
willexecutor = willexecutors[w]
Willexecutors.initialize_willexecutor(willexecutor, w, "New", False)
# bal_plugin.WILLEXECUTORS.set(willexecutors)
return willexecutors
except Exception as e:
_logger.error(f"error opening willexecutors json: {e}")
return {}
@staticmethod
def check_transaction(txid, url, *, timeout=CHECK_TIMEOUT,
max_retries=CHECK_MAX_RETRIES,
retry_sleep=CHECK_RETRY_SLEEP):
_logger.debug(f"{url}:{txid}")
try:
w = Willexecutors.send_request(
"post", url + "/searchtx", data=txid.encode("ascii"),
timeout=timeout, max_retries=max_retries, retry_sleep=retry_sleep,
)
if not isinstance(w, dict):
_logger.warning(
f"unexpected check_transaction response type "
f"from {url}: {type(w).__name__}"
)
return None
return w
except Exception as e:
_logger.error(f"error contacting {url} for checking txs {e}")
raise e
@staticmethod
def compute_id(willexecutor):
return "{}-{}".format(willexecutor.get("url"), willexecutor.get("chain"))
#class WillExecutor:
# def __init__(
# self,
# url,
# base_fee,
# chain,
# info,
# version,
# status,
# is_selected=False,
# promo_code="",
# ):
# self.url = url
# self.base_fee = base_fee
# self.chain = chain
# self.info = info
# self.version = version
# self.status = status
# self.promo_code = promo_code
# self.is_selected = is_selected
# self.id = self.compute_id()
#
# def from_dict(d):
# return WillExecutor(
# url=d.get("url", "http://localhost:8000"),
# base_fee=d.get("base_fee", 1000),
# chain=d.get("chain", chainname),
# info=d.get("info", ""),
# version=d.get("version", 0),
# status=d.get("status", "Ko"),
# is_selected=d.get("is_selected", "False"),
# promo_code=d.get("promo_code", ""),
# )
#
# def to_dict(self):
# return {
# "url": self.url,
# "base_fee": self.base_fee,
# "chain": self.chain,
# "info": self.info,
# "version": self.version,
# "promo_code": self.promo_code,
# }
#
# def compute_id(self):
# return f"{self.url}-{self.chain}"