All checks were successful
Deploy to Phoenix / validate (push) Successful in 1m11s
Deploy to Phoenix / deploy (push) Successful in 43s
Deploy to Phoenix / deploy-atomic-swap-dapp (push) Successful in 1m32s
phoenix-deploy Deployed to cloudflare-sync
Deploy to Phoenix / cloudflare (push) Successful in 38s
340 lines
12 KiB
Bash
340 lines
12 KiB
Bash
#!/usr/bin/env bash
|
||
# Mint Ethereum mainnet cWUSDC directly to each address in config/pmm-soak-wallet-grid.json
|
||
# (EI matrix). Requires PRIVATE_KEY with MINTER_ROLE on the cWUSDC token.
|
||
#
|
||
# Modes (exactly one):
|
||
# --mint-raw R Same raw units minted to every wallet in the slice.
|
||
# --total-mint-raw B Total supply to mint across the slice, split with ±spread
|
||
# then renormalized to B (same algorithm as transfer distribution).
|
||
#
|
||
# Usage:
|
||
# ./scripts/deployment/mint-cwusdc-ei-matrix-wallets.sh [--dry-run] [--limit N] [--offset N|--resume-next]
|
||
# (--mint-raw R | --total-mint-raw B [--spread-pct S])
|
||
#
|
||
# --quiet-dry-run With --dry-run, suppress per-wallet lines.
|
||
# --legacy Pass --legacy to cast send.
|
||
#
|
||
# Env: ETHEREUM_MAINNET_RPC, CWUSDC_MAINNET, PRIVATE_KEY,
|
||
# EI_MATRIX_MINT_GAS_EST (default 60000), EI_MATRIX_GAS_HEADROOM_BPS (default 10500),
|
||
# EI_MATRIX_SKIP_GAS_CHECK=1 to bypass ETH preflight.
|
||
#
|
||
# Progress: reports/status/ei-matrix-cwusdc-mint-last-idx.txt
|
||
# Failures: reports/status/ei-matrix-cwusdc-mint-failures.log
|
||
# Lock: reports/status/ei-matrix-cwusdc-mint.lock
|
||
#
|
||
# On-chain: cWUSDC uses CompliantWrappedToken-style mint(address,uint256) for MINTER_ROLE.
|
||
# If mint reverts (reserve policy, roles), fix on-chain state before retrying.
|
||
#
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||
cd "$PROJECT_ROOT"
|
||
|
||
DRY_RUN=false
|
||
LIMIT=""
|
||
OFFSET="0"
|
||
OFFSET_EXPLICIT=false
|
||
RESUME_NEXT=false
|
||
SPREAD_PCT="${EI_MATRIX_SPREAD_PCT:-15}"
|
||
CAST_LEGACY=false
|
||
QUIET_DRY_RUN=false
|
||
MINT_RAW=""
|
||
TOTAL_MINT_RAW=""
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--dry-run) DRY_RUN=true; shift ;;
|
||
--quiet-dry-run) QUIET_DRY_RUN=true; shift ;;
|
||
--limit) LIMIT="${2:?}"; shift 2 ;;
|
||
--resume-next) RESUME_NEXT=true; shift ;;
|
||
--offset) OFFSET="${2:?}"; OFFSET_EXPLICIT=true; shift 2 ;;
|
||
--spread-pct) SPREAD_PCT="${2:?}"; shift 2 ;;
|
||
--mint-raw) MINT_RAW="${2:?}"; shift 2 ;;
|
||
--total-mint-raw) TOTAL_MINT_RAW="${2:?}"; shift 2 ;;
|
||
--legacy) CAST_LEGACY=true; shift ;;
|
||
*) echo "Unknown arg: $1" >&2; exit 1 ;;
|
||
esac
|
||
done
|
||
|
||
LAST_IDX_FILE="${EI_MATRIX_CWUSDC_MINT_LAST_IDX_FILE:-${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-mint-last-idx.txt}"
|
||
if $RESUME_NEXT && $OFFSET_EXPLICIT; then
|
||
echo "Use only one of --offset or --resume-next." >&2
|
||
exit 1
|
||
fi
|
||
if $RESUME_NEXT; then
|
||
[[ -f "$LAST_IDX_FILE" ]] || { echo "Missing last-index file for --resume-next: $LAST_IDX_FILE" >&2; exit 1; }
|
||
_last="$(tr -d '[:space:]' < "$LAST_IDX_FILE" || echo "")"
|
||
[[ -n "$_last" ]] || { echo "Empty $LAST_IDX_FILE" >&2; exit 1; }
|
||
OFFSET=$((_last + 1))
|
||
echo "Resume-next (mint): last completed idx=$_last → offset=$OFFSET"
|
||
fi
|
||
|
||
if [[ -n "$MINT_RAW" && -n "$TOTAL_MINT_RAW" ]]; then
|
||
echo "Use only one of --mint-raw or --total-mint-raw." >&2
|
||
exit 1
|
||
fi
|
||
if [[ -z "$MINT_RAW" && -z "$TOTAL_MINT_RAW" ]]; then
|
||
echo "Set --mint-raw or --total-mint-raw." >&2
|
||
exit 1
|
||
fi
|
||
|
||
# shellcheck disable=SC1091
|
||
source "$PROJECT_ROOT/scripts/lib/load-project-env.sh"
|
||
|
||
CWUSDC="${CWUSDC_MAINNET:-0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a}"
|
||
PUBLIC_ETH_RPC="${ETHEREUM_MAINNET_PUBLIC_RPC:-https://ethereum-rpc.publicnode.com}"
|
||
RPC="${ETHEREUM_MAINNET_RPC:-${RPC_URL_1:-${ETH_MAINNET_RPC_URL:-$PUBLIC_ETH_RPC}}}"
|
||
BALANCE_RPC="${EI_MATRIX_BALANCE_RPC:-$RPC}"
|
||
|
||
LOCK_FILE="${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-mint.lock"
|
||
MANIFEST_DIR="${PROJECT_ROOT}/reports/status"
|
||
mkdir -p "$MANIFEST_DIR"
|
||
exec 200>"$LOCK_FILE"
|
||
if ! flock -n 200; then
|
||
echo "Another mint-cwusdc-ei-matrix-wallets.sh is already running (lock: $LOCK_FILE)." >&2
|
||
exit 1
|
||
fi
|
||
|
||
GRID="$PROJECT_ROOT/config/pmm-soak-wallet-grid.json"
|
||
DEPLOYER_CANONICAL="0x4A666F96fC8764181194447A7dFdb7d471b301C8"
|
||
|
||
[[ -f "$GRID" ]] || { echo "Missing $GRID" >&2; exit 1; }
|
||
command -v cast &>/dev/null || { echo "cast required" >&2; exit 1; }
|
||
command -v jq &>/dev/null || { echo "jq required" >&2; exit 1; }
|
||
[[ -n "${PRIVATE_KEY:-}" ]] || { echo "PRIVATE_KEY not set" >&2; exit 1; }
|
||
|
||
FROM_ADDR=$(cast wallet address --private-key "$PRIVATE_KEY")
|
||
CHAIN_ID=$(cast chain-id --rpc-url "$RPC" 2>/dev/null | tr -d '[:space:]' || true)
|
||
[[ -n "$CHAIN_ID" ]] || CHAIN_ID="1"
|
||
if [[ "$CHAIN_ID" != "1" ]]; then
|
||
echo "[WARN] chain-id=$CHAIN_ID (expected 1)." >&2
|
||
fi
|
||
|
||
pending_nonce() {
|
||
local resp hex
|
||
resp=$(curl -sS -X POST "$RPC" -H "Content-Type: application/json" \
|
||
-d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionCount\",\"params\":[\"${FROM_ADDR}\",\"pending\"],\"id\":1}" 2>/dev/null) || return 1
|
||
hex=$(echo "$resp" | jq -r '.result // empty')
|
||
[[ -n "$hex" ]] || return 1
|
||
cast to-dec "$hex"
|
||
}
|
||
|
||
token_decimals() {
|
||
cast call "$CWUSDC" 'decimals()(uint8)' --rpc-url "$BALANCE_RPC" 2>/dev/null | awk '{print $1}'
|
||
}
|
||
|
||
generate_spread_amounts_raw() {
|
||
local count="$1" budget="$2" spread="$3"
|
||
python3 - "$count" "$budget" "$spread" <<'PY'
|
||
import random
|
||
import sys
|
||
n = int(sys.argv[1])
|
||
budget = int(sys.argv[2])
|
||
spread = float(sys.argv[3])
|
||
if n <= 0:
|
||
sys.exit("count must be positive")
|
||
if budget < 0:
|
||
sys.exit("budget must be non-negative")
|
||
if spread < 0 or spread > 100:
|
||
sys.exit("spread-pct must be in [0, 100]")
|
||
base = 10000
|
||
low_w = max(1, (100 * base - int(spread * base)) // 100)
|
||
high_w = (100 * base + int(spread * base)) // 100
|
||
w = [random.randint(low_w, high_w) for _ in range(n)]
|
||
s = sum(w)
|
||
raw = [(budget * wi) // s for wi in w]
|
||
rem = budget - sum(raw)
|
||
for i in range(rem):
|
||
raw[i % n] += 1
|
||
for x in raw:
|
||
print(x)
|
||
PY
|
||
}
|
||
|
||
stream_addresses() {
|
||
if [[ -n "${LIMIT:-}" ]]; then
|
||
jq -r --argjson o "$OFFSET" --argjson l "$LIMIT" '.wallets[$o:$o+$l][] | .address' "$GRID"
|
||
else
|
||
jq -r --argjson o "$OFFSET" '.wallets[$o:][] | .address' "$GRID"
|
||
fi
|
||
}
|
||
|
||
ERR_LOG="${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-mint-failures.log"
|
||
LAST_IDX="${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-mint-last-idx.txt"
|
||
|
||
matrix_try_mint() {
|
||
local addr="$1" raw_amt="$2" idx="$3"
|
||
local dec human out tx attempt=1
|
||
dec="${DECIMALS:-6}"
|
||
if [[ "$raw_amt" == "0" ]]; then
|
||
echo "[skip] idx=$idx $addr zero raw"
|
||
return 0
|
||
fi
|
||
human=$(python3 -c "d=int('$dec'); a=int('$raw_amt'); print(f'{a / (10**d):.{min(d,8)}f}')" 2>/dev/null || echo "$raw_amt")
|
||
if $DRY_RUN; then
|
||
if ! $QUIET_DRY_RUN; then
|
||
echo "[dry-run] idx=$idx $addr raw=$raw_amt (~$human)"
|
||
fi
|
||
return 0
|
||
fi
|
||
local cast_extra=()
|
||
$CAST_LEGACY && cast_extra+=(--legacy)
|
||
while [[ "$attempt" -le 2 ]]; do
|
||
if out=$(cast send "$CWUSDC" "mint(address,uint256)" "$addr" "$raw_amt" \
|
||
--rpc-url "$RPC" --private-key "$PRIVATE_KEY" \
|
||
--nonce "$NONCE" "${cast_extra[@]}" 2>&1); then
|
||
tx=$(echo "$out" | tail -n1)
|
||
echo "[ok] idx=$idx nonce=$NONCE $addr raw=$raw_amt (~$human) tx=$tx"
|
||
sent=$((sent + 1))
|
||
NONCE=$((NONCE + 1))
|
||
echo "$idx" > "$LAST_IDX"
|
||
return 0
|
||
fi
|
||
if [[ "$attempt" -eq 1 ]] && echo "$out" | grep -qi 'nonce too low'; then
|
||
NONCE=$(pending_nonce) || true
|
||
echo "[retry] idx=$idx nonce refreshed to $NONCE (nonce too low)" >&2
|
||
attempt=$((attempt + 1))
|
||
continue
|
||
fi
|
||
echo "[fail] idx=$idx nonce=$NONCE $addr $out" | tee -a "$ERR_LOG" >&2
|
||
failed=$((failed + 1))
|
||
NONCE=$(pending_nonce) || true
|
||
return 0
|
||
done
|
||
}
|
||
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo "EI matrix cWUSDC mint (mainnet)"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo "RPC: $RPC"
|
||
echo "Token: $CWUSDC"
|
||
echo "Signer: $FROM_ADDR"
|
||
echo "Grid: $GRID"
|
||
echo "Dry-run: $DRY_RUN Quiet: $QUIET_DRY_RUN"
|
||
echo "Offset: $OFFSET Limit: ${LIMIT:-all}"
|
||
if [[ -n "$MINT_RAW" ]]; then
|
||
echo "Mode: fixed --mint-raw $MINT_RAW per wallet"
|
||
else
|
||
echo "Mode: --total-mint-raw $TOTAL_MINT_RAW spread: ±${SPREAD_PCT}% normalized"
|
||
fi
|
||
echo ""
|
||
|
||
if [[ "${FROM_ADDR,,}" != "${DEPLOYER_CANONICAL,,}" ]]; then
|
||
echo "[WARN] Signer is not canonical deployer $DEPLOYER_CANONICAL — minter role may still be granted."
|
||
echo ""
|
||
fi
|
||
|
||
DECIMALS=$(token_decimals || echo "6")
|
||
|
||
ADDR_TMP=$(mktemp)
|
||
AMOUNTS_TMP=$(mktemp)
|
||
cleanup_tmp() {
|
||
[[ -f "$ADDR_TMP" ]] && rm -f "$ADDR_TMP"
|
||
[[ -f "$AMOUNTS_TMP" ]] && rm -f "$AMOUNTS_TMP"
|
||
}
|
||
trap cleanup_tmp EXIT
|
||
|
||
stream_addresses > "$ADDR_TMP"
|
||
WALLET_COUNT=$(wc -l < "$ADDR_TMP" | tr -d '[:space:]')
|
||
if [[ -z "$WALLET_COUNT" || "$WALLET_COUNT" -eq 0 ]]; then
|
||
echo "No wallets in range (offset=$OFFSET limit=${LIMIT:-all})." >&2
|
||
exit 1
|
||
fi
|
||
|
||
if [[ -n "$MINT_RAW" ]]; then
|
||
awk -v r="$MINT_RAW" '{print r}' "$ADDR_TMP" > "$AMOUNTS_TMP"
|
||
BUDGET_RAW=$((MINT_RAW * WALLET_COUNT))
|
||
else
|
||
BUDGET_RAW="$TOTAL_MINT_RAW"
|
||
if [[ "$BUDGET_RAW" -le 0 ]]; then
|
||
echo "total-mint-raw must be positive." >&2
|
||
exit 1
|
||
fi
|
||
generate_spread_amounts_raw "$WALLET_COUNT" "$BUDGET_RAW" "$SPREAD_PCT" > "$AMOUNTS_TMP"
|
||
fi
|
||
|
||
SUM_CHECK=$(awk '{s+=$1} END {print s}' "$AMOUNTS_TMP")
|
||
if [[ "$SUM_CHECK" != "$BUDGET_RAW" ]]; then
|
||
echo "INTERNAL: amount sum $SUM_CHECK != budget $BUDGET_RAW" >&2
|
||
exit 1
|
||
fi
|
||
|
||
AMOUNTS_SHA256=$(sha256sum "$AMOUNTS_TMP" | awk '{print $1}')
|
||
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||
MANIFEST="$MANIFEST_DIR/ei-matrix-cwusdc-mint-manifest-${TS//:/-}.json"
|
||
cat >"$MANIFEST" <<EOF
|
||
{
|
||
"version": 1,
|
||
"kind": "ei-matrix-cwusdc-mint",
|
||
"timestamp": "$TS",
|
||
"chainId": 1,
|
||
"token": "$CWUSDC",
|
||
"signer": "$FROM_ADDR",
|
||
"offset": $OFFSET,
|
||
"limit": ${LIMIT:-null},
|
||
"walletCount": $WALLET_COUNT,
|
||
"budgetRaw": "$BUDGET_RAW",
|
||
"fixedMintRaw": ${MINT_RAW:-null},
|
||
"spreadPct": ${SPREAD_PCT},
|
||
"amountsSha256": "$AMOUNTS_SHA256",
|
||
"manifestPath": "$MANIFEST"
|
||
}
|
||
EOF
|
||
echo "Manifest: $MANIFEST"
|
||
echo "Amounts SHA256: $AMOUNTS_SHA256"
|
||
echo ""
|
||
|
||
GAS_EST="${EI_MATRIX_MINT_GAS_EST:-60000}"
|
||
HEADROOM_BPS="${EI_MATRIX_GAS_HEADROOM_BPS:-10500}"
|
||
ETH_WEI=$(cast balance "$FROM_ADDR" --rpc-url "$BALANCE_RPC" 2>/dev/null | awk '{print $1}' || echo "0")
|
||
ETH_HUMAN=$(python3 -c "print(f'{int(\"$ETH_WEI\") / 1e18:.6f}')" 2>/dev/null || echo "?")
|
||
echo "Signer ETH (gas): ${ETH_WEI} wei (~$ETH_HUMAN ETH)"
|
||
|
||
if ! $DRY_RUN && [[ "${EI_MATRIX_SKIP_GAS_CHECK:-}" != "1" ]]; then
|
||
GAS_PRICE_WEI=$(cast gas-price --rpc-url "$RPC" 2>/dev/null | awk '{print $1}' | head -1)
|
||
[[ -n "$GAS_PRICE_WEI" ]] || GAS_PRICE_WEI=0
|
||
MIN_WEI=$(python3 -c "c=int('$WALLET_COUNT'); g=int('$GAS_EST'); p=int('$GAS_PRICE_WEI'); b=int('$HEADROOM_BPS'); print(c*g*p*b//10000)")
|
||
if python3 -c "import sys; sys.exit(0 if int('$ETH_WEI') >= int('$MIN_WEI') else 1)"; then
|
||
echo "Gas preflight OK: est ${GAS_EST} gas/tx × $WALLET_COUNT × gasPrice $GAS_PRICE_WEI × (${HEADROOM_BPS}/10000) ≈ $MIN_WEI wei."
|
||
else
|
||
echo "Insufficient ETH for gas preflight. Need ≈ $MIN_WEI wei." >&2
|
||
echo "Set EI_MATRIX_SKIP_GAS_CHECK=1 to override (operator risk)." >&2
|
||
exit 1
|
||
fi
|
||
fi
|
||
echo ""
|
||
|
||
echo "Sample (first 3, last 3):"
|
||
_s_idx=$OFFSET
|
||
while IFS= read -r s_addr && IFS= read -r s_raw <&3; do
|
||
h=$(python3 -c "d=int('$DECIMALS'); a=int('$s_raw'); print(f'{a / (10**d):.6f}')" 2>/dev/null || echo "$s_raw")
|
||
echo " idx=$_s_idx $s_addr raw=$s_raw (~$h cWUSDC)"
|
||
_s_idx=$((_s_idx + 1))
|
||
done < <(head -3 "$ADDR_TMP") 3< <(head -3 "$AMOUNTS_TMP")
|
||
_s_idx=$((OFFSET + WALLET_COUNT - 3))
|
||
while IFS= read -r s_addr && IFS= read -r s_raw <&3; do
|
||
h=$(python3 -c "d=int('$DECIMALS'); a=int('$s_raw'); print(f'{a / (10**d):.6f}')" 2>/dev/null || echo "$s_raw")
|
||
echo " idx=$_s_idx $s_addr raw=$s_raw (~$h cWUSDC)"
|
||
_s_idx=$((_s_idx + 1))
|
||
done < <(tail -3 "$ADDR_TMP") 3< <(tail -3 "$AMOUNTS_TMP")
|
||
echo ""
|
||
|
||
sent=0
|
||
failed=0
|
||
idx=$OFFSET
|
||
NONCE=$(pending_nonce) || { echo "Could not read pending nonce" >&2; exit 1; }
|
||
echo "Starting nonce (pending): $NONCE"
|
||
echo ""
|
||
|
||
while IFS=$'\t' read -r addr raw_amt; do
|
||
matrix_try_mint "$addr" "$raw_amt" "$idx"
|
||
idx=$((idx + 1))
|
||
done < <(paste -d $'\t' "$ADDR_TMP" "$AMOUNTS_TMP")
|
||
|
||
if $DRY_RUN; then
|
||
echo "Dry-run complete. Indices covered: $OFFSET..$((idx - 1))."
|
||
else
|
||
echo "Done. Mint txs attempted: sent=$sent failed=$failed"
|
||
fi
|