From ff4a885effc30aa1e9d1ddf8ad4aa3e6111957b7 Mon Sep 17 00:00:00 2001 From: AbuJulaybeeb Date: Sun, 26 Jul 2026 02:18:37 +0100 Subject: [PATCH] feat: add automated TypeScript contract client package builder (#635) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements scripts/generate-bindings.sh — a fully automated build-pipeline script that converts every workspace contract WASM into a typed TypeScript client package consumable by stellarflow-frontend. Key features ──────────── • Discovers all cdylib workspace members under contracts/ automatically; no manual configuration required when a new contract is added. • Invokes `stellar contract bindings typescript` per contract, writing each generated TS package into packages//. • Scopes each generated package as @stellarflow/ and patches package.json with proper repository and keywords metadata. • Accepts --network, --rpc-url, --output, and --contracts CLI flags so the script works identically for local dev, CI, and multi-environment deploys. • Respects STELLAR_NETWORK, STELLAR_RPC_URL, and BINDINGS_OUTPUT_DIR env vars for CI/CD pipeline integration without flag changes. • Colour-coded console output with clear INFO / OK / WARN / ERROR prefixes and a summary table showing generated vs skipped contract count. • Performs preflight checks for stellar CLI and cargo before attempting a build, giving actionable install instructions on failure. • Adds packages/ output directory anchored by packages/.gitkeep; generated contents are gitignored so only the script is versioned. Usage ───── # Generate bindings for all workspace contracts (testnet, default output) ./scripts/generate-bindings.sh # Target a specific network and output directory ./scripts/generate-bindings.sh --network mainnet --output ../stellarflow-frontend/packages # Generate bindings for a subset of contracts only ./scripts/generate-bindings.sh --contracts price-oracle,liquidity-lock Closes #635 --- .gitignore | Bin 127 -> 280 bytes packages/.gitkeep | 0 scripts/generate-bindings.sh | 254 +++++++++++++++++++++++++++++++++++ 3 files changed, 254 insertions(+) create mode 100644 packages/.gitkeep create mode 100755 scripts/generate-bindings.sh diff --git a/.gitignore b/.gitignore index 16ea372c4043b6bed98e5b624acd2feae11713da..d2a99140e83f4fb758cee0b683db0de4485de29e 100644 GIT binary patch literal 280 zcmYL^OAf*?3`F-ig%Jxr66F{yU#gXkI_4cF^l$|Zl%p0%iI;w4yc(ghi z4M`A;Wpd^v!kk;vg@ezeFiRz>>y(9iU)o^E*~_dsp+LN0p!-yD3zDi)pUN;>zTr)F z1}fPTRDF2)2tEFZluVHyw0$T4zjra&tGRa6w|XfS7PO$a@B$Y@F;b=hZ)C0zB^k@2 YIsAHb`lLKRlc&xt4faR3T9zN~16)I6!~g&Q delta 108 zcmW-XI}U>|5CxwoSK0&6A*IGA;SSLT77K8^$lg9zNXc>eG@}`5j^_7oXC{{t)tQ)H z-C(1nuB!WzV`U1}ihU&C=9-d}`(xV6d$O$IJM9pR*e*sWdL-D>0W0d|84SV+s}URZ diff --git a/packages/.gitkeep b/packages/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scripts/generate-bindings.sh b/scripts/generate-bindings.sh new file mode 100755 index 00000000..82c5cea5 --- /dev/null +++ b/scripts/generate-bindings.sh @@ -0,0 +1,254 @@ +#!/usr/bin/env bash +# ============================================================================= +# generate-bindings.sh — Automated TypeScript Contract Client Package Builder +# Issue: #635 | stellarflow-contracts +# +# Invokes `stellar contract bindings typescript` for every workspace contract +# WASM and emits a typed TS package per contract into the configured output +# directory, ready for consumption by stellarflow-frontend. +# +# Usage: +# ./scripts/generate-bindings.sh [OPTIONS] +# +# Options: +# --network Soroban network alias (default: testnet) +# --rpc-url Override the Soroban RPC URL +# --output Root directory for generated packages +# (default: packages/) +# --contracts Comma-separated list of contract names to process. +# Omit to process every discovered contract. +# --help Show this help message and exit. +# +# Environment variables (all optional, lower precedence than flags): +# STELLAR_NETWORK — same as --network +# STELLAR_RPC_URL — same as --rpc-url +# BINDINGS_OUTPUT_DIR — same as --output +# +# Requirements: +# • stellar CLI ≥ v21 (https://developers.stellar.org/docs/tools/developer-tools/cli/install-cli) +# • Rust / cargo build tool-chain (for `cargo build --release`) +# • node / npm (for package.json scaffolding inside each generated package) +# ============================================================================= + +set -euo pipefail + +# ── Colour helpers ───────────────────────────────────────────────────────── +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' +CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' + +info() { echo -e "${CYAN}[INFO]${RESET} $*"; } +success() { echo -e "${GREEN}[OK]${RESET} $*"; } +warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; } +error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; } +die() { error "$*"; exit 1; } + +# ── Defaults ─────────────────────────────────────────────────────────────── +NETWORK="${STELLAR_NETWORK:-testnet}" +RPC_URL="${STELLAR_RPC_URL:-}" +OUTPUT_DIR="${BINDINGS_OUTPUT_DIR:-packages}" +FILTER_CONTRACTS="" +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +WASM_DIR="${REPO_ROOT}/target/wasm32-unknown-unknown/release" + +# ── Parse CLI flags ──────────────────────────────────────────────────────── +print_help() { + sed -n '2,35p' "${BASH_SOURCE[0]}" + exit 0 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --network) NETWORK="$2"; shift 2 ;; + --rpc-url) RPC_URL="$2"; shift 2 ;; + --output) OUTPUT_DIR="$2"; shift 2 ;; + --contracts) FILTER_CONTRACTS="$2"; shift 2 ;; + --help|-h) print_help ;; + *) die "Unknown option: $1. Run with --help for usage." ;; + esac +done + +# Resolve output dir relative to repo root when not absolute +[[ "${OUTPUT_DIR}" = /* ]] || OUTPUT_DIR="${REPO_ROOT}/${OUTPUT_DIR}" + +# ── Preflight checks ─────────────────────────────────────────────────────── +info "Checking required tools..." + +command -v stellar &>/dev/null \ + || die "'stellar' CLI not found. Install from: https://developers.stellar.org/docs/tools/developer-tools/cli/install-cli" + +STELLAR_VERSION="$(stellar --version 2>&1 | head -n1)" +info "stellar CLI: ${STELLAR_VERSION}" + +command -v cargo &>/dev/null \ + || die "'cargo' not found. Install Rust from: https://rustup.rs" + +# ── Discover workspace contracts ─────────────────────────────────────────── +# Each workspace member in contracts/ that produces a cdylib is a candidate. +discover_contracts() { + local contracts_dir="${REPO_ROOT}/contracts" + local found=() + + for manifest in "${contracts_dir}"/*/Cargo.toml; do + local dir; dir="$(dirname "${manifest}")" + local name; name="$(basename "${dir}")" + + # Only include members that build a cdylib (i.e., a deployable contract) + if grep -q 'cdylib' "${manifest}" 2>/dev/null; then + found+=("${name}") + fi + done + + echo "${found[@]:-}" +} + +# ── Build contracts ──────────────────────────────────────────────────────── +build_contracts() { + info "Building all workspace contracts in release mode..." + ( + cd "${REPO_ROOT}" + cargo build \ + --release \ + --target wasm32-unknown-unknown \ + --workspace \ + --exclude stellarflow-contracts \ + 2>&1 + ) && success "Build complete." || die "Cargo build failed — fix compilation errors first." +} + +# ── Generate bindings for a single contract ──────────────────────────────── +generate_for_contract() { + local contract_name="$1" + # stellar CLI expects hyphen-separated names to be underscore in WASM filename + local wasm_name="${contract_name//-/_}" + local wasm_path="${WASM_DIR}/${wasm_name}.wasm" + + if [[ ! -f "${wasm_path}" ]]; then + warn "WASM not found for '${contract_name}' at ${wasm_path}. Skipping." + return 0 + fi + + local pkg_dir="${OUTPUT_DIR}/${contract_name}" + mkdir -p "${pkg_dir}" + + info "Generating TypeScript bindings for '${contract_name}'..." + + # Build the stellar CLI command + local cmd=( + stellar contract bindings typescript + --wasm "${wasm_path}" + --output-dir "${pkg_dir}" + --overwrite + ) + + # Append network/rpc flags when provided + [[ -n "${NETWORK}" ]] && cmd+=(--network "${NETWORK}") + [[ -n "${RPC_URL}" ]] && cmd+=(--rpc-url "${RPC_URL}") + + if "${cmd[@]}" 2>&1; then + success " → ${contract_name}: bindings written to ${pkg_dir}" + else + warn " → ${contract_name}: stellar CLI returned non-zero. Check WASM validity." + return 1 + fi +} + +# ── Patch package.json name field ───────────────────────────────────────── +# stellar CLI generates a generic package.json; we scope it under @stellarflow. +patch_package_json() { + local contract_name="$1" + local pkg_json="${OUTPUT_DIR}/${contract_name}/package.json" + + if [[ ! -f "${pkg_json}" ]]; then + return 0 + fi + + local scoped_name="@stellarflow/${contract_name}" + + # Use node if available for robust JSON editing; fall back to sed + if command -v node &>/dev/null; then + node - "${pkg_json}" "${scoped_name}" <<'NODE_EOF' +const fs = require('fs'); +const path = process.argv[2]; +const name = process.argv[3]; +const pkg = JSON.parse(fs.readFileSync(path, 'utf8')); +pkg.name = name; +pkg.repository = { + type: 'git', + url: 'https://github.com/StellarFlow-Network/stellarflow-contracts', + directory: `packages/${name.split('/')[1]}`, +}; +pkg.keywords = [...(pkg.keywords || []), 'stellarflow', 'soroban', 'stellar']; +fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n'); +console.log(`Patched package.json name → ${name}`); +NODE_EOF + else + # Best-effort sed patch (works for simple cases) + sed -i "s|\"name\":[[:space:]]*\"[^\"]*\"|\"name\": \"${scoped_name}\"|" "${pkg_json}" + warn "node not found — used sed to patch package.json (may be imprecise)." + fi +} + +# ── Main ─────────────────────────────────────────────────────────────────── +main() { + echo -e "${BOLD}" + echo "════════════════════════════════════════════════════════════" + echo " StellarFlow — TypeScript Contract Bindings Generator" + echo " Issue #635" + echo "════════════════════════════════════════════════════════════" + echo -e "${RESET}" + + info "Repository root : ${REPO_ROOT}" + info "Output directory: ${OUTPUT_DIR}" + info "Network : ${NETWORK:-}" + + # Determine contracts to process + local all_contracts + mapfile -t all_contracts < <(discover_contracts | tr ' ' '\n') + + local targets=() + if [[ -n "${FILTER_CONTRACTS}" ]]; then + IFS=',' read -ra targets <<< "${FILTER_CONTRACTS}" + else + targets=("${all_contracts[@]}") + fi + + if [[ ${#targets[@]} -eq 0 ]]; then + die "No deployable contracts found under contracts/. Nothing to generate." + fi + + info "Contracts to process: ${targets[*]}" + + # Build step + build_contracts + + # Generation loop + local ok=0 fail=0 + mkdir -p "${OUTPUT_DIR}" + + for contract in "${targets[@]}"; do + if generate_for_contract "${contract}"; then + patch_package_json "${contract}" + (( ok++ )) || true + else + (( fail++ )) || true + fi + done + + echo "" + echo -e "${BOLD}════ Summary ════${RESET}" + success "Generated : ${ok} package(s)" + [[ ${fail} -gt 0 ]] && warn "Skipped / failed: ${fail} contract(s)" + + echo "" + info "Next steps:" + echo " 1. Review generated packages in: ${OUTPUT_DIR}/" + echo " 2. In each package run: npm install && npm run build" + echo " 3. Publish or link into stellarflow-frontend via:" + echo " npm install ${OUTPUT_DIR}/" + echo " or add to frontend package.json as a workspace dependency." + echo "" + + [[ ${fail} -gt 0 ]] && exit 1 || exit 0 +} + +main "$@"