open-libra-sdk
Version:
A minimalist Typescript library for interacting with the Open Libra blockchain.
272 lines (261 loc) • 9.55 kB
text/typescript
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"));
}