Files
proxmox/scripts/verify/build-cw-public-price-table.py
2026-04-29 23:53:54 -07:00

636 lines
23 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import json
import math
import re
import subprocess
import time
import urllib.parse
import urllib.request
from collections import deque
from dataclasses import dataclass
from decimal import Decimal, InvalidOperation, getcontext
from pathlib import Path
getcontext().prec = 50
ROOT = Path(__file__).resolve().parents[2]
DEPLOYMENT_STATUS = ROOT / "cross-chain-pmm-lps" / "config" / "deployment-status.json"
UNISWAP_DISCOVERY = ROOT / "reports" / "extraction" / "promod-uniswap-v2-live-pair-discovery-latest.json"
JSON_OUT = ROOT / "reports" / "status" / "cw-public-prices-latest.json"
DOC_OUT = ROOT / "docs" / "03-deployment" / "CW_PUBLIC_NETWORK_PRICES.md"
ROOT_ENV_PATH = ROOT / ".env"
SMOM_ENV_PATH = ROOT / "smom-dbis-138" / ".env"
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
UINT_RE = re.compile(r"\b\d+\b")
CHAIN_CONFIG = {
"1": {"rpc_keys": ["ETHEREUM_MAINNET_RPC"]},
"10": {"rpc_keys": ["OPTIMISM_RPC_URL", "OPTIMISM_MAINNET_RPC"]},
"25": {"rpc_keys": ["CRONOS_RPC_URL", "CRONOS_MAINNET_RPC"]},
"56": {"rpc_keys": ["BSC_RPC_URL", "BSC_MAINNET_RPC"]},
"100": {"rpc_keys": ["GNOSIS_RPC_URL", "GNOSIS_MAINNET_RPC", "GNOSIS_RPC"]},
"137": {"rpc_keys": ["POLYGON_MAINNET_RPC", "POLYGON_RPC_URL"]},
"1111": {"rpc_keys": ["WEMIX_RPC_URL", "WEMIX_MAINNET_RPC"]},
"8453": {"rpc_keys": ["BASE_RPC_URL", "BASE_MAINNET_RPC"]},
"42161": {"rpc_keys": ["ARBITRUM_RPC_URL", "ARBITRUM_MAINNET_RPC"]},
"42220": {"rpc_keys": ["CELO_RPC_URL", "CELO_MAINNET_RPC", "CELO_RPC"]},
"43114": {"rpc_keys": ["AVALANCHE_RPC_URL", "AVALANCHE_MAINNET_RPC"]},
}
STABLES = {"USDC": Decimal("1"), "USDT": Decimal("1")}
ORACLE_ISO_USD_REFERENCE = {
"USD": Decimal("1"),
"EUR": Decimal("1.08"),
"GBP": Decimal("1.27"),
"AUD": Decimal("0.66"),
"JPY": Decimal("0.0067"),
"CHF": Decimal("1.11"),
"CAD": Decimal("0.74"),
"XAU": Decimal("3200"),
}
COINGECKO_PLATFORM_IDS = {
"1": "ethereum",
"10": "optimistic-ethereum",
"25": "cronos",
"56": "binance-smart-chain",
"100": "xdai",
"137": "polygon-pos",
"8453": "base",
"42161": "arbitrum-one",
"42220": "celo",
"43114": "avalanche",
}
@dataclass(frozen=True)
class Edge:
src: str
dst: str
ratio: Decimal
venue: str
path_label: str
price_detail: str
liquidity_note: str
def now() -> str:
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
def load_json(path: Path) -> dict:
return json.loads(path.read_text())
def write_json(path: Path, payload: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(payload, indent=2) + "\n")
def write_text(path: Path, text: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text.rstrip() + "\n")
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))
values.update(load_env_from_shell())
return values
def load_env_from_shell() -> dict[str, str]:
loader = ROOT / "smom-dbis-138" / "scripts" / "load-env.sh"
if not loader.exists():
return {}
proc = subprocess.run(
["bash", "-lc", f"source {loader} >/dev/null 2>&1 && env"],
text=True,
capture_output=True,
timeout=15,
check=False,
cwd=ROOT,
)
if proc.returncode != 0:
return {}
values: dict[str, str] = {}
for raw_line in proc.stdout.splitlines():
if "=" not in raw_line:
continue
key, value = raw_line.split("=", 1)
values[key.strip()] = value.strip()
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 = 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 parse_uint(value: str) -> int:
cleaned = re.sub(r"\[[^\]]*\]", "", value)
matches = UINT_RE.findall(cleaned)
if matches:
return int(matches[0])
for line in value.splitlines():
token = line.strip().split(" ", 1)[0]
if token.isdigit():
return int(token)
raise ValueError(f"could not parse integer from {value!r}")
def parse_uints(value: str, count: int) -> list[int]:
cleaned = re.sub(r"\[[^\]]*\]", "", value)
matches = [int(match) for match in UINT_RE.findall(cleaned)]
if len(matches) >= count:
return matches[:count]
matches = []
for line in value.splitlines():
token = line.strip().split(" ", 1)[0]
if token.isdigit():
matches.append(int(token))
if len(matches) < count:
raise ValueError(f"expected {count} integers, got {value!r}")
return matches[:count]
def cast_call(rpc_url: str, target: str, signature: str, *args: str) -> str:
cmd = ["cast", "call", target, signature, *args, "--rpc-url", rpc_url]
proc = subprocess.run(cmd, text=True, capture_output=True, timeout=3, check=False)
if proc.returncode != 0:
stderr = proc.stderr.strip() or proc.stdout.strip() or "cast call failed"
raise RuntimeError(stderr)
return proc.stdout.strip()
def safe_decimal(value: str | int | float | Decimal | None) -> Decimal | None:
if value is None:
return None
try:
return Decimal(str(value))
except (InvalidOperation, ValueError):
return None
def format_decimal(value: Decimal | None, places: int = 8) -> str:
if value is None:
return "not found"
quant = Decimal(10) ** -places
try:
rounded = value.quantize(quant)
except InvalidOperation:
return str(value)
return format(rounded, "f")
def format_metamask_price(row: dict) -> str:
price = safe_decimal(row.get("metamaskDisplayedPriceOracleQuote"))
if price is not None:
return format_decimal(price)
status = row.get("metamaskDisplayStatus") or "unknown"
if status == "api_error":
note = row.get("metamaskDisplayNote") or ""
if "429" in note:
return "unknown (API 429)"
return "unknown (API error)"
return {
"no_coingecko_platform": "unknown",
"not_listed": "not displayed",
"not_found": "not displayed",
}.get(status, status)
def symbol_oracle_iso4217(symbol: str) -> str:
"""Return the ISO-4217 unit embedded in a cW public wrapper symbol."""
if not symbol.startswith("cW"):
return ""
if len(symbol) >= 7 and symbol[2].isalpha() and symbol[3:6].isalpha():
return symbol[3:6]
if len(symbol) >= 6 and symbol[2:5].isalpha():
return symbol[2:5]
return ""
def oracle_iso_usd_reference(symbol: str) -> tuple[str, Decimal | None]:
oracle_iso = symbol_oracle_iso4217(symbol)
return oracle_iso, ORACLE_ISO_USD_REFERENCE.get(oracle_iso)
def oracle_quote_price(symbol: str, price_usd: Decimal | None) -> Decimal | None:
_oracle_iso, usd_reference = oracle_iso_usd_reference(symbol)
if price_usd is None or usd_reference is None or usd_reference <= 0:
return None
return price_usd / usd_reference
def normalize_18(raw: int) -> Decimal:
return Decimal(raw) / (Decimal(10) ** 18)
def rpc_for_chain(chain_id: str, env_values: dict[str, str]) -> str:
if chain_id == "1":
infura_project_id = resolve_env_value("INFURA_PROJECT_ID", env_values)
if infura_project_id:
return f"https://mainnet.infura.io/v3/{infura_project_id}"
config = CHAIN_CONFIG.get(chain_id, {})
for key in config.get("rpc_keys", []):
value = resolve_env_value(key, env_values)
if value:
return value
return ""
def fetch_metamask_display_prices(chain_id: str, chain: dict, env_values: dict[str, str]) -> tuple[dict[str, dict], str]:
"""Approximate MetaMask native display via CoinGecko token prices.
Repo docs record that MetaMask does not read the on-chain PMM/oracle price
directly; CoinGecko listing is the practical observable proxy.
"""
platform = COINGECKO_PLATFORM_IDS.get(chain_id)
if not platform:
return {}, "no_coingecko_platform"
tokens = {
symbol: address
for symbol, address in (chain.get("cwTokens") or {}).items()
if symbol.startswith("cW") and isinstance(address, str) and address.startswith("0x")
}
if not tokens:
return {}, "not_found"
vs_currencies = sorted(
{
iso.lower()
for symbol in tokens
for iso, _usd_reference in [oracle_iso_usd_reference(symbol)]
if iso
}
)
if not vs_currencies:
vs_currencies = ["usd"]
query = urllib.parse.urlencode(
{
"contract_addresses": ",".join(address.lower() for address in tokens.values()),
"vs_currencies": ",".join(vs_currencies),
},
safe=",",
)
url = f"https://api.coingecko.com/api/v3/simple/token_price/{platform}?{query}"
headers = {"User-Agent": "proxmox-cw-price-table/1.0"}
api_key = resolve_env_value("COINGECKO_API_KEY", env_values) or resolve_env_value(
"COINGECKO_DEMO_API_KEY", env_values
)
if api_key:
headers["x-cg-demo-api-key"] = api_key
try:
request = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(request, timeout=12) as response:
payload = json.loads(response.read().decode("utf-8"))
except Exception as exc:
return {
symbol: {
"priceOracleQuote": None,
"status": "api_error",
"source": "coingecko_token_price",
"note": f"CoinGecko token-price lookup failed: {exc}",
}
for symbol in tokens
}, "api_error"
prices: dict[str, dict] = {}
by_address = {address.lower(): symbol for symbol, address in tokens.items()}
for address, symbol in by_address.items():
oracle_iso, _usd_reference = oracle_iso_usd_reference(symbol)
vs_currency = (oracle_iso or "USD").lower()
oracle_quote_price_value = (payload.get(address) or {}).get(vs_currency)
if oracle_quote_price_value is None:
prices[symbol] = {
"priceOracleQuote": None,
"status": "not_listed",
"source": "coingecko_token_price",
"note": (
f"No CoinGecko token-price result for {oracle_iso or 'USD'}; "
"MetaMask native display is expected to be unavailable."
),
}
continue
prices[symbol] = {
"priceOracleQuote": str(oracle_quote_price_value),
"status": "priced",
"source": "coingecko_token_price",
"note": f"CoinGecko token-price result in {oracle_iso or 'USD'} used as MetaMask native-display proxy.",
}
return prices, "ok"
def build_uniswap_edges(entry: dict) -> list[Edge]:
edges: list[Edge] = []
for row in entry.get("pairsChecked") or []:
if not row.get("live"):
continue
health = row.get("health") or {}
price = safe_decimal(health.get("priceQuotePerBase"))
if price is None or price <= 0:
continue
base = row["base"]
quote = row["quote"]
pair = f"{base}/{quote}"
addr = row.get("poolAddress") or ""
reserves = f"base={health.get('baseReserveUnits', '?')}, quote={health.get('quoteReserveUnits', '?')}"
liquidity_note = (
f"Uniswap V2 pair {addr}; healthy={health.get('healthy')}; "
f"depthOk={health.get('depthOk')}; parityOk={health.get('parityOk')}; {reserves}"
)
edges.append(
Edge(
src=base,
dst=quote,
ratio=price,
venue="uniswap_v2",
path_label=pair,
price_detail=f"reserve ratio from {pair}",
liquidity_note=liquidity_note,
)
)
edges.append(
Edge(
src=quote,
dst=base,
ratio=Decimal(1) / price,
venue="uniswap_v2",
path_label=pair,
price_detail=f"inverse reserve ratio from {pair}",
liquidity_note=liquidity_note,
)
)
return edges
def build_pmm_edges(chain: dict, rpc_url: str) -> tuple[list[Edge], list[dict]]:
edges: list[Edge] = []
snapshots: list[dict] = []
if not rpc_url:
return edges, snapshots
for row in chain.get("pmmPools") or []:
pool = row.get("poolAddress") or ""
base = row.get("base")
quote = row.get("quote")
if not pool or pool.lower() == ZERO_ADDRESS or not base or not quote:
continue
try:
mid_price = normalize_18(parse_uint(cast_call(rpc_url, pool, "getMidPrice()(uint256)")))
except Exception as exc:
snapshots.append(
{
"base": base,
"quote": quote,
"poolAddress": pool,
"venue": row.get("venue", "dodo_pmm"),
"error": str(exc),
}
)
continue
if mid_price <= 0:
continue
pair = f"{base}/{quote}"
liquidity_note = f"DODO PMM {pool}; midPrice={mid_price}"
edges.append(
Edge(
src=base,
dst=quote,
ratio=mid_price,
venue="dodo_pmm",
path_label=pair,
price_detail=f"PMM mid price from {pair}",
liquidity_note=liquidity_note,
)
)
edges.append(
Edge(
src=quote,
dst=base,
ratio=Decimal(1) / mid_price,
venue="dodo_pmm",
path_label=pair,
price_detail=f"inverse PMM mid price from {pair}",
liquidity_note=liquidity_note,
)
)
snapshots.append(
{
"base": base,
"quote": quote,
"poolAddress": pool,
"venue": row.get("venue", "dodo_pmm"),
"midPrice": str(mid_price),
}
)
return edges, snapshots
def best_prices_for_chain(chain: dict, edges: list[Edge]) -> dict[str, dict]:
adjacency: dict[str, list[Edge]] = {}
for edge in edges:
adjacency.setdefault(edge.src, []).append(edge)
best: dict[str, dict] = {}
queue: deque[tuple[str, Decimal, list[str], list[str], list[str], int]] = deque()
for stable, price in STABLES.items():
best[stable] = {
"price": price,
"steps": [],
"venues": [],
"notes": [f"{stable} anchored at 1 USD"],
"hops": 0,
}
queue.append((stable, price, [], [], [f"{stable} anchored at 1 USD"], 0))
while queue:
token, usd_price, steps, venues, notes, hops = queue.popleft()
for edge in adjacency.get(token, []):
next_price = usd_price / edge.ratio
next_steps = steps + [edge.path_label]
next_venues = venues + [edge.venue]
next_notes = notes + [edge.liquidity_note]
next_hops = hops + 1
current = best.get(edge.dst)
should_replace = current is None or next_hops < current["hops"]
if not should_replace and current is not None and next_hops == current["hops"]:
current_venue_score = 0 if "dodo_pmm" in current["venues"] else 1
next_venue_score = 0 if "dodo_pmm" in next_venues else 1
should_replace = next_venue_score < current_venue_score
if should_replace:
best[edge.dst] = {
"price": next_price,
"steps": next_steps,
"venues": next_venues,
"notes": next_notes,
"hops": next_hops,
}
queue.append((edge.dst, next_price, next_steps, next_venues, next_notes, next_hops))
out: dict[str, dict] = {}
for symbol in sorted((chain.get("cwTokens") or {}).keys()):
oracle_iso, usd_reference = oracle_iso_usd_reference(symbol)
resolution = best.get(symbol)
if resolution is None:
out[symbol] = {
"priceUsd": None,
"priceOracleQuote": "1" if oracle_iso else None,
"observedPriceOracleQuote": None,
"oracleIso4217": oracle_iso,
"oracleIsoUsdReference": str(usd_reference) if usd_reference is not None else None,
"shownValueOracleQuote": "1" if oracle_iso else None,
"derivedFrom": "not found",
"sourceType": "not_found",
"notes": ["No live direct or bridged price path was found from USDC/USDT anchors."],
}
continue
price_usd = resolution["price"]
price_oracle_quote = oracle_quote_price(symbol, price_usd)
out[symbol] = {
"priceUsd": str(price_usd),
"priceOracleQuote": "1" if oracle_iso else None,
"observedPriceOracleQuote": str(price_oracle_quote) if price_oracle_quote is not None else None,
"oracleIso4217": oracle_iso,
"oracleIsoUsdReference": str(usd_reference) if usd_reference is not None else None,
"shownValueOracleQuote": "1" if oracle_iso else None,
"derivedFrom": " -> ".join(resolution["steps"]) if resolution["steps"] else "stable anchor",
"sourceType": resolution["venues"][0] if resolution["venues"] else "stable_anchor",
"notes": resolution["notes"],
}
return out
def build_report() -> dict:
env_values = merged_env_values()
deployment = load_json(DEPLOYMENT_STATUS)
discovery = load_json(UNISWAP_DISCOVERY)
discovery_by_chain = {str(entry["chain_id"]): entry for entry in discovery.get("entries") or []}
chains_out: list[dict] = []
for chain_id, chain in sorted((deployment.get("chains") or {}).items(), key=lambda item: int(item[0])):
if int(chain_id) == 138:
continue
rpc_url = rpc_for_chain(chain_id, env_values)
uniswap_edges = build_uniswap_edges(discovery_by_chain.get(chain_id, {}))
pmm_edges, pmm_snapshots = build_pmm_edges(chain, rpc_url)
price_rows = best_prices_for_chain(chain, uniswap_edges + pmm_edges)
metamask_prices, metamask_status = fetch_metamask_display_prices(chain_id, chain, env_values)
for symbol, row in price_rows.items():
metamask_row = metamask_prices.get(symbol)
row["metamaskDisplayedPriceOracleQuote"] = (metamask_row or {}).get("priceOracleQuote")
row["metamaskDisplayStatus"] = (metamask_row or {}).get("status", metamask_status)
row["metamaskDisplaySource"] = (metamask_row or {}).get("source", "coingecko_token_price")
row["metamaskDisplayNote"] = (metamask_row or {}).get(
"note", "No supported CoinGecko platform mapping for MetaMask display-price lookup."
)
chains_out.append(
{
"chainId": int(chain_id),
"network": chain.get("name", ""),
"activationState": chain.get("activationState", ""),
"rpcConfigured": bool(rpc_url),
"prices": price_rows,
"pmmSnapshots": pmm_snapshots,
}
)
return {
"generatedAt": now(),
"inputs": {
"deploymentStatus": str(DEPLOYMENT_STATUS),
"uniswapDiscovery": str(UNISWAP_DISCOVERY),
},
"chains": chains_out,
}
def render_markdown(payload: dict) -> str:
lines = [
"# cW Public Network Prices",
"",
f"- Generated: `{payload['generatedAt']}`",
f"- Deployment inventory: `{payload['inputs']['deploymentStatus']}`",
f"- Uniswap discovery snapshot: `{payload['inputs']['uniswapDiscovery']}`",
"- Public Price (USD) is the observed USDC/USDT-anchored execution price per 1 token.",
"- Quote Price (Oracle ISO) and Shown Value (Oracle ISO) are the intended oracle peg display: 1 token = 1 ISO-4217 unit (or 1 troy oz for XAU).",
"- MetaMask Display (Oracle ISO) uses CoinGecko token-price lookup in the token's oracle ISO-4217 unit as the closest observable proxy for native MetaMask balance pricing; `not displayed` means no token-price listing was returned.",
"- `not found` means the generator could not reach the token from a live USDC/USDT anchor using the current public-pair snapshot plus live PMM mid-price reads.",
"",
"| Chain | Token | Oracle ISO | Quote Price (Oracle ISO) | Shown Value (Oracle ISO) | Public Price (USD) | MetaMask Display (Oracle ISO) | Derived From | Source | Notes |",
"|---|---|---|---:|---:|---:|---:|---|---|---|",
]
for chain in payload["chains"]:
first_row = True
prices = chain["prices"]
for symbol in sorted(prices.keys()):
row = prices[symbol]
chain_cell = f"`{chain['chainId']}` {chain['network']}" if first_row else ""
first_row = False
notes = "; ".join(row["notes"][:2])
oracle_iso = row.get("oracleIso4217") or "unknown"
shown_value = safe_decimal(row.get("shownValueOracleQuote"))
lines.append(
f"| {chain_cell} | `{symbol}` | `{oracle_iso}` | "
f"`{format_decimal(safe_decimal(row.get('priceOracleQuote')))}` | "
f"`{format_decimal(shown_value) if shown_value is not None else 'not found'}` | "
f"`{format_decimal(safe_decimal(row['priceUsd']))}` | "
f"`{format_metamask_price(row)}` | "
f"`{row['derivedFrom']}` | `{row['sourceType']}` | {notes} |"
)
if prices:
lines.append(
f"| | | | | | | | | | Activation state: `{chain['activationState'] or 'active'}`; RPC configured: `{chain['rpcConfigured']}` |"
)
return "\n".join(lines)
def main() -> None:
payload = build_report()
write_json(JSON_OUT, payload)
write_text(DOC_OUT, render_markdown(payload))
print(JSON_OUT)
print(DOC_OUT)
if __name__ == "__main__":
main()