#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" STATE_DIR="$ROOT_DIR/tmp/local-env" LOG_DIR="$STATE_DIR/logs" ANVIL_PID_FILE="$STATE_DIR/anvil.pid" PONDER_PID_FILE="$STATE_DIR/ponder.pid" WEBAPP_PID_FILE="$STATE_DIR/webapp.pid" TXNBOT_PID_FILE="$STATE_DIR/txnBot.pid" ANVIL_LOG="$LOG_DIR/anvil.log" PONDER_LOG="$LOG_DIR/ponder.log" WEBAPP_LOG="$LOG_DIR/webapp.log" TXNBOT_LOG="$LOG_DIR/txnBot.log" SETUP_LOG="$LOG_DIR/setup.log" TXNBOT_ENV_FILE="$STATE_DIR/txnBot.env" MNEMONIC_FILE="$ROOT_DIR/onchain/.secret.local" FORK_URL=${FORK_URL:-"https://sepolia.base.org"} ANVIL_RPC="http://127.0.0.1:8545" GRAPHQL_HEALTH="http://127.0.0.1:42069/health" GRAPHQL_ENDPOINT="http://127.0.0.1:42069/graphql" FRONTEND_URL="http://127.0.0.1:5173" FOUNDRY_BIN=${FOUNDRY_BIN:-"$HOME/.foundry/bin"} FORGE="$FOUNDRY_BIN/forge" CAST="$FOUNDRY_BIN/cast" ANVIL="$FOUNDRY_BIN/anvil" DEFAULT_DEPLOYER_PK="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" DEFAULT_DEPLOYER_ADDR="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" DEPLOYER_PK=${DEPLOYER_PK:-$DEFAULT_DEPLOYER_PK} DEPLOYER_ADDR=${DEPLOYER_ADDR:-$DEFAULT_DEPLOYER_ADDR} FEE_DEST="0xf6a3eef9088A255c32b6aD2025f83E57291D9011" WETH="0x4200000000000000000000000000000000000006" SWAP_ROUTER="0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4" DEFAULT_TXNBOT_PRIVATE_KEY="0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" DEFAULT_TXNBOT_ADDRESS="0x70997970C51812dc3A010C7d01b50e0d17dc79C8" TXNBOT_PRIVATE_KEY=${TXNBOT_PRIVATE_KEY:-$DEFAULT_TXNBOT_PRIVATE_KEY} TXNBOT_ADDRESS=${TXNBOT_ADDRESS:-$DEFAULT_TXNBOT_ADDRESS} TXNBOT_FUND_VALUE=${TXNBOT_FUND_VALUE:-1ether} MAX_UINT="0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" SKIP_CLEANUP=false COMMAND="start" cleanup() { stop_process "Frontend" "$WEBAPP_PID_FILE" stop_process "Ponder" "$PONDER_PID_FILE" stop_process "TxnBot" "$TXNBOT_PID_FILE" stop_process "Anvil" "$ANVIL_PID_FILE" if [[ "$SKIP_CLEANUP" == true ]]; then log "Skipping state cleanup (--no-cleanup). State preserved at $STATE_DIR" return fi if [[ -d "$STATE_DIR" ]]; then rm -rf "$STATE_DIR" fi } stop_process() { local name="$1" local pid_file="$2" if [[ -f "$pid_file" ]]; then local pid pid="$(cat "$pid_file")" if kill -0 "$pid" 2>/dev/null; then echo "[local-env] Stopping $name (pid $pid)" kill "$pid" 2>/dev/null || true wait "$pid" 2>/dev/null || true fi rm -f "$pid_file" fi } log() { echo "[local-env] $*" } wait_for_rpc() { local url="$1" for _ in {1..60}; do if curl -s -o /dev/null -X POST "$url" -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}'; then return 0 fi sleep 1 done log "Timed out waiting for RPC at $url" return 1 } wait_for_http() { local url="$1" for _ in {1..60}; do if curl -sSf "$url" >/dev/null 2>&1; then return 0 fi sleep 1 done log "Timed out waiting for $url" return 1 } set_deployer_credentials() { if [[ -n "$DEPLOYER_PK" && -n "$DEPLOYER_ADDR" ]]; then return fi if [[ -f "$MNEMONIC_FILE" ]]; then local mnemonic mnemonic="$(tr -d '\n\r' < "$MNEMONIC_FILE")" if [[ -n "$mnemonic" ]]; then local derived_pk derived_pk="$("$CAST" wallet private-key --mnemonic "$mnemonic" \ --mnemonic-derivation-path "m/44'/60'/0'/0/0")" local derived_addr derived_addr="$("$CAST" wallet address --private-key "$derived_pk")" DEPLOYER_PK=${DEPLOYER_PK:-$derived_pk} DEPLOYER_ADDR=${DEPLOYER_ADDR:-$derived_addr} fi fi DEPLOYER_PK=${DEPLOYER_PK:-$DEFAULT_DEPLOYER_PK} DEPLOYER_ADDR=${DEPLOYER_ADDR:-$DEFAULT_DEPLOYER_ADDR} } ensure_tools() { for cmd in "$ANVIL" "$FORGE" "$CAST" jq npm curl; do if ! command -v "$cmd" >/dev/null 2>&1; then log "Required command not found: $cmd" exit 1 fi done } start_directories() { mkdir -p "$LOG_DIR" } ensure_dependencies() { if [[ ! -d "$ROOT_DIR/services/ponder/node_modules" ]]; then log "Installing ponder dependencies" pushd "$ROOT_DIR/services/ponder" >/dev/null npm install >>"$SETUP_LOG" 2>&1 popd >/dev/null fi if [[ ! -d "$ROOT_DIR/web-app/node_modules" ]]; then log "Installing web-app dependencies" pushd "$ROOT_DIR/web-app" >/dev/null npm install >>"$SETUP_LOG" 2>&1 popd >/dev/null fi if [[ ! -d "$ROOT_DIR/services/txnBot/node_modules" ]]; then log "Installing txnBot dependencies" pushd "$ROOT_DIR/services/txnBot" >/dev/null npm install >>"$SETUP_LOG" 2>&1 popd >/dev/null fi } start_anvil() { if [[ -f "$ANVIL_PID_FILE" ]]; then log "Anvil already running (pid $(cat "$ANVIL_PID_FILE"))" return fi log "Starting Anvil (forking $FORK_URL)" local anvil_args=("--fork-url" "$FORK_URL" "--chain-id" 31337 "--block-time" 1 \ "--host" 127.0.0.1 "--port" 8545) if [[ -f "$MNEMONIC_FILE" ]]; then local mnemonic mnemonic="$(tr -d '\n\r' < "$MNEMONIC_FILE")" if [[ -n "$mnemonic" ]]; then anvil_args+=("--mnemonic" "$mnemonic") fi fi "$ANVIL" "${anvil_args[@]}" >"$ANVIL_LOG" 2>&1 & echo $! >"$ANVIL_PID_FILE" wait_for_rpc "$ANVIL_RPC" } deploy_protocol() { log "Deploying contracts" pushd "$ROOT_DIR/onchain" >/dev/null "$FORGE" script script/DeployLocal.sol --fork-url "$ANVIL_RPC" --broadcast \ >>"$SETUP_LOG" 2>&1 popd >/dev/null } extract_addresses() { local run_file run_file="$(ls -t "$ROOT_DIR/onchain/broadcast/DeployLocal.sol"/*/run-latest.json 2>/dev/null | head -n1)" if [[ -z "$run_file" || ! -f "$run_file" ]]; then log "Deployment artifact not found under onchain/broadcast/DeployLocal.sol" exit 1 fi LIQUIDITY_MANAGER="$(jq -r '.transactions[] | select(.contractName=="LiquidityManager") | .contractAddress' "$run_file" | head -n1)" KRAIKEN="$(jq -r '.transactions[] | select(.contractName=="Kraiken") | .contractAddress' "$run_file" | head -n1)" STAKE="$(jq -r '.transactions[] | select(.contractName=="Stake") | .contractAddress' "$run_file" | head -n1)" log "Using deployment artifact: ${run_file#$ROOT_DIR/}" if [[ -z "$LIQUIDITY_MANAGER" || "$LIQUIDITY_MANAGER" == "null" ]]; then log "Failed to extract LiquidityManager address" exit 1 fi echo "LIQUIDITY_MANAGER=$LIQUIDITY_MANAGER" >"$STATE_DIR/contracts.env" echo "KRAIKEN=$KRAIKEN" >>"$STATE_DIR/contracts.env" echo "STAKE=$STAKE" >>"$STATE_DIR/contracts.env" # Get deployment block number local deploy_block deploy_block="$(jq -r '.receipts[0].blockNumber' "$run_file" | xargs printf "%d")" # Create .env.local for Ponder with deployed addresses cat > "$ROOT_DIR/services/ponder/.env.local" <>"$SETUP_LOG" 2>&1 log "Granting recenter access" "$CAST" rpc --rpc-url "$ANVIL_RPC" anvil_impersonateAccount "$FEE_DEST" >/dev/null "$CAST" send --rpc-url "$ANVIL_RPC" --from "$FEE_DEST" --unlocked \ "$LIQUIDITY_MANAGER" "setRecenterAccess(address)" "$DEPLOYER_ADDR" >>"$SETUP_LOG" 2>&1 "$CAST" rpc --rpc-url "$ANVIL_RPC" anvil_stopImpersonatingAccount "$FEE_DEST" >/dev/null log "Calling recenter()" "$CAST" send --rpc-url "$ANVIL_RPC" --private-key "$DEPLOYER_PK" \ "$LIQUIDITY_MANAGER" "recenter()" >>"$SETUP_LOG" 2>&1 } prepare_application_state() { source_addresses || { log "Contract addresses not found"; exit 1; } log "Wrapping 0.02 ETH into WETH" "$CAST" send --rpc-url "$ANVIL_RPC" --private-key "$DEPLOYER_PK" \ "$WETH" "deposit()" --value 0.02ether >>"$SETUP_LOG" 2>&1 log "Approving SwapRouter" \ && "$CAST" send --rpc-url "$ANVIL_RPC" --private-key "$DEPLOYER_PK" \ "$WETH" "approve(address,uint256)" "$SWAP_ROUTER" "$MAX_UINT" >>"$SETUP_LOG" 2>&1 log "Executing initial KRK buy" "$CAST" send --legacy --gas-limit 300000 --rpc-url "$ANVIL_RPC" --private-key "$DEPLOYER_PK" \ "$SWAP_ROUTER" "exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))" \ "($WETH,$KRAIKEN,10000,$DEPLOYER_ADDR,10000000000000000,0,0)" >>"$SETUP_LOG" 2>&1 } prime_chain_for_indexing() { log "Pre-mining blocks for indexer stability" for _ in {1..1200}; do "$CAST" rpc --rpc-url "$ANVIL_RPC" evm_mine > /dev/null 2>&1 || true done } write_txnbot_env() { source_addresses || { log "Contract addresses not found"; exit 1; } cat > "$TXNBOT_ENV_FILE" <>"$SETUP_LOG" 2>&1 || \ log "Funding txnBot wallet failed (see setup log)" } start_txnbot() { if [[ -f "$TXNBOT_PID_FILE" ]]; then log "txnBot already running (pid $(cat \"$TXNBOT_PID_FILE\"))" return fi write_txnbot_env fund_txnbot_wallet log "Starting txnBot automation" pushd "$ROOT_DIR/services/txnBot" >/dev/null TXN_BOT_ENV_FILE="$TXNBOT_ENV_FILE" node service.js >"$TXNBOT_LOG" 2>&1 & popd >/dev/null echo $! >"$TXNBOT_PID_FILE" } start_ponder() { if [[ -f "$PONDER_PID_FILE" ]]; then log "Ponder already running (pid $(cat "$PONDER_PID_FILE"))" return fi log "Starting Ponder indexer" pushd "$ROOT_DIR/services/ponder" >/dev/null PONDER_NETWORK=BASE_SEPOLIA_LOCAL_FORK npm run dev >"$PONDER_LOG" 2>&1 & popd >/dev/null echo $! >"$PONDER_PID_FILE" wait_for_http "$GRAPHQL_HEALTH" } start_frontend() { if [[ -f "$WEBAPP_PID_FILE" ]]; then log "Frontend already running (pid $(cat "$WEBAPP_PID_FILE"))" return fi log "Starting frontend (Vite dev server)" pushd "$ROOT_DIR/web-app" >/dev/null npm run dev -- --host 0.0.0.0 --port 5173 >"$WEBAPP_LOG" 2>&1 & popd >/dev/null echo $! >"$WEBAPP_PID_FILE" wait_for_http "$FRONTEND_URL" } start_environment() { ensure_tools set_deployer_credentials start_directories ensure_dependencies start_anvil deploy_protocol extract_addresses bootstrap_liquidity_manager prepare_application_state prime_chain_for_indexing start_ponder # Re-enabled with PostgreSQL start_txnbot start_frontend } start_and_wait() { start_environment source_addresses || true log "Environment ready" log " RPC: $ANVIL_RPC" log " Kraiken: $KRAIKEN" log " Stake: $STAKE" log " LM: $LIQUIDITY_MANAGER" log " Indexer: $GRAPHQL_HEALTH" log " Frontend: $FRONTEND_URL" log "Press Ctrl+C to shut everything down" wait "$(cat "$ANVIL_PID_FILE")" "$(cat "$PONDER_PID_FILE")" "$(cat "$WEBAPP_PID_FILE")" "$(cat "$TXNBOT_PID_FILE")" } stop_environment() { local previous_skip="$SKIP_CLEANUP" SKIP_CLEANUP=false cleanup SKIP_CLEANUP="$previous_skip" log "Environment stopped" } on_exit() { local status=$? if [[ "$COMMAND" != "start" ]]; then return fi if [[ $status -ne 0 ]]; then log "Start command exited with status $status" if [[ -f "$SETUP_LOG" ]]; then log "Last 40 lines of setup log:" tail -n 40 "$SETUP_LOG" fi fi cleanup if [[ "$SKIP_CLEANUP" == true ]]; then log "State directory preserved at $STATE_DIR" else log "Environment stopped" fi } usage() { cat </dev/null || true printf '\n%-12s %-8s %-10s %s\n' "Service" "Status" "PID" "Details" printf '%s\n' "-------------------------------------------------------------" service_status "Anvil" "$ANVIL_PID_FILE" "$ANVIL_LOG" service_status "Ponder" "$PONDER_PID_FILE" "$PONDER_LOG" service_status "Frontend" "$WEBAPP_PID_FILE" "$WEBAPP_LOG" service_status "TxnBot" "$TXNBOT_PID_FILE" "$TXNBOT_LOG" if [[ -f "$STATE_DIR/contracts.env" ]]; then printf '\nContracts:\n' cat "$STATE_DIR/contracts.env" printf '\n' fi } service_status() { local name="$1" pid_file="$2" log_file="$3" if [[ -f "$pid_file" ]]; then local pid="$(cat "$pid_file")" if kill -0 "$pid" 2>/dev/null; then printf '%-12s %-8s %-10s %s\n' "$name" "running" "$pid" "$log_file" return fi fi printf '%-12s %-8s %-10s %s\n' "$name" "stopped" "-" "$log_file" } COMMAND="start" while (($#)); do case "$1" in --no-cleanup) SKIP_CLEANUP=true ;; start|stop|status) COMMAND="$1" ;; *) usage exit 1 ;; esac shift done if [[ "$COMMAND" == "start" ]]; then trap on_exit EXIT fi case "$COMMAND" in start) trap 'log "Caught signal, stopping..."; exit 0' INT TERM start_and_wait ;; stop) stop_environment ;; status) status_environment ;; *) usage exit 1 ;; esac