import {
classifyPermissionContract,
extractContractName,
formatManifestParameters,
formatManifestType,
makeUniqueIdentifier,
sanitizeIdentifier,
} from "./manifest.js";
import { describeCallFlags } from "./nef.js";
import { describeMethodToken } from "./native-contracts.js";
import { hexOffset, upperHex } from "./util.js";
export function renderHighLevelMethodGroups(groups, manifest, context = null, liftMethodBody) {
const contractName = extractContractName(manifest);
const lines = [`contract ${contractName} {`];
writeContractHeader(lines, manifest, context);
const used = new Set();
let firstEmitted = true;
for (const group of groups) {
const body = liftMethodBody(group.instructions, group.source, context, group.start);
if (context?.highLevelWarnings) {
context.highLevelWarnings.push(...body.warnings);
}
const isInferred = !group.source;
if (isInferred && body.statements.length === 0) {
continue;
}
if (!firstEmitted) {
lines.push("");
}
firstEmitted = false;
const signature = renderMethodSignature(group, used, context);
lines.push(` ${signature} {`);
if (body.statements.length === 0) {
if (group.source && Number.isInteger(group.start)) {
lines.push(
` // no instructions decoded for manifest method at offset 0x${hexOffset(group.start)}`,
);
} else {
lines.push(" // no instructions decoded");
}
} else {
writeMethodStatements(lines, body.statements, group, context);
}
lines.push(" }");
}
lines.push("}");
return `${lines.join("\n")}\n`;
}
function writeContractHeader(lines, manifest, context) {
const scriptHash = context?.scriptHash;
const scriptHashLE = context?.scriptHashLE;
if (scriptHash) {
lines.push(` // script hash (little-endian): ${scriptHashLE}`);
lines.push(` // script hash (big-endian): ${scriptHash}`);
}
if (context?.compiler) {
lines.push(` // compiler: ${context.compiler}`);
}
if (context?.source) {
lines.push(` // source: ${context.source}`);
}
if (!manifest) {
lines.push(" // manifest not provided");
}
if (manifest) {
writeManifestSummary(lines, manifest);
}
writeMethodTokenSummary(lines, context);
lines.push("");
}
function writeManifestSummary(lines, manifest) {
if (manifest.supportedStandards?.length) {
const formatted = manifest.supportedStandards.map((s) => `"${s}"`).join(", ");
lines.push(` supported_standards = [${formatted}];`);
}
if (manifest.features && Object.keys(manifest.features).length > 0) {
lines.push(" features {");
for (const key of Object.keys(manifest.features).sort()) {
lines.push(` ${key} = ${JSON.stringify(manifest.features[key])};`);
}
lines.push(" }");
}
if (manifest.groups?.length) {
lines.push(" groups {");
for (const group of manifest.groups) {
if (group?.pubkey) {
lines.push(` pubkey=${group.pubkey}`);
}
}
lines.push(" }");
}
if (manifest.permissions?.length) {
writePermissions(lines, manifest.permissions);
}
if (manifest.trusts !== null && manifest.trusts !== undefined) {
const formatted = formatManifestTrusts(manifest.trusts);
if (formatted !== null) {
lines.push(` trusts = ${formatted};`);
}
}
if (manifest.extra && typeof manifest.extra === "object" && !Array.isArray(manifest.extra)) {
writeExtraScalars(lines, manifest.extra);
}
if (manifest.abi?.methods?.length) {
writeAbiMethods(lines, manifest.abi.methods);
}
if (manifest.abi?.events?.length) {
writeAbiEvents(lines, manifest.abi.events);
}
}
function writePermissions(lines, permissions) {
lines.push(" permissions {");
for (const perm of permissions) {
const classified = classifyPermissionContract(perm.contract);
const contractPart =
classified.kind === "wildcard"
? `contract=${classified.value}`
: classified.kind === "hash"
? `contract=hash:${classified.hash}`
: classified.kind === "group"
? `contract=group:${classified.group}`
: `contract=${JSON.stringify(classified.value)}`;
const methodsPart =
perm.methods === undefined || perm.methods === null
? "methods=*"
: typeof perm.methods === "string"
? `methods=${perm.methods}`
: Array.isArray(perm.methods)
? `methods=[${perm.methods.map((m) => `"${m}"`).join(", ")}]`
: `methods=${JSON.stringify(perm.methods)}`;
lines.push(` ${contractPart} ${methodsPart}`);
}
lines.push(" }");
}
function writeExtraScalars(lines, extra) {
for (const key of Object.keys(extra).sort()) {
const rendered = renderExtraScalar(extra[key]);
if (rendered !== null) {
lines.push(` // ${key}: ${rendered}`);
}
}
}
function writeAbiMethods(lines, methods) {
lines.push(" // ABI methods");
for (const method of methods) {
const params =
method.parameters
?.map((p) => `${sanitizeIdentifier(p.name)}: ${formatManifestType(p.kind)}`)
.join(", ") ?? "";
const returnType = formatManifestType(method.returnType ?? "Void");
const sanitisedName = sanitizeIdentifier(method.name);
const meta = [];
if (sanitisedName !== method.name) {
meta.push(`manifest ${JSON.stringify(method.name)}`);
}
if (method.safe) {
meta.push("safe");
}
if (typeof method.offset === "number") {
meta.push(`offset ${method.offset}`);
}
const metaComment = meta.length > 0 ? ` // ${meta.join(", ")}` : "";
lines.push(` fn ${sanitisedName}(${params}) -> ${returnType};${metaComment}`);
}
}
function writeAbiEvents(lines, events) {
lines.push(" // ABI events");
for (const event of events) {
const params =
event.parameters
?.map((p) => `${sanitizeIdentifier(p.name)}: ${formatManifestType(p.kind)}`)
.join(", ") ?? "";
const sanitised = sanitizeIdentifier(event.name);
const note = sanitised !== event.name ? ` // manifest ${JSON.stringify(event.name)}` : "";
lines.push(` event ${sanitised}(${params});${note}`);
}
}
function writeMethodTokenSummary(lines, context) {
const methodTokens = context?.methodTokens ?? [];
if (methodTokens.length === 0) {
return;
}
lines.push(" // method tokens declared in NEF");
for (const token of methodTokens) {
const hint = describeMethodToken(token.hash, token.method);
const contractNote = hint ? ` (${hint.formattedLabel(token.method)})` : "";
const flagsHex = token.callFlags.toString(16).padStart(2, "0").toUpperCase();
lines.push(
` // ${token.method}${contractNote} hash=${upperHex(token.hash)} ` +
`params=${token.parametersCount} returns=${token.hasReturnValue} ` +
`flags=0x${flagsHex} (${describeCallFlags(token.callFlags)})`,
);
if (hint && !hint.hasExactMethod()) {
lines.push(
` // warning: native contract ${hint.contract} does not expose method ${token.method}`,
);
}
}
}
function writeMethodStatements(lines, statements, group, context) {
let indentLevel = 0;
const slotTypes = context?.slotTypesByOffset?.get(group.start) ?? null;
for (const rawStatement of statements) {
const statement = context?.typedDeclarations
? annotateStatementTypes(rawStatement.trim(), slotTypes)
: rawStatement.trim();
const trimmed = statement.trim();
if (trimmed.startsWith("}")) {
indentLevel = Math.max(0, indentLevel - 1);
}
lines.push(`${" ".repeat(8 + indentLevel * 4)}${trimmed}`);
if (trimmed.endsWith("{")) {
indentLevel += 1;
}
}
}
function annotateStatementTypes(line, slotTypes) {
if (!slotTypes) {
return line;
}
if (line.startsWith("let ")) {
return annotateLet(line.slice("let ".length), slotTypes) ?? line;
}
if (line.startsWith("for (let ")) {
const rest = line.slice("for (let ".length);
const annotated = annotateLet(rest, slotTypes);
return annotated ? `for (${annotated}` : line;
}
return line;
}
function annotateLet(rest, slotTypes) {
const name = rest.split(/[\s=]/u, 1)[0] ?? "";
const ty = declarationType(name, slotTypes);
return ty ? `${ty} ${rest}` : null;
}
function declarationType(name, slotTypes) {
let slots;
let indexText;
if (name.startsWith("loc")) {
slots = slotTypes.locals;
indexText = name.slice("loc".length);
} else if (name.startsWith("static")) {
slots = slotTypes.statics;
indexText = name.slice("static".length);
} else {
return "";
}
if (!/^[0-9]+$/u.test(indexText)) {
return "";
}
return slots?.[Number(indexText)] ?? "";
}
function renderExtraScalar(value) {
if (typeof value === "string") return value;
if (typeof value === "boolean") return value.toString();
if (typeof value === "number" && Number.isFinite(value)) return value.toString();
if (typeof value === "bigint") return value.toString();
return null;
}
function formatManifestTrusts(trusts) {
if (trusts === "*") {
return "*";
}
if (Array.isArray(trusts)) {
if (trusts.length === 0) {
return "[]";
}
if (trusts.every((entry) => typeof entry === "string")) {
return `[${trusts.map((entry) => `"${entry}"`).join(", ")}]`;
}
return JSON.stringify(trusts);
}
if (trusts && typeof trusts === "object") {
const structured = formatStructuredTrusts(trusts);
if (structured !== null) {
return structured;
}
}
return JSON.stringify(trusts);
}
function formatStructuredTrusts(object) {
const allowedKeys = new Set(["hashes", "groups"]);
for (const key of Object.keys(object)) {
if (!allowedKeys.has(key)) {
return null;
}
}
const hashes = parseTypedTrustEntries(object.hashes, "hash");
if (hashes === null) {
return null;
}
const groups = parseTypedTrustEntries(object.groups, "group");
if (groups === null) {
return null;
}
return `[${[...hashes, ...groups].join(", ")}]`;
}
function parseTypedTrustEntries(value, prefix) {
if (value === undefined || value === null) {
return [];
}
if (!Array.isArray(value)) {
return null;
}
const entries = [];
for (const entry of value) {
if (typeof entry !== "string") {
return null;
}
entries.push(`${prefix}:${entry}`);
}
return entries;
}
function renderMethodSignature(group, used, context = null) {
const name = makeUniqueIdentifier(group.name, used);
const parameters = group.source?.parameters ?? [];
let args;
if (parameters.length > 0) {
args = formatManifestParameters(parameters);
} else {
const inferredArgCount = context?.methodArgCountsByOffset?.get(group.start) ?? 0;
args = formatInferredParameters(inferredArgCount, group.start, context);
}
const returnType = group.source?.returnType;
const renderedReturnType = returnType ? formatManifestType(returnType) : null;
if (renderedReturnType && renderedReturnType !== "void") {
return `fn ${name}(${args}) -> ${renderedReturnType}`;
}
return `fn ${name}(${args})`;
}
function formatInferredParameters(count, methodOffset, context) {
const slotTypes = context?.typedDeclarations
? context?.slotTypesByOffset?.get(methodOffset)
: null;
return Array.from({ length: count }, (_, index) => {
const label = `arg${index}`;
const ty = slotTypes?.arguments?.[index] ?? "";
return ty ? `${label}: ${ty}` : label;
}).join(", ");
}