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>
247 lines
9.9 KiB
Python
247 lines
9.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Batch mainnet cWUSDC to EI matrix wallets via canonical Multicall3 aggregate3.
|
|
|
|
Each inner call is transferFrom(deployer, recipient, amount) on the token, so
|
|
msg.sender is Multicall3. Requires a prior approve(deployer -> Multicall3) for
|
|
at least the sum of amounts in this run (one tx before batches).
|
|
|
|
Default Multicall3 (Ethereum): 0xcA11bde05977b3631167028862bE2a173976CA11
|
|
|
|
Examples:
|
|
python3 scripts/lib/ei_matrix_multicall3_cwusdc_batch.py --dry-run \\
|
|
--tsv reports/status/ei-matrix-cwusdc-topup-amounts.tsv
|
|
|
|
python3 scripts/lib/ei_matrix_multicall3_cwusdc_batch.py --execute \\
|
|
--tsv reports/status/ei-matrix-cwusdc-topup-amounts.tsv
|
|
|
|
Env: PRIVATE_KEY (or DEPLOYER_ADDRESS for dry-run calldata only), ETHEREUM_MAINNET_RPC,
|
|
CWUSDC_MAINNET (optional), MULTICALL3_MAINNET (optional), EI_MATRIX_MC_CHUNK (default 200).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
MULTICALL3_MAINNET = "0xcA11bde05977b3631167028862bE2a173976CA11"
|
|
DEFAULT_CWUSDC = "0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a"
|
|
|
|
|
|
def _sh(cmd: list[str]) -> str:
|
|
r = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
|
if r.returncode != 0:
|
|
raise RuntimeError(f"command failed: {' '.join(cmd)}\n{(r.stderr or r.stdout).strip()}")
|
|
return (r.stdout or "").strip()
|
|
|
|
|
|
def _deployer(pk: str | None) -> str:
|
|
if pk:
|
|
return _sh(["cast", "wallet", "address", "--private-key", pk])
|
|
env = (os.environ.get("DEPLOYER_ADDRESS") or os.environ.get("DEPLOYER") or "").strip()
|
|
if env:
|
|
return env
|
|
raise SystemExit("Set PRIVATE_KEY or DEPLOYER_ADDRESS for transferFrom(from=...)")
|
|
|
|
|
|
def _cast_calldata_transfer_from(from_addr: str, to_addr: str, amount: int) -> str:
|
|
out = _sh(["cast", "calldata", "transferFrom(address,address,uint256)", from_addr, to_addr, str(amount)])
|
|
return out if out.startswith("0x") else "0x" + out
|
|
|
|
|
|
def _cast_calldata_aggregate3(calls_tuple_str: str) -> str:
|
|
out = _sh(["cast", "calldata", "aggregate3((address,bool,bytes)[])", calls_tuple_str])
|
|
return out if out.startswith("0x") else "0x" + out
|
|
|
|
|
|
def _estimate_gas(from_addr: str, multicall: str, data: str, rpc_url: str) -> int:
|
|
payload = json.dumps({"from": from_addr, "to": multicall, "data": data})
|
|
raw = _sh(["cast", "rpc", "eth_estimateGas", payload, "--rpc-url", rpc_url])
|
|
return int(raw, 16)
|
|
|
|
|
|
def _allowance(token: str, owner: str, spender: str, rpc_url: str) -> int:
|
|
out = _sh(["cast", "call", token, "allowance(address,address)(uint256)", owner, spender, "--rpc-url", rpc_url])
|
|
return int(out.split()[0], 0)
|
|
|
|
|
|
def _send_cast_send(to: str, sig: str, args: list[str], rpc_url: str, pk: str, gas_limit: str | None) -> None:
|
|
cmd = ["cast", "send", to, sig, *args, "--rpc-url", rpc_url, "--private-key", pk]
|
|
if gas_limit:
|
|
cmd.extend(["--gas-limit", gas_limit])
|
|
print("→", " ".join(cmd[:8]), "…", file=sys.stderr)
|
|
r = subprocess.run(cmd, env={**os.environ})
|
|
if r.returncode != 0:
|
|
sys.exit(r.returncode)
|
|
|
|
|
|
def _send_raw_calldata(to: str, data: str, rpc_url: str, pk: str, gas_limit: str) -> None:
|
|
cmd = ["cast", "send", to, data, "--rpc-url", rpc_url, "--private-key", pk, "--gas-limit", gas_limit]
|
|
print("→ cast send", to[:10] + "…", "--gas-limit", gas_limit, file=sys.stderr)
|
|
r = subprocess.run(cmd, env={**os.environ})
|
|
if r.returncode != 0:
|
|
sys.exit(r.returncode)
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--tsv", required=True, help="linearIndex TAB amountRaw")
|
|
ap.add_argument("--grid", default="config/pmm-soak-wallet-grid.json")
|
|
ap.add_argument("--chunk-size", type=int, default=int(os.environ.get("EI_MATRIX_MC_CHUNK", "200")))
|
|
ap.add_argument("--multicall", default=os.environ.get("MULTICALL3_MAINNET", MULTICALL3_MAINNET))
|
|
ap.add_argument("--token", default=os.environ.get("CWUSDC_MAINNET", DEFAULT_CWUSDC))
|
|
ap.add_argument("--rpc-url", default=os.environ.get("ETHEREUM_MAINNET_RPC") or os.environ.get("RPC_URL_1") or "")
|
|
ap.add_argument("--dry-run", action="store_true")
|
|
ap.add_argument("--execute", action="store_true")
|
|
ap.add_argument("--gas-headroom-bps", type=int, default=13000)
|
|
ap.add_argument("--min-gas-per-batch", type=int, default=500_000)
|
|
ap.add_argument("--start-batch", type=int, default=0)
|
|
ap.add_argument("--max-batches", type=int, default=0, help="0 = all remaining")
|
|
ap.add_argument("--progress-file", default="reports/status/ei-matrix-multicall3-batch-progress.txt")
|
|
args = ap.parse_args()
|
|
|
|
if not args.rpc_url:
|
|
print("Need --rpc-url or ETHEREUM_MAINNET_RPC / RPC_URL_1", file=sys.stderr)
|
|
return 2
|
|
if args.dry_run == args.execute:
|
|
print("Specify exactly one of --dry-run or --execute", file=sys.stderr)
|
|
return 2
|
|
|
|
repo = Path(__file__).resolve().parents[2]
|
|
grid_path = repo / args.grid if not os.path.isabs(args.grid) else Path(args.grid)
|
|
tsv_path = repo / args.tsv if not os.path.isabs(args.tsv) else Path(args.tsv)
|
|
|
|
wallets = json.loads(grid_path.read_text(encoding="utf-8"))["wallets"]
|
|
rows: list[tuple[str, int]] = []
|
|
for line in tsv_path.read_text(encoding="utf-8").splitlines():
|
|
line = line.split("#", 1)[0].strip()
|
|
if not line:
|
|
continue
|
|
parts = line.split("\t")
|
|
if len(parts) < 2:
|
|
parts = line.split()
|
|
if len(parts) < 2:
|
|
continue
|
|
idx = int(parts[0])
|
|
amt = int(parts[1])
|
|
if amt <= 0:
|
|
continue
|
|
addr = wallets[idx]["address"]
|
|
rows.append((addr, amt))
|
|
|
|
if not rows:
|
|
print("No positive-amount rows in TSV.", file=sys.stderr)
|
|
return 0
|
|
|
|
pk = os.environ.get("PRIVATE_KEY", "").strip() or None
|
|
if args.execute and not pk:
|
|
print("PRIVATE_KEY required for --execute", file=sys.stderr)
|
|
return 2
|
|
|
|
deployer = _deployer(pk)
|
|
|
|
mc = args.multicall
|
|
token = args.token
|
|
|
|
all_chunks: list[list[tuple[str, int]]] = []
|
|
for i in range(0, len(rows), args.chunk_size):
|
|
all_chunks.append(rows[i : i + args.chunk_size])
|
|
|
|
start_b = max(0, args.start_batch)
|
|
if args.max_batches > 0:
|
|
end_b = min(len(all_chunks), start_b + args.max_batches)
|
|
else:
|
|
end_b = len(all_chunks)
|
|
chunks = all_chunks[start_b:end_b]
|
|
budget_raw = sum(amt for c in chunks for _, amt in c)
|
|
|
|
if not chunks:
|
|
print("No batches in range.", file=sys.stderr)
|
|
return 0
|
|
|
|
print(
|
|
f"batches {start_b}..{end_b - 1} of {len(all_chunks)} transfers={sum(len(c) for c in chunks)} "
|
|
f"budget_raw={budget_raw}",
|
|
file=sys.stderr,
|
|
)
|
|
|
|
if args.dry_run:
|
|
try:
|
|
allow = _allowance(token, deployer, mc, args.rpc_url)
|
|
except Exception:
|
|
allow = 0
|
|
print(f"# allowance Multicall3: {allow} budget_this_run: {budget_raw}", file=sys.stderr)
|
|
if allow < budget_raw:
|
|
print(
|
|
f"cast send {token} \"approve(address,uint256)\" {mc} {budget_raw} \\\n"
|
|
f" --rpc-url \"$ETHEREUM_MAINNET_RPC\" --private-key \"$PRIVATE_KEY\" --gas-limit 120000",
|
|
file=sys.stderr,
|
|
)
|
|
chunk = chunks[0]
|
|
parts = []
|
|
for addr, amt in chunk:
|
|
data = _cast_calldata_transfer_from(deployer, addr, amt)
|
|
parts.append(f"({token},false,{data})")
|
|
tuple_str = "[" + ",".join(parts) + "]"
|
|
calldata = _cast_calldata_aggregate3(tuple_str)
|
|
gl = args.min_gas_per_batch + 65_000 * len(chunk)
|
|
sample_hex = repo / "reports/status/ei-matrix-multicall3-dryrun-sample-batch.hex"
|
|
sample_hex.write_text(calldata + "\n", encoding="utf-8")
|
|
rel = os.path.relpath(str(sample_hex), str(repo))
|
|
print(f"\n# sample batch 0 n={len(chunk)} gas_limit~{gl}", file=sys.stderr)
|
|
print(f"# calldata written: {rel}", file=sys.stderr)
|
|
print(
|
|
f"cast send {mc} $(cat {rel}) --rpc-url \"$ETHEREUM_MAINNET_RPC\" \\\n"
|
|
f" --private-key \"$PRIVATE_KEY\" --gas-limit {gl}"
|
|
)
|
|
print(f"\n# … {len(chunks)} batches total (chunk_size={args.chunk_size})", file=sys.stderr)
|
|
return 0
|
|
|
|
assert pk is not None
|
|
allow = _allowance(token, deployer, mc, args.rpc_url)
|
|
if allow < budget_raw:
|
|
print(f"Approving Multicall3 for {budget_raw} raw (was {allow})", file=sys.stderr)
|
|
_send_cast_send(token, "approve(address,uint256)", [mc, str(budget_raw)], args.rpc_url, pk, "120000")
|
|
time.sleep(2)
|
|
allow2 = _allowance(token, deployer, mc, args.rpc_url)
|
|
if allow2 < budget_raw:
|
|
print(f"Allowance insufficient: {allow2} < {budget_raw}", file=sys.stderr)
|
|
return 1
|
|
|
|
progress_path = repo / args.progress_file
|
|
progress_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
for bi, chunk in enumerate(chunks):
|
|
global_batch_idx = start_b + bi
|
|
parts = []
|
|
for addr, amt in chunk:
|
|
data = _cast_calldata_transfer_from(deployer, addr, amt)
|
|
parts.append(f"({token},false,{data})")
|
|
tuple_str = "[" + ",".join(parts) + "]"
|
|
calldata = _cast_calldata_aggregate3(tuple_str)
|
|
|
|
gas_est = args.min_gas_per_batch
|
|
try:
|
|
gas_est = _estimate_gas(deployer, mc, calldata, args.rpc_url)
|
|
except Exception as e:
|
|
print(f"[warn] estimateGas failed, fallback: {e}", file=sys.stderr)
|
|
gas_est = 70_000 * len(chunk) + 400_000
|
|
|
|
gas_with_headroom = max(args.min_gas_per_batch, (gas_est * args.gas_headroom_bps + 9999) // 10000)
|
|
print(f"Batch {global_batch_idx}: n={len(chunk)} estimate={gas_est} limit={gas_with_headroom}", file=sys.stderr)
|
|
|
|
_send_raw_calldata(mc, calldata, args.rpc_url, pk, str(gas_with_headroom))
|
|
progress_path.write_text(f"{global_batch_idx}\n", encoding="utf-8")
|
|
time.sleep(1)
|
|
|
|
print("Done.", file=sys.stderr)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|