from __future__ import annotations import argparse import json import sys from collections.abc import Sequence from .api import ApiClient from .constants import COIN_DISPLAY_SYMBOL, COIN_OPTIONS, POLYGON from .formatters import format_amount_display, format_validation_badge from .lookup import build_lookup_result from .models import LookupResult from .validators import validate_address COIN_ALIASES = { "MATIC": POLYGON, "POLYGON": POLYGON, "BNB": "BSC", } SUPPORTED_COINS = tuple(coin for coin, _ in COIN_OPTIONS) EXTRA_COIN_ALIASES = tuple(sorted(alias for alias in COIN_ALIASES if alias not in SUPPORTED_COINS)) def launch_tui() -> None: from .app import CheckAddyApp CheckAddyApp().run() def normalize_coin(value: str) -> str: coin = value.strip().upper() normalized = COIN_ALIASES.get(coin, coin) if normalized not in SUPPORTED_COINS: supported = ", ".join(SUPPORTED_COINS + EXTRA_COIN_ALIASES) raise argparse.ArgumentTypeError(f"unsupported coin '{value}'. Supported: {supported}") return normalized def detect_matching_coins(address: str) -> list[tuple[str, str]]: matches: list[tuple[str, str]] = [] for coin in SUPPORTED_COINS: is_valid, reason = validate_address(coin, address) if is_valid: matches.append((coin, reason)) return matches def resolve_coin(address: str, requested_coin: str | None) -> tuple[str | None, str | None]: if requested_coin is not None: return requested_coin, None matches = detect_matching_coins(address) if not matches: return None, "Address does not match any supported network format." if len(matches) == 1: return matches[0][0], None choices = ", ".join(coin for coin, _ in matches) return None, f"Address is ambiguous across networks: {choices}. Re-run with --coin." def render_text_result(result: LookupResult) -> str: tx_count = str(result.tx_count) if result.tx_count is not None else "N/A" status = "api skipped" if result.api_skipped else "api error" if result.api_error else "ok" lines = [ "checkaddy", f"Status: {status}", f"Coin: {result.coin} ({COIN_DISPLAY_SYMBOL[result.coin]})", f"Address: {result.address}", f"Validation: {format_validation_badge(result.is_valid_format, result.validation_reason)}", f"Confirmed balance: {format_amount_display(result.coin, result.confirmed_balance)}", f"Unconfirmed balance: {format_amount_display(result.coin, result.unconfirmed_balance)}", f"Total received: {format_amount_display(result.coin, result.total_received)}", f"Total sent: {format_amount_display(result.coin, result.total_sent)}", f"Transaction count: {tx_count}", f"Data source: {result.data_source}", f"Explorer: {result.explorer_url}", f"Fetched at UTC: {result.fetched_at_utc}", ] if result.api_error: lines.append(f"API error: {result.api_error}") if result.api_skipped: lines.append("Remote lookup skipped because the address format is invalid.") return "\n".join(lines) def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="checkaddy", description=( "Check a public cryptocurrency address from the terminal. " "Run without arguments to launch the Textual TUI." ), ) parser.add_argument("address", nargs="?", help="public wallet address to validate and inspect") parser.add_argument( "-c", "--coin", type=normalize_coin, help="network to check: BTC, LTC, DOGE, DASH, BCH, ETH, BSC, POLYGON/MATIC", ) parser.add_argument("--json", action="store_true", help="print normalized JSON output") parser.add_argument("--tui", action="store_true", help="launch the Textual TUI") return parser def run_lookup(address: str, coin: str, *, as_json: bool) -> int: client = ApiClient() try: result = build_lookup_result(client, coin, address) finally: client.close() if as_json: print(json.dumps(result.as_dict(), indent=2, ensure_ascii=False)) else: print(render_text_result(result)) if result.api_skipped: return 2 if result.api_error: return 1 return 0 def main(argv: Sequence[str] | None = None) -> int: args_list = list(sys.argv[1:] if argv is None else argv) if not args_list: launch_tui() return 0 parser = build_parser() args = parser.parse_args(args_list) if args.tui: launch_tui() return 0 if not args.address: parser.error("address is required unless running without arguments or using --tui") coin, error = resolve_coin(args.address.strip(), args.coin) if error is not None: if args.json: print(json.dumps({"error": error}, indent=2), file=sys.stderr) else: print(f"checkaddy: {error}", file=sys.stderr) return 2 assert coin is not None return run_lookup(args.address.strip(), coin, as_json=args.json) if __name__ == "__main__": raise SystemExit(main())