UNPKG

open-libra-sdk

Version:

A minimalist Typescript library for interacting with the Open Libra blockchain.

272 lines (261 loc) 9.55 kB
import * as fs from "fs"; import * as path from "path"; import type { ViewArgs } from "../types/clientPayloads"; // Type representing a parsed Move view function type ViewFunction = { name: string; args: Array<{ name: string; type: string }>; file: string; }; /** * Recursively find all .move files in a directory */ function findMoveFiles(dir: string): string[] { let results: string[] = []; const list = fs.readdirSync(dir); list.forEach((file) => { const filePath = path.join(dir, file); const stat = fs.statSync(filePath); if (stat && stat.isDirectory()) { results = results.concat(findMoveFiles(filePath)); } else if ( file.endsWith(".move") && !file.endsWith(".test.move") && !file.endsWith(".spec.move") ) { results.push(filePath); } }); return results; } /** * Parse Move source code and extract #[view] functions */ function parseViewFunctionsFromSource( source: string, file: string, ): ViewFunction[] { const lines = source.split(/\r?\n/); const viewFunctions: ViewFunction[] = []; for (let i = 0; i < lines.length; i++) { if (lines[i].trim().startsWith("#[view]")) { // Look ahead for the function signature let j = i + 1; while (j < lines.length && !lines[j].includes("fun ")) j++; if (j >= lines.length) continue; const funLine = lines[j].trim(); // Match function signature: public fun name(args...): returnType { const funMatch = funLine.match(/fun\s+([a-zA-Z0-9_]+)\s*\(([^)]*)\)/); if (!funMatch) continue; const [, name, argsStr] = funMatch; // Parse arguments const args = argsStr .split(",") .map((arg) => arg.trim()) .filter(Boolean) .map((arg) => { // arg: name: type const [argName, ...typeParts] = arg.split(":").map((s) => s.trim()); return argName && typeParts.length > 0 ? { name: argName, type: typeParts.join(": ") } : null; }) .filter(Boolean) as { name: string; type: string }[]; viewFunctions.push({ name, args, file }); } } return viewFunctions; } /** * Main function: Given a directory, return all view functions as TypeScript types */ export function extractViewFunctions(dir: string): ViewFunction[] { const moveFiles = findMoveFiles(dir); let allViews: ViewFunction[] = []; for (const file of moveFiles) { const source = fs.readFileSync(file, "utf8"); const views = parseViewFunctionsFromSource(source, file); allViews = allViews.concat(views); } return allViews; } // Helper: Generate TypeScript type definitions for view functions export function generateViewTypes(viewFns: ViewFunction[]): string { return viewFns .map((fn) => { const typeName = fn.name.charAt(0).toUpperCase() + fn.name.slice(1) + "View"; return `export type ${typeName} = {\n name: string;\n args: Array<{ name: string; type: string }>;\n file: string;\n}`; }) .join("\n\n"); } /** * Convert a ViewFunction to a ViewArgs payload (Aptos SDK compatible) * @param fn ViewFunction * @param params Array of argument values to use as functionArguments * @param moduleAddress Optional module address (defaults to 0x1) * @returns ViewArgs */ export function viewFunctionToViewArgs( fn: ViewFunction, params: unknown[] = [], moduleAddress = "0x1", ): ViewArgs { // Try to infer module name from file path, fallback to 'module' const fileName = fn.file.split(path.sep).pop() || "module.move"; const moduleMatch = fileName.match(/([^.]+)\.move$/); const moduleName = moduleMatch ? moduleMatch[1] : "module"; const functionName = `${moduleAddress}::${moduleName}::${fn.name}` as `${string}::${string}::${string}`; return { payload: { function: functionName, functionArguments: params, }, }; } /** * Generate a TypeScript file with constants for each discovered ViewFunction. * @param moveDir Directory to search for .move files * @param outFile Output .ts file path */ export function generate( moveDir: string, outFile: string, importPath = "../types/clientPayloads", ) { const views = extractViewFunctions(moveDir); // Deduplicate by fully qualified function name const seen = new Set<string>(); const dedupedViews = views.filter((fn) => { const fileName = fn.file.split(path.sep).pop() || "module.move"; const moduleMatch = fileName.match(/([^.]+)\.move$/); const moduleName = moduleMatch ? moduleMatch[1] : "module"; const functionName = `0x1::${moduleName}::${fn.name}`; if (seen.has(functionName)) return false; seen.add(functionName); return true; }); const lines: string[] = []; lines.push( "// This file is auto-generated by parseViewFunctions.ts. Do not edit manually.", ); lines.push(`import type { ViewArgs } from "${importPath}";`); lines.push(""); for (const fn of dedupedViews) { // Create a constant name in camelCase const constName = fn.name.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); // Compose the function string const fileName = fn.file.split(path.sep).pop() || "module.move"; const moduleMatch = fileName.match(/([^.]+)\.move$/); const moduleName = moduleMatch ? moduleMatch[1] : "module"; const functionName = `0x1::${moduleName}::${fn.name}`; // Arguments as a comment const argComment = fn.args.length ? `// args: ${fn.args.map((a) => `${a.name}: ${a.type}`).join(", ")}` : "// no args"; // FunctionArguments placeholder const argsArray = fn.args.length ? `[${fn.args.map((a) => `/* ${a.name}: ${a.type} */`).join(", ")}]` : "[]"; lines.push(`${argComment}`); lines.push(`export const ${constName}View: ViewArgs = {`); lines.push(` payload: {`); lines.push(` function: "${functionName}",`); lines.push(` functionArguments: ${argsArray},`); lines.push(` },`); lines.push(`};\n`); } fs.writeFileSync(outFile, lines.join("\n")); } /** * Generate a TypeScript file with sugar functions for each discovered ViewFunction. * Each function mirrors the Move view function signature and returns a ViewArgs payload. * @param moveDir Directory to search for .move files * @param outFile Output .ts file path */ export function generateSugar( moveDir: string, outFile: string, importPath = "../types/clientPayloads", ) { const views = extractViewFunctions(moveDir); // Deduplicate by fully qualified function name const seen = new Set<string>(); const dedupedViews = views.filter((fn) => { const fileName = fn.file.split(path.sep).pop() || "module.move"; const moduleMatch = fileName.match(/([^.]+)\.move$/); const moduleName = moduleMatch ? moduleMatch[1] : "module"; const functionName = `0x1::${moduleName}::${fn.name}`; if (seen.has(functionName)) return false; seen.add(functionName); return true; }); const lines: string[] = []; lines.push( "// This file is auto-generated by parseViewFunctions.ts. Do not edit manually.", ); lines.push(`import type { ViewArgs } from "${importPath}";`); lines.push(""); for (const fn of dedupedViews) { // Compose the Move module and function names const fileName = fn.file.split(path.sep).pop() || "module.move"; const moduleMatch = fileName.match(/([^.]+)\.move$/); const moduleNameRaw = moduleMatch ? moduleMatch[1] : "module"; // Helper to camelCase the function name (first part lower, then capitalize after underscores) function toCamelCase(str: string) { return str.replace(/_([a-zA-Z])/g, (_, c) => c.toUpperCase()); } // Helper to lower-case only the first letter function lowerFirst(str: string) { return str.charAt(0).toLowerCase() + str.slice(1); } const moduleNameCamel = lowerFirst(toCamelCase(moduleNameRaw)); const functionNameCamel = toCamelCase(fn.name); // TS function name: moduleNameCamel_functionNameCamel const tsFuncName = `${moduleNameCamel}_${functionNameCamel}`; // Args signature function mapMoveTypeToTsType(moveType: string): string { const t = moveType.trim().toLowerCase(); if ( t === "u8" || t === "u16" || t === "u32" || t === "u64" || t === "u128" || t === "u256" ) return "number"; if (t === "bool") return "boolean"; if (t.startsWith("vector<u8>")) return "string"; // treat bytes as string if (t === "address" || t.startsWith("vector<")) return "string"; return "any"; } const argList = fn.args .map((a, i) => `${a.name || "arg" + i}: ${mapMoveTypeToTsType(a.type)}`) .join(", "); const argNames = fn.args.map((a, i) => a.name || "arg" + i).join(", "); // Compose the Move function name const functionName = `0x1::${moduleNameRaw}::${fn.name}`; // JSDoc lines.push("/**"); lines.push(` * Sugar for ${tsFuncName}`); if (fn.args.length) { fn.args.forEach((a, i) => lines.push(` * @param ${a.name || "arg" + i} ${a.type}`), ); } lines.push(" * @returns ViewArgs"); lines.push(" */"); lines.push(`export function ${tsFuncName}(${argList}): ViewArgs {`); lines.push(" return {"); lines.push(" payload: {"); lines.push(` function: "${functionName}",`); lines.push(` functionArguments: [${argNames}],`); lines.push(" },"); lines.push(" };"); lines.push("}"); lines.push(""); } fs.writeFileSync(outFile, lines.join("\n")); }