@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
JavaScript
;
// ============================================================================
// 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