recon-generate 0.0.10 → 0.0.11

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.
Files changed (45) hide show
  1. package/dist/generator.d.ts +2 -0
  2. package/dist/generator.js +71 -0
  3. package/dist/index.js +55 -1
  4. package/dist/pathsGenerator.d.ts +5 -1
  5. package/dist/pathsGenerator.js +62 -11
  6. package/dist/templates/setup.js +4 -3
  7. package/dist/templates/wake/before_after.d.ts +1 -0
  8. package/dist/templates/wake/before_after.js +33 -0
  9. package/dist/templates/wake/conftest.d.ts +1 -0
  10. package/dist/templates/wake/conftest.js +18 -0
  11. package/dist/templates/wake/crytic_tester.d.ts +1 -0
  12. package/dist/templates/wake/crytic_tester.js +20 -0
  13. package/dist/templates/wake/flows.d.ts +1 -0
  14. package/dist/templates/wake/flows.js +18 -0
  15. package/dist/templates/wake/fuzz-test.d.ts +1 -0
  16. package/dist/templates/wake/fuzz-test.js +62 -0
  17. package/dist/templates/wake/handlebars_helpers.d.ts +2 -0
  18. package/dist/templates/wake/handlebars_helpers.js +84 -0
  19. package/dist/templates/wake/helpers/actor_manager.d.ts +1 -0
  20. package/dist/templates/wake/helpers/actor_manager.js +44 -0
  21. package/dist/templates/wake/helpers/asset_manager.d.ts +1 -0
  22. package/dist/templates/wake/helpers/asset_manager.js +44 -0
  23. package/dist/templates/wake/helpers/utils.d.ts +1 -0
  24. package/dist/templates/wake/helpers/utils.js +11 -0
  25. package/dist/templates/wake/helpers.d.ts +2 -0
  26. package/dist/templates/wake/helpers.js +81 -0
  27. package/dist/templates/wake/index.d.ts +10 -0
  28. package/dist/templates/wake/index.js +26 -0
  29. package/dist/templates/wake/invariants.d.ts +1 -0
  30. package/dist/templates/wake/invariants.js +21 -0
  31. package/dist/templates/wake/properties.d.ts +1 -0
  32. package/dist/templates/wake/properties.js +19 -0
  33. package/dist/templates/wake/setup.d.ts +1 -0
  34. package/dist/templates/wake/setup.js +48 -0
  35. package/dist/templates/wake/target_functions.d.ts +1 -0
  36. package/dist/templates/wake/target_functions.js +24 -0
  37. package/dist/templates/wake/targets/contract_targets.d.ts +1 -0
  38. package/dist/templates/wake/targets/contract_targets.js +23 -0
  39. package/dist/templates/wake/targets/managers_targets.d.ts +1 -0
  40. package/dist/templates/wake/targets/managers_targets.js +26 -0
  41. package/dist/templates/wake/test_fuzz.d.ts +1 -0
  42. package/dist/templates/wake/test_fuzz.js +27 -0
  43. package/dist/wakeGenerator.d.ts +32 -0
  44. package/dist/wakeGenerator.js +447 -0
  45. package/package.json +1 -1
