Files
proxmox/scripts/lib/source_to_cex_offchain_sink_tool.py

234 lines
9.5 KiB
Python
Raw Normal View History

#!/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())