#!/usr/bin/env bash # Check deployer balances on all configured networks, estimate gas costs via gas API, # ensure sufficient network token, then optionally deploy (Chain 138 phased core). # Uses PRIVATE_KEY from .env (smom-dbis-138). # # Balance check: On EVM chains only the NATIVE token can be used for gas. We check that # token for each network (cast balance = native currency). ERC-20 tokens (USDT, LINK, etc.) # cannot be used for gas unless a meta-tx relayer is in use. # # Gas token per network (only these can be used for gas): # Chain 138, Ethereum, Base, Optimism, Sepolia, Base Sepolia, Optimism Sepolia → ETH # Polygon, Polygon Amoy → MATIC # BSC → BNB # Avalanche → AVAX # Cronos → CRO # Gnosis → xDAI # Arbitrum → ETH # # Usage: # ./scripts/deployment/check-balances-gas-and-deploy.sh # report only # ./scripts/deployment/check-balances-gas-and-deploy.sh --deploy # deploy on Chain 138 (requires PRIVATE_KEY) # # Uses .env for RPCs and PRIVATE_KEY; --deploy is a tag (not .env). set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" cd "$PROJECT_ROOT" source "$SCRIPT_DIR/../lib/deployment/dotenv.sh" source "$SCRIPT_DIR/../lib/deployment/prompts.sh" load_deployment_env parse_deploy_tag "$@" DO_DEPLOY="${DO_DEPLOY:-0}" # Infura: use Basic Auth URL when INFURA_PROJECT_SECRET is set (fixes "error sending request" from Infura) SCRIPT_LIB="$SCRIPT_DIR/../lib" [ -f "${SCRIPT_LIB}/infura.sh" ] && source "${SCRIPT_LIB}/infura.sh" # Deployer address: use DEPLOYER_ADDRESS if set (matches MetaMask Portfolio), else derive from PRIVATE_KEY if [ -n "${DEPLOYER_ADDRESS:-}" ]; then DEPLOYER="${DEPLOYER_ADDRESS}" [[ "$DEPLOYER" != 0x* ]] && DEPLOYER="0x$DEPLOYER" else if [ -z "${PRIVATE_KEY:-}" ]; then echo "ERROR: Set PRIVATE_KEY in .env or set DEPLOYER_ADDRESS=0x4A666F96fC8764181194447A7dFdb7d471b301C8 for read-only balance check" exit 1 fi DEPLOYER=$(cast wallet address "$PRIVATE_KEY" 2>/dev/null || true) if [ -z "$DEPLOYER" ]; then echo "ERROR: Could not derive deployer address from PRIVATE_KEY (is 'cast' available?)" exit 1 fi fi # Gas estimate for "full" deploy per chain (conservative) GAS_FULL_DEPLOY_138="${GAS_FULL_DEPLOY_138:-5000000}" GAS_FULL_DEPLOY_MAINNET="${GAS_FULL_DEPLOY_MAINNET:-5000000}" BUFFER_BPS=120 # 20% buffer RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # Short form for matching MetaMask Portfolio (0x4A66...b301C8) DEPLOYER_SHORT="${DEPLOYER:0:6}...${DEPLOYER: -6}" echo -e "${BLUE}============================================${NC}" echo -e "${BLUE}Deployer balance & gas check (all networks)${NC}" echo -e "${BLUE}============================================${NC}" echo "Deployer: $DEPLOYER_SHORT (full: $DEPLOYER)" echo " (Matches MetaMask Portfolio when RPCs are reachable; use DEPLOYER_ADDRESS=0x... to check a specific wallet)" echo "" # Format wei to ether with 8 decimal places (avoids excessive decimals from cast) format_wei_to_ether() { local wei="${1:-0}" local eth eth=$(echo "scale=10; $wei / 1000000000000000000" | bc 2>/dev/null) || eth="0" printf "%.8f" "${eth:-0}" } # Infura Gas API: supported chain IDs (EIP-1559 style; API returns suggestedMaxFeePerGas in gwei) INFURA_GAS_API_CHAINS="1 137 10 8453 42161 11155111 84532 11155420 80002 43114 56 100 324 534352" # Get gas price in wei from Infura Gas API for a given chain_id (optional). # Returns wei or empty; caller should fallback to cast gas-price. get_gas_price_wei_infura_api() { local chain_id="${1:-1}" local key="" if [ -n "${INFURA_GAS_API:-}" ]; then local url="$INFURA_GAS_API" if [[ "$url" == *"/v3/"* ]]; then key="${url#*v3/}" key="${key%%/*}" else key="$url" fi elif [ -n "${INFURA_PROJECT_ID:-}" ]; then key="${INFURA_PROJECT_ID}" fi if [ -z "$key" ] || ! command -v curl >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then return 1 fi local api_url="https://gas.api.infura.io/v3/${key}/networks/${chain_id}/suggestedGasFees" local res res=$(curl -sL --connect-timeout 5 "$api_url" 2>/dev/null || true) if [ -z "$res" ] || echo "$res" | grep -qi "error\|required\|private key\|not found"; then return 1 fi local gwei gwei=$(echo "$res" | jq -r '.medium.suggestedMaxFeePerGas // .standard.suggestedMaxFeePerGas // .standard.maxFeePerGas // .low.suggestedMaxFeePerGas // empty' 2>/dev/null) if [ -z "$gwei" ] || [ "$gwei" = "null" ]; then return 1 fi local wei wei=$(echo "scale=0; ($gwei) * 1000000000 / 1" | bc 2>/dev/null) [ -n "$wei" ] && [ "$wei" != "0" ] && echo "$wei" } # Get mainnet gas price from Infura Gas API (preferred) or RPC fallback. get_mainnet_gas_price_wei() { local wei wei=$(get_gas_price_wei_infura_api 1) if [ -z "$wei" ] || [ "$wei" = "0" ]; then [ -n "${ETHEREUM_MAINNET_RPC:-}" ] && command -v cast >/dev/null 2>&1 && wei=$(cast gas-price --rpc-url "$ETHEREUM_MAINNET_RPC" 2>/dev/null || true) fi [ -n "$wei" ] && [ "$wei" != "0" ] && echo "$wei" || echo "30000000000" } # Resolve gas price for a chain: try Infura Gas API first (if chain supported), else RPC, else default_wei. # Usage: get_gas_price_for_chain chain_id rpc_url default_wei # Output: wei (and sets GAS_SOURCE="Infura Gas API" or "RPC" or "default") get_gas_price_for_chain() { local chain_id="$1" local rpc_url="$2" local default_wei="${3:-30000000000}" local wei="" [ -n "$rpc_url" ] && type ensure_infura_rpc_url &>/dev/null && rpc_url=$(ensure_infura_rpc_url "$rpc_url") if [[ " $INFURA_GAS_API_CHAINS " == *" $chain_id "* ]]; then wei=$(get_gas_price_wei_infura_api "$chain_id") [ -n "$wei" ] && GAS_SOURCE="Infura Gas API" && echo "$wei" && return fi if [ -n "$rpc_url" ] && command -v cast >/dev/null 2>&1; then wei=$(cast gas-price --rpc-url "$rpc_url" 2>/dev/null || true) [ -n "$wei" ] && [ "$wei" != "0" ] && GAS_SOURCE="RPC" && echo "$wei" && return fi GAS_SOURCE="default" echo "$default_wei" } mainnet_gas_wei=$(get_mainnet_gas_price_wei) mainnet_gas_gwei=$(printf "%.2f" $(echo "scale=4; $mainnet_gas_wei / 1000000000" | bc 2>/dev/null || echo "30")) echo "Gas prices (verified via Infura Gas API where supported, else RPC or default):" echo " Ethereum Mainnet (1): ${mainnet_gas_gwei} gwei (Infura Gas API)" echo "" # Check native (gas) token balance for one network. Only this token can be used for gas on EVM. check_network() { local name="$1" local rpc="$2" local chain_id="${3:-}" local gas_token="${4:-ETH}" local gas_limit="${5:-$GAS_FULL_DEPLOY_MAINNET}" local gas_price_wei="${6:-$mainnet_gas_wei}" if [ -z "$rpc" ]; then echo -e " ${YELLOW}$name (chain $chain_id): no RPC — gas token = $gas_token (not checked)${NC}" return 0 fi type ensure_infura_rpc_url &>/dev/null && rpc=$(ensure_infura_rpc_url "$rpc") local balance_wei balance_wei=$(cast balance "$DEPLOYER" --rpc-url "$rpc" 2>/dev/null) || balance_wei="" local rpc_ok=1 if [ -z "$balance_wei" ] || ! [[ "$balance_wei" =~ ^[0-9]+$ ]]; then rpc_ok=0 balance_wei="0" fi local balance_display if [ "$rpc_ok" = "0" ]; then balance_display="(unable to fetch — RPC unreachable?)" else balance_display=$(format_wei_to_ether "$balance_wei") fi local cost_wei cost_wei=$(echo "$gas_limit * $gas_price_wei" | bc 2>/dev/null || echo "0") local cost_with_buffer cost_with_buffer=$(echo "$cost_wei * $BUFFER_BPS / 100" | bc 2>/dev/null || echo "$cost_wei") local cost_display cost_display=$(format_wei_to_ether "$cost_with_buffer") local ok=0 [ "$rpc_ok" = "1" ] && [ "$(echo "$balance_wei >= $cost_with_buffer" | bc 2>/dev/null)" = "1" ] && ok=1 if [ "$ok" = "1" ]; then echo -e " ${GREEN}$name (chain $chain_id): gas token $gas_token — balance ${balance_display}, required ~${cost_display} — OK for deploy${NC}" elif [ "$rpc_ok" = "0" ]; then echo -e " ${YELLOW}$name (chain $chain_id): gas token $gas_token — balance ${balance_display}, required ~${cost_display} — check RPC / run from network with access${NC}" else echo -e " ${RED}$name (chain $chain_id): gas token $gas_token — balance ${balance_display}, required ~${cost_display} — INSUFFICIENT for deploy${NC}" fi return $((1 - ok)) } # All networks: order matches MetaMask Portfolio (Ethereum, BSC, Arbitrum, Optimism, Polygon, Cronos, …) # Gas needed from Infura Gas API or RPC. Native gas token only (ETH, BNB, POL, CRO, etc.). echo "Checking gas token balance (same order as MetaMask Portfolio where applicable):" echo "" any_insufficient=0 # --- Portfolio main networks (Ethereum, BSC, Arbitrum, Optimism, Polygon, Cronos) --- check_network "Ethereum Mainnet" "${ETHEREUM_MAINNET_RPC:-}" "1" "ETH" "$GAS_FULL_DEPLOY_MAINNET" "$mainnet_gas_wei" || any_insufficient=1 check_network "BSC" "${BSC_RPC_URL:-${BSC_MAINNET_RPC:-https://bsc-dataseed.binance.org}}" "56" "BNB" "5000000" "$(get_gas_price_for_chain 56 "${BSC_RPC_URL:-${BSC_MAINNET_RPC:-}}" "5000000000")" || true check_network "Arbitrum" "${ARBITRUM_MAINNET_RPC:-https://arbitrum-one.publicnode.com}" "42161" "ETH" "5000000" "$(get_gas_price_for_chain 42161 "${ARBITRUM_MAINNET_RPC:-}" "100000000")" || true check_network "Optimism" "${OPTIMISM_MAINNET_RPC:-}" "10" "ETH" "5000000" "$(get_gas_price_for_chain 10 "${OPTIMISM_MAINNET_RPC:-}" "1000000")" || true check_network "Polygon" "${POLYGON_MAINNET_RPC:-}" "137" "POL" "5000000" "$(get_gas_price_for_chain 137 "${POLYGON_MAINNET_RPC:-}" "50000000000")" || any_insufficient=1 check_network "Cronos" "${CRONOS_RPC_URL:-https://evm.cronos.org}" "25" "CRO" "5000000" "$(get_gas_price_for_chain 25 "${CRONOS_RPC_URL:-}" "1000000000")" || true # --- Chain 138 (project chain) --- check_network "Chain 138" "${RPC_URL_138:-}" "138" "ETH" "$GAS_FULL_DEPLOY_138" "$(cast gas-price --rpc-url "${RPC_URL_138:-http://192.168.11.211:8545}" 2>/dev/null || echo "1000000000")" || any_insufficient=1 # --- Other mainnets --- check_network "Base" "${BASE_MAINNET_RPC:-}" "8453" "ETH" "5000000" "$(get_gas_price_for_chain 8453 "${BASE_MAINNET_RPC:-}" "100000000")" || true check_network "Avalanche" "${AVALANCHE_RPC_URL:-${AVALANCHE_MAINNET_RPC:-https://avalanche-c-chain.publicnode.com}}" "43114" "AVAX" "5000000" "$(get_gas_price_for_chain 43114 "${AVALANCHE_RPC_URL:-${AVALANCHE_MAINNET_RPC:-}}" "25000000000")" || true check_network "Gnosis" "${GNOSIS_MAINNET_RPC:-${GNOSIS_RPC:-https://rpc.gnosischain.com}}" "100" "xDAI" "5000000" "$(get_gas_price_for_chain 100 "${GNOSIS_MAINNET_RPC:-${GNOSIS_RPC:-}}" "1000000000")" || true # --- Testnets --- check_network "Ethereum Sepolia" "${ETHEREUM_SEPOLIA_RPC:-}" "11155111" "ETH" "5000000" "$(get_gas_price_for_chain 11155111 "${ETHEREUM_SEPOLIA_RPC:-}" "20000000000")" || true check_network "Polygon Amoy" "${POLYGON_AMOY_RPC:-}" "80002" "MATIC" "5000000" "$(get_gas_price_for_chain 80002 "${POLYGON_AMOY_RPC:-}" "30000000000")" || true check_network "Base Sepolia" "${BASE_SEPOLIA_RPC:-}" "84532" "ETH" "5000000" "$(get_gas_price_for_chain 84532 "${BASE_SEPOLIA_RPC:-}" "100000000")" || true check_network "Optimism Sepolia" "${OPTIMISM_SEPOLIA_RPC:-}" "11155420" "ETH" "5000000" "$(get_gas_price_for_chain 11155420 "${OPTIMISM_SEPOLIA_RPC:-}" "1000000")" || true echo "" echo "Gas token summary: each chain uses only its native token for gas. ERC-20 (USDT, LINK, etc.) cannot pay gas." echo "Gas needed above was computed from: Infura Gas API (chains 1, 10, 56, 100, 137, 42161, 43114, 8453, 84532, 80002, 11155111, 11155420) or RPC eth_gasPrice when API unavailable." echo "" if [ "${DO_DEPLOY:-0}" = "1" ]; then if [ -n "${DEPLOYER_ADDRESS:-}" ]; then echo -e "${RED}ERROR: --deploy requires PRIVATE_KEY in .env (unset DEPLOYER_ADDRESS to use key-derived deployer).${NC}" exit 1 fi if [ -z "${PRIVATE_KEY:-}" ]; then echo -e "${RED}ERROR: PRIVATE_KEY required for --deploy.${NC}" exit 1 fi echo -e "${BLUE}Deploy phase (Chain 138 only; other chains need CCIP_* env)${NC}" RPC_138="${RPC_URL_138:-http://192.168.11.211:8545}" balance_138=$(cast balance "$DEPLOYER" --rpc-url "$RPC_138" 2>/dev/null || echo "0") need_138=$(echo "5000000 * 1000000000 * $BUFFER_BPS / 100" | bc 2>/dev/null || echo "6000000000000000000") if [ -n "$balance_138" ] && [ "$(echo "$balance_138 >= $need_138" | bc 2>/dev/null)" = "1" ]; then echo "Deploying phased core (01_DeployCore, 02_DeployBridges) on Chain 138..." export PRIVATE_KEY out_01=$(mktemp) trap "rm -f $out_01" EXIT # Chain 138 often requires minimum gas price (e.g. 1 gwei) GAS_PRICE_138="${GAS_PRICE_138:-1000000000}" if forge script script/deploy/01_DeployCore.s.sol:DeployCore \ --rpc-url "$RPC_138" \ --private-key "$PRIVATE_KEY" \ --broadcast \ --with-gas-price "$GAS_PRICE_138" \ -vvv 2>&1 | tee "$out_01"; then reg=$(grep "UNIVERSAL_ASSET_REGISTRY" "$out_01" | tail -1 | grep -oE "0x[a-fA-F0-9]{40}" | head -1) if [ -n "$reg" ] && [ -n "${CCIP_ROUTER:-}" ]; then export UNIVERSAL_ASSET_REGISTRY="$reg" forge script script/deploy/02_DeployBridges.s.sol:DeployBridges \ --rpc-url "$RPC_138" \ --private-key "$PRIVATE_KEY" \ --broadcast \ --with-gas-price "$GAS_PRICE_138" \ -vvv 2>&1 || echo "02_DeployBridges failed or skipped." fi else echo "01_DeployCore failed or skipped (may already be deployed)." fi echo -e "${GREEN}Chain 138 deploy phase finished.${NC}" else echo -e "${RED}Chain 138 balance insufficient for deploy (need more native ETH on 138). Fund deployer and re-run.${NC}" exit 1 fi else echo "Run with --deploy to run deployment on Chain 138 (phased core)." echo "Other chains: fund deployer with that chain's native token (MATIC, BNB, etc.) and set CCIP_* in .env for DeployAll." fi