Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions bin/stellar-js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env node

// Entry point for the stellar CLI
require("../lib/cli/index.js").runCli();
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@
"files": [
"/types",
"/lib",
"/dist"
"/dist",
"/bin"
],
"bin": {
"stellar-js": "./bin/stellar-js"
},
"exports": {
".": {
"browser": "./dist/stellar-sdk.min.js",
Expand Down Expand Up @@ -108,6 +112,7 @@
"fmt": "yarn _prettier && yarn eslint src/ --fix",
"preversion": "yarn clean && yarn _prettier && yarn build:prod && yarn test",
"prepare": "yarn build:prod",
"download-sac-spec": "node scripts/download-sac-spec.js",
"_build": "yarn build:node:all && yarn build:browser:all",
"_babel": "babel --extensions '.ts' --out-dir ${OUTPUT_DIR:-lib} src/",
"_nyc": "node node_modules/.bin/nyc --nycrc-path config/.nycrc",
Expand Down Expand Up @@ -197,6 +202,7 @@
"@stellar/stellar-base": "^14.0.2",
"axios": "^1.12.2",
"bignumber.js": "^9.3.1",
"commander": "^14.0.2",
"eventsource": "^2.0.2",
"feaxios": "^0.0.23",
"randombytes": "^2.1.0",
Expand Down
82 changes: 82 additions & 0 deletions scripts/download-sac-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env node

import https from "https";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";

// Get __dirname equivalent in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Get repo root
const REPO_ROOT = path.join(__dirname, "..");

// URLs and paths
const REPO_URL =
"https://raw.githubusercontent.com/stellar/stellar-asset-contract-spec/main/stellar-asset-spec.xdr";
const TS_FILE = path.join(REPO_ROOT, "src/bindings/sac-spec.ts");

console.log("Downloading Stellar Asset Contract spec from GitHub...");

// Download file
https
.get(REPO_URL, (response) => {
if (response.statusCode !== 200) {
console.error(`✗ Failed to download: HTTP ${response.statusCode}`);
process.exit(1);
}

const chunks = [];

response.on("data", (chunk) => {
chunks.push(chunk);
});

response.on("end", () => {
const buffer = Buffer.concat(chunks);

// Verify file has content
if (buffer.length === 0) {
console.error("✗ Downloaded file is empty");
process.exit(1);
}

console.log("✓ Successfully downloaded file");
console.log(` Size: ${buffer.length} bytes`);

// Convert to base64
console.log("Converting to base64...");
const base64Content = buffer.toString("base64");

if (!base64Content) {
console.error("✗ Base64 conversion failed");
process.exit(1);
}

// Update TypeScript file
console.log(`Updating ${path.relative(REPO_ROOT, TS_FILE)}...`);
const tsContent = [
"// Auto-generated by scripts/download-sac-spec.sh",
"// Do not edit manually - run the script to update",
`export const SAC_SPEC = "${base64Content}";`,
"", // trailing newline
].join("\n");

try {
fs.writeFileSync(TS_FILE, tsContent, "utf8");
console.log(
`✓ Successfully updated ${path.relative(REPO_ROOT, TS_FILE)}`,
);
} catch (error) {
console.error(`✗ Failed to write file: ${error.message}`);
process.exit(1);
}

console.log("✓ Done");
});
})
.on("error", (error) => {
console.error(`✗ Error: ${error.message}`);
process.exit(1);
});
213 changes: 213 additions & 0 deletions src/bindings/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { xdr } from "@stellar/stellar-base";
import { Spec } from "../contract";
import {
parseTypeFromTypeDef,
generateTypeImports,
sanitizeName,
formatJSDocComment,
} from "./utils";

/**
* Generates TypeScript client class for contract methods
*/
export class ClientGenerator {
private spec: Spec;

constructor(spec: Spec) {
this.spec = spec;
}

/**
* Generate client class
*/
generate(): string {
// Generate constructor method if it exists
let deployMethod = "";
try {
const constructorFunc = this.spec.getFunc("__constructor");
deployMethod = this.generateDeployMethod(constructorFunc);
} catch {
// For specs without a constructor, generate a deploy method without params
deployMethod = this.generateDeployMethod(undefined);
}
// Generate interface methods
const interfaceMethods = this.spec
.funcs()
.filter((func) => func.name().toString() !== "__constructor")
.map((func) => this.generateInterfaceMethod(func))
.join("\n");

const imports = this.generateImports();

const specEntries = this.spec.entries.map(
(entry) => `"${entry.toXDR("base64")}"`,
);

const fromJSON = this.spec
.funcs()
.filter((func) => func.name().toString() !== "__constructor")
.map((func) => this.generateFromJSONMethod(func))
.join(",");

return `${imports}

export interface Client {
${interfaceMethods}
}

export class Client extends ContractClient {
constructor(public readonly options: ContractClientOptions) {
super(
new Spec([${specEntries.join(", ")}]),
options
);
}

${deployMethod}
public readonly fromJSON = {
${fromJSON}
};
}`;
}

private generateImports(): string {
const { stellarContractImports, typeFileImports, stellarImports } =
generateTypeImports(
this.spec.funcs().flatMap((func) => {
const inputs = func.inputs();
const outputs = func.outputs();
const defs = inputs.map((input) => input.type()).concat(outputs);
return defs;
}),
);
// Ensure necessary imports for Client class
stellarContractImports.push(
"Spec",
"AssembledTransaction",
"Client as ContractClient",
"ClientOptions as ContractClientOptions",
"MethodOptions",
);
// Build imports
const importLines: string[] = [];
if (typeFileImports.length > 0) {
importLines.push(
`import {\n${typeFileImports.join(",\n")}\n} from './types.js';`,
);
}
if (stellarContractImports.length > 0) {
importLines.push(
`import {\n${stellarContractImports.join(
",\n",
)}\n} from '@stellar/stellar-sdk/contract';`,
);
}
if (stellarImports.length > 0) {
importLines.push(
`import {\n${stellarImports.join(
",\n",
)}\n} from '@stellar/stellar-sdk';`,
);
}
importLines.push(`import { Buffer } from 'buffer';`);
const imports = importLines.join("\n");
return imports;
}

/**
* Generate interface method signature
*/
private generateInterfaceMethod(func: xdr.ScSpecFunctionV0): string {
const name = sanitizeName(func.name().toString());
const inputs = func.inputs().map((input: any) => ({
name: input.name().toString(),
type: parseTypeFromTypeDef(input.type()),
}));
const outputType =
func.outputs().length > 0
? parseTypeFromTypeDef(func.outputs()[0])
: "void";
const docs = formatJSDocComment(func.doc().toString(), 2);
const params = this.formatMethodParameters(inputs);

return `${docs} ${name}(${params}): Promise<AssembledTransaction<${outputType}>>;`;
}

private generateFromJSONMethod(func: xdr.ScSpecFunctionV0): string {
const name = func.name().toString();
const outputType =
func.outputs().length > 0
? parseTypeFromTypeDef(func.outputs()[0])
: "void";

return ` ${name} : this.txFromJSON<${outputType}>`;
}
/**
* Generate deploy method
*/
private generateDeployMethod(
constructorFunc: xdr.ScSpecFunctionV0 | undefined,
): string {
// If no constructor, generate deploy with no params
if (!constructorFunc) {
const params = this.formatConstructorParameters([]);
return ` static deploy<T = Client>(${params}): Promise<AssembledTransaction<T>> {
return ContractClient.deploy(null, options);
}`;
}
const inputs = constructorFunc.inputs().map((input) => ({
name: input.name().toString(),
type: parseTypeFromTypeDef(input.type()),
}));

const params = this.formatConstructorParameters(inputs);
const inputsDestructure =
inputs.length > 0 ? `{ ${inputs.map((i) => i.name).join(", ")} }, ` : "";

return ` static deploy<T = Client>(${params}): Promise<AssembledTransaction<T>> {
return ContractClient.deploy(${inputsDestructure}options);
}`;
}

/**
* Format method parameters
*/
private formatMethodParameters(
inputs: Array<{ name: string; type: string }>,
): string {
const params: string[] = [];

if (inputs.length > 0) {
const inputsParam = `{ ${inputs.map((i) => `${i.name}: ${i.type}`).join("; ")} }`;
params.push(
`{ ${inputs.map((i) => i.name).join(", ")} }: ${inputsParam}`,
);
}

params.push("options?: MethodOptions");

return params.join(", ");
}

/**
* Format constructor parameters
*/
private formatConstructorParameters(
inputs: Array<{ name: string; type: string }>,
): string {
const params: string[] = [];

if (inputs.length > 0) {
const inputsParam = `{ ${inputs.map((i) => `${i.name}: ${i.type}`).join("; ")} }`;
params.push(
`{ ${inputs.map((i) => i.name).join(", ")} }: ${inputsParam}`,
);
}

params.push(
'options: MethodOptions & Omit<ContractClientOptions, \'contractId\'> & { wasmHash: Buffer | string; salt?: Buffer | Uint8Array; format?: "hex" | "base64"; address?: string; }',
);

return params.join(", ");
}
}
Loading
Loading