234 lines
9.5 KiB
Python
234 lines
9.5 KiB
Python
#!/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())
|