|
| 1 | +#!/usr/bin/env bash |
| 2 | +# ============================================================================= |
| 3 | +# generate-bindings.sh — Automated TypeScript Contract Client Package Builder |
| 4 | +# Issue: #635 | stellarflow-contracts |
| 5 | +# |
| 6 | +# Invokes `stellar contract bindings typescript` for every workspace contract |
| 7 | +# WASM and emits a typed TS package per contract into the configured output |
| 8 | +# directory, ready for consumption by stellarflow-frontend. |
| 9 | +# |
| 10 | +# Usage: |
| 11 | +# ./scripts/generate-bindings.sh [OPTIONS] |
| 12 | +# |
| 13 | +# Options: |
| 14 | +# --network <network> Soroban network alias (default: testnet) |
| 15 | +# --rpc-url <url> Override the Soroban RPC URL |
| 16 | +# --output <dir> Root directory for generated packages |
| 17 | +# (default: packages/) |
| 18 | +# --contracts <list> Comma-separated list of contract names to process. |
| 19 | +# Omit to process every discovered contract. |
| 20 | +# --help Show this help message and exit. |
| 21 | +# |
| 22 | +# Environment variables (all optional, lower precedence than flags): |
| 23 | +# STELLAR_NETWORK — same as --network |
| 24 | +# STELLAR_RPC_URL — same as --rpc-url |
| 25 | +# BINDINGS_OUTPUT_DIR — same as --output |
| 26 | +# |
| 27 | +# Requirements: |
| 28 | +# • stellar CLI ≥ v21 (https://developers.stellar.org/docs/tools/developer-tools/cli/install-cli) |
| 29 | +# • Rust / cargo build tool-chain (for `cargo build --release`) |
| 30 | +# • node / npm (for package.json scaffolding inside each generated package) |
| 31 | +# ============================================================================= |
| 32 | + |
| 33 | +set -euo pipefail |
| 34 | + |
| 35 | +# ── Colour helpers ───────────────────────────────────────────────────────── |
| 36 | +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' |
| 37 | +CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' |
| 38 | + |
| 39 | +info() { echo -e "${CYAN}[INFO]${RESET} $*"; } |
| 40 | +success() { echo -e "${GREEN}[OK]${RESET} $*"; } |
| 41 | +warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; } |
| 42 | +error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; } |
| 43 | +die() { error "$*"; exit 1; } |
| 44 | + |
| 45 | +# ── Defaults ─────────────────────────────────────────────────────────────── |
| 46 | +NETWORK="${STELLAR_NETWORK:-testnet}" |
| 47 | +RPC_URL="${STELLAR_RPC_URL:-}" |
| 48 | +OUTPUT_DIR="${BINDINGS_OUTPUT_DIR:-packages}" |
| 49 | +FILTER_CONTRACTS="" |
| 50 | +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" |
| 51 | +WASM_DIR="${REPO_ROOT}/target/wasm32-unknown-unknown/release" |
| 52 | + |
| 53 | +# ── Parse CLI flags ──────────────────────────────────────────────────────── |
| 54 | +print_help() { |
| 55 | + sed -n '2,35p' "${BASH_SOURCE[0]}" |
| 56 | + exit 0 |
| 57 | +} |
| 58 | + |
| 59 | +while [[ $# -gt 0 ]]; do |
| 60 | + case "$1" in |
| 61 | + --network) NETWORK="$2"; shift 2 ;; |
| 62 | + --rpc-url) RPC_URL="$2"; shift 2 ;; |
| 63 | + --output) OUTPUT_DIR="$2"; shift 2 ;; |
| 64 | + --contracts) FILTER_CONTRACTS="$2"; shift 2 ;; |
| 65 | + --help|-h) print_help ;; |
| 66 | + *) die "Unknown option: $1. Run with --help for usage." ;; |
| 67 | + esac |
| 68 | +done |
| 69 | + |
| 70 | +# Resolve output dir relative to repo root when not absolute |
| 71 | +[[ "${OUTPUT_DIR}" = /* ]] || OUTPUT_DIR="${REPO_ROOT}/${OUTPUT_DIR}" |
| 72 | + |
| 73 | +# ── Preflight checks ─────────────────────────────────────────────────────── |
| 74 | +info "Checking required tools..." |
| 75 | + |
| 76 | +command -v stellar &>/dev/null \ |
| 77 | + || die "'stellar' CLI not found. Install from: https://developers.stellar.org/docs/tools/developer-tools/cli/install-cli" |
| 78 | + |
| 79 | +STELLAR_VERSION="$(stellar --version 2>&1 | head -n1)" |
| 80 | +info "stellar CLI: ${STELLAR_VERSION}" |
| 81 | + |
| 82 | +command -v cargo &>/dev/null \ |
| 83 | + || die "'cargo' not found. Install Rust from: https://rustup.rs" |
| 84 | + |
| 85 | +# ── Discover workspace contracts ─────────────────────────────────────────── |
| 86 | +# Each workspace member in contracts/ that produces a cdylib is a candidate. |
| 87 | +discover_contracts() { |
| 88 | + local contracts_dir="${REPO_ROOT}/contracts" |
| 89 | + local found=() |
| 90 | + |
| 91 | + for manifest in "${contracts_dir}"/*/Cargo.toml; do |
| 92 | + local dir; dir="$(dirname "${manifest}")" |
| 93 | + local name; name="$(basename "${dir}")" |
| 94 | + |
| 95 | + # Only include members that build a cdylib (i.e., a deployable contract) |
| 96 | + if grep -q 'cdylib' "${manifest}" 2>/dev/null; then |
| 97 | + found+=("${name}") |
| 98 | + fi |
| 99 | + done |
| 100 | + |
| 101 | + echo "${found[@]:-}" |
| 102 | +} |
| 103 | + |
| 104 | +# ── Build contracts ──────────────────────────────────────────────────────── |
| 105 | +build_contracts() { |
| 106 | + info "Building all workspace contracts in release mode..." |
| 107 | + ( |
| 108 | + cd "${REPO_ROOT}" |
| 109 | + cargo build \ |
| 110 | + --release \ |
| 111 | + --target wasm32-unknown-unknown \ |
| 112 | + --workspace \ |
| 113 | + --exclude stellarflow-contracts \ |
| 114 | + 2>&1 |
| 115 | + ) && success "Build complete." || die "Cargo build failed — fix compilation errors first." |
| 116 | +} |
| 117 | + |
| 118 | +# ── Generate bindings for a single contract ──────────────────────────────── |
| 119 | +generate_for_contract() { |
| 120 | + local contract_name="$1" |
| 121 | + # stellar CLI expects hyphen-separated names to be underscore in WASM filename |
| 122 | + local wasm_name="${contract_name//-/_}" |
| 123 | + local wasm_path="${WASM_DIR}/${wasm_name}.wasm" |
| 124 | + |
| 125 | + if [[ ! -f "${wasm_path}" ]]; then |
| 126 | + warn "WASM not found for '${contract_name}' at ${wasm_path}. Skipping." |
| 127 | + return 0 |
| 128 | + fi |
| 129 | + |
| 130 | + local pkg_dir="${OUTPUT_DIR}/${contract_name}" |
| 131 | + mkdir -p "${pkg_dir}" |
| 132 | + |
| 133 | + info "Generating TypeScript bindings for '${contract_name}'..." |
| 134 | + |
| 135 | + # Build the stellar CLI command |
| 136 | + local cmd=( |
| 137 | + stellar contract bindings typescript |
| 138 | + --wasm "${wasm_path}" |
| 139 | + --output-dir "${pkg_dir}" |
| 140 | + --overwrite |
| 141 | + ) |
| 142 | + |
| 143 | + # Append network/rpc flags when provided |
| 144 | + [[ -n "${NETWORK}" ]] && cmd+=(--network "${NETWORK}") |
| 145 | + [[ -n "${RPC_URL}" ]] && cmd+=(--rpc-url "${RPC_URL}") |
| 146 | + |
| 147 | + if "${cmd[@]}" 2>&1; then |
| 148 | + success " → ${contract_name}: bindings written to ${pkg_dir}" |
| 149 | + else |
| 150 | + warn " → ${contract_name}: stellar CLI returned non-zero. Check WASM validity." |
| 151 | + return 1 |
| 152 | + fi |
| 153 | +} |
| 154 | + |
| 155 | +# ── Patch package.json name field ───────────────────────────────────────── |
| 156 | +# stellar CLI generates a generic package.json; we scope it under @stellarflow. |
| 157 | +patch_package_json() { |
| 158 | + local contract_name="$1" |
| 159 | + local pkg_json="${OUTPUT_DIR}/${contract_name}/package.json" |
| 160 | + |
| 161 | + if [[ ! -f "${pkg_json}" ]]; then |
| 162 | + return 0 |
| 163 | + fi |
| 164 | + |
| 165 | + local scoped_name="@stellarflow/${contract_name}" |
| 166 | + |
| 167 | + # Use node if available for robust JSON editing; fall back to sed |
| 168 | + if command -v node &>/dev/null; then |
| 169 | + node - "${pkg_json}" "${scoped_name}" <<'NODE_EOF' |
| 170 | +const fs = require('fs'); |
| 171 | +const path = process.argv[2]; |
| 172 | +const name = process.argv[3]; |
| 173 | +const pkg = JSON.parse(fs.readFileSync(path, 'utf8')); |
| 174 | +pkg.name = name; |
| 175 | +pkg.repository = { |
| 176 | + type: 'git', |
| 177 | + url: 'https://github.com/StellarFlow-Network/stellarflow-contracts', |
| 178 | + directory: `packages/${name.split('/')[1]}`, |
| 179 | +}; |
| 180 | +pkg.keywords = [...(pkg.keywords || []), 'stellarflow', 'soroban', 'stellar']; |
| 181 | +fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n'); |
| 182 | +console.log(`Patched package.json name → ${name}`); |
| 183 | +NODE_EOF |
| 184 | + else |
| 185 | + # Best-effort sed patch (works for simple cases) |
| 186 | + sed -i "s|\"name\":[[:space:]]*\"[^\"]*\"|\"name\": \"${scoped_name}\"|" "${pkg_json}" |
| 187 | + warn "node not found — used sed to patch package.json (may be imprecise)." |
| 188 | + fi |
| 189 | +} |
| 190 | + |
| 191 | +# ── Main ─────────────────────────────────────────────────────────────────── |
| 192 | +main() { |
| 193 | + echo -e "${BOLD}" |
| 194 | + echo "════════════════════════════════════════════════════════════" |
| 195 | + echo " StellarFlow — TypeScript Contract Bindings Generator" |
| 196 | + echo " Issue #635" |
| 197 | + echo "════════════════════════════════════════════════════════════" |
| 198 | + echo -e "${RESET}" |
| 199 | + |
| 200 | + info "Repository root : ${REPO_ROOT}" |
| 201 | + info "Output directory: ${OUTPUT_DIR}" |
| 202 | + info "Network : ${NETWORK:-<none>}" |
| 203 | + |
| 204 | + # Determine contracts to process |
| 205 | + local all_contracts |
| 206 | + mapfile -t all_contracts < <(discover_contracts | tr ' ' '\n') |
| 207 | + |
| 208 | + local targets=() |
| 209 | + if [[ -n "${FILTER_CONTRACTS}" ]]; then |
| 210 | + IFS=',' read -ra targets <<< "${FILTER_CONTRACTS}" |
| 211 | + else |
| 212 | + targets=("${all_contracts[@]}") |
| 213 | + fi |
| 214 | + |
| 215 | + if [[ ${#targets[@]} -eq 0 ]]; then |
| 216 | + die "No deployable contracts found under contracts/. Nothing to generate." |
| 217 | + fi |
| 218 | + |
| 219 | + info "Contracts to process: ${targets[*]}" |
| 220 | + |
| 221 | + # Build step |
| 222 | + build_contracts |
| 223 | + |
| 224 | + # Generation loop |
| 225 | + local ok=0 fail=0 |
| 226 | + mkdir -p "${OUTPUT_DIR}" |
| 227 | + |
| 228 | + for contract in "${targets[@]}"; do |
| 229 | + if generate_for_contract "${contract}"; then |
| 230 | + patch_package_json "${contract}" |
| 231 | + (( ok++ )) || true |
| 232 | + else |
| 233 | + (( fail++ )) || true |
| 234 | + fi |
| 235 | + done |
| 236 | + |
| 237 | + echo "" |
| 238 | + echo -e "${BOLD}════ Summary ════${RESET}" |
| 239 | + success "Generated : ${ok} package(s)" |
| 240 | + [[ ${fail} -gt 0 ]] && warn "Skipped / failed: ${fail} contract(s)" |
| 241 | + |
| 242 | + echo "" |
| 243 | + info "Next steps:" |
| 244 | + echo " 1. Review generated packages in: ${OUTPUT_DIR}/" |
| 245 | + echo " 2. In each package run: npm install && npm run build" |
| 246 | + echo " 3. Publish or link into stellarflow-frontend via:" |
| 247 | + echo " npm install ${OUTPUT_DIR}/<contract-name>" |
| 248 | + echo " or add to frontend package.json as a workspace dependency." |
| 249 | + echo "" |
| 250 | + |
| 251 | + [[ ${fail} -gt 0 ]] && exit 1 || exit 0 |
| 252 | +} |
| 253 | + |
| 254 | +main "$@" |
0 commit comments