diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c35b414..4fdd924 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,3 +82,43 @@ jobs: - name: Run tests run: npm test + + # ─── Indexer CI ───────────────────────────────────────────────────────── + indexer: + name: Indexer (TypeScript + Prisma) + runs-on: ubuntu-latest + defaults: + run: + working-directory: indexer + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: indexer/package-lock.json + + - name: Install dependencies + run: npm ci + + # Cache the generated Prisma client so it is not rebuilt unless the + # schema changes. The cache key includes the schema hash so any model + # change automatically invalidates it. + - name: Cache Prisma generated client + id: prisma-cache + uses: actions/cache@v4 + with: + path: indexer/node_modules/.prisma + key: prisma-${{ runner.os }}-${{ hashFiles('indexer/prisma/schema.prisma') }} + restore-keys: | + prisma-${{ runner.os }}- + + - name: Generate Prisma client + if: steps.prisma-cache.outputs.cache-hit != 'true' + run: npm run prisma:generate + + - name: Build + run: npm run build diff --git a/.github/workflows/deploy-react.yml b/.github/workflows/deploy-react.yml new file mode 100644 index 0000000..cc21c49 --- /dev/null +++ b/.github/workflows/deploy-react.yml @@ -0,0 +1,168 @@ +# .github/workflows/deploy-react.yml +# +# Vercel Deployment — bc-forge React App (Issue #336) +# +# Triggers: +# • push to main → production deployment +# • pull_request → preview deployment (URL posted as PR comment) +# +# Required repository secrets (Settings → Secrets and variables → Actions): +# VERCEL_TOKEN – Vercel personal access token +# VERCEL_ORG_ID – Vercel organisation / team ID +# VERCEL_PROJECT_ID – Vercel project ID (from .vercel/project.json) + +name: Deploy React App to Vercel + +on: + push: + branches: [main] + paths: + - 'react/**' + - '.github/workflows/deploy-react.yml' + pull_request: + branches: [main] + paths: + - 'react/**' + +jobs: + # ─── Build & Test ────────────────────────────────────────────────────────── + build: + name: Build & Test React App + runs-on: ubuntu-latest + defaults: + run: + working-directory: react + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: react/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + - name: Build + run: npm run build + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: react-dist + path: react/dist + retention-days: 1 + + # ─── Vercel Preview (PRs) ────────────────────────────────────────────────── + deploy-preview: + name: Vercel Preview Deployment + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'pull_request' + environment: + name: preview + url: ${{ steps.deploy.outputs.preview-url }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: react/package-lock.json + + - name: Install Vercel CLI + run: npm install --global vercel@latest + + - name: Pull Vercel environment + run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} + working-directory: react + env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + + - name: Build for Vercel (preview) + run: vercel build --token=${{ secrets.VERCEL_TOKEN }} + working-directory: react + env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + + - name: Deploy to Vercel (preview) + id: deploy + run: | + PREVIEW_URL=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}) + echo "preview-url=$PREVIEW_URL" >> "$GITHUB_OUTPUT" + working-directory: react + env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + + - name: Comment preview URL on PR + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `## ✅ Vercel Preview Deployment\n\n🔗 **Preview URL:** ${{ steps.deploy.outputs.preview-url }}\n\n> Deployed from commit \`${{ github.sha }}\`` + }) + + # ─── Vercel Production (main) ────────────────────────────────────────────── + deploy-production: + name: Vercel Production Deployment + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + environment: + name: production + url: ${{ steps.deploy.outputs.production-url }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: react/package-lock.json + + - name: Install Vercel CLI + run: npm install --global vercel@latest + + - name: Pull Vercel environment + run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} + working-directory: react + env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + + - name: Build for Vercel (production) + run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} + working-directory: react + env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + + - name: Deploy to Vercel (production) + id: deploy + run: | + PROD_URL=$(vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}) + echo "production-url=$PROD_URL" >> "$GITHUB_OUTPUT" + working-directory: react + env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} diff --git a/deployments/deploy-contracts-testnet.md b/deployments/deploy-contracts-testnet.md new file mode 100644 index 0000000..83de033 --- /dev/null +++ b/deployments/deploy-contracts-testnet.md @@ -0,0 +1,332 @@ +# bc-forge — Contract Deployment Guide (Stellar Testnet) + +> **Issue #335** — Deployment Automation Script +> Covers every step required to build, deploy, initialise, and smoke-test +> the `bc-forge-token` and `bc-forge-wrapper` Soroban smart contracts on +> the Stellar Testnet. + +--- + +## Table of Contents + +1. [Prerequisites](#1-prerequisites) +2. [Quick Start (automated script)](#2-quick-start-automated-script) +3. [Manual Step-by-Step](#3-manual-step-by-step) + - [3.1 Build WASM](#31-build-wasm) + - [3.2 Generate / Fund Identity](#32-generate--fund-identity) + - [3.3 Deploy Token Contract](#33-deploy-token-contract) + - [3.4 Initialise Token Contract](#34-initialise-token-contract) + - [3.5 Deploy Wrapper Contract](#35-deploy-wrapper-contract) + - [3.6 Initialise Wrapper Contract](#36-initialise-wrapper-contract) + - [3.7 Verify Deployment](#37-verify-deployment) + - [3.8 Test Basic Invocation — Wrap / Unwrap](#38-test-basic-invocation--wrap--unwrap) +4. [Deployed Contract IDs](#4-deployed-contract-ids) +5. [Environment Variables Reference](#5-environment-variables-reference) +6. [Troubleshooting](#6-troubleshooting) + +--- + +## 1. Prerequisites + +| Tool | Version | Install | +|------|---------|---------| +| Rust | stable | `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \| sh` | +| wasm32 target | — | `rustup target add wasm32-unknown-unknown` | +| Soroban CLI | v22+ | `cargo install --locked soroban-cli` | +| curl | any | system package manager | + +Verify the Soroban CLI is available: + +```bash +soroban --version +# soroban 22.x.x +``` + +--- + +## 2. Quick Start (automated script) + +The repository ships a fully-automated script that handles every step below. + +```bash +# Option A — pass secret key directly +./deployments/deploy-contracts-testnet.sh S + +# Option B — via environment variable (preferred for CI) +export ADMIN_SEED=S +./deployments/deploy-contracts-testnet.sh +``` + +The script will: +1. Build all contracts to WASM +2. Deploy `bc-forge-token` → initialize it +3. Deploy `bc-forge-wrapper` → initialize it (pointing at the token) +4. Run invocation smoke-tests against both contracts +5. Write a JSON manifest to `deployments/contracts-testnet.json` + +--- + +## 3. Manual Step-by-Step + +### 3.1 Build WASM + +```bash +cargo build --target wasm32-unknown-unknown --release +``` + +Binaries are emitted to: + +``` +target/wasm32-unknown-unknown/release/bc_forge_token.wasm +target/wasm32-unknown-unknown/release/bc_forge_wrapper.wasm +``` + +Check sizes: + +```bash +ls -lh target/wasm32-unknown-unknown/release/bc_forge_*.wasm +``` + +--- + +### 3.2 Generate / Fund Identity + +```bash +# Generate a named key (skip if you already have one) +soroban keys generate --global bc-forge-admin + +# Show the public key +soroban keys address bc-forge-admin + +# Fund via Friendbot +curl "https://friendbot.stellar.org?addr=$(soroban keys address bc-forge-admin)" +``` + +--- + +### 3.3 Deploy Token Contract + +```bash +TOKEN_ID=$(soroban contract deploy \ + --wasm target/wasm32-unknown-unknown/release/bc_forge_token.wasm \ + --source bc-forge-admin \ + --rpc-url https://soroban-testnet.stellar.org \ + --network-passphrase "Test SDF Network ; September 2015" \ + --fee 100) + +echo "Token Contract ID: $TOKEN_ID" +``` + +--- + +### 3.4 Initialise Token Contract + +```bash +soroban contract invoke \ + --id "$TOKEN_ID" \ + --source bc-forge-admin \ + --rpc-url https://soroban-testnet.stellar.org \ + --network-passphrase "Test SDF Network ; September 2015" \ + --fee 100 \ + -- \ + initialize \ + --admin $(soroban keys address bc-forge-admin) \ + --decimal 7 \ + --name "BC Forge Token" \ + --symbol "BCF" +``` + +--- + +### 3.5 Deploy Wrapper Contract + +```bash +WRAPPER_ID=$(soroban contract deploy \ + --wasm target/wasm32-unknown-unknown/release/bc_forge_wrapper.wasm \ + --source bc-forge-admin \ + --rpc-url https://soroban-testnet.stellar.org \ + --network-passphrase "Test SDF Network ; September 2015" \ + --fee 100) + +echo "Wrapper Contract ID: $WRAPPER_ID" +``` + +--- + +### 3.6 Initialise Wrapper Contract + +```bash +soroban contract invoke \ + --id "$WRAPPER_ID" \ + --source bc-forge-admin \ + --rpc-url https://soroban-testnet.stellar.org \ + --network-passphrase "Test SDF Network ; September 2015" \ + --fee 100 \ + -- \ + initialize \ + --admin $(soroban keys address bc-forge-admin) \ + --token_contract_id "$TOKEN_ID" \ + --decimal 7 \ + --name "Wrapped BCF" \ + --symbol "wBCF" +``` + +--- + +### 3.7 Verify Deployment + +**Token contract:** + +```bash +soroban contract invoke \ + --id "$TOKEN_ID" \ + --rpc-url https://soroban-testnet.stellar.org \ + --network-passphrase "Test SDF Network ; September 2015" \ + -- name +# Expected: "BC Forge Token" + +soroban contract invoke \ + --id "$TOKEN_ID" \ + --rpc-url https://soroban-testnet.stellar.org \ + --network-passphrase "Test SDF Network ; September 2015" \ + -- symbol +# Expected: "BCF" + +soroban contract invoke \ + --id "$TOKEN_ID" \ + --rpc-url https://soroban-testnet.stellar.org \ + --network-passphrase "Test SDF Network ; September 2015" \ + -- decimals +# Expected: 7 +``` + +**Wrapper contract:** + +```bash +soroban contract invoke \ + --id "$WRAPPER_ID" \ + --rpc-url https://soroban-testnet.stellar.org \ + --network-passphrase "Test SDF Network ; September 2015" \ + -- version +# Expected: "1.0.0" + +soroban contract invoke \ + --id "$WRAPPER_ID" \ + --rpc-url https://soroban-testnet.stellar.org \ + --network-passphrase "Test SDF Network ; September 2015" \ + -- supply +# Expected: 0 +``` + +--- + +### 3.8 Test Basic Invocation — Wrap / Unwrap + +> Replace `` and `` with a funded test account. + +**Mint underlying tokens to user:** + +```bash +soroban contract invoke \ + --id "$TOKEN_ID" \ + --source bc-forge-admin \ + --rpc-url https://soroban-testnet.stellar.org \ + --network-passphrase "Test SDF Network ; September 2015" \ + -- mint \ + --to \ + --amount 10000000 +``` + +**Approve wrapper to spend user's tokens:** + +```bash +soroban contract invoke \ + --id "$TOKEN_ID" \ + --source \ + --rpc-url https://soroban-testnet.stellar.org \ + --network-passphrase "Test SDF Network ; September 2015" \ + -- approve \ + --from \ + --spender "$WRAPPER_ID" \ + --amount 10000000 \ + --expiration_ledger 4294967295 +``` + +**Wrap 5 BCF:** + +```bash +soroban contract invoke \ + --id "$WRAPPER_ID" \ + --source \ + --rpc-url https://soroban-testnet.stellar.org \ + --network-passphrase "Test SDF Network ; September 2015" \ + -- wrap \ + --caller \ + --amount 5000000 +``` + +**Check wrapped balance:** + +```bash +soroban contract invoke \ + --id "$WRAPPER_ID" \ + --rpc-url https://soroban-testnet.stellar.org \ + --network-passphrase "Test SDF Network ; September 2015" \ + -- balance \ + --id +# Expected: 5000000 +``` + +**Unwrap 2 wBCF:** + +```bash +soroban contract invoke \ + --id "$WRAPPER_ID" \ + --source \ + --rpc-url https://soroban-testnet.stellar.org \ + --network-passphrase "Test SDF Network ; September 2015" \ + -- unwrap \ + --caller \ + --wrapped_amount 2000000 +``` + +--- + +## 4. Deployed Contract IDs + +> **Note:** Run `deploy-contracts-testnet.sh` to populate the live values below. +> The script writes a machine-readable version to `deployments/contracts-testnet.json`. + +| Contract | Testnet Contract ID | +|----------|---------------------| +| `bc-forge-token` | *(see `deployments/contracts-testnet.json` after deployment)* | +| `bc-forge-wrapper` | *(see `deployments/contracts-testnet.json` after deployment)* | + +**Network:** Stellar Testnet (`Test SDF Network ; September 2015`) +**RPC:** `https://soroban-testnet.stellar.org` + +--- + +## 5. Environment Variables Reference + +| Variable | Default | Description | +|----------|---------|-------------| +| `ADMIN_SEED` | — | Admin Stellar secret key (`S…`) | +| `RPC_URL` | `https://soroban-testnet.stellar.org` | Soroban RPC endpoint | +| `NETWORK_PASSPHRASE` | `Test SDF Network ; September 2015` | Network passphrase | + +--- + +## 6. Troubleshooting + +| Error | Cause | Fix | +|-------|-------|-----| +| `HostError: Error(Contract, #2)` | Contract not initialised | Call `initialize` first | +| `HostError: Error(Contract, #3)` | Invalid amount (≤ 0) | Check amount values | +| `HostError: Error(Contract, #4)` | Insufficient balance | Ensure the caller holds enough wrapped tokens | +| `HostError: Error(Contract, #5)` | Insufficient allowance | Call `approve` with the correct amount before wrapping | +| `HostError: Error(Contract, #6)` | Contract is paused | Call `unpause` (admin only) | +| `HostError: Error(Contract, #7)` | Reentrant call | Should not occur in normal usage | +| `HostError: Error(Contract, #8)` | Underlying token call failed | Verify the token contract is initialised and healthy | +| `account not found` | Account not funded | Run Friendbot: `curl "https://friendbot.stellar.org?addr="` | +| `wasm file not found` | Build not run | `cargo build --target wasm32-unknown-unknown --release` | diff --git a/deployments/deploy-contracts-testnet.sh b/deployments/deploy-contracts-testnet.sh new file mode 100755 index 0000000..dd45dd6 --- /dev/null +++ b/deployments/deploy-contracts-testnet.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash +# ============================================================================= +# deploy-contracts-testnet.sh — bc-forge full-suite deployment (Issue #335) +# +# Builds all Soroban contracts to WASM, deploys them to the Stellar Testnet +# in dependency order, runs basic invocation smoke-tests, and writes a JSON +# deployment manifest to deployments/contracts-testnet.json. +# +# Usage: +# ./deployments/deploy-contracts-testnet.sh [ADMIN_SECRET_KEY] +# export ADMIN_SEED= && ./deployments/deploy-contracts-testnet.sh +# +# Prerequisites: +# - soroban CLI (v22+) : https://soroban.stellar.org/docs/getting-started/setup +# - Rust + wasm32 target : rustup target add wasm32-unknown-unknown +# - Funded testnet account (use Friendbot if needed) +# ============================================================================= + +set -euo pipefail + +# ── Colour helpers ──────────────────────────────────────────────────────────── +GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m' +info() { echo -e "${GREEN}[INFO]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +section() { echo -e "\n${YELLOW}══════════════════════════════════════════${NC}"; echo -e "${YELLOW} $*${NC}"; echo -e "${YELLOW}══════════════════════════════════════════${NC}"; } + +# ── Config ──────────────────────────────────────────────────────────────────── +ADMIN_SEED="${1:-${ADMIN_SEED:-}}" +if [ -z "$ADMIN_SEED" ]; then + echo -e "${RED}[ERROR]${NC} No admin secret key provided." + echo " Usage: $0 " + echo " or: export ADMIN_SEED= && $0" + exit 1 +fi + +RPC_URL="${RPC_URL:-https://soroban-testnet.stellar.org}" +NETWORK_PASSPHRASE="${NETWORK_PASSPHRASE:-Test SDF Network ; September 2015}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +MANIFEST="$SCRIPT_DIR/contracts-testnet.json" +DEPLOYED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + +cd "$PROJECT_DIR" + +# ── Helper: deploy a single contract ───────────────────────────────────────── +deploy_contract() { + local pkg="$1" # Cargo package name (e.g. bc-forge-token) + local wasm_name="$2" # WASM file base name (e.g. bc_forge_token) + + local wasm_path="target/wasm32-unknown-unknown/release/${wasm_name}.wasm" + + info "Deploying ${pkg} …" + local contract_id + contract_id=$(soroban contract deploy \ + --wasm "$wasm_path" \ + --source-account "$ADMIN_SEED" \ + --rpc-url "$RPC_URL" \ + --network-passphrase "$NETWORK_PASSPHRASE" \ + --fee 100) + info "${pkg} → ${contract_id}" + echo "$contract_id" +} + +# ── Helper: invoke a contract fn and display result ─────────────────────────── +invoke_fn() { + local contract_id="$1" + local fn_name="$2" + shift 2 + + info "Invoking ${fn_name} on ${contract_id:0:12}…" + soroban contract invoke \ + --id "$contract_id" \ + --rpc-url "$RPC_URL" \ + --network-passphrase "$NETWORK_PASSPHRASE" \ + -- \ + "$fn_name" "$@" +} + +# ═════════════════════════════════════════════════════════════════════════════ +section "Step 1 — Build all contracts (WASM)" +# ═════════════════════════════════════════════════════════════════════════════ +cargo build --target wasm32-unknown-unknown --release +info "Build complete." + +# ═════════════════════════════════════════════════════════════════════════════ +section "Step 2 — Deploy token contract" +# ═════════════════════════════════════════════════════════════════════════════ +TOKEN_ID=$(deploy_contract "bc-forge-token" "bc_forge_token") + +section "Step 3 — Initialize token contract" +soroban contract invoke \ + --id "$TOKEN_ID" \ + --source-account "$ADMIN_SEED" \ + --rpc-url "$RPC_URL" \ + --network-passphrase "$NETWORK_PASSPHRASE" \ + --fee 100 \ + -- \ + initialize \ + --admin "$(soroban keys address "$ADMIN_SEED" 2>/dev/null || echo "$ADMIN_SEED")" \ + --decimal 7 \ + --name "BC Forge Token" \ + --symbol "BCF" +info "Token contract initialised." + +# ═════════════════════════════════════════════════════════════════════════════ +section "Step 4 — Deploy wrapper contract" +# ═════════════════════════════════════════════════════════════════════════════ +WRAPPER_ID=$(deploy_contract "bc-forge-wrapper" "bc_forge_wrapper") + +section "Step 5 — Initialize wrapper contract" +soroban contract invoke \ + --id "$WRAPPER_ID" \ + --source-account "$ADMIN_SEED" \ + --rpc-url "$RPC_URL" \ + --network-passphrase "$NETWORK_PASSPHRASE" \ + --fee 100 \ + -- \ + initialize \ + --admin "$(soroban keys address "$ADMIN_SEED" 2>/dev/null || echo "$ADMIN_SEED")" \ + --token_contract_id "$TOKEN_ID" \ + --decimal 7 \ + --name "Wrapped BCF" \ + --symbol "wBCF" +info "Wrapper contract initialised." + +# ═════════════════════════════════════════════════════════════════════════════ +section "Step 6 — Smoke-test invocations" +# ═════════════════════════════════════════════════════════════════════════════ +info "--- Token Contract ---" +invoke_fn "$TOKEN_ID" "name" +invoke_fn "$TOKEN_ID" "symbol" +invoke_fn "$TOKEN_ID" "decimals" + +info "--- Wrapper Contract ---" +invoke_fn "$WRAPPER_ID" "version" +invoke_fn "$WRAPPER_ID" "name" +invoke_fn "$WRAPPER_ID" "symbol" +invoke_fn "$WRAPPER_ID" "supply" + +# ═════════════════════════════════════════════════════════════════════════════ +section "Step 7 — Write deployment manifest" +# ═════════════════════════════════════════════════════════════════════════════ +cat > "$MANIFEST" <