@@ -24,6 +24,7 @@ export declare class ReconGenerator {
24
24
  private generatedMocks;
25
25
  private allowedMockNames;
26
26
  private contractKindCache;
27
+ private abstractContractCache;
27
28
  constructor(foundryRoot: string, options: GeneratorOptions);
28
29
  private logDebug;
29
30
  private outDir;
@@ -37,6 +38,7 @@ export declare class ReconGenerator {
37
38
  private updateRemappings;
38
39
  private updateGitignore;
39
40
  private getContractKind;
41
+ private isAbstractContract;
40
42
  private findSourceContracts;
41
43
  private matchesSignatureOrName;
42
44
  private paramType;
package/dist/generator.js CHANGED
@@ -54,6 +54,7 @@ class ReconGenerator {
54
54
  this.generatedMocks = new Set();
55
55
  this.allowedMockNames = new Set();
56
56
  this.contractKindCache = new Map();
57
+ this.abstractContractCache = new Map();
57
58
  }
58
59
  logDebug(message, obj) {
59
60
  if (!this.options.debug)
@@ -196,6 +197,53 @@ class ReconGenerator {
196
197
  this.contractKindCache.set(absPath, fileCache);
197
198
  return kind;
198
199
  }
200
+ async isAbstractContract(sourcePath, contractName) {
201
+ const absPath = path.isAbsolute(sourcePath) ? sourcePath : path.join(this.foundryRoot, sourcePath);
202
+ let fileCache = this.abstractContractCache.get(absPath);
203
+ if (fileCache && fileCache.has(contractName)) {
204
+ return fileCache.get(contractName);
205
+ }
206
+ let content;
207
+ try {
208
+ content = await fs.readFile(absPath, 'utf8');
209
+ }
210
+ catch (e) {
211
+ this.logDebug('Failed to read source when determining abstract contract', { sourcePath, error: String(e) });
212
+ return false;
213
+ }
214
+ let ast;
215
+ try {
216
+ ast = parser_1.default.parse(content, { tolerant: true });
217
+ }
218
+ catch (e) {
219
+ this.logDebug('Failed to parse source when determining abstract contract', { sourcePath, error: String(e) });
220
+ ast = null;
221
+ }
222
+ fileCache = fileCache !== null && fileCache !== void 0 ? fileCache : new Map();
223
+ let isAbstract = false;
224
+ const children = ast === null || ast === void 0 ? void 0 : ast.children;
225
+ if (Array.isArray(children)) {
226
+ for (const node of children) {
227
+ if ((node === null || node === void 0 ? void 0 : node.type) === 'ContractDefinition' && node.name === contractName) {
228
+ if (node.abstract === true) {
229
+ isAbstract = true;
230
+ }
231
+ break;
232
+ }
233
+ }
234
+ }
235
+ if (!isAbstract) {
236
+ // Fallback: text search for `abstract contract <Name>` to cover parser misses
237
+ const stripped = content.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
238
+ const pattern = new RegExp(`\\babstract\\s+contract\\s+${contractName}\\b`);
239
+ if (pattern.test(stripped)) {
240
+ isAbstract = true;
241
+ }
242
+ }
243
+ fileCache.set(contractName, isAbstract);
244
+ this.abstractContractCache.set(absPath, fileCache);
245
+ return isAbstract;
246
+ }
199
247
  async findSourceContracts() {
200
248
  var _a, _b;
201
249
  const contracts = [];
@@ -226,11 +274,34 @@ class ReconGenerator {
226
274
  const lowerName = String(contractName).toLowerCase();
227
275
  const isGeneratedMock = this.generatedMocks.has(String(contractName));
228
276
  const contractKind = await this.getContractKind(sourcePath, String(contractName));
277
+ const isAbstract = await this.isAbstractContract(sourcePath, String(contractName));
278
+ const absSourcePath = path.isAbsolute(sourcePath)
279
+ ? sourcePath
280
+ : path.join(this.foundryRoot, sourcePath);
281
+ const relSourcePath = path.relative(this.foundryRoot, absSourcePath).replace(/\\/g, '/').toLowerCase();
282
+ const skipPrefixes = [
283
+ 'test/',
284
+ 'tests/',
285
+ 'script/',
286
+ 'scripts/',
287
+ 'contracts/test/',
288
+ 'contracts/tests/',
289
+ 'lib/',
290
+ ];
229
291
  // Check if contract is explicitly included or mocked - if so, don't filter by kind
230
292
  const isExplicitlyIncluded = this.options.include &&
231
293
  (this.options.include.contractOnly.has(String(contractName)) ||
232
294
  this.options.include.functions.has(String(contractName)));
233
295
  const isExplicitlyMocked = (_b = this.options.mocks) === null || _b === void 0 ? void 0 : _b.has(String(contractName));
296
+ const inSkipDir = skipPrefixes.some(prefix => relSourcePath.startsWith(prefix));
297
+ if (!isGeneratedMock && inSkipDir && !isExplicitlyIncluded && !isExplicitlyMocked) {
298
+ this.logDebug('Skipping artifact from non-source directory', { contractName, sourcePath });
299
+ continue;
300
+ }
301
+ if (isAbstract && !isExplicitlyIncluded && !isExplicitlyMocked) {
302
+ this.logDebug('Skipping abstract contract', { contractName, sourcePath });
303
+ continue;
304
+ }
234
305
  if ((contractKind === 'interface' || contractKind === 'library') && !isExplicitlyIncluded && !isExplicitlyMocked) {
235
306
  this.logDebug('Skipping non-contract artifact', { contractName, sourcePath, contractKind });
236
307
  continue;
package/dist/index.js CHANGED
@@ -38,6 +38,7 @@ const commander_1 = require("commander");
38
38
  const path = __importStar(require("path"));
39
39
  const case_1 = require("case");
40
40
  const generator_1 = require("./generator");
41
+ const wakeGenerator_1 = require("./wakeGenerator");
41
42
  const coverage_1 = require("./coverage");
42
43
  const pathsGenerator_1 = require("./pathsGenerator");
43
44
  const utils_1 = require("./utils");
@@ -146,6 +147,8 @@ async function main() {
146
147
  .option('--crytic-name <name>', 'Name of the Crytic tester contract to compile', 'CryticTester')
147
148
  .option('--name <suite>', 'Suite name; affects paths filename (recon-<name>-paths.json)')
148
149
  .option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)')
150
+ .option('--dry-run', 'Do not enumerate paths; only report which functions/external calls would be processed')
151
+ .option('--max-paths <n>', 'Cap path enumeration per function to avoid OOM (default: 5000)')
149
152
  .action(async (opts, cmd) => {
150
153
  var _a;
151
154
  const workspaceRoot = process.cwd();
@@ -154,7 +157,11 @@ async function main() {
154
157
  const parentOpts = ((_a = cmd.parent) === null || _a === void 0 ? void 0 : _a.opts()) || {};
155
158
  const suiteRaw = opts.name || parentOpts.name ? String(opts.name || parentOpts.name).trim() : '';
156
159
  const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
157
- await (0, pathsGenerator_1.runPaths)(foundryRoot, opts.cryticName || 'CryticTester', suiteSnake);
160
+ const maxPaths = opts.maxPaths ? parseInt(String(opts.maxPaths), 10) : 5000;
161
+ await (0, pathsGenerator_1.runPaths)(foundryRoot, opts.cryticName || 'CryticTester', suiteSnake, {
162
+ dryRun: !!opts.dryRun,
163
+ maxPaths: Number.isFinite(maxPaths) && maxPaths > 0 ? maxPaths : undefined,
164
+ });
158
165
  });
159
166
  program
160
167
  .command('link')
@@ -185,6 +192,53 @@ async function main() {
185
192
  : path.join(foundryRoot, medusaConfigOpt);
186
193
  await (0, link_1.runLink)(foundryRoot, echidnaConfigPath, medusaConfigPath, !!opts.verbose);
187
194
  });
195
+ program
196
+ .command('wake')
197
+ .description('Generate Python fuzzing suite using Wake framework')
198
+ .option('--include <spec>', 'Include filter: e.g. "A,B:{foo(uint256)|bar(address)},C"')
199
+ .option('--exclude <spec>', 'Exclude filter: e.g. "A:{foo(uint256)},D" or just "B"')
200
+ .option('--admin <spec>', 'Mark functions as admin: e.g. "A:{foo(uint256),bar()},B:{baz(address)}"')
201
+ .option('--name <suite>', 'Suite name; affects folder (tests/recon-name)')
202
+ .option('-o, --output <path>', 'Custom output folder for tests')
203
+ .option('--debug', 'Print debug info about filters and selection')
204
+ .option('--list', 'List available contracts/functions (after filters) and exit')
205
+ .option('--force', 'Replace existing generated suite output')
206
+ .option('--force-build', 'Delete .recon/out to force a fresh forge build before generating')
207
+ .option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)')
208
+ .action(async (opts, cmd) => {
209
+ var _a;
210
+ const workspaceRoot = process.cwd();
211
+ const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
212
+ const foundryRoot = path.dirname(foundryConfig);
213
+ const parentOpts = ((_a = cmd.parent) === null || _a === void 0 ? void 0 : _a.opts()) || {};
214
+ const includeFilter = parseFilter(opts.include);
215
+ const excludeFilter = parseFilter(opts.exclude);
216
+ const adminFilter = parseFilter(opts.admin);
217
+ const suiteRaw = opts.name || parentOpts.name ? String(opts.name || parentOpts.name).trim() : '';
218
+ const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
219
+ const suitePascal = suiteRaw ? (0, case_1.pascal)(suiteRaw) : '';
220
+ const suiteFolderName = suiteSnake ? `recon_${suiteSnake}` : 'recon';
221
+ const baseOut = opts.output ? String(opts.output) : 'tests';
222
+ const baseOutPath = path.isAbsolute(baseOut) ? baseOut : path.resolve(foundryRoot, baseOut);
223
+ const suiteDir = path.resolve(baseOutPath, suiteFolderName);
224
+ const generator = new wakeGenerator_1.WakeGenerator(foundryRoot, {
225
+ suiteDir,
226
+ foundryConfigPath: foundryConfig,
227
+ include: includeFilter,
228
+ exclude: excludeFilter,
229
+ admin: adminFilter ? adminFilter.functions : undefined,
230
+ debug: !!opts.debug || !!parentOpts.debug,
231
+ force: Boolean(opts.force || parentOpts.force),
232
+ forceBuild: !!opts.forceBuild || !!parentOpts.forceBuild,
233
+ suiteNameSnake: suiteSnake,
234
+ suiteNamePascal: suitePascal,
235
+ });
236
+ if (opts.list) {
237
+ await generator.listAvailable();
238
+ return;
239
+ }
240
+ await generator.run();
241
+ });
188
242
  program
189
243
  .action(async (opts) => {
190
244
  const workspaceRoot = process.cwd();
@@ -4,6 +4,10 @@
4
4
  * Generates minimal paths needed for 100% branch coverage.
5
5
  * Output is optimized for LLM consumption.
6
6
  */
7
+ export interface RunPathsOptions {
8
+ dryRun?: boolean;
9
+ maxPaths?: number;
10
+ }
7
11
  /** Simple output: function name -> array of path condition strings */
8
12
  export type PathOutput = Record<string, string[]>;
9
- export declare const runPaths: (foundryRoot: string, cryticName: string, suiteNameSnake?: string) => Promise<void>;
13
+ export declare const runPaths: (foundryRoot: string, cryticName: string, suiteNameSnake?: string, options?: RunPathsOptions) => Promise<void>;
@@ -103,7 +103,7 @@ const loadLatestSourceUnits = async (foundryRoot) => {
103
103
  return reader.read(filteredAstData);
104
104
  };
105
105
  // ==================== Main Entry ====================
106
- const runPaths = async (foundryRoot, cryticName, suiteNameSnake) => {
106
+ const runPaths = async (foundryRoot, cryticName, suiteNameSnake, options = {}) => {
107
107
  const buildCmd = `forge build --contracts ${cryticName} --build-info --out .recon/out`.replace(/\s+/g, ' ').trim();
108
108
  await runCmd(buildCmd, foundryRoot);
109
109
  const sourceUnits = await loadLatestSourceUnits(foundryRoot);
@@ -123,6 +123,18 @@ const runPaths = async (foundryRoot, cryticName, suiteNameSnake) => {
123
123
  // Process each function
124
124
  const output = {};
125
125
  let totalPaths = 0;
126
+ if (options.dryRun) {
127
+ console.log(`[dry-run] Crytic contract: ${cryticName}`);
128
+ console.log(`[dry-run] Target functions (${targetFunctions.length}):`);
129
+ for (const func of targetFunctions) {
130
+ const externalCall = findExternalCall(func);
131
+ const extLabel = externalCall ? `${externalCall.contract}.${externalCall.function}` : 'none';
132
+ console.log(` - ${func.name} -> external call: ${extLabel}`);
133
+ }
134
+ console.log('[dry-run] Skipping path enumeration.');
135
+ return;
136
+ }
137
+ const maxPaths = options.maxPaths && options.maxPaths > 0 ? options.maxPaths : undefined;
126
138
  for (const func of targetFunctions) {
127
139
  // Find external call
128
140
  const externalCall = findExternalCall(func);
@@ -145,8 +157,8 @@ const runPaths = async (foundryRoot, cryticName, suiteNameSnake) => {
145
157
  const callTree = (0, call_tree_builder_1.buildCallTree)(extFunc, [], undefined, undefined, context);
146
158
  // Build param mapping
147
159
  const paramMapping = buildParamMapping(func, externalCall);
148
- // Enumerate paths
149
- const enumerator = new PathEnumerator(sourceUnits, paramMapping);
160
+ // Enumerate paths with optional cap
161
+ const enumerator = new PathEnumerator(sourceUnits, paramMapping, maxPaths);
150
162
  const paths = enumerator.enumerate(callTree);
151
163
  if (paths.length === 0)
152
164
  continue;
@@ -157,11 +169,14 @@ const runPaths = async (foundryRoot, cryticName, suiteNameSnake) => {
157
169
  continue;
158
170
  output[func.name] = simplified;
159
171
  totalPaths += simplified.length;
172
+ if (enumerator.wasTruncated()) {
173
+ console.warn(`[recon-generate] Warning: Path enumeration for ${func.name} reached the max limit${maxPaths ? ` (${maxPaths})` : ''} and was truncated.`);
174
+ }
160
175
  }
161
176
  const pathsName = suiteNameSnake ? `recon-${suiteNameSnake}-paths.json` : 'recon-paths.json';
162
177
  const pathsFilePath = path.join(foundryRoot, pathsName);
163
178
  await fs.writeFile(pathsFilePath, JSON.stringify(output, null, 2));
164
- console.log(`[recon-generate] Wrote paths file to ${pathsFilePath}`);
179
+ console.log(`[recon-generate] Wrote paths file to ${pathsFilePath} (total paths: ${totalPaths})`);
165
180
  };
166
181
  exports.runPaths = runPaths;
167
182
  // ==================== Source Helpers ====================
@@ -224,20 +239,26 @@ function buildParamMapping(func, extCall) {
224
239
  }
225
240
  // ==================== Path Enumeration ====================
226
241
  class PathEnumerator {
227
- constructor(sourceUnits, paramMap) {
242
+ constructor(sourceUnits, paramMap, maxPaths) {
228
243
  this.pathIdCounter = 0;
229
244
  this.branchPoints = 0;
245
+ this.truncated = false;
230
246
  this.sourceUnits = sourceUnits;
231
247
  this.paramMap = new Map(paramMap);
248
+ this.maxPaths = maxPaths;
232
249
  }
233
250
  enumerate(callTree) {
234
251
  this.pathIdCounter = 0;
235
252
  this.branchPoints = 0;
253
+ this.truncated = false;
236
254
  const paths = this.processCallTree(callTree);
237
255
  return paths
238
256
  .filter(p => p.result === 'success')
239
257
  .map(p => ({ id: p.id, conditions: p.conditions, requires: p.requires, result: p.result }));
240
258
  }
259
+ wasTruncated() {
260
+ return this.truncated;
261
+ }
241
262
  processCallTree(node) {
242
263
  const def = node.definition;
243
264
  // Process children first (leaf to root)
@@ -252,8 +273,13 @@ class PathEnumerator {
252
273
  this.paramMap = savedMap;
253
274
  }
254
275
  // Process function body
276
+ const firstId = ++this.pathIdCounter;
277
+ if (this.maxPaths && firstId > this.maxPaths) {
278
+ this.truncated = true;
279
+ return [];
280
+ }
255
281
  let activePaths = [{
256
- id: ++this.pathIdCounter,
282
+ id: firstId,
257
283
  conditions: [],
258
284
  requires: [],
259
285
  terminated: false
@@ -336,8 +362,13 @@ class PathEnumerator {
336
362
  for (const clause of stmt.vClauses) {
337
363
  if (clause.errorName === '') {
338
364
  // Success clause (try block) - external call succeeds
365
+ const nextId = ++this.pathIdCounter;
366
+ if (this.maxPaths && nextId > this.maxPaths) {
367
+ this.truncated = true;
368
+ continue;
369
+ }
339
370
  const successPath = {
340
- id: ++this.pathIdCounter,
371
+ id: nextId,
341
372
  conditions: [...path.conditions, {
342
373
  original: callExpr,
343
374
  resolved: callExpr,
@@ -351,8 +382,13 @@ class PathEnumerator {
351
382
  }
352
383
  else {
353
384
  // Catch clause - external call fails
385
+ const nextCatchId = ++this.pathIdCounter;
386
+ if (this.maxPaths && nextCatchId > this.maxPaths) {
387
+ this.truncated = true;
388
+ continue;
389
+ }
354
390
  const catchPath = {
355
- id: ++this.pathIdCounter,
391
+ id: nextCatchId,
356
392
  conditions: [...path.conditions, {
357
393
  original: callExpr,
358
394
  resolved: callExpr,
@@ -411,8 +447,13 @@ class PathEnumerator {
411
447
  continue;
412
448
  }
413
449
  // True branch
450
+ const nextIdTrue = ++this.pathIdCounter;
451
+ if (this.maxPaths && nextIdTrue > this.maxPaths) {
452
+ this.truncated = true;
453
+ break;
454
+ }
414
455
  const truePath = {
415
- id: ++this.pathIdCounter,
456
+ id: nextIdTrue,
416
457
  conditions: [...path.conditions, { original: condOrig, resolved: condResolved, mustBeTrue: true }],
417
458
  requires: [...path.requires],
418
459
  terminated: false
@@ -427,8 +468,13 @@ class PathEnumerator {
427
468
  results.push(truePath);
428
469
  }
429
470
  // False branch
471
+ const nextIdFalse = ++this.pathIdCounter;
472
+ if (this.maxPaths && nextIdFalse > this.maxPaths) {
473
+ this.truncated = true;
474
+ break;
475
+ }
430
476
  const falsePath = {
431
- id: ++this.pathIdCounter,
477
+ id: nextIdFalse,
432
478
  conditions: [...path.conditions, { original: condOrig, resolved: condResolved, mustBeTrue: false }],
433
479
  requires: [...path.requires],
434
480
  terminated: false
@@ -483,8 +529,13 @@ class PathEnumerator {
483
529
  for (const cp of cPaths) {
484
530
  if (cp.result === 'revert')
485
531
  continue;
532
+ const nextId = ++this.pathIdCounter;
533
+ if (this.maxPaths && nextId > this.maxPaths) {
534
+ this.truncated = true;
535
+ continue;
536
+ }
486
537
  merged.push({
487
- id: ++this.pathIdCounter,
538
+ id: nextId,
488
539
  conditions: [...p.conditions, ...cp.conditions],
489
540
  requires: [...p.requires, ...cp.requires],
490
541
  terminated: false
@@ -27,11 +27,12 @@ import "{{this.path}}";
27
27
  {{/each}}
28
28
 
29
29
  abstract contract Setup is BaseSetup, ActorManager, AssetManager, Utils {
30
- {{#each contracts}}
30
+ {{#each contracts}}
31
31
  {{this.name}} {{camel this.name}};
32
- {{#if this.isDynamic}}address[] {{camel this.name}}_s;
32
+ {{#if this.isDynamic}}
33
+ address[] {{camel this.name}}_s;
33
34
  {{/if}}
34
- {{/each}}
35
+ {{/each}}
35
36
 
36
37
  /// === Setup === ///
37
38
  /// This contains all calls to be performed in the tester constructor, both for Echidna and Foundry
@@ -0,0 +1 @@
1
+ export declare const beforeAfterTemplate: HandlebarsTemplateDelegate<any>;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.beforeAfterTemplate = void 0;
7
+ const handlebars_1 = __importDefault(require("handlebars"));
8
+ exports.beforeAfterTemplate = handlebars_1.default.compile(`
9
+ from wake.testing import *
10
+ from wake.testing.fuzzing import *
11
+ from .setup import Setup
12
+ from dataclasses import dataclass
13
+
14
+ @dataclass
15
+ class Vars:
16
+ # Example ghost variables for tracking state
17
+ # total_assets: int = 0
18
+ pass
19
+
20
+ class BeforeAfter(Setup):
21
+ _before: Vars
22
+ _after: Vars
23
+
24
+ def pre_flow(self, flow: Callable) -> None:
25
+ # Called before each flow
26
+ # self._before = Vars(total_assets=self.complexVault.totalAssets())
27
+ pass
28
+
29
+ def post_flow(self, flow: Callable) -> None:
30
+ # Called after each flow
31
+ # self._after = Vars(total_assets=self.complexVault.totalAssets())
32
+ pass
33
+ `, { noEscape: true });
@@ -0,0 +1 @@
1
+ export declare const conftestTemplate: HandlebarsTemplateDelegate<any>;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.conftestTemplate = void 0;
7
+ const handlebars_1 = __importDefault(require("handlebars"));
8
+ const handlebars_helpers_1 = require("./handlebars_helpers");
9
+ (0, handlebars_helpers_1.registerWakeHelpers)(handlebars_1.default);
10
+ exports.conftestTemplate = handlebars_1.default.compile(`import pytest
11
+ from wake.testing import chain
12
+
13
+ @pytest.fixture(scope="module", autouse=True)
14
+ def chain_connection():
15
+ """Connect to a local chain for all tests in this module."""
16
+ with chain.connect():
17
+ yield
18
+ `, { noEscape: true });
@@ -0,0 +1 @@
1
+ export declare const cryticTesterTemplate: HandlebarsTemplateDelegate<any>;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.cryticTesterTemplate = void 0;
7
+ const handlebars_1 = __importDefault(require("handlebars"));
8
+ exports.cryticTesterTemplate = handlebars_1.default.compile(`
9
+ from wake.testing import *
10
+ from wake.testing.fuzzing import FuzzTest, flow, invariant
11
+ from .target_functions import TargetFunctions
12
+
13
+ class TestCrytic(TargetFunctions):
14
+ def pre_sequence(self):
15
+ self.setup()
16
+
17
+ @chain.connect()
18
+ def test_recon_fuzz():
19
+ TestCrytic().run(sequences_count=30, flows_count=100)
20
+ `);
@@ -0,0 +1 @@
1
+ export declare const flowsTemplate: HandlebarsTemplateDelegate<any>;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.flowsTemplate = void 0;
7
+ const handlebars_1 = __importDefault(require("handlebars"));
8
+ const helpers_1 = require("./helpers");
9
+ (0, helpers_1.registerWakeHelpers)(handlebars_1.default);
10
+ exports.flowsTemplate = handlebars_1.default.compile(`
11
+ from wake.testing import *
12
+ from wake.testing.fuzzing import *
13
+
14
+ class Flows:
15
+ {{#each flows}}
16
+ {{flowDefinition this}}
17
+ {{/each}}
18
+ `, { noEscape: true });
@@ -0,0 +1 @@
1
+ export declare const fuzzTestTemplate: HandlebarsTemplateDelegate<any>;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.fuzzTestTemplate = void 0;
7
+ const handlebars_1 = __importDefault(require("handlebars"));
8
+ const helpers_1 = require("./helpers");
9
+ (0, helpers_1.registerWakeHelpers)(handlebars_1.default);
10
+ exports.fuzzTestTemplate = handlebars_1.default.compile(`"""
11
+ {{pascal suiteName}} Fuzz Test
12
+ """
13
+ from wake.testing import *
14
+ from wake.testing.fuzzing import *
15
+ import random
16
+
17
+ # Import pytypes
18
+ {{#each contracts}}
19
+ from {{module}} import {{name}}
20
+ {{/each}}
21
+
22
+ from .flows import Flows
23
+ from .invariants import Invariants
24
+
25
+ class {{pascal suiteName}}FuzzTest(FuzzTest, Flows, Invariants):
26
+ # Contract instances
27
+ {{#each contracts}}
28
+ {{camel name}}: {{name}}
29
+ {{/each}}
30
+
31
+ # Actors
32
+ actors: list[Account]
33
+ admin: Account
34
+
35
+ def pre_sequence(self) -> None:
36
+ # Setup actors
37
+ self.admin = default_chain.accounts[0]
38
+ self.actors = [Account.new() for _ in range(3)]
39
+
40
+ # Deploy contracts
41
+ {{#each contracts}}
42
+ # Deploy {{name}}
43
+ # Args: {{#each deploy_args}}{{name}}: {{type}}, {{/each}}
44
+ self.{{camel name}} = {{name}}.deploy(
45
+ {{#each deploy_args}}{{name}}={{pythonDefault type}}, {{/each}}
46
+ from_=self.admin
47
+ )
48
+ {{/each}}
49
+
50
+
51
+ def _get_actor(self) -> Account:
52
+ return self.actors[0] if self.actors else self.admin
53
+
54
+ @flow()
55
+ def flow_switch_actor(self, entropy: uint8) -> None:
56
+ if self.actors:
57
+ idx = entropy % len(self.actors)
58
+ self.actors = self.actors[idx:] + self.actors[:idx]
59
+
60
+ def test_{{snake suiteName}}_fuzz():
61
+ {{pascal suiteName}}FuzzTest().run(sequences_count=10, flows_count=100)
62
+ `, { noEscape: true });
@@ -0,0 +1,2 @@
1
+ import Handlebars from "handlebars";
2
+ export declare function registerWakeHelpers(handlebars: typeof Handlebars): void;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerWakeHelpers = registerWakeHelpers;
4
+ const case_1 = require("case");
5
+ const types_1 = require("../../types"); // Keeping types for Actor/Mode
6
+ function registerWakeHelpers(handlebars) {
7
+ handlebars.registerHelper('snake', function (str) {
8
+ return (0, case_1.snake)(str);
9
+ });
10
+ handlebars.registerHelper('camel', function (str) {
11
+ return (0, case_1.camel)(str);
12
+ });
13
+ handlebars.registerHelper('pascal', function (str) {
14
+ return (0, case_1.pascal)(str);
15
+ });
16
+ /**
17
+ * Python default value based on type string.
18
+ */
19
+ function pythonDefault(type) {
20
+ if (type.includes("Address"))
21
+ return "Address(0)";
22
+ if (type.includes("int"))
23
+ return "0";
24
+ if (type === "bool")
25
+ return "False";
26
+ if (type.includes("bytes"))
27
+ return 'b""';
28
+ if (type.includes("list") || type.includes("List"))
29
+ return "[]";
30
+ return "()";
31
+ }
32
+ handlebars.registerHelper('pythonDefault', pythonDefault);
33
+ /**
34
+ * Generate a @flow decorated method definition for Wake FuzzTest.
35
+ * Expects: { contractName, method: { name, args: [{name, type}] }, actor, mode }
36
+ */
37
+ handlebars.registerHelper('flowDefinition', function ({ contractName, method, actor, mode }) {
38
+ const contractVar = (0, case_1.camel)(contractName);
39
+ const funcName = method.name;
40
+ // Build parameter list
41
+ const params = method.args.map((arg) => {
42
+ let typeStr = arg.type;
43
+ // Fix imported types if needed? Usually they are unqualified if imported, or fully qualified.
44
+ // Pytypes usually gives "ComplexVault.Status" or "Address".
45
+ return `${arg.name}: ${typeStr}`;
46
+ });
47
+ // Add self as first parameter
48
+ const paramStr = params.length > 0 ? `self, ${params.join(', ')}` : 'self';
49
+ // Determine sender based on actor type
50
+ let senderLine;
51
+ if (actor === types_1.Actor.ADMIN) {
52
+ senderLine = ' sender = default_chain.accounts[0] # Admin';
53
+ }
54
+ else {
55
+ senderLine = ' sender = self._get_actor()';
56
+ }
57
+ // Build argument list for the contract call
58
+ const callArgs = method.args.map((arg) => arg.name);
59
+ const callArgsStr = callArgs.length > 0 ? `${callArgs.join(', ')}, ` : '';
60
+ // Handle different modes (using existing logic, though pytypes inspection doesn't perfectly enable this yet)
61
+ let methodBody = `${senderLine}
62
+ # self.${contractVar}.${funcName}(${callArgsStr}from_=sender)`;
63
+ // We wrap in try-except by default per "identical suite" robustness usually found in these tools
64
+ // But user said "user only need to fix the setup and add invariants" implies flows should work or fail?
65
+ // Let's assume standard basic call.
66
+ methodBody = `${senderLine}
67
+ try:
68
+ self.${contractVar}.${funcName}(${callArgsStr}from_=sender)
69
+ except Exception:
70
+ pass`;
71
+ return `
72
+ @flow()
73
+ def flow_${(0, case_1.snake)(contractName)}_${(0, case_1.snake)(funcName)}(${paramStr}) -> None:
74
+ """Call ${contractName}.${funcName}"""
75
+ ${methodBody}
76
+ `;
77
+ });
78
+ handlebars.registerHelper('hasItems', function (arr, options) {
79
+ if (arr && arr.length > 0) {
80
+ return options.fn(this);
81
+ }
82
+ return options.inverse(this);
83
+ });
84
+ }
@@ -0,0 +1 @@
1
+ export declare const actorManagerTemplate: HandlebarsTemplateDelegate<any>;