diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..68ee148 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +# Applies to every docker build rooted at the repo (apps/backend-relayer and +# scripts/docker-local/Dockerfile.deployer both use the repo as context). + +node_modules +**/node_modules + +contracts/stellar/target +contracts/evm/out +contracts/evm/cache + +apps/frontend/.next +apps/backend-relayer/dist + +.git +.github + +.chains.env +.chains.anvil.log +.chains.anvil.pid +.relayer.log +.postgres.log + +**/.env +**/.env.local +**/.env.test diff --git a/.github/workflows/contracts-release.yml b/.github/workflows/contracts-release.yml new file mode 100644 index 0000000..76a7fc3 --- /dev/null +++ b/.github/workflows/contracts-release.yml @@ -0,0 +1,153 @@ +name: Publish Contracts Bundle + +# Builds Stellar WASMs, EVM artifacts, and the deposit verifier key, then +# publishes them as a tarball on two GitHub releases: + +on: + push: + branches: [main] + paths: + - "contracts/**" + - "proof_circuits/**" + - "scripts/build_circuits.sh" + - ".github/workflows/contracts-release.yml" + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: contracts-release + cancel-in-progress: false + +jobs: + build-and-release: + name: Build + release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set short sha + id: sha + run: echo "short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + + # ── toolchains ────────────────────────────────────────────────── + - uses: dtolnay/rust-toolchain@stable + + - name: Install wasm32v1-none target + run: rustup target add wasm32v1-none + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + contracts/stellar/target + key: ${{ runner.os }}-cargo-stellar-${{ hashFiles('contracts/stellar/Cargo.lock') }} + + - name: Install Stellar CLI + run: | + curl -Ls https://github.com/stellar/stellar-cli/releases/download/v23.3.0/stellar-cli-23.3.0-x86_64-unknown-linux-gnu.tar.gz -o /tmp/stellar-cli.tar.gz + mkdir -p "$HOME/.local/bin" + tar -xzf /tmp/stellar-cli.tar.gz -C "$HOME/.local/bin" stellar + chmod +x "$HOME/.local/bin/stellar" + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + - uses: foundry-rs/foundry-toolchain@v1 + + # ── builds ────────────────────────────────────────────────────── + - name: Build Stellar WASMs + working-directory: contracts/stellar + run: | + stellar contract build --package verifier + stellar contract build --package merkle-manager + stellar contract build --package ad-manager + stellar contract build --package order-portal + + - name: Build EVM artifacts + working-directory: contracts/evm + env: + FOUNDRY_PROFILE: default + run: forge build --silent + + - name: Build deposit circuit VK + run: scripts/build_circuits.sh proof_circuits/deposits + + # ── bundle ────────────────────────────────────────────────────── + # Layout mirrors the on-disk paths the docker-local deployer expects + # (deploy.ts reads from these directories verbatim), so the frontend + # dev just extracts the tarball into a bind-mount root. + - name: Assemble bundle + id: bundle + run: | + set -euo pipefail + STAGE="$RUNNER_TEMP/bundle" + WASM_DIR="$STAGE/contracts/stellar/target/wasm32v1-none/release" + EVM_OUT="$STAGE/contracts/evm/out" + VK_DIR="$STAGE/proof_circuits/deposits/target" + mkdir -p "$WASM_DIR" "$EVM_OUT" "$VK_DIR" + + cp contracts/stellar/target/wasm32v1-none/release/ad_manager.wasm "$WASM_DIR/" + cp contracts/stellar/target/wasm32v1-none/release/order_portal.wasm "$WASM_DIR/" + cp contracts/stellar/target/wasm32v1-none/release/merkle_manager.wasm "$WASM_DIR/" + cp contracts/stellar/target/wasm32v1-none/release/verifier.wasm "$WASM_DIR/" + + # Copy only the artifacts the deployer actually loads. + for c in OrderPortal AdManager MerkleManager Verifier wNativeToken ERC20Mock; do + mkdir -p "$EVM_OUT/${c}.sol" + cp contracts/evm/out/${c}.sol/*.json "$EVM_OUT/${c}.sol/" + done + + cp proof_circuits/deposits/target/vk "$VK_DIR/vk" + + TARBALL="$RUNNER_TEMP/contracts-bundle.tar.gz" + tar -C "$STAGE" -czf "$TARBALL" contracts proof_circuits + + # Emit a manifest for quick eyeballing from the release page. + MANIFEST="$RUNNER_TEMP/contracts-bundle.manifest.txt" + { + echo "commit: $GITHUB_SHA" + echo "built: $(date -u +%FT%TZ)" + echo "size: $(du -h "$TARBALL" | cut -f1)" + echo "sha256: $(sha256sum "$TARBALL" | cut -d' ' -f1)" + echo + tar -tzf "$TARBALL" + } > "$MANIFEST" + + echo "tarball=$TARBALL" >> "$GITHUB_OUTPUT" + echo "manifest=$MANIFEST" >> "$GITHUB_OUTPUT" + + # ── immutable per-sha release ─────────────────────────────────── + - name: Release contracts-${{ steps.sha.outputs.short }} + uses: softprops/action-gh-release@v2 + with: + tag_name: contracts-${{ steps.sha.outputs.short }} + name: Contracts @ ${{ steps.sha.outputs.short }} + body: | + Bundled WASMs, EVM artifacts, and deposit VK built from `${{ github.sha }}`. + + Consumed by `scripts/docker-local/up.sh` (defaults to `contracts-latest`; + pin this tag by exporting `CONTRACTS_BUNDLE_TAG=contracts-${{ steps.sha.outputs.short }}`). + files: | + ${{ steps.bundle.outputs.tarball }} + ${{ steps.bundle.outputs.manifest }} + make_latest: "false" + + # ── rolling pointer for docker-local ──────────────────────────── + - name: Update contracts-latest + uses: softprops/action-gh-release@v2 + with: + tag_name: contracts-latest + name: Contracts (latest main) + body: | + Rolling pointer to the most recent contracts build from `main`. + Current commit: ${{ github.sha }} + + `scripts/docker-local/up.sh` pulls this tarball by default. + files: | + ${{ steps.bundle.outputs.tarball }} + ${{ steps.bundle.outputs.manifest }} + make_latest: "false" diff --git a/.gitignore b/.gitignore index ab0e875..49ad359 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules -target \ No newline at end of file +target +scripts/docker-local/.artifacts/ +scripts/docker-local/.env \ No newline at end of file diff --git a/scripts/docker-local/.env.example b/scripts/docker-local/.env.example new file mode 100644 index 0000000..1f49451 --- /dev/null +++ b/scripts/docker-local/.env.example @@ -0,0 +1,19 @@ +# Copy to `.env` next to docker-compose.yaml and fill in the addresses you +# want the local stack to fund. Both fields are optional — leave blank to +# skip that chain. +# +# cp scripts/docker-local/.env.example scripts/docker-local/.env +# $EDITOR scripts/docker-local/.env +# bash scripts/docker-local/up.sh + +# Your MetaMask (or similar) address on anvil. Receives 100 ETH + 1,000,000 +# TT (ERC20Mock test token) on first deploy. +DEV_EVM_ADDRESS= + +# Your Stellar account G-strkey. Friendbot-funded with ~10k XLM on the +# local quickstart. +DEV_STELLAR_ADDRESS= + +# Pin a specific contracts build (immutable per-sha release). Defaults to +# `contracts-latest` if unset. +# CONTRACTS_BUNDLE_TAG=contracts-abc1234 diff --git a/scripts/docker-local/Dockerfile.deployer b/scripts/docker-local/Dockerfile.deployer new file mode 100644 index 0000000..e37bab1 --- /dev/null +++ b/scripts/docker-local/Dockerfile.deployer @@ -0,0 +1,62 @@ +# syntax=docker/dockerfile:1.7 +# +# Slim deployer image for the local docker stack. +# +# Only the runtime deploy toolkit is baked in (stellar CLI + node + pnpm). +# Contract artifacts (WASMs, EVM ABIs, deposit VK) are NOT built here — they +# are pulled by `up.sh` from the `contracts-latest` GitHub Release and +# bind-mounted into the container at the paths deploy.ts expects. +# +# Image build is ~1 min on a cold cache; rebuilds happen only when +# workspace manifests or the deploy scripts change. + +FROM node:20-slim + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + jq \ + && rm -rf /var/lib/apt/lists/* + +# Stellar CLI — runtime `contract deploy` / `contract invoke` only. +ARG STELLAR_CLI_VERSION=23.3.0 +ARG STELLAR_CLI_SHA256=b3a5455d7113a53a8bd0f1ba0148a14b7aa7a46ee2f49c3b9277424775b309ad +RUN url="https://github.com/stellar/stellar-cli/releases/download/v${STELLAR_CLI_VERSION}/stellar-cli-${STELLAR_CLI_VERSION}-x86_64-unknown-linux-gnu.tar.gz" \ + && curl -fsSL "$url" -o /tmp/stellar.tgz \ + && echo "${STELLAR_CLI_SHA256} /tmp/stellar.tgz" | sha256sum -c - \ + && tar -xzf /tmp/stellar.tgz -C /usr/local/bin stellar \ + && chmod +x /usr/local/bin/stellar \ + && rm /tmp/stellar.tgz + +RUN corepack enable && corepack prepare pnpm@10.10.0 --activate + +WORKDIR /repo + +# Cache-friendly pnpm install — manifests first, then the workspace sources +# the deployer actually executes. +COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./ +COPY apps/backend-relayer/package.json apps/backend-relayer/ +COPY apps/frontend/package.json apps/frontend/ +COPY packages/proofbridge_mmr/package.json packages/proofbridge_mmr/ +COPY scripts/relayer-e2e/package.json scripts/relayer-e2e/ +COPY scripts/cross-chain-e2e/package.json scripts/cross-chain-e2e/ +COPY contracts/stellar/tests/fixtures/package.json contracts/stellar/tests/fixtures/ + +RUN pnpm fetch --ignore-scripts + +# Sources needed at runtime: backend-relayer for prisma schema/migrations, +# packages/* for workspace links, scripts/* for the deploy + seed CLIs. +COPY apps/backend-relayer apps/backend-relayer +COPY packages packages +COPY scripts scripts + +RUN pnpm install --frozen-lockfile --offline --ignore-scripts \ + && pnpm --filter backend-relayer exec prisma generate + +COPY scripts/docker-local/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/scripts/docker-local/README.md b/scripts/docker-local/README.md new file mode 100644 index 0000000..6cf64a3 --- /dev/null +++ b/scripts/docker-local/README.md @@ -0,0 +1,138 @@ +# docker-local + +Single-command local stack for frontend development. Starts anvil + stellar +quickstart + postgres, deploys the EVM and Soroban contracts, seeds the +relayer DB with chains/tokens/routes, and runs `backend-relayer` on +`http://localhost:2005` — no host toolchain beyond Docker. + +## Requirements + +- Docker 24+ with compose v2 +- `curl` (used by `up.sh` to fetch the contracts bundle) + +No rust / foundry / nargo / stellar CLI needed on the host — contract +artifacts are downloaded from the public `contracts-latest` GitHub Release +and bind-mounted into the deployer container. + +## Usage + +```bash +bash scripts/docker-local/up.sh # pull artifacts + start everything +bash scripts/docker-local/down.sh # stop (keeps postgres + deploy snapshot) +bash scripts/docker-local/down.sh -r # stop + wipe volumes for a fresh deploy +``` + +First run downloads a ~few-MB tarball and builds the deployer image +(~1 min). Subsequent `up`s reuse both, so cold-start drops to ~20-30s. + +### Funding your dev wallets + +Export your frontend-dev addresses before running `up.sh` — the deployer +will top up native balance + mint test tokens on the respective chains: + +```bash +export DEV_EVM_ADDRESS=0xYourMetaMaskAddress +export DEV_STELLAR_ADDRESS=GYOURSTELLAR... +bash scripts/docker-local/up.sh +``` + +Or drop them into `scripts/docker-local/.env` (docker-compose auto-loads +that file): + +```text +DEV_EVM_ADDRESS=0x... +DEV_STELLAR_ADDRESS=G... +``` + +What you get on first successful deploy: + +- **EVM**: 100 ETH on anvil (via `anvil_setBalance`) + 1,000,000 TT (the + `ERC20Mock` test token) minted to the dev address +- **Stellar**: friendbot-funded G-address (~10k XLM on the local + quickstart) + +Leaving either variable unset skips that chain. Reruns are idempotent: +`anvil_setBalance` overwrites and friendbot swallows +`op_already_exists`. + +### Pinning a specific build + +`up.sh` defaults to the rolling `contracts-latest` tag, which is updated +by `.github/workflows/contracts-release.yml` on every push to `main`. +To pin a specific commit's artifacts: + +```bash +CONTRACTS_BUNDLE_TAG=contracts- bash scripts/docker-local/up.sh +``` + +Every `main` build also publishes an immutable `contracts-` +release, so bisecting regressions is cheap. + +### Iterating on contracts locally + +If you're actively editing contracts / circuits and have the toolchains +installed, skip the download and bind-mount your repo's own build tree: + +```bash +# build the pieces you changed +pushd contracts/stellar && stellar contract build && popd +pushd contracts/evm && forge build --silent && popd +scripts/build_circuits.sh proof_circuits/deposits + +# then start the stack pointed at your tree +bash scripts/docker-local/up.sh --local +``` + +`--local` copies from `contracts/stellar/target/...`, `contracts/evm/out`, +and `proof_circuits/deposits/target/vk` into `.artifacts/` and bails out +early if anything is missing. + +Exposed ports: + +| Service | Host URL | +| -------- | --------------------------------------------------- | +| relayer | | +| postgres | postgresql://relayer:relayer@localhost:5433/relayer | +| anvil | (chain id 31337) | +| stellar | (soroban RPC: /soroban/rpc) | + +## Point the frontend at it + +The Next.js app in `apps/frontend` reads the relayer URL from its usual +environment variables. For local dev: + +```text +NEXT_PUBLIC_RELAYER_URL=http://localhost:2005 +``` + +Chain RPCs seeded into the DB point at the in-compose hostnames (`anvil`, +`stellar`), which are unreachable from the browser. The frontend talks +only to the relayer; direct wallet→chain traffic goes through the wallet +provider (wagmi for EVM, stellar-wallets-kit for Stellar) using the host +URLs above. + +## What's inside + +- `docker-compose.yaml` — the stack definition; bind-mounts `.artifacts/` + into the deployer at the paths `deploy.ts` reads +- `Dockerfile.deployer` — slim runtime image (node + pnpm + stellar CLI + for `contract deploy`/`invoke`). No rust/foundry — contracts are + built in CI, not here. +- `entrypoint.sh` — runs inside the deployer: verifies artifacts are + mounted, configures stellar network, generates + friendbot-funds the + admin identity, deploys contracts, runs prisma migrations, seeds the + DB, writes the admin secret to a shared volume +- `up.sh` / `down.sh` — convenience wrappers; `up.sh` fetches the + contracts bundle before invoking compose +- `.artifacts/` (gitignored) — extracted contracts bundle + +## Notes + +- The Stellar admin secret is generated fresh on every full `up --build` + (anvil + stellar are ephemeral), written to a docker volume, and read + by the relayer at start. `down -r` clears it; plain `down` keeps it. +- To refresh artifacts without touching containers: delete `.artifacts/` + and re-run `up.sh`, or set `CONTRACTS_BUNDLE_TAG=contracts-latest` + explicitly to force a download. +- The relayer image reuses `apps/backend-relayer/Dockerfile` — the same + one the CI e2e and production builds use. diff --git a/scripts/docker-local/docker-compose.yaml b/scripts/docker-local/docker-compose.yaml new file mode 100644 index 0000000..0a43eea --- /dev/null +++ b/scripts/docker-local/docker-compose.yaml @@ -0,0 +1,126 @@ +# Self-contained local stack for frontend development. +# +# docker compose -f scripts/docker-local/docker-compose.yaml up --build +# +# Brings up: stellar quickstart + anvil + postgres + one-shot deployer + +# backend-relayer. Relayer is reachable at http://localhost:2005. +# +# For convenience wrappers, see up.sh / down.sh in this directory. + +name: proofbridge-local + +services: + stellar: + image: stellar/quickstart:future + command: ["--local", "--limits", "unlimited"] + ports: + - "8000:8000" + healthcheck: + test: ["CMD-SHELL", "curl -sf -X POST -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"method\":\"getHealth\",\"id\":1}' http://localhost:8000/soroban/rpc | grep -q '\"status\":\"healthy\"'"] + interval: 5s + timeout: 5s + retries: 120 + start_period: 30s + + anvil: + image: ghcr.io/foundry-rs/foundry:stable + # The foundry image's entrypoint is `sh -c`, so the command is one string. + entrypoint: ["anvil"] + command: ["--host", "0.0.0.0", "--port", "8545", "--block-time", "2", "--chain-id", "31337"] + ports: + - "9545:8545" + healthcheck: + test: ["CMD-SHELL", "cast block-number --rpc-url http://localhost:8545 >/dev/null 2>&1"] + interval: 3s + timeout: 3s + retries: 40 + + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: relayer + POSTGRES_PASSWORD: relayer + POSTGRES_DB: relayer + ports: + - "5433:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U relayer -d relayer"] + interval: 2s + timeout: 3s + retries: 30 + + deployer: + build: + context: ../.. + dockerfile: scripts/docker-local/Dockerfile.deployer + depends_on: + stellar: { condition: service_healthy } + anvil: { condition: service_healthy } + postgres: { condition: service_healthy } + environment: + ROOT_DIR: /repo + EVM_RPC_URL: http://anvil:8545 + STELLAR_RPC_URL: http://stellar:8000/soroban/rpc + STELLAR_NETWORK_PASSPHRASE: "Standalone Network ; February 2017" + STELLAR_NETWORK: local + STELLAR_SOURCE_ACCOUNT: admin + # Foundry's prefunded key #0 — matches start_chains.sh. + EVM_ADMIN_PRIVATE_KEY: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + DATABASE_URL: postgresql://relayer:relayer@postgres:5432/relayer + # Optional dev wallet funding — set in .env or export before `up.sh`. + # Empty strings are skipped by the fund step. + DEV_EVM_ADDRESS: ${DEV_EVM_ADDRESS:-} + DEV_STELLAR_ADDRESS: ${DEV_STELLAR_ADDRESS:-} + volumes: + - shared:/shared + # Contract artifacts are pulled by up.sh from the `contracts-latest` + # GitHub Release (or built locally with `up.sh --local`) and extracted + # into ./.artifacts. deploy.ts reads from these paths verbatim. + - ./.artifacts/contracts/stellar/target/wasm32v1-none/release:/repo/contracts/stellar/target/wasm32v1-none/release:ro + - ./.artifacts/contracts/evm/out:/repo/contracts/evm/out:ro + - ./.artifacts/proof_circuits/deposits/target:/repo/proof_circuits/deposits/target:ro + restart: "no" + + backend-relayer: + build: + context: ../.. + dockerfile: apps/backend-relayer/Dockerfile + depends_on: + postgres: { condition: service_healthy } + deployer: { condition: service_completed_successfully } + environment: + NODE_ENV: production + PORT: 2005 + DATABASE_URL: postgresql://relayer:relayer@postgres:5432/relayer + JWT_ACCESS_SECRET: local-access-secret + JWT_REFRESH_SECRET: local-refresh-secret + SIGN_DOMAIN: localhost + SIGN_URI: http://localhost + ETHEREUM_RPC_URL: http://anvil:8545 + STELLAR_RPC_URL: http://stellar:8000/soroban/rpc + STELLAR_NETWORK_PASSPHRASE: "Standalone Network ; February 2017" + STELLAR_AUTH_SECRET: SA3C2KPR5TCHYJ5TNQXAY2776Z3H4CB723GDCAMEX5I2NLWP25QUYB3X + SECRET_KEY: "0xfdba5a242ddce02cd1d585297aa4afe5aa2831391198746c680a3e16a41676dc" + EVM_ADMIN_PRIVATE_KEY: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + # STELLAR_ADMIN_SECRET is generated fresh by the deployer, written to a + # shared volume, and sourced here so the relayer can sign pre-auths. + command: + - "sh" + - "-c" + - | + export STELLAR_ADMIN_SECRET="$(cat /shared/stellar-admin.secret)" + npx prisma migrate deploy + exec node dist/src/main.js + volumes: + - shared:/shared:ro + ports: + - "2005:2005" + healthcheck: + test: ["CMD-SHELL", "node -e \"fetch('http://localhost:2005/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""] + interval: 3s + timeout: 5s + retries: 40 + start_period: 10s + +volumes: + shared: diff --git a/scripts/docker-local/down.sh b/scripts/docker-local/down.sh new file mode 100755 index 0000000..03d63c1 --- /dev/null +++ b/scripts/docker-local/down.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -uo pipefail + +# Tears down the local stack. Pass --reset to also wipe volumes (postgres +# data + shared deploy snapshot), forcing a fresh contract deploy next up. + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +COMPOSE_FILE="$ROOT_DIR/scripts/docker-local/docker-compose.yaml" + +RESET=0 +for arg in "$@"; do + case "$arg" in + --reset|-r) RESET=1 ;; + *) echo "[down.sh] unknown flag: $arg"; exit 2 ;; + esac +done + +if [[ "$RESET" -eq 1 ]]; then + echo "[down.sh] tearing down + wiping volumes…" + docker compose -f "$COMPOSE_FILE" down --remove-orphans --volumes +else + echo "[down.sh] tearing down (volumes preserved — pass --reset to wipe)…" + docker compose -f "$COMPOSE_FILE" down --remove-orphans +fi + +echo "[down.sh] done." diff --git a/scripts/docker-local/entrypoint.sh b/scripts/docker-local/entrypoint.sh new file mode 100755 index 0000000..929abe4 --- /dev/null +++ b/scripts/docker-local/entrypoint.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +set -euo pipefail + +# One-shot deploy + migrate + seed for the docker-local stack. +# +# Expects these env vars from docker-compose: +# EVM_RPC_URL, STELLAR_RPC_URL, STELLAR_NETWORK_PASSPHRASE, +# STELLAR_NETWORK, STELLAR_SOURCE_ACCOUNT, EVM_ADMIN_PRIVATE_KEY, +# DATABASE_URL, ROOT_DIR. +# +# On success, writes: +# /shared/deployed.json — contract addresses for the seed step +# /shared/stellar-admin.secret — secret key consumed by the relayer + +SHARED_DIR="${SHARED_DIR:-/shared}" +SNAPSHOT_PATH="$SHARED_DIR/deployed.json" +ADMIN_SECRET_PATH="$SHARED_DIR/stellar-admin.secret" +mkdir -p "$SHARED_DIR" + +log() { echo "[deployer] $*"; } + +# ── wait for chains ────────────────────────────────────────────────── + +log "waiting for stellar soroban RPC at $STELLAR_RPC_URL…" +for i in $(seq 1 90); do + if curl -sf -X POST -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"getHealth","id":1}' \ + "$STELLAR_RPC_URL" 2>/dev/null | grep -q '"status":"healthy"'; then + break + fi + sleep 2 + [[ $i -eq 90 ]] && { log "stellar RPC never became healthy"; exit 1; } +done +log "stellar RPC healthy." + +log "waiting for anvil at $EVM_RPC_URL…" +for i in $(seq 1 30); do + if cast block-number --rpc-url "$EVM_RPC_URL" >/dev/null 2>&1; then + break + fi + sleep 2 + [[ $i -eq 30 ]] && { log "anvil never responded"; exit 1; } +done +log "anvil up." + +# ── stellar network + keys ─────────────────────────────────────────── + +log "configuring stellar network profile '$STELLAR_NETWORK'…" +stellar network rm "$STELLAR_NETWORK" >/dev/null 2>&1 || true +stellar network add "$STELLAR_NETWORK" \ + --rpc-url "$STELLAR_RPC_URL" \ + --network-passphrase "$STELLAR_NETWORK_PASSPHRASE" +stellar network use "$STELLAR_NETWORK" + +ADMIN_ACCT="$STELLAR_SOURCE_ACCOUNT" +log "generating stellar identity '$ADMIN_ACCT'…" +stellar keys rm "$ADMIN_ACCT" >/dev/null 2>&1 || true +stellar keys generate "$ADMIN_ACCT" --network "$STELLAR_NETWORK" + +log "friendbot-funding '$ADMIN_ACCT'…" +for i in $(seq 1 30); do + if stellar keys fund "$ADMIN_ACCT" --network "$STELLAR_NETWORK" 2>/dev/null; then + break + fi + log " friendbot not ready (try $i/30)…" + sleep 4 + [[ $i -eq 30 ]] && { log "friendbot funding failed"; exit 1; } +done + +ADMIN_SECRET="$(stellar keys show "$ADMIN_ACCT")" +printf '%s' "$ADMIN_SECRET" > "$ADMIN_SECRET_PATH" +log "wrote admin secret → $ADMIN_SECRET_PATH" + +# ── deploy contracts ───────────────────────────────────────────────── +# +# WASMs, EVM artifacts, and the deposit VK are bind-mounted in from +# $ROOT_DIR (populated by up.sh from the contracts-latest GitHub Release, +# or from the repo's locally-built tree via `up.sh --local`). Sanity-check +# their presence before running deploy so missing artifacts fail fast. + +for wasm in verifier merkle_manager ad_manager order_portal; do + path="$ROOT_DIR/contracts/stellar/target/wasm32v1-none/release/${wasm}.wasm" + [[ -f "$path" ]] || { log "missing stellar wasm: $path"; exit 1; } +done +[[ -d "$ROOT_DIR/contracts/evm/out/OrderPortal.sol" ]] \ + || { log "missing EVM artifacts under $ROOT_DIR/contracts/evm/out"; exit 1; } +[[ -f "$ROOT_DIR/proof_circuits/deposits/target/vk" ]] \ + || { log "missing deposit VK at $ROOT_DIR/proof_circuits/deposits/target/vk"; exit 1; } + +log "deploying contracts…" +cd "$ROOT_DIR/scripts/relayer-e2e" +ROOT_DIR="$ROOT_DIR" \ +STELLAR_SOURCE_ACCOUNT="$ADMIN_ACCT" \ +EVM_RPC_URL="$EVM_RPC_URL" \ +EVM_ADMIN_PRIVATE_KEY="$EVM_ADMIN_PRIVATE_KEY" \ + pnpm exec tsx cli.ts deploy --out "$SNAPSHOT_PATH" + +# ── db migrations + seed ───────────────────────────────────────────── + +log "running prisma migrations…" +cd "$ROOT_DIR/apps/backend-relayer" +DATABASE_URL="$DATABASE_URL" npx prisma migrate deploy + +log "seeding database…" +cd "$ROOT_DIR/scripts/relayer-e2e" +DATABASE_URL="$DATABASE_URL" pnpm exec tsx cli.ts seed --in "$SNAPSHOT_PATH" + +# ── optional: fund frontend dev wallets ────────────────────────────── + +if [[ -n "${DEV_EVM_ADDRESS:-}" || -n "${DEV_STELLAR_ADDRESS:-}" ]]; then + log "funding dev wallets (evm=${DEV_EVM_ADDRESS:-} stellar=${DEV_STELLAR_ADDRESS:-})…" + ROOT_DIR="$ROOT_DIR" \ + EVM_RPC_URL="$EVM_RPC_URL" \ + STELLAR_RPC_URL="$STELLAR_RPC_URL" \ + EVM_ADMIN_PRIVATE_KEY="$EVM_ADMIN_PRIVATE_KEY" \ + DEV_EVM_ADDRESS="${DEV_EVM_ADDRESS:-}" \ + DEV_STELLAR_ADDRESS="${DEV_STELLAR_ADDRESS:-}" \ + pnpm exec tsx cli.ts fund --in "$SNAPSHOT_PATH" +fi + +log "done ✓" diff --git a/scripts/docker-local/up.sh b/scripts/docker-local/up.sh new file mode 100755 index 0000000..f3cbc7b --- /dev/null +++ b/scripts/docker-local/up.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Brings up the full local stack: chains + postgres + deployer + relayer. +# Relayer is exposed at http://localhost:2005 on success. +# +# Contract artifacts (Stellar WASMs, EVM ABIs, deposit VK) are pulled from +# the public `contracts-latest` GitHub Release by default — no host +# toolchain required. Override with: +# CONTRACTS_BUNDLE_TAG=contracts- pin a specific build +# up.sh --local use the repo's locally-built tree +# (skips download; assumes you ran +# `stellar contract build`, `forge +# build`, and `scripts/build_circuits.sh` +# yourself) + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yaml" +ARTIFACTS_DIR="$SCRIPT_DIR/.artifacts" + +# Compose auto-loads .env for container env; source it here too so up.sh +# itself (e.g. CONTRACTS_BUNDLE_TAG) sees the same values. +if [[ -f "$SCRIPT_DIR/.env" ]]; then + set -a + # shellcheck disable=SC1091 + source "$SCRIPT_DIR/.env" + set +a +fi + +GH_REPO="${CONTRACTS_BUNDLE_REPO:-Explore-Beyond-Innovations/ProofBridge}" +BUNDLE_TAG="${CONTRACTS_BUNDLE_TAG:-contracts-latest}" + +USE_LOCAL=0 +for arg in "$@"; do + case "$arg" in + --local) USE_LOCAL=1 ;; + -h|--help) + sed -n '3,16p' "$0" + exit 0 + ;; + *) echo "[up.sh] unknown flag: $arg"; exit 2 ;; + esac +done + +have_artifact() { + [[ -f "$ARTIFACTS_DIR/contracts/stellar/target/wasm32v1-none/release/order_portal.wasm" \ + && -d "$ARTIFACTS_DIR/contracts/evm/out/OrderPortal.sol" \ + && -f "$ARTIFACTS_DIR/proof_circuits/deposits/target/vk" ]] +} + +sync_from_local_tree() { + echo "[up.sh] --local: bind-mounting locally-built artifacts…" + local stellar_wasm="$ROOT_DIR/contracts/stellar/target/wasm32v1-none/release" + local evm_out="$ROOT_DIR/contracts/evm/out" + local vk="$ROOT_DIR/proof_circuits/deposits/target/vk" + + for f in verifier.wasm merkle_manager.wasm ad_manager.wasm order_portal.wasm; do + if [[ ! -f "$stellar_wasm/$f" ]]; then + echo "[up.sh] missing $stellar_wasm/$f — run 'stellar contract build' first" >&2 + exit 1 + fi + done + for c in OrderPortal AdManager MerkleManager Verifier wNativeToken ERC20Mock; do + if [[ ! -d "$evm_out/${c}.sol" ]]; then + echo "[up.sh] missing $evm_out/${c}.sol — run 'forge build' in contracts/evm first" >&2 + exit 1 + fi + done + if [[ ! -f "$vk" ]]; then + echo "[up.sh] missing $vk — run scripts/build_circuits.sh proof_circuits/deposits first" >&2 + exit 1 + fi + + rm -rf "$ARTIFACTS_DIR" + mkdir -p "$ARTIFACTS_DIR/contracts/stellar/target/wasm32v1-none/release" \ + "$ARTIFACTS_DIR/contracts/evm/out" \ + "$ARTIFACTS_DIR/proof_circuits/deposits/target" + cp "$stellar_wasm"/*.wasm "$ARTIFACTS_DIR/contracts/stellar/target/wasm32v1-none/release/" + cp -r "$evm_out"/. "$ARTIFACTS_DIR/contracts/evm/out/" + cp "$vk" "$ARTIFACTS_DIR/proof_circuits/deposits/target/vk" +} + +download_bundle() { + local url="https://github.com/${GH_REPO}/releases/download/${BUNDLE_TAG}/contracts-bundle.tar.gz" + local tmp + tmp="$(mktemp -d)" + trap 'rm -rf "$tmp"' RETURN + + echo "[up.sh] downloading $BUNDLE_TAG from $GH_REPO…" + if ! curl -fsSL "$url" -o "$tmp/bundle.tgz"; then + echo "[up.sh] failed to fetch $url" >&2 + echo "[up.sh] (is the tag published? try \`CONTRACTS_BUNDLE_TAG=contracts-latest\` or \`--local\`)" >&2 + exit 1 + fi + + rm -rf "$ARTIFACTS_DIR" + mkdir -p "$ARTIFACTS_DIR" + tar -xzf "$tmp/bundle.tgz" -C "$ARTIFACTS_DIR" + echo "[up.sh] extracted bundle → $ARTIFACTS_DIR" +} + +if [[ $USE_LOCAL -eq 1 ]]; then + sync_from_local_tree +else + if [[ -n "${CONTRACTS_BUNDLE_TAG:-}" ]] || ! have_artifact; then + download_bundle + else + echo "[up.sh] reusing cached artifacts in $ARTIFACTS_DIR (set CONTRACTS_BUNDLE_TAG to force refresh)" + fi +fi + +if ! have_artifact; then + echo "[up.sh] artifact layout incomplete under $ARTIFACTS_DIR" >&2 + exit 1 +fi + +echo "[up.sh] building + starting services…" +docker compose -f "$COMPOSE_FILE" up -d --build --wait + +echo "" +echo "[up.sh] stack is up ✓" +echo " relayer: http://localhost:2005" +echo " postgres: postgresql://relayer:relayer@localhost:5433/relayer" +echo " anvil: http://localhost:9545" +echo " stellar: http://localhost:8000 (soroban RPC: /soroban/rpc)" +echo "" +echo "[up.sh] follow logs with:" +echo " docker compose -f $COMPOSE_FILE logs -f backend-relayer" +echo "[up.sh] tear down with:" +echo " bash scripts/docker-local/down.sh" diff --git a/scripts/relayer-e2e/cli.ts b/scripts/relayer-e2e/cli.ts index 1887df5..57619a3 100644 --- a/scripts/relayer-e2e/cli.ts +++ b/scripts/relayer-e2e/cli.ts @@ -3,11 +3,13 @@ // Usage: // tsx cli.ts deploy [--out ] // tsx cli.ts seed [--in ] +// tsx cli.ts fund [--in ] // tsx cli.ts flows // tsx cli.ts all [--out ] // // `deploy` brings up the on-chain state and writes a JSON snapshot. // `seed` feeds that snapshot into Postgres via Prisma. +// `fund` tops up DEV_EVM_ADDRESS / DEV_STELLAR_ADDRESS on the local chains. // `flows` drives the relayer over HTTP through the ad + trade lifecycles. // `all` runs every step in-process; intended for local dev. In CI, the shell // orchestrator in `e2e.sh` calls the subcommands individually so Docker can @@ -17,6 +19,7 @@ import * as fs from "fs"; import * as path from "path"; import { deploy } from "./lib/deploy.js"; import { seedDb, type DeployedContracts } from "./lib/seed.js"; +import { fundDevWallets } from "./lib/fund.js"; import { runAdLifecycle } from "./flows/ad-lifecycle.js"; import { runTradeLifecycle } from "./flows/trade-lifecycle.js"; @@ -46,6 +49,23 @@ async function cmdSeed(argv: string[]): Promise { await seedDb(snapshot); } +async function cmdFund(argv: string[]): Promise { + const inPath = parseFlag(argv, "--in") ?? defaultSnapshotPath(); + const snapshot = readSnapshot(inPath); + await fundDevWallets(snapshot, { + devEvmAddress: process.env.DEV_EVM_ADDRESS, + devStellarAddress: process.env.DEV_STELLAR_ADDRESS, + evmRpcUrl: requireEnv("EVM_RPC_URL"), + stellarRpcUrl: requireEnv("STELLAR_RPC_URL"), + }); +} + +function requireEnv(name: string): string { + const v = process.env[name]; + if (!v) throw new Error(`${name} must be set`); + return v; +} + async function cmdFlows(): Promise { await runAdLifecycle(); await runTradeLifecycle(); @@ -68,6 +88,9 @@ async function main(): Promise { case "seed": await cmdSeed(rest); return; + case "fund": + await cmdFund(rest); + return; case "flows": await cmdFlows(); return; diff --git a/scripts/relayer-e2e/lib/fund.ts b/scripts/relayer-e2e/lib/fund.ts new file mode 100644 index 0000000..146197e --- /dev/null +++ b/scripts/relayer-e2e/lib/fund.ts @@ -0,0 +1,116 @@ +// Funds the frontend developer's dev wallets against the local docker stack. +// +// EVM: sets native balance via `anvil_setBalance`, then mints ERC20Mock. +// Stellar: friendbot-funds the G-address against the local quickstart. +// +// Addresses are read from DEV_EVM_ADDRESS / DEV_STELLAR_ADDRESS. Any field +// left unset is skipped with a warning — funding is entirely best-effort. + +import { ethers } from "ethers"; +import type { DeployedContracts } from "./seed.js"; + +const DEFAULT_NATIVE_WEI = 10n ** 20n; // 100 ETH +const DEFAULT_TOKEN_UNITS = 1_000_000n; // 1,000,000 TT (pre-decimals) + +export interface FundOpts { + devEvmAddress?: string; + devStellarAddress?: string; + evmRpcUrl: string; + stellarRpcUrl: string; + /** Amount of native token units (already applied with 10^18). Defaults to 100 ETH. */ + nativeWei?: bigint; + /** Human-readable token amount; converted using the snapshot decimals. Defaults to 1,000,000. */ + tokenAmount?: bigint; +} + +export async function fundDevWallets( + snapshot: DeployedContracts, + opts: FundOpts, +): Promise { + await fundEvm(snapshot, opts); + await fundStellar(snapshot, opts); +} + +async function fundEvm( + snapshot: DeployedContracts, + opts: FundOpts, +): Promise { + const addr = opts.devEvmAddress?.trim(); + if (!addr) { + console.log("[fund] DEV_EVM_ADDRESS not set — skipping EVM funding."); + return; + } + if (!ethers.isAddress(addr)) { + throw new Error(`[fund] DEV_EVM_ADDRESS is not a valid 0x address: ${addr}`); + } + + const provider = new ethers.JsonRpcProvider(opts.evmRpcUrl); + + const nativeWei = opts.nativeWei ?? DEFAULT_NATIVE_WEI; + console.log( + `[fund] EVM: setting native balance of ${addr} to ${ethers.formatEther(nativeWei)} ETH`, + ); + await provider.send("anvil_setBalance", [ + addr, + "0x" + nativeWei.toString(16), + ]); + + const tokenAddr = snapshot.eth.tokenAddress; + const decimals = snapshot.eth.tokenDecimals ?? 18; + const tokenAmount = (opts.tokenAmount ?? DEFAULT_TOKEN_UNITS) * 10n ** BigInt(decimals); + + const pk = requireEnv("EVM_ADMIN_PRIVATE_KEY"); + const signer = new ethers.Wallet(pk, provider); + const token = new ethers.Contract( + tokenAddr, + ["function mint(address to, uint256 amount) external"], + signer, + ); + console.log( + `[fund] EVM: minting ${tokenAmount} units of ${snapshot.eth.tokenSymbol} (${tokenAddr}) to ${addr}`, + ); + const tx = await token.getFunction("mint")(addr, tokenAmount); + await tx.wait(); +} + +async function fundStellar( + _snapshot: DeployedContracts, + opts: FundOpts, +): Promise { + const addr = opts.devStellarAddress?.trim(); + if (!addr) { + console.log("[fund] DEV_STELLAR_ADDRESS not set — skipping Stellar funding."); + return; + } + if (!/^G[A-Z2-7]{55}$/.test(addr)) { + throw new Error( + `[fund] DEV_STELLAR_ADDRESS is not a valid G-strkey: ${addr}`, + ); + } + + // STELLAR_RPC_URL looks like http://stellar:8000/soroban/rpc — friendbot + // is served off the same host at /friendbot. + const base = new URL(opts.stellarRpcUrl); + const friendbotUrl = `${base.protocol}//${base.host}/friendbot?addr=${encodeURIComponent(addr)}`; + + console.log(`[fund] Stellar: friendbot-funding ${addr}`); + const res = await fetch(friendbotUrl); + if (!res.ok) { + const body = await res.text().catch(() => ""); + // Friendbot returns 400 when the account is already funded — treat that + // as success so reruns are idempotent. + if (res.status === 400 && /op_already_exists|createAccountAlreadyExist/i.test(body)) { + console.log(`[fund] Stellar: account already funded, skipping.`); + return; + } + throw new Error( + `[fund] friendbot failed (${res.status}): ${body.slice(0, 200)}`, + ); + } +} + +function requireEnv(name: string): string { + const v = process.env[name]; + if (!v) throw new Error(`[fund] ${name} must be set`); + return v; +}