From a693847fca36077e0edc3f282ced1f80ce8d3057 Mon Sep 17 00:00:00 2001 From: maxx-GENIUS <340030598+maxx-GENIUS@users.noreply.github.com> Date: Mon, 26 Oct 2026 13:37:08 +0545 Subject: [PATCH] docs(contracts): add end-to-end local sandbox guide and make target Add `contracts/LOCAL.md` and a root `make sandbox` target with automated script `contracts/scripts/sandbox.sh` to spin up a standalone Stellar Quickstart Docker container, configure local CLI networks, fund test identities, build and deploy `payment_escrow`, execute sample `create` and `release` invocations, and print copy-pasteable `.env` lines for the backend Soroban configuration. Closes #2027 --- Makefile | 6 +- contracts/CHANGELOG.md | 1 + contracts/LOCAL.md | 278 +++++++++++++++++++++++++++++++++++ contracts/README.md | 3 +- contracts/scripts/sandbox.sh | 218 +++++++++++++++++++++++++++ 5 files changed, 504 insertions(+), 2 deletions(-) create mode 100644 contracts/LOCAL.md create mode 100755 contracts/scripts/sandbox.sh diff --git a/Makefile b/Makefile index fe8e5f7b..2ca162d8 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ CONTRACTS_DIR := contracts NETWORK ?= testnet SOURCE ?= -.PHONY: help build test deploy clean audit +.PHONY: help build test deploy clean audit dev sandbox help: @echo "Targets:" @@ -15,6 +15,7 @@ help: @echo " make test - cargo test for the contracts workspace" @echo " make deploy - build, optimise, deploy and initialise the contracts with the stellar CLI" @echo " (NETWORK= default testnet, SOURCE= required)" + @echo " make sandbox - run the local standalone Stellar sandbox (Docker + deploy + create/release + .env)" @echo " make clean - cargo clean for the contracts workspace" @echo " make audit - run the CT-72 require_auth() coverage audit" @@ -34,5 +35,8 @@ clean: audit: bash scripts/audit_require_auth.sh +sandbox: + bash $(CONTRACTS_DIR)/scripts/sandbox.sh $(ARGS) + dev: docker compose up --build diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index 6a6dd731..e9977f3e 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -10,6 +10,7 @@ backend and indexer integrators can track compatibility across releases (#1877). lockfile so CI can build the workspace with current transitive dependencies. ### Added +- End-to-end local sandbox (Docker standalone node, deployment, create/release invocations, backend `.env` generation) and `contracts/LOCAL.md` documentation (#2027). - `payment_escrow::get_escrow(id)` read-only view function, so integrators no longer need a state-changing call to read escrow data (#1878). - End-to-end create -> release walkthrough in `contracts/RUNBOOK.md` (#1879). diff --git a/contracts/LOCAL.md b/contracts/LOCAL.md new file mode 100644 index 00000000..98de1ab7 --- /dev/null +++ b/contracts/LOCAL.md @@ -0,0 +1,278 @@ +# Local Soroban Contracts Sandbox + +This document details how to run, deploy, and interact with the ManageHub Soroban smart contracts locally against a standalone Stellar Quickstart node, eliminating reliance on public testnet rate limits, faucet availability, and network latency. + +--- + +## Overview + +The local sandbox environment provisions: +- A standalone Stellar network running inside Docker (`stellar/quickstart:testing`). +- A local Horizon instance (`http://localhost:8000`) and Soroban RPC endpoint (`http://localhost:8000/soroban/rpc`). +- Local test keypairs funded via local Friendbot. +- A compiled and deployed instance of the `payment_escrow` smart contract. +- Verified `create` and `release` lifecycle invocations. +- Copy-pasteable environment configuration for the NestJS backend (`backend/.env`). + +--- + +## Network Architecture & Endpoints + +| Component | Endpoint / Value | +|---|---| +| **Horizon API** | `http://localhost:8000` | +| **Soroban RPC** | `http://localhost:8000/soroban/rpc` | +| **Friendbot Faucet** | `http://localhost:8000/friendbot` | +| **Network Passphrase** | `Standalone Network ; February 2017` | +| **CLI Network Name** | `standalone` | + +--- + +## Prerequisites + +1. **Docker Engine / Docker Desktop** (running and accessible by current user). +2. **Rust Toolchain**: Rust 1.98.1 with the `wasm32-unknown-unknown` target: + ```bash + rustup target add wasm32-unknown-unknown + ``` +3. **Stellar CLI**: + ```bash + cargo install --locked stellar-cli + ``` + Verify installation: + ```bash + stellar --version + ``` + +--- + +## Quickstart (Automated Sandbox) + +You can launch the entire local sandbox with a single command from the repository root: + +```bash +make sandbox +``` + +Or execute the script directly: +```bash +bash contracts/scripts/sandbox.sh +``` + +### What this does: +1. Verifies local `docker` and `stellar` CLI dependencies. +2. Starts a `stellar/quickstart:testing` container exposing port `8000`. +3. Waits for the Soroban RPC healthcheck (`/soroban/rpc`) to return `healthy`. +4. Adds the `standalone` network profile to your Stellar CLI configuration. +5. Generates and funds two local test accounts: `sandbox-deployer` (payer & admin) and `sandbox-beneficiary`. +6. Compiles `contracts/payment_escrow` to WebAssembly (`wasm32-unknown-unknown`). +7. Deploys `payment_escrow` to the local standalone network and initialises the admin. +8. Resolves the native Stellar asset (XLM) contract address. +9. Executes an end-to-end escrow lifecycle: + - Invokes `create` to deposit funds into escrow. + - Asserts status is `1` (`LOCKED`). + - Invokes `release` to disburse funds to the beneficiary. + - Asserts status is `2` (`RELEASED`). +10. Prints the exact `.env` configuration lines needed by the backend. + +### Stopping the Sandbox +To stop and remove the standalone Docker container: +```bash +make sandbox ARGS=--stop +# or +bash contracts/scripts/sandbox.sh --stop +``` + +--- + +## Manual Step-by-Step Guide + +If you prefer to run the steps manually or inspect each phase: + +### 1. Start the Standalone Docker Node + +```bash +docker run -d --rm \ + --name stellar-sandbox \ + -p 8000:8000 \ + stellar/quickstart:testing \ + --standalone \ + --enable-soroban-rpc +``` + +Verify that the Soroban RPC is responding: +```bash +curl -s -X POST http://localhost:8000/soroban/rpc \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"getHealth"}' +``` +Response should contain `"status": "healthy"`. + +### 2. Configure the Stellar CLI Network Profile + +```bash +stellar network add \ + --global standalone \ + --rpc-url "http://localhost:8000/soroban/rpc" \ + --network-passphrase "Standalone Network ; February 2017" +``` + +### 3. Generate and Fund Local Identities + +Create an operator/deployer account and a beneficiary account: +```bash +# Generate keys +stellar keys generate --global alice --network standalone +stellar keys generate --global bob --network standalone + +# Fund keys with friendbot +stellar keys fund alice --network standalone +stellar keys fund bob --network standalone +``` + +Inspect addresses and secret keys: +```bash +export ALICE_ADDR="$(stellar keys address alice)" +export BOB_ADDR="$(stellar keys address bob)" +export ALICE_SECRET="$(stellar keys secret alice)" +echo "Alice (Deployer/Payer): $ALICE_ADDR" +echo "Bob (Beneficiary): $BOB_ADDR" +``` + +### 4. Build and Deploy `payment_escrow` + +```bash +# Build contracts workspace +cd contracts +cargo build --release --target wasm32-unknown-unknown +cd .. + +# Deploy payment_escrow +export ESCROW_CONTRACT_ID="$(stellar contract deploy \ + --wasm contracts/target/wasm32-unknown-unknown/release/payment_escrow.wasm \ + --source alice \ + --network standalone)" +echo "Deployed ESCROW_CONTRACT_ID: $ESCROW_CONTRACT_ID" + +# Initialise admin +stellar contract invoke \ + --id "$ESCROW_CONTRACT_ID" \ + --source alice \ + --network standalone \ + -- initialize --admin "$ALICE_ADDR" +``` + +### 5. Resolve Native Asset Contract + +Stellar contracts handle XLM payments through the native asset contract wrapper: +```bash +export TOKEN_CONTRACT_ID="$(stellar contract id asset --asset native --network standalone)" +echo "Native Token Contract ID: $TOKEN_CONTRACT_ID" +``` + +### 6. Execute End-to-End Escrow Lifecycle + +#### A. Create Escrow +Generate a 32-byte hex string (SHA-256 equivalent) for the escrow ID: +```bash +export ESCROW_ID="0101010101010101010101010101010101010101010101010101010101010101" +``` + +Invoke `create` (locks 1.0 XLM = `10000000` stroops): +```bash +stellar contract invoke \ + --id "$ESCROW_CONTRACT_ID" \ + --source alice \ + --network standalone \ + -- create \ + --escrow_id "$ESCROW_ID" \ + --payer "$ALICE_ADDR" \ + --beneficiary "$BOB_ADDR" \ + --amount 10000000 \ + --token "$TOKEN_CONTRACT_ID" +``` + +#### B. Check Status (Locked) +Verify that the escrow status is `1` (`LOCKED`): +```bash +stellar contract invoke \ + --id "$ESCROW_CONTRACT_ID" \ + --source alice \ + --network standalone \ + -- get_status --escrow_id "$ESCROW_ID" +``` +Output: +``` +1 +``` + +#### C. Release Escrow +Release the funds from the contract to Bob: +```bash +stellar contract invoke \ + --id "$ESCROW_CONTRACT_ID" \ + --source alice \ + --network standalone \ + -- release \ + --escrow_id "$ESCROW_ID" +``` + +#### D. Check Status (Released) +Verify that the status is now `2` (`RELEASED`): +```bash +stellar contract invoke \ + --id "$ESCROW_CONTRACT_ID" \ + --source alice \ + --network standalone \ + -- get_status --escrow_id "$ESCROW_ID" +``` +Output: +``` +2 +``` + +--- + +## Backend Configuration (`backend/.env`) + +To point the NestJS backend to your local sandbox node, update `backend/.env` with: + +```env +# Activate the Soroban on-chain escrow rail +SOROBAN_ENABLED=true + +# Contract and Identity Config +STELLAR_ESCROW_CONTRACT_ID= +STELLAR_SECRET_KEY= +STELLAR_BENEFICIARY_ADDRESS= + +# Standalone Network Settings +STELLAR_NETWORK="Standalone Network ; February 2017" +STELLAR_HORIZON_URL=http://localhost:8000 +STELLAR_RPC_URLS=http://localhost:8000/soroban/rpc + +# Optional: Poll & Timeout Settings +SOROBAN_RPC_TIMEOUT_MS=10000 +SOROBAN_POLL_TIMEOUT_MS=15000 +SOROBAN_POLL_INTERVAL_MS=2000 +``` + +--- + +## Pointing Backend Integration Specs at Local Sandbox + +The backend integration spec at [`backend/src/payments/soroban/testnet-integration.spec.ts`](../backend/src/payments/soroban/testnet-integration.spec.ts) can be run locally against the standalone sandbox instead of the public testnet. + +Set the following environment variables: +```bash +export RUN_SOROBAN_TESTNET_TESTS=true +export SOROBAN_TESTNET_ESCROW_CONTRACT_ID="" +``` + +And run Jest: +```bash +cd backend +npm test -- src/payments/soroban/testnet-integration.spec.ts +``` + +This validates contract interaction through NestJS clients without consuming public testnet faucet quotas. diff --git a/contracts/README.md b/contracts/README.md index f64e0308..7975d4cc 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -106,8 +106,9 @@ as workspace members in [`Cargo.toml`](Cargo.toml) as they are developed. ## Tooling -- `make build` / `make test` / `make deploy` / `make clean` — see the root +- `make build` / `make test` / `make deploy` / `make clean` / `make sandbox` — see the root [`Makefile`](../Makefile) (CT-70). +- [`LOCAL.md`](LOCAL.md) — end-to-end local sandbox guide (standalone Docker node, deployment, create/release invocations, and backend `.env` configuration) (#2027). - `make audit` — CI-enforced authorization audit; see [`scripts/audit_require_auth.sh`](../scripts/audit_require_auth.sh) (CT-72). diff --git a/contracts/scripts/sandbox.sh b/contracts/scripts/sandbox.sh new file mode 100755 index 00000000..69c17351 --- /dev/null +++ b/contracts/scripts/sandbox.sh @@ -0,0 +1,218 @@ +#!/usr/bin/env bash +# +# End-to-end local sandbox for Soroban contracts (#2027). +# +# Starts a local standalone Stellar Quickstart container, generates and funds +# local test identities, builds and deploys payment_escrow, executes create/release +# invocations with the Stellar CLI, and outputs copy-pasteable .env configuration +# for the backend. +# +# Usage: +# bash contracts/scripts/sandbox.sh # Full start, deploy, and verify +# bash contracts/scripts/sandbox.sh --stop # Stop and remove the sandbox container +# bash contracts/scripts/sandbox.sh --skip-docker# Run deploy/calls against already running node +set -euo pipefail + +CONTAINER_NAME="${SANDBOX_CONTAINER:-stellar-sandbox}" +QUICKSTART_IMAGE="${QUICKSTART_IMAGE:-stellar/quickstart:testing}" +RPC_URL="${SANDBOX_RPC_URL:-http://localhost:8000/soroban/rpc}" +NETWORK_PASSPHRASE="${SANDBOX_NETWORK_PASSPHRASE:-Standalone Network ; February 2017}" +NETWORK_NAME="standalone" +SOURCE_IDENTITY="${SANDBOX_SOURCE:-sandbox-deployer}" +BENEFICIARY_IDENTITY="${SANDBOX_BENEFICIARY:-sandbox-beneficiary}" +CONTRACTS_DIR="contracts" +WASM_PATH="$CONTRACTS_DIR/target/wasm32-unknown-unknown/release/payment_escrow.wasm" + +handle_stop() { + echo "==> Stopping sandbox container '$CONTAINER_NAME'..." + if command -v docker >/dev/null 2>&1; then + docker stop "$CONTAINER_NAME" 2>/dev/null || true + echo "Sandbox container '$CONTAINER_NAME' stopped." + else + echo "Docker command not found; nothing to stop." + fi + exit 0 +} + +if [[ "${1:-}" == "--stop" || "${1:-}" == "-s" ]]; then + handle_stop +fi + +SKIP_DOCKER=0 +if [[ "${1:-}" == "--skip-docker" ]]; then + SKIP_DOCKER=1 +fi + +echo "======================================================================" +echo " ManageHub Soroban Local Sandbox" +echo "======================================================================" + +# 1. Dependency checks +echo "==> 1. Checking dependencies..." +if [[ "$SKIP_DOCKER" -eq 0 ]]; then + if ! command -v docker >/dev/null 2>&1; then + echo "Error: 'docker' is required to spin up the local Stellar quickstart node." >&2 + echo "Please install Docker or run with --skip-docker if using an external standalone node." >&2 + exit 1 + fi +fi + +if ! command -v stellar >/dev/null 2>&1; then + echo "Error: 'stellar' CLI is required. Install via:" >&2 + echo " cargo install --locked stellar-cli" >&2 + exit 1 +fi + +# 2. Start Stellar Quickstart container +if [[ "$SKIP_DOCKER" -eq 0 ]]; then + echo "==> 2. Starting Stellar Quickstart container ($CONTAINER_NAME)..." + if docker ps --filter "name=^/${CONTAINER_NAME}$" --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo " Container '$CONTAINER_NAME' is already running." + else + docker rm -f "$CONTAINER_NAME" 2>/dev/null || true + echo " Starting container with image: $QUICKSTART_IMAGE" + docker run -d --rm \ + --name "$CONTAINER_NAME" \ + -p 8000:8000 \ + "$QUICKSTART_IMAGE" \ + --standalone \ + --enable-soroban-rpc + fi + + echo " Waiting for Soroban RPC to become ready at $RPC_URL..." + attempts=0 + max_attempts=30 + until curl -s -f -X POST "$RPC_URL" \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":1,"method":"getHealth"}' >/dev/null 2>&1; do + attempts=$((attempts + 1)) + if [ "$attempts" -ge "$max_attempts" ]; then + echo "Error: Timed out waiting for Soroban RPC to become healthy at $RPC_URL" >&2 + exit 1 + fi + sleep 2 + done + echo " Soroban RPC is online and healthy!" +else + echo "==> 2. Skipping Docker container start (--skip-docker requested)." +fi + +# 3. Configure Stellar CLI network +echo "==> 3. Configuring local network '$NETWORK_NAME' in Stellar CLI..." +stellar network add \ + --global "$NETWORK_NAME" \ + --rpc-url "$RPC_URL" \ + --network-passphrase "$NETWORK_PASSPHRASE" 2>/dev/null \ + || stellar network modify \ + --global "$NETWORK_NAME" \ + --rpc-url "$RPC_URL" \ + --network-passphrase "$NETWORK_PASSPHRASE" 2>/dev/null || true + +# 4. Generate and fund identities +echo "==> 4. Setting up local identities..." +if ! stellar keys address "$SOURCE_IDENTITY" >/dev/null 2>&1; then + echo " Generating deployer key: $SOURCE_IDENTITY" + stellar keys generate --global "$SOURCE_IDENTITY" --network "$NETWORK_NAME" +fi +echo " Funding deployer ($SOURCE_IDENTITY)..." +stellar keys fund "$SOURCE_IDENTITY" --network "$NETWORK_NAME" || true + +if ! stellar keys address "$BENEFICIARY_IDENTITY" >/dev/null 2>&1; then + echo " Generating beneficiary key: $BENEFICIARY_IDENTITY" + stellar keys generate --global "$BENEFICIARY_IDENTITY" --network "$NETWORK_NAME" +fi +echo " Funding beneficiary ($BENEFICIARY_IDENTITY)..." +stellar keys fund "$BENEFICIARY_IDENTITY" --network "$NETWORK_NAME" || true + +SOURCE_ADDR="$(stellar keys address "$SOURCE_IDENTITY")" +SOURCE_SECRET="$(stellar keys secret "$SOURCE_IDENTITY" 2>/dev/null || stellar keys show "$SOURCE_IDENTITY" 2>/dev/null || echo "N/A")" +BENEFICIARY_ADDR="$(stellar keys address "$BENEFICIARY_IDENTITY")" + +echo " Deployer Address: $SOURCE_ADDR" +echo " Beneficiary Address: $BENEFICIARY_ADDR" + +# 5. Build and deploy payment_escrow +echo "==> 5. Building contracts workspace..." +cd "$CONTRACTS_DIR" +cargo build --release --target wasm32-unknown-unknown +cd .. + +echo " Deploying payment_escrow contract..." +ESCROW_CONTRACT_ID="$(stellar contract deploy \ + --wasm "$WASM_PATH" \ + --source "$SOURCE_IDENTITY" \ + --network "$NETWORK_NAME")" +echo " payment_escrow deployed at: $ESCROW_CONTRACT_ID" + +echo " Initialising contract admin..." +stellar contract invoke \ + --id "$ESCROW_CONTRACT_ID" \ + --source "$SOURCE_IDENTITY" \ + --network "$NETWORK_NAME" \ + -- initialize --admin "$SOURCE_ADDR" 2>/dev/null || echo " (Admin already initialised)" + +# Resolve or deploy native asset contract for token payments +echo " Resolving native asset contract..." +TOKEN_CONTRACT_ID="$(stellar contract id asset --asset native --network "$NETWORK_NAME" 2>/dev/null || echo "")" +if [ -z "$TOKEN_CONTRACT_ID" ]; then + TOKEN_CONTRACT_ID="$(stellar contract deploy-asset --asset native --source "$SOURCE_IDENTITY" --network "$NETWORK_NAME" 2>/dev/null || echo "$ESCROW_CONTRACT_ID")" +fi +echo " Token Contract ID: $TOKEN_CONTRACT_ID" + +# 6. End-to-end invoke: create -> release +echo "==> 6. Testing end-to-end escrow lifecycle (create -> release)..." +SAMPLE_ESCROW_ID="0101010101010101010101010101010101010101010101010101010101010101" +SAMPLE_AMOUNT=10000000 # 1 XLM (10^7 stroops) + +echo " a) Calling 'create' (Escrow ID: $SAMPLE_ESCROW_ID)..." +stellar contract invoke \ + --id "$ESCROW_CONTRACT_ID" \ + --source "$SOURCE_IDENTITY" \ + --network "$NETWORK_NAME" \ + -- create \ + --escrow_id "$SAMPLE_ESCROW_ID" \ + --payer "$SOURCE_ADDR" \ + --beneficiary "$BENEFICIARY_ADDR" \ + --amount "$SAMPLE_AMOUNT" \ + --token "$TOKEN_CONTRACT_ID" + +echo " b) Verifying status is LOCKED (1)..." +STATUS_LOCKED="$(stellar contract invoke \ + --id "$ESCROW_CONTRACT_ID" \ + --source "$SOURCE_IDENTITY" \ + --network "$NETWORK_NAME" \ + -- get_status --escrow_id "$SAMPLE_ESCROW_ID")" +echo " Status returned: $STATUS_LOCKED" + +echo " c) Calling 'release'..." +stellar contract invoke \ + --id "$ESCROW_CONTRACT_ID" \ + --source "$SOURCE_IDENTITY" \ + --network "$NETWORK_NAME" \ + -- release \ + --escrow_id "$SAMPLE_ESCROW_ID" + +echo " d) Verifying status is RELEASED (2)..." +STATUS_RELEASED="$(stellar contract invoke \ + --id "$ESCROW_CONTRACT_ID" \ + --source "$SOURCE_IDENTITY" \ + --network "$NETWORK_NAME" \ + -- get_status --escrow_id "$SAMPLE_ESCROW_ID")" +echo " Status returned: $STATUS_RELEASED" + +# 7. Print backend .env configuration +echo +echo "======================================================================" +echo " Sandbox Ready! Backend Configuration (.env):" +echo "======================================================================" +cat <