Source

lib/bindings/generator.js

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.BindingGenerator = void 0;
var _contract = require("../contract");
var _config = require("./config");
var _types = require("./types");
var _client = require("./client");
var _wasm_spec_parser = require("../contract/wasm_spec_parser");
var _wasm_fetcher = require("./wasm_fetcher");
var _sacSpec = require("./sac-spec");
/**
 * Options for generating TypeScript bindings.
 *
 * @property contractName - The name used for the generated package and client class.
 *   Should be in kebab-case (e.g., "my-contract").
 */

/**
 * The output of the binding generation process.
 *
 * Contains all generated TypeScript source files and configuration files
 * needed to create a standalone npm package for interacting with a Stellar contract.
 *
 * @property index - The index.ts barrel export file that re-exports Client and types
 * @property types - The types.ts file containing TypeScript interfaces for contract structs, enums, and unions
 * @property client - The client.ts file containing the generated Client class with typed methods
 * @property packageJson - The package.json for the generated npm package
 * @property tsConfig - The tsconfig.json for TypeScript compilation
 * @property readme - The README.md with usage documentation
 * @property gitignore - The .gitignore file for the generated package
 */

/**
 * Generates TypeScript bindings for Stellar smart contracts.
 *
 * This class creates fully-typed TypeScript client code from a contract's specification,
 * allowing developers to interact with Stellar smart contracts with full IDE support
 * and compile-time type checking.
 *
 * @example
 * // Create from a local WASM file
 * const wasmBuffer = fs.readFileSync("./my_contract.wasm");
 * const generator = await BindingGenerator.fromWasm(wasmBuffer);
 * const bindings = generator.generate({ contractName: "my-contract" });
 *
 * @example
 * // Create from a contract deployed on the network
 * const generator = await BindingGenerator.fromContractId(
 *   "CABC...XYZ",
 *   "https://soroban-testnet.stellar.org",
 *   Networks.TESTNET
 * );
 * const bindings = generator.generate({ contractName: "my-contract" });
 *
 * @example
 * // Create from a Spec object directly
 * const spec = new Spec(specEntries);
 * const generator = BindingGenerator.fromSpec(spec);
 * const bindings = generator.generate({ contractName: "my-contract" });
 */
class BindingGenerator {
  /**
   * Private constructor - use static factory methods instead.
   *
   * @param spec - The parsed contract specification
   */
  constructor(spec) {
    this.spec = spec;
  }

  /**
   * Creates a BindingGenerator from an existing Spec object.
   *
   * Use this when you already have a parsed contract specification,
   * such as from manually constructed spec entries or from another source.
   *
   * @param spec - The contract specification containing function and type definitions
   * @returns A new BindingGenerator instance
   *
   * @example
   * const spec = new Spec(specEntries);
   * const generator = BindingGenerator.fromSpec(spec);
   */
  static fromSpec(spec) {
    return new BindingGenerator(spec);
  }

  /**
   * Creates a BindingGenerator from a WASM binary buffer.
   *
   * Parses the contract specification directly from the WASM file's custom section.
   * This is the most common method when working with locally compiled contracts.
   *
   * @param wasmBuffer - The raw WASM binary as a Buffer
   * @returns A Promise resolving to a new BindingGenerator instance
   * @throws {Error} If the WASM file doesn't contain a valid contract spec
   *
   * @example
   * const wasmBuffer = fs.readFileSync("./target/wasm32-unknown-unknown/release/my_contract.wasm");
   * const generator = await BindingGenerator.fromWasm(wasmBuffer);
   */
  static fromWasm(wasmBuffer) {
    const spec = new _contract.Spec((0, _wasm_spec_parser.specFromWasm)(wasmBuffer));
    return new BindingGenerator(spec);
  }

