export-ton-verifier
Version:
Tool for generating Groth16 and PLONK verifier code for the TON blockchain from SnarkJS .zkey or verification key JSON files.
428 lines (401 loc) • 15.1 kB
JavaScript
#!/usr/bin/env node
import {
proofToMessageBoc
} from "./chunk-OYYC4SR3.js";
import {
formatDoctorReport,
inspectVerifierInput
} from "./chunk-K5M5T7RZ.js";
import "./chunk-SFKUOXLN.js";
import {
generateVerifier
} from "./chunk-4RQ5LN5R.js";
import "./chunk-LXOLCHR7.js";
import "./chunk-6TDHKTNH.js";
import "./chunk-5MKK56I7.js";
import "./chunk-TB4DFXWQ.js";
import "./chunk-MCE4ZZVW.js";
import "./chunk-IELVU6ER.js";
import "./chunk-I7S4EEHA.js";
import "./chunk-4I2ZWZKN.js";
// src/cli.js
import path3 from "path";
import { fileURLToPath } from "url";
// src/cli/commands.js
import fsp2 from "fs/promises";
import path2 from "path";
// src/cli/errors.js
var CliUsageError = class extends Error {
constructor(message, { showHelp = false } = {}) {
super(message);
this.name = "CliUsageError";
this.showHelp = showHelp;
}
};
// src/cli/help.js
var HELP_TEXT = `
Usage:
export-ton-verifier <inputPath> <outputPath> [--func | --tolk | --tact] [--contract-name <name>] [--wrapper-dest <destPath>] [--groth16 | --plonk] [--force]
export-ton-verifier doctor <inputPath> [--func | --tolk | --tact] [--json]
export-ton-verifier proof-to-message <proof.json> <public.json> [--func | --tolk] [--groth16 | --plonk] [--format base64|hex] [--output <file>]
export-ton-verifier import-wrapper <destPath> [--func | --tolk] [--groth16 | --plonk] [--force]
Description:
1) Generates a TON verifier smart contract from .zkey, snarkjs JSON, gnark, or arkworks artifacts.
Protocol (Groth16/PLONK) is auto-detected from the input artifact.
2) Diagnoses verifier artifacts before generation with the 'doctor' subcommand.
3) Converts proof/public JSON into supported internal message body BOCs.
4) (Optional) Can also copy a TypeScript wrapper template to your project.
Wrapper selection supports language- and protocol-specific files.
Notes:
- Tolk is the default for Groth16 and PLONK verifier generation.
Use --func to generate FunC output.
- If language is Tact (--tact), wrapper copy is skipped even if --wrapper-dest is provided.
Arguments:
inputPath Path to .zkey, snarkjs verification_key.json, gnark JSON/.bin, or arkworks JSON
outputPath Path to save the generated verifier file
Subcommands:
doctor Reports input format, protocol, curve, public input count, template support, and TVM limits.
proof-to-message
Converts proof/public JSON into a base64 or hex internal message body BOC.
import-wrapper Copies a language-specific wrapper to <destPath> (file or directory).
If <destPath> is a directory, the file will be saved as <destPath>/Verifier.ts.
Requires one of --groth16 or --plonk.
Options:
-h, --help Show this help message and exit
--func Use FunC language template for the verifier
--tolk Use Tolk language template for the verifier [default]
--tact Use Tact language template for the verifier
--contract-name <name> Wrap the generated Tolk verifier in a named struct receiver
--wrapper-dest <destPath> After generation, copy a language-specific TypeScript wrapper to <destPath>
--groth16 Protocol hint for wrapper selection (wrapper only; verifier protocol is auto-detected)
--plonk Protocol hint for wrapper selection (wrapper only; verifier protocol is auto-detected)
--json Print machine-readable JSON for 'doctor'
--format <base64|hex> Encoding for 'proof-to-message' output [default: base64]
--output <file> Write 'proof-to-message' output to a file
--force Overwrite existing file when used with 'import-wrapper' or '--wrapper-dest'
Examples:
# Generate Tolk verifier from .zkey (default language for Groth16 and PLONK)
npx export-ton-verifier ./circuits/verifier.zkey ./verifier.tolk
# Generate from verification_key.json
npx export-ton-verifier ./circuits/verification_key.json ./verifier.tolk
# Generate from native BLS12-381 gnark JSON or binary
npx export-ton-verifier ./circuits/verification_key_gnark.json ./verifier.tolk
npx export-ton-verifier ./circuits/verification_key.bin ./verifier.tolk
# Generate from native BLS12-381 arkworks compressed JSON/bundle
npx export-ton-verifier ./circuits/groth16_artifacts.json ./verifier.tolk
# Generate FunC verifier (requires --func)
npx export-ton-verifier ./circuits/verifier.zkey ./verifier.fc --func
# Generate Tact verifier
npx export-ton-verifier ./circuits/verifier.zkey ./verifier.tact --tact
# Generate and also drop a wrapper (auto-detect protocol from the input artifact)
npx export-ton-verifier ./circuits/verification_key.json ./verifier.tolk --wrapper-dest ./wrappers/ --force
# Generate a named Tolk verifier receiver with getter verify_MultiplierVerifier
npx export-ton-verifier ./circuits/verifier.zkey ./verifier.tolk --contract-name multiplierVerifier
# Diagnose generation support
npx export-ton-verifier doctor ./circuits/verification_key.json --tolk
npx export-ton-verifier doctor ./circuits/verification_key.json --json
# Build internal message body BOC from proof/public JSON
npx export-ton-verifier proof-to-message ./circuits/proof.json ./circuits/public.json --groth16 --tolk
npx export-ton-verifier proof-to-message ./circuits/proof.json ./circuits/public.json --plonk --tolk --format hex --output ./body.hex
# Copy the Tolk Groth16 wrapper (default wrapper language)
npx export-ton-verifier import-wrapper ./wrappers/ --groth16 --force
# Copy the PLONK TypeScript wrapper with the default language selection
npx export-ton-verifier import-wrapper ./wrappers/ --plonk --force
# Copy the PLONK TypeScript wrapper for FunC
npx export-ton-verifier import-wrapper ./wrappers/ --plonk --func --force
`;
function printHelp(stdout = process.stdout) {
stdout.write(`${HELP_TEXT}
`);
}
// src/cli/options.js
function parseLangFlag(args) {
const allowed = ["--func", "--tolk", "--tact"];
const chosen = allowed.filter((flag) => args.includes(flag));
if (chosen.length > 1) {
throw new CliUsageError("Use only one of --func, --tolk, --tact.");
}
if (chosen.length === 0) return "tolk";
return chosen[0].slice(2);
}
function parseProtocolFlag(args, { required = false } = {}) {
const isGroth = args.includes("--groth16");
const isPlonk = args.includes("--plonk");
if (isGroth && isPlonk) {
throw new CliUsageError("Use only one of --groth16 or --plonk.");
}
if (!isGroth && !isPlonk) {
if (required) {
throw new CliUsageError("Missing protocol flag: use --groth16 or --plonk.");
}
return null;
}
return isGroth ? "groth16" : "plonk";
}
function getFlagValue(args, flag) {
const idx = args.indexOf(flag);
if (idx === -1) return null;
const value = args[idx + 1];
if (!value || value.startsWith("--")) {
throw new CliUsageError(`Missing value for ${flag}.`);
}
return value;
}
function positionalArgs(args, { valueFlags = [] } = {}) {
const flagsWithValues = new Set(valueFlags);
const positional = [];
for (let idx = 0; idx < args.length; idx += 1) {
const arg = args[idx];
if (arg.startsWith("--")) {
if (flagsWithValues.has(arg) && args[idx + 1] && !args[idx + 1].startsWith("--")) {
idx += 1;
}
continue;
}
positional.push(arg);
}
return positional;
}
// src/cli/wrappers.js
import fs from "fs";
import fsp from "fs/promises";
import path from "path";
function fileExists(p) {
try {
return fs.existsSync(p);
} catch {
return false;
}
}
async function ensureDir(dir) {
await fsp.mkdir(dir, { recursive: true });
}
async function copyWrapper(wrapperSrc, destPath, { force = false, stdout = process.stdout } = {}) {
if (!destPath) throw new CliUsageError("Missing <destPath> for wrapper copy.");
let stat = null;
try {
stat = await fsp.stat(destPath);
} catch {
}
let finalDest = destPath;
if ((stat == null ? void 0 : stat.isDirectory()) || hasTrailingPathSeparator(destPath)) {
await ensureDir(destPath);
finalDest = path.join(destPath, "Verifier.ts");
} else {
await ensureDir(path.dirname(destPath));
}
if (fileExists(finalDest) && !force) {
throw new Error(
`Wrapper already exists: ${finalDest}. Use --force to overwrite.`
);
}
const content = await fsp.readFile(wrapperSrc);
await fsp.writeFile(finalDest, content);
stdout.write(`Wrapper copied to: ${finalDest}
`);
}
function hasTrailingPathSeparator(destPath) {
return /[\\/]$/.test(destPath);
}
function wrapperTemplateCandidates(lang, protocol) {
switch (protocol) {
case "groth16":
case "plonk":
return [`Verifier_${lang}_${protocol}.ts`, `Verifier_${protocol}.ts`];
default:
throw new CliUsageError(`Unknown protocol for wrapper: ${protocol}`);
}
}
function resolveWrapperTemplate(templatesDir, lang, protocol) {
if (lang === "tact") {
throw new CliUsageError("TypeScript wrappers are not available for Tact.");
}
for (const file of wrapperTemplateCandidates(lang, protocol)) {
const fullPath = path.join(templatesDir, file);
if (fileExists(fullPath)) {
return fullPath;
}
}
throw new CliUsageError(
`Wrapper template not found for language '${lang}' and protocol '${protocol}'.`
);
}
// src/cli/commands.js
function writeLine(stream, message = "") {
stream.write(`${message}
`);
}
function stdoutLogger(stdout) {
return {
log(message) {
writeLine(stdout, message);
}
};
}
function normalizeEnv(env) {
return {
stdout: env.stdout ?? process.stdout,
stderr: env.stderr ?? process.stderr,
templatesDir: env.templatesDir
};
}
function writeUsageError(err, ctx) {
writeLine(ctx.stderr, `Error: ${err.message}`);
if (err.showHelp) {
printHelp(ctx.stdout);
}
}
async function runDoctorCommand(args, ctx) {
const [inputPath] = positionalArgs(args.slice(1));
const json = args.includes("--json");
if (!inputPath || inputPath.startsWith("--")) {
const message = "Missing <inputPath> for 'doctor'.";
if (json) {
writeLine(ctx.stdout, JSON.stringify({ ok: false, error: message }, null, 2));
} else {
writeLine(ctx.stderr, `Error: ${message}`);
printHelp(ctx.stdout);
}
return 1;
}
try {
const report = await inspectVerifierInput(inputPath, {
lang: parseLangFlag(args),
templatesDir: ctx.templatesDir
});
if (json) {
writeLine(ctx.stdout, JSON.stringify(report, null, 2));
} else {
ctx.stdout.write(formatDoctorReport(report));
}
return report.ok ? 0 : 1;
} catch (err) {
const message = (err == null ? void 0 : err.message) || String(err);
if (json) {
writeLine(ctx.stdout, JSON.stringify({ ok: false, error: message }, null, 2));
} else {
writeLine(ctx.stderr, `Error: ${message}`);
}
return 1;
}
}
async function runProofToMessageCommand(args, ctx) {
const [proofPath, publicPath] = positionalArgs(args.slice(1), {
valueFlags: ["--format", "--output"]
});
if (!proofPath || proofPath.startsWith("--") || !publicPath || publicPath.startsWith("--")) {
writeLine(
ctx.stderr,
"Error: Missing <proof.json> or <public.json> for 'proof-to-message'."
);
printHelp(ctx.stdout);
return 1;
}
try {
const proof = JSON.parse(await fsp2.readFile(proofPath, "utf8"));
const publicSignals = JSON.parse(await fsp2.readFile(publicPath, "utf8"));
const format = getFlagValue(args, "--format") || "base64";
const outputPath = getFlagValue(args, "--output");
const encoded = await proofToMessageBoc({
proof,
publicSignals,
protocol: parseProtocolFlag(args, { required: false }),
lang: parseLangFlag(args),
format
});
if (outputPath) {
await ensureDir(path2.dirname(outputPath));
await fsp2.writeFile(outputPath, encoded, "utf8");
} else {
writeLine(ctx.stdout, encoded);
}
return 0;
} catch (err) {
writeLine(ctx.stderr, `Error: ${(err == null ? void 0 : err.message) || err}`);
return 1;
}
}
async function runImportWrapperCommand(args, ctx) {
const [destPath] = positionalArgs(args.slice(1));
if (!destPath || destPath.startsWith("--")) {
throw new CliUsageError("Missing <destPath> for 'import-wrapper'.", {
showHelp: true
});
}
const protocol = parseProtocolFlag(args, { required: true });
const lang = parseLangFlag(args);
const force = args.includes("--force");
const wrapperSrc = resolveWrapperTemplate(ctx.templatesDir, lang, protocol);
await copyWrapper(wrapperSrc, destPath, { force, stdout: ctx.stdout });
return 0;
}
async function runGenerateCommand(args, ctx) {
const positional = positionalArgs(args, {
valueFlags: ["--contract-name", "--wrapper-dest"]
});
if (positional.length < 2) {
throw new CliUsageError("Missing required arguments.", { showHelp: true });
}
const [inputPath, outputPath] = positional;
const lang = parseLangFlag(args);
const contractName = getFlagValue(args, "--contract-name");
const wrapperDest = getFlagValue(args, "--wrapper-dest");
const force = args.includes("--force");
if (!fileExists(inputPath)) {
throw new Error(`Input file not found: ${inputPath}`);
}
const detectedProtocol = await generateVerifier(inputPath, outputPath, {
lang,
templatesDir: ctx.templatesDir,
contractName,
logger: stdoutLogger(ctx.stdout)
});
if (wrapperDest) {
if (lang === "tact") {
writeLine(
ctx.stdout,
"Tact selected - wrapper copy is skipped even though --wrapper-dest was provided."
);
} else {
const protocol = parseProtocolFlag(args, { required: false }) || detectedProtocol;
const wrapperSrc = resolveWrapperTemplate(ctx.templatesDir, lang, protocol);
await copyWrapper(wrapperSrc, wrapperDest, { force, stdout: ctx.stdout });
}
}
return 0;
}
async function runCli(args, env = {}) {
const ctx = normalizeEnv(env);
try {
if (args.includes("--help") || args.includes("-h") || args.length === 0) {
printHelp(ctx.stdout);
return 0;
}
if (args.includes("--vk")) {
throw new CliUsageError("--vk is no longer supported. Use a .json file directly.");
}
switch (args[0]) {
case "doctor":
return await runDoctorCommand(args, ctx);
case "proof-to-message":
return await runProofToMessageCommand(args, ctx);
case "import-wrapper":
return await runImportWrapperCommand(args, ctx);
default:
return await runGenerateCommand(args, ctx);
}
} catch (err) {
if (err instanceof CliUsageError) {
writeUsageError(err, ctx);
} else {
writeLine(ctx.stderr, `Error: ${(err == null ? void 0 : err.message) || err}`);
}
return 1;
}
}
// src/cli.js
var __dirname = path3.dirname(fileURLToPath(import.meta.url));
var status = await runCli(process.argv.slice(2), {
templatesDir: path3.join(__dirname, "../templates")
});
process.exit(status);