410 lines
18 KiB
Python
410 lines
18 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 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")
|
||
|
|
|
||
|
|
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"',
|
||
|
|
]
|
||
|
|
)
|
||
|
|
|
||
|
|
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": (
|
||
|
|
"ready"
|
||
|
|
if public_base_shortfall_raw == 0
|
||
|
|
or (public_base_funding["covered"] and public_quote_funding["covered"])
|
||
|
|
else "needs_inventory"
|
||
|
|
),
|
||
|
|
},
|
||
|
|
]
|
||
|
|
|
||
|
|
return {
|
||
|
|
"generatedAt": datetime.now(timezone.utc).isoformat(),
|
||
|
|
"mode": "read_only_repeg_plan",
|
||
|
|
"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": (
|
||
|
|
not defended_quote_funding["covered"]
|
||
|
|
or not public_base_funding["covered"]
|
||
|
|
or not public_quote_funding["covered"]
|
||
|
|
),
|
||
|
|
"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())
|