UNPKG

@eliseev_s/tolk-tlb-transpiler

Version:

Transpile Tolk structs to TLB definitions and generate TypeScript wrappers for TON blockchain smart contracts

827 lines 34.7 kB
"use strict"; // ============================================================================ // TypeScript Wrapper Generator - Generate Smart Contract Wrapper Classes // ============================================================================ Object.defineProperty(exports, "__esModule", { value: true }); exports.WrapperGenerator = void 0; const tlb_codegen_1 = require("@ton-community/tlb-codegen"); class WrapperGenerator { config; additionalImports = []; utilityTypes = []; wrapperClass = ''; typescriptCode = ''; constructor(config) { this.config = config; } /** * Create and initialize a ready-to-use WrapperGenerator instance * * @param config - Configuration for the wrapper generator * @returns Promise resolving to initialized WrapperGenerator instance */ static async create(config) { const generator = new WrapperGenerator(config); await generator.initialize(); generator.generateImports().generateUtilityTypes().generateWrapperClass(); return generator; } /** * Initialize wrapper generator with TypeScript code generation */ async initialize() { // Generate TypeScript code from TLB definitions const commonTLB = `// Common TLB definitions bool_false$0 = Bool; bool_true$1 = Bool; nothing$0 {X:Type} = Maybe X; just$1 {X:Type} value:X = Maybe X; left$0 {X:Type} {Y:Type} value:X = Either X Y; right$1 {X:Type} {Y:Type} value:Y = Either X Y; pair$_ {X:Type} {Y:Type} first:X second:Y = Both X Y; `; const tlbString = this.formatTlbDefinitions(); this.typescriptCode = await (0, tlb_codegen_1.generateCodeFromData)(commonTLB + tlbString, 'typescript'); } /** * Format TLB definitions into a readable string format */ formatTlbDefinitions() { const output = []; output.push('// ============================================================================'); output.push(`// TLB Schema for ${this.config.contractName}`); output.push('// ============================================================================'); output.push(''); for (const definition of this.config.tlbDefinitions) { output.push(`// ${definition.name}`); for (const constructor of definition.statement) { output.push(constructor); } output.push(''); } return output.join('\n'); } /** * Generate additional imports needed for wrapper */ generateImports() { this.additionalImports = [ "import { Contract, contractAddress, ContractProvider, Sender, SendMode } from '@ton/core';", ]; return this; } /** * Generate utility types for send methods */ generateUtilityTypes() { const { internalMessageType, structures, typeAliases } = this.config; // Extract send methods to generate types for them const sendMethods = WrapperGenerator.extractSendMethods(internalMessageType, structures, typeAliases); this.utilityTypes = [ "// Utility type to exclude 'kind' field from send method parameters", "type OmitKind<T> = T extends { kind: any } ? Omit<T, 'kind'> : T;", '', '// Body types for send methods', ...sendMethods.map((method) => `export type ${method.messageType}Body = OmitKind<${method.messageType}>;`), ]; return this; } /** * Generate wrapper class */ generateWrapperClass() { const { contractName, storageType, internalMessageType, getMethods, structures, typeAliases, } = this.config; // Extract send methods from internal message union const sendMethods = WrapperGenerator.extractSendMethods(internalMessageType, structures, typeAliases); const parts = [ WrapperGenerator.generateClassDeclaration(contractName), WrapperGenerator.generateCreateFromAddress(contractName), WrapperGenerator.generateCreateFromConfig(storageType, contractName), WrapperGenerator.generateSendDeploy(), ...sendMethods.map((method) => WrapperGenerator.generateCreateBodyMethod(method)), ...sendMethods.map((method) => WrapperGenerator.generateSendMethod(method, contractName)), ...getMethods.map((method) => WrapperGenerator.generateGetMethod(method, structures)), WrapperGenerator.generateClassEnd(), ]; this.wrapperClass = parts.join('\n\n'); return this; } /** * Render the complete wrapper code */ render() { // Parse TypeScript code to find import section const lines = this.typescriptCode.split('\n'); const importEndIndex = lines.findIndex((line) => line.trim() === '' && lines.slice(0, lines.indexOf(line)).some((l) => l.startsWith('import'))); let fullCode; if (importEndIndex > 0) { // Check if beginCell is already imported const existingImports = lines.slice(0, importEndIndex).join('\n'); const hasBeginCell = existingImports.includes('beginCell'); // Filter out beginCell from additional imports if it's already imported const filteredImports = this.additionalImports.map((importLine) => { if (hasBeginCell && importLine.includes('beginCell')) { return importLine .replace(/,\s*beginCell\s*/, '') .replace(/beginCell\s*,\s*/, ''); } return importLine; }); // Insert additional imports after existing imports lines.splice(importEndIndex, 0, ...filteredImports); const modifiedTypescriptCode = lines.join('\n'); // Combine with utility types and wrapper class const parts = [ modifiedTypescriptCode, this.utilityTypes.join('\n'), this.wrapperClass, ].filter((part) => part.trim()); fullCode = parts.join('\n\n'); } else { // Fallback: combine all parts const parts = [ this.typescriptCode, this.additionalImports.join('\n'), this.utilityTypes.join('\n'), this.wrapperClass, ].filter((part) => part.trim()); fullCode = parts.join('\n\n'); } return fullCode; } /** * Get the generated TypeScript code */ getTypescriptCode() { return this.typescriptCode; } /** * Get the generated wrapper class code */ getWrapperCode() { return this.wrapperClass; } /** * Get the formatted TLB definitions */ getTlbString() { return this.formatTlbDefinitions(); } /** * Generate additional imports needed for wrapper (avoiding duplicates) */ static generateAdditionalImports() { // Only add imports that are not already present in TLB codegen output return `import { Contract, contractAddress, ContractProvider, Sender, SendMode } from '@ton/core';`; } /** * Generate utility types for send methods */ static generateUtilityTypes() { return `// Utility type to exclude 'kind' field from send method parameters type OmitKind<T> = T extends { kind: any } ? Omit<T, 'kind'> : T;`; } /** * Generate class declaration */ static generateClassDeclaration(contractName) { return `export class ${contractName} implements Contract { constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell }) {}`; } /** * Generate createFromAddress method */ static generateCreateFromAddress(contractName) { return ` static createFromAddress(address: Address) { return new ${contractName}(address); }`; } /** * Generate createFromConfig method */ static generateCreateFromConfig(storageType, contractName) { return ` static createFromConfig(storage: ${storageType}, code: Cell, workchain = 0) { const data = beginCell().store(store${storageType}(storage)).endCell(); const init = { code, data }; return new ${contractName}(contractAddress(workchain, init), init); }`; } /** * Generate sendDeploy method */ static generateSendDeploy() { return ` async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { await provider.internal(via, { value, sendMode: SendMode.PAY_GAS_SEPARATELY, body: beginCell().endCell(), }); }`; } /** * Generate send method for internal message */ static generateSendMethod(method, contractName) { const bodyTypeName = `${method.messageType}Body`; const createBodyMethodName = `create${method.messageType}Body`; return ` async ${method.name}( provider: ContractProvider, via: Sender, value: bigint, params: ${bodyTypeName} ) { await provider.internal(via, { value, sendMode: SendMode.PAY_GAS_SEPARATELY, body: ${contractName}.${createBodyMethodName}(params), }); }`; } /** * Generate static create body method for internal message */ static generateCreateBodyMethod(method) { const bodyTypeName = `${method.messageType}Body`; const createBodyMethodName = `create${method.messageType}Body`; return ` static ${createBodyMethodName}(params: ${bodyTypeName}) { return beginCell().store(store${method.messageType}({ ...params, kind: '${method.messageType}' })).endCell(); }`; } /** * Generate get method */ static generateGetMethod(method, structures) { // Normalize the method name (convert snake_case to camelCase) const normalizedName = WrapperGenerator.normalizeMethodName(method.name); // If the original method already starts with 'get', don't add another 'get' prefix const methodName = normalizedName.startsWith('get') ? normalizedName : `get${WrapperGenerator.capitalize(normalizedName)}`; const returnType = WrapperGenerator.tolkTypeToTSType(method.returnType); let parameters = ''; let args = ''; if (method.parameters.length > 0) { const paramStrings = method.parameters.map((param) => `${param.name}: ${WrapperGenerator.tolkTypeToTSType(param.type)}`); parameters = `, ${paramStrings.join(', ')}`; const argStrings = method.parameters.map((param) => { return WrapperGenerator.generateStackArgument(param.type, param.name); }); args = `, [${argStrings.join(',\n ')}]`; } else { // For methods without parameters, pass empty array args = ', []'; } return ` async ${methodName}(provider: ContractProvider${parameters}): Promise<${returnType}> { const { stack } = await provider.get('${method.name}'${args}); return ${WrapperGenerator.generateStackReader(method.returnType, structures)}; }`; } /** * Generate stack argument for get method parameter */ static generateStackArgument(type, paramName) { const paramType = WrapperGenerator.tolkTypeToTSType(type); if (paramType === 'Address') { return `{ type: 'cell', cell: beginCell().storeAddress(${paramName}).endCell(), }`; } else if (paramType === 'bigint') { return `{ type: 'int', value: ${paramName}, }`; } else if (paramType === 'boolean') { return `{ type: 'int', value: ${paramName} ? 1n : 0n, }`; } else if (paramType === 'Cell') { // Handle cell parameter - just pass the cell directly return `{ type: 'cell', cell: ${paramName}, }`; } else if (paramType === 'Cell | null') { // Handle dict type return `${paramName} === null ? { type: 'null', } : { type: 'cell', cell: ${paramName}, }`; } else if (type.kind === 'primitive') { switch (type.name) { case 'int': return `{ type: 'cell', cell: beginCell().storeInt(${paramName}, 257).endCell(), }`; case 'uint': return `{ type: 'cell', cell: beginCell().storeUint(${paramName}, 256).endCell(), }`; case 'bool': return `{ type: 'cell', cell: beginCell().storeUint(${paramName} ? 1 : 0, 1).endCell(), }`; case 'address': return `{ type: 'cell', cell: beginCell().storeAddress(${paramName}).endCell(), }`; case 'coins': return `{ type: 'cell', cell: beginCell().storeCoins(${paramName}).endCell(), }`; case 'cell': // This case should not be reached since Cell parameters are handled above return `{ type: 'cell', cell: ${paramName}, }`; case 'slice': return `{ type: 'slice', cell: ${paramName}, }`; default: return `{ type: 'cell', cell: beginCell().storeRef(${paramName}).endCell(), }`; } } else if (type.kind === 'int') { return `{ type: 'cell', cell: beginCell().storeInt(${paramName}, ${type.width}).endCell(), }`; } else if (type.kind === 'uint') { return `{ type: 'cell', cell: beginCell().storeUint(${paramName}, ${type.width}).endCell(), }`; } else if (type.kind === 'type_identifier') { // Handle common type identifiers that are parsed as type_identifier instead of primitive switch (type.name) { case 'int': return `{ type: 'cell', cell: beginCell().storeInt(${paramName}, 257).endCell(), }`; case 'uint': return `{ type: 'cell', cell: beginCell().storeUint(${paramName}, 256).endCell(), }`; case 'bool': return `{ type: 'cell', cell: beginCell().storeUint(${paramName} ? 1 : 0, 1).endCell(), }`; case 'address': return `{ type: 'cell', cell: beginCell().storeAddress(${paramName}).endCell(), }`; case 'coins': return `{ type: 'cell', cell: beginCell().storeCoins(${paramName}).endCell(), }`; case 'cell': return `{ type: 'cell', cell: ${paramName}, }`; case 'slice': return `{ type: 'slice', cell: ${paramName}, }`; case 'dict': return `${paramName} === null ? { type: 'null', } : { type: 'cell', cell: ${paramName}, }`; default: // For custom types, use the appropriate store function return `{ type: 'cell', cell: beginCell().store(store${WrapperGenerator.tolkTypeToStoreName(type)}(${paramName})).endCell(), }`; } } else { // For other custom types, use the appropriate store function return `{ type: 'cell', cell: beginCell().store(store${WrapperGenerator.tolkTypeToStoreName(type)}(${paramName})).endCell(), }`; } } /** * Generate stack reader for return type */ static generateStackReader(returnType, structures) { switch (returnType.kind) { case 'primitive': switch (returnType.name) { case 'void': return 'undefined'; case 'bool': return 'stack.readBoolean()'; case 'int': case 'uint': case 'coins': return 'stack.readBigNumber()'; case 'address': return 'stack.readAddress()'; case 'cell': case 'slice': return 'stack.readCell()'; default: return 'stack.readCell()'; } case 'int': case 'uint': return 'stack.readBigNumber()'; case 'dict': return 'stack.readCellOpt()'; case 'special': switch (returnType.name) { default: return 'stack.readCell()'; } case 'type_identifier': // Handle void type if (returnType.name === 'void') { return 'undefined'; } // Handle Bool type specifically if (returnType.name === 'Bool') { return '{ kind: "Bool", value: stack.readBoolean() }'; } // Handle common type identifiers switch (returnType.name) { case 'int': return 'stack.readBigNumber()'; case 'uint': return 'stack.readBigNumber()'; case 'bool': return 'stack.readBoolean()'; case 'address': return 'stack.readAddress()'; case 'cell': return 'stack.readCell()'; case 'coins': return 'stack.readBigNumber()'; case 'slice': return 'stack.readCell()'; default: // Check if this is a struct type const struct = structures.find((s) => s.name === returnType.name); if (struct) { return WrapperGenerator.generateStackReaderForStruct(struct, structures); } // For other custom types, assume they need cell parsing return 'stack.readCell()'; } case 'tensor': // Handle tensor returns as tuple - read each element from stack const tensorReaders = returnType.items.map((item) => WrapperGenerator.generateStackReader(item, structures)); return `[${tensorReaders.join(', ')}]`; case 'tuple': // Handle tuple returns as tuple - read each element from stack const tupleReaders = returnType.items.map((item) => WrapperGenerator.generateStackReader(item, structures)); return `[${tupleReaders.join(', ')}]`; case 'nullable': // Handle nullable returns using readCellOpt const innerReader = WrapperGenerator.generateStackReader(returnType.inner, structures); return `(() => { const cell = stack.readCellOpt(); return cell === null ? null : ${innerReader.replace('stack.', 'cell.beginParse().')}; })()`; default: return 'stack.readCell()'; } } /** * Generate stack reader for struct type */ static generateStackReaderForStruct(struct, structures) { // Generate the field readers const fieldReaders = struct.fields.map((field) => { const reader = WrapperGenerator.generateStructFieldStackReader(field.type, structures); return `${field.name}: ${reader}`; }); // Create the object with kind field return `{ kind: '${struct.name}', ${fieldReaders.join(',\n ')} }`; } /** * Generate stack reader for struct field type (handles special cases for struct compatibility) */ static generateStructFieldStackReader(fieldType, structures) { switch (fieldType.kind) { case 'primitive': switch (fieldType.name) { case 'bool': // Bool struct type expects an object with kind and value return '{ kind: "Bool", value: stack.readBoolean() }'; case 'int': case 'uint': // For general int/uint, use bigint return 'stack.readBigNumber()'; case 'coins': return 'stack.readBigNumber()'; case 'address': return 'stack.readAddress()'; case 'cell': case 'slice': return 'stack.readCell()'; default: return 'stack.readCell()'; } case 'uint': // For specific uint types, uint32 and smaller should be number, larger should be bigint if (fieldType.width <= 32) { return 'stack.readNumber()'; } else { return 'stack.readBigNumber()'; } case 'int': // For specific int types, int32 and smaller should be number, larger should be bigint if (fieldType.width <= 32) { return 'stack.readNumber()'; } else { return 'stack.readBigNumber()'; } case 'dict': // Dict should be Maybe<Cell> - convert null to Maybe_nothing return '(() => { const cell = stack.readCellOpt(); return cell === null ? { kind: "Maybe_nothing" } : { kind: "Maybe_just", value: cell }; })()'; case 'nullable': // Handle nullable types using loadMaybe return WrapperGenerator.generateNullableFieldReader(fieldType.inner, structures); case 'type_identifier': // Handle common type identifiers switch (fieldType.name) { case 'int': case 'uint': case 'coins': return 'stack.readBigNumber()'; case 'bool': return '{ kind: "Bool", value: stack.readBoolean() }'; case 'address': return 'stack.readAddress()'; case 'cell': case 'slice': return 'stack.readCell()'; default: // Check if this is a struct type const struct = structures.find((s) => s.name === fieldType.name); if (struct) { return WrapperGenerator.generateStackReaderForStruct(struct, structures); } return 'stack.readCell()'; } default: return WrapperGenerator.generateStackReader(fieldType, structures); } } /** * Generate reader for nullable field using loadMaybe */ static generateNullableFieldReader(innerType, structures) { switch (innerType.kind) { case 'primitive': switch (innerType.name) { case 'bool': return '(() => { const val = stack.readBooleanOpt(); return val === null ? { kind: "Maybe_nothing" } : { kind: "Maybe_just", value: { kind: "Bool", value: val } }; })()'; case 'int': case 'uint': case 'coins': return '(() => { const val = stack.readBigNumberOpt(); return val === null ? { kind: "Maybe_nothing" } : { kind: "Maybe_just", value: val }; })()'; case 'address': return '(() => { const val = stack.readAddressOpt(); return val === null ? { kind: "Maybe_nothing" } : { kind: "Maybe_just", value: val }; })()'; case 'cell': case 'slice': return '(() => { const val = stack.readCellOpt(); return val === null ? { kind: "Maybe_nothing" } : { kind: "Maybe_just", value: val }; })()'; default: return '(() => { const val = stack.readCellOpt(); return val === null ? { kind: "Maybe_nothing" } : { kind: "Maybe_just", value: val }; })()'; } case 'uint': if (innerType.width <= 32) { return '(() => { const val = stack.readNumberOpt(); return val === null ? { kind: "Maybe_nothing" } : { kind: "Maybe_just", value: val }; })()'; } else { return '(() => { const val = stack.readBigNumberOpt(); return val === null ? { kind: "Maybe_nothing" } : { kind: "Maybe_just", value: val }; })()'; } case 'int': if (innerType.width <= 32) { return '(() => { const val = stack.readNumberOpt(); return val === null ? { kind: "Maybe_nothing" } : { kind: "Maybe_just", value: val }; })()'; } else { return '(() => { const val = stack.readBigNumberOpt(); return val === null ? { kind: "Maybe_nothing" } : { kind: "Maybe_just", value: val }; })()'; } case 'type_identifier': // Handle common type identifiers switch (innerType.name) { case 'int': case 'uint': case 'coins': return '(() => { const val = stack.readBigNumberOpt(); return val === null ? { kind: "Maybe_nothing" } : { kind: "Maybe_just", value: val }; })()'; case 'bool': return '(() => { const val = stack.readBooleanOpt(); return val === null ? { kind: "Maybe_nothing" } : { kind: "Maybe_just", value: { kind: "Bool", value: val } }; })()'; case 'address': return '(() => { const val = stack.readAddressOpt(); return val === null ? { kind: "Maybe_nothing" } : { kind: "Maybe_just", value: val }; })()'; case 'cell': case 'slice': return '(() => { const val = stack.readCellOpt(); return val === null ? { kind: "Maybe_nothing" } : { kind: "Maybe_just", value: val }; })()'; default: // Check if this is a struct type const struct = structures.find((s) => s.name === innerType.name); if (struct) { return ('(() => { const val = stack.readCellOpt(); return val === null ? { kind: "Maybe_nothing" } : { kind: "Maybe_just", value: load' + innerType.name + '(val.beginParse()) }; })()'); } return '(() => { const val = stack.readCellOpt(); return val === null ? { kind: "Maybe_nothing" } : { kind: "Maybe_just", value: val }; })()'; } default: return '(() => { const val = stack.readCellOpt(); return val === null ? { kind: "Maybe_nothing" } : { kind: "Maybe_just", value: val }; })()'; } } /** * Convert Tolk type to TypeScript type */ static tolkTypeToTSType(type) { switch (type.kind) { case 'primitive': switch (type.name) { case 'void': return 'void'; case 'bool': return 'boolean'; case 'int': case 'uint': case 'coins': return 'bigint'; case 'address': return 'Address'; case 'cell': case 'slice': case 'builder': return 'Cell'; default: return 'Cell'; } case 'int': case 'uint': return 'bigint'; case 'type_identifier': // Handle void type if (type.name === 'void') { return 'void'; } // Handle common type identifiers that should be mapped to TypeScript types switch (type.name) { case 'int': case 'uint': case 'coins': return 'bigint'; case 'bool': return 'boolean'; case 'address': return 'Address'; case 'cell': case 'slice': return 'Cell'; default: return type.name; } case 'dict': return 'Cell | null'; case 'special': switch (type.name) { default: return 'Cell'; } case 'nullable': return `${WrapperGenerator.tolkTypeToTSType(type.inner)} | null`; case 'tuple': if (type.items.length === 0) { return 'void'; } const itemTypes = type.items.map((item) => WrapperGenerator.tolkTypeToTSType(item)); return `[${itemTypes.join(', ')}]`; case 'tensor': // Tensor types should also be represented as arrays in TypeScript, not objects if (type.items.length === 0) { return 'void'; } const tensorItems = type.items.map((item) => WrapperGenerator.tolkTypeToTSType(item)); return `[${tensorItems.join(', ')}]`; default: return 'Cell'; } } /** * Convert Tolk type to store function name */ static tolkTypeToStoreName(type) { if (type.kind === 'type_identifier') { return type.name; } return 'Cell'; } /** * Extract send methods from internal message union type name */ static extractSendMethods(internalMessageType, structures, typeAliases) { // Find the type alias for the internal message type const unionTypeAlias = typeAliases.find((alias) => alias.name === internalMessageType); if (!unionTypeAlias) { return []; } const sendMethods = []; // If the underlying type is a union, extract its alternatives if (unionTypeAlias.underlyingType.kind === 'union') { const alternatives = unionTypeAlias.underlyingType.alternatives; for (const alternative of alternatives) { if (alternative.kind === 'type_identifier') { const messageStructName = alternative.name; // Find the corresponding struct const messageStruct = structures.find((struct) => struct.name === messageStructName); if (messageStruct) { // Convert message struct name to send method name // e.g., "increment_message" -> "sendIncrement", "IncrementMessage" -> "sendIncrement" let methodName = messageStruct.name; if (methodName.endsWith('Message')) { methodName = methodName.slice(0, -7); // Remove "Message" } // Normalize the method name (convert snake_case to camelCase) const normalizedName = WrapperGenerator.normalizeMethodName(methodName); methodName = `send${WrapperGenerator.capitalize(normalizedName)}`; const sendMethod = { name: methodName, messageType: messageStruct.name, parameters: messageStruct.fields, }; sendMethods.push(sendMethod); } } } } return sendMethods; } /** * Generate class end */ static generateClassEnd() { return '}'; } /** * Capitalize first letter */ static capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1); } /** * Convert snake_case to camelCase */ static snakeToCamelCase(str) { return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); } /** * Convert method name to camelCase (handles both snake_case and PascalCase) */ static normalizeMethodName(methodName) { // First convert snake_case to camelCase let normalized = WrapperGenerator.snakeToCamelCase(methodName); // If the result starts with uppercase, make it lowercase (PascalCase -> camelCase) if (normalized.charAt(0) === normalized.charAt(0).toUpperCase()) { normalized = normalized.charAt(0).toLowerCase() + normalized.slice(1); } return normalized; } } exports.WrapperGenerator = WrapperGenerator; //# sourceMappingURL=wrapper-generator.js.map