From 3a11a49dd39051a1a4464a9c281c3235ec4ea1dd Mon Sep 17 00:00:00 2001 From: Landyn Date: Thu, 21 May 2026 12:05:49 -0500 Subject: [PATCH 1/3] logging: tag provider RPC calls, log backend lineup at startup --- allways/chain_providers/__init__.py | 5 +++++ allways/chain_providers/base.py | 4 ++++ allways/chain_providers/bitcoin.py | 28 ++++++++++++++++++---------- allways/chain_providers/subtensor.py | 11 ++++++++--- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/allways/chain_providers/__init__.py b/allways/chain_providers/__init__.py index 5d98db7b..1538be36 100644 --- a/allways/chain_providers/__init__.py +++ b/allways/chain_providers/__init__.py @@ -42,4 +42,9 @@ def create_chain_providers(check: bool = False, require_send: bool = True, **kwa raise RuntimeError(f'{cls.__name__} failed startup check: {e}') from e bt.logging.warning(f'{cls.__name__} not available: {e}') + if check and providers: + bt.logging.info('Chain providers ready:') + for chain_id, provider in providers.items(): + bt.logging.info(f' {chain_id} → {provider.describe()}') + return providers diff --git a/allways/chain_providers/base.py b/allways/chain_providers/base.py index 0f12bed7..e36dbf8b 100644 --- a/allways/chain_providers/base.py +++ b/allways/chain_providers/base.py @@ -34,6 +34,10 @@ class ChainProvider(ABC): @abstractmethod def get_chain(self) -> ChainDefinition: ... + def describe(self) -> str: + """One-line summary of the backend/API this provider talks to, for startup logs.""" + return self.get_chain().name + @abstractmethod def check_connection(self, **kwargs) -> None: """Verify the chain provider can reach its backend (RPC node, subtensor, etc). diff --git a/allways/chain_providers/bitcoin.py b/allways/chain_providers/bitcoin.py index 2decaaaf..949f155e 100644 --- a/allways/chain_providers/bitcoin.py +++ b/allways/chain_providers/bitcoin.py @@ -17,6 +17,9 @@ ADDR_TYPE_P2WPKH = 'p2wpkh' ADDR_TYPE_P2TR = 'p2tr' +LOG_RPC = '[BTC-RPC]' +LOG_ESPLORA = '[Esplora]' + def detect_address_type(address: str) -> str: """Detect Bitcoin address type from its prefix.""" @@ -149,6 +152,11 @@ def _send_error(self, msg: str) -> None: def get_chain(self) -> ChainDefinition: return CHAIN_BTC + def describe(self) -> str: + if self.mode == 'lightweight': + return f'Esplora API ({self.network})' + return f'Core RPC {self.rpc_url} (primary) + Esplora (fallback)' + def check_connection(self, require_send: bool = True) -> None: if self.mode == 'lightweight': if require_send and not os.environ.get('BTC_PRIVATE_KEY'): @@ -161,7 +169,7 @@ def check_connection(self, require_send: bool = True) -> None: resp = self.btc_api_get('/blocks/tip/height', timeout=10) resp.raise_for_status() tip = int(resp.text.strip()) - bt.logging.success(f'BTC lightweight mode: network={self.network}, Esplora tip={tip}') + bt.logging.success(f'{LOG_ESPLORA} connected: network={self.network}, tip={tip}') except Exception as e: raise ConnectionError(f'Cannot reach Esplora API: {e}') from e return @@ -169,7 +177,7 @@ def check_connection(self, require_send: bool = True) -> None: result = self.rpc_call('getblockchaininfo', []) if result is None: raise ConnectionError(f'Cannot reach Bitcoin RPC at {self.rpc_url}') - bt.logging.success(f'BTC RPC connected: chain={result.get("chain")}, blocks={result.get("blocks")}') + bt.logging.success(f'{LOG_RPC} connected: chain={result.get("chain")}, blocks={result.get("blocks")}') def rpc_call(self, method: str, params: Optional[list] = None) -> Optional[dict]: """Generic JSON-RPC helper for BTC Core.""" @@ -187,11 +195,11 @@ def rpc_call(self, method: str, params: Optional[list] = None) -> Optional[dict] response.raise_for_status() result = response.json() if result.get('error'): - bt.logging.error(f'BTC RPC error ({method}): {result["error"]}') + bt.logging.error(f'{LOG_RPC} error ({method}): {result["error"]}') return None return result.get('result') except Exception as e: - bt.logging.error(f'BTC RPC call failed ({method}): {e}') + bt.logging.error(f'{LOG_RPC} call failed ({method}): {e}') return None def fetch_matching_tx( @@ -213,9 +221,9 @@ def fetch_matching_tx( result = self.rpc_verify_transaction(tx_hash, expected_recipient, expected_amount) if result is not None: - bt.logging.debug(f'BTC verify: served by local RPC (tx {tx_hash[:16]}...)') + bt.logging.debug(f'{LOG_RPC} served tx {tx_hash[:16]}...') return result - bt.logging.debug(f'BTC verify: local RPC had no match, falling back to Esplora (tx {tx_hash[:16]}...)') + bt.logging.debug(f'{LOG_RPC} no match for tx {tx_hash[:16]}..., falling back to Esplora') return self.api_verify_transaction(tx_hash, expected_recipient, expected_amount) def rpc_verify_transaction( @@ -257,7 +265,7 @@ def rpc_verify_transaction( ) bt.logging.warning( - f'BTC RPC: tx {tx_hash[:16]}... has no vout paying {expected_recipient} >= {expected_amount} sat' + f'{LOG_RPC} tx {tx_hash[:16]}... has no vout paying {expected_recipient} >= {expected_amount} sat' ) return None @@ -303,7 +311,7 @@ def api_verify_transaction( try: resp = self.btc_api_get(f'/tx/{tx_hash}', timeout=15) if resp.status_code == 404: - bt.logging.debug(f'BTC Esplora: tx {tx_hash[:16]}... not found (404)') + bt.logging.debug(f'{LOG_ESPLORA} tx {tx_hash[:16]}... not found (404)') return None resp.raise_for_status() data = resp.json() @@ -329,7 +337,7 @@ def api_verify_transaction( if status_resp.ok and status_resp.json().get('in_best_chain') is False: return None # block was reorged out except Exception as e: - bt.logging.debug(f'canonical-chain check skipped for {tx_hash}: {e}') + bt.logging.debug(f'{LOG_ESPLORA} canonical-chain check skipped for {tx_hash}: {e}') for vout in data.get('vout', []): addr = vout.get('scriptpubkey_address', '') @@ -351,7 +359,7 @@ def api_verify_transaction( ) bt.logging.warning( - f'BTC Esplora: tx {tx_hash[:16]}... has no vout paying {expected_recipient} >= {expected_amount} sat' + f'{LOG_ESPLORA} tx {tx_hash[:16]}... has no vout paying {expected_recipient} >= {expected_amount} sat' ) return None except (requests.ConnectionError, requests.Timeout) as e: diff --git a/allways/chain_providers/subtensor.py b/allways/chain_providers/subtensor.py index c533b691..3e4dc5e0 100644 --- a/allways/chain_providers/subtensor.py +++ b/allways/chain_providers/subtensor.py @@ -8,6 +8,8 @@ from allways.chain_providers.base import ChainProvider, ProviderUnreachableError, TransactionInfo from allways.chains import CHAIN_TAO, ChainDefinition +LOG_SUB = '[Subtensor]' + class SubtensorProvider(ChainProvider): """TAO chain provider using bt.Subtensor and substrate-interface. @@ -30,10 +32,13 @@ def __init__(self, subtensor: bt.Subtensor, wallet: Optional['bt.Wallet'] = None def get_chain(self) -> ChainDefinition: return CHAIN_TAO + def describe(self) -> str: + return f'Subtensor {self.subtensor.chain_endpoint}' + def check_connection(self, **kwargs) -> None: try: block = self.subtensor.get_current_block() - bt.logging.success(f'Subtensor connected: block={block}') + bt.logging.success(f'{LOG_SUB} connected: block={block}') except Exception as e: raise ConnectionError(f'Cannot reach Subtensor: {e}') from e @@ -222,10 +227,10 @@ def fetch_matching_tx( if tx_hash_seen: bt.logging.warning( - f'TAO scan: tx {tx_hash[:16]}... found but no transfer pays {expected_recipient} >= {expected_amount} rao' + f'{LOG_SUB} scan: tx {tx_hash[:16]}... found but no transfer pays {expected_recipient} >= {expected_amount} rao' ) else: - bt.logging.debug(f'TAO scan: tx {tx_hash[:16]}... not found in scan window') + bt.logging.debug(f'{LOG_SUB} scan: tx {tx_hash[:16]}... not found in scan window') return None except ProviderUnreachableError: raise From 65e689b486cf95be7ce0adf944d660ab02d1a2fd Mon Sep 17 00:00:00 2001 From: Landyn Date: Thu, 21 May 2026 12:08:40 -0500 Subject: [PATCH 2/3] logging: name configured Esplora hosts in startup summary --- allways/chain_providers/bitcoin.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/allways/chain_providers/bitcoin.py b/allways/chain_providers/bitcoin.py index 949f155e..9c76d48a 100644 --- a/allways/chain_providers/bitcoin.py +++ b/allways/chain_providers/bitcoin.py @@ -1,6 +1,7 @@ import os import time from typing import Any, Optional, Tuple +from urllib.parse import urlparse import base58 import bech32 @@ -153,9 +154,10 @@ def get_chain(self) -> ChainDefinition: return CHAIN_BTC def describe(self) -> str: + hosts = ', '.join(urlparse(base).netloc or base for base, _ in self.btc_api_bases()) if self.mode == 'lightweight': - return f'Esplora API ({self.network})' - return f'Core RPC {self.rpc_url} (primary) + Esplora (fallback)' + return f'Esplora API ({self.network}): {hosts}' + return f'Core RPC {self.rpc_url} (primary) + Esplora fallback: {hosts}' def check_connection(self, require_send: bool = True) -> None: if self.mode == 'lightweight': From 87b3d4558a79ccc6a139752ccce27378753cb36e Mon Sep 17 00:00:00 2001 From: Landyn Date: Thu, 21 May 2026 12:10:40 -0500 Subject: [PATCH 3/3] logging: short host tags in Esplora failover narration --- allways/chain_providers/bitcoin.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/allways/chain_providers/bitcoin.py b/allways/chain_providers/bitcoin.py index 9c76d48a..a15fde34 100644 --- a/allways/chain_providers/bitcoin.py +++ b/allways/chain_providers/bitcoin.py @@ -93,6 +93,13 @@ def parse_esplora_urls(raw: str, auth_header: str = 'Authorization') -> list[tup return bases +def esplora_tag(base: str) -> str: + """Short, log-friendly host label for an Esplora endpoint (e.g. 'blockstream', 'gomaestro-api').""" + host = (urlparse(base).netloc or base).split(':')[0].removeprefix('www.') + parts = host.split('.') + return parts[-2] if len(parts) >= 2 else host + + class BitcoinProvider(ChainProvider): """Bitcoin chain provider. Supports two modes: @@ -439,25 +446,26 @@ def btc_api_request(self, method: str, path: str, timeout: int, **kwargs) -> req last_err: Optional[Exception] = None for i, (base, headers) in enumerate(bases): pos = f'[{i + 1}/{len(bases)}]' - nxt = bases[i + 1][0] if i + 1 < len(bases) else None - tail = f'falling back to next provider: {nxt}' if nxt else 'no providers left, giving up' + tag = esplora_tag(base) + nxt = esplora_tag(bases[i + 1][0]) if i + 1 < len(bases) else None + tail = f'falling back to: {nxt}' if nxt else 'no providers left, giving up' try: resp = self.http.request(method, f'{base}{path}', timeout=timeout, headers=headers, **kwargs) except Exception as e: last_err = e - bt.logging.warning(f'Esplora {pos} {base}{path} → request error: {e}; {tail}') + bt.logging.warning(f'Esplora {pos} {tag}{path} → request error: {e}; {tail}') continue reason = self.failover_reason(resp) if reason: last_err = requests.HTTPError(f'{base}{path}: {resp.status_code}', response=resp) - bt.logging.warning(f'Esplora {pos} {base}{path} → {reason}; {tail}') + bt.logging.warning(f'Esplora {pos} {tag}{path} → {reason}; {tail}') continue if resp.status_code >= 400 and resp.status_code != 404: - bt.logging.warning(f'Esplora {pos} {base}{path} → HTTP {resp.status_code}: {resp.text[:200].strip()}') + bt.logging.warning(f'Esplora {pos} {tag}{path} → HTTP {resp.status_code}: {resp.text[:200].strip()}') elif i > 0: - bt.logging.info(f'Esplora {pos} {base}{path} → {resp.status_code} (served after {i} fallback(s))') + bt.logging.info(f'Esplora {pos} {tag}{path} → {resp.status_code} (served after {i} fallback(s))') return resp raise last_err or RuntimeError('all BTC APIs failed')