diff --git a/willexecutors.py b/willexecutors.py index b86655d..2d7d1d1 100644 --- a/willexecutors.py +++ b/willexecutors.py @@ -1,3 +1,13 @@ +"""Willexecutors module for BAL Electrum Plugin. + +This module provides functionality to manage will executor servers that +broadcast timelocked transactions at the appropriate locktime. + +Classes: + Willexecutors: Static class for managing executor list and communication + WillExecutor: Data class representing a single will executor +""" + import json from datetime import datetime import time @@ -18,8 +28,19 @@ chainname = constants.net.NET_NAME if constants.net.NET_NAME != "mainnet" else " class Willexecutors: + """Static class managing will executor servers. + + Provides methods to save/load configurations, communicate via HTTP, + push transactions, and download executor lists from remote sources. + """ def save(bal_plugin, willexecutors): + """Save will executor configuration to plugin settings. + + Args: + bal_plugin: BAL plugin instance + willexecutors: Dictionary of executor configs keyed by URL + """ _logger.debug(f"save {willexecutors},{chainname}") aw = bal_plugin.WILLEXECUTORS.get() aw[chainname] = willexecutors @@ -30,6 +51,18 @@ class Willexecutors: def get_willexecutors( bal_plugin, update=False, bal_window=False, force=False, task=True ): + """Retrieve and update the list of available will executors. + + Args: + bal_plugin: BAL plugin instance + update: If True, ping servers to refresh data + bal_window: GUI window for user prompts + force: Force update regardless of cached data age + task: Run as background task if True + + Returns: + dict: Sorted dictionary of executor configurations + """ willexecutors = bal_plugin.WILLEXECUTORS.get() willexecutors = willexecutors.get(chainname, {}) to_del = [] @@ -79,6 +112,15 @@ class Willexecutors: return w_sorted def is_selected(willexecutor, value=None): + """Check or set the selection status of an executor. + + Args: + willexecutor: Executor configuration dictionary + value: Optional boolean to set selection status + + Returns: + bool: Current selection status + """ if not willexecutor: return False if value is not None: @@ -90,6 +132,15 @@ class Willexecutors: return False def get_willexecutor_transactions(will, force=False): + """Collect transactions grouped by executor for valid, complete wills. + + Args: + will: Dictionary of will items keyed by ID + force: Include already-pushed transactions + + Returns: + dict: Executors with their aggregated transactions + """ willexecutors = {} for wid, willitem in will.items(): if willitem.get_status("VALID"): @@ -127,6 +178,22 @@ class Willexecutors: def send_request( method, url, data=None, *, timeout=10, handle_response=None, count_reply=0 ): + """Send HTTP request to a will executor server. + + Args: + method: HTTP method (get/post) + url: Target server URL + data: Request payload + timeout: Timeout in seconds + handle_response: Response processing function + count_reply: Retry counter for timeouts + + Returns: + Server response object + + Raises: + Exception: On connection errors or invalid method + """ network = Network.get_instance() if not network: raise Exception("You are offline.") @@ -178,12 +245,28 @@ class Willexecutors: return response def get_we_url_from_response(resp): + """Extract base URL from response object. + + Args: + resp: Response object with url attribute + + Returns: + str: Base URL without trailing paths + """ url_slices = str(resp.url).split("/") if len(url_slices) > 2: url_slices = url_slices[:-2] return "/".join(url_slices) async def handle_response(resp: ClientResponse): + """Parse JSON response from executor server. + + Args: + resp: aiohttp ClientResponse object + + Returns: + Parsed JSON data or raw text on parse failure + """ r = await resp.text() try: @@ -197,9 +280,21 @@ class Willexecutors: return r class AlreadyPresentException(Exception): + """Raised when transactions already exist on executor server.""" pass def push_transactions_to_willexecutor(willexecutor): + """Push timelocked transactions to an executor server for broadcast. + + Args: + willexecutor: Dict containing url and txs keys + + Returns: + bool: True on success, False on failure + + Raises: + AlreadyPresentException: If transactions already exist + """ out = True try: _logger.debug(f"{willexecutor['url']}: {willexecutor['txs']}") @@ -225,10 +320,24 @@ class Willexecutors: return out def ping_servers(willexecutors): + """Ping all executor servers to update their information. + + Args: + willexecutors: Dictionary of executor configurations + """ for url, we in willexecutors.items(): Willexecutors.get_info_task(url, we) def get_info_task(url, willexecutor): + """Fetch current information from a single executor server. + + Args: + url: Executor server URL + willexecutor: Configuration dict to update + + Returns: + Updated willexecutor dict with status, base_fee, address + """ w = None try: _logger.info("GETINFO_WILLEXECUTOR") @@ -249,6 +358,14 @@ class Willexecutors: return willexecutor def initialize_willexecutor(willexecutor, url, status=None, old_willexecutor={}): + """Initialize or merge executor configuration preserving user settings. + + Args: + willexecutor: New executor configuration dict + url: Executor server URL + status: Optional status override + old_willexecutor: Previous config to preserve user preferences + """ willexecutor["url"] = url if status is not None: willexecutor["status"] = status @@ -261,6 +378,14 @@ class Willexecutors: def download_list(old_willexecutors): + """Download latest executor list from remote source. + + Args: + old_willexecutors: Existing configs to merge with new list + + Returns: + dict: Merged executor configurations + """ try: willexecutors = Willexecutors.send_request( "get", @@ -281,6 +406,11 @@ class Willexecutors: return {} def get_willexecutors_list_from_json(): + """Load executor list from local willexecutors.json file. + + Returns: + dict: Executor configurations from JSON file + """ try: with open("willexecutors.json") as f: willexecutors = json.load(f) @@ -295,6 +425,15 @@ class Willexecutors: return {} def check_transaction(txid, url): + """Check if a transaction exists on executor server. + + Args: + txid: Transaction ID string + url: Executor server URL + + Returns: + Server response about transaction existence + """ _logger.debug(f"{url}:{txid}") try: w = Willexecutors.send_request( @@ -306,10 +445,31 @@ class Willexecutors: raise e def compute_id(willexecutor): + """Compute unique identifier for an executor. + + Args: + willexecutor: Executor configuration dict + + Returns: + str: Unique ID combining URL and chain name + """ return "{}-{}".format(willexecutor.get("url"), willexecutor.get("chain")) class WillExecutor: + """Data class representing a single will executor server. + + Attributes: + url: Executor server URL + base_fee: Fixed fee in satoshis + chain: Bitcoin chain name + info: Additional executor information + version: Plugin version compatibility + status: Connection status + is_selected: User selection flag + promo_code: Promotional discount code + """ + def __init__( self, url, @@ -321,6 +481,18 @@ class WillExecutor: is_selected=False, promo_code="", ): + """Initialize a new WillExecutor instance. + + Args: + url: Executor server URL + base_fee: Fixed fee in satoshis + chain: Bitcoin chain name + info: Additional executor information + version: Plugin version compatibility + status: Connection status (OK/Ko) + is_selected: Whether user has selected this executor + promo_code: Promotional discount code + """ self.url = url self.base_fee = base_fee self.chain = chain @@ -332,6 +504,14 @@ class WillExecutor: self.id = self.compute_id() def from_dict(d): + """Create WillExecutor instance from a dictionary. + + Args: + d: Dictionary containing executor data fields + + Returns: + WillExecutor: New instance with defaults for missing fields + """ return WillExecutor( url=d.get("url", "http://localhost:8000"), base_fee=d.get("base_fee", 1000), @@ -344,6 +524,11 @@ class WillExecutor: ) def to_dict(self): + """Convert WillExecutor to dictionary representation. + + Returns: + dict: Serializable representation excluding computed fields + """ return { "url": self.url, "base_fee": self.base_fee, @@ -354,4 +539,9 @@ class WillExecutor: } def compute_id(self): + """Generate unique identifier for this executor. + + Returns: + str: Unique ID from URL and chain name + """ return f"{self.url}-{self.chain}"