@stellar/stellar-sdk
Version:
A library for working with the Stellar network, including communication with the Horizon and Soroban RPC servers.
311 lines (304 loc) • 13 kB
JavaScript
;
require('../node_modules/.pnpm/@stellar_js-xdr@4.0.0/node_modules/@stellar/js-xdr/src/int.js');
require('../node_modules/.pnpm/@stellar_js-xdr@4.0.0/node_modules/@stellar/js-xdr/src/hyper.js');
require('../node_modules/.pnpm/@stellar_js-xdr@4.0.0/node_modules/@stellar/js-xdr/src/unsigned-int.js');
require('../node_modules/.pnpm/@stellar_js-xdr@4.0.0/node_modules/@stellar/js-xdr/src/unsigned-hyper.js');
require('../node_modules/.pnpm/@stellar_js-xdr@4.0.0/node_modules/@stellar/js-xdr/src/xdr-type.js');
require('buffer');
var curr_generated = require('../base/generated/curr_generated.js');
require('@noble/hashes/sha2.js');
require('../base/signing.js');
require('../base/keypair.js');
require('base32.js');
require('../base/util/continued_fraction.js');
require('../base/util/bignumber.js');
require('../base/transaction_builder.js');
require('../base/muxed_account.js');
require('../base/scval.js');
require('../base/numbers/uint128.js');
require('../base/numbers/uint256.js');
require('../base/numbers/int128.js');
require('../base/numbers/int256.js');
var utils = require('./utils.js');
class ClientGenerator {
spec;
// event index (in declaration order) -> resolved (possibly disambiguated)
// filter method 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 resolveEventFilterMethodNames().
eventFilterMethodNames = null;
constructor(spec) {
this.spec = spec;
}
/**
* Generate client class
*/
generate() {
let deployMethod = "";
try {
const constructorFunc = this.spec.getFunc("__constructor");
deployMethod = this.generateDeployMethod(constructorFunc);
} catch {
deployMethod = this.generateDeployMethod(void 0);
}
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(",");
const events = this.spec.events();
const parseEventMethodName = this.parseEventMethodName();
const eventMethods = events.length > 0 ? `
${this.generateParseEventMethod(parseEventMethodName)}
${events.map(
(event, eventIndex) => this.generateEventFilterMethod(event, eventIndex)
).join("\n")}` : "";
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}
};
${eventMethods}
}`;
}
generateImports() {
const imports = utils.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;
})
);
const events = this.spec.events();
if (events.length > 0) {
imports.typeFileImports.add("ContractEvent");
imports.stellarImports.add("xdr");
events.forEach((event) => {
const topicParams = event.params().filter(
(param) => param.location().value === curr_generated.default.ScSpecEventParamLocationV0.scSpecEventParamLocationTopicList().value
);
topicParams.forEach((param) => {
const nested = utils.generateTypeImports([param.type()]);
nested.typeFileImports.forEach((t) => imports.typeFileImports.add(t));
nested.stellarContractImports.forEach(
(t) => imports.stellarContractImports.add(t)
);
nested.stellarImports.forEach((t) => imports.stellarImports.add(t));
imports.needsBufferImport = imports.needsBufferImport || nested.needsBufferImport;
});
});
}
return utils.formatImports(imports, {
includeTypeFileImports: true,
// Client imports types
additionalStellarContractImports: [
"Spec",
"AssembledTransaction",
"Client as ContractClient",
"ClientOptions as ContractClientOptions",
"MethodOptions"
]
});
}
/**
* Generate the parseEvent method, which delegates to the underlying Spec
* to decode a raw event's topics/data into a typed ContractEvent.
*/
generateParseEventMethod(methodName) {
return ` /**
* Parse a raw contract event (topics + data) into a typed {@link ContractEvent}.
*/
${methodName}(topics: xdr.ScVal[] | string[], data: xdr.ScVal | string): ContractEvent | undefined {
return this.spec.parseEvent(topics, data) as ContractEvent | undefined;
}`;
}
parseEventMethodName() {
const reserved = new Set(
this.spec.funcs().filter((func) => func.name().toString() !== "__constructor").map((func) => utils.sanitizeIdentifier(func.name().toString()))
);
let candidate = "parseEvent";
let suffix = 2;
while (reserved.has(candidate)) {
candidate = `parseEvent${suffix}`;
suffix += 1;
}
return candidate;
}
/**
* The `<camelCase>EventFilter` method name derived from an event name is
* not injective (e.g. "FooBar" and "foo_bar" both produce
* "fooBarEventFilter"), a contract may declare several events with the
* very same name (composed modules each emitting their own "transfer"),
* and it can just as easily collide with a generated contract function
* name (e.g. an event "transfer" plus a function "transferEventFilter").
* Since function member names are load-bearing (called directly by
* users) and event filter names are already synthetic, function 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.
*/
resolveEventFilterMethodNames() {
if (this.eventFilterMethodNames !== null) {
return this.eventFilterMethodNames;
}
const reserved = /* @__PURE__ */ new Set();
this.spec.funcs().filter((func) => func.name().toString() !== "__constructor").forEach((func) => {
reserved.add(utils.sanitizeIdentifier(func.name().toString()));
});
const resolved = /* @__PURE__ */ new Map();
this.spec.events().forEach((event, eventIndex) => {
const preferred = `${utils.toCamelCase(utils.sanitizeIdentifier(event.name().toString()))}EventFilter`;
let candidate = preferred;
let suffix = 2;
while (reserved.has(candidate)) {
candidate = `${preferred}${suffix}`;
suffix += 1;
}
reserved.add(candidate);
resolved.set(eventIndex, candidate);
});
this.eventFilterMethodNames = resolved;
return resolved;
}
/**
* Compute the resolved (possibly disambiguated) filter method name for an
* event; see {@link resolveEventFilterMethodNames}.
*/
eventFilterMethodName(event, eventIndex) {
const resolved = this.resolveEventFilterMethodNames().get(eventIndex);
if (resolved === void 0) {
return `${utils.toCamelCase(utils.sanitizeIdentifier(event.name().toString()))}EventFilter`;
}
return resolved;
}
/**
* The resolved (possibly disambiguated) filter method 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.
*/
eventFilterMethodNamesInOrder() {
return this.spec.events().map(
(event, eventIndex) => this.eventFilterMethodName(event, eventIndex)
);
}
/**
* Generate a per-event helper that builds a topics-filter row for
* `Api.EventFilter.topics`, suitable for passing to server.getEvents.
*/
generateEventFilterMethod(event, eventIndex) {
const rawName = event.name().toString();
const methodName = this.eventFilterMethodName(event, eventIndex);
const preferredMethodName = `${utils.toCamelCase(utils.sanitizeIdentifier(rawName))}EventFilter`;
const occurrence = this.spec.events().slice(0, eventIndex).filter((e) => e.name().toString() === rawName).length;
const occurrenceNote = occurrence > 0 ? ` This targets declaration ${occurrence + 1} of the "${rawName}" event in the contract spec.` : "";
const renameNote = methodName !== preferredMethodName ? ` Note: renamed from "${preferredMethodName}" to avoid a collision with another generated name.` : "";
const topicParams = event.params().filter(
(param) => param.location().value === curr_generated.default.ScSpecEventParamLocationV0.scSpecEventParamLocationTopicList().value
);
const fields = topicParams.map((param) => {
const rawParamName = param.name().toString();
const fieldName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(rawParamName) ? rawParamName : `"${utils.escapeStringLiteral(rawParamName)}"`;
const fieldType = utils.parseTypeFromTypeDef(param.type(), true);
return `${fieldName}?: ${fieldType}`;
}).join("; ");
const doc = utils.formatJSDocComment(
`Build a topics filter row for the "${rawName}" event, for use in \`Api.EventFilter.topics\` when calling \`server.getEvents\`. Omitted fields match any value.${occurrenceNote}${renameNote}`,
2
);
const escapedName = utils.escapeStringLiteral(rawName);
const occurrenceArg = occurrence > 0 ? `, ${occurrence}` : "";
if (topicParams.length === 0) {
return `${doc} ${methodName}(): string[] {
return this.spec.eventTopicFilter("${escapedName}"${occurrence > 0 ? `, undefined${occurrenceArg}` : ""});
}`;
}
return `${doc} ${methodName}(topicValues?: { ${fields} }): string[] {
return this.spec.eventTopicFilter("${escapedName}", topicValues${occurrenceArg});
}`;
}
/**
* Generate interface method signature
*/
generateInterfaceMethod(func) {
const name = utils.sanitizeIdentifier(func.name().toString());
const inputs = func.inputs().map((input) => ({
name: utils.sanitizeIdentifier(input.name().toString()),
type: utils.parseTypeFromTypeDef(input.type(), true)
}));
const outputType = func.outputs().length > 0 ? utils.parseTypeFromTypeDef(func.outputs()[0]) : "void";
const docs = utils.formatJSDocComment(func.doc().toString(), 2);
const params = this.formatMethodParameters(inputs);
return `${docs} ${name}(${params}): Promise<AssembledTransaction<${outputType}>>;`;
}
generateFromJSONMethod(func) {
const name = utils.sanitizeIdentifier(func.name().toString());
const outputType = func.outputs().length > 0 ? utils.parseTypeFromTypeDef(func.outputs()[0]) : "void";
return ` ${name} : this.txFromJSON<${outputType}>`;
}
/**
* Generate deploy method
*/
generateDeployMethod(constructorFunc) {
if (!constructorFunc) {
const params2 = this.formatConstructorParameters([]);
return ` static deploy<T = Client>(${params2}): Promise<AssembledTransaction<T>> {
return ContractClient.deploy(null, options);
}`;
}
const inputs = constructorFunc.inputs().map((input) => ({
name: utils.sanitizeIdentifier(input.name().toString()),
type: utils.parseTypeFromTypeDef(input.type(), true)
}));
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
*/
formatMethodParameters(inputs) {
const params = [];
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
*/
formatConstructorParameters(inputs) {
const params = [];
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(", ");
}
}
exports.ClientGenerator = ClientGenerator;
//# sourceMappingURL=client.js.map