All checks were successful
Deploy to Phoenix / validate (push) Successful in 1m11s
Deploy to Phoenix / deploy (push) Successful in 43s
Deploy to Phoenix / deploy-atomic-swap-dapp (push) Successful in 1m32s
phoenix-deploy Deployed to cloudflare-sync
Deploy to Phoenix / cloudflare (push) Successful in 38s
480 lines
22 KiB
Python
480 lines
22 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from decimal import Decimal, ROUND_CEILING, getcontext
|
|
from pathlib import Path
|
|
|
|
getcontext().prec = 42
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
LATEST_SNAPSHOT = ROOT / "reports" / "status" / "mainnet-cwusdc-usdc-preflight-latest.json"
|
|
POLICY_PATH = ROOT / "config" / "extraction" / "mainnet-cwusdc-usdc-support-policy.json"
|
|
ROOT_ENV_PATH = ROOT / ".env"
|
|
SMOM_ENV_PATH = ROOT / "smom-dbis-138" / ".env"
|
|
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
|
|
DEFAULT_CWUSDC = "0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a"
|
|
DEFAULT_USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
|
SIX_DECIMALS = Decimal(10) ** 6
|
|
ADDRESS_RE = re.compile(r"0x[a-fA-F0-9]{40}")
|
|
UINT_RE = re.compile(r"\b\d+\b")
|
|
|
|
|
|
def load_json(path: Path) -> dict:
|
|
return json.loads(path.read_text())
|
|
|
|
|
|
def load_env_file(path: Path) -> dict[str, str]:
|
|
values: dict[str, str] = {}
|
|
if not path.exists():
|
|
return values
|
|
for raw_line in path.read_text().splitlines():
|
|
line = raw_line.strip()
|
|
if not line or line.startswith("#") or "=" not in line:
|
|
continue
|
|
key, value = line.split("=", 1)
|
|
values[key.strip()] = value.strip().strip('"').strip("'")
|
|
return values
|
|
|
|
|
|
def merged_env_values() -> dict[str, str]:
|
|
values: dict[str, str] = {}
|
|
values.update(load_env_file(ROOT_ENV_PATH))
|
|
values.update(load_env_file(SMOM_ENV_PATH))
|
|
return values
|
|
|
|
|
|
def resolve_env_value(key: str, env_values: dict[str, str], seen: set[str] | None = None) -> str:
|
|
if seen is None:
|
|
seen = set()
|
|
if key in seen:
|
|
return env_values.get(key, "")
|
|
seen.add(key)
|
|
value = os.environ.get(key) or env_values.get(key, "")
|
|
if value.startswith("${") and value.endswith("}"):
|
|
inner = value[2:-1]
|
|
target = inner.split(":-", 1)[0]
|
|
fallback = inner.split(":-", 1)[1] if ":-" in inner else ""
|
|
resolved = resolve_env_value(target, env_values, seen)
|
|
return resolved or fallback
|
|
return value.rstrip("\r\n")
|
|
|
|
|
|
def normalize_units(raw: int, decimals: int = 6) -> Decimal:
|
|
return Decimal(raw) / (Decimal(10) ** decimals)
|
|
|
|
|
|
def units_to_raw(units: Decimal, decimals: int = 6) -> int:
|
|
scale = Decimal(10) ** decimals
|
|
return int((units * scale).to_integral_value(rounding=ROUND_CEILING))
|
|
|
|
|
|
def decimal_max(a: Decimal, b: Decimal) -> Decimal:
|
|
return a if a >= b else b
|
|
|
|
|
|
def parse_uint(value: str) -> int:
|
|
matches = UINT_RE.findall(value)
|
|
if not matches:
|
|
raise ValueError(f"could not parse integer from {value!r}")
|
|
return int(matches[0])
|
|
|
|
|
|
def parse_address(value: str) -> str:
|
|
match = ADDRESS_RE.search(value)
|
|
if not match:
|
|
raise ValueError(f"could not parse address from {value!r}")
|
|
return match.group(0)
|
|
|
|
|
|
def cast_call(rpc_url: str, target: str, signature: str, *args: str) -> str:
|
|
cmd = ["cast", "call", target, signature, *args, "--rpc-url", rpc_url]
|
|
return subprocess.check_output(cmd, text=True).strip()
|
|
|
|
|
|
def query_balance(rpc_url: str, token: str, holder: str) -> int:
|
|
return parse_uint(cast_call(rpc_url, token, "balanceOf(address)(uint256)", holder))
|
|
|
|
|
|
def derive_holder_from_private_key(env_values: dict[str, str]) -> str:
|
|
private_key = resolve_env_value("PRIVATE_KEY", env_values) or resolve_env_value("KEEPER_PRIVATE_KEY", env_values)
|
|
if not private_key or "${" in private_key:
|
|
return ""
|
|
output = subprocess.check_output(["cast", "wallet", "address", "--private-key", private_key], text=True).strip()
|
|
return parse_address(output)
|
|
|
|
|
|
def shell_quote(value: str) -> str:
|
|
return "'" + value.replace("'", "'\"'\"'") + "'"
|
|
|
|
|
|
def command_block(lines: list[str]) -> str:
|
|
return "\n".join(lines)
|
|
|
|
|
|
def funding_status(required_raw: int, available_raw: int, decimals: int = 6) -> dict:
|
|
shortfall_raw = max(required_raw - available_raw, 0)
|
|
return {
|
|
"requiredRaw": str(required_raw),
|
|
"requiredUnits": str(normalize_units(required_raw, decimals)),
|
|
"availableRaw": str(available_raw),
|
|
"availableUnits": str(normalize_units(available_raw, decimals)),
|
|
"shortfallRaw": str(shortfall_raw),
|
|
"shortfallUnits": str(normalize_units(shortfall_raw, decimals)),
|
|
"covered": shortfall_raw == 0,
|
|
}
|
|
|
|
|
|
def public_reseed_status(
|
|
base_shortfall_raw: int,
|
|
quote_shortfall_raw: int,
|
|
base_funding: dict,
|
|
quote_funding: dict,
|
|
) -> str:
|
|
if base_shortfall_raw == 0 and quote_shortfall_raw == 0:
|
|
return "ready"
|
|
if quote_shortfall_raw > 0 and not quote_funding["covered"]:
|
|
return "needs_usdc"
|
|
if base_shortfall_raw > 0 and not base_funding["covered"]:
|
|
return "needs_cwusdc"
|
|
if quote_shortfall_raw > 0:
|
|
return "needs_quote_side_repair"
|
|
return "needs_base_side_reseed"
|
|
|
|
|
|
def build_plan(snapshot: dict, policy: dict, env_values: dict[str, str], holder_override: str) -> dict:
|
|
rpc_url = resolve_env_value("ETHEREUM_MAINNET_RPC", env_values)
|
|
if not rpc_url:
|
|
raise RuntimeError("missing ETHEREUM_MAINNET_RPC")
|
|
|
|
summary = snapshot["summary"]
|
|
public_health = snapshot["health"]["publicPairHealth"]
|
|
defended_health = snapshot["health"]["defendedVenueHealth"]
|
|
treasury = snapshot.get("treasuryManager") or {}
|
|
|
|
base_reserve_raw = int(summary["defendedBaseReserveRaw"])
|
|
quote_reserve_raw = int(summary["defendedQuoteReserveRaw"])
|
|
target_reserve_raw = max(base_reserve_raw, quote_reserve_raw)
|
|
add_quote_raw = max(base_reserve_raw - quote_reserve_raw, 0)
|
|
add_base_raw = max(quote_reserve_raw - base_reserve_raw, 0)
|
|
|
|
min_base_units = Decimal(str(policy["thresholds"]["minBaseReserveUnits"]))
|
|
min_quote_units = Decimal(str(policy["thresholds"]["minQuoteReserveUnits"]))
|
|
public_base_units = Decimal(str(summary["publicPairBaseReserveUnits"]))
|
|
public_quote_units = Decimal(str(summary["publicPairQuoteReserveUnits"]))
|
|
public_base_shortfall_units = decimal_max(min_base_units - public_base_units, Decimal(0))
|
|
public_quote_shortfall_units = decimal_max(min_quote_units - public_quote_units, Decimal(0))
|
|
public_base_shortfall_raw = units_to_raw(public_base_shortfall_units)
|
|
public_quote_shortfall_raw = units_to_raw(public_quote_shortfall_units)
|
|
|
|
max_automated_raw = int(policy["managedCycle"]["maxAutomatedFlashQuoteAmountRaw"])
|
|
manager_available_raw = int(treasury.get("availableQuoteRaw") or 0)
|
|
|
|
holder = holder_override or derive_holder_from_private_key(env_values)
|
|
cwusdc = resolve_env_value("CWUSDC_MAINNET", env_values) or DEFAULT_CWUSDC
|
|
usdc = resolve_env_value("USDC_MAINNET", env_values) or DEFAULT_USDC
|
|
manager = snapshot["resolvedAddresses"].get("treasuryManager") or ""
|
|
receiver = snapshot["resolvedAddresses"].get("receiver") or ""
|
|
defended_pool = snapshot["health"]["defendedVenue"]["poolAddress"]
|
|
public_pair = snapshot["health"]["publicPair"]["poolAddress"]
|
|
integration = resolve_env_value("DODO_PMM_INTEGRATION_MAINNET", env_values)
|
|
router = resolve_env_value("CHAIN_1_UNISWAP_V2_ROUTER", env_values)
|
|
|
|
holder_state = None
|
|
holder_usdc_raw = 0
|
|
holder_cwusdc_raw = 0
|
|
holder_blockers: list[str] = []
|
|
if holder and holder.lower() != ZERO_ADDRESS:
|
|
try:
|
|
holder_cwusdc_raw = query_balance(rpc_url, cwusdc, holder)
|
|
holder_usdc_raw = query_balance(rpc_url, usdc, holder)
|
|
holder_state = {
|
|
"address": holder,
|
|
"cwusdcBalanceRaw": str(holder_cwusdc_raw),
|
|
"cwusdcBalanceUnits": str(normalize_units(holder_cwusdc_raw)),
|
|
"usdcBalanceRaw": str(holder_usdc_raw),
|
|
"usdcBalanceUnits": str(normalize_units(holder_usdc_raw)),
|
|
}
|
|
except Exception as exc:
|
|
holder_blockers.append(f"holder balance query failed: {exc}")
|
|
|
|
manager_funding = funding_status(max_automated_raw, manager_available_raw)
|
|
defended_quote_funding = funding_status(add_quote_raw, holder_usdc_raw)
|
|
public_base_funding = funding_status(public_base_shortfall_raw, holder_cwusdc_raw)
|
|
public_quote_funding = funding_status(public_quote_shortfall_raw, holder_usdc_raw)
|
|
|
|
blockers: list[str] = []
|
|
warnings = snapshot.get("warnings") or []
|
|
if add_base_raw > 0:
|
|
blockers.append("defended pool needs base-side top-up logic; current plan only supports quote-side top-up for this rail")
|
|
if add_quote_raw > 0 and holder_state and not defended_quote_funding["covered"]:
|
|
blockers.append(
|
|
"operator wallet does not hold enough USDC to restore defended pool reserve parity; external funding is required"
|
|
)
|
|
if public_base_shortfall_raw > 0 and holder_state and not public_base_funding["covered"]:
|
|
blockers.append(
|
|
"operator wallet does not hold enough cWUSDC to reseed the public pair to policy floor; external mint/bridge is required"
|
|
)
|
|
if public_quote_shortfall_raw > 0 and holder_state and not public_quote_funding["covered"]:
|
|
blockers.append(
|
|
"operator wallet does not hold enough USDC to reseed the public pair to policy floor"
|
|
)
|
|
if manager_funding["covered"] is False and holder_state and holder_usdc_raw < max_automated_raw:
|
|
blockers.append("operator wallet cannot fully fund even one max-sized automated defense cycle from current USDC balance")
|
|
if not integration:
|
|
blockers.append("missing DODO_PMM_INTEGRATION_MAINNET")
|
|
if not router:
|
|
blockers.append("missing CHAIN_1_UNISWAP_V2_ROUTER")
|
|
if any("defended quote query failed" in warning for warning in warnings):
|
|
blockers.append("defended pool quote preview reverted; set MIN_BASE_OUT_RAW manually before any quote-in trade")
|
|
|
|
public_deviation_bps = Decimal(str(summary["publicPairDeviationBps"]))
|
|
public_price_distorted = public_deviation_bps >= Decimal(str(policy["thresholds"]["warnDeviationBps"]))
|
|
if public_price_distorted:
|
|
warnings.append("public pair is materially asymmetric; plain addLiquidity follows the current reserve ratio and will not repair price")
|
|
|
|
operator_commands = {
|
|
"rerunPreflight": "bash scripts/verify/snapshot-mainnet-cwusdc-usdc-preflight.sh",
|
|
"rerunPlan": "bash scripts/verify/plan-mainnet-cwusdc-usdc-repeg.sh",
|
|
}
|
|
|
|
if manager and manager.lower() != ZERO_ADDRESS:
|
|
operator_commands["fundManagerUsdc"] = command_block(
|
|
[
|
|
"source smom-dbis-138/scripts/load-env.sh >/dev/null",
|
|
f"export USDC={usdc}",
|
|
f"export MANAGER={manager}",
|
|
f"export AMOUNT_RAW={max_automated_raw}",
|
|
'cast send "$USDC" \'transfer(address,uint256)(bool)\' "$MANAGER" "$AMOUNT_RAW" \\',
|
|
' --private-key "$PRIVATE_KEY" --rpc-url "$ETHEREUM_MAINNET_RPC"',
|
|
]
|
|
)
|
|
|
|
if integration and add_quote_raw > 0:
|
|
operator_commands["tradeDefendedPoolQuoteIn"] = command_block(
|
|
[
|
|
"source smom-dbis-138/scripts/load-env.sh >/dev/null",
|
|
f"export CWUSDC={cwusdc}",
|
|
f"export USDC={usdc}",
|
|
f"export INTEGRATION={integration}",
|
|
f"export POOL={defended_pool}",
|
|
f"export QUOTE_IN_RAW={add_quote_raw}",
|
|
"export MIN_BASE_OUT_RAW=REPLACE_AFTER_DRY_RUN",
|
|
'cast send "$USDC" \'approve(address,uint256)(bool)\' "$INTEGRATION" "$QUOTE_IN_RAW" \\',
|
|
' --private-key "$PRIVATE_KEY" --rpc-url "$ETHEREUM_MAINNET_RPC"',
|
|
'cast send "$INTEGRATION" \'swapExactIn(address,address,uint256,uint256)\' "$POOL" "$USDC" "$QUOTE_IN_RAW" "$MIN_BASE_OUT_RAW" \\',
|
|
' --private-key "$PRIVATE_KEY" --rpc-url "$ETHEREUM_MAINNET_RPC"',
|
|
]
|
|
)
|
|
|
|
if router and public_base_shortfall_raw > 0 and public_quote_shortfall_raw > 0:
|
|
operator_commands["reseedPublicPair"] = command_block(
|
|
[
|
|
"source smom-dbis-138/scripts/load-env.sh >/dev/null",
|
|
f"export ROUTER={router}",
|
|
f"export CWUSDC={cwusdc}",
|
|
f"export USDC={usdc}",
|
|
f"export BASE_AMOUNT_RAW={public_base_shortfall_raw}",
|
|
f"export QUOTE_AMOUNT_RAW={public_quote_shortfall_raw}",
|
|
'export DEADLINE="$(( $(date +%s) + 3600 ))"',
|
|
'export SIGNER="$(cast wallet address --private-key "$PRIVATE_KEY")"',
|
|
'cast send "$CWUSDC" \'approve(address,uint256)(bool)\' "$ROUTER" "$BASE_AMOUNT_RAW" \\',
|
|
' --private-key "$PRIVATE_KEY" --rpc-url "$ETHEREUM_MAINNET_RPC"',
|
|
'cast send "$USDC" \'approve(address,uint256)(bool)\' "$ROUTER" "$QUOTE_AMOUNT_RAW" \\',
|
|
' --private-key "$PRIVATE_KEY" --rpc-url "$ETHEREUM_MAINNET_RPC"',
|
|
'cast send "$ROUTER" \'addLiquidity(address,address,uint256,uint256,uint256,uint256,address,uint256)\' \\',
|
|
' "$CWUSDC" "$USDC" "$BASE_AMOUNT_RAW" "$QUOTE_AMOUNT_RAW" "$BASE_AMOUNT_RAW" "$QUOTE_AMOUNT_RAW" "$SIGNER" "$DEADLINE" \\',
|
|
' --private-key "$PRIVATE_KEY" --rpc-url "$ETHEREUM_MAINNET_RPC"',
|
|
]
|
|
)
|
|
|
|
if (
|
|
public_base_shortfall_raw > 0
|
|
or public_quote_shortfall_raw > 0
|
|
or public_price_distorted
|
|
):
|
|
operator_commands["publicPairRepairGuidance"] = command_block(
|
|
[
|
|
"# Public cWUSDC/USDC is one-sided or price-distorted.",
|
|
"# Do not use addLiquidity by itself as a price repair when only one reserve side is short.",
|
|
"# Use official Mainnet USDC for a quote-side repair swap first, then add balanced",
|
|
"# liquidity only after the reserve-implied price is back inside policy gates.",
|
|
"bash scripts/verify/snapshot-mainnet-cwusdc-usdc-preflight.sh",
|
|
"bash scripts/verify/plan-mainnet-cwusdc-usdc-repeg.sh",
|
|
]
|
|
)
|
|
|
|
public_action_status = public_reseed_status(
|
|
public_base_shortfall_raw,
|
|
public_quote_shortfall_raw,
|
|
public_base_funding,
|
|
public_quote_funding,
|
|
)
|
|
needs_external_funding = (
|
|
not defended_quote_funding["covered"]
|
|
or not public_base_funding["covered"]
|
|
or not public_quote_funding["covered"]
|
|
)
|
|
recommended_next_action = "execute_verified_repair"
|
|
if public_quote_shortfall_raw > 0 and not public_quote_funding["covered"]:
|
|
recommended_next_action = "acquire_official_mainnet_usdc"
|
|
elif public_quote_shortfall_raw > 0 or public_price_distorted:
|
|
recommended_next_action = "run_quote_side_public_pair_repair_before_balanced_liquidity"
|
|
elif add_quote_raw > 0 and not defended_quote_funding["covered"]:
|
|
recommended_next_action = "acquire_official_mainnet_usdc_for_defended_parity"
|
|
|
|
recommended_actions = [
|
|
{
|
|
"step": "fund_manager_for_one_max_cycle",
|
|
"quoteAmountRaw": str(max_automated_raw),
|
|
"quoteAmountUnits": str(normalize_units(max_automated_raw)),
|
|
"status": "ready" if manager_funding["covered"] else "needs_usdc",
|
|
},
|
|
{
|
|
"step": "sell_usdc_into_defended_pool_toward_simple_1_to_1_reserve_parity",
|
|
"baseAmountRaw": str(add_base_raw),
|
|
"quoteAmountRaw": str(add_quote_raw),
|
|
"quoteAmountUnits": str(normalize_units(add_quote_raw)),
|
|
"status": "ready" if add_quote_raw == 0 or defended_quote_funding["covered"] else "needs_usdc",
|
|
},
|
|
{
|
|
"step": "reseed_public_pair_to_policy_floor",
|
|
"baseAmountRaw": str(public_base_shortfall_raw),
|
|
"baseAmountUnits": str(normalize_units(public_base_shortfall_raw)),
|
|
"quoteAmountRaw": str(public_quote_shortfall_raw),
|
|
"quoteAmountUnits": str(normalize_units(public_quote_shortfall_raw)),
|
|
"status": public_action_status,
|
|
"guidance": (
|
|
"Use quote-side repair before balanced liquidity; plain addLiquidity cannot repair an asymmetric public pair."
|
|
if public_quote_shortfall_raw > 0 or public_price_distorted
|
|
else "Use addLiquidity only after price and reserve gates pass."
|
|
),
|
|
},
|
|
]
|
|
|
|
return {
|
|
"generatedAt": datetime.now(timezone.utc).isoformat(),
|
|
"mode": "read_only_repeg_plan",
|
|
"summary": {
|
|
"publicPairBaseReserveUnits": str(public_base_units),
|
|
"publicPairQuoteReserveUnits": str(public_quote_units),
|
|
"publicPairDeviationBps": str(summary["publicPairDeviationBps"]),
|
|
"publicPolicyFloorBaseShortfallUnits": str(normalize_units(public_base_shortfall_raw)),
|
|
"publicPolicyFloorQuoteShortfallUnits": str(normalize_units(public_quote_shortfall_raw)),
|
|
"publicPairRepairRequiresQuoteSideAction": public_quote_shortfall_raw > 0 or public_price_distorted,
|
|
"publicIndexedLpComplianceStatus": (
|
|
"blocked_missing_quote_side_public_lp_evidence"
|
|
if public_quote_shortfall_raw > 0 or public_price_distorted
|
|
else "ready_for_evidence_review"
|
|
),
|
|
"defendedAddQuoteUnits": str(normalize_units(add_quote_raw)),
|
|
"managerFundingShortfallUnits": manager_funding["shortfallUnits"],
|
|
"needsExternalFunding": needs_external_funding,
|
|
"recommendedNextAction": recommended_next_action,
|
|
},
|
|
"snapshotPath": str(LATEST_SNAPSHOT),
|
|
"policyPath": str(POLICY_PATH),
|
|
"inferenceNotes": [
|
|
"Defended-pool 1:1 sizing is inferred from equal 6-decimal matched-rail tokens and reserve-balance parity.",
|
|
"DODO PMM mid-price can still differ from reserve ratio; rerun preflight after each funding action.",
|
|
"Public-pair reseed target uses the current policy reserve floors, not a smaller cosmetic liquidity target.",
|
|
],
|
|
"resolvedAddresses": {
|
|
"holder": holder or None,
|
|
"cwusdc": cwusdc,
|
|
"usdc": usdc,
|
|
"publicPair": public_pair,
|
|
"defendedPool": defended_pool,
|
|
"treasuryManager": manager or None,
|
|
"receiver": receiver or None,
|
|
"dodoIntegration": integration or None,
|
|
"uniswapV2Router": router or None,
|
|
},
|
|
"defendedVenue": {
|
|
"midPrice": summary["defendedMidPrice"],
|
|
"deviationBps": summary["defendedDeviationBps"],
|
|
"baseReserveRaw": str(base_reserve_raw),
|
|
"baseReserveUnits": str(normalize_units(base_reserve_raw)),
|
|
"quoteReserveRaw": str(quote_reserve_raw),
|
|
"quoteReserveUnits": str(normalize_units(quote_reserve_raw)),
|
|
"simpleReserveParity": {
|
|
"targetReservePerSideRaw": str(target_reserve_raw),
|
|
"targetReservePerSideUnits": str(normalize_units(target_reserve_raw)),
|
|
"addBaseRaw": str(add_base_raw),
|
|
"addBaseUnits": str(normalize_units(add_base_raw)),
|
|
"addQuoteRaw": str(add_quote_raw),
|
|
"addQuoteUnits": str(normalize_units(add_quote_raw)),
|
|
},
|
|
},
|
|
"publicLane": {
|
|
"pairAddress": public_pair,
|
|
"priceQuotePerBase": public_health["priceQuotePerBase"],
|
|
"deviationBps": summary["publicPairDeviationBps"],
|
|
"baseReserveUnits": str(public_base_units),
|
|
"quoteReserveUnits": str(public_quote_units),
|
|
"policyFloorBaseUnits": str(min_base_units),
|
|
"policyFloorQuoteUnits": str(min_quote_units),
|
|
"policyFloorBaseShortfallRaw": str(public_base_shortfall_raw),
|
|
"policyFloorBaseShortfallUnits": str(normalize_units(public_base_shortfall_raw)),
|
|
"policyFloorQuoteShortfallRaw": str(public_quote_shortfall_raw),
|
|
"policyFloorQuoteShortfallUnits": str(normalize_units(public_quote_shortfall_raw)),
|
|
},
|
|
"automation": {
|
|
"managerAvailableQuoteRaw": str(manager_available_raw),
|
|
"managerAvailableQuoteUnits": str(normalize_units(manager_available_raw)),
|
|
"maxAutomatedFlashQuoteAmountRaw": str(max_automated_raw),
|
|
"maxAutomatedFlashQuoteAmountUnits": str(normalize_units(max_automated_raw)),
|
|
"managerFundingForOneMaxCycle": manager_funding,
|
|
},
|
|
"holderState": holder_state,
|
|
"holderFundingChecks": {
|
|
"defendedQuoteTopUp": defended_quote_funding,
|
|
"publicPairBaseTopUp": public_base_funding,
|
|
"publicPairQuoteTopUp": public_quote_funding,
|
|
},
|
|
"recommendedActions": recommended_actions,
|
|
"operatorCommands": operator_commands,
|
|
"warnings": warnings,
|
|
"blockers": holder_blockers + blockers,
|
|
"status": {
|
|
"canFullyReachSimple1To1WithCurrentHolder": len(holder_blockers + blockers) == 0,
|
|
"needsExternalFunding": needs_external_funding,
|
|
"canFundManagerFromCurrentHolder": holder_usdc_raw >= max_automated_raw if holder_state else None,
|
|
},
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--snapshot", default=str(LATEST_SNAPSHOT), help="Path to a preflight snapshot JSON.")
|
|
parser.add_argument("--holder", default="", help="Optional holder address to inventory-check instead of deriving from PRIVATE_KEY.")
|
|
parser.add_argument("--out", help="Write the plan JSON to this file.")
|
|
args = parser.parse_args()
|
|
|
|
snapshot_path = Path(args.snapshot)
|
|
if not snapshot_path.exists():
|
|
raise RuntimeError(f"missing snapshot file: {snapshot_path}")
|
|
|
|
snapshot = load_json(snapshot_path)
|
|
policy = load_json(POLICY_PATH)
|
|
env_values = merged_env_values()
|
|
plan = build_plan(snapshot, policy, env_values, args.holder.strip())
|
|
|
|
output = json.dumps(plan, indent=2)
|
|
if args.out:
|
|
out_path = Path(args.out)
|
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
out_path.write_text(output + "\n")
|
|
print(output)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|