stellar-contracts-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,951 @@
1
+ #!/usr/bin/env node
2
+ import { mkdirSync, writeFileSync, readFileSync } from 'fs';
3
+ import { resolve, dirname, extname, basename, join } from 'path';
4
+ import { createInterface } from 'readline';
5
+ import { rpc, contract } from '@stellar/stellar-sdk';
6
+
7
+ // src/network/config.ts
8
+ var NETWORKS = {
9
+ mainnet: {
10
+ rpcUrl: "https://mainnet.stellar.validationcloud.io/v1/soroban/rpc",
11
+ networkPassphrase: "Public Global Stellar Network ; September 2015",
12
+ horizonUrl: "https://horizon.stellar.org"
13
+ },
14
+ testnet: {
15
+ rpcUrl: "https://soroban-testnet.stellar.org",
16
+ networkPassphrase: "Test SDF Network ; September 2015",
17
+ horizonUrl: "https://horizon-testnet.stellar.org"
18
+ },
19
+ futurenet: {
20
+ rpcUrl: "https://rpc-futurenet.stellar.org",
21
+ networkPassphrase: "Test SDF Future Network ; October 2022",
22
+ horizonUrl: "https://horizon-futurenet.stellar.org"
23
+ }
24
+ };
25
+ function resolveNetwork(network) {
26
+ if (typeof network === "string") {
27
+ return NETWORKS[network];
28
+ }
29
+ return network;
30
+ }
31
+ function createServer(network) {
32
+ return new rpc.Server(network.rpcUrl, { allowHttp: network.rpcUrl.startsWith("http://") });
33
+ }
34
+
35
+ // src/errors/index.ts
36
+ var StellarContractError = class extends Error {
37
+ constructor(code, message, cause) {
38
+ super(message);
39
+ this.name = "StellarContractError";
40
+ this.code = code;
41
+ this.cause = cause;
42
+ }
43
+ };
44
+ function makeError(code, message, cause) {
45
+ return new StellarContractError(code, message, cause);
46
+ }
47
+ function isContractKitError(err) {
48
+ return err instanceof StellarContractError;
49
+ }
50
+
51
+ // src/contract/spec.ts
52
+ function validateContractId(id) {
53
+ if (!/^C[A-Z2-7]{55}$/.test(id)) {
54
+ throw makeError(
55
+ "INVALID_CONTRACT_ID",
56
+ `"${id}" is not a valid Soroban contract address. Expected a 56-character string starting with C (e.g. CABC...).`
57
+ );
58
+ }
59
+ }
60
+ async function fetchContractSpec(contractId, server, network) {
61
+ validateContractId(contractId);
62
+ let wasm;
63
+ try {
64
+ wasm = await server.getContractWasmByContractId(contractId);
65
+ } catch (err) {
66
+ throw makeError("CONTRACT_NOT_FOUND", `Contract ${contractId} not found or has no WASM`, err);
67
+ }
68
+ try {
69
+ const client = await contract.Client.fromWasm(wasm, {
70
+ contractId,
71
+ networkPassphrase: network.networkPassphrase,
72
+ rpcUrl: network.rpcUrl
73
+ });
74
+ return client.spec;
75
+ } catch (err) {
76
+ throw makeError("CONTRACT_SPEC_ERROR", `Failed to parse contract spec for ${contractId}`, err);
77
+ }
78
+ }
79
+
80
+ // src/cli/codegen.ts
81
+ function placeholder(typeDef, lang = "ts") {
82
+ const name = typeDef.switch().name;
83
+ switch (name) {
84
+ case "scSpecTypeBool":
85
+ return "false";
86
+ case "scSpecTypeVoid":
87
+ return "undefined";
88
+ case "scSpecTypeU32":
89
+ case "scSpecTypeI32":
90
+ return "0";
91
+ case "scSpecTypeU64":
92
+ case "scSpecTypeI64":
93
+ case "scSpecTypeU128":
94
+ case "scSpecTypeI128":
95
+ case "scSpecTypeU256":
96
+ case "scSpecTypeI256":
97
+ case "scSpecTypeTimepoint":
98
+ case "scSpecTypeDuration":
99
+ return "0n";
100
+ case "scSpecTypeString":
101
+ case "scSpecTypeSymbol":
102
+ return "''";
103
+ case "scSpecTypeAddress":
104
+ case "scSpecTypeMuxedAddress":
105
+ return "'G...'";
106
+ case "scSpecTypeBytes":
107
+ case "scSpecTypeBytesN":
108
+ return "new Uint8Array()";
109
+ case "scSpecTypeOption":
110
+ return "null";
111
+ case "scSpecTypeVec":
112
+ return "[]";
113
+ case "scSpecTypeMap":
114
+ return "new Map()";
115
+ case "scSpecTypeTuple": {
116
+ const types = typeDef.tuple().valueTypes();
117
+ return `[${types.map((t) => placeholder(t, lang)).join(", ")}]`;
118
+ }
119
+ case "scSpecTypeUdt": {
120
+ const udtName = typeDef.udt().name().toString();
121
+ return lang === "ts" ? `{} as unknown as ${udtName}` : "{}";
122
+ }
123
+ default:
124
+ return "undefined";
125
+ }
126
+ }
127
+ function toTs(typeDef) {
128
+ const name = typeDef.switch().name;
129
+ switch (name) {
130
+ case "scSpecTypeBool":
131
+ return "boolean";
132
+ case "scSpecTypeVoid":
133
+ return "void";
134
+ case "scSpecTypeU32":
135
+ case "scSpecTypeI32":
136
+ return "number";
137
+ case "scSpecTypeU64":
138
+ case "scSpecTypeI64":
139
+ case "scSpecTypeU128":
140
+ case "scSpecTypeI128":
141
+ case "scSpecTypeU256":
142
+ case "scSpecTypeI256":
143
+ case "scSpecTypeTimepoint":
144
+ case "scSpecTypeDuration":
145
+ return "bigint";
146
+ case "scSpecTypeString":
147
+ case "scSpecTypeSymbol":
148
+ return "string";
149
+ case "scSpecTypeAddress":
150
+ case "scSpecTypeMuxedAddress":
151
+ return "string";
152
+ case "scSpecTypeBytes":
153
+ case "scSpecTypeBytesN":
154
+ return "Uint8Array";
155
+ case "scSpecTypeOption": {
156
+ const inner = toTs(typeDef.option().valueType());
157
+ return `${inner} | null`;
158
+ }
159
+ case "scSpecTypeResult": {
160
+ return toTs(typeDef.result().okType());
161
+ }
162
+ case "scSpecTypeVec": {
163
+ const inner = toTs(typeDef.vec().elementType());
164
+ return `Array<${inner}>`;
165
+ }
166
+ case "scSpecTypeMap": {
167
+ const k = toTs(typeDef.map().keyType());
168
+ const v = toTs(typeDef.map().valueType());
169
+ return `Map<${k}, ${v}>`;
170
+ }
171
+ case "scSpecTypeTuple": {
172
+ const types = typeDef.tuple().valueTypes().map(toTs);
173
+ return `[${types.join(", ")}]`;
174
+ }
175
+ case "scSpecTypeUdt": {
176
+ return typeDef.udt().name().toString();
177
+ }
178
+ default:
179
+ return "unknown";
180
+ }
181
+ }
182
+ function generateCustomTypes(entries) {
183
+ const out = [];
184
+ for (const entry of entries) {
185
+ const kind = entry.switch().name;
186
+ try {
187
+ if (kind === "scSpecEntryUdtStructV0") {
188
+ const s = entry.udtStructV0();
189
+ const fields = s.fields();
190
+ const isTuple = fields.length > 0 && fields.every((f) => /^\d+$/.test(f.name().toString()));
191
+ if (isTuple) {
192
+ const types = fields.map((f) => toTs(f.type()));
193
+ out.push(`export type ${s.name().toString()} = [${types.join(", ")}]`);
194
+ } else {
195
+ out.push(`export interface ${s.name().toString()} {`);
196
+ for (const f of fields) {
197
+ out.push(` ${f.name().toString()}: ${toTs(f.type())}`);
198
+ }
199
+ out.push("}");
200
+ }
201
+ out.push("");
202
+ }
203
+ if (kind === "scSpecEntryUdtEnumV0" || kind === "scSpecEntryUdtErrorEnumV0") {
204
+ const e = kind === "scSpecEntryUdtEnumV0" ? entry.udtEnumV0() : entry.udtErrorEnumV0();
205
+ out.push(`export enum ${e.name().toString()} {`);
206
+ for (const c of e.cases()) {
207
+ out.push(` ${c.name().toString()} = ${c.value()},`);
208
+ }
209
+ out.push("}");
210
+ out.push("");
211
+ }
212
+ if (kind === "scSpecEntryUdtUnionV0") {
213
+ const u = entry.udtUnionV0();
214
+ const caseTypes = u.cases().map((c) => {
215
+ const isVoid = c.switch().name === "scSpecUdtUnionCaseVoidV0";
216
+ const inner = isVoid ? c.voidV0() : c.tupleV0();
217
+ const tag = inner.name().toString();
218
+ if (isVoid) return `{ tag: '${tag}' }`;
219
+ const types = inner.type().map(toTs);
220
+ return types.length === 1 ? `{ tag: '${tag}'; value: ${types[0]} }` : `{ tag: '${tag}'; value: [${types.join(", ")}] }`;
221
+ });
222
+ out.push(`export type ${u.name().toString()} =`);
223
+ out.push(caseTypes.map((t) => ` | ${t}`).join("\n"));
224
+ out.push("");
225
+ }
226
+ } catch {
227
+ }
228
+ }
229
+ return out.join("\n");
230
+ }
231
+ function generateInterface(spec, interfaceName) {
232
+ const out = [];
233
+ out.push(`export interface ${interfaceName} {`);
234
+ for (const fn of spec.funcs()) {
235
+ try {
236
+ const methodName = fn.name().toString();
237
+ const inputs = fn.inputs();
238
+ const outputs = fn.outputs();
239
+ const returnType = outputs.length === 0 ? "void" : outputs.length === 1 ? toTs(outputs[0]) : `[${outputs.map(toTs).join(", ")}]`;
240
+ const argParts = inputs.map((i) => `${i.name().toString()}: ${toTs(i.type())}`);
241
+ const argsTuple = `[${argParts.join(", ")}]`;
242
+ out.push(` ${methodName}: ContractMethodFn<${returnType}, ${argsTuple}>`);
243
+ } catch {
244
+ }
245
+ }
246
+ out.push("}");
247
+ return out.join("\n");
248
+ }
249
+ function generateExample(spec, contractId, interfaceName, importBasename, networkLabel, typesRelPath, aliasImport) {
250
+ const isPreset = ["testnet", "mainnet", "futurenet"].includes(networkLabel);
251
+ const networkInit = isPreset ? `{ network: '${networkLabel}' }` : `{ network: { rpcUrl: 'https://...', networkPassphrase: '...', horizonUrl: 'https://...' } }`;
252
+ const importPath = aliasImport ? `${aliasImport}.js` : `./${importBasename}.js`;
253
+ const importComment = !aliasImport ? typesRelPath ? `// Types file: ${typesRelPath}. Update the path below if using snippets elsewhere.` : `// Update the import path below to match your file's location relative to the types file.` : null;
254
+ const referencedUdts = /* @__PURE__ */ new Set();
255
+ for (const fn of spec.funcs()) {
256
+ try {
257
+ for (const i of fn.inputs()) {
258
+ if (i.type().switch().name === "scSpecTypeUdt") {
259
+ referencedUdts.add(i.type().udt().name().toString());
260
+ }
261
+ }
262
+ } catch {
263
+ }
264
+ }
265
+ const typeImports = [interfaceName, ...Array.from(referencedUdts).sort()].join(", ");
266
+ const lines = [
267
+ `// Auto-generated by stellar-contracts-kit`,
268
+ `// Example usage for ${interfaceName}`,
269
+ `// Contract : ${contractId}`,
270
+ `// Network : ${networkLabel}`,
271
+ `// Adapt this file to your project. Re-run \`npx sck generate\` to refresh after a contract upgrade.`,
272
+ ``,
273
+ `import { StellarContractsKit } from 'stellar-contracts-kit'`,
274
+ ...importComment ? [importComment] : [],
275
+ `import type { ${typeImports} } from '${importPath}'`,
276
+ `import { CONTRACT_ID } from '${importPath}'`,
277
+ ``,
278
+ `const kit = new StellarContractsKit(${networkInit})`,
279
+ ``,
280
+ `// Connect wallet (opens picker modal if no wallet specified)`,
281
+ `const { address } = await kit.connect()`,
282
+ `console.log('Connected:', address)`,
283
+ ``,
284
+ `// Load the typed contract client`,
285
+ `const contract = await kit.contract<${interfaceName}>(CONTRACT_ID)`,
286
+ ``
287
+ ];
288
+ for (const fn of spec.funcs()) {
289
+ const methodName = fn.name().toString();
290
+ const inputs = fn.inputs();
291
+ const outputs = fn.outputs();
292
+ const isVoid = outputs.length === 0;
293
+ const hasParams = inputs.length > 0;
294
+ const returnType = isVoid ? "void" : outputs.length === 1 ? toTs(outputs[0]) : `[${outputs.map(toTs).join(", ")}]`;
295
+ const sig = `${methodName}(${inputs.map((i) => `${i.name().toString()}: ${toTs(i.type())}`).join(", ")}) -> ${returnType}`;
296
+ lines.push(`// ${sig}`);
297
+ const argLines = inputs.map((i) => {
298
+ const val = placeholder(i.type(), "ts");
299
+ return ` ${val}, // ${i.name().toString()}: ${toTs(i.type())}`;
300
+ });
301
+ const isLikelyRead = !hasParams && !isVoid;
302
+ if (isLikelyRead) {
303
+ lines.push(`const { result: ${methodName}Result } = await contract.${methodName}.read()`);
304
+ lines.push(`console.log('${methodName}:', ${methodName}Result)`);
305
+ } else if (isVoid && !hasParams) {
306
+ lines.push(`const { txHash: ${methodName}TxHash } = await contract.${methodName}.invoke()`);
307
+ lines.push(`console.log('${methodName} txHash:', ${methodName}TxHash)`);
308
+ } else if (isVoid) {
309
+ lines.push(`const { txHash: ${methodName}TxHash } = await contract.${methodName}.invoke(`);
310
+ lines.push(...argLines);
311
+ lines.push(`)`);
312
+ lines.push(`console.log('${methodName} txHash:', ${methodName}TxHash)`);
313
+ } else {
314
+ lines.push(`const { txHash: ${methodName}TxHash, result: ${methodName}Result } = await contract.${methodName}.invoke(`);
315
+ lines.push(...argLines);
316
+ lines.push(`)`);
317
+ lines.push(`console.log('${methodName} txHash:', ${methodName}TxHash, 'result:', ${methodName}Result)`);
318
+ }
319
+ lines.push(``);
320
+ }
321
+ return lines.join("\n");
322
+ }
323
+ function generateCustomTypesJs(entries) {
324
+ const out = [];
325
+ for (const entry of entries) {
326
+ const kind = entry.switch().name;
327
+ try {
328
+ if (kind === "scSpecEntryUdtStructV0") {
329
+ const s = entry.udtStructV0();
330
+ const fields = s.fields();
331
+ const isTuple = fields.length > 0 && fields.every((f) => /^\d+$/.test(f.name().toString()));
332
+ if (isTuple) {
333
+ const types = fields.map((f) => toTs(f.type())).join(", ");
334
+ out.push(`/** @typedef {[${types}]} ${s.name().toString()} */`);
335
+ } else {
336
+ const fieldStr = fields.map((f) => `${f.name().toString()}: ${toTs(f.type())}`).join(", ");
337
+ out.push(`/** @typedef {{ ${fieldStr} }} ${s.name().toString()} */`);
338
+ }
339
+ out.push("");
340
+ }
341
+ if (kind === "scSpecEntryUdtEnumV0" || kind === "scSpecEntryUdtErrorEnumV0") {
342
+ const e = kind === "scSpecEntryUdtEnumV0" ? entry.udtEnumV0() : entry.udtErrorEnumV0();
343
+ out.push(`export const ${e.name().toString()} = Object.freeze({`);
344
+ for (const c of e.cases()) {
345
+ out.push(` ${c.name().toString()}: ${c.value()},`);
346
+ }
347
+ out.push(`})`);
348
+ out.push("");
349
+ }
350
+ if (kind === "scSpecEntryUdtUnionV0") {
351
+ const u = entry.udtUnionV0();
352
+ const caseTypes = u.cases().map((c) => {
353
+ const isVoid = c.switch().name === "scSpecUdtUnionCaseVoidV0";
354
+ const inner = isVoid ? c.voidV0() : c.tupleV0();
355
+ const tag = inner.name().toString();
356
+ if (isVoid) return `{ tag: '${tag}' }`;
357
+ const types = inner.type().map(toTs);
358
+ return types.length === 1 ? `{ tag: '${tag}', value: ${types[0]} }` : `{ tag: '${tag}', value: [${types.join(", ")}] }`;
359
+ });
360
+ out.push(`/** @typedef {${caseTypes.join(" | ")}} ${u.name().toString()} */`);
361
+ out.push("");
362
+ }
363
+ } catch {
364
+ }
365
+ }
366
+ return out.join("\n");
367
+ }
368
+ function generateInterfaceJs(spec, interfaceName) {
369
+ const lines = ["/**", ` * @typedef {Object} ${interfaceName}`];
370
+ for (const fn of spec.funcs()) {
371
+ try {
372
+ const methodName = fn.name().toString();
373
+ const inputs = fn.inputs();
374
+ const outputs = fn.outputs();
375
+ const returnType = outputs.length === 0 ? "void" : outputs.length === 1 ? toTs(outputs[0]) : `[${outputs.map(toTs).join(", ")}]`;
376
+ const argParts = inputs.map((i) => `${i.name().toString()}: ${toTs(i.type())}`);
377
+ const argsTuple = `[${argParts.join(", ")}]`;
378
+ lines.push(` * @property {import('stellar-contracts-kit').ContractMethodFn<${returnType}, ${argsTuple}>} ${methodName}`);
379
+ } catch {
380
+ }
381
+ }
382
+ lines.push(" */");
383
+ return lines.join("\n");
384
+ }
385
+ function generateOutputJs(spec, contractId, interfaceName, networkLabel) {
386
+ const entries = spec.entries;
387
+ const customTypes = generateCustomTypesJs(entries).trim();
388
+ const iface = generateInterfaceJs(spec, interfaceName);
389
+ const lines = [
390
+ `// Auto-generated by stellar-contracts-kit`,
391
+ `// Contract : ${contractId}`,
392
+ `// Network : ${networkLabel}`,
393
+ `// Re-run \`npx sck generate\` to update after a contract upgrade.`,
394
+ ``
395
+ ];
396
+ if (customTypes) {
397
+ lines.push(`// Custom Types`);
398
+ lines.push(``);
399
+ lines.push(customTypes);
400
+ lines.push(``);
401
+ }
402
+ lines.push(`// Contract Interface`);
403
+ lines.push(``);
404
+ lines.push(iface);
405
+ lines.push(``);
406
+ lines.push(`export const CONTRACT_ID = '${contractId}'`);
407
+ lines.push(``);
408
+ return lines.join("\n");
409
+ }
410
+ function generateExampleJs(spec, contractId, interfaceName, importBasename, networkLabel, typesRelPath, aliasImport) {
411
+ const isPreset = ["testnet", "mainnet", "futurenet"].includes(networkLabel);
412
+ const networkInit = isPreset ? `{ network: '${networkLabel}' }` : `{ network: { rpcUrl: 'https://...', networkPassphrase: '...', horizonUrl: 'https://...' } }`;
413
+ const importPath = aliasImport ? `${aliasImport}.js` : `./${importBasename}.js`;
414
+ const importComment = !aliasImport ? typesRelPath ? `// Types file: ${typesRelPath}. Update the path below if using snippets elsewhere.` : `// Update the import path below to match your file's location relative to the types file.` : null;
415
+ const lines = [
416
+ `// Auto-generated by stellar-contracts-kit`,
417
+ `// Example usage for ${interfaceName}`,
418
+ `// Contract : ${contractId}`,
419
+ `// Network : ${networkLabel}`,
420
+ `// @ts-check`,
421
+ ``,
422
+ `import { StellarContractsKit } from 'stellar-contracts-kit'`,
423
+ ...importComment ? [importComment] : [],
424
+ `import { CONTRACT_ID } from '${importPath}'`,
425
+ ``,
426
+ `const kit = new StellarContractsKit(${networkInit})`,
427
+ ``,
428
+ `// Connect wallet (opens picker modal if no wallet specified)`,
429
+ `const { address } = await kit.connect()`,
430
+ `console.log('Connected:', address)`,
431
+ ``,
432
+ `// Load the contract client`,
433
+ `const contract = await kit.contract(CONTRACT_ID)`,
434
+ ``
435
+ ];
436
+ for (const fn of spec.funcs()) {
437
+ const methodName = fn.name().toString();
438
+ const inputs = fn.inputs();
439
+ const outputs = fn.outputs();
440
+ const isVoid = outputs.length === 0;
441
+ const hasParams = inputs.length > 0;
442
+ const returnType = isVoid ? "void" : outputs.length === 1 ? toTs(outputs[0]) : `[${outputs.map(toTs).join(", ")}]`;
443
+ lines.push(`// ${methodName}(${inputs.map((i) => `${i.name().toString()}: ${toTs(i.type())}`).join(", ")}) -> ${returnType}`);
444
+ const argLines = inputs.map((i) => ` ${placeholder(i.type(), "js")}, // ${i.name().toString()}: ${toTs(i.type())}`);
445
+ const isLikelyRead = !hasParams && !isVoid;
446
+ if (isLikelyRead) {
447
+ lines.push(`const { result: ${methodName}Result } = await contract.${methodName}.read()`);
448
+ lines.push(`console.log('${methodName}:', ${methodName}Result)`);
449
+ } else if (isVoid && !hasParams) {
450
+ lines.push(`const { txHash: ${methodName}TxHash } = await contract.${methodName}.invoke()`);
451
+ lines.push(`console.log('${methodName} txHash:', ${methodName}TxHash)`);
452
+ } else if (isVoid) {
453
+ lines.push(`const { txHash: ${methodName}TxHash } = await contract.${methodName}.invoke(`);
454
+ lines.push(...argLines);
455
+ lines.push(`)`);
456
+ lines.push(`console.log('${methodName} txHash:', ${methodName}TxHash)`);
457
+ } else {
458
+ lines.push(`const { txHash: ${methodName}TxHash, result: ${methodName}Result } = await contract.${methodName}.invoke(`);
459
+ lines.push(...argLines);
460
+ lines.push(`)`);
461
+ lines.push(`console.log('${methodName} txHash:', ${methodName}TxHash, 'result:', ${methodName}Result)`);
462
+ }
463
+ lines.push(``);
464
+ }
465
+ return lines.join("\n");
466
+ }
467
+ function generateOutput(spec, contractId, interfaceName, networkLabel) {
468
+ const entries = spec.entries;
469
+ const customTypes = generateCustomTypes(entries).trim();
470
+ const iface = generateInterface(spec, interfaceName);
471
+ const lines = [
472
+ `// Auto-generated by stellar-contracts-kit`,
473
+ `// Contract : ${contractId}`,
474
+ `// Network : ${networkLabel}`,
475
+ `// Re-run \`npx sck generate\` to update after a contract upgrade.`,
476
+ ``,
477
+ `import type { ContractMethodFn } from 'stellar-contracts-kit'`,
478
+ ``
479
+ ];
480
+ if (customTypes) {
481
+ lines.push(`// Custom Types`);
482
+ lines.push(``);
483
+ lines.push(customTypes);
484
+ lines.push(``);
485
+ }
486
+ lines.push(`// Contract Interface`);
487
+ lines.push(``);
488
+ lines.push(iface);
489
+ lines.push(``);
490
+ lines.push(`export const CONTRACT_ID = '${contractId}' as const`);
491
+ lines.push(``);
492
+ return lines.join("\n");
493
+ }
494
+
495
+ // src/cli/generate.ts
496
+ function deriveInterfaceName(outPath) {
497
+ const base = basename(outPath, extname(outPath));
498
+ return base.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
499
+ }
500
+ function nameToKebab(name) {
501
+ return name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
502
+ }
503
+ function detectAliasImport(outPath, aliasHint) {
504
+ try {
505
+ const raw = readFileSync("tsconfig.json", "utf-8");
506
+ const stripped = raw.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
507
+ const tsconfig = JSON.parse(stripped);
508
+ const paths = tsconfig?.compilerOptions?.paths ?? {};
509
+ const normalizedOut = outPath.replace(/^\.\//, "").replace(/\.ts$/, "");
510
+ for (const [pattern, targets] of Object.entries(paths)) {
511
+ if (!pattern.endsWith("/*")) continue;
512
+ const aliasPrefix = pattern.slice(0, -2);
513
+ if (aliasHint && aliasPrefix !== aliasHint) continue;
514
+ const target = targets[0];
515
+ if (!target?.endsWith("/*")) continue;
516
+ const targetPrefix = target.slice(0, -2).replace(/^\.\//, "");
517
+ if (normalizedOut.startsWith(targetPrefix + "/")) {
518
+ const rest = normalizedOut.slice(targetPrefix.length + 1);
519
+ return `${aliasPrefix}/${rest}`;
520
+ }
521
+ }
522
+ } catch {
523
+ }
524
+ return null;
525
+ }
526
+ var A = {
527
+ reset: "\x1B[0m",
528
+ bold: "\x1B[1m",
529
+ dim: "\x1B[2m",
530
+ cyan: "\x1B[36m",
531
+ green: "\x1B[32m"
532
+ };
533
+ function promptText(label, defaultVal = "", validate) {
534
+ const hint = defaultVal ? ` [${defaultVal}]` : "";
535
+ return new Promise((resolve2) => {
536
+ function ask() {
537
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
538
+ rl.question(`${label}${hint}: `, (input) => {
539
+ rl.close();
540
+ const value = input.trim() || defaultVal;
541
+ if (validate) {
542
+ const err = validate(value);
543
+ if (err) {
544
+ process.stderr.write(` ${A.dim}${err}${A.reset}
545
+ `);
546
+ ask();
547
+ return;
548
+ }
549
+ }
550
+ resolve2(value);
551
+ });
552
+ }
553
+ ask();
554
+ });
555
+ }
556
+ function promptEnter(message) {
557
+ return new Promise((resolve2) => {
558
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
559
+ rl.question(message, () => {
560
+ rl.close();
561
+ resolve2();
562
+ });
563
+ });
564
+ }
565
+ function promptSelect(label, options, defaultIdx = 0) {
566
+ return new Promise((resolve2) => {
567
+ let idx = defaultIdx;
568
+ const N = options.length;
569
+ const w = (s) => process.stderr.write(s);
570
+ function render(initial) {
571
+ if (!initial) w(`\x1B[${N}A`);
572
+ options.forEach((opt, i) => {
573
+ w("\x1B[2K\r");
574
+ w(i === idx ? ` ${A.cyan}>${A.reset} ${A.bold}${opt}${A.reset}
575
+ ` : ` ${A.dim}${opt}${A.reset}
576
+ `);
577
+ });
578
+ }
579
+ w(`${label}
580
+ `);
581
+ render(true);
582
+ process.stdin.setRawMode(true);
583
+ process.stdin.resume();
584
+ process.stdin.setEncoding("utf8");
585
+ function onKey(key) {
586
+ if (key === "") {
587
+ process.stdin.setRawMode(false);
588
+ process.stdin.pause();
589
+ w("\nCancelled.\n");
590
+ process.exit(0);
591
+ }
592
+ if (key === "\x1B[A") {
593
+ idx = (idx - 1 + N) % N;
594
+ render(false);
595
+ }
596
+ if (key === "\x1B[B") {
597
+ idx = (idx + 1) % N;
598
+ render(false);
599
+ }
600
+ if (key === "\r") {
601
+ process.stdin.removeListener("data", onKey);
602
+ process.stdin.setRawMode(false);
603
+ process.stdin.pause();
604
+ w(`\x1B[${N + 1}A\x1B[0J`);
605
+ w(`${label}: ${A.cyan}${options[idx]}${A.reset}
606
+ `);
607
+ resolve2(options[idx]);
608
+ }
609
+ }
610
+ process.stdin.on("data", onKey);
611
+ });
612
+ }
613
+ function validateContractAddress(val) {
614
+ if (!val) return "Contract address is required";
615
+ if (!/^C[A-Z2-7]{55}$/.test(val)) return "Must be a 56-character Soroban address starting with C (uppercase, A-Z and 2-7 only)";
616
+ return null;
617
+ }
618
+ function validateContractName(val) {
619
+ if (!val) return null;
620
+ if (!/^[A-Za-z][A-Za-z0-9]*$/.test(val)) return "Must start with a letter and contain only letters and numbers (becomes a TypeScript interface name)";
621
+ return null;
622
+ }
623
+ async function promptNetwork() {
624
+ const networkChoice = await promptSelect("Network", ["testnet", "mainnet", "futurenet", "custom"]);
625
+ if (networkChoice === "custom") {
626
+ const rpcUrl = await promptText("RPC URL");
627
+ const passphrase = await promptText("Network passphrase");
628
+ return { rpcUrl, passphrase };
629
+ }
630
+ return { networkArg: networkChoice };
631
+ }
632
+ async function runInteractive() {
633
+ process.stderr.write(`
634
+ `);
635
+ process.stderr.write(`\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557
636
+ `);
637
+ process.stderr.write(`\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u255D
638
+ `);
639
+ process.stderr.write(`\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2554\u255D
640
+ `);
641
+ process.stderr.write(`\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2588\u2588\u2557
642
+ `);
643
+ process.stderr.write(`\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2557
644
+ `);
645
+ process.stderr.write(`\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D
646
+ `);
647
+ process.stderr.write(`${A.dim}stellar-contracts-kit \u2022 Press Ctrl+C to cancel${A.reset}
648
+
649
+ `);
650
+ const command = await promptSelect("Command", ["generate", "inspect"]);
651
+ if (command === "inspect") {
652
+ const contractId2 = await promptText("Contract address", "", validateContractAddress);
653
+ const { networkArg: networkArg2, rpcUrl: rpcUrl2, passphrase: passphrase2 } = await promptNetwork();
654
+ const opts2 = { contractId: contractId2 };
655
+ if (networkArg2) opts2.networkArg = networkArg2;
656
+ if (rpcUrl2) opts2.rpcUrl = rpcUrl2;
657
+ if (passphrase2) opts2.passphrase = passphrase2;
658
+ await runInspect(opts2);
659
+ return;
660
+ }
661
+ const contractId = await promptText("Contract address", "", validateContractAddress);
662
+ const rawNameInput = await promptText("Interface name", "", validateContractName);
663
+ const rawName = rawNameInput || "Contract";
664
+ const langChoice = await promptSelect("Language", ["TypeScript", "JavaScript"]);
665
+ const lang = langChoice === "JavaScript" ? "js" : "ts";
666
+ const outRaw = `./contracts/${nameToKebab(rawName)}.${lang}`;
667
+ const { networkArg, rpcUrl, passphrase } = await promptNetwork();
668
+ process.stderr.write(`
669
+ ${A.dim}`);
670
+ process.stderr.write(` Contract : ${contractId}
671
+ `);
672
+ process.stderr.write(` Network : ${networkArg ?? "custom"}
673
+ `);
674
+ process.stderr.write(` Interface : ${rawName}
675
+ `);
676
+ process.stderr.write(` Language : ${langChoice}
677
+ `);
678
+ process.stderr.write(` Output : ${outRaw}
679
+ `);
680
+ process.stderr.write(A.reset);
681
+ await promptEnter(`${A.dim} Press Enter to generate, Ctrl+C to cancel${A.reset} `);
682
+ const opts = { contractId, lang, rawOut: outRaw, rawName };
683
+ if (networkArg) opts.networkArg = networkArg;
684
+ if (rpcUrl) opts.rpcUrl = rpcUrl;
685
+ if (passphrase) opts.passphrase = passphrase;
686
+ await runGenerate(opts);
687
+ }
688
+ async function runGenerate(opts) {
689
+ const { contractId, networkArg, rpcUrl, passphrase, rawName, aliasHint, lang } = opts;
690
+ const interfaceName = rawName ?? (opts.rawOut ? deriveInterfaceName(opts.rawOut) : "Contract");
691
+ const outPath = opts.rawOut ?? `./contracts/${nameToKebab(interfaceName)}.${lang}`;
692
+ if (!networkArg && !(rpcUrl && passphrase)) {
693
+ process.stderr.write("Error: --network or (--rpc-url + --passphrase) is required\n");
694
+ process.exit(1);
695
+ }
696
+ const network = rpcUrl && passphrase ? { rpcUrl, networkPassphrase: passphrase, horizonUrl: "" } : resolveNetwork(networkArg);
697
+ const server = createServer(network);
698
+ const networkLabel = networkArg ?? rpcUrl ?? "custom";
699
+ process.stderr.write(`
700
+ Fetching spec for ${contractId} on ${networkLabel}...
701
+ `);
702
+ let spec;
703
+ try {
704
+ spec = await fetchContractSpec(contractId, server, network);
705
+ } catch (err) {
706
+ if (isContractKitError(err)) {
707
+ process.stderr.write(`Error [${err.code}]: ${err.message}
708
+ `);
709
+ } else {
710
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
711
+ `);
712
+ }
713
+ process.exit(1);
714
+ }
715
+ process.stderr.write(`Generating types for interface "${interfaceName}"...
716
+ `);
717
+ const absOut = resolve(outPath);
718
+ const dir = dirname(absOut);
719
+ try {
720
+ mkdirSync(dir, { recursive: true });
721
+ } catch (err) {
722
+ process.stderr.write(`Error: could not create directory "${dir}": ${err instanceof Error ? err.message : String(err)}
723
+ `);
724
+ process.exit(1);
725
+ }
726
+ const ext = extname(absOut);
727
+ const base = basename(absOut, ext);
728
+ const exampleExt = lang === "js" ? ".example.js" : ".example.ts";
729
+ const examplePath = join(dir, `${base}${exampleExt}`);
730
+ const aliasImport = detectAliasImport(outPath, aliasHint && aliasHint !== "true" ? aliasHint : void 0);
731
+ if (aliasImport) {
732
+ process.stderr.write(`Using path alias: ${aliasImport}.js
733
+ `);
734
+ }
735
+ const output = lang === "js" ? generateOutputJs(spec, contractId, interfaceName, networkLabel) : generateOutput(spec, contractId, interfaceName, networkLabel);
736
+ const example = lang === "js" ? generateExampleJs(spec, contractId, interfaceName, base, networkLabel, outPath, aliasImport ?? void 0) : generateExample(spec, contractId, interfaceName, base, networkLabel, outPath, aliasImport ?? void 0);
737
+ try {
738
+ writeFileSync(absOut, output, "utf-8");
739
+ writeFileSync(examplePath, example, "utf-8");
740
+ } catch (err) {
741
+ process.stderr.write(`Error: could not write output file: ${err instanceof Error ? err.message : String(err)}
742
+ `);
743
+ process.exit(1);
744
+ }
745
+ process.stderr.write(`${A.green}Done.${A.reset}
746
+ Types -> ${absOut}
747
+ Example -> ${examplePath}
748
+ `);
749
+ }
750
+ async function runInspect(opts) {
751
+ const { contractId, networkArg, rpcUrl, passphrase } = opts;
752
+ if (!networkArg && !(rpcUrl && passphrase)) {
753
+ process.stderr.write("Error: --network or (--rpc-url + --passphrase) is required\n");
754
+ process.exit(1);
755
+ }
756
+ const network = rpcUrl && passphrase ? { rpcUrl, networkPassphrase: passphrase, horizonUrl: "" } : resolveNetwork(networkArg);
757
+ const server = createServer(network);
758
+ const networkLabel = networkArg ?? rpcUrl ?? "custom";
759
+ process.stderr.write(`
760
+ Fetching spec for ${contractId} on ${networkLabel}...
761
+ `);
762
+ let spec;
763
+ try {
764
+ spec = await fetchContractSpec(contractId, server, network);
765
+ } catch (err) {
766
+ if (isContractKitError(err)) {
767
+ process.stderr.write(`Error [${err.code}]: ${err.message}
768
+ `);
769
+ } else {
770
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
771
+ `);
772
+ }
773
+ process.exit(1);
774
+ }
775
+ const entries = spec.entries;
776
+ const fns = spec.funcs();
777
+ const w = (s) => process.stdout.write(s);
778
+ const pad = (s, n) => s + " ".repeat(Math.max(0, n - s.length));
779
+ w(`
780
+ `);
781
+ w(`${A.bold}Contract${A.reset} : ${contractId}
782
+ `);
783
+ w(`${A.bold}Network ${A.reset} : ${networkLabel}
784
+ `);
785
+ w(`
786
+ ${A.bold}${A.cyan}Functions (${fns.length})${A.reset}
787
+ `);
788
+ for (const fn of fns) {
789
+ try {
790
+ const name = fn.name().toString();
791
+ const inputs = fn.inputs();
792
+ const outputs = fn.outputs();
793
+ const retType = outputs.length === 0 ? "void" : outputs.length === 1 ? toTs(outputs[0]) : `[${outputs.map(toTs).join(", ")}]`;
794
+ const params = inputs.map((i) => `${i.name().toString()}: ${toTs(i.type())}`).join(", ");
795
+ w(` ${A.green}${name}${A.reset}(${A.dim}${params}${A.reset}) ${A.dim}->${A.reset} ${retType}
796
+ `);
797
+ } catch {
798
+ }
799
+ }
800
+ const typeEntries = entries.filter((e) => {
801
+ const k = e.switch().name;
802
+ return k === "scSpecEntryUdtStructV0" || k === "scSpecEntryUdtEnumV0" || k === "scSpecEntryUdtErrorEnumV0" || k === "scSpecEntryUdtUnionV0";
803
+ });
804
+ if (typeEntries.length > 0) {
805
+ w(`
806
+ ${A.bold}${A.cyan}Custom Types (${typeEntries.length})${A.reset}
807
+ `);
808
+ for (const entry of typeEntries) {
809
+ try {
810
+ const kind = entry.switch().name;
811
+ if (kind === "scSpecEntryUdtStructV0") {
812
+ const s = entry.udtStructV0();
813
+ const fields = s.fields();
814
+ const name = s.name().toString();
815
+ const isTuple = fields.length > 0 && fields.every((f) => /^\d+$/.test(f.name().toString()));
816
+ if (isTuple) {
817
+ const types = fields.map((f) => toTs(f.type())).join(", ");
818
+ w(` ${A.dim}${pad("type", 6)}${A.reset} ${A.bold}${name}${A.reset} = [${types}]
819
+ `);
820
+ } else {
821
+ const fieldStr = fields.map((f) => `${f.name().toString()}: ${toTs(f.type())}`).join(", ");
822
+ w(` ${A.dim}${pad("struct", 6)}${A.reset} ${A.bold}${name}${A.reset} { ${fieldStr} }
823
+ `);
824
+ }
825
+ }
826
+ if (kind === "scSpecEntryUdtEnumV0" || kind === "scSpecEntryUdtErrorEnumV0") {
827
+ const isErr = kind === "scSpecEntryUdtErrorEnumV0";
828
+ const e = isErr ? entry.udtErrorEnumV0() : entry.udtEnumV0();
829
+ const name = e.name().toString();
830
+ const cases = e.cases().map((c) => `${c.name().toString()} = ${c.value()}`).join(", ");
831
+ w(` ${A.dim}${pad(isErr ? "error" : "enum", 6)}${A.reset} ${A.bold}${name}${A.reset} { ${cases} }
832
+ `);
833
+ }
834
+ if (kind === "scSpecEntryUdtUnionV0") {
835
+ const u = entry.udtUnionV0();
836
+ const name = u.name().toString();
837
+ const variants = u.cases().map((c) => {
838
+ const isVoid = c.switch().name === "scSpecUdtUnionCaseVoidV0";
839
+ const inner = isVoid ? c.voidV0() : c.tupleV0();
840
+ const tag = inner.name().toString();
841
+ if (isVoid) return `'${tag}'`;
842
+ const types = inner.type().map(toTs);
843
+ return types.length === 1 ? `'${tag}'(${types[0]})` : `'${tag}'(${types.join(", ")})`;
844
+ }).join(" | ");
845
+ w(` ${A.dim}${pad("union", 6)}${A.reset} ${A.bold}${name}${A.reset} ${variants}
846
+ `);
847
+ }
848
+ } catch {
849
+ }
850
+ }
851
+ }
852
+ w(`
853
+ `);
854
+ }
855
+ function printHelp() {
856
+ process.stdout.write(`
857
+ ${A.bold}sck${A.reset} (stellar-contracts-kit): TypeScript SDK for Soroban smart contracts
858
+
859
+ Commands:
860
+ generate Generate TypeScript types and example file for a contract
861
+ inspect Print contract functions and custom types to the terminal
862
+
863
+ Usage:
864
+ npx sck Interactive mode with arrow-key selection
865
+ npx sck generate [options] Generate types (non-interactive)
866
+ npx sck inspect [options] Inspect a contract (non-interactive)
867
+ npx sck [options] Shorthand for generate (backwards-compatible)
868
+
869
+ Options (generate + inspect):
870
+ --contract Contract address (C..., 56 chars)
871
+ --network testnet | mainnet | futurenet
872
+ --rpc-url Custom RPC URL (use with --passphrase instead of --network)
873
+ --passphrase Custom network passphrase
874
+
875
+ Options (generate only):
876
+ --out Output file path (default: ./contracts/<name>.ts)
877
+ --name Interface name (default: derived from --out filename)
878
+ --alias Path alias prefix, e.g. @ (auto-detected from tsconfig.json)
879
+ --js Generate JavaScript output instead of TypeScript
880
+ --help, -h Show this help
881
+
882
+ Examples:
883
+ npx sck
884
+ npx sck generate --contract CABC... --network testnet
885
+ npx sck inspect --contract CABC... --network testnet
886
+ npx sck generate --contract CABC... --network testnet --out src/contracts/counter.ts
887
+ `);
888
+ }
889
+ async function main() {
890
+ const rawArgs = process.argv.slice(2);
891
+ if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
892
+ printHelp();
893
+ process.exit(0);
894
+ }
895
+ const subcommand = rawArgs[0] && !rawArgs[0].startsWith("--") ? rawArgs[0] : void 0;
896
+ const flagArgs = subcommand ? rawArgs.slice(1) : rawArgs;
897
+ const args = {};
898
+ for (let i = 0; i < flagArgs.length; i++) {
899
+ if (flagArgs[i].startsWith("--")) {
900
+ const key = flagArgs[i].slice(2);
901
+ const next = flagArgs[i + 1];
902
+ args[key] = next && !next.startsWith("--") ? (i++, next) : "true";
903
+ }
904
+ }
905
+ const contractId = args["contract"] ?? args["contract-id"];
906
+ if (subcommand === "inspect") {
907
+ if (!contractId) {
908
+ process.stderr.write("Error: --contract is required\n");
909
+ process.stderr.write("Usage: npx sck inspect --contract <ID> --network <preset>\n");
910
+ process.stderr.write(" npx sck (interactive mode)\n");
911
+ process.exit(1);
912
+ }
913
+ await runInspect({
914
+ contractId,
915
+ networkArg: args["network"],
916
+ rpcUrl: args["rpc-url"],
917
+ passphrase: args["passphrase"]
918
+ });
919
+ return;
920
+ }
921
+ if (subcommand && subcommand !== "generate") {
922
+ process.stderr.write(`Error: unknown command '${subcommand}'
923
+ `);
924
+ process.stderr.write("Run npx sck --help for usage.\n");
925
+ process.exit(1);
926
+ }
927
+ if (!contractId) {
928
+ if (!process.stdin.isTTY) {
929
+ process.stderr.write("Error: --contract is required (interactive mode needs a terminal)\n");
930
+ process.stderr.write("Usage: npx sck --contract <ID> --network <preset>\n");
931
+ process.exit(1);
932
+ }
933
+ await runInteractive();
934
+ return;
935
+ }
936
+ await runGenerate({
937
+ contractId,
938
+ networkArg: args["network"],
939
+ rpcUrl: args["rpc-url"],
940
+ passphrase: args["passphrase"],
941
+ rawOut: args["out"] ?? args["output"],
942
+ rawName: args["name"],
943
+ aliasHint: args["alias"],
944
+ lang: args["js"] === "true" ? "js" : "ts"
945
+ });
946
+ }
947
+ main().catch((err) => {
948
+ process.stderr.write(`Unexpected error: ${err instanceof Error ? err.message : String(err)}
949
+ `);
950
+ process.exit(1);
951
+ });