diff --git a/playground/.env.example b/playground/.env.example index deb4362cd1..bb1dae5c47 100644 --- a/playground/.env.example +++ b/playground/.env.example @@ -6,3 +6,8 @@ POSTGRES_PASSWORD=123 TOML_TRACE_ERROR=1 CHAIN=1 ETHFLOW_CONTRACTS=0x04501b9b1d52e67f6862d157e00d13419d2d6e95 + +# Otterscan Sourcify source: "cloud" (default) or "local" +# - cloud: Shows publicly verified contracts from sourcify.dev +# - local: Shows contracts verified on the local Sourcify instance +SOURCIFY_MODE=cloud diff --git a/playground/Dockerfile b/playground/Dockerfile index a9439ec7db..08c4e97745 100644 --- a/playground/Dockerfile +++ b/playground/Dockerfile @@ -1,6 +1,6 @@ FROM debian:bookworm AS chef WORKDIR /src/ -RUN apt-get update && apt-get install -y curl git clang mold libssl-dev pkg-config git && apt-get clean +RUN apt-get update && apt-get install -y curl git clang mold libssl-dev pkg-config git make && apt-get clean RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y ENV PATH="$PATH:/root/.cargo/bin" RUN rustup component add clippy rustfmt diff --git a/playground/Dockerfile.cowswap b/playground/Dockerfile.cowswap index 7ba495ced9..838f469465 100644 --- a/playground/Dockerfile.cowswap +++ b/playground/Dockerfile.cowswap @@ -6,6 +6,10 @@ WORKDIR /usr/src/app ARG REACT_APP_NETWORK_URL_1=https://rpc.mevblocker.io ARG REACT_APP_NETWORK_URL_5=https://ethereum-goerli.publicnode.com ARG REACT_APP_NETWORK_URL_100=https://gnosis.publicnode.com +ARG REACT_APP_EXPLORER_URL_DEV=http://localhost:8001 + +# Block explorer URL (Otterscan for local development) +ARG REACT_APP_BLOCK_EXPLORER_URL=http://localhost:8003 # Orderbook URL args ARG REACT_APP_ORDER_BOOK_URLS='{"1":"https://api.cow.fi/mainnet","100":"https://api.cow.fi/goerli","5":"https://api.cow.fi/xdai"}' @@ -34,6 +38,8 @@ ENV REACT_APP_NETWORK_URL_1=$REACT_APP_NETWORK_URL_1 ENV REACT_APP_NETWORK_URL_5=$REACT_APP_NETWORK_URL_5 ENV REACT_APP_NETWORK_URL_100=$REACT_APP_NETWORK_URL_100 ENV REACT_APP_ORDER_BOOK_URLS=$REACT_APP_ORDER_BOOK_URLS +ENV REACT_APP_EXPLORER_URL_DEV=$REACT_APP_EXPLORER_URL_DEV +ENV REACT_APP_BLOCK_EXPLORER_URL=$REACT_APP_BLOCK_EXPLORER_URL # Update environment variables based on "chain" and "ETH_RPC_URL" and build the frontend RUN if [ -n "$ETH_RPC_URL" ]; then \ @@ -54,7 +60,9 @@ RUN if [ -n "$ETH_RPC_URL" ]; then \ NODE_OPTIONS="--max-old-space-size=4096" NX_NO_CLOUD=true yarn build --env REACT_APP_NETWORK_URL_1=$REACT_APP_NETWORK_URL_1 \ --env REACT_APP_NETWORK_URL_5=$REACT_APP_NETWORK_URL_5 \ --env REACT_APP_NETWORK_URL_100=$REACT_APP_NETWORK_URL_100 \ - --env REACT_APP_ORDER_BOOK_URLS="$REACT_APP_ORDER_BOOK_URLS"; \ + --env REACT_APP_ORDER_BOOK_URLS="$REACT_APP_ORDER_BOOK_URLS" \ + --env REACT_APP_EXPLORER_URL_DEV="$REACT_APP_EXPLORER_URL_DEV" \ + --env REACT_APP_BLOCK_EXPLORER_URL="$REACT_APP_BLOCK_EXPLORER_URL"; \ fi # Stage 2: Copy the frontend to the nginx container diff --git a/playground/Dockerfile.explorer b/playground/Dockerfile.explorer index 34a2929007..71107c2420 100644 --- a/playground/Dockerfile.explorer +++ b/playground/Dockerfile.explorer @@ -13,13 +13,16 @@ RUN git clone https://github.com/cowprotocol/cowswap . && \ # Install npm dependencies RUN yarn install --frozen-lockfile --no-cache +# Build environment variables ENV REACT_APP_ORDER_BOOK_URLS='{"1":"http://localhost:8080"}' +ENV REACT_APP_BLOCK_EXPLORER_URL=http://localhost:8003 # Build the frontend -RUN APP_ID=1 yarn build:explorer +RUN APP_ID=1 REACT_APP_BLOCK_EXPLORER_URL=$REACT_APP_BLOCK_EXPLORER_URL yarn build:explorer # Stage 2: Copy the frontend to the nginx container FROM docker.io/nginx:1.21-alpine AS frontend COPY --from=node-build /usr/src/app/build/explorer /usr/share/nginx/html +COPY nginx-spa.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] diff --git a/playground/Dockerfile.otterscan b/playground/Dockerfile.otterscan new file mode 100644 index 0000000000..e850887ee8 --- /dev/null +++ b/playground/Dockerfile.otterscan @@ -0,0 +1,6 @@ +FROM otterscan/otterscan:latest + +COPY otterscan-entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/playground/Dockerfile.sourcify b/playground/Dockerfile.sourcify new file mode 100644 index 0000000000..ce280796c1 --- /dev/null +++ b/playground/Dockerfile.sourcify @@ -0,0 +1,31 @@ +FROM node:22-bookworm-slim + +# Install dependencies +RUN apt-get update && apt-get install -y git curl postgresql-client && \ + curl -fsSL -o /usr/local/bin/dbmate https://github.com/amacneil/dbmate/releases/download/v2.21.0/dbmate-linux-amd64 && \ + chmod +x /usr/local/bin/dbmate && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +# Clone Sourcify with submodules +RUN git clone --depth 1 --recurse-submodules https://github.com/ethereum/sourcify.git /sourcify + +WORKDIR /sourcify + +# Install dependencies and build +RUN npm install && npm run build:lerna + +# Prepare migrations +WORKDIR /sourcify/services/database +RUN mkdir -p /migrations && \ + cp -r database-specs/migrations/* /migrations/ 2>/dev/null || true && \ + cp -r migrations/* /migrations/ 2>/dev/null || true + +WORKDIR /sourcify/services/server + +# Copy custom entrypoint +COPY sourcify-entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +EXPOSE 5555 + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/playground/README.md b/playground/README.md index 44af35a9ae..3269971f77 100644 --- a/playground/README.md +++ b/playground/README.md @@ -42,7 +42,7 @@ Now with Rabby configured, and the services started, you can browse to http://lo > The EthFlow is not configured by default, the next section explains how to set it up. > You can follow along with watching the logs of the `autopilot`, `driver`, and `baseline` solver to see how the Protocol interacts. > If you make any changes to the files in your repo directory, services will automatically be recompiled and restarted. -> The CoW Explorer is avialable at http://localhost:8001 to see more information about transaction status +> The CoW Explorer is available at http://localhost:8001 to see more information about transaction status ### Resetting the playground @@ -115,6 +115,9 @@ await window.ethereum.request({ | Postgres | postgres | 5432 | 5432 | N/A | Local/Fork | | Adminer | adminer | 8082 | 8080 | N/A | Local/Fork | | Grafana | grafana | 3000 | 3000 | N/A | Local/Fork | +| Otterscan | otterscan | 8003 | 80 | N/A | Local/Fork | +| Sourcify | sourcify | 5555 | 5555 | N/A | Local/Fork | +| Sourcify DB | sourcify-db | N/A | 5432 | N/A | Local/Fork | **NOTE**: Currently only **FORK** mode is supported. @@ -137,16 +140,61 @@ In this mode, the stack will spin up: - Postgres (with migrations) - Adminer - RPC (forked from `reth` or `erigon` node) -- Otterscan (*not yet implemented*) +- Otterscan +- Sourcify (contract verification) - Orderbook - Autopilot - Driver - Baseline - Cow Swap -- Cow Explorer (*not yet implemented*) +- Cow Explorer ### Local **NOT YET IMPLEMENTED** - As per fork, but with a local node (not forked from Erigon) + +## Contract Verification with Sourcify + +The playground includes a local [Sourcify](https://sourcify.dev/) instance for contract verification. Sourcify is a decentralized contract verification service that matches deployed bytecode with source code. Verified contracts display their source code in Otterscan. + +**How it works:** + +- **Cloud mode** (`SOURCIFY_MODE=cloud`): Otterscan fetches verified source code from the public Sourcify repository. This shows source code for well-known contracts (CoW Protocol, USDC, etc.) that have been publicly verified. +- **Local mode** (`SOURCIFY_MODE=local`): Otterscan fetches from your local Sourcify instance. Use this when testing contracts you deploy and verify locally. + +### Sourcify Sources Configuration + +Configure which Sourcify source Otterscan uses in your `.env` file: + +```bash +# Use public Sourcify (default) - shows publicly verified contracts +SOURCIFY_MODE=cloud + +# Use local Sourcify - shows contracts verified on your local instance +SOURCIFY_MODE=local +``` + +After changing this value, recreate the Otterscan container: + +```bash +docker compose -f docker-compose.fork.yml up -d otterscan +``` + +or + +```bash +docker compose -f docker-compose.non-interactive.yml up -d otterscan +``` + +> **Note**: A simple `docker compose restart` won't work because it doesn't re-read `.env` - you need to recreate the container. + +### Verifying Contracts + +You can verify contracts on the local Sourcify instance using: + +1. **Sourcify API**: POST to `http://localhost:5555/verify` with your contract address, chain ID, and source files +2. **Foundry**: Use `forge verify-contract` with `--verifier sourcify --verifier-url http://localhost:5555` + +After verification, view the contract source in Otterscan at `http://localhost:8003/address/`. diff --git a/playground/docker-compose.fork.yml b/playground/docker-compose.fork.yml index 17f401cfb2..5d5869df2f 100644 --- a/playground/docker-compose.fork.yml +++ b/playground/docker-compose.fork.yml @@ -213,6 +213,65 @@ services: ports: - 8000:80 + # Sourcify - Contract verification service + sourcify-db: + image: postgres:15-alpine + restart: always + environment: + - POSTGRES_USER=sourcify + - POSTGRES_PASSWORD=sourcify + - POSTGRES_DB=sourcify + volumes: + - sourcify-postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U sourcify"] + interval: 5s + timeout: 5s + retries: 5 + + sourcify: + build: + context: . + dockerfile: Dockerfile.sourcify + restart: always + depends_on: + sourcify-db: + condition: service_healthy + environment: + - SOURCIFY_POSTGRES_HOST=sourcify-db + - SOURCIFY_POSTGRES_PORT=5432 + - SOURCIFY_POSTGRES_USER=sourcify + - SOURCIFY_POSTGRES_PASSWORD=sourcify + - SOURCIFY_POSTGRES_DB=sourcify + - NODE_ENV=development + volumes: + - ./sourcify-chains.json:/sourcify/services/server/dist/sourcify-chains.json + ports: + - 5555:5555 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5555/health"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + + otterscan: + build: + context: . + dockerfile: Dockerfile.otterscan + restart: always + environment: + - ERIGON_URL=http://127.0.0.1:8545 + - SOURCIFY_MODE=${SOURCIFY_MODE:-cloud} + - LOCAL_SOURCIFY_URL=http://sourcify:5555 + ports: + - 8003:80 + depends_on: + chain: + condition: service_healthy + sourcify: + condition: service_healthy + explorer: build: context: . @@ -253,3 +312,4 @@ services: volumes: postgres: + sourcify-postgres: diff --git a/playground/docker-compose.non-interactive.yml b/playground/docker-compose.non-interactive.yml index f41847875b..fbd5b15d8b 100644 --- a/playground/docker-compose.non-interactive.yml +++ b/playground/docker-compose.non-interactive.yml @@ -202,6 +202,66 @@ services: ports: - 8000:80 + # Sourcify - Contract verification service + sourcify-db: + image: postgres:15-alpine + restart: always + environment: + - POSTGRES_USER=sourcify + - POSTGRES_PASSWORD=sourcify + - POSTGRES_DB=sourcify + volumes: + - sourcify-postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U sourcify"] + interval: 5s + timeout: 5s + retries: 5 + + sourcify: + build: + context: . + dockerfile: Dockerfile.sourcify + restart: always + depends_on: + sourcify-db: + condition: service_healthy + environment: + - SOURCIFY_POSTGRES_HOST=sourcify-db + - SOURCIFY_POSTGRES_PORT=5432 + - SOURCIFY_POSTGRES_USER=sourcify + - SOURCIFY_POSTGRES_PASSWORD=sourcify + - SOURCIFY_POSTGRES_DB=sourcify + - NODE_ENV=development + volumes: + - ./sourcify-chains.json:/sourcify/services/server/dist/sourcify-chains.json + ports: + - 5555:5555 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5555/health"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + + # Otterscan - Local blockchain block explorer for Anvil + otterscan: + build: + context: . + dockerfile: Dockerfile.otterscan + restart: always + environment: + - ERIGON_URL=http://127.0.0.1:8545 + - SOURCIFY_MODE=${SOURCIFY_MODE:-cloud} + - LOCAL_SOURCIFY_URL=http://sourcify:5555 + ports: + - 8003:80 + depends_on: + chain: + condition: service_healthy + sourcify: + condition: service_healthy + explorer: build: context: . @@ -231,7 +291,6 @@ services: volumes: - ./grafana-prometheus.yml:/etc/grafana/provisioning/datasources/prometheus.yml - prometheus: image: prom/prometheus:latest container_name: prometheus @@ -243,3 +302,4 @@ services: volumes: postgres: + sourcify-postgres: diff --git a/playground/nginx-spa.conf b/playground/nginx-spa.conf new file mode 100644 index 0000000000..2d96d1589b --- /dev/null +++ b/playground/nginx-spa.conf @@ -0,0 +1,10 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/playground/otterscan-entrypoint.sh b/playground/otterscan-entrypoint.sh new file mode 100755 index 0000000000..6af791a8bd --- /dev/null +++ b/playground/otterscan-entrypoint.sh @@ -0,0 +1,51 @@ +#!/bin/sh +# Otterscan entrypoint that configures Sourcify based on SOURCIFY_MODE + +CONFIG_FILE="/usr/share/nginx/html/config.json" +ERIGON_URL="${ERIGON_URL:-http://127.0.0.1:8545}" +LOCAL_SOURCIFY_URL="${LOCAL_SOURCIFY_URL:-http://localhost:5555}" + +echo "=== Otterscan Entrypoint ===" +echo "SOURCIFY_MODE: ${SOURCIFY_MODE}" + +case "${SOURCIFY_MODE:-cloud}" in + local) + echo "Using LOCAL Sourcify as primary source" + cat > "$CONFIG_FILE" << 'EOF' +{ + "erigonURL": "${ERIGON_URL}", + "sourcify": { + "sources": { + "Local Sourcify": { + "url": "${LOCAL_SOURCIFY_URL}/repository", + "backendFormat": "RepositoryV1" + } + } + } +} +EOF + ;; + cloud|*) + echo "Using CLOUD Sourcify as primary source" + cat > "$CONFIG_FILE" << 'EOF' +{ + "erigonURL": "${ERIGON_URL}", + "sourcify": { + "sources": { + "Sourcify": { + "url": "https://repo.sourcify.dev", + "backendFormat": "RepositoryV1" + } + } + } +} +EOF + ;; +esac + +echo "Config written to $CONFIG_FILE:" +cat "$CONFIG_FILE" +echo "" +echo "=== Starting nginx ===" + +exec nginx -g "daemon off;" diff --git a/playground/sourcify-chains.json b/playground/sourcify-chains.json new file mode 100644 index 0000000000..b820af7586 --- /dev/null +++ b/playground/sourcify-chains.json @@ -0,0 +1,12 @@ +{ + "1": { + "sourcifyName": "Mainnet (Forked)", + "supported": true, + "rpc": ["http://chain:8545"] + }, + "31337": { + "sourcifyName": "Anvil Local", + "supported": true, + "rpc": ["http://chain:8545"] + } +} diff --git a/playground/sourcify-entrypoint.sh b/playground/sourcify-entrypoint.sh new file mode 100644 index 0000000000..6ccada206c --- /dev/null +++ b/playground/sourcify-entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +echo "=== Sourcify Server Starting ===" + +# Wait for database to be ready +echo "Waiting for database..." +until pg_isready -h "$SOURCIFY_POSTGRES_HOST" -p "$SOURCIFY_POSTGRES_PORT" -U "$SOURCIFY_POSTGRES_USER" -d "$SOURCIFY_POSTGRES_DB" > /dev/null 2>&1; do + echo "Database not ready, waiting..." + sleep 2 +done +echo "Database is ready!" + +# Run migrations +echo "Running database migrations..." +DATABASE_URL="postgres://${SOURCIFY_POSTGRES_USER}:${SOURCIFY_POSTGRES_PASSWORD}@${SOURCIFY_POSTGRES_HOST}:${SOURCIFY_POSTGRES_PORT}/${SOURCIFY_POSTGRES_DB}?sslmode=disable" +dbmate --url "$DATABASE_URL" --migrations-dir /migrations --no-dump-schema up +echo "Migrations complete!" + +# Start the server +echo "Starting Sourcify server..." +exec node dist/server/cli.js