neo-decompiler 0.10.1

Neo N3 NEF decompiler: parse, disassemble, lift bytecode to high-level pseudocode and C# skeletons, with a CLI, JSON reports, and optional WebAssembly bindings.
Documentation
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);
    }

    // Inferred helpers (no manifest source) whose body lifts to nothing are
    // usually padding runs of NOPs between real methods. Manifest-declared
    // methods still get the placeholder so the user sees the ABI is honoured.
    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(", ");
}