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>
311 lines
13 KiB
Python
Executable File
311 lines
13 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Audit Chain 138 cUSDC and Ethereum cWUSDC explorer feeds.
|
|
|
|
This produces an evidence packet for Etherscan/listing submissions. It does not
|
|
ask Etherscan to merge Chain 138 traffic into the Ethereum token tracker; rather,
|
|
it documents that Ethereum Mainnet cWUSDC is the wrapped public-network transport
|
|
representation of canonical Chain 138 cUSDC and summarizes both API feeds.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import datetime as dt
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.parse
|
|
import urllib.request
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
CHAIN138_CUSDC = "0xf22258f57794CC8E06237084b353Ab30fFfa640b"
|
|
MAINNET_CWUSDC = "0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a"
|
|
CHAIN138_EXPLORER_API = "https://explorer.d-bis.org/api/v2"
|
|
ETHERSCAN_V2_API = "https://api.etherscan.io/v2/api"
|
|
REPORT_BASE = Path("reports/status/cusdc-cwusdc-etherscan-feed-audit-latest")
|
|
|
|
|
|
def fetch_json(url: str, timeout: int = 30) -> Any:
|
|
req = urllib.request.Request(url, headers={"User-Agent": "dbis-cusdc-cwusdc-feed-audit/1.0"})
|
|
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
payload = response.read().decode("utf-8")
|
|
return json.loads(payload)
|
|
|
|
|
|
def human_units(raw: int, decimals: int) -> str:
|
|
sign = "-" if raw < 0 else ""
|
|
raw = abs(raw)
|
|
whole = raw // (10**decimals)
|
|
frac = str(raw % (10**decimals)).rjust(decimals, "0").rstrip("0")
|
|
return f"{sign}{whole:,}" + (f".{frac}" if frac else "")
|
|
|
|
|
|
def addresses_from_transfer(item: dict[str, Any], style: str) -> set[str]:
|
|
if style == "blockscout":
|
|
values = [
|
|
item.get("from", {}).get("hash"),
|
|
item.get("to", {}).get("hash"),
|
|
]
|
|
else:
|
|
values = [item.get("from"), item.get("to")]
|
|
return {str(v).lower() for v in values if v}
|
|
|
|
|
|
def summarize_blockscout_transfers(items: list[dict[str, Any]], decimals: int) -> dict[str, Any]:
|
|
total_raw = 0
|
|
addresses: set[str] = set()
|
|
methods: dict[str, int] = {}
|
|
latest = items[0] if items else None
|
|
for item in items:
|
|
value = item.get("total", {}).get("value", "0")
|
|
try:
|
|
total_raw += int(value)
|
|
except (TypeError, ValueError):
|
|
pass
|
|
addresses.update(addresses_from_transfer(item, "blockscout"))
|
|
method = item.get("method") or "unknown"
|
|
methods[method] = methods.get(method, 0) + 1
|
|
return {
|
|
"sample_count": len(items),
|
|
"sample_volume_raw": str(total_raw),
|
|
"sample_volume_units": human_units(total_raw, decimals),
|
|
"unique_addresses_in_sample": len(addresses),
|
|
"method_counts": methods,
|
|
"latest_transfer": {
|
|
"hash": latest.get("transaction_hash"),
|
|
"timestamp": latest.get("timestamp"),
|
|
"from": latest.get("from", {}).get("hash"),
|
|
"to": latest.get("to", {}).get("hash"),
|
|
"value_raw": latest.get("total", {}).get("value"),
|
|
"value_units": human_units(int(latest.get("total", {}).get("value", "0")), decimals),
|
|
"method": latest.get("method"),
|
|
}
|
|
if latest
|
|
else None,
|
|
"addresses": sorted(addresses),
|
|
}
|
|
|
|
|
|
def summarize_etherscan_transfers(items: list[dict[str, Any]], decimals: int) -> dict[str, Any]:
|
|
total_raw = 0
|
|
addresses: set[str] = set()
|
|
methods: dict[str, int] = {}
|
|
latest = items[0] if items else None
|
|
for item in items:
|
|
try:
|
|
total_raw += int(item.get("value", "0"))
|
|
except (TypeError, ValueError):
|
|
pass
|
|
addresses.update(addresses_from_transfer(item, "etherscan"))
|
|
method = item.get("methodId") or item.get("functionName") or "unknown"
|
|
methods[method] = methods.get(method, 0) + 1
|
|
return {
|
|
"sample_count": len(items),
|
|
"sample_volume_raw": str(total_raw),
|
|
"sample_volume_units": human_units(total_raw, decimals),
|
|
"unique_addresses_in_sample": len(addresses),
|
|
"method_counts": methods,
|
|
"latest_transfer": {
|
|
"hash": latest.get("hash"),
|
|
"blockNumber": latest.get("blockNumber"),
|
|
"timeStamp": latest.get("timeStamp"),
|
|
"from": latest.get("from"),
|
|
"to": latest.get("to"),
|
|
"value_raw": latest.get("value"),
|
|
"value_units": human_units(int(latest.get("value", "0")), decimals),
|
|
"methodId": latest.get("methodId"),
|
|
"functionName": latest.get("functionName"),
|
|
}
|
|
if latest
|
|
else None,
|
|
"addresses": sorted(addresses),
|
|
}
|
|
|
|
|
|
def blockscout_token_metadata(address: str) -> dict[str, Any]:
|
|
return fetch_json(f"{CHAIN138_EXPLORER_API}/tokens/{address}")
|
|
|
|
|
|
def blockscout_transfers(address: str, pages: int) -> list[dict[str, Any]]:
|
|
items: list[dict[str, Any]] = []
|
|
params: dict[str, Any] | None = None
|
|
for _ in range(pages):
|
|
url = f"{CHAIN138_EXPLORER_API}/tokens/{address}/transfers"
|
|
if params:
|
|
url += "?" + urllib.parse.urlencode(params)
|
|
payload = fetch_json(url)
|
|
items.extend(payload.get("items", []))
|
|
params = payload.get("next_page_params")
|
|
if not params:
|
|
break
|
|
return items
|
|
|
|
|
|
def etherscan_call(params: dict[str, str], api_key: str) -> Any:
|
|
query = {"chainid": "1", **params, "apikey": api_key}
|
|
payload = fetch_json(f"{ETHERSCAN_V2_API}?{urllib.parse.urlencode(query)}")
|
|
if payload.get("status") == "0" and "No transactions found" not in str(payload.get("message")):
|
|
raise RuntimeError(f"Etherscan API error: {payload.get('message')} {payload.get('result')}")
|
|
return payload.get("result", [])
|
|
|
|
|
|
def build_report(args: argparse.Namespace) -> dict[str, Any]:
|
|
api_key = args.etherscan_api_key or os.environ.get("ETHERSCAN_API_KEY", "")
|
|
if not api_key:
|
|
raise SystemExit("ETHERSCAN_API_KEY is required for Ethereum cWUSDC Etherscan API checks")
|
|
|
|
c138_meta = blockscout_token_metadata(args.chain138_cusdc)
|
|
c138_decimals = int(c138_meta.get("decimals") or 6)
|
|
c138_transfers = blockscout_transfers(args.chain138_cusdc, args.chain138_pages)
|
|
|
|
cw_supply_raw = etherscan_call(
|
|
{
|
|
"module": "stats",
|
|
"action": "tokensupply",
|
|
"contractaddress": args.mainnet_cwusdc,
|
|
},
|
|
api_key,
|
|
)
|
|
cw_transfers = etherscan_call(
|
|
{
|
|
"module": "account",
|
|
"action": "tokentx",
|
|
"contractaddress": args.mainnet_cwusdc,
|
|
"page": "1",
|
|
"offset": str(args.etherscan_offset),
|
|
"sort": "desc",
|
|
},
|
|
api_key,
|
|
)
|
|
if not isinstance(cw_transfers, list):
|
|
cw_transfers = []
|
|
|
|
c138_summary = summarize_blockscout_transfers(c138_transfers, c138_decimals)
|
|
cw_summary = summarize_etherscan_transfers(cw_transfers, 6)
|
|
common_addresses = sorted(set(c138_summary["addresses"]) & set(cw_summary["addresses"]))
|
|
|
|
c138_summary_public = {k: v for k, v in c138_summary.items() if k != "addresses"}
|
|
cw_summary_public = {k: v for k, v in cw_summary.items() if k != "addresses"}
|
|
|
|
return {
|
|
"generatedAt": dt.datetime.now(dt.UTC).isoformat().replace("+00:00", "Z"),
|
|
"purpose": "Evidence packet for Etherscan/listing feeds: Chain 138 cUSDC is the canonical source asset; Ethereum cWUSDC is the wrapped transport representation.",
|
|
"canonicalRelationship": {
|
|
"sourceChainId": 138,
|
|
"sourceToken": {
|
|
"symbol": "cUSDC",
|
|
"name": "USD Coin (Compliant)",
|
|
"address": args.chain138_cusdc,
|
|
"explorer": f"https://explorer.d-bis.org/token/{args.chain138_cusdc}",
|
|
"api": f"{CHAIN138_EXPLORER_API}/tokens/{args.chain138_cusdc}",
|
|
},
|
|
"wrappedChainId": 1,
|
|
"wrappedToken": {
|
|
"symbol": "cWUSDC",
|
|
"name": "Wrapped cUSDC",
|
|
"address": args.mainnet_cwusdc,
|
|
"explorer": f"https://etherscan.io/token/{args.mainnet_cwusdc}",
|
|
"api": ETHERSCAN_V2_API,
|
|
},
|
|
"mappingSource": "config/token-mapping-multichain.json: 138 cUSDC -> Ethereum Mainnet cWUSDC",
|
|
"trackerLanguage": "cWUSDC is the Ethereum Mainnet compliant wrapped transport representation of canonical Chain 138 cUSDC. It is not Circle-issued USDC.",
|
|
},
|
|
"chain138Cusdc": {
|
|
"metadata": {
|
|
"name": c138_meta.get("name"),
|
|
"symbol": c138_meta.get("symbol"),
|
|
"decimals": c138_meta.get("decimals"),
|
|
"holders": c138_meta.get("holders"),
|
|
"totalSupplyRaw": c138_meta.get("total_supply"),
|
|
"totalSupplyUnits": human_units(int(c138_meta.get("total_supply") or 0), c138_decimals),
|
|
},
|
|
"transferFeed": c138_summary_public,
|
|
},
|
|
"mainnetCwusdc": {
|
|
"metadata": {
|
|
"name": "Wrapped cUSDC",
|
|
"symbol": "cWUSDC",
|
|
"decimals": "6",
|
|
"totalSupplyRaw": str(cw_supply_raw),
|
|
"totalSupplyUnits": human_units(int(cw_supply_raw or 0), 6),
|
|
},
|
|
"transferFeed": cw_summary_public,
|
|
},
|
|
"crossFeedSignals": {
|
|
"commonAddressesInRecentSamples": common_addresses,
|
|
"commonAddressCount": len(common_addresses),
|
|
"interpretation": "Common addresses are supporting evidence only. Canonical linkage is established by the token mapping, metadata registry, and bridge/listing documentation; Etherscan itself will only index Ethereum Mainnet cWUSDC traffic for the token page.",
|
|
},
|
|
"etherscanSubmissionNote": "Ask Etherscan to list the Ethereum token as Wrapped cUSDC (cWUSDC), with Chain 138 cUSDC identified as the canonical source asset in the description/supporting links. Do not ask Etherscan to add Chain 138 transfer counts to the Ethereum token tracker totals.",
|
|
}
|
|
|
|
|
|
def write_markdown(report: dict[str, Any], path: Path) -> None:
|
|
rel = report["canonicalRelationship"]
|
|
c138 = report["chain138Cusdc"]
|
|
cw = report["mainnetCwusdc"]
|
|
signals = report["crossFeedSignals"]
|
|
lines = [
|
|
"# cUSDC / cWUSDC Etherscan Feed Audit",
|
|
"",
|
|
f"Generated: `{report['generatedAt']}`",
|
|
"",
|
|
"## Relationship",
|
|
"",
|
|
f"- Source asset: Chain 138 `cUSDC` at `{rel['sourceToken']['address']}`",
|
|
f"- Wrapped transport asset: Ethereum Mainnet `cWUSDC` at `{rel['wrappedToken']['address']}`",
|
|
f"- Mapping source: `{rel['mappingSource']}`",
|
|
f"- Tracker language: {rel['trackerLanguage']}",
|
|
"",
|
|
"## API Feed Summary",
|
|
"",
|
|
"| Feed | Supply | Recent sample transfers | Recent sample volume | Unique addresses in sample |",
|
|
"|---|---:|---:|---:|---:|",
|
|
f"| Chain 138 cUSDC Blockscout | {c138['metadata']['totalSupplyUnits']} | {c138['transferFeed']['sample_count']} | {c138['transferFeed']['sample_volume_units']} | {c138['transferFeed']['unique_addresses_in_sample']} |",
|
|
f"| Ethereum cWUSDC Etherscan | {cw['metadata']['totalSupplyUnits']} | {cw['transferFeed']['sample_count']} | {cw['transferFeed']['sample_volume_units']} | {cw['transferFeed']['unique_addresses_in_sample']} |",
|
|
"",
|
|
"## Latest Transfers",
|
|
"",
|
|
f"- Chain 138 cUSDC latest: `{(c138['transferFeed']['latest_transfer'] or {}).get('hash')}`",
|
|
f"- Ethereum cWUSDC latest: `{(cw['transferFeed']['latest_transfer'] or {}).get('hash')}`",
|
|
"",
|
|
"## Cross-Feed Signal",
|
|
"",
|
|
f"- Common addresses in recent API samples: `{signals['commonAddressCount']}`",
|
|
f"- Interpretation: {signals['interpretation']}",
|
|
"",
|
|
"## Etherscan Submission Note",
|
|
"",
|
|
report["etherscanSubmissionNote"],
|
|
"",
|
|
]
|
|
path.write_text("\n".join(lines), encoding="utf-8")
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--chain138-cusdc", default=CHAIN138_CUSDC)
|
|
parser.add_argument("--mainnet-cwusdc", default=MAINNET_CWUSDC)
|
|
parser.add_argument("--etherscan-api-key", default="")
|
|
parser.add_argument("--chain138-pages", type=int, default=3)
|
|
parser.add_argument("--etherscan-offset", type=int, default=150)
|
|
parser.add_argument("--json-out", default=f"{REPORT_BASE}.json")
|
|
parser.add_argument("--md-out", default=f"{REPORT_BASE}.md")
|
|
args = parser.parse_args()
|
|
|
|
report = build_report(args)
|
|
json_path = Path(args.json_out)
|
|
md_path = Path(args.md_out)
|
|
json_path.parent.mkdir(parents=True, exist_ok=True)
|
|
json_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
|
|
write_markdown(report, md_path)
|
|
print(f"Wrote {json_path}")
|
|
print(f"Wrote {md_path}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|