#!/usr/bin/env python3 from __future__ import annotations import argparse import json import os import sys import time from pathlib import Path from typing import Any, Dict, List ROOT = Path(__file__).resolve().parents[2] CONFIG = ROOT / "config" / "extraction" REPORTS = ROOT / "reports" / "extraction" INVENTORY = CONFIG / "additional-wallet-inventory.json" OUT_VALIDATION = REPORTS / "source-to-cex-offchain-sink-validation-latest.json" SOURCE_TO_CEX_PLAN = REPORTS / "source-to-cex-execution-plan-latest.json" REQUIRED_ENV = { "label": "SOURCE_TO_CEX_SINK_LABEL", "platform": "SOURCE_TO_CEX_SINK_PLATFORM", "account_type": "SOURCE_TO_CEX_SINK_ACCOUNT_TYPE", "preferred_deposit_asset": "SOURCE_TO_CEX_SINK_PREFERRED_DEPOSIT_ASSET", "deposit_chain_id": "SOURCE_TO_CEX_SINK_DEPOSIT_CHAIN_ID", "deposit_chain_name": "SOURCE_TO_CEX_SINK_DEPOSIT_CHAIN_NAME", "deposit_address": "SOURCE_TO_CEX_SINK_DEPOSIT_ADDRESS", } def now() -> str: return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) def load_json(path: Path) -> Dict[str, Any]: return json.loads(path.read_text()) def write_json(path: Path, data: Dict[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(data, indent=2) + "\n") def first_missing_env() -> List[str]: missing = [] for env_name in REQUIRED_ENV.values(): if not os.environ.get(env_name): missing.append(env_name) return missing def placeholder_like(text: str) -> bool: lowered = text.strip().lower() return lowered in {"", "unknown", "example-exchange", "planned"} or lowered.startswith("example") def validate_inventory() -> Dict[str, Any]: inv = load_json(INVENTORY) plan = load_json(SOURCE_TO_CEX_PLAN) if SOURCE_TO_CEX_PLAN.exists() else {} included = [row for row in inv.get("offchain_accounts", []) if row.get("include_in_baseline")] sink_rows: List[Dict[str, Any]] = [] blocking: List[str] = [] warnings: List[str] = [] for row in included: issues = [] if placeholder_like(str(row.get("label", ""))): issues.append("label still looks like placeholder data") if placeholder_like(str(row.get("platform", ""))): issues.append("platform is missing or placeholder") if not row.get("enabled_for_production_handoff"): issues.append("enabled_for_production_handoff is false") if row.get("operational_status") != "enabled": issues.append("operational_status is not enabled") if not str(row.get("deposit_address", "")).strip(): issues.append("deposit_address missing") if not row.get("preferred_deposit_asset"): issues.append("preferred_deposit_asset missing") if row.get("min_packet_usd") is None or row.get("max_packet_usd") is None: issues.append("packet bounds missing") if row.get("preferred_deposit_asset") and row.get("accepted_deposit_assets"): if row["preferred_deposit_asset"] not in row["accepted_deposit_assets"]: warnings.append( f"{row.get('label')}: preferred_deposit_asset is not listed in accepted_deposit_assets" ) if issues: blocking.extend(f"{row.get('label')}: {item}" for item in issues) sink_rows.append( { "label": row.get("label"), "platform": row.get("platform"), "account_type": row.get("account_type"), "operational_status": row.get("operational_status"), "enabled_for_production_handoff": bool(row.get("enabled_for_production_handoff")), "preferred_deposit_asset": row.get("preferred_deposit_asset"), "deposit_chain_id": str(row.get("deposit_chain_id", "")), "deposit_chain_name": row.get("deposit_chain_name"), "deposit_address_present": bool(str(row.get("deposit_address", "")).strip()), "min_packet_usd": row.get("min_packet_usd"), "max_packet_usd": row.get("max_packet_usd"), "accepted_deposit_assets": row.get("accepted_deposit_assets", []), } ) if not included: blocking.append("no off-chain sink rows are currently included in baseline scope") payload = { "generated_at": now(), "inventory_path": str(INVENTORY.relative_to(ROOT)), "mainnet_funding_posture": plan.get("mainnet_funding_posture"), "included_sink_count": len(included), "ready": len(blocking) == 0, "blocking_issues": blocking, "warnings": warnings, "sinks": sink_rows, } write_json(OUT_VALIDATION, payload) return payload def import_from_env(enable_production: bool) -> Dict[str, Any]: missing = first_missing_env() if missing: raise SystemExit( "Missing required env vars for sink import: " + ", ".join(missing) ) inv = load_json(INVENTORY) row = { "label": os.environ["SOURCE_TO_CEX_SINK_LABEL"], "platform": os.environ["SOURCE_TO_CEX_SINK_PLATFORM"], "account_type": os.environ["SOURCE_TO_CEX_SINK_ACCOUNT_TYPE"], "operational_status": os.environ.get("SOURCE_TO_CEX_SINK_OPERATIONAL_STATUS", "enabled"), "enabled_for_production_handoff": os.environ.get("SOURCE_TO_CEX_SINK_ENABLE_HANDOFF", "1") not in {"0", "false", "False"}, "accepted_deposit_assets": [ item.strip() for item in os.environ.get("SOURCE_TO_CEX_SINK_ACCEPTED_DEPOSIT_ASSETS", os.environ["SOURCE_TO_CEX_SINK_PREFERRED_DEPOSIT_ASSET"]).split(",") if item.strip() ], "preferred_deposit_asset": os.environ["SOURCE_TO_CEX_SINK_PREFERRED_DEPOSIT_ASSET"], "deposit_chain_id": os.environ["SOURCE_TO_CEX_SINK_DEPOSIT_CHAIN_ID"], "deposit_chain_name": os.environ["SOURCE_TO_CEX_SINK_DEPOSIT_CHAIN_NAME"], "deposit_address": os.environ["SOURCE_TO_CEX_SINK_DEPOSIT_ADDRESS"], "min_packet_usd": int(os.environ.get("SOURCE_TO_CEX_SINK_MIN_PACKET_USD", "1000")), "max_packet_usd": int(os.environ.get("SOURCE_TO_CEX_SINK_MAX_PACKET_USD", "250000")), "slippage_ceiling_bps": int(os.environ.get("SOURCE_TO_CEX_SINK_SLIPPAGE_CEILING_BPS", "100")), "asset_balances": [ { "symbol": os.environ.get("SOURCE_TO_CEX_SINK_BALANCE_SYMBOL", os.environ["SOURCE_TO_CEX_SINK_PREFERRED_DEPOSIT_ASSET"]), "amount": int(os.environ.get("SOURCE_TO_CEX_SINK_BALANCE_AMOUNT", "0")), "estimated_usd": int(os.environ.get("SOURCE_TO_CEX_SINK_BALANCE_ESTIMATED_USD", "0")), "chain_id": "offchain", "chain_name": "Off-chain / custodial", "notes": "Imported from environment for production sink onboarding.", } ], "include_in_baseline": True, "notes": os.environ.get("SOURCE_TO_CEX_SINK_NOTES", "Imported from environment for source-to-CEX production handoff."), } current = inv.get("offchain_accounts", []) replaced = False for idx, existing in enumerate(current): if existing.get("label") == row["label"]: current[idx] = row replaced = True break if not replaced: current.insert(0, row) inv["offchain_accounts"] = current write_json(INVENTORY, inv) if enable_production: policy_path = CONFIG / "source-to-cex-production-policy.json" policy = load_json(policy_path) policy["production_enabled"] = True write_json(policy_path, policy) return { "generated_at": now(), "inventory_path": str(INVENTORY.relative_to(ROOT)), "imported_sink_label": row["label"], "production_enabled_set": enable_production, "accepted_deposit_assets": row["accepted_deposit_assets"], "preferred_deposit_asset": row["preferred_deposit_asset"], } def print_env_template() -> int: print( "\n".join( [ "SOURCE_TO_CEX_SINK_LABEL=", "SOURCE_TO_CEX_SINK_PLATFORM=", "SOURCE_TO_CEX_SINK_ACCOUNT_TYPE=cex", "SOURCE_TO_CEX_SINK_PREFERRED_DEPOSIT_ASSET=USDC", "SOURCE_TO_CEX_SINK_ACCEPTED_DEPOSIT_ASSETS=USDC", "SOURCE_TO_CEX_SINK_DEPOSIT_CHAIN_ID=1", "SOURCE_TO_CEX_SINK_DEPOSIT_CHAIN_NAME=Ethereum Mainnet", "SOURCE_TO_CEX_SINK_DEPOSIT_ADDRESS=", "SOURCE_TO_CEX_SINK_OPERATIONAL_STATUS=enabled", "SOURCE_TO_CEX_SINK_ENABLE_HANDOFF=1", "SOURCE_TO_CEX_SINK_MIN_PACKET_USD=1000", "SOURCE_TO_CEX_SINK_MAX_PACKET_USD=250000", "SOURCE_TO_CEX_SINK_SLIPPAGE_CEILING_BPS=100", "SOURCE_TO_CEX_SINK_NOTES=", ] ) ) return 0 def main() -> int: parser = argparse.ArgumentParser() sub = parser.add_subparsers(dest="cmd", required=True) sub.add_parser("validate") import_parser = sub.add_parser("import-env") import_parser.add_argument("--enable-production", action="store_true") sub.add_parser("print-env-template") args = parser.parse_args() if args.cmd == "validate": payload = validate_inventory() print(json.dumps(payload, indent=2)) return 0 if payload["ready"] else 1 if args.cmd == "import-env": payload = import_from_env(args.enable_production) print(json.dumps(payload, indent=2)) return 0 if args.cmd == "print-env-template": return print_env_template() return 1 if __name__ == "__main__": raise SystemExit(main())