Add optional Cosmos/Engine-X/act-runner templates, CWUSDC/EI-matrix tooling, non-EVM route planner in multi-chain-execution (tests passing), token list and extraction updates, and documentation (MetaMask matrix, GRU/CWUSDC packets). Ignore institutional evidence tarballs/sha256 under reports/status. Validated with: bash scripts/verify/run-all-validation.sh --skip-genesis Co-authored-by: Cursor <cursoragent@cursor.com>
529 lines
23 KiB
Python
Executable File
529 lines
23 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Build a submission-ready cWUSDC Etherscan Value dossier.
|
|
|
|
The dossier intentionally separates Ethereum Mainnet cWUSDC evidence from
|
|
global cUSDC/cWUSDC family context. It is read-only: it runs monitors and proof
|
|
generators, then summarizes what can be submitted and what remains externally
|
|
blocked.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import datetime as dt
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import urllib.parse
|
|
import urllib.request
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
REPORT_JSON = ROOT / "reports" / "status" / "cwusdc-etherscan-value-dossier-latest.json"
|
|
REPORT_MD = ROOT / "reports" / "status" / "cwusdc-etherscan-value-dossier-latest.md"
|
|
|
|
CWUSDC = "0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a"
|
|
ETHERSCAN_CHAINLIST_URL = "https://api.etherscan.io/v2/chainlist"
|
|
ETHERSCAN_V2_API = "https://api.etherscan.io/v2/api"
|
|
DEPLOYER_FALLBACK = "0x4A666F96fC8764181194447A7dFdb7d471b301C8"
|
|
L2_DEPOSIT_CHAINS = {
|
|
"10": "OP Mainnet",
|
|
"42161": "Arbitrum One Mainnet",
|
|
}
|
|
ARTIFACTS = {
|
|
"mainnetSupply": ROOT / "reports" / "status" / "cwusdc-supply-circulating-attestation-latest.json",
|
|
"globalFamilySupply": ROOT / "reports" / "status" / "global-cusdc-cwusdc-family-supply-proof-latest.json",
|
|
"feedAudit": ROOT / "reports" / "status" / "cusdc-cwusdc-etherscan-feed-audit-latest.json",
|
|
"propagation": ROOT / "reports" / "status" / "cwusdc-etherscan-value-propagation-latest.json",
|
|
}
|
|
DOCS = {
|
|
"executionPlan": ROOT / "docs" / "04-configuration" / "etherscan" / "CWUSDC_ETHERSCAN_VALUE_EXECUTION_PLAN.md",
|
|
"bridgeLayerMap": ROOT / "docs" / "04-configuration" / "etherscan" / "CWUSDC_ETHERSCAN_BRIDGE_CROSSCHAIN_LAYER_MAP.md",
|
|
"profilePacket": ROOT / "docs" / "04-configuration" / "etherscan" / "CWUSDC_MAINNET_ETHERSCAN_PROFILE_PACKET.md",
|
|
"e2eRecommendations": ROOT / "docs" / "04-configuration" / "etherscan" / "CWUSDC_ETHERSCAN_E2E_RECOMMENDATIONS.md",
|
|
"trackerPacket": ROOT / "docs" / "04-configuration" / "coingecko" / "CWUSDC_MAINNET_TRACKER_SUBMISSION_PACKET.md",
|
|
}
|
|
|
|
|
|
def load_dotenv(path: Path, env: dict[str, str]) -> dict[str, str]:
|
|
if not path.exists():
|
|
return env
|
|
merged = dict(env)
|
|
for line in path.read_text().splitlines():
|
|
line = line.strip()
|
|
if not line or line.startswith("#") or "=" not in line:
|
|
continue
|
|
key, value = line.split("=", 1)
|
|
key = key.strip()
|
|
value = value.strip().strip('"').strip("'")
|
|
if key and key not in merged:
|
|
merged[key] = value
|
|
return merged
|
|
|
|
|
|
def run_command(command: list[str], env: dict[str, str]) -> dict[str, Any]:
|
|
proc = subprocess.run(
|
|
command,
|
|
cwd=ROOT,
|
|
env=env,
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
check=False,
|
|
)
|
|
return {
|
|
"command": command,
|
|
"returncode": proc.returncode,
|
|
"stdout": proc.stdout.strip(),
|
|
"stderr": proc.stderr.strip(),
|
|
"ok": proc.returncode == 0,
|
|
}
|
|
|
|
|
|
def read_json(path: Path) -> Any | None:
|
|
if not path.exists():
|
|
return None
|
|
return json.loads(path.read_text())
|
|
|
|
|
|
def fetch_json_url(url: str, timeout: int = 30) -> Any:
|
|
req = urllib.request.Request(url, headers={"User-Agent": "dbis-cwusdc-etherscan-dossier/1.0"})
|
|
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
return json.loads(response.read().decode("utf-8"))
|
|
|
|
|
|
def load_etherscan_chainlist() -> dict[str, Any]:
|
|
try:
|
|
payload = fetch_json_url(ETHERSCAN_CHAINLIST_URL)
|
|
except Exception as exc: # noqa: BLE001 - dossier should capture diagnostics instead of crashing
|
|
return {
|
|
"url": ETHERSCAN_CHAINLIST_URL,
|
|
"available": False,
|
|
"error": str(exc),
|
|
"totalcount": None,
|
|
"supportedChainIds": [],
|
|
"statusByChainId": {},
|
|
}
|
|
|
|
result = payload.get("result") if isinstance(payload, dict) else None
|
|
chains = result if isinstance(result, list) else []
|
|
status_by_chain_id = {
|
|
str(item.get("chainid")): {
|
|
"chainname": item.get("chainname"),
|
|
"blockexplorer": item.get("blockexplorer"),
|
|
"apiurl": item.get("apiurl"),
|
|
"status": item.get("status"),
|
|
"comment": item.get("comment"),
|
|
}
|
|
for item in chains
|
|
if isinstance(item, dict) and item.get("chainid") is not None
|
|
}
|
|
return {
|
|
"url": ETHERSCAN_CHAINLIST_URL,
|
|
"available": True,
|
|
"comments": payload.get("comments") if isinstance(payload, dict) else None,
|
|
"totalcount": payload.get("totalcount") if isinstance(payload, dict) else len(chains),
|
|
"supportedChainIds": sorted(status_by_chain_id, key=lambda value: int(value) if value.isdigit() else value),
|
|
"statusByChainId": status_by_chain_id,
|
|
}
|
|
|
|
|
|
def human_token_value(raw: Any, token_address: str | None) -> str | None:
|
|
try:
|
|
raw_int = int(str(raw))
|
|
except (TypeError, ValueError):
|
|
return None
|
|
decimals = 18 if token_address == "ETH" else 6 if token_address and token_address.lower() == CWUSDC.lower() else 18
|
|
whole = raw_int // (10**decimals)
|
|
frac = str(raw_int % (10**decimals)).rjust(decimals, "0").rstrip("0")
|
|
return f"{whole}" + (f".{frac}" if frac else "")
|
|
|
|
|
|
def human_wei(raw: Any) -> str | None:
|
|
try:
|
|
raw_int = int(str(raw))
|
|
except (TypeError, ValueError):
|
|
return None
|
|
whole = raw_int // (10**18)
|
|
frac = str(raw_int % (10**18)).rjust(18, "0").rstrip("0")
|
|
return f"{whole}" + (f".{frac}" if frac else "")
|
|
|
|
|
|
def normalize_deposit_row(row: dict[str, Any]) -> dict[str, Any]:
|
|
token_address = row.get("tokenAddress")
|
|
return {
|
|
"hash": row.get("hash"),
|
|
"l1TransactionHash": row.get("L1transactionhash"),
|
|
"timeStamp": row.get("timeStamp"),
|
|
"from": row.get("from"),
|
|
"to": row.get("to"),
|
|
"valueRaw": row.get("value"),
|
|
"valueEth": human_wei(row.get("value")),
|
|
"tokenAddress": token_address,
|
|
"tokenValueRaw": row.get("tokenValue"),
|
|
"tokenValueUnits": human_token_value(row.get("tokenValue"), token_address),
|
|
"txreceiptStatus": row.get("txreceipt_status"),
|
|
"isError": row.get("isError"),
|
|
}
|
|
|
|
|
|
def etherscan_v2_call(params: dict[str, str], api_key: str) -> dict[str, Any]:
|
|
query = {**params, "apikey": api_key}
|
|
url = f"{ETHERSCAN_V2_API}?{urllib.parse.urlencode(query)}"
|
|
redacted_query = {**params, "apikey": "REDACTED"}
|
|
redacted_url = f"{ETHERSCAN_V2_API}?{urllib.parse.urlencode(redacted_query)}"
|
|
try:
|
|
payload = fetch_json_url(url)
|
|
except Exception as exc: # noqa: BLE001 - capture diagnostics instead of crashing the dossier
|
|
return {"url": redacted_url, "ok": False, "error": str(exc), "status": None, "message": None, "result": None}
|
|
status = str(payload.get("status")) if isinstance(payload, dict) else None
|
|
message = payload.get("message") if isinstance(payload, dict) else None
|
|
result = payload.get("result") if isinstance(payload, dict) else None
|
|
no_rows = isinstance(result, str) and "No transactions found" in result
|
|
return {
|
|
"url": redacted_url,
|
|
"ok": status == "1" or no_rows,
|
|
"status": status,
|
|
"message": message,
|
|
"result": [] if no_rows else result,
|
|
"error": None,
|
|
}
|
|
|
|
|
|
def load_l2_deposit_evidence(api_key: str, chainlist: dict[str, Any], address: str) -> dict[str, Any]:
|
|
if not api_key:
|
|
return {
|
|
"checked": False,
|
|
"reason": "ETHERSCAN_API_KEY is not set.",
|
|
"address": address,
|
|
"chains": {},
|
|
}
|
|
|
|
supported = set(chainlist.get("statusByChainId", {}))
|
|
chains: dict[str, Any] = {}
|
|
for chain_id, chain_name in L2_DEPOSIT_CHAINS.items():
|
|
if chain_id not in supported:
|
|
chains[chain_id] = {
|
|
"chainName": chain_name,
|
|
"checked": False,
|
|
"reason": "chain is not present in Etherscan V2 chainlist",
|
|
}
|
|
continue
|
|
response = etherscan_v2_call(
|
|
{
|
|
"chainid": chain_id,
|
|
"module": "account",
|
|
"action": "getdeposittxs",
|
|
"address": address,
|
|
"page": "1",
|
|
"offset": "10",
|
|
"sort": "desc",
|
|
},
|
|
api_key,
|
|
)
|
|
result = response.get("result")
|
|
rows = result if isinstance(result, list) else []
|
|
chains[chain_id] = {
|
|
"chainName": chain_name,
|
|
"checked": True,
|
|
"ok": response.get("ok"),
|
|
"status": response.get("status"),
|
|
"message": response.get("message"),
|
|
"sampleCount": len(rows),
|
|
"latest": normalize_deposit_row(rows[0]) if rows else None,
|
|
"url": response.get("url"),
|
|
"error": response.get("error"),
|
|
}
|
|
|
|
return {
|
|
"checked": True,
|
|
"address": address,
|
|
"scope": "Etherscan-indexed L2 deposits by address. This is bridge provenance only and does not set Mainnet cWUSDC USD Value.",
|
|
"rawUnitNote": "tokenValue is returned as raw token units. ETH uses 18 decimals; ERC-20 rows must be normalized with that token contract's decimals.",
|
|
"chains": chains,
|
|
}
|
|
|
|
|
|
def load_contract_source_verification(api_key: str, address: str) -> dict[str, Any]:
|
|
if not api_key:
|
|
return {
|
|
"checked": False,
|
|
"reason": "ETHERSCAN_API_KEY is not set.",
|
|
"address": address,
|
|
"verified": False,
|
|
}
|
|
response = etherscan_v2_call(
|
|
{
|
|
"chainid": "1",
|
|
"module": "contract",
|
|
"action": "getsourcecode",
|
|
"address": address,
|
|
},
|
|
api_key,
|
|
)
|
|
result = response.get("result")
|
|
entry = result[0] if isinstance(result, list) and result and isinstance(result[0], dict) else {}
|
|
source_code = str(entry.get("SourceCode") or "")
|
|
abi = str(entry.get("ABI") or "")
|
|
contract_name = str(entry.get("ContractName") or "")
|
|
return {
|
|
"checked": True,
|
|
"address": address,
|
|
"ok": response.get("ok"),
|
|
"status": response.get("status"),
|
|
"message": response.get("message"),
|
|
"verified": bool(source_code and contract_name and abi and abi != "Contract source code not verified"),
|
|
"contractName": contract_name or None,
|
|
"compilerVersion": entry.get("CompilerVersion") or None,
|
|
"optimizationUsed": entry.get("OptimizationUsed") or None,
|
|
"runs": entry.get("Runs") or None,
|
|
"constructorArgumentsPresent": bool(entry.get("ConstructorArguments")),
|
|
"evmVersion": entry.get("EVMVersion") or None,
|
|
"licenseType": entry.get("LicenseType") or None,
|
|
"proxy": entry.get("Proxy") or None,
|
|
"implementation": entry.get("Implementation") or None,
|
|
"sourceCodeBytes": len(source_code),
|
|
"abiAvailable": bool(abi and abi != "Contract source code not verified"),
|
|
"url": response.get("url"),
|
|
"error": response.get("error"),
|
|
}
|
|
|
|
|
|
def rel(path: Path) -> str:
|
|
return str(path.relative_to(ROOT))
|
|
|
|
|
|
def build(args: argparse.Namespace) -> dict[str, Any]:
|
|
env = load_dotenv(ROOT / ".env", dict(os.environ))
|
|
etherscan_api_key = env.get("ETHERSCAN_API_KEY", "")
|
|
l2_deposit_address = args.l2_deposit_address or env.get("DEPLOYER_ADDRESS") or DEPLOYER_FALLBACK
|
|
commands: list[dict[str, Any]] = []
|
|
if args.refresh:
|
|
commands = [
|
|
run_command(["python3", "scripts/verify/generate-cwusdc-supply-circulating-attestation.py"], env),
|
|
run_command(["python3", "scripts/verify/generate-global-cusdc-cwusdc-family-supply-proof.py"], env),
|
|
run_command(["python3", "scripts/verify/audit-cusdc-cwusdc-etherscan-feeds.py"], env),
|
|
run_command(["python3", "scripts/verify/monitor-cwusdc-etherscan-value-propagation.py"], env),
|
|
run_command(["bash", "scripts/verify/check-cwusdc-etherscan-prereq-urls.sh"], env),
|
|
]
|
|
|
|
artifacts = {key: read_json(path) for key, path in ARTIFACTS.items()}
|
|
propagation = artifacts["propagation"] or {}
|
|
supply = artifacts["mainnetSupply"] or {}
|
|
global_family = artifacts["globalFamilySupply"] or {}
|
|
feed_audit = artifacts["feedAudit"] or {}
|
|
chainlist = load_etherscan_chainlist()
|
|
l2_deposits = load_l2_deposit_evidence(etherscan_api_key, chainlist, l2_deposit_address)
|
|
contract_source = load_contract_source_verification(etherscan_api_key, CWUSDC)
|
|
family_chain_ids = sorted(
|
|
{str(item.get("chainId")) for item in global_family.get("entries", []) if isinstance(item, dict) and item.get("chainId") is not None},
|
|
key=lambda value: int(value) if value.isdigit() else value,
|
|
)
|
|
supported_family_chain_ids = [chain_id for chain_id in family_chain_ids if chain_id in chainlist.get("statusByChainId", {})]
|
|
unsupported_family_chain_ids = [chain_id for chain_id in family_chain_ids if chain_id not in chainlist.get("statusByChainId", {})]
|
|
|
|
blockers = list(((propagation.get("summary") or {}).get("blockers") or []))
|
|
command_failures = [item for item in commands if not item["ok"]]
|
|
for item in command_failures:
|
|
blockers.append("Command failed: " + " ".join(item["command"]))
|
|
|
|
ready_evidence = {
|
|
"mainnetSupplyAttestation": bool(supply.get("supply")),
|
|
"globalFamilySupplyContext": bool(global_family.get("summary")),
|
|
"chain138MainnetFeedAudit": bool(feed_audit.get("canonicalRelationship")),
|
|
"mainnetContractSourceVerified": bool(contract_source.get("verified")),
|
|
"propagationMonitor": bool(propagation.get("checks")),
|
|
"publicPrereqUrls": not any(
|
|
item["command"] == ["bash", "scripts/verify/check-cwusdc-etherscan-prereq-urls.sh"] and not item["ok"]
|
|
for item in commands
|
|
)
|
|
if commands
|
|
else None,
|
|
"documentationPacket": {key: path.exists() for key, path in DOCS.items()},
|
|
}
|
|
|
|
next_actions = []
|
|
if blockers:
|
|
next_actions.extend(
|
|
[
|
|
"Submit/update Etherscan token profile for the exact Ethereum Mainnet cWUSDC contract.",
|
|
"Submit/update CoinGecko and CoinMarketCap listings with Mainnet supply proof, DEX evidence, and bridge-family context.",
|
|
"Use the global family supply proof only as context; use the Ethereum Mainnet cWUSDC attestation as the token-page supply basis.",
|
|
"Re-run this dossier after each external approval or tracker response.",
|
|
]
|
|
)
|
|
else:
|
|
next_actions.append("No blockers detected by local monitors; capture Etherscan screenshots and continue propagation monitoring.")
|
|
|
|
return {
|
|
"schema": "cwusdc-etherscan-value-dossier/v1",
|
|
"generatedAt": dt.datetime.now(dt.UTC).isoformat().replace("+00:00", "Z"),
|
|
"purpose": "Single submission and monitoring packet for making Etherscan show USD value for Ethereum Mainnet cWUSDC.",
|
|
"target": {
|
|
"network": "Ethereum Mainnet",
|
|
"chainId": 1,
|
|
"contract": CWUSDC,
|
|
"caip19": f"eip155:1/erc20:{CWUSDC}",
|
|
"name": "Wrapped cUSDC",
|
|
"symbol": "cWUSDC",
|
|
"decimals": 6,
|
|
},
|
|
"readiness": {
|
|
"readyForExternalSubmission": ready_evidence["mainnetSupplyAttestation"]
|
|
and ready_evidence["chain138MainnetFeedAudit"]
|
|
and ready_evidence["mainnetContractSourceVerified"]
|
|
and ready_evidence["propagationMonitor"],
|
|
"etherscanValueReady": (propagation.get("summary") or {}).get("etherscanValueReady"),
|
|
"coinGeckoPriceReady": (propagation.get("summary") or {}).get("coingeckoPriceReady"),
|
|
"blockers": blockers,
|
|
},
|
|
"evidence": {
|
|
"artifacts": {key: rel(path) for key, path in ARTIFACTS.items()},
|
|
"docs": {key: rel(path) for key, path in DOCS.items()},
|
|
"readyEvidence": ready_evidence,
|
|
"mainnetContractSourceVerification": contract_source,
|
|
"mainnetSupply": supply.get("supply"),
|
|
"globalFamilyWarning": (global_family.get("caveats") or ["Global family supply is context only; do not use it as Ethereum Etherscan token-page supply."])[0],
|
|
"globalFamilySummary": global_family.get("summary"),
|
|
"feedRelationship": feed_audit.get("canonicalRelationship"),
|
|
"etherscanChainlist": {
|
|
**chainlist,
|
|
"familyChainIds": family_chain_ids,
|
|
"etherscanSupportedFamilyChainIds": supported_family_chain_ids,
|
|
"notEtherscanSupportedFamilyChainIds": unsupported_family_chain_ids,
|
|
"chain138SupportedByEtherscanV2": "138" in chainlist.get("statusByChainId", {}),
|
|
"interpretation": "Only chains present in Etherscan V2 chainlist should be described as first-class Etherscan-family API evidence. Chain 138 remains provenance/context evidence unless Etherscan adds chainid 138.",
|
|
},
|
|
"l2DepositTransactions": l2_deposits,
|
|
},
|
|
"commands": commands,
|
|
"nextActions": next_actions,
|
|
}
|
|
|
|
|
|
def write_md(payload: dict[str, Any], path: Path) -> None:
|
|
readiness = payload["readiness"]
|
|
evidence = payload["evidence"]
|
|
target = payload["target"]
|
|
lines = [
|
|
"# cWUSDC Etherscan Value Dossier",
|
|
"",
|
|
f"- Generated: `{payload['generatedAt']}`",
|
|
f"- Target: `{target['contract']}`",
|
|
f"- CAIP-19: `{target['caip19']}`",
|
|
f"- Ready for external submission: `{readiness['readyForExternalSubmission']}`",
|
|
f"- Etherscan value ready: `{readiness['etherscanValueReady']}`",
|
|
f"- CoinGecko price ready: `{readiness['coinGeckoPriceReady']}`",
|
|
"",
|
|
"## Blockers",
|
|
"",
|
|
]
|
|
if readiness["blockers"]:
|
|
lines.extend(f"- {item}" for item in readiness["blockers"])
|
|
else:
|
|
lines.append("- None detected by this dossier.")
|
|
|
|
lines.extend(
|
|
[
|
|
"",
|
|
"## Evidence Artifacts",
|
|
"",
|
|
"| Artifact | Path |",
|
|
"|---|---|",
|
|
]
|
|
)
|
|
for key, value in evidence["artifacts"].items():
|
|
lines.append(f"| `{key}` | `{value}` |")
|
|
|
|
lines.extend(["", "## Documentation Packet", "", "| Document | Path |", "|---|---|"])
|
|
for key, value in evidence["docs"].items():
|
|
lines.append(f"| `{key}` | `{value}` |")
|
|
|
|
lines.extend(
|
|
[
|
|
"",
|
|
"## Mainnet Contract Verification",
|
|
"",
|
|
f"- Checked: `{(evidence['mainnetContractSourceVerification'] or {}).get('checked')}`",
|
|
f"- Verified: `{(evidence['mainnetContractSourceVerification'] or {}).get('verified')}`",
|
|
f"- Contract name: `{(evidence['mainnetContractSourceVerification'] or {}).get('contractName')}`",
|
|
f"- Compiler: `{(evidence['mainnetContractSourceVerification'] or {}).get('compilerVersion')}`",
|
|
f"- License: `{(evidence['mainnetContractSourceVerification'] or {}).get('licenseType')}`",
|
|
f"- Proxy: `{(evidence['mainnetContractSourceVerification'] or {}).get('proxy')}`",
|
|
"",
|
|
"## Supply Boundary",
|
|
"",
|
|
f"- Ethereum Mainnet cWUSDC supply basis: `{(evidence['mainnetSupply'] or {}).get('totalSupplyUnits')}`",
|
|
f"- Circulating supply basis: `{(evidence['mainnetSupply'] or {}).get('circulatingSupplyUnits')}`",
|
|
f"- Global family warning: {evidence['globalFamilyWarning']}",
|
|
"",
|
|
"## Etherscan Chainlist Boundary",
|
|
"",
|
|
f"- Etherscan V2 chainlist total: `{(evidence['etherscanChainlist'] or {}).get('totalcount')}`",
|
|
f"- Family chain IDs: `{', '.join((evidence['etherscanChainlist'] or {}).get('familyChainIds') or [])}`",
|
|
f"- Etherscan-supported family chain IDs: `{', '.join((evidence['etherscanChainlist'] or {}).get('etherscanSupportedFamilyChainIds') or [])}`",
|
|
f"- Not Etherscan-supported family chain IDs: `{', '.join((evidence['etherscanChainlist'] or {}).get('notEtherscanSupportedFamilyChainIds') or [])}`",
|
|
f"- Chain 138 supported by Etherscan V2: `{(evidence['etherscanChainlist'] or {}).get('chain138SupportedByEtherscanV2')}`",
|
|
"",
|
|
"## L2 Deposit Transaction Boundary",
|
|
"",
|
|
f"- Address checked: `{(evidence['l2DepositTransactions'] or {}).get('address')}`",
|
|
f"- Checked: `{(evidence['l2DepositTransactions'] or {}).get('checked')}`",
|
|
"- Scope: Etherscan-indexed OP/Arbitrum deposit provenance only; it does not set Mainnet cWUSDC USD Value.",
|
|
"- Unit note: `value` is raw wei; `tokenValue` is raw token units. `1195403000000000` in `value` is `0.001195403 ETH`; `598200000000000` with `tokenAddress=ETH` is `0.0005982 ETH`.",
|
|
"",
|
|
"| Chain | Checked | Sample deposits | Latest tx | Native value | Token value |",
|
|
"|---|---:|---:|---|---:|---:|",
|
|
]
|
|
)
|
|
for chain_id, item in (evidence["l2DepositTransactions"].get("chains") or {}).items():
|
|
latest = item.get("latest") or {}
|
|
lines.append(
|
|
f"| `{chain_id}` {item.get('chainName')} | `{item.get('checked')}` | `{item.get('sampleCount', 0)}` | `{latest.get('hash')}` | `{latest.get('valueEth')}` ETH | `{latest.get('tokenValueUnits')}` {latest.get('tokenAddress') or ''} |"
|
|
)
|
|
lines.extend(
|
|
[
|
|
"",
|
|
"## Next Actions",
|
|
"",
|
|
]
|
|
)
|
|
lines.extend(f"- {item}" for item in payload["nextActions"])
|
|
|
|
if payload["commands"]:
|
|
lines.extend(["", "## Command Results", "", "| Command | Exit |", "|---|---:|"])
|
|
for item in payload["commands"]:
|
|
lines.append(f"| `{' '.join(item['command'])}` | `{item['returncode']}` |")
|
|
|
|
path.write_text("\n".join(lines) + "\n")
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--no-refresh", action="store_true", help="Only aggregate existing reports; do not rerun checks.")
|
|
parser.add_argument("--json-out", type=Path, default=REPORT_JSON)
|
|
parser.add_argument("--md-out", type=Path, default=REPORT_MD)
|
|
parser.add_argument("--l2-deposit-address", default="", help="Address to check with Etherscan getdeposittxs.")
|
|
parser.add_argument("--strict", action="store_true")
|
|
args = parser.parse_args()
|
|
args.refresh = not args.no_refresh
|
|
|
|
payload = build(args)
|
|
args.json_out.parent.mkdir(parents=True, exist_ok=True)
|
|
args.json_out.write_text(json.dumps(payload, indent=2) + "\n")
|
|
write_md(payload, args.md_out)
|
|
print(f"Wrote {args.json_out.relative_to(ROOT)}")
|
|
print(f"Wrote {args.md_out.relative_to(ROOT)}")
|
|
print(f"readyForExternalSubmission={payload['readiness']['readyForExternalSubmission']}")
|
|
if payload["readiness"]["blockers"]:
|
|
print("Blockers: " + "; ".join(payload["readiness"]["blockers"]))
|
|
if args.strict and payload["readiness"]["blockers"]:
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|