UNPKG

@eliseev_s/tolk-tlb-transpiler

Version:

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

509 lines 24.6 kB
"use strict"; // ============================================================================ // Graph Adapter - Generate transaction visualization helpers from Tolk code // ============================================================================ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TolkGraphAdapter = void 0; exports.mergeGraphAdapterResults = mergeGraphAdapterResults; const tlb_codegen_1 = require("@eliseev_s/tlb-codegen"); const core_1 = require("@ton/core"); const fs_1 = require("fs"); const typescript_1 = __importDefault(require("typescript")); const parser_1 = require("./parser"); const constants_1 = require("./constants"); const defaultOptions = { blacklistParams: ['query_id', 'excesses'], hideAddresses: false, showAll: false, errorConstantPrefix: 'ERROR_', }; // ============================================================================ // Utility Functions // ============================================================================ /** * Flatten nested kind objects into a flat record */ function flattenKindObject(from, to, prefix = '') { Object.entries(from).forEach(([key, val]) => { if (isObject(val) && 'kind' in val && typeof val.kind === 'string') { flattenKindObject(val, to, `${key}_`); } else { to[`${prefix}${key}`] = val; } }); return to; } function isObject(value) { return typeof value === 'object' && value !== null; } /** * Convert snake_case to PascalCase */ function snakeToPascal(str) { return str .split('_') .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(''); } // ============================================================================ // TypeScript AST Transformers for Parser Extraction // ============================================================================ /** * Creates a transformer that extracts function declarations matching a predicate * and collects their referenced helper functions */ function createFunctionExtractorTransformer(nodePredicate, referencedFunctions) { return (_context) => { return (sourceFile) => { const referencedFunctionsIdentifiers = new Set(); const collectReferences = (node) => { if (typescript_1.default.isCallExpression(node) && typescript_1.default.isIdentifier(node.expression)) { referencedFunctionsIdentifiers.add(node.expression.text); } typescript_1.default.forEachChild(node, collectReferences); }; const includeReferences = (node) => { if (typescript_1.default.isFunctionDeclaration(node) && node.name && referencedFunctionsIdentifiers.has(node.name.text)) { const funcNode = typescript_1.default.factory.createFunctionDeclaration(undefined, undefined, node.name, undefined, node.parameters, node.type, node.body); referencedFunctions.set(node.name.text, funcNode); return node; } return undefined; }; const findFunction = (node) => { if (nodePredicate(node)) { collectReferences(node); return node; } return undefined; }; const newStatements = sourceFile.statements .map((statement) => typescript_1.default.visitNode(statement, findFunction)) .filter((node) => node !== undefined && typescript_1.default.isStatement(node)); sourceFile.statements.forEach((statement) => typescript_1.default.visitNode(statement, includeReferences)); return typescript_1.default.factory.updateSourceFile(sourceFile, newStatements); }; }; } /** * Creates a transformer that extracts individual opcode branches from a message loader * and creates a handlers object */ function createBranchesToFunctionMapTransformer(functionName, referencedFunctions) { return (context) => { return (sourceFile) => { const handlers = []; const findOpcode = (node) => { let op = null; let bits = null; if (typescript_1.default.isParenthesizedExpression(node.expression)) { node.expression.getChildren().forEach((condition) => { if (typescript_1.default.isBinaryExpression(condition)) { if (typescript_1.default.isParenthesizedExpression(condition.left)) { condition.left.getChildren().forEach((child) => { if (typescript_1.default.isBinaryExpression(child)) { bits = Number(child.right.getText()); } }); } if (typescript_1.default.isParenthesizedExpression(condition.right)) { condition.right.getChildren().forEach((child) => { if (typescript_1.default.isBinaryExpression(child)) { op = Number(child.right.getText()); } }); } } }); } if (op !== null && bits !== null) return { op, bits }; return null; }; const handleBodyChild = (node, functionsToImport) => { if (typescript_1.default.isCallExpression(node) && typescript_1.default.isIdentifier(node.expression) && referencedFunctions.has(node.expression.text)) { functionsToImport.set(node.expression.text, referencedFunctions.get(node.expression.text)); } typescript_1.default.forEachChild(node, (child) => handleBodyChild(child, functionsToImport)); }; const handleIfBody = (node, op) => { const functionsToImport = new Map(); const parameter = typescript_1.default.factory.createParameterDeclaration(undefined, undefined, 'slice', undefined, typescript_1.default.factory.createTypeReferenceNode('Slice')); typescript_1.default.forEachChild(node, (child) => handleBodyChild(child, functionsToImport)); const arrowFunc = typescript_1.default.factory.createArrowFunction(undefined, undefined, [parameter], undefined, typescript_1.default.factory.createToken(typescript_1.default.SyntaxKind.EqualsGreaterThanToken), typescript_1.default.factory.createBlock([...functionsToImport.values(), ...node.statements], true)); handlers.push([op, arrowFunc]); }; const handleIfStatement = (node) => { const opInfo = findOpcode(node); if (!opInfo) return; if (typescript_1.default.isBlock(node.thenStatement)) { handleIfBody(node.thenStatement, opInfo.op); } }; const visitor = (node) => { if (typescript_1.default.isFunctionDeclaration(node) && node.name?.text === functionName && node.body && typescript_1.default.isBlock(node.body)) { node.body.statements.forEach((statement) => { if (typescript_1.default.isIfStatement(statement)) { handleIfStatement(statement); } }); } return undefined; }; typescript_1.default.visitEachChild(sourceFile, visitor, context); const objectDeclaration = typescript_1.default.factory.createVariableStatement([typescript_1.default.factory.createModifier(typescript_1.default.SyntaxKind.ExportKeyword)], typescript_1.default.factory.createVariableDeclarationList([ typescript_1.default.factory.createVariableDeclaration('handlers', undefined, undefined, typescript_1.default.factory.createObjectLiteralExpression(handlers.map(([op, func]) => typescript_1.default.factory.createPropertyAssignment(op.toString(), func)), true)), ], typescript_1.default.NodeFlags.Const)); return typescript_1.default.factory.updateSourceFile(sourceFile, [objectDeclaration]); }; }; } /** * Creates a transformer that converts matched function declarations into an exported object */ function createFunctionsToObjectTransformer(referencedFunctions) { return (context) => { return (sourceFile) => { const functionDeclarations = []; const collectFunctions = (node) => { if (typescript_1.default.isFunctionDeclaration(node)) functionDeclarations.push(node); }; sourceFile.forEachChild(collectFunctions); const properties = functionDeclarations.map((func) => { const functionsToImport = new Map(); const handleBodyChild = (node) => { if (typescript_1.default.isCallExpression(node) && typescript_1.default.isIdentifier(node.expression) && referencedFunctions.has(node.expression.text)) { functionsToImport.set(node.expression.text, referencedFunctions.get(node.expression.text)); } typescript_1.default.forEachChild(node, handleBodyChild); }; if (func.body) handleBodyChild(func.body); const newBody = func.body ? typescript_1.default.factory.createBlock([...functionsToImport.values(), ...func.body.statements], true) : typescript_1.default.factory.createBlock([]); const arrowFunc = typescript_1.default.factory.createArrowFunction(func.modifiers?.filter((m) => typescript_1.default.isModifier(m) && m.kind !== typescript_1.default.SyntaxKind.ExportKeyword), func.typeParameters, func.parameters, func.type, typescript_1.default.factory.createToken(typescript_1.default.SyntaxKind.EqualsGreaterThanToken), newBody); return typescript_1.default.factory.createPropertyAssignment(func.name.text, arrowFunc); }); const exportDeclaration = typescript_1.default.factory.createVariableStatement([typescript_1.default.factory.createModifier(typescript_1.default.SyntaxKind.ExportKeyword)], typescript_1.default.factory.createVariableDeclarationList([ typescript_1.default.factory.createVariableDeclaration('functions', undefined, undefined, typescript_1.default.factory.createObjectLiteralExpression(properties, true)), ], typescript_1.default.NodeFlags.Const)); return typescript_1.default.factory.updateSourceFile(sourceFile, [exportDeclaration]); }; }; } // ============================================================================ // Caption Handler Factory // ============================================================================ /** * Convert a raw codegen handler to a caption handler with options applied */ function toCaptionHandler(handler, options, opMap, internalMsgBodyKindPrefix = 'InternalMsgBody_') { return (params) => { const sc = params.body.beginParse(); const op = sc.preloadUint(32); const opName = opMap?.get(op); let rawCaptions = handler(sc); // Remove kind field if it matches the expected message type if (opName && rawCaptions.kind === internalMsgBodyKindPrefix + opName) { delete rawCaptions.kind; } if (!options.showAll) { const blacklistParams = options.blacklistParams ?? ['query_id', 'excesses']; if (blacklistParams) { blacklistParams.forEach((param) => delete rawCaptions[param]); } if (options.hideAddresses) { for (const [key, value] of Object.entries(rawCaptions)) { if (core_1.Address.isAddress(value)) { delete rawCaptions[key]; } } } } return flattenKindObject(rawCaptions, {}); }; } /** * Convert a raw codegen handler to a storage parser */ function toStorageParser(handler) { return (cell) => { const result = handler(cell.beginParse()); return flattenKindObject(result, {}); }; } // ============================================================================ // Parser Extraction from TLB // ============================================================================ /** * Extract message and storage parsers from TLB definitions */ function extractParsers(sourceTLB, options, contracts, internalMessageType, opMap) { // Full TLB with common definitions const commonTLB = ` 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 tlb = `${commonTLB}\n${sourceTLB}`; // Generate TypeScript code from TLB const code = (0, tlb_codegen_1.generateCode)(tlb, 'typescript'); const sourceFile = typescript_1.default.createSourceFile('generated.ts', code, typescript_1.default.ScriptTarget.Latest, true); // Internal message loader function name const internalMsgFunctionName = `load${internalMessageType}`; const referencedFunctions = new Map(); // Try to extract caption handlers from the internal message loader const captionsMap = new Map(); try { const captionsTransformResult = typescript_1.default.transform(sourceFile, [ createFunctionExtractorTransformer((node) => typescript_1.default.isFunctionDeclaration(node) && node.name?.text === internalMsgFunctionName, referencedFunctions), createBranchesToFunctionMapTransformer(internalMsgFunctionName, referencedFunctions), ]); const transformedCode = typescript_1.default .createPrinter() .printFile(captionsTransformResult.transformed[0]); const transpiled = typescript_1.default.transpile(transformedCode); const codegenHandlers = eval(transpiled); for (const [op, handler] of Object.entries(codegenHandlers)) { captionsMap.set(Number(op), toCaptionHandler(handler, options, opMap)); } } catch (e) { // Caption extraction may fail if no InternalMsgBody type exists console.warn(`Warning: Could not extract caption handlers: ${e}`); } // Extract storage parsers for contracts const storageParserIdentifiers = Array.from(contracts).reduce((acc, contract) => { const identifier = `load${snakeToPascal(contract)}Storage`; return acc.set(identifier, contract); }, new Map()); const contractToParserMap = new Map(); try { const storageTransformResult = typescript_1.default.transform(sourceFile, [ createFunctionExtractorTransformer((node) => typescript_1.default.isFunctionDeclaration(node) && storageParserIdentifiers.has(node.name?.text ?? ''), referencedFunctions), createFunctionsToObjectTransformer(referencedFunctions), ]); const storageTransformedCode = typescript_1.default .createPrinter() .printFile(storageTransformResult.transformed[0]); const storageTranspiled = typescript_1.default.transpile(storageTransformedCode); const storageCodegenHandlers = eval(storageTranspiled); storageParserIdentifiers.forEach((contract, identifier) => { if (storageCodegenHandlers[identifier]) { contractToParserMap.set(contract, toStorageParser(storageCodegenHandlers[identifier])); } }); } catch (e) { // Storage parser extraction may fail if no storage types exist console.warn(`Warning: Could not extract storage parsers: ${e}`); } return { captionsMap, contractToParserMap }; } // ============================================================================ // TolkGraphAdapter - Main Class // ============================================================================ /** * Adapter class for generating transaction visualization helpers from Tolk code * * @remarks * This class generates: * - `opMap`: Maps operation codes to their names * - `errMap`: Maps error codes to their names * - `captionsMap`: Contains handlers for parsing message bodies * - `contractToParserMap`: Contains handlers for parsing contract storage * * @example * ```typescript * const result = await TolkGraphAdapter.fromFiles({ * structuresPath: 'contracts/structures.tolk', * errorsPath: 'contracts/errors.tolk', * contractName: 'MyContract', * internalMessageType: 'MyContractInternalMessage', * }); * * // Use the adapter for transaction visualization * const { opMap, errMap, captionsMap, contractToParserMap } = result; * ``` */ class TolkGraphAdapter { /** * Create adapter from Tolk file paths * * @param config - Configuration with file paths and contract info * @param options - Additional options for caption generation * @returns Promise resolving to adapter result with all maps */ static async fromFiles(config, options = {}) { const mergedOptions = { ...defaultOptions, ...options }; // Read file contents if (!(0, fs_1.existsSync)(config.structuresPath)) { throw new Error(`Structures file not found: ${config.structuresPath}`); } const structuresCode = (0, fs_1.readFileSync)(config.structuresPath, 'utf-8'); let errorsCode = ''; if (config.errorsPath && (0, fs_1.existsSync)(config.errorsPath)) { errorsCode = (0, fs_1.readFileSync)(config.errorsPath, 'utf-8'); } let opcodesCode = ''; if (config.opcodesPath && (0, fs_1.existsSync)(config.opcodesPath)) { opcodesCode = (0, fs_1.readFileSync)(config.opcodesPath, 'utf-8'); } return TolkGraphAdapter.fromContent({ structuresCode, errorsCode, opcodesCode, contractName: config.contractName, internalMessageType: config.internalMessageType, }, mergedOptions); } /** * Create adapter from Tolk source code strings * * @param config - Configuration with source code and contract info * @param options - Additional options for caption generation * @returns Promise resolving to adapter result with all maps */ static async fromContent(config, options = {}) { const mergedOptions = { ...defaultOptions, ...options }; // Initialize processor const processor = await parser_1.TolkASTProcessor.initialize(); // Process structures to get TLB definitions const tlbDefinitions = processor.transpile(config.structuresCode); const structures = processor.getStructs(); const typeAliases = processor.getTypeAliases(); // Extract opMap from message structs with prefixes const opMap = TolkGraphAdapter.extractOpcodesFromStructs(config.internalMessageType, structures, typeAliases); // Extract additional opcodes from constants if provided if (config.opcodesCode && mergedOptions.opcodeConstantPrefix) { const opProcessor = await parser_1.TolkASTProcessor.initialize(); const opConstants = opProcessor.extractConstants(config.opcodesCode, (c) => c.name.startsWith(mergedOptions.opcodeConstantPrefix)); for (const [name, value] of Object.entries(opConstants)) { if (!opMap.has(value)) { opMap.set(value, name); } } } // Extract error constants const errMap = new Map(); if (config.errorsCode) { const errProcessor = await parser_1.TolkASTProcessor.initialize(); const errorPrefix = mergedOptions.errorConstantPrefix ?? 'ERROR_'; const errConstants = errProcessor.extractConstants(config.errorsCode, (c) => c.name.startsWith(errorPrefix)); for (const [name, value] of Object.entries(errConstants)) { errMap.set(value, name); } } // Generate TLB string for parser extraction const tlbString = TolkGraphAdapter.formatTlbDefinitions(tlbDefinitions); // Extract contracts for storage parsers const contracts = new Set([config.contractName]); // Extract parsers const { captionsMap, contractToParserMap } = extractParsers(constants_1.COMMON_TLB_DEFINITIONS + tlbString, mergedOptions, contracts, config.internalMessageType, opMap); return { opMap, errMap, captionsMap, contractToParserMap }; } /** * Extract opcodes from message structs that have prefixes */ static extractOpcodesFromStructs(internalMessageType, structures, typeAliases) { const opMap = new Map(); // Find the union type for internal messages const unionTypeAlias = typeAliases.find((alias) => alias.name === internalMessageType); if (!unionTypeAlias) { // No union type, try to extract from all structures with prefixes for (const struct of structures) { if (struct.prefix) { opMap.set(struct.prefix.value, struct.name); } } return opMap; } const processMessageType = (messageStructName) => { const messageStruct = structures.find((s) => s.name === messageStructName); if (messageStruct?.prefix) { opMap.set(messageStruct.prefix.value, messageStruct.name); } }; if (unionTypeAlias.underlyingType.kind === 'union') { for (const alt of unionTypeAlias.underlyingType.alternatives) { if (alt.kind === 'type_identifier') { processMessageType(alt.name); } } } else if (unionTypeAlias.underlyingType.kind === 'type_identifier') { processMessageType(unionTypeAlias.underlyingType.name); } return opMap; } /** * Format TLB definitions to string */ static formatTlbDefinitions(definitions) { const output = []; for (const definition of definitions) { for (const constructor of definition.statement) { output.push(constructor); } } return output.join('\n'); } } exports.TolkGraphAdapter = TolkGraphAdapter; // ============================================================================ // Utility: Merge Multiple Adapter Results // ============================================================================ /** * Merge multiple TolkGraphAdapterResult objects into one * * @param results - Array of adapter results to merge * @returns Merged result with all maps combined * * @example * ```typescript * const result1 = await TolkGraphAdapter.fromFiles({ ... }); * const result2 = await TolkGraphAdapter.fromFiles({ ... }); * const merged = mergeGraphAdapterResults([result1, result2]); * ``` */ function mergeGraphAdapterResults(results) { const opMap = new Map(); const errMap = new Map(); const captionsMap = new Map(); const contractToParserMap = new Map(); for (const result of results) { for (const [key, value] of result.opMap) { opMap.set(key, value); } for (const [key, value] of result.errMap) { errMap.set(key, value); } for (const [key, value] of result.captionsMap) { captionsMap.set(key, value); } for (const [key, value] of result.contractToParserMap) { contractToParserMap.set(key, value); } } return { opMap, errMap, captionsMap, contractToParserMap }; } //# sourceMappingURL=graph-adapter.js.map