Skip to content

Commit 8886111

Browse files
authored
Merge pull request #669 from AbuJulaybeeb/feat/635-automated-typescript-bindings-generator
feat: add automated TypeScript contract client package builder (#635)
2 parents 77b43bf + ff4a885 commit 8886111

3 files changed

Lines changed: 254 additions & 0 deletions

File tree

.gitignore

153 Bytes
Binary file not shown.

packages/.gitkeep

Whitespace-only changes.

scripts/generate-bindings.sh

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
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

Comments
 (0)