Implement CLI functionality and enhance README instructions
This commit is contained in:
156
checkaddy_app/cli.py
Normal file
156
checkaddy_app/cli.py
Normal file
@@ -0,0 +1,156 @@
|
||||
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())
|
||||
Reference in New Issue
Block a user