midnight-mcp 0.1.28 → 0.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -84
- package/dist/server.js +5 -5
- package/dist/tools/meta.js +1 -10
- package/dist/tools/repository/handlers.d.ts +1 -1
- package/dist/tools/repository/handlers.js +1 -1
- package/dist/tools/repository/index.d.ts +2 -2
- package/dist/tools/repository/index.js +2 -2
- package/dist/tools/repository/schemas.d.ts +0 -22
- package/dist/tools/repository/schemas.js +0 -20
- package/dist/tools/repository/tools.js +9 -112
- package/dist/tools/repository/validation.d.ts +2 -533
- package/dist/tools/repository/validation.js +3 -741
- package/dist/types/mcp.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Contract validation handlers
|
|
3
|
-
*
|
|
3
|
+
* Static analysis for Compact contracts
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { writeFile, mkdir, readFile, rm } from "fs/promises";
|
|
8
|
-
import { join, basename, resolve, isAbsolute } from "path";
|
|
9
|
-
import { tmpdir } from "os";
|
|
5
|
+
import { readFile } from "fs/promises";
|
|
6
|
+
import { basename, isAbsolute, resolve } from "path";
|
|
10
7
|
import { platform } from "process";
|
|
11
8
|
import { logger } from "../../utils/index.js";
|
|
12
|
-
const execAsync = promisify(exec);
|
|
13
|
-
const execFileAsync = promisify(execFile);
|
|
14
9
|
// ============================================================================
|
|
15
10
|
// SECURITY & VALIDATION HELPERS
|
|
16
11
|
// ============================================================================
|
|
@@ -76,739 +71,6 @@ function isValidUtf8Text(content) {
|
|
|
76
71
|
}
|
|
77
72
|
return true;
|
|
78
73
|
}
|
|
79
|
-
/**
|
|
80
|
-
* Detect local includes that won't work in temp directory
|
|
81
|
-
*/
|
|
82
|
-
function detectLocalIncludes(code) {
|
|
83
|
-
const localIncludes = [];
|
|
84
|
-
// Pattern: include "something.compact" or include "./path"
|
|
85
|
-
const includePattern = /include\s+"([^"]+)"/g;
|
|
86
|
-
let match;
|
|
87
|
-
while ((match = includePattern.exec(code)) !== null) {
|
|
88
|
-
const includePath = match[1];
|
|
89
|
-
// Skip standard library includes
|
|
90
|
-
if (includePath === "std" ||
|
|
91
|
-
includePath.startsWith("CompactStandardLibrary")) {
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
// Local file reference
|
|
95
|
-
if (includePath.endsWith(".compact") ||
|
|
96
|
-
includePath.startsWith("./") ||
|
|
97
|
-
includePath.startsWith("../")) {
|
|
98
|
-
localIncludes.push(includePath);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return localIncludes;
|
|
102
|
-
}
|
|
103
|
-
// ============================================================================
|
|
104
|
-
// VALIDATION HANDLERS
|
|
105
|
-
// ============================================================================
|
|
106
|
-
/**
|
|
107
|
-
* Validate a Compact contract by running the compiler
|
|
108
|
-
* This provides pre-compilation validation with detailed error diagnostics
|
|
109
|
-
*/
|
|
110
|
-
export async function validateContract(input) {
|
|
111
|
-
logger.debug("Validating contract", {
|
|
112
|
-
filename: input.filename,
|
|
113
|
-
hasCode: !!input.code,
|
|
114
|
-
filePath: input.filePath,
|
|
115
|
-
});
|
|
116
|
-
// ============================================================================
|
|
117
|
-
// RESOLVE CODE SOURCE - Either from code string or file path
|
|
118
|
-
// ============================================================================
|
|
119
|
-
let code;
|
|
120
|
-
let filename;
|
|
121
|
-
let sourceDir = null; // Track source directory for local includes
|
|
122
|
-
let originalFilePath = null; // Track original file path for compilation
|
|
123
|
-
if (input.filePath) {
|
|
124
|
-
// SECURITY: Validate file path first
|
|
125
|
-
const pathValidation = validateFilePath(input.filePath);
|
|
126
|
-
if (!pathValidation.valid) {
|
|
127
|
-
return {
|
|
128
|
-
success: false,
|
|
129
|
-
errorType: "security_error",
|
|
130
|
-
error: "Invalid file path",
|
|
131
|
-
message: `❌ ${pathValidation.error}`,
|
|
132
|
-
userAction: {
|
|
133
|
-
problem: pathValidation.error,
|
|
134
|
-
solution: "Provide an absolute path to a .compact file in your project directory",
|
|
135
|
-
example: { filePath: "/Users/you/projects/myapp/contract.compact" },
|
|
136
|
-
isUserFault: true,
|
|
137
|
-
},
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
const safePath = pathValidation.normalizedPath;
|
|
141
|
-
sourceDir = join(safePath, "..");
|
|
142
|
-
originalFilePath = safePath; // Store for use in compilation
|
|
143
|
-
// SECURITY: Validate sourceDir against blocked paths
|
|
144
|
-
// This prevents malicious includes from accessing system directories
|
|
145
|
-
const sourceDirValidation = validateFilePath(join(sourceDir, "dummy.compact"));
|
|
146
|
-
if (!sourceDirValidation.valid &&
|
|
147
|
-
sourceDirValidation.error?.includes("system directories")) {
|
|
148
|
-
return {
|
|
149
|
-
success: false,
|
|
150
|
-
errorType: "security_error",
|
|
151
|
-
error: "Invalid source directory",
|
|
152
|
-
message: "❌ Cannot access files in system directories",
|
|
153
|
-
userAction: {
|
|
154
|
-
problem: "The contract's parent directory is a restricted system location",
|
|
155
|
-
solution: "Move your contract files to a user project directory",
|
|
156
|
-
isUserFault: true,
|
|
157
|
-
},
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
// Read code from file
|
|
161
|
-
try {
|
|
162
|
-
code = await readFile(safePath, "utf-8");
|
|
163
|
-
filename = basename(safePath);
|
|
164
|
-
// SECURITY: Check for binary/non-UTF8 content
|
|
165
|
-
if (!isValidUtf8Text(code)) {
|
|
166
|
-
return {
|
|
167
|
-
success: false,
|
|
168
|
-
errorType: "user_error",
|
|
169
|
-
error: "Invalid file content",
|
|
170
|
-
message: "❌ File appears to be binary or contains invalid characters",
|
|
171
|
-
userAction: {
|
|
172
|
-
problem: "The file is not a valid UTF-8 text file",
|
|
173
|
-
solution: "Ensure you're pointing to a Compact source file (.compact), not a compiled binary",
|
|
174
|
-
isUserFault: true,
|
|
175
|
-
},
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
catch (fsError) {
|
|
180
|
-
const err = fsError;
|
|
181
|
-
return {
|
|
182
|
-
success: false,
|
|
183
|
-
errorType: "user_error",
|
|
184
|
-
error: "Failed to read file",
|
|
185
|
-
message: `❌ Cannot read file: ${input.filePath}`,
|
|
186
|
-
userAction: {
|
|
187
|
-
problem: err.code === "ENOENT"
|
|
188
|
-
? "File does not exist"
|
|
189
|
-
: err.code === "EACCES"
|
|
190
|
-
? "Permission denied"
|
|
191
|
-
: "Cannot read file",
|
|
192
|
-
solution: err.code === "ENOENT"
|
|
193
|
-
? "Check that the file path is correct"
|
|
194
|
-
: "Check file permissions",
|
|
195
|
-
details: err.message,
|
|
196
|
-
isUserFault: true,
|
|
197
|
-
},
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
else if (input.code) {
|
|
202
|
-
code = input.code;
|
|
203
|
-
// Sanitize filename to prevent command injection
|
|
204
|
-
const rawFilename = input.filename || "contract.compact";
|
|
205
|
-
filename = rawFilename.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
206
|
-
if (!filename.endsWith(".compact")) {
|
|
207
|
-
filename = "contract.compact";
|
|
208
|
-
}
|
|
209
|
-
// Check for binary content in provided code
|
|
210
|
-
if (!isValidUtf8Text(code)) {
|
|
211
|
-
return {
|
|
212
|
-
success: false,
|
|
213
|
-
errorType: "user_error",
|
|
214
|
-
error: "Invalid code content",
|
|
215
|
-
message: "❌ Code contains invalid characters",
|
|
216
|
-
userAction: {
|
|
217
|
-
problem: "The provided code contains binary or non-printable characters",
|
|
218
|
-
solution: "Provide valid UTF-8 Compact source code",
|
|
219
|
-
isUserFault: true,
|
|
220
|
-
},
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
// Neither code nor filePath provided
|
|
226
|
-
return {
|
|
227
|
-
success: false,
|
|
228
|
-
errorType: "user_error",
|
|
229
|
-
error: "No contract provided",
|
|
230
|
-
message: "❌ Must provide either 'code' or 'filePath'",
|
|
231
|
-
userAction: {
|
|
232
|
-
problem: "Neither code string nor file path was provided",
|
|
233
|
-
solution: "Provide the contract source code OR a path to a .compact file",
|
|
234
|
-
example: {
|
|
235
|
-
withCode: { code: "pragma language_version >= 0.16; ..." },
|
|
236
|
-
withFile: { filePath: "/path/to/contract.compact" },
|
|
237
|
-
},
|
|
238
|
-
isUserFault: true,
|
|
239
|
-
},
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
// ============================================================================
|
|
243
|
-
// INPUT VALIDATION - Check for user errors before attempting compilation
|
|
244
|
-
// ============================================================================
|
|
245
|
-
// Check for local includes that won't work in temp directory
|
|
246
|
-
const localIncludes = detectLocalIncludes(code);
|
|
247
|
-
if (localIncludes.length > 0 && !sourceDir) {
|
|
248
|
-
// Code was provided directly (not from file) and has local includes
|
|
249
|
-
return {
|
|
250
|
-
success: false,
|
|
251
|
-
errorType: "user_error",
|
|
252
|
-
error: "Local includes detected",
|
|
253
|
-
message: "❌ Contract has local file includes that cannot be resolved",
|
|
254
|
-
userAction: {
|
|
255
|
-
problem: `Contract includes local files: ${localIncludes.join(", ")}`,
|
|
256
|
-
solution: "Use filePath instead of code when your contract has local includes, so we can resolve relative paths",
|
|
257
|
-
detectedIncludes: localIncludes,
|
|
258
|
-
example: {
|
|
259
|
-
instead: '{ code: "include \\"utils.compact\\"; ..." }',
|
|
260
|
-
use: '{ filePath: "/path/to/your/contract.compact" }',
|
|
261
|
-
},
|
|
262
|
-
isUserFault: true,
|
|
263
|
-
},
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
// Warn about local includes (they may fail during compilation)
|
|
267
|
-
const localIncludeWarning = localIncludes.length > 0
|
|
268
|
-
? {
|
|
269
|
-
warning: "Contract has local includes",
|
|
270
|
-
includes: localIncludes,
|
|
271
|
-
note: "Local includes may fail if files are not in the expected location relative to the contract",
|
|
272
|
-
}
|
|
273
|
-
: null;
|
|
274
|
-
// Check for empty input
|
|
275
|
-
if (!code || code.trim().length === 0) {
|
|
276
|
-
return {
|
|
277
|
-
success: false,
|
|
278
|
-
errorType: "user_error",
|
|
279
|
-
error: "Empty contract code provided",
|
|
280
|
-
message: "❌ No contract code to validate",
|
|
281
|
-
userAction: {
|
|
282
|
-
problem: "The contract code is empty or contains only whitespace",
|
|
283
|
-
solution: "Provide valid Compact contract source code",
|
|
284
|
-
example: `pragma language_version >= 0.16;
|
|
285
|
-
|
|
286
|
-
import CompactStandardLibrary;
|
|
287
|
-
|
|
288
|
-
export ledger counter: Counter;
|
|
289
|
-
|
|
290
|
-
export circuit increment(): [] {
|
|
291
|
-
counter.increment(1);
|
|
292
|
-
}`,
|
|
293
|
-
},
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
// Check for excessively large input (potential abuse or mistake)
|
|
297
|
-
const MAX_CODE_SIZE = 1024 * 1024; // 1MB
|
|
298
|
-
if (code.length > MAX_CODE_SIZE) {
|
|
299
|
-
return {
|
|
300
|
-
success: false,
|
|
301
|
-
errorType: "user_error",
|
|
302
|
-
error: "Contract code too large",
|
|
303
|
-
message: "❌ Contract code exceeds maximum size",
|
|
304
|
-
userAction: {
|
|
305
|
-
problem: `Contract is ${(code.length / 1024).toFixed(1)}KB, maximum is ${MAX_CODE_SIZE / 1024}KB`,
|
|
306
|
-
solution: "Reduce contract size or split into multiple files",
|
|
307
|
-
},
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
// Check for missing pragma (common user mistake)
|
|
311
|
-
if (!code.includes("pragma language_version")) {
|
|
312
|
-
return {
|
|
313
|
-
success: false,
|
|
314
|
-
errorType: "user_error",
|
|
315
|
-
error: "Missing pragma directive",
|
|
316
|
-
message: "❌ Contract is missing required pragma directive",
|
|
317
|
-
userAction: {
|
|
318
|
-
problem: "All Compact contracts must start with a pragma language_version directive",
|
|
319
|
-
solution: "Add pragma directive at the beginning of your contract",
|
|
320
|
-
fix: "Add: pragma language_version >= 0.16;",
|
|
321
|
-
example: `pragma language_version >= 0.16;
|
|
322
|
-
|
|
323
|
-
import CompactStandardLibrary;
|
|
324
|
-
|
|
325
|
-
// ... rest of your contract`,
|
|
326
|
-
},
|
|
327
|
-
detectedIssues: ["Missing pragma language_version directive"],
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
// Check for missing import (common for Counter, Map, etc.)
|
|
331
|
-
// Use word boundaries to avoid false positives in comments/strings
|
|
332
|
-
const usesStdLib = /\bCounter\b/.test(code) ||
|
|
333
|
-
/\bMap\s*</.test(code) ||
|
|
334
|
-
/\bSet\s*</.test(code) ||
|
|
335
|
-
/\bOpaque\s*</.test(code);
|
|
336
|
-
const hasImport = /\bimport\s+CompactStandardLibrary\b/.test(code) ||
|
|
337
|
-
/\binclude\s+"std"/.test(code);
|
|
338
|
-
if (usesStdLib && !hasImport) {
|
|
339
|
-
return {
|
|
340
|
-
success: false,
|
|
341
|
-
errorType: "user_error",
|
|
342
|
-
error: "Missing standard library import",
|
|
343
|
-
message: "❌ Contract uses standard library types without importing them",
|
|
344
|
-
userAction: {
|
|
345
|
-
problem: "You're using types like Counter, Map, Set, or Opaque without importing the standard library",
|
|
346
|
-
solution: "Add the import statement after your pragma directive",
|
|
347
|
-
fix: "Add: import CompactStandardLibrary;",
|
|
348
|
-
example: `pragma language_version >= 0.16;
|
|
349
|
-
|
|
350
|
-
import CompactStandardLibrary;
|
|
351
|
-
|
|
352
|
-
export ledger counter: Counter;
|
|
353
|
-
// ...`,
|
|
354
|
-
},
|
|
355
|
-
detectedIssues: [
|
|
356
|
-
"Uses standard library types (Counter, Map, Set, Opaque)",
|
|
357
|
-
"Missing: import CompactStandardLibrary;",
|
|
358
|
-
],
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
// ============================================================================
|
|
362
|
-
// COMPILER CHECK - Verify compiler is available
|
|
363
|
-
// ============================================================================
|
|
364
|
-
let compactPath = "";
|
|
365
|
-
let compilerVersion = "";
|
|
366
|
-
try {
|
|
367
|
-
if (platform === "win32") {
|
|
368
|
-
// On Windows, avoid the built-in NTFS 'compact.exe' from System32
|
|
369
|
-
// by iterating through all candidates and verifying each one
|
|
370
|
-
const { stdout: whereOutput } = await execAsync("where compact.exe");
|
|
371
|
-
const candidates = whereOutput
|
|
372
|
-
.trim()
|
|
373
|
-
.split(/\r?\n/)
|
|
374
|
-
.filter((line) => line.trim().length > 0);
|
|
375
|
-
let found = false;
|
|
376
|
-
for (const candidate of candidates) {
|
|
377
|
-
try {
|
|
378
|
-
const candidatePath = candidate.trim();
|
|
379
|
-
// Skip Windows System32 compact.exe (NTFS compression utility)
|
|
380
|
-
if (candidatePath.toLowerCase().includes("system32")) {
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
const { stdout: versionOutput } = await execFileAsync(candidatePath, [
|
|
384
|
-
"compile",
|
|
385
|
-
"--version",
|
|
386
|
-
]);
|
|
387
|
-
compactPath = candidatePath;
|
|
388
|
-
compilerVersion = versionOutput.trim();
|
|
389
|
-
found = true;
|
|
390
|
-
break;
|
|
391
|
-
}
|
|
392
|
-
catch {
|
|
393
|
-
// Try next candidate
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
if (!found) {
|
|
397
|
-
throw new Error("Compact compiler not found in PATH");
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
else {
|
|
401
|
-
// Unix: use which to find compact
|
|
402
|
-
const { stdout: whichOutput } = await execAsync("which compact");
|
|
403
|
-
compactPath = whichOutput.trim().split(/\r?\n/)[0];
|
|
404
|
-
const { stdout: versionOutput } = await execFileAsync(compactPath, [
|
|
405
|
-
"compile",
|
|
406
|
-
"--version",
|
|
407
|
-
]);
|
|
408
|
-
compilerVersion = versionOutput.trim();
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
catch {
|
|
412
|
-
return {
|
|
413
|
-
success: false,
|
|
414
|
-
errorType: "environment_error",
|
|
415
|
-
compilerInstalled: false,
|
|
416
|
-
error: "Compact compiler not found",
|
|
417
|
-
message: "❌ Compact compiler is not installed",
|
|
418
|
-
installation: {
|
|
419
|
-
message: "The Compact compiler is required for contract validation. Install it with:",
|
|
420
|
-
command: `curl --proto '=https' --tlsv1.2 -LsSf https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh`,
|
|
421
|
-
postInstall: [
|
|
422
|
-
"After installation, run: compact update",
|
|
423
|
-
"Then verify with: compact compile --version",
|
|
424
|
-
],
|
|
425
|
-
docs: "https://docs.midnight.network/develop/tutorial/building",
|
|
426
|
-
},
|
|
427
|
-
userAction: {
|
|
428
|
-
problem: "The Compact compiler is not installed on this system",
|
|
429
|
-
solution: "Install the compiler using the command above, then retry validation",
|
|
430
|
-
isUserFault: false,
|
|
431
|
-
},
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
// Check compiler version compatibility
|
|
435
|
-
const versionMatch = compilerVersion.match(/(\d+)\.(\d+)/);
|
|
436
|
-
if (versionMatch) {
|
|
437
|
-
const major = parseInt(versionMatch[1], 10);
|
|
438
|
-
const minor = parseInt(versionMatch[2], 10);
|
|
439
|
-
if (major === 0 && minor < 16) {
|
|
440
|
-
return {
|
|
441
|
-
success: false,
|
|
442
|
-
errorType: "environment_error",
|
|
443
|
-
compilerInstalled: true,
|
|
444
|
-
compilerVersion,
|
|
445
|
-
error: "Compiler version too old",
|
|
446
|
-
message: `❌ Compact compiler ${compilerVersion} is outdated`,
|
|
447
|
-
userAction: {
|
|
448
|
-
problem: `Your compiler version (${compilerVersion}) may not support current syntax`,
|
|
449
|
-
solution: "Update to the latest compiler version",
|
|
450
|
-
command: "compact update",
|
|
451
|
-
isUserFault: false,
|
|
452
|
-
},
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
// ============================================================================
|
|
457
|
-
// COMPILATION - Create temp files and run compiler
|
|
458
|
-
// ============================================================================
|
|
459
|
-
const tempDir = join(tmpdir(), `midnight-validate-${Date.now()}`);
|
|
460
|
-
const contractPath = join(tempDir, filename);
|
|
461
|
-
const outputDir = join(tempDir, "output");
|
|
462
|
-
try {
|
|
463
|
-
// Create temp directory
|
|
464
|
-
try {
|
|
465
|
-
await mkdir(tempDir, { recursive: true });
|
|
466
|
-
await mkdir(outputDir, { recursive: true });
|
|
467
|
-
}
|
|
468
|
-
catch (fsError) {
|
|
469
|
-
const err = fsError;
|
|
470
|
-
return {
|
|
471
|
-
success: false,
|
|
472
|
-
errorType: "system_error",
|
|
473
|
-
error: "Failed to create temporary directory",
|
|
474
|
-
message: "❌ System error: Cannot create temp files",
|
|
475
|
-
systemError: {
|
|
476
|
-
code: err.code,
|
|
477
|
-
details: err.message,
|
|
478
|
-
problem: err.code === "ENOSPC"
|
|
479
|
-
? "Disk is full"
|
|
480
|
-
: err.code === "EACCES"
|
|
481
|
-
? "Permission denied"
|
|
482
|
-
: "File system error",
|
|
483
|
-
solution: err.code === "ENOSPC"
|
|
484
|
-
? "Free up disk space and retry"
|
|
485
|
-
: err.code === "EACCES"
|
|
486
|
-
? "Check file system permissions"
|
|
487
|
-
: "Check system resources",
|
|
488
|
-
isUserFault: false,
|
|
489
|
-
},
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
// Write contract file
|
|
493
|
-
try {
|
|
494
|
-
await writeFile(contractPath, code, "utf-8");
|
|
495
|
-
}
|
|
496
|
-
catch (writeError) {
|
|
497
|
-
const err = writeError;
|
|
498
|
-
return {
|
|
499
|
-
success: false,
|
|
500
|
-
errorType: "system_error",
|
|
501
|
-
error: "Failed to write contract file",
|
|
502
|
-
message: "❌ System error: Cannot write temp file",
|
|
503
|
-
systemError: {
|
|
504
|
-
code: err.code,
|
|
505
|
-
details: err.message,
|
|
506
|
-
isUserFault: false,
|
|
507
|
-
},
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
// Run compilation
|
|
511
|
-
// When originalFilePath is available (file path provided), compile the original file
|
|
512
|
-
// from its source directory to resolve local includes correctly.
|
|
513
|
-
// Otherwise, compile the temp file from the temp directory.
|
|
514
|
-
const fileToCompile = originalFilePath || contractPath;
|
|
515
|
-
const compileCwd = originalFilePath ? sourceDir : tempDir;
|
|
516
|
-
try {
|
|
517
|
-
const execOptions = {
|
|
518
|
-
timeout: 60000, // 60 second timeout
|
|
519
|
-
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
520
|
-
cwd: compileCwd, // Use appropriate directory for include resolution
|
|
521
|
-
};
|
|
522
|
-
// Use execFile with array arguments to avoid shell injection vulnerabilities
|
|
523
|
-
// This is safer than string interpolation as paths are passed directly
|
|
524
|
-
const { stdout, stderr } = await execFileAsync("compact", ["compile", fileToCompile, outputDir], execOptions);
|
|
525
|
-
// Compilation succeeded!
|
|
526
|
-
const allWarnings = stderr ? parseWarnings(stderr) : [];
|
|
527
|
-
// Add local include warning if applicable
|
|
528
|
-
if (localIncludeWarning) {
|
|
529
|
-
allWarnings.push(`Note: Contract has local includes (${localIncludes.join(", ")}) - ensure these files exist relative to your contract`);
|
|
530
|
-
}
|
|
531
|
-
return {
|
|
532
|
-
success: true,
|
|
533
|
-
errorType: null,
|
|
534
|
-
compilerInstalled: true,
|
|
535
|
-
compilerVersion,
|
|
536
|
-
compilerPath: compactPath,
|
|
537
|
-
message: "✅ Contract compiled successfully!",
|
|
538
|
-
output: stdout || "Compilation completed without errors",
|
|
539
|
-
warnings: allWarnings,
|
|
540
|
-
localIncludes: localIncludes.length > 0 ? localIncludes : undefined,
|
|
541
|
-
contractInfo: {
|
|
542
|
-
filename,
|
|
543
|
-
codeLength: code.length,
|
|
544
|
-
lineCount: code.split("\n").length,
|
|
545
|
-
},
|
|
546
|
-
nextSteps: [
|
|
547
|
-
"The contract syntax is valid and compiles",
|
|
548
|
-
"Generated files would be in the output directory",
|
|
549
|
-
"You can proceed with deployment or further development",
|
|
550
|
-
],
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
catch (compileError) {
|
|
554
|
-
// Compilation failed - parse and categorize the error
|
|
555
|
-
const error = compileError;
|
|
556
|
-
// Check for timeout
|
|
557
|
-
if (error.killed || error.signal === "SIGTERM") {
|
|
558
|
-
return {
|
|
559
|
-
success: false,
|
|
560
|
-
errorType: "timeout_error",
|
|
561
|
-
compilerInstalled: true,
|
|
562
|
-
compilerVersion,
|
|
563
|
-
error: "Compilation timed out",
|
|
564
|
-
message: "❌ Compilation timed out after 60 seconds",
|
|
565
|
-
userAction: {
|
|
566
|
-
problem: "The contract took too long to compile",
|
|
567
|
-
solution: "Simplify the contract or check for infinite loops in circuit logic",
|
|
568
|
-
possibleCauses: [
|
|
569
|
-
"Very complex contract with many circuits",
|
|
570
|
-
"Recursive or deeply nested structures",
|
|
571
|
-
"Large number of constraints",
|
|
572
|
-
],
|
|
573
|
-
isUserFault: true,
|
|
574
|
-
},
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
const errorOutput = error.stderr || error.stdout || error.message || "";
|
|
578
|
-
const diagnostics = parseCompilerErrors(errorOutput, code);
|
|
579
|
-
// Categorize the error for better user feedback
|
|
580
|
-
const errorCategory = categorizeCompilerError(errorOutput);
|
|
581
|
-
return {
|
|
582
|
-
success: false,
|
|
583
|
-
errorType: "compilation_error",
|
|
584
|
-
errorCategory,
|
|
585
|
-
compilerInstalled: true,
|
|
586
|
-
compilerVersion,
|
|
587
|
-
compilerPath: compactPath,
|
|
588
|
-
message: `❌ ${errorCategory.title}`,
|
|
589
|
-
errors: diagnostics.errors,
|
|
590
|
-
errorCount: diagnostics.errors.length,
|
|
591
|
-
rawOutput: errorOutput.slice(0, 2000),
|
|
592
|
-
contractInfo: {
|
|
593
|
-
filename,
|
|
594
|
-
codeLength: code.length,
|
|
595
|
-
lineCount: code.split("\n").length,
|
|
596
|
-
},
|
|
597
|
-
userAction: {
|
|
598
|
-
problem: errorCategory.explanation,
|
|
599
|
-
solution: errorCategory.solution,
|
|
600
|
-
isUserFault: true,
|
|
601
|
-
},
|
|
602
|
-
suggestions: diagnostics.suggestions,
|
|
603
|
-
commonFixes: getCommonFixes(diagnostics.errors),
|
|
604
|
-
};
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
finally {
|
|
608
|
-
// Cleanup temp files (cross-platform)
|
|
609
|
-
try {
|
|
610
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
611
|
-
}
|
|
612
|
-
catch {
|
|
613
|
-
// Ignore cleanup errors
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
// ============================================================================
|
|
618
|
-
// ERROR PARSING HELPERS
|
|
619
|
-
// ============================================================================
|
|
620
|
-
/**
|
|
621
|
-
* Categorize compiler errors for better user feedback
|
|
622
|
-
*/
|
|
623
|
-
function categorizeCompilerError(output) {
|
|
624
|
-
const lowerOutput = output.toLowerCase();
|
|
625
|
-
if (lowerOutput.includes("parse error") ||
|
|
626
|
-
lowerOutput.includes("looking for")) {
|
|
627
|
-
return {
|
|
628
|
-
category: "syntax_error",
|
|
629
|
-
title: "Syntax Error",
|
|
630
|
-
explanation: "The contract has invalid syntax that the parser cannot understand",
|
|
631
|
-
solution: "Check for missing semicolons, brackets, or typos near the indicated line",
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
if (lowerOutput.includes("type") &&
|
|
635
|
-
(lowerOutput.includes("mismatch") || lowerOutput.includes("expected"))) {
|
|
636
|
-
return {
|
|
637
|
-
category: "type_error",
|
|
638
|
-
title: "Type Error",
|
|
639
|
-
explanation: "There is a type mismatch in your contract",
|
|
640
|
-
solution: "Ensure variable types match expected types in operations",
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
if (lowerOutput.includes("undefined") ||
|
|
644
|
-
lowerOutput.includes("not found") ||
|
|
645
|
-
lowerOutput.includes("unknown")) {
|
|
646
|
-
return {
|
|
647
|
-
category: "reference_error",
|
|
648
|
-
title: "Reference Error",
|
|
649
|
-
explanation: "The contract references something that doesn't exist",
|
|
650
|
-
solution: "Check that all variables, types, and functions are properly defined or imported",
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
if (lowerOutput.includes("import") ||
|
|
654
|
-
lowerOutput.includes("include") ||
|
|
655
|
-
lowerOutput.includes("module")) {
|
|
656
|
-
return {
|
|
657
|
-
category: "import_error",
|
|
658
|
-
title: "Import Error",
|
|
659
|
-
explanation: "There is a problem with an import or include statement",
|
|
660
|
-
solution: "Verify import paths and ensure required libraries are available",
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
if (lowerOutput.includes("circuit") ||
|
|
664
|
-
lowerOutput.includes("witness") ||
|
|
665
|
-
lowerOutput.includes("ledger")) {
|
|
666
|
-
return {
|
|
667
|
-
category: "structure_error",
|
|
668
|
-
title: "Contract Structure Error",
|
|
669
|
-
explanation: "There is an issue with the contract structure (circuits, witnesses, or ledger)",
|
|
670
|
-
solution: "Review the contract structure against Compact documentation",
|
|
671
|
-
};
|
|
672
|
-
}
|
|
673
|
-
return {
|
|
674
|
-
category: "unknown_error",
|
|
675
|
-
title: "Compilation Failed",
|
|
676
|
-
explanation: "The compiler encountered an error",
|
|
677
|
-
solution: "Review the error message and check Compact documentation",
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
|
-
/**
|
|
681
|
-
* Parse compiler error output into structured diagnostics
|
|
682
|
-
*/
|
|
683
|
-
function parseCompilerErrors(output, sourceCode) {
|
|
684
|
-
const errors = [];
|
|
685
|
-
const suggestions = [];
|
|
686
|
-
const lines = sourceCode.split("\n");
|
|
687
|
-
// Common patterns in Compact compiler output
|
|
688
|
-
// Pattern: "error: <message>" or "Error: <message>"
|
|
689
|
-
const errorLinePattern = /(?:error|Error):\s*(.+)/gi;
|
|
690
|
-
// Pattern: "line <n>:" or "at line <n>" or "<filename>:<line>:<col>"
|
|
691
|
-
const lineNumberPattern = /(?:line\s*(\d+)|at\s+line\s+(\d+)|:(\d+):(\d+))/i;
|
|
692
|
-
// Pattern: "expected <x>, found <y>"
|
|
693
|
-
const expectedPattern = /expected\s+['"`]?([^'"`]+)['"`]?,?\s*(?:found|got)\s+['"`]?([^'"`]+)['"`]?/i;
|
|
694
|
-
// Split output into logical segments
|
|
695
|
-
const segments = output.split(/(?=error:|Error:)/i);
|
|
696
|
-
for (const segment of segments) {
|
|
697
|
-
if (!segment.trim())
|
|
698
|
-
continue;
|
|
699
|
-
const errorMatch = segment.match(errorLinePattern);
|
|
700
|
-
if (errorMatch) {
|
|
701
|
-
const message = errorMatch[0].replace(/^(?:error|Error):\s*/i, "").trim();
|
|
702
|
-
// Try to extract line number
|
|
703
|
-
const lineMatch = segment.match(lineNumberPattern);
|
|
704
|
-
const line = lineMatch
|
|
705
|
-
? parseInt(lineMatch[1] || lineMatch[2] || lineMatch[3], 10)
|
|
706
|
-
: undefined;
|
|
707
|
-
const column = lineMatch && lineMatch[4] ? parseInt(lineMatch[4], 10) : undefined;
|
|
708
|
-
// Get source context if we have a line number
|
|
709
|
-
let context;
|
|
710
|
-
if (line && line > 0 && line <= lines.length) {
|
|
711
|
-
const start = Math.max(0, line - 2);
|
|
712
|
-
const end = Math.min(lines.length, line + 1);
|
|
713
|
-
context = lines
|
|
714
|
-
.slice(start, end)
|
|
715
|
-
.map((l, i) => `${start + i + 1}: ${l}`)
|
|
716
|
-
.join("\n");
|
|
717
|
-
}
|
|
718
|
-
errors.push({
|
|
719
|
-
line,
|
|
720
|
-
column,
|
|
721
|
-
message,
|
|
722
|
-
severity: "error",
|
|
723
|
-
context,
|
|
724
|
-
});
|
|
725
|
-
// Generate suggestions based on error type
|
|
726
|
-
const expectedMatch = message.match(expectedPattern);
|
|
727
|
-
if (expectedMatch) {
|
|
728
|
-
suggestions.push(`Expected "${expectedMatch[1]}" but found "${expectedMatch[2]}". Check your syntax.`);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
// If no structured errors found, add the raw output as an error
|
|
733
|
-
if (errors.length === 0 && output.trim()) {
|
|
734
|
-
errors.push({
|
|
735
|
-
message: output.trim().slice(0, 500),
|
|
736
|
-
severity: "error",
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
// Add general suggestions based on common issues
|
|
740
|
-
if (output.includes("Cell")) {
|
|
741
|
-
suggestions.push("Remember to use .value to access Cell<T> contents (e.g., state.value)");
|
|
742
|
-
}
|
|
743
|
-
if (output.includes("Opaque")) {
|
|
744
|
-
suggestions.push('Opaque<"string"> is a type, not a type alias. Use it directly in signatures.');
|
|
745
|
-
}
|
|
746
|
-
if (output.includes("disclose")) {
|
|
747
|
-
suggestions.push("In conditionals, use: const x = disclose(expr); if (x) { ... } instead of if (disclose(expr))");
|
|
748
|
-
}
|
|
749
|
-
if (output.includes("Counter")) {
|
|
750
|
-
suggestions.push("Counter type requires initialization: counter = Counter.increment(counter, 1)");
|
|
751
|
-
}
|
|
752
|
-
return { errors, suggestions };
|
|
753
|
-
}
|
|
754
|
-
/**
|
|
755
|
-
* Parse warnings from compiler output
|
|
756
|
-
*/
|
|
757
|
-
function parseWarnings(output) {
|
|
758
|
-
const warnings = [];
|
|
759
|
-
const warningPattern = /(?:warning|Warning):\s*(.+)/gi;
|
|
760
|
-
let match;
|
|
761
|
-
while ((match = warningPattern.exec(output)) !== null) {
|
|
762
|
-
warnings.push(match[1].trim());
|
|
763
|
-
}
|
|
764
|
-
return warnings;
|
|
765
|
-
}
|
|
766
|
-
/**
|
|
767
|
-
* Get common fixes based on error patterns
|
|
768
|
-
*/
|
|
769
|
-
function getCommonFixes(errors) {
|
|
770
|
-
const fixes = [];
|
|
771
|
-
const messages = errors.map((e) => e.message.toLowerCase()).join(" ");
|
|
772
|
-
if (messages.includes("cell") || messages.includes("value")) {
|
|
773
|
-
fixes.push({
|
|
774
|
-
pattern: "Cell<T> access error",
|
|
775
|
-
fix: "Use `state.value` instead of just `state` when accessing Cell contents",
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
if (messages.includes("opaque") || messages.includes("string type")) {
|
|
779
|
-
fixes.push({
|
|
780
|
-
pattern: "Opaque string type error",
|
|
781
|
-
fix: 'Use `Opaque<"your_type_name">` directly - it cannot be aliased with type keyword',
|
|
782
|
-
});
|
|
783
|
-
}
|
|
784
|
-
if (messages.includes("boolean") || messages.includes("witness")) {
|
|
785
|
-
fixes.push({
|
|
786
|
-
pattern: "Boolean witness error",
|
|
787
|
-
fix: "Witnesses return `Uint<1>` not `Boolean` - use `x != 0` to convert to Boolean",
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
if (messages.includes("disclose") || messages.includes("conditional")) {
|
|
791
|
-
fixes.push({
|
|
792
|
-
pattern: "Disclosure in conditional error",
|
|
793
|
-
fix: "Store disclose() result in const before using in if: `const revealed = disclose(x); if (revealed) { ... }`",
|
|
794
|
-
});
|
|
795
|
-
}
|
|
796
|
-
if (messages.includes("counter") || messages.includes("increment")) {
|
|
797
|
-
fixes.push({
|
|
798
|
-
pattern: "Counter initialization error",
|
|
799
|
-
fix: "Initialize counters with: `counter = Counter.increment(counter, 1)`",
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
|
-
if (messages.includes("map") ||
|
|
803
|
-
messages.includes("key") ||
|
|
804
|
-
messages.includes("insert")) {
|
|
805
|
-
fixes.push({
|
|
806
|
-
pattern: "Map operation error",
|
|
807
|
-
fix: "Maps require aligned access: insert at key before reading, or use default values",
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
return fixes;
|
|
811
|
-
}
|
|
812
74
|
// ============================================================================
|
|
813
75
|
// CONTRACT STRUCTURE EXTRACTION
|
|
814
76
|
// ============================================================================
|