diff --git a/containers/Caddyfile b/containers/Caddyfile new file mode 100644 index 0000000..a8197c1 --- /dev/null +++ b/containers/Caddyfile @@ -0,0 +1,15 @@ +:80 { + route /graphql* { + reverse_proxy ponder:42069 + } + route /health* { + reverse_proxy ponder:42069 + } + route /rpc/anvil* { + reverse_proxy anvil:8545 + } + route /txn* { + reverse_proxy txn-bot:43069 + } + reverse_proxy frontend:5173 +} diff --git a/containers/anvil-entrypoint.sh b/containers/anvil-entrypoint.sh new file mode 100755 index 0000000..8a1a541 --- /dev/null +++ b/containers/anvil-entrypoint.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +MNEMONIC_FILE=/workspace/onchain/.secret.local +ANVIL_CMD=(anvil --fork-url "${FORK_URL:-https://sepolia.base.org}" --chain-id 31337 --block-time 1 --host 0.0.0.0 --port 8545) + +if [[ -f "$MNEMONIC_FILE" ]]; then + MNEMONIC="$(tr -d '\n\r' <"$MNEMONIC_FILE")" + if [[ -n "$MNEMONIC" ]]; then + ANVIL_CMD+=(--mnemonic "$MNEMONIC") + fi +fi + +exec "${ANVIL_CMD[@]}" diff --git a/containers/bootstrap.sh b/containers/bootstrap.sh new file mode 100755 index 0000000..c7b32c8 --- /dev/null +++ b/containers/bootstrap.sh @@ -0,0 +1,199 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR=/workspace +STATE_DIR=$ROOT_DIR/tmp/podman +LOG_DIR=$STATE_DIR/logs +SETUP_LOG=$LOG_DIR/setup.log +CONTRACT_ENV=$STATE_DIR/contracts.env +TXNBOT_ENV=$STATE_DIR/txnBot.env +MNEMONIC_FILE=$ROOT_DIR/onchain/.secret.local + +mkdir -p "$LOG_DIR" +: >"$SETUP_LOG" + +ANVIL_RPC=${ANVIL_RPC:-"http://anvil:8545"} +FEE_DEST=0xf6a3eef9088A255c32b6aD2025f83E57291D9011 +WETH=0x4200000000000000000000000000000000000006 +SWAP_ROUTER=0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4 +MAX_UINT=0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + +DEFAULT_DEPLOYER_PK=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +DEFAULT_DEPLOYER_ADDR=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 +DEPLOYER_PK=${DEPLOYER_PK:-$DEFAULT_DEPLOYER_PK} +DEPLOYER_ADDR=${DEPLOYER_ADDR:-$DEFAULT_DEPLOYER_ADDR} + +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} + +log() { + echo "[bootstrap] $*" +} + +wait_for_rpc() { + for _ in {1..120}; do + if curl -s -o /dev/null -X POST "$ANVIL_RPC" \ + -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 Anvil at $ANVIL_RPC" + return 1 +} + +maybe_set_deployer_from_mnemonic() { + if [[ -n "$DEPLOYER_PK" && -n "$DEPLOYER_ADDR" ]]; then + return + fi + if [[ -f "$MNEMONIC_FILE" ]]; then + local mnemonic pk addr + mnemonic="$(tr -d '\n\r' <"$MNEMONIC_FILE")" + if [[ -n "$mnemonic" ]]; then + pk="$(cast wallet private-key --mnemonic "$mnemonic" --mnemonic-derivation-path "m/44'/60'/0'/0/0")" + addr="$(cast wallet address --private-key "$pk")" + DEPLOYER_PK=${DEPLOYER_PK:-$pk} + DEPLOYER_ADDR=${DEPLOYER_ADDR:-$addr} + fi + fi + DEPLOYER_PK=${DEPLOYER_PK:-$DEFAULT_DEPLOYER_PK} + DEPLOYER_ADDR=${DEPLOYER_ADDR:-$DEFAULT_DEPLOYER_ADDR} +} + +run_forge_script() { + log "Deploying contracts to fork" + 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" ]]; then + log "Deployment artifact not found" + exit 1 + fi + log "Using artifact ${run_file#$ROOT_DIR/}" + 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)" + DEPLOY_BLOCK="$(jq -r '.receipts[0].blockNumber' "$run_file" | xargs printf "%d")" + if [[ -z "$LIQUIDITY_MANAGER" || "$LIQUIDITY_MANAGER" == "null" ]]; then + log "LiquidityManager address missing" + exit 1 + fi + cat >"$CONTRACT_ENV" <>"$SETUP_LOG" 2>&1 +} + +grant_recenter_access() { + log "Granting recenter access" + cast rpc --rpc-url "$ANVIL_RPC" anvil_impersonateAccount "$FEE_DEST" >>"$SETUP_LOG" 2>&1 + 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" >>"$SETUP_LOG" 2>&1 + if [[ -n "$TXNBOT_ADDRESS" ]]; then + cast rpc --rpc-url "$ANVIL_RPC" anvil_impersonateAccount "$FEE_DEST" >>"$SETUP_LOG" 2>&1 + cast send --rpc-url "$ANVIL_RPC" --from "$FEE_DEST" --unlocked \ + "$LIQUIDITY_MANAGER" "setRecenterAccess(address)" "$TXNBOT_ADDRESS" >>"$SETUP_LOG" 2>&1 + cast rpc --rpc-url "$ANVIL_RPC" anvil_stopImpersonatingAccount "$FEE_DEST" >>"$SETUP_LOG" 2>&1 + fi +} + +call_recenter() { + log "Calling recenter()" + cast send --rpc-url "$ANVIL_RPC" --private-key "$DEPLOYER_PK" \ + "$LIQUIDITY_MANAGER" "recenter()" >>"$SETUP_LOG" 2>&1 +} + +seed_application_state() { + log "Wrapping ETH to WETH" + cast send --rpc-url "$ANVIL_RPC" --private-key "$DEPLOYER_PK" \ + "$WETH" "deposit()" --value 0.02ether >>"$SETUP_LOG" 2>&1 + log "Approving router" + 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 swap" + 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() { + log "Pre-mining blocks" + for _ in {1..1200}; do + cast rpc --rpc-url "$ANVIL_RPC" evm_mine >/dev/null 2>&1 || true + done +} + +write_ponder_env() { + cat >"$ROOT_DIR/services/ponder/.env.local" <"$TXNBOT_ENV" <>"$SETUP_LOG" 2>&1 || true + local wei hex + wei="$(cast --to-unit "$TXNBOT_FUND_VALUE" wei)" + hex="$(cast --to-hex "$wei")" + cast rpc --rpc-url "$ANVIL_RPC" anvil_setBalance "$TXNBOT_ADDRESS" "$hex" >>"$SETUP_LOG" 2>&1 +} + +main() { + log "Waiting for Anvil" + wait_for_rpc + maybe_set_deployer_from_mnemonic + run_forge_script + extract_addresses + fund_liquidity_manager + grant_recenter_access + call_recenter + seed_application_state + prime_chain + write_ponder_env + write_txn_bot_env + fund_txn_bot_wallet + log "Bootstrap complete" + log "Kraiken: $KRAIKEN" + log "Stake: $STAKE" + log "LiquidityManager: $LIQUIDITY_MANAGER" +} + +main "$@" diff --git a/containers/foundry.Containerfile b/containers/foundry.Containerfile new file mode 100644 index 0000000..3b42feb --- /dev/null +++ b/containers/foundry.Containerfile @@ -0,0 +1,25 @@ +FROM debian:bookworm-slim + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + jq \ + git \ + build-essential \ + pkg-config \ + libssl-dev \ + libclang-dev \ + llvm \ + unzip \ + && rm -rf /var/lib/apt/lists/* + +RUN useradd -m -u 1000 foundry +USER foundry +WORKDIR /home/foundry + +RUN curl -L https://foundry.paradigm.xyz | bash \ + && /home/foundry/.foundry/bin/foundryup + +ENV PATH="/home/foundry/.foundry/bin:${PATH}" +WORKDIR /workspace diff --git a/containers/frontend-dev-entrypoint.sh b/containers/frontend-dev-entrypoint.sh new file mode 100755 index 0000000..c0f4e17 --- /dev/null +++ b/containers/frontend-dev-entrypoint.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR=/workspace +CONTRACT_ENV=$ROOT_DIR/tmp/podman/contracts.env +APP_DIR=$ROOT_DIR/web-app +SWAP_ROUTER=0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4 + +while [[ ! -f "$CONTRACT_ENV" ]]; do + echo "[frontend-entrypoint] waiting for contracts env" + sleep 2 +done + +# shellcheck disable=SC1090 +source "$CONTRACT_ENV" + +cd "$APP_DIR" +if [[ ! -d node_modules ]]; then + npm install +fi + +export VITE_DEFAULT_CHAIN_ID=${VITE_DEFAULT_CHAIN_ID:-31337} +export VITE_LOCAL_RPC_URL=${VITE_LOCAL_RPC_URL:-/rpc/anvil} +export VITE_LOCAL_RPC_PROXY_TARGET=${VITE_LOCAL_RPC_PROXY_TARGET:-http://anvil:8545} +export VITE_KRAIKEN_ADDRESS=$KRAIKEN +export VITE_STAKE_ADDRESS=$STAKE +export VITE_SWAP_ROUTER=$SWAP_ROUTER +export VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK=${VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK:-/graphql} +export VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK=${VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK:-/txn} +export CHOKIDAR_USEPOLLING=${CHOKIDAR_USEPOLLING:-1} + +exec npm run dev -- --host 0.0.0.0 --port 5173 diff --git a/containers/node-dev.Containerfile b/containers/node-dev.Containerfile new file mode 100644 index 0000000..1490337 --- /dev/null +++ b/containers/node-dev.Containerfile @@ -0,0 +1,11 @@ +FROM node:20-bookworm + +RUN apt-get update \ + && apt-get install -y --no-install-recommends dumb-init \ + && rm -rf /var/lib/apt/lists/* + +USER node +WORKDIR /workspace + +ENV PATH="/workspace/node_modules/.bin:${PATH}" +ENTRYPOINT ["dumb-init", "--"] diff --git a/containers/ponder-dev-entrypoint.sh b/containers/ponder-dev-entrypoint.sh new file mode 100755 index 0000000..9a79210 --- /dev/null +++ b/containers/ponder-dev-entrypoint.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR=/workspace +CONTRACT_ENV=$ROOT_DIR/tmp/podman/contracts.env +PONDER_WORKDIR=$ROOT_DIR/services/ponder + +while [[ ! -f "$CONTRACT_ENV" ]]; do + echo "[ponder-entrypoint] waiting for contracts env" + sleep 2 +done + +cd "$PONDER_WORKDIR" +if [[ ! -d node_modules ]]; then + npm install +fi + +export CHOKIDAR_USEPOLLING=${CHOKIDAR_USEPOLLING:-1} +export HOST=0.0.0.0 +export PORT=${PORT:-42069} + +exec npm run dev diff --git a/containers/txn-bot-entrypoint.sh b/containers/txn-bot-entrypoint.sh new file mode 100755 index 0000000..0f4a68c --- /dev/null +++ b/containers/txn-bot-entrypoint.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR=/workspace +TXNBOT_ENV_FILE=$ROOT_DIR/tmp/podman/txnBot.env +BOT_DIR=$ROOT_DIR/services/txnBot + +while [[ ! -f "$TXNBOT_ENV_FILE" ]]; do + echo "[txn-bot-entrypoint] waiting for env file" + sleep 2 +done + +cd "$BOT_DIR" +if [[ ! -d node_modules ]]; then + npm install +fi + +export TXN_BOT_ENV_FILE="$TXNBOT_ENV_FILE" +exec npm run start diff --git a/docs/podman.md b/docs/podman.md new file mode 100644 index 0000000..6553848 --- /dev/null +++ b/docs/podman.md @@ -0,0 +1,44 @@ +# Podman Staging Environment + +The Podman stack mirrors `scripts/local_env.sh` using long-lived containers. Every boot spins up a fresh Base Sepolia fork, redeploys contracts, seeds liquidity, and launches the live-reload services behind Caddy on port 80. + +## Service Topology +- `anvil` – Base Sepolia fork with optional mnemonic from `onchain/.secret.local` +- `bootstrap` – one-shot job running `DeployLocal.sol`, seeding liquidity, priming blocks, and writing shared env files +- `ponder` – `npm run dev` for the indexer (port 42069 inside the pod) +- `frontend` – Vite dev server for `web-app` (port 5173 inside the pod) +- `txn-bot` – automation loop plus Express status API (port 43069 inside the pod) +- `caddy` – front door at `http://:80`, routing `/graphql`, `/health`, `/rpc/anvil`, and `/txn` to the internal services + +All containers mount the repository so code edits hot-reload exactly as the local script. Named volumes keep `node_modules` caches between restarts. + +## Prerequisites +- Podman 4.x (rootless recommended) +- `podman-compose` + +## Launching +```bash +podman-compose -f podman-compose.yml build +podman-compose -f podman-compose.yml up +``` +- First run takes several minutes while Foundry installs deps, deploys contracts, and runs the seeding transactions. +- Use `podman-compose down` to stop. Bring-up always redeploys and rewrites `services/ponder/.env.local` plus `tmp/podman/txnBot.env`. + +### Access Points (via Caddy) +- Frontend: `http:///` +- GraphQL: `http:///graphql` +- RPC passthrough: `http:///rpc/anvil` +- Txn bot status: `http:///txn/status` + +## Configuration Knobs +Set environment variables before `podman-compose up`: +- `FORK_URL` – Anvil upstream RPC (defaults to `https://sepolia.base.org`) +- `DEPLOYER_PK`, `DEPLOYER_ADDR` – override deployer wallet; otherwise derived from `.secret.local` or Foundry defaults +- `TXNBOT_PRIVATE_KEY`, `TXNBOT_ADDRESS`, `TXNBOT_FUND_VALUE` – customise bot signer and funding + +Edit `containers/Caddyfile` if you need different routes or ports. + +## Known Limitations +- State is ephemeral; every restart wipes the fork and redeploys contracts. +- Processes run in dev/watch mode (`npm run dev`), so staging traffic is not production hardened. +- Secrets live in env files inside the repo mount because no external secret store is wired in. diff --git a/podman-compose.yml b/podman-compose.yml new file mode 100644 index 0000000..3bde1cb --- /dev/null +++ b/podman-compose.yml @@ -0,0 +1,101 @@ +version: "3.8" + +services: + anvil: + build: + context: . + dockerfile: containers/foundry.Containerfile + command: ["/workspace/containers/anvil-entrypoint.sh"] + volumes: + - .:/workspace:Z + expose: + - "8545" + restart: unless-stopped + + bootstrap: + build: + context: . + dockerfile: containers/foundry.Containerfile + command: ["/workspace/containers/bootstrap.sh"] + volumes: + - .:/workspace:Z + environment: + - ANVIL_RPC=http://anvil:8545 + depends_on: + anvil: + condition: service_started + restart: "no" + + ponder: + build: + context: . + dockerfile: containers/node-dev.Containerfile + entrypoint: ["/workspace/containers/ponder-dev-entrypoint.sh"] + volumes: + - .:/workspace:Z + - ponder-node-modules:/workspace/services/ponder/node_modules + working_dir: /workspace + environment: + - CHOKIDAR_USEPOLLING=1 + depends_on: + bootstrap: + condition: service_completed_successfully + expose: + - "42069" + restart: unless-stopped + + frontend: + build: + context: . + dockerfile: containers/node-dev.Containerfile + entrypoint: ["/workspace/containers/frontend-dev-entrypoint.sh"] + volumes: + - .:/workspace:Z + - frontend-node-modules:/workspace/web-app/node_modules + working_dir: /workspace + environment: + - CHOKIDAR_USEPOLLING=1 + depends_on: + bootstrap: + condition: service_completed_successfully + expose: + - "5173" + restart: unless-stopped + + txn-bot: + build: + context: . + dockerfile: containers/node-dev.Containerfile + entrypoint: ["/workspace/containers/txn-bot-entrypoint.sh"] + volumes: + - .:/workspace:Z + - txn-node-modules:/workspace/services/txnBot/node_modules + working_dir: /workspace + depends_on: + bootstrap: + condition: service_completed_successfully + expose: + - "43069" + restart: unless-stopped + + caddy: + image: caddy:2.8 + volumes: + - ./containers/Caddyfile:/etc/caddy/Caddyfile:Z + ports: + - "80:80" + depends_on: + anvil: + condition: service_started + ponder: + condition: service_started + frontend: + condition: service_started + txn-bot: + condition: service_started + restart: unless-stopped + +volumes: + frontend-node-modules: + ponder-node-modules: + txn-node-modules: