From 6fb6bd3993867dd558d2b80557918856f6869282 Mon Sep 17 00:00:00 2001 From: defiQUG Date: Sun, 12 Apr 2026 03:43:35 -0700 Subject: [PATCH] fix(pmm-soak): shellcheck-clean bots/tick/bootstrap; expand CI ShellCheck - bootstrap: split mnemonic export (SC2155), PMM_SOAK_RPC_URL_OVERRIDE, noprofile fund-grid, ASCII echoes, help header only - chain138-tick: document POOLS global for SC2153 - random/grid bots: log line references INTEGRATION, USD bounds, gold, gas, log_tag (SC2034) - validate-config: ShellCheck all 11 PMM soak + chain138-pmm shell scripts incl. smoke + bootstrap Made-with: Cursor --- .github/workflows/validate-config.yml | 21 +- .../chain138-pmm-random-soak-swaps.sh | 153 +++++++++++ .../deployment/chain138-pmm-soak-grid-bot.sh | 253 ++++++++++++++++++ .../pmm-soak-complete-operator-bootstrap.sh | 132 +++++++++ scripts/lib/pmm-soak-chain138-tick.sh | 208 ++++++++++++++ 5 files changed, 760 insertions(+), 7 deletions(-) create mode 100755 scripts/deployment/chain138-pmm-random-soak-swaps.sh create mode 100755 scripts/deployment/chain138-pmm-soak-grid-bot.sh create mode 100755 scripts/deployment/pmm-soak-complete-operator-bootstrap.sh create mode 100755 scripts/lib/pmm-soak-chain138-tick.sh diff --git a/.github/workflows/validate-config.yml b/.github/workflows/validate-config.yml index a3ca592a..452295dc 100644 --- a/.github/workflows/validate-config.yml +++ b/.github/workflows/validate-config.yml @@ -117,18 +117,25 @@ jobs: bash explorer-monorepo/scripts/generate-topology-graph.sh git diff --exit-code explorer-monorepo/frontend/public/config/topology-graph.json - - name: ShellCheck (PMM soak grid operator scripts) + - name: ShellCheck (PMM soak grid scripts) run: | sudo apt-get update -qq sudo apt-get install -y shellcheck set -euo pipefail - shellcheck -x \ - scripts/deployment/pmm-soak-operator-fund-grid.sh \ - scripts/deployment/pmm-soak-complete-grid-funding-operator.sh \ - scripts/deployment/pmm-soak-operator-fund-full-grid-tranches.sh \ - scripts/deployment/pmm-soak-mint-mirror-usdc-deployer-shortfall.sh \ - scripts/lib/pmm-soak-dotenv-override.sh \ + shells=( + scripts/deployment/pmm-soak-operator-fund-grid.sh + scripts/deployment/pmm-soak-complete-grid-funding-operator.sh + scripts/deployment/pmm-soak-operator-fund-full-grid-tranches.sh + scripts/deployment/pmm-soak-mint-mirror-usdc-deployer-shortfall.sh + scripts/deployment/pmm-soak-complete-operator-bootstrap.sh + scripts/deployment/pmm-soak-grid-smoke-check.sh + scripts/lib/pmm-soak-dotenv-override.sh scripts/lib/pmm-soak-pools.sh + scripts/lib/pmm-soak-chain138-tick.sh + scripts/deployment/chain138-pmm-random-soak-swaps.sh + scripts/deployment/chain138-pmm-soak-grid-bot.sh + ) + shellcheck -x "${shells[@]}" - name: Shellcheck (optional) run: bash scripts/verify/run-shellcheck.sh --optional diff --git a/scripts/deployment/chain138-pmm-random-soak-swaps.sh b/scripts/deployment/chain138-pmm-random-soak-swaps.sh new file mode 100755 index 00000000..9aacabea --- /dev/null +++ b/scripts/deployment/chain138-pmm-random-soak-swaps.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +# Chain 138 — continuous random PMM swaps across funded pools (single deployer wallet). +# +# Each interval (default 6s): pick a random live pool, random direction (base/quote in), +# random notional between USD_MIN and USD_MAX (for USD-pegged stables), then execute swap +# (default CHAIN138_PMM_SOAK_SWAP_VIA=pool: transfer + sellBase/sellQuote; optional integration). +# +# For 33x33x6 grid wallets + operator funding, use chain138-pmm-soak-grid-bot.sh +# +# Safety: default is dry-run (no chain writes). Live swaps require: --apply and PRIVATE_KEY. +# +# Usage: +# bash scripts/deployment/chain138-pmm-random-soak-swaps.sh --dry-run --max-ticks 5 +# PMM_SOAK_INTERVAL_SEC=6 bash scripts/deployment/chain138-pmm-random-soak-swaps.sh --apply +# +# Env: (see scripts/lib/pmm-soak-chain138-tick.sh) + PMM_SOAK_MAX_ITER / --max-ticks +# Pools: PMM_SOAK_POOLS | PMM_SOAK_POOLS_FILE | PMM_SOAK_POOL_PRESET (see scripts/lib/pmm-soak-pools.sh) +# CLI: --pool-preset | --swap-via pool|integration +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +APPLY=0 +CLI_MAX_ITER="" +CLI_POOL_PRESET="" +CLI_SWAP_VIA="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --apply) APPLY=1 ;; + --dry-run) APPLY=0 ;; + --max-ticks) + CLI_MAX_ITER="$2" + shift + ;; + --pool-preset) + CLI_POOL_PRESET="$2" + shift + ;; + --swap-via) + CLI_SWAP_VIA="$2" + shift + ;; + -h | --help) + echo "chain138-pmm-random-soak-swaps.sh — single-wallet PMM soak (deployer)" + echo "" + echo "Usage: $0 [--dry-run|--apply] [--max-ticks N] [--pool-preset NAME] [--swap-via pool|integration]" + exit 0 + ;; + *) + echo "[pmm-soak] unknown arg: $1" >&2 + exit 2 + ;; + esac + shift +done + +# shellcheck source=/dev/null +source "${PROJECT_ROOT}/scripts/lib/pmm-soak-dotenv-override.sh" +pmm_soak_snapshot_pool_env_for_restore +# shellcheck source=/dev/null +[[ -f "${PROJECT_ROOT}/scripts/lib/load-project-env.sh" ]] && source "${PROJECT_ROOT}/scripts/lib/load-project-env.sh" +pmm_soak_restore_pool_env_after_dotenv + +if [[ -n "$CLI_SWAP_VIA" ]]; then + export CHAIN138_PMM_SOAK_SWAP_VIA="$CLI_SWAP_VIA" +fi +if [[ -n "$CLI_POOL_PRESET" ]]; then + export PMM_SOAK_POOL_PRESET="$CLI_POOL_PRESET" + unset PMM_SOAK_POOLS PMM_SOAK_POOLS_FILE 2>/dev/null || true +fi + +# shellcheck source=/dev/null +source "${PROJECT_ROOT}/scripts/lib/pmm-soak-chain138-tick.sh" +# shellcheck source=/dev/null +source "${PROJECT_ROOT}/scripts/lib/pmm-soak-pools.sh" + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { + echo "[pmm-soak] missing: $1" >&2 + exit 1 + } +} + +require_cmd cast +require_cmd python3 +require_cmd bc + +PK="${DEPLOYER_PRIVATE_KEY:-${PRIVATE_KEY:-}}" +RPC="${RPC_URL_138:-http://192.168.11.211:8545}" +INTEGRATION="${DODO_PMM_INTEGRATION_ADDRESS:-${CHAIN_138_DODO_PMM_INTEGRATION:-0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895}}" +INTERVAL="${PMM_SOAK_INTERVAL_SEC:-6}" +USD_MIN="${PMM_SOAK_USD_MIN:-10}" +USD_MAX="${PMM_SOAK_USD_MAX:-5000}" +SLIP_BPS="${PMM_SOAK_SLIPPAGE_BPS:-100}" +GOLD_USD="${GOLD_USD_PRICE:-3300}" +GAS_WEI="${CHAIN138_DEPLOY_GAS_PRICE_WEI:-1000}" +SOAK_MAX_ITER="${CLI_MAX_ITER:-${PMM_SOAK_MAX_ITER:-}}" + +pmm_soak_load_pools || exit 1 + +PMM_SOAK_LOG_TAG="pmm-soak" +log() { pmm_soak_log "$@"; } + +deployer_addr() { + if [[ -z "$PK" ]]; then + echo "" + return + fi + cast wallet address --private-key "$PK" 2>/dev/null || true +} + +DEPLOYER="$(deployer_addr)" + +if [[ "$APPLY" -eq 1 ]]; then + if [[ -z "$PK" ]]; then + log "FATAL: --apply requires PRIVATE_KEY or DEPLOYER_PRIVATE_KEY" + exit 1 + fi + if [[ -z "$DEPLOYER" ]]; then + log "FATAL: could not derive address from private key" + exit 1 + fi +else + DEPLOYER="${DEPLOYER_ADDRESS:-0x4A666F96fC8764181194447A7dFdb7d471b301C8}" + log "Dry-run mode (no txs). Use --apply for live swaps." +fi + +LOCK="/tmp/chain138-pmm-random-soak.${USER}.lock" +if [[ "$APPLY" -eq 1 ]]; then + if ! mkdir "$LOCK" 2>/dev/null; then + log "FATAL: lock exists $LOCK — another soak bot may be running" + exit 1 + fi + trap 'rmdir "$LOCK" 2>/dev/null || true' EXIT +fi + +SWAP_VIA_LOG="${CHAIN138_PMM_SOAK_SWAP_VIA:-pool}" +log "RPC=$RPC integration=$INTEGRATION swap_via=$SWAP_VIA_LOG interval=${INTERVAL}s deployer=$DEPLOYER pools=${#POOLS[@]} USD=${USD_MIN}-${USD_MAX} slip_bps=$SLIP_BPS gold_usd=$GOLD_USD gas_wei=$GAS_WEI log_tag=$PMM_SOAK_LOG_TAG apply=$APPLY" + +tick=0 +while true; do + tick=$((tick + 1)) + pmm_soak_chain138_run_tick_iteration "$tick" "$DEPLOYER" "$PK" "$APPLY" + + if [[ -n "$SOAK_MAX_ITER" && "$tick" -ge "$SOAK_MAX_ITER" ]]; then + log "SOAK_MAX_ITER=$SOAK_MAX_ITER done" + break + fi + sleep "$INTERVAL" +done diff --git a/scripts/deployment/chain138-pmm-soak-grid-bot.sh b/scripts/deployment/chain138-pmm-soak-grid-bot.sh new file mode 100755 index 00000000..d6883a85 --- /dev/null +++ b/scripts/deployment/chain138-pmm-soak-grid-bot.sh @@ -0,0 +1,253 @@ +#!/usr/bin/env bash +# Chain 138 PMM soak — 33x33x6 grid wallets (6534) rotate swaps; deployer is operator (fund + tune via env). +# +# Wallets: BIP44 path m/44'/60'/0'/0/{linearIndex} for linearIndex = lpbca*198 + branch*6 + class +# (lpbca, branch in 0..32; class in 0..5). +# +# Optional inter-wallet ERC-20 transfers: set PMM_SOAK_TRANSFER_PROBABILITY and PMM_SOAK_TRANSFER_TOKEN +# +# Usage: +# PMM_SOAK_GRID_MNEMONIC='twelve words...' bash scripts/deployment/chain138-pmm-soak-grid-bot.sh --dry-run --max-ticks 5 +# PMM_SOAK_GRID_JSON=config/pmm-soak-wallet-grid.json bash scripts/deployment/chain138-pmm-soak-grid-bot.sh --dry-run --max-ticks 5 +# ... --apply +# +# Export addresses (no keys in file): pmm-soak-export-wallet-grid.py +# Fund from operator: pmm-soak-operator-fund-grid.sh +# +# Env (tuning): +# PMM_SOAK_GRID_MNEMONIC — required for --apply; for address lookup without exposing mnemonic in process use PMM_SOAK_GRID_JSON +# PMM_SOAK_GRID_JSON — optional exported manifest (faster address resolve) +# PMM_SOAK_INTERVAL_SEC — default 6 +# PMM_SOAK_TRANSFER_PROBABILITY — 0..1, default 0 +# PMM_SOAK_TRANSFER_TOKEN / PMM_SOAK_TRANSFER_USD_MIN / PMM_SOAK_TRANSFER_USD_MAX +# PMM_SOAK_LINEAR_MIN / PMM_SOAK_LINEAR_MAX — default 0..6533 +# PMM_SOAK_MAX_ITER / --max-ticks +# PMM_SOAK_POOLS | PMM_SOAK_POOLS_FILE | PMM_SOAK_POOL_PRESET (scripts/lib/pmm-soak-pools.sh) +# CHAIN138_PMM_SOAK_SWAP_VIA — pool (default) | integration (see scripts/lib/pmm-soak-chain138-tick.sh) +# Caller exports for pool/swap-via override values from root .env after load-project-env (see pmm-soak-dotenv-override.sh). +# CLI: --pool-preset | --swap-via pool|integration +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +APPLY=0 +CLI_MAX_ITER="" +CLI_POOL_PRESET="" +CLI_SWAP_VIA="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --apply) APPLY=1 ;; + --dry-run) APPLY=0 ;; + --max-ticks) + CLI_MAX_ITER="$2" + shift + ;; + --pool-preset) + CLI_POOL_PRESET="$2" + shift + ;; + --swap-via) + CLI_SWAP_VIA="$2" + shift + ;; + -h | --help) + echo "chain138-pmm-soak-grid-bot.sh — PMM soak across 6,534 grid wallets" + echo "" + echo "Usage: $0 [--dry-run|--apply] [--max-ticks N] [--pool-preset NAME] [--swap-via pool|integration]" + echo "Env: PMM_SOAK_GRID_MNEMONIC, PMM_SOAK_GRID_JSON, PMM_SOAK_LINEAR_MIN/MAX, PMM_SOAK_POOL_PRESET, ..." + exit 0 + ;; + *) + echo "[pmm-grid] unknown arg: $1" >&2 + exit 2 + ;; + esac + shift +done + +# shellcheck source=/dev/null +source "${PROJECT_ROOT}/scripts/lib/pmm-soak-dotenv-override.sh" +pmm_soak_snapshot_pool_env_for_restore +# shellcheck source=/dev/null +[[ -f "${PROJECT_ROOT}/scripts/lib/load-project-env.sh" ]] && source "${PROJECT_ROOT}/scripts/lib/load-project-env.sh" +pmm_soak_restore_pool_env_after_dotenv + +if [[ -n "$CLI_SWAP_VIA" ]]; then + export CHAIN138_PMM_SOAK_SWAP_VIA="$CLI_SWAP_VIA" +fi +if [[ -n "$CLI_POOL_PRESET" ]]; then + export PMM_SOAK_POOL_PRESET="$CLI_POOL_PRESET" + unset PMM_SOAK_POOLS PMM_SOAK_POOLS_FILE 2>/dev/null || true +fi + +# shellcheck source=/dev/null +source "${PROJECT_ROOT}/scripts/lib/pmm-soak-chain138-tick.sh" +# shellcheck source=/dev/null +source "${PROJECT_ROOT}/scripts/lib/pmm-soak-pools.sh" + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { + echo "[pmm-grid] missing: $1" >&2 + exit 1 + } +} + +require_cmd cast +require_cmd python3 +require_cmd bc + +MN="${PMM_SOAK_GRID_MNEMONIC:-}" +RPC="${RPC_URL_138:-http://192.168.11.211:8545}" +INTEGRATION="${DODO_PMM_INTEGRATION_ADDRESS:-${CHAIN_138_DODO_PMM_INTEGRATION:-0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895}}" +INTERVAL="${PMM_SOAK_INTERVAL_SEC:-6}" +# For 6-decimal stables, amount = random(usd_min..usd_max) * 10^6 (face value in token units, not wei of ETH). +USD_MIN="${PMM_SOAK_USD_MIN:-10}" +USD_MAX="${PMM_SOAK_USD_MAX:-5000}" +SLIP_BPS="${PMM_SOAK_SLIPPAGE_BPS:-100}" +GOLD_USD="${GOLD_USD_PRICE:-3300}" +GAS_WEI="${CHAIN138_DEPLOY_GAS_PRICE_WEI:-1000}" +SOAK_MAX_ITER="${CLI_MAX_ITER:-${PMM_SOAK_MAX_ITER:-}}" +LINEAR_MIN="${PMM_SOAK_LINEAR_MIN:-0}" +LINEAR_MAX="${PMM_SOAK_LINEAR_MAX:-6533}" +TRANSFER_P="${PMM_SOAK_TRANSFER_PROBABILITY:-0}" +TRANSFER_TOKEN="${PMM_SOAK_TRANSFER_TOKEN:-}" +TRANSFER_USD_MIN="${PMM_SOAK_TRANSFER_USD_MIN:-100}" +TRANSFER_USD_MAX="${PMM_SOAK_TRANSFER_USD_MAX:-10000}" +GRID_JSON="${PMM_SOAK_GRID_JSON:-}" + +PMM_SOAK_LOG_TAG="pmm-grid" +log() { pmm_soak_log "$@"; } + +pmm_soak_load_pools || exit 1 + +grid_address_for_linear() { + local li="$1" + local path="m/44'/60'/0'/0/${li}" + if [[ -n "$GRID_JSON" && -f "$GRID_JSON" ]]; then + python3 - "$GRID_JSON" "$li" <<'PY' || return 1 +import json, sys +path, li = sys.argv[1], int(sys.argv[2]) +with open(path, encoding="utf-8") as f: + doc = json.load(f) +for w in doc["wallets"]: + if int(w["linearIndex"]) == li: + print(w["address"]) + raise SystemExit(0) +raise SystemExit(1) +PY + return 0 + fi + if [[ -z "$MN" ]]; then + return 1 + fi + cast wallet address --mnemonic "$MN" --mnemonic-derivation-path "$path" 2>/dev/null +} + +grid_private_key_for_linear() { + local li="$1" + local path="m/44'/60'/0'/0/${li}" + [[ -n "$MN" ]] || return 1 + cast wallet private-key --mnemonic "$MN" --mnemonic-derivation-path "$path" 2>/dev/null +} + +pick_linear_index() { + local span=$((LINEAR_MAX - LINEAR_MIN + 1)) + echo $((LINEAR_MIN + RANDOM % span)) +} + +maybe_inter_wallet_transfer() { + local tick="$1" + [[ -n "$TRANSFER_TOKEN" ]] || return 0 + python3 -c "import random,sys; sys.exit(0 if random.random() < float('$TRANSFER_P') else 1)" || return 0 + + local li_a li_b addr_a addr_b pk_a amt bal_out + li_a="$(pick_linear_index)" + li_b="$(pick_linear_index)" + [[ "$li_a" != "$li_b" ]] || return 0 + + addr_a="$(grid_address_for_linear "$li_a")" || return 0 + addr_b="$(grid_address_for_linear "$li_b")" || return 0 + [[ -n "$addr_a" && -n "$addr_b" ]] || return 0 + + amt="$(python3 -c "import random; print(random.randint(int('$TRANSFER_USD_MIN'), int('$TRANSFER_USD_MAX')) * 10**6)")" + + log "tick $tick transfer prep linear $li_a -> $li_b amt_raw=$amt token=$TRANSFER_TOKEN" + + if [[ "$APPLY" -eq 0 ]]; then + return 0 + fi + + if ! pk_a="$(grid_private_key_for_linear "$li_a")"; then + log "WARN: could not derive sender key" + return 0 + fi + + if ! bal_out="$(cast call "$TRANSFER_TOKEN" 'balanceOf(address)(uint256)' "$addr_a" --rpc-url "$RPC" 2>/dev/null | awk '{print $1}')"; then + return 0 + fi + if (( $(echo "$bal_out < $amt" | bc -l) )); then + log "tick $tick skip transfer (low balance sender $li_a)" + return 0 + fi + + if ! cast send "$TRANSFER_TOKEN" 'transfer(address,uint256)(bool)' "$addr_b" "$amt" \ + --rpc-url "$RPC" --private-key "$pk_a" --legacy --gas-price "$GAS_WEI"; then + log "WARN: transfer failed $li_a -> $li_b" + fi + return 0 +} + +if [[ "$APPLY" -eq 1 ]]; then + if [[ -z "$MN" ]]; then + log "FATAL: --apply requires PMM_SOAK_GRID_MNEMONIC" + exit 1 + fi +else + log "Dry-run (no chain writes). Use --apply for live grid txs." +fi + +LOCK="/tmp/chain138-pmm-soak-grid.${USER}.lock" +if [[ "$APPLY" -eq 1 ]]; then + if ! mkdir "$LOCK" 2>/dev/null; then + log "FATAL: lock $LOCK — another grid bot running?" + exit 1 + fi + trap 'rmdir "$LOCK" 2>/dev/null || true' EXIT +fi + +SWAP_VIA_LOG="${CHAIN138_PMM_SOAK_SWAP_VIA:-pool}" +log "RPC=$RPC integration=$INTEGRATION pools=${#POOLS[@]} swap_via=$SWAP_VIA_LOG linear=[$LINEAR_MIN,$LINEAR_MAX] interval=${INTERVAL}s USD=${USD_MIN}-${USD_MAX} slip_bps=$SLIP_BPS gold_usd=$GOLD_USD gas_wei=$GAS_WEI log_tag=$PMM_SOAK_LOG_TAG transfer_p=$TRANSFER_P apply=$APPLY" + +tick=0 +while true; do + tick=$((tick + 1)) + li="$(pick_linear_index)" + + maybe_inter_wallet_transfer "$tick" || true + + if ! addr="$(grid_address_for_linear "$li")"; then + log "tick $tick skip (no address for linear=$li; set PMM_SOAK_GRID_MNEMONIC or PMM_SOAK_GRID_JSON)" + else + pk="" + if [[ "$APPLY" -eq 1 ]]; then + if ! pk="$(grid_private_key_for_linear "$li")"; then + log "tick $tick skip linear=$li (no private key)" + else + log "tick $tick linear=$li trader=$addr" + pmm_soak_chain138_run_tick_iteration "$tick" "$addr" "$pk" "$APPLY" + fi + else + log "tick $tick linear=$li trader=$addr" + pmm_soak_chain138_run_tick_iteration "$tick" "$addr" "$pk" "$APPLY" + fi + fi + + if [[ -n "${SOAK_MAX_ITER:-}" && "$tick" -ge "$SOAK_MAX_ITER" ]]; then + log "SOAK_MAX_ITER=$SOAK_MAX_ITER done" + break + fi + sleep "$INTERVAL" +done diff --git a/scripts/deployment/pmm-soak-complete-operator-bootstrap.sh b/scripts/deployment/pmm-soak-complete-operator-bootstrap.sh new file mode 100755 index 00000000..bd35b70f --- /dev/null +++ b/scripts/deployment/pmm-soak-complete-operator-bootstrap.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +# One-shot: ensure grid mnemonic on disk, export 6,534 addresses, optionally fund first tranche from deployer. +# +# Usage: +# bash scripts/deployment/pmm-soak-complete-operator-bootstrap.sh +# bash scripts/deployment/pmm-soak-complete-operator-bootstrap.sh --apply-funds --to-linear 19 +# +# Flags: +# --init-mnemonic Create ~/.secure-secrets/chain138-pmm-soak-grid.mnemonic if missing (cast new-mnemonic). +# --apply-funds After export, run native + cUSDT funding (requires PRIVATE_KEY). +# --to-linear N Last linear index for funding tranche (default 19). +# --skip-export Use existing PMM_SOAK_GRID_JSON (default config/pmm-soak-wallet-grid.json). +# +# Env: +# PMM_SOAK_GRID_MNEMONIC_FILE — default ~/.secure-secrets/chain138-pmm-soak-grid.mnemonic +# PMM_SOAK_GRID_JSON — output path (default: $PROJECT_ROOT/config/pmm-soak-wallet-grid.json) +# NATIVE_AMOUNT_WEI — default 20000000000000000 (0.02 native per wallet) +# FUND_TOKEN / AMOUNT_WEI_PER_WALLET — ERC-20 tranche (defaults: cUSDT, 50000000 = 50 USDT face) +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +# shellcheck source=/dev/null +source "${PROJECT_ROOT}/scripts/lib/pmm-soak-dotenv-override.sh" +pmm_soak_snapshot_pool_env_for_restore +# shellcheck source=/dev/null +[[ -f "${PROJECT_ROOT}/scripts/lib/load-project-env.sh" ]] && source "${PROJECT_ROOT}/scripts/lib/load-project-env.sh" +pmm_soak_restore_pool_env_after_dotenv +if [[ -n "${PMM_SOAK_RPC_URL_OVERRIDE:-}" ]]; then + export RPC_URL_138="$PMM_SOAK_RPC_URL_OVERRIDE" + export CHAIN138_RPC_URL="$RPC_URL_138" + export CHAIN138_RPC="$RPC_URL_138" + export ETH_RPC_URL="$RPC_URL_138" +fi + +INIT_MN=0 +APPLY_FUND=0 +SKIP_EXPORT=0 +TO_LINEAR=19 + +while [[ $# -gt 0 ]]; do + case "$1" in + --init-mnemonic) INIT_MN=1 ;; + --apply-funds) APPLY_FUND=1 ;; + --skip-export) SKIP_EXPORT=1 ;; + --to-linear) + TO_LINEAR="$2" + shift + ;; + -h | --help) + sed -n '2,19p' "$0" + exit 0 + ;; + *) + echo "[bootstrap] unknown arg: $1" >&2 + exit 2 + ;; + esac + shift +done + +SECRETS_DIR="${HOME}/.secure-secrets" +MN_FILE="${PMM_SOAK_GRID_MNEMONIC_FILE:-${SECRETS_DIR}/chain138-pmm-soak-grid.mnemonic}" +OUT_JSON="${PMM_SOAK_GRID_JSON:-${PROJECT_ROOT}/config/pmm-soak-wallet-grid.json}" +NATIVE_AMT="${NATIVE_AMOUNT_WEI:-20000000000000000}" +FUND_TOKEN="${FUND_TOKEN:-0x93E66202A11B1772E55407B32B44e5Cd8eda7f22}" +ERC20_AMT="${AMOUNT_WEI_PER_WALLET:-50000000}" + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { + echo "[bootstrap] missing: $1" >&2 + exit 1 + } +} + +require_cmd cast +require_cmd python3 + +mkdir -p "$SECRETS_DIR" + +if [[ ! -f "$MN_FILE" ]]; then + if [[ "$INIT_MN" -eq 1 ]] || [[ "${PMM_SOAK_AUTO_INIT_GRID_MNEMONIC:-}" == "1" ]]; then + echo "[bootstrap] creating grid mnemonic at $MN_FILE (chmod 600)" + cast wallet new-mnemonic 2>&1 | awk 'f{print; exit} /^Phrase:/{f=1}' | tr -d '\r\n' >"$MN_FILE" + chmod 600 "$MN_FILE" + echo "[bootstrap] BACK UP this file; it controls all 6,534 grid addresses." + else + echo "[bootstrap] No mnemonic file at $MN_FILE" >&2 + echo " Option A: PMM_SOAK_GRID_MNEMONIC='twelve words...' $0" >&2 + echo " Option B: $0 --init-mnemonic (creates file; then re-run with export/funds)" >&2 + exit 1 + fi +fi + +if [[ -n "${PMM_SOAK_GRID_MNEMONIC:-}" ]]; then + export PMM_SOAK_GRID_MNEMONIC +elif [[ -f "$MN_FILE" ]]; then + PMM_SOAK_GRID_MNEMONIC="$(tr -d '\r\n' <"$MN_FILE")" + export PMM_SOAK_GRID_MNEMONIC +else + echo "[bootstrap] internal error: no mnemonic" >&2 + exit 1 +fi + +if [[ "$SKIP_EXPORT" -eq 0 ]]; then + echo "[bootstrap] exporting 6,534 addresses to $OUT_JSON (parallel cast; may take a few minutes)..." + python3 "${PROJECT_ROOT}/scripts/deployment/pmm-soak-export-wallet-grid.py" --out "$OUT_JSON" --jobs 8 +else + echo "[bootstrap] skip export; using existing $OUT_JSON" + [[ -f "$OUT_JSON" ]] || { + echo "[bootstrap] missing $OUT_JSON" >&2 + exit 1 + } +fi + +if [[ "$APPLY_FUND" -eq 1 ]]; then + PK="${DEPLOYER_PRIVATE_KEY:-${PRIVATE_KEY:-}}" + if [[ -z "$PK" ]]; then + echo "[bootstrap] --apply-funds needs PRIVATE_KEY / DEPLOYER_PRIVATE_KEY" >&2 + exit 1 + fi + echo "[bootstrap] funding linear 0..$TO_LINEAR native=$NATIVE_AMT wei" + PMM_SOAK_GRID_JSON="$OUT_JSON" NATIVE_AMOUNT_WEI="$NATIVE_AMT" \ + bash --noprofile --norc "${PROJECT_ROOT}/scripts/deployment/pmm-soak-operator-fund-grid.sh" --native --apply --from-linear 0 --to-linear "$TO_LINEAR" + echo "[bootstrap] funding same range ERC-20 token=$FUND_TOKEN amount=$ERC20_AMT" + PMM_SOAK_GRID_JSON="$OUT_JSON" TOKEN="$FUND_TOKEN" AMOUNT_WEI_PER_WALLET="$ERC20_AMT" \ + bash --noprofile --norc "${PROJECT_ROOT}/scripts/deployment/pmm-soak-operator-fund-grid.sh" --apply --from-linear 0 --to-linear "$TO_LINEAR" +fi + +echo "[bootstrap] done. Grid JSON: $OUT_JSON Mnemonic file: $MN_FILE" +echo "[bootstrap] Next: export PMM_SOAK_GRID_MNEMONIC from $MN_FILE, set PMM_SOAK_GRID_JSON=$OUT_JSON, run chain138-pmm-soak-grid-bot.sh --dry-run then --apply (e.g. --pool-preset stable or cusdt-cusdc; default swap path is pool - see docs/11-references/CHAIN138_GRID_6534_WALLET_FUNDING_PLAN.md)." diff --git a/scripts/lib/pmm-soak-chain138-tick.sh b/scripts/lib/pmm-soak-chain138-tick.sh new file mode 100755 index 00000000..a6c905c6 --- /dev/null +++ b/scripts/lib/pmm-soak-chain138-tick.sh @@ -0,0 +1,208 @@ +#!/usr/bin/env bash +# Chain 138 PMM soak — one tick (quote + optional approve/swap). Source after setting: +# POOLS (array), RPC, INTEGRATION, SLIP_BPS, GAS_WEI, GOLD_USD, USD_MIN, USD_MAX +# Optional: PMM_SOAK_LOG_TAG (default pmm-soak) +# +# Optional: CHAIN138_PMM_SOAK_GAS_LIMIT — when set, passed as --gas-limit to approve/swap cast send (helps Besu estimation). +# Optional: CHAIN138_PMM_SOAK_SWAP_VIA — pool (default) | integration +# On Chain 138, DODOPMMIntegration.swapExactIn reverts inside the pool when msg.sender is the integration contract; +# EOA transfer(token, pool, amt) + pool.sellBase/sellQuote(trader) succeeds. Default pool path matches that. +# Requires: cast, bc, python3 (pick_random_amount) +# shellcheck shell=bash + +PMM_SOAK_CXAUC_LC="${PMM_SOAK_CXAUC_LC:-0x290e52a8819a4fbd0714e517225429aa2b70ec6b}" +PMM_SOAK_CXAUT_LC="${PMM_SOAK_CXAUT_LC:-0x94e408e26c6fd8f4ee00b54df19082fda07dc96e}" + +pmm_soak_to_lower() { + printf '%s' "$1" | tr '[:upper:]' '[:lower:]' +} + +pmm_soak_log() { + echo "[${PMM_SOAK_LOG_TAG:-pmm-soak} $(date -Iseconds)] $*" +} + +pmm_soak_pick_random_amount() { + local token_lc="$1" + local decimals="$2" + PMM_SOAK_XAU_ADDRS="${PMM_SOAK_CXAUC_LC},${PMM_SOAK_CXAUT_LC}" \ + GOLD_USD="$GOLD_USD" USD_MIN="$USD_MIN" USD_MAX="$USD_MAX" \ + python3 - "$token_lc" "$decimals" <<'PY' +import os, random, sys +tok = sys.argv[1].lower() +d = int(sys.argv[2]) +usd_min = int(os.environ["USD_MIN"]) +usd_max = int(os.environ["USD_MAX"]) +gold = float(os.environ["GOLD_USD"]) +scale = 10 ** d +xau = {a.strip().lower() for a in os.environ["PMM_SOAK_XAU_ADDRS"].split(",") if a.strip()} + +if tok in xau: + usd = random.randint(usd_min, usd_max) + oz = usd / gold + raw = int(oz * scale) + print(max(raw, 1)) +else: + u = random.randint(usd_min, usd_max) + print(u * scale) +PY +} + +# One loop iteration: random pool, quote as trader, swap with signer_pk when apply=1. +# Globals: POOLS RPC INTEGRATION SLIP_BPS GAS_WEI +pmm_soak_chain138_run_tick_iteration() { + local tick="$1" + local trader="$2" + local signer_pk="$3" + local apply="$4" + + # POOLS[] is set by the caller (pmm-soak-pools.sh); not the local POOL below. + # shellcheck disable=SC2153 + local idx=$((RANDOM % ${#POOLS[@]})) + local POOL base_out quote_out base quote base_lc quote_lc token_in token_in_lc token_out + local dec_out dec bal_out bal amt qraw quoted min_out + local swap_via="${CHAIN138_PMM_SOAK_SWAP_VIA:-pool}" + POOL="${POOLS[$idx]}" + + if ! base_out="$(cast call "$POOL" '_BASE_TOKEN_()(address)' --rpc-url "$RPC" 2>/dev/null)"; then + pmm_soak_log "tick $tick skip pool=$POOL (base token read failed)" + return 0 + fi + if ! quote_out="$(cast call "$POOL" '_QUOTE_TOKEN_()(address)' --rpc-url "$RPC" 2>/dev/null)"; then + pmm_soak_log "tick $tick skip pool=$POOL (quote token read failed)" + return 0 + fi + base="$(printf '%s\n' "$base_out" | awk '{print $1}')" + quote="$(printf '%s\n' "$quote_out" | awk '{print $1}')" + base_lc="$(pmm_soak_to_lower "$base")" + quote_lc="$(pmm_soak_to_lower "$quote")" + + if (( RANDOM % 2 == 0 )); then + token_in="$base" + token_in_lc="$base_lc" + token_out="$quote" + else + token_in="$quote" + token_in_lc="$quote_lc" + token_out="$base" + fi + + if ! dec_out="$(cast call "$token_in" 'decimals()(uint8)' --rpc-url "$RPC" 2>/dev/null)"; then + pmm_soak_log "tick $tick skip pool=$POOL (decimals failed)" + return 0 + fi + dec="$(printf '%s\n' "$dec_out" | awk '{print $1}')" + if ! bal_out="$(cast call "$token_in" 'balanceOf(address)(uint256)' "$trader" --rpc-url "$RPC" 2>/dev/null)"; then + pmm_soak_log "tick $tick skip pool=$POOL (balance read failed)" + return 0 + fi + bal="$(printf '%s\n' "$bal_out" | awk '{print $1}')" + + amt="$(pmm_soak_pick_random_amount "$token_in_lc" "$dec")" + if [[ -z "$amt" || "$amt" == "0" ]]; then + pmm_soak_log "tick $tick skip pool=$POOL (zero amount)" + return 0 + fi + + if (( $(echo "$bal < $amt" | bc -l) )); then + if (( $(echo "$bal == 0" | bc -l) )); then + pmm_soak_log "tick $tick skip pool=$POOL token_in=$token_in zero balance" + return 0 + fi + amt=$((bal * 9 / 10)) + pmm_soak_log "tick $tick clamp amount to 90% balance -> $amt" + fi + + if [[ "$token_in_lc" == "$base_lc" ]]; then + if ! qraw="$(cast call "$POOL" 'querySellBase(address,uint256)(uint256,uint256)' "$trader" "$amt" --rpc-url "$RPC" 2>/dev/null)"; then + pmm_soak_log "tick $tick skip pool=$POOL querySellBase reverted amt=$amt" + return 0 + fi + else + if ! qraw="$(cast call "$POOL" 'querySellQuote(address,uint256)(uint256,uint256)' "$trader" "$amt" --rpc-url "$RPC" 2>/dev/null)"; then + pmm_soak_log "tick $tick skip pool=$POOL querySellQuote reverted amt=$amt" + return 0 + fi + fi + quoted="$(printf '%s\n' "$qraw" | head -1 | awk '{print $1}')" + + if [[ -z "$quoted" || "$quoted" == "0" ]]; then + pmm_soak_log "tick $tick skip pool=$POOL token_in=$token_in amt=$amt (no quote)" + return 0 + fi + + min_out="$(echo "scale=0; (${quoted} * (10000 - ${SLIP_BPS})) / 10000" | bc)" + + pmm_soak_log "tick $tick trader=$trader pool=$POOL token_in=$token_in amt=$amt quoted_out=$quoted min_out=$min_out" + + if [[ "$apply" -eq 1 ]]; then + if [[ -z "$signer_pk" ]]; then + pmm_soak_log "WARN: apply=1 but empty signer_pk; skip swap" + return 0 + fi + local _gas_t _gas_s _out_bal_before _out_bal_after _received + _gas_t=() + _gas_s=() + [[ -n "${CHAIN138_PMM_SOAK_GAS_LIMIT:-}" ]] && _gas_t=(--gas-limit "${CHAIN138_PMM_SOAK_GAS_LIMIT}") + [[ -n "${CHAIN138_PMM_SOAK_GAS_LIMIT:-}" ]] && _gas_s=(--gas-limit "${CHAIN138_PMM_SOAK_GAS_LIMIT}") + + if [[ "$swap_via" == "integration" ]]; then + local allowance al_out can_swap + if ! al_out="$(cast call "$token_in" 'allowance(address,address)(uint256)' "$trader" "$INTEGRATION" --rpc-url "$RPC" 2>/dev/null)"; then + pmm_soak_log "WARN: allowance read failed; skip swap" + else + allowance="$(printf '%s\n' "$al_out" | awk '{print $1}')" + can_swap=1 + if (( $(echo "$allowance < $amt" | bc -l) )); then + pmm_soak_log "approve $token_in -> integration amount=$amt" + if ! cast send "$token_in" 'approve(address,uint256)(bool)' "$INTEGRATION" "$amt" \ + --rpc-url "$RPC" --private-key "$signer_pk" --legacy --gas-price "$GAS_WEI" "${_gas_t[@]}" >/dev/null; then + pmm_soak_log "WARN: approve failed; skip swap" + can_swap=0 + fi + fi + if [[ "$can_swap" -eq 1 ]]; then + if ! cast send "$INTEGRATION" \ + 'swapExactIn(address,address,uint256,uint256)(uint256)' \ + "$POOL" "$token_in" "$amt" "$min_out" \ + --rpc-url "$RPC" --private-key "$signer_pk" --legacy --gas-price "$GAS_WEI" "${_gas_s[@]}"; then + pmm_soak_log "WARN: swap failed pool=$POOL (continuing)" + fi + fi + fi + else + # Direct pool: transfer in then sellBase / sellQuote (EOA msg.sender; avoids integration+pool revert on this chain). + if ! _out_bal_before="$(cast call "$token_out" 'balanceOf(address)(uint256)' "$trader" --rpc-url "$RPC" 2>/dev/null | awk '{print $1}')"; then + pmm_soak_log "WARN: token_out balance read failed; skip swap" + else + pmm_soak_log "swap_via=pool transfer token_in -> pool amt=$amt" + if ! cast send "$token_in" 'transfer(address,uint256)(bool)' "$POOL" "$amt" \ + --rpc-url "$RPC" --private-key "$signer_pk" --legacy --gas-price "$GAS_WEI" "${_gas_t[@]}" >/dev/null; then + pmm_soak_log "WARN: transfer to pool failed pool=$POOL (continuing)" + else + if [[ "$token_in_lc" == "$base_lc" ]]; then + if ! cast send "$POOL" 'sellBase(address)(uint256)' "$trader" \ + --rpc-url "$RPC" --private-key "$signer_pk" --legacy --gas-price "$GAS_WEI" "${_gas_s[@]}" >/dev/null; then + pmm_soak_log "WARN: sellBase failed pool=$POOL (continuing)" + fi + else + if ! cast send "$POOL" 'sellQuote(address)(uint256)' "$trader" \ + --rpc-url "$RPC" --private-key "$signer_pk" --legacy --gas-price "$GAS_WEI" "${_gas_s[@]}" >/dev/null; then + pmm_soak_log "WARN: sellQuote failed pool=$POOL (continuing)" + fi + fi + if ! _out_bal_after="$(cast call "$token_out" 'balanceOf(address)(uint256)' "$trader" --rpc-url "$RPC" 2>/dev/null | awk '{print $1}')"; then + pmm_soak_log "WARN: token_out balance re-read failed after swap" + else + _received="$(echo "${_out_bal_after} - ${_out_bal_before}" | bc)" + if (( $(echo "${_received} < ${min_out}" | bc -l) )); then + pmm_soak_log "WARN: output ${_received} < min_out ${min_out} (slippage / partial fill)" + else + pmm_soak_log "swap_via=pool ok received_out=${_received}" + fi + fi + fi + fi + fi + fi + return 0 +}