@stellar/stellar-sdk
Version:
A library for working with the Stellar network, including communication with the Horizon and Soroban RPC servers.
358 lines (353 loc) • 13.9 kB
JavaScript
import '../node_modules/.pnpm/@stellar_js-xdr@4.0.0/node_modules/@stellar/js-xdr/src/int.js';
import '../node_modules/.pnpm/@stellar_js-xdr@4.0.0/node_modules/@stellar/js-xdr/src/hyper.js';
import '../node_modules/.pnpm/@stellar_js-xdr@4.0.0/node_modules/@stellar/js-xdr/src/unsigned-int.js';
import '../node_modules/.pnpm/@stellar_js-xdr@4.0.0/node_modules/@stellar/js-xdr/src/unsigned-hyper.js';
import '../node_modules/.pnpm/@stellar_js-xdr@4.0.0/node_modules/@stellar/js-xdr/src/xdr-type.js';
import 'buffer';
import types from '../base/generated/curr_generated.js';
import '@noble/hashes/sha2.js';
import '../base/signing.js';
import '../base/keypair.js';
import 'base32.js';
import '../base/util/continued_fraction.js';
import '../base/util/bignumber.js';
import '../base/transaction_builder.js';
import '../base/muxed_account.js';
import '../base/scval.js';
import '../base/numbers/uint128.js';
import '../base/numbers/uint256.js';
import '../base/numbers/int128.js';
import '../base/numbers/int256.js';
import { isTupleStruct, generateTypeImports, formatImports, sanitizeIdentifier, formatJSDocComment, parseTypeFromTypeDef, escapeStringLiteral, toPascalCase } from './utils.js';
class TypeGenerator {
spec;
// event index (in event-entry declaration order) -> resolved (possibly
// disambiguated) interface name. Keyed by index rather than raw name
// because a contract may declare several events with the same name.
// Lazily computed on first use; see resolveEventInterfaceNames().
eventInterfaceNames = null;
constructor(spec) {
this.spec = spec;
}
/**
* Generate all TypeScript type definitions
*/
generate() {
let eventIndex = 0;
const types$1 = this.spec.entries.map(
(entry) => entry.switch() === types.ScSpecEntryKind.scSpecEntryEventV0() ? this.generateEvent(entry.eventV0(), eventIndex++) : this.generateEntry(entry)
).filter((t) => t).join("\n\n");
const imports = this.generateImports();
const eventUnion = this.generateContractEventUnion();
return `${imports}
${types$1}
${eventUnion}
`;
}
/**
* Generate TypeScript for a single spec entry
*/
generateEntry(entry) {
switch (entry.switch()) {
case types.ScSpecEntryKind.scSpecEntryUdtStructV0():
if (isTupleStruct(entry.udtStructV0())) {
return this.generateTupleStruct(entry.udtStructV0());
}
return this.generateStruct(entry.udtStructV0());
case types.ScSpecEntryKind.scSpecEntryUdtUnionV0():
return this.generateUnion(entry.udtUnionV0());
case types.ScSpecEntryKind.scSpecEntryUdtEnumV0():
return this.generateEnum(entry.udtEnumV0());
case types.ScSpecEntryKind.scSpecEntryUdtErrorEnumV0():
return this.generateErrorEnum(entry.udtErrorEnumV0());
// Events are handled directly in generate(), which numbers them in
// declaration order.
default:
return null;
}
}
generateImports() {
const imports = generateTypeImports(
this.spec.entries.flatMap((entry) => {
switch (entry.switch()) {
case types.ScSpecEntryKind.scSpecEntryUdtStructV0():
return entry.udtStructV0().fields().map((field) => field.type());
case types.ScSpecEntryKind.scSpecEntryUdtUnionV0():
return entry.udtUnionV0().cases().flatMap((unionCase) => {
if (unionCase.switch() === types.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseTupleV0()) {
return unionCase.tupleCase().type();
}
return [];
});
case types.ScSpecEntryKind.scSpecEntryUdtEnumV0():
return [];
case types.ScSpecEntryKind.scSpecEntryUdtErrorEnumV0():
return [];
case types.ScSpecEntryKind.scSpecEntryEventV0():
return entry.eventV0().params().map((param) => param.type());
default:
return [];
}
})
);
return formatImports(imports, {
includeTypeFileImports: false
// Types file doesn't import from itself
});
}
/**
* Generate TypeScript interface for a struct
*/
generateStruct(struct) {
const name = sanitizeIdentifier(struct.name().toString());
const doc = formatJSDocComment(
struct.doc().toString() || `Struct: ${name}`,
0
);
const fields = struct.fields().map((field) => {
const fieldName = sanitizeIdentifier(field.name().toString());
const fieldType = parseTypeFromTypeDef(field.type());
const fieldDoc = formatJSDocComment(field.doc().toString(), 2);
return `${fieldDoc} ${fieldName}: ${fieldType};`;
}).join("\n");
return `${doc}export interface ${name} {
${fields}
}`;
}
/**
* Generate TypeScript union type
*/
generateUnion(union) {
const name = sanitizeIdentifier(union.name().toString());
const doc = formatJSDocComment(
union.doc().toString() || `Union: ${name}`,
0
);
const cases = union.cases().map((unionCase) => this.generateUnionCase(unionCase));
const caseTypes = cases.map((c) => {
if (c.types.length > 0) {
return `${formatJSDocComment(c.doc, 2)} { tag: "${escapeStringLiteral(c.name)}"; values: readonly [${c.types.join(", ")}] }`;
}
return `${formatJSDocComment(c.doc, 2)} { tag: "${escapeStringLiteral(c.name)}"; values: void }`;
}).join(" |\n");
return `${doc} export type ${name} =
${caseTypes};`;
}
/**
* Generate TypeScript enum
*/
generateEnum(enumEntry) {
const name = sanitizeIdentifier(enumEntry.name().toString());
const doc = formatJSDocComment(
enumEntry.doc().toString() || `Enum: ${name}`,
0
);
const members = enumEntry.cases().map((enumCase) => {
const caseName = sanitizeIdentifier(enumCase.name().toString());
const caseValue = enumCase.value();
const caseDoc = enumCase.doc().toString() || `Enum Case: ${caseName}`;
return `${formatJSDocComment(caseDoc, 2)} ${caseName} = ${caseValue}`;
}).join(",\n");
return `${doc}export enum ${name} {
${members}
}`;
}
/**
* Generate TypeScript error enum
*/
generateErrorEnum(errorEnum) {
const name = sanitizeIdentifier(errorEnum.name().toString());
const doc = formatJSDocComment(
errorEnum.doc().toString() || `Error Enum: ${name}`,
0
);
const cases = errorEnum.cases().map((enumCase) => this.generateEnumCase(enumCase));
const members = cases.map((c) => {
return `${formatJSDocComment(c.doc, 2)} ${c.value} : { message: "${escapeStringLiteral(c.name)}" }`;
}).join(",\n");
return `${doc}export const ${name} = {
${members}
}`;
}
/**
* Generate union case
*/
generateUnionCase(unionCase) {
switch (unionCase.switch()) {
case types.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseVoidV0(): {
const voidCase = unionCase.voidCase();
return {
doc: voidCase.doc().toString(),
name: voidCase.name().toString(),
types: []
};
}
case types.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseTupleV0(): {
const tupleCase = unionCase.tupleCase();
return {
doc: tupleCase.doc().toString(),
name: tupleCase.name().toString(),
types: tupleCase.type().map((t) => parseTypeFromTypeDef(t))
};
}
default:
throw new Error(`Unknown union case kind: ${unionCase.switch()}`);
}
}
/**
* Generate enum case
*/
generateEnumCase(enumCase) {
return {
doc: enumCase.doc().toString(),
name: enumCase.name().toString(),
value: enumCase.value()
};
}
/**
* Compute the exported TS interface name for an event, e.g. "transfer"
* becomes "TransferEvent". Resolved (and disambiguated if necessary) via
* {@link resolveEventInterfaceNames}, so every call site agrees.
*/
eventInterfaceName(event, eventIndex) {
const resolved = this.resolveEventInterfaceNames().get(eventIndex);
if (resolved === void 0) {
return `${toPascalCase(sanitizeIdentifier(event.name().toString()))}Event`;
}
return resolved;
}
/**
* The resolved (possibly disambiguated) interface name of every event in
* the spec, in declaration order. Exposed so callers (e.g. the bindings
* generator's diagnostics) can report renames and duplicates.
*/
eventInterfaceNamesInOrder() {
return this.spec.events().map((event, eventIndex) => this.eventInterfaceName(event, eventIndex));
}
/**
* True if the given event's resolved interface name differs from its
* preferred (unsuffixed) form, i.e. it was disambiguated away from a
* collision.
*/
eventInterfaceNameWasRenamed(event, eventIndex) {
const preferred = `${toPascalCase(sanitizeIdentifier(event.name().toString()))}Event`;
return this.eventInterfaceName(event, eventIndex) !== preferred;
}
/**
* The name-normalization used for event interface names (and UDT type
* names) is not injective — e.g. events "FooBar" and "foo_bar" both
* produce the interface name "FooBarEvent", a contract may declare
* several events with the very same name (composed modules each emitting
* their own "transfer"), and an event can just as easily collide with a
* UDT (struct/union/enum) of the same generated name. Since UDT/function
* names are load-bearing (referenced directly in signatures) and
* event-derived names are already synthetic, UDT names always win: they
* are reserved first, in spec-entry order. Events are then resolved in
* spec-entry order, appending the smallest integer 2 or greater needed to
* make the name unique (and reserving whatever name results, so later
* events see it too). This is deterministic for a given spec.
*/
resolveEventInterfaceNames() {
if (this.eventInterfaceNames !== null) {
return this.eventInterfaceNames;
}
const reserved = /* @__PURE__ */ new Set(["ContractEvent"]);
for (const entry of this.spec.entries) {
switch (entry.switch()) {
case types.ScSpecEntryKind.scSpecEntryUdtStructV0():
reserved.add(
sanitizeIdentifier(entry.udtStructV0().name().toString())
);
break;
case types.ScSpecEntryKind.scSpecEntryUdtUnionV0():
reserved.add(
sanitizeIdentifier(entry.udtUnionV0().name().toString())
);
break;
case types.ScSpecEntryKind.scSpecEntryUdtEnumV0():
reserved.add(sanitizeIdentifier(entry.udtEnumV0().name().toString()));
break;
case types.ScSpecEntryKind.scSpecEntryUdtErrorEnumV0():
reserved.add(
sanitizeIdentifier(entry.udtErrorEnumV0().name().toString())
);
break;
}
}
const resolved = /* @__PURE__ */ new Map();
let eventIndex = 0;
for (const entry of this.spec.entries) {
if (entry.switch() !== types.ScSpecEntryKind.scSpecEntryEventV0()) {
continue;
}
const event = entry.eventV0();
const preferred = `${toPascalCase(sanitizeIdentifier(event.name().toString()))}Event`;
let candidate = preferred;
let suffix = 2;
while (reserved.has(candidate)) {
candidate = `${preferred}${suffix}`;
suffix += 1;
}
reserved.add(candidate);
resolved.set(eventIndex, candidate);
eventIndex += 1;
}
this.eventInterfaceNames = resolved;
return resolved;
}
/**
* Generate TypeScript interface for a Soroban contract event
*/
generateEvent(event, eventIndex) {
const rawName = event.name().toString();
const name = this.eventInterfaceName(event, eventIndex);
const preferred = `${toPascalCase(sanitizeIdentifier(rawName))}Event`;
const renameNote = this.eventInterfaceNameWasRenamed(event, eventIndex) ? `
Note: renamed from "${preferred}" to avoid a collision with another generated name.` : "";
const doc = formatJSDocComment(
(event.doc().toString() || `Event: ${rawName}`) + renameNote,
0
);
const fieldKey = (rawParamName) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(rawParamName) ? rawParamName : `"${escapeStringLiteral(rawParamName)}"`;
const dataIsMapFormat = event.dataFormat().value === types.ScSpecEventDataFormat.scSpecEventDataFormatMap().value;
const dataFields = event.params().map((param) => {
const fieldName = fieldKey(param.name().toString());
const fieldType = parseTypeFromTypeDef(param.type());
const fieldDoc = formatJSDocComment(param.doc().toString(), 4);
const optional = dataIsMapFormat && param.location().value === types.ScSpecEventParamLocationV0.scSpecEventParamLocationData().value;
return `${fieldDoc} ${fieldName}${optional ? "?" : ""}: ${fieldType};`;
}).join("\n");
return `${doc}export interface ${name} {
name: "${escapeStringLiteral(rawName)}";
data: {
${dataFields}
};
}`;
}
/**
* Generate the discriminated union of all contract events, if the spec defines any.
*/
generateContractEventUnion() {
const eventEntries = this.spec.entries.filter(
(entry) => entry.switch() === types.ScSpecEntryKind.scSpecEntryEventV0()
);
if (eventEntries.length === 0) {
return "";
}
const names = eventEntries.map(
(entry, eventIndex) => this.eventInterfaceName(entry.eventV0(), eventIndex)
);
return `export type ContractEvent = ${names.join(" | ")};`;
}
generateTupleStruct(udtStruct) {
const name = sanitizeIdentifier(udtStruct.name().toString());
const doc = formatJSDocComment(
udtStruct.doc().toString() || `Tuple Struct: ${name}`,
0
);
const types = udtStruct.fields().map((field) => parseTypeFromTypeDef(field.type())).join(", ");
return `${doc}export type ${name} = readonly [${types}];`;
}
}
export { TypeGenerator };
//# sourceMappingURL=types.js.map