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 { buildCallGraph } from "./call-graph.js";
import { NeoDecompilerError, NefParseError, DisassemblyError, ManifestParseError } from "./errors.js";
import { parseNef, parseNefWithWarnings } from "./nef.js";
import { disassembleScript } from "./disassembler.js";
import { inferRequiredEntryStackDepth } from "./entry-stack.js";
import { renderGroupedPseudocode } from "./grouped-pseudocode.js";
import { renderHighLevelMethodGroups } from "./high-level.js";
import { classifyPermissionContract, parseManifest } from "./manifest.js";
import { buildMethodGroups } from "./methods.js";
import { describeMethodToken } from "./native-contracts.js";
import { renderPseudocode } from "./pseudocode.js";
import { inferTypes } from "./types.js";
import { buildXrefs } from "./xrefs.js";

export {
  buildCallGraph,
  buildMethodGroups,
  buildXrefs,
  classifyPermissionContract,
  inferTypes,
  parseManifest,
  parseNef,
  disassembleScript,
  renderGroupedPseudocode,
  renderPseudocode,
  renderHighLevelMethodGroups,
  NeoDecompilerError,
  NefParseError,
  DisassemblyError,
  ManifestParseError,
};

/**
 * Parse a NEF blob and return its instruction stream plus rendered
 * pseudocode (no manifest correlation, no high-level lifting).
 *
 * @param {Uint8Array | ArrayBuffer | number[]} bytes - Raw NEF bytes.
 * @param {Object} [options] - Disassembly options.
 * @param {boolean} [options.failOnUnknownOpcodes] - Throw instead of
 *   emitting `UNKNOWN_0xNN` when an unknown opcode is encountered.
 * @returns {{
 *   nef: import('./index').NefFile,
 *   instructions: import('./index').Instruction[],
 *   warnings: string[],
 *   pseudocode: string,
 * }}
 */
export function decompileBytes(bytes, options = {}) {
  const parsed = parseNefWithWarnings(bytes);
  const nef = parsed.nef;
  const disassembly = disassembleScript(nef.script, options);
  return {
    nef,
    instructions: disassembly.instructions,
    warnings: [...parsed.warnings, ...disassembly.warnings],
    pseudocode: renderPseudocode(disassembly.instructions),
  };
}

/**
 * Run the full analysis pipeline (CFG, xrefs, type inference) against
 * a NEF and an optional manifest. Returns the same fields as
 * `decompileBytes` plus method groups, call graph, xrefs, and
 * inferred types.
 */
export function analyzeBytes(bytes, manifestInput = null, options = {}) {
  const manifest = manifestInput ? parseManifest(manifestInput) : null;
  const result = decompileBytes(bytes, options);
  const methodGroups = buildMethodGroups(result.instructions, manifest);
  // Analysis (call graph / xrefs / types) groups on the same baseline as the
  // Rust port's `analysis::MethodTable`, which excludes post-terminator
  // detached tails (a presentation-only heuristic). Using the tail-included
  // `methodGroups` here would attribute padding/tail chunks to spurious method
  // entries that the Rust analysis never reports, diverging the two ports.
  const analysisGroups = buildMethodGroups(result.instructions, manifest, {
    includePostTerminatorTails: false,
  });
  return {
    ...result,
    manifest,
    methodGroups,
    callGraph: buildCallGraph(result.nef, result.instructions, analysisGroups),
    xrefs: buildXrefs(result.instructions, analysisGroups),
    types: inferTypes(result.instructions, analysisGroups, manifest, result.nef.methodTokens),
  };
}

/**
 * Like `decompileBytes` but with a manifest, so methods can be
 * grouped by their declared offsets and the grouped pseudocode
 * surface (`groupedPseudocode`) becomes available.
 *
 * @param {Uint8Array | ArrayBuffer | number[]} bytes
 * @param {string | object} manifestInput - Manifest JSON string or
 *   parsed object.
 * @param {Object} [options]
 */
export function decompileBytesWithManifest(bytes, manifestInput, options = {}) {
  const manifest = parseManifest(manifestInput);
  const result = decompileBytes(bytes, options);
  const methodGroups = buildMethodGroups(result.instructions, manifest);
  return {
    ...result,
    manifest,
    methodGroups,
    groupedPseudocode: renderGroupedPseudocode(methodGroups, manifest),
  };
}

/**
 * Decompile a NEF directly to high-level pseudocode (no manifest).
 * The resulting `highLevel` string is the human-readable surface
 * that mirrors the Rust CLI's `--format high-level` default.
 *
 * @param {Uint8Array | ArrayBuffer | number[]} bytes
 * @param {Object} [options] - See `DecompileOptions` in index.d.ts.
 * @param {boolean} [options.clean] - Opt-in maximum-readability
 *   shorthand: enables `inlineSingleUseTemps` and strips informational
 *   slot-declaration comments. Off by default.
 * @param {boolean} [options.inlineSingleUseTemps] - Inline single-use
 *   `tN` temporaries into their use site. Off by default; implied by
 *   `clean: true`.
 * @param {boolean} [options.typedDeclarations] - Annotate inferred argument
 *   signatures and local/static declarations with Neo VM types. Off by default.
 * @param {boolean} [options.failOnUnknownOpcodes] - Throw instead of
 *   emitting `UNKNOWN_0xNN` when an unknown opcode is encountered.
 */