  /**
   * Creates a BindingGenerator by fetching WASM from the network using its hash.
   *
   * Retrieves the WASM bytecode from Stellar RPC using the WASM hash,
   * then parses the contract specification from it. Useful when you know
   * the hash of an installed WASM but don't have the binary locally.
   *
   * @param wasmHash - The hex-encoded hash of the installed WASM blob
   * @param rpcServer - The Stellar RPC server instance
   * @returns A Promise resolving to a new BindingGenerator instance
   * @throws {Error} If the WASM cannot be fetched or doesn't contain a valid spec
   *
   * @example
   * const generator = await BindingGenerator.fromWasmHash(
   *   "a1b2c3...xyz",
   *   "https://soroban-testnet.stellar.org",
   *   Networks.TESTNET
   * );
   */
  static async fromWasmHash(wasmHash, rpcServer) {
    const wasm = await (0, _wasm_fetcher.fetchFromWasmHash)(wasmHash, rpcServer);
    if (wasm.type !== "wasm") {
      throw new Error("Fetched contract is not of type 'wasm'");
    }
    return BindingGenerator.fromWasm(wasm.wasmBytes);
  }

  /**
   * Creates a BindingGenerator by fetching contract info from a deployed contract ID.
   *
   * Retrieves the contract's WASM from the network using the contract ID,
   * then parses the specification. If the contract is a Stellar Asset Contract (SAC),
   * returns a generator with the standard SAC specification.
   *
   * @param contractId - The contract ID (C... address) of the deployed contract
   * @param rpcServer - The Stellar RPC server instance
   * @returns A Promise resolving to a new BindingGenerator instance
   * @throws {Error} If the contract cannot be found or fetched
   *
   * @example
   * const generator = await BindingGenerator.fromContractId(
   *   "CABC123...XYZ",
   *   rpcServer
   * );
   */
  static async fromContractId(contractId, rpcServer) {
    const result = await (0, _wasm_fetcher.fetchFromContractId)(contractId, rpcServer);
    if (result.type === "wasm") {
      return BindingGenerator.fromWasm(result.wasmBytes);
    }
    // Stellar Asset Contract
    const spec = new _contract.Spec(_sacSpec.SAC_SPEC);
    return BindingGenerator.fromSpec(spec);
  }

  /**
   * Generates TypeScript bindings for the contract.
   *
   * Produces all the files needed for a standalone npm package:
   * - `client.ts`: A typed Client class with methods for each contract function
   * - `types.ts`: TypeScript interfaces for all contract types (structs, enums, unions)
   * - `index.ts`: Barrel export file
   * - `package.json`, `tsconfig.json`, `README.md`, `.gitignore`: Package configuration
   *
   * The generated code does not write to disk - use the returned strings
   * to write files as needed.
   *
   * @param options - Configuration options for generation
   * @param options.contractName - Required. The name for the generated package (kebab-case recommended)
   * @returns An object containing all generated file contents as strings
   * @throws {Error} If contractName is missing or empty
   *
   * @example
   * const bindings = generator.generate({
   *   contractName: "my-token",
   *   contractAddress: "CABC...XYZ",
   *   rpcUrl: "https://soroban-testnet.stellar.org",
   *   networkPassphrase: Networks.TESTNET
   * });
   *
   * // Write files to disk
   * fs.writeFileSync("./src/client.ts", bindings.client);
   * fs.writeFileSync("./src/types.ts", bindings.types);
   */
  generate(options) {
    this.validateOptions(options);
    const typeGenerator = new _types.TypeGenerator(this.spec);
    const clientGenerator = new _client.ClientGenerator(this.spec);
    const types = typeGenerator.generate();
    const client = clientGenerator.generate();
    let index = `export { Client } from "./client.js";`;
    if (types.trim() !== "") {
      index = index.concat(`\nexport * from "./types.js";`);
    }

    // Generate config files
    const configGenerator = new _config.ConfigGenerator();
    const {
      packageJson,
      tsConfig,
      readme,
      gitignore
    } = configGenerator.generate(options);
    return {
      index,
      types,
      client,
      packageJson,
      tsConfig,
      readme,
      gitignore
    };
  }

  /**
   * Validates that required generation options are provided.
   *
   * @param options - The options to validate
   * @throws {Error} If contractName is missing or empty
   */
  validateOptions(options) {
    if (!options.contractName || options.contractName.trim() === "") {
      throw new Error("contractName is required and cannot be empty");
    }
  }
}
exports.BindingGenerator = BindingGenerator;