UNPKG

@stellar/stellar-sdk

Version:

A library for working with the Stellar network, including communication with the Horizon and Soroban RPC servers.

309 lines (303 loc) 12.8 kB
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 { generateTypeImports, formatImports, sanitizeIdentifier, toCamelCase, escapeStringLiteral, parseTypeFromTypeDef, formatJSDocComment } from './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 = 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 === types.ScSpecEventParamLocationV0.scSpecEventParamLocationTopicList().value ); topicParams.forEach((param) => { const nested = 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 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) => 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(sanitizeIdentifier(func.name().toString())); }); const resolved = /* @__PURE__ */ new Map(); this.spec.events().forEach((event, eventIndex) => { const preferred = `${toCamelCase(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 `${toCamelCase(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 = `${toCamelCase(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 === types.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 : `"${escapeStringLiteral(rawParamName)}"`; const fieldType = parseTypeFromTypeDef(param.type(), true); return `${fieldName}?: ${fieldType}`; }).join("; "); const doc = 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 = 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 = sanitizeIdentifier(func.name().toString()); const inputs = func.inputs().map((input) => ({ name: sanitizeIdentifier(input.name().toString()), type: parseTypeFromTypeDef(input.type(), true) })); 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}>>;`; } generateFromJSONMethod(func) { const name = sanitizeIdentifier(func.name().toString()); const outputType = func.outputs().length > 0 ? 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: sanitizeIdentifier(input.name().toString()), type: 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(", "); } } export { ClientGenerator }; //# sourceMappingURL=client.js.map