export function decompileHighLevelBytes(bytes, options = {}) {
  const result = decompileBytes(bytes, options);
  const methodGroups = buildMethodGroups(result.instructions, null);
  const context = buildHighLevelContext(methodGroups, result.nef, options, null, result.instructions);
  const highLevel = renderHighLevelMethodGroups(methodGroups, null, context);
  return {
    ...result,
    warnings: [...result.warnings, ...context.highLevelWarnings],
    methodGroups,
    highLevel,
  };
}

/**
 * High-level decompile + manifest correlation in one call. Same
 * `highLevel` surface as `decompileHighLevelBytes`, plus method
 * groups and grouped pseudocode for callers that want both views.
 *
 * @param {Uint8Array | ArrayBuffer | number[]} bytes
 * @param {string | object} manifestInput
 * @param {Object} [options]
 */
export function decompileHighLevelBytesWithManifest(bytes, manifestInput, options = {}) {
  const manifest = parseManifest(manifestInput);
  const result = decompileBytes(bytes, options);
  const methodGroups = buildMethodGroups(result.instructions, manifest);
  const context = buildHighLevelContext(
    methodGroups,
    result.nef,
    options,
    manifest,
    result.instructions,
  );
  const highLevel = renderHighLevelMethodGroups(methodGroups, manifest, context);
  return {
    ...result,
    warnings: [...result.warnings, ...context.highLevelWarnings],
    manifest,
    methodGroups,
    highLevel,
    groupedPseudocode: renderGroupedPseudocode(methodGroups, manifest),
  };
}

function buildHighLevelContext(methodGroups, nef, options = {}, manifest = null, instructions = []) {
  const entryOffset = methodGroups[0]?.start ?? 0;
  const slotTypesByOffset = options.typedDeclarations
    ? buildSlotTypesByOffset(inferTypes(instructions, methodGroups, manifest, nef.methodTokens))
    : new Map();
  return {
    methodLabelsByOffset: new Map(methodGroups.map((group) => [group.start, group.name])),
    methodArgCountsByOffset: new Map(
      methodGroups.map((group) => [group.start, inferMethodArgCount(group, entryOffset)]),
    ),
    // Resolve token-call labels through the native-contract describe
    // table so calls into known contracts render as
    // `GasToken::Transfer(...)` rather than just `Transfer(...)`. The
    // qualified form mirrors Rust's `callt_labels` (which already runs
    // through `native_contracts::describe_method_token` →
    // `formatted_label`). Falls back to the raw method name when the
    // hash isn't in the native-contract table.
    calltLabels: nef.methodTokens.map((token) => {
      const hint = describeMethodToken(token.hash, token.method);
      return hint ? hint.formattedLabel(token.method) : token.method;
    }),
    calltParamCounts: nef.methodTokens.map((token) => token.parametersCount),
    calltReturnsValue: nef.methodTokens.map((token) => token.hasReturnValue),
    methodTokens: nef.methodTokens,
    scriptHash: nef.scriptHash,
    scriptHashLE: nef.scriptHashLE,
    compiler: nef.header?.compiler,
    source: nef.header?.source,
    highLevelWarnings: [],
    typedDeclarations: !!options.typedDeclarations,
    slotTypesByOffset,
    postprocessOptions: {
      // `clean: true` is a convenience shorthand that enables every
      // readability-focused postprocess option. Today that's
      // `inlineSingleUseTemps` plus stripping informational slot-declaration
      // comments, but new options will compose under the same shorthand
      // without callers needing to update.
      inlineSingleUseTemps:
        !!options.inlineSingleUseTemps || !!options.clean,
      clean: !!options.clean,
    },
  };
}

function buildSlotTypesByOffset(types) {
  const statics = types.statics.map(inferredTypeToPseudo);
  return new Map(
    types.methods.map((method) => [
      method.method.offset,
      {
        arguments: method.arguments.map(inferredTypeToPseudo),
        locals: method.locals.map(inferredTypeToPseudo),
        statics,
      },
    ]),
  );
}

function inferredTypeToPseudo(ty) {
  switch (ty) {
    case "any":
    case "null":
      return "any";
    case "bool":
      return "bool";
    case "integer":
      return "int";
    case "bytestring":
    case "buffer":
      return "byte[]";
    case "array":
    case "struct":
      return "object[]";
    case "map":
      return "map";
    case "interopinterface":
      return "interop";
    case "pointer":
      return "pointer";
    default:
      return "";
  }
}

function inferMethodArgCount(group, entryOffset) {
  if (group.source?.parameters) {
    return group.source.parameters.length;
  }
  const first = group.instructions[0];
  if (first?.opcode?.mnemonic === "INITSLOT" && first.operand?.kind === "Bytes" && first.operand.value.length >= 2) {
    return first.operand.value[1];
  }
  if (group.start === entryOffset) {
    return 0;
  }
  return inferRequiredEntryStackDepth(group.instructions);
}