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
@@ -0,0 +1,447 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.WakeGenerator = void 0;
37
+ const child_process_1 = require("child_process");
38
+ const fs = __importStar(require("fs/promises"));
39
+ const path = __importStar(require("path"));
40
+ const types_1 = require("./types");
41
+ const utils_1 = require("./utils");
42
+ const wakeTemplates = __importStar(require("./templates/wake"));
43
+ const case_1 = require("case");
44
+ // Embed the Python introspection script
45
+ const INSPECT_SCRIPT = `
46
+ import inspect
47
+ import json
48
+ import sys
49
+ import os
50
+ from pathlib import Path
51
+ import importlib.util
52
+
53
+ # Ensure we can import from current directory
54
+ sys.path.insert(0, os.getcwd())
55
+
56
+ def format_annotation(annotation):
57
+ if annotation == inspect.Parameter.empty:
58
+ return "Any"
59
+
60
+ # Get string representation
61
+ s = str(annotation)
62
+
63
+ # Simplify Union and Optional for Wake generators
64
+ # We recursively flatten until we find a concrete type
65
+ # This prevents the TypeError: issubclass() arg 1 must be a class in Wake generators
66
+ import re
67
+
68
+ def simplify(type_str):
69
+ while "Union" in type_str or "Optional" in type_str:
70
+ # Find the innermost Union[...] or Optional[...]
71
+ m = re.search(r"(Union|Optional)\[([^\[\]]+)\]", type_str)
72
+ if not m: break
73
+ content = m.group(2)
74
+ parts = [p.strip() for p in content.split(",")]
75
+ # Pick first non-None
76
+ best = parts[0]
77
+ for p in parts:
78
+ if p not in ("None", "NoneType"):
79
+ best = p
80
+ break
81
+
82
+ # Special case: if Address or Account is present, use Address for Wake compatibility
83
+ if any("Address" in p or "Account" in p for p in parts):
84
+ best = "Address"
85
+
86
+ type_str = type_str[:m.start()] + best + type_str[m.end():]
87
+ return type_str
88
+
89
+ s = simplify(s)
90
+ s = s.replace("typing.", "").replace("wake.testing.core.", "")
91
+ s = s.replace("<class '", "").replace("'>", "")
92
+ return s
93
+
94
+ def inspect_pytypes():
95
+ contracts = []
96
+ mock_erc20 = None
97
+ target_dir = Path("pytypes")
98
+
99
+ if not target_dir.exists():
100
+ return { "contracts": [], "mock_erc20_module": None }
101
+
102
+ for root, dirs, files in os.walk(target_dir):
103
+ for file in files:
104
+ if file.endswith(".py") and file != "__init__.py":
105
+ rel_path = Path(root) / file
106
+ module_path = str(rel_path).replace(os.sep, ".").replace(".py", "")
107
+
108
+ try:
109
+ module = importlib.import_module(module_path)
110
+
111
+ for name, obj in inspect.getmembers(module):
112
+ if inspect.isclass(obj) and hasattr(obj, "deploy") and obj.__module__ == module_path:
113
+ # Check for MockERC20 globally
114
+ if name == "MockERC20":
115
+ mock_erc20 = module_path
116
+
117
+ # STRICT FILTER: Only include contracts from src folder
118
+ if not module_path.startswith("pytypes.src"):
119
+ continue
120
+
121
+ # Determine mutable functions from _abi
122
+ mutable_functions = set()
123
+ if hasattr(obj, "_abi"):
124
+ try:
125
+ # _abi is dict: selector -> item
126
+ for item in obj._abi.values():
127
+ if item.get("type") == "function" and item.get("stateMutability") not in ["view", "pure"]:
128
+ mutable_functions.add(item.get("name"))
129
+ except Exception:
130
+ pass
131
+
132
+ contract_info = {
133
+ "name": name,
134
+ "module": module_path,
135
+ "methods": [],
136
+ "deploy_args": []
137
+ }
138
+
139
+ try:
140
+ deploy_method = getattr(obj, "deploy")
141
+ sig = inspect.signature(deploy_method)
142
+ for param_name, param in sig.parameters.items():
143
+ if param_name in ["cls", "return_tx", "request_type", "chain", "from_", "value", "gas_limit", "gas_price", "max_fee_per_gas", "max_priority_fee_per_gas", "access_list", "type", "block", "confirmations", "to"]:
144
+ continue
145
+
146
+ contract_info["deploy_args"].append({
147
+ "name": param_name,
148
+ "type": format_annotation(param.annotation),
149
+ "default": str(param.default) if param.default != inspect.Parameter.empty else None
150
+ })
151
+ except Exception:
152
+ pass
153
+
154
+ for method_name, method in inspect.getmembers(obj):
155
+ if method_name.startswith("_") or method_name in ["deploy", "address", "balance", "code", "chain"]:
156
+ continue
157
+
158
+ if not inspect.isfunction(method):
159
+ continue
160
+
161
+ # Filter: Must be mutable
162
+ if method_name not in mutable_functions:
163
+ continue
164
+
165
+ try:
166
+ sig = inspect.signature(method)
167
+ args = []
168
+ for param_name, param in sig.parameters.items():
169
+ if param_name == "self": continue
170
+ if param_name in ["from_", "value", "gas_limit", "gas_price", "max_fee_per_gas", "max_priority_fee_per_gas", "access_list", "type", "block", "confirmations", "return_tx", "request_type", "to"]:
171
+ continue
172
+
173
+ args.append({
174
+ "name": param_name,
175
+ "type": format_annotation(param.annotation)
176
+ })
177
+
178
+ contract_info["methods"].append({
179
+ "name": method_name,
180
+ "args": args
181
+ })
182
+ except Exception:
183
+ continue
184
+
185
+ contracts.append(contract_info)
186
+ except Exception as e:
187
+ pass
188
+
189
+ return { "contracts": contracts, "mock_erc20_module": mock_erc20 }
190
+
191
+ if __name__ == "__main__":
192
+ print(json.dumps(inspect_pytypes(), indent=2))
193
+ `;
194
+ class WakeGenerator {
195
+ constructor(foundryRoot, options) {
196
+ this.foundryRoot = foundryRoot;
197
+ this.options = options;
198
+ }
199
+ logDebug(message, obj) {
200
+ if (!this.options.debug)
201
+ return;
202
+ if (obj !== undefined) {
203
+ console.info(`[recon-generate wake] ${message}`, obj);
204
+ }
205
+ else {
206
+ console.info(`[recon-generate wake] ${message}`);
207
+ }
208
+ }
209
+ runCmd(cmd, cwd) {
210
+ return new Promise((resolve, reject) => {
211
+ (0, child_process_1.exec)(cmd, { cwd, env: { ...process.env, PATH: (0, utils_1.getEnvPath)() } }, (err, stdout, stderr) => {
212
+ if (err) {
213
+ reject(new Error(stderr || err.message));
214
+ }
215
+ else {
216
+ resolve(stdout);
217
+ }
218
+ });
219
+ });
220
+ }
221
+ async ensureBuild() {
222
+ const outDir = path.join(this.foundryRoot, '.recon', 'out');
223
+ if (await (0, utils_1.fileExists)(outDir) && !this.options.forceBuild) {
224
+ return;
225
+ }
226
+ console.log('Building project...');
227
+ await fs.mkdir(path.join(this.foundryRoot, '.recon'), { recursive: true });
228
+ const skipPaths = [
229
+ 'test',
230
+ 'tests',
231
+ 'script',
232
+ 'scripts',
233
+ 'contracts/test',
234
+ 'contracts/tests',
235
+ 'lib/**/test/**',
236
+ 'lib/**/tests/**'
237
+ ];
238
+ const skipArg = skipPaths.map(p => `--skip ${p}`).join(' ');
239
+ const cmd = `forge build --build-info ${skipArg} --out .recon/out`.replace(/\s+/g, ' ').trim();
240
+ await this.runCmd(cmd, this.foundryRoot);
241
+ }
242
+ async runWakeUp() {
243
+ console.log('Running wake up to generate pytypes...');
244
+ try {
245
+ await this.runCmd('wake up', this.foundryRoot);
246
+ console.log('✓ pytypes generated');
247
+ }
248
+ catch (e) {
249
+ console.warn('⚠ wake up failed - ensure wake is installed and configured:', e);
250
+ throw e;
251
+ }
252
+ }
253
+ async copyBuiltins() {
254
+ const reconPytypesDir = path.join(this.foundryRoot, 'pytypes', 'recon');
255
+ await fs.mkdir(reconPytypesDir, { recursive: true });
256
+ // locate builtins directory relative to dist/
257
+ const candidates = [
258
+ path.resolve(__dirname, '..', 'src', 'templates', 'wake', 'builtins'),
259
+ path.resolve(__dirname, '../../src/templates/wake/builtins'),
260
+ path.resolve(__dirname, 'templates/wake/builtins')
261
+ ];
262
+ let builtinsDir;
263
+ for (const p of candidates) {
264
+ if (await (0, utils_1.fileExists)(p)) {
265
+ builtinsDir = p;
266
+ break;
267
+ }
268
+ }
269
+ if (!builtinsDir) {
270
+ console.warn("⚠ Could not find builtin templates directory. AssetManager might fail.");
271
+ console.warn("Checked paths:", candidates);
272
+ return;
273
+ }
274
+ await fs.writeFile(path.join(reconPytypesDir, '__init__.py'), '');
275
+ const files = await fs.readdir(builtinsDir);
276
+ for (const file of files) {
277
+ if (file.endsWith('.py')) {
278
+ const content = await fs.readFile(path.join(builtinsDir, file), 'utf8');
279
+ await fs.writeFile(path.join(reconPytypesDir, file), content);
280
+ console.log(`✓ Copied builtin ${file} to ${path.join(reconPytypesDir, file)}`);
281
+ }
282
+ }
283
+ }
284
+ async inspectPytypes() {
285
+ const tempScript = path.join(this.foundryRoot, '.recon', 'inspect_wake.py');
286
+ await fs.mkdir(path.dirname(tempScript), { recursive: true });
287
+ await fs.writeFile(tempScript, INSPECT_SCRIPT);
288
+ try {
289
+ const output = await this.runCmd(`python3 ${tempScript}`, this.foundryRoot);
290
+ return JSON.parse(output);
291
+ }
292
+ catch (e) {
293
+ // Fallback to python
294
+ try {
295
+ const output = await this.runCmd(`python ${tempScript}`, this.foundryRoot);
296
+ return JSON.parse(output);
297
+ }
298
+ catch (e2) {
299
+ throw new Error(`Failed to run introspection script: ${String(e2)}`);
300
+ }
301
+ }
302
+ }
303
+ matchesSignatureOrName(signature, set) {
304
+ if (!set || set.size === 0) {
305
+ return false;
306
+ }
307
+ const nameOnly = signature.split('(')[0];
308
+ if (set.has(signature) || set.has(nameOnly)) {
309
+ return true;
310
+ }
311
+ return false;
312
+ }
313
+ isContractAllowed(name) {
314
+ const { include, exclude } = this.options;
315
+ if (include && (include.contractOnly.size > 0 || include.functions.size > 0)) {
316
+ const inInclude = include.contractOnly.has(name) || include.functions.has(name);
317
+ if (!inInclude)
318
+ return false;
319
+ }
320
+ if (exclude && exclude.contractOnly.has(name)) {
321
+ return false;
322
+ }
323
+ return true;
324
+ }
325
+ isFunctionIncluded(contractName, method) {
326
+ const sig = `${method.name}(${method.args.map(a => a.type).join(',')})`;
327
+ const { include, exclude } = this.options;
328
+ if (include && include.functions.size > 0) {
329
+ const fnSet = include.functions.get(contractName);
330
+ if (fnSet) {
331
+ if (!this.matchesSignatureOrName(sig, fnSet)) {
332
+ return false;
333
+ }
334
+ }
335
+ }
336
+ if (exclude && exclude.functions.size > 0) {
337
+ const fnSet = exclude.functions.get(contractName);
338
+ if (fnSet && this.matchesSignatureOrName(sig, fnSet)) {
339
+ return false;
340
+ }
341
+ }
342
+ return true;
343
+ }
344
+ async run() {
345
+ if (!this.options.force && await (0, utils_1.fileExists)(this.options.suiteDir)) {
346
+ throw new Error(`${this.options.suiteDir} exists. Use --force.`);
347
+ }
348
+ await this.ensureBuild();
349
+ try {
350
+ await this.runWakeUp();
351
+ }
352
+ catch (e) {
353
+ console.warn('Wake up warning (continuing):', e);
354
+ }
355
+ // Setup builtin mock
356
+ await this.copyBuiltins();
357
+ const introspection = await this.inspectPytypes();
358
+ const contracts = introspection.contracts;
359
+ this.logDebug('Discovered contracts from pytypes', contracts.map(c => c.name));
360
+ const filteredContracts = contracts.filter(c => this.isContractAllowed(c.name));
361
+ const contractFlows = new Map();
362
+ for (const contract of filteredContracts) {
363
+ const flows = [];
364
+ for (const method of contract.methods) {
365
+ if (this.isFunctionIncluded(contract.name, method)) {
366
+ // Check admin
367
+ const sig = `${method.name}(${method.args.map(a => a.type).join(',')})`;
368
+ let actor = types_1.Actor.ACTOR;
369
+ if (this.options.admin && this.options.admin.has(contract.name)) {
370
+ const adminSet = this.options.admin.get(contract.name);
371
+ if (this.matchesSignatureOrName(sig, adminSet)) {
372
+ actor = types_1.Actor.ADMIN;
373
+ }
374
+ }
375
+ flows.push({
376
+ contractName: contract.name,
377
+ method: method,
378
+ actor: actor,
379
+ mode: types_1.Mode.NORMAL
380
+ });
381
+ }
382
+ }
383
+ if (flows.length > 0) {
384
+ contractFlows.set(contract.name, flows);
385
+ }
386
+ }
387
+ if (await (0, utils_1.fileExists)(this.options.suiteDir)) {
388
+ await fs.rm(this.options.suiteDir, { recursive: true, force: true });
389
+ }
390
+ await fs.mkdir(this.options.suiteDir, { recursive: true });
391
+ // Create directories
392
+ const helpersDir = path.join(this.options.suiteDir, 'helpers');
393
+ const targetsDir = path.join(this.options.suiteDir, 'targets');
394
+ await fs.mkdir(helpersDir, { recursive: true });
395
+ await fs.mkdir(targetsDir, { recursive: true });
396
+ // __init__.py files
397
+ await fs.writeFile(path.join(this.options.suiteDir, '__init__.py'), '');
398
+ await fs.writeFile(path.join(helpersDir, '__init__.py'), '');
399
+ await fs.writeFile(path.join(targetsDir, '__init__.py'), '');
400
+ // Helpers
401
+ await fs.writeFile(path.join(helpersDir, 'actor_manager.py'), wakeTemplates.actorManagerTemplate({}));
402
+ await fs.writeFile(path.join(helpersDir, 'asset_manager.py'), wakeTemplates.assetManagerTemplate({}));
403
+ await fs.writeFile(path.join(helpersDir, 'utils.py'), wakeTemplates.utilsTemplate({}));
404
+ // targets/managers_targets.py
405
+ await fs.writeFile(path.join(targetsDir, 'managers_targets.py'), wakeTemplates.managersTargetsTemplate({}));
406
+ // targets/{contract}_targets.py
407
+ for (const contract of filteredContracts) {
408
+ const flows = contractFlows.get(contract.name) || [];
409
+ // Even if no flows, we might want the class? Usually yes.
410
+ const content = wakeTemplates.contractTargetsTemplate({
411
+ contract,
412
+ flows
413
+ });
414
+ const filename = `${(0, case_1.snake)(contract.name)}_targets.py`;
415
+ await fs.writeFile(path.join(targetsDir, filename), content);
416
+ }
417
+ // setup.py
418
+ await fs.writeFile(path.join(this.options.suiteDir, 'setup.py'), wakeTemplates.setupTemplate({ contracts: filteredContracts }));
419
+ // target_functions.py
420
+ await fs.writeFile(path.join(this.options.suiteDir, 'target_functions.py'), wakeTemplates.targetFunctionsTemplate({ contracts: filteredContracts }));
421
+ // properties.py
422
+ await fs.writeFile(path.join(this.options.suiteDir, 'properties.py'), wakeTemplates.propertiesTemplate({}));
423
+ // before_after.py
424
+ await fs.writeFile(path.join(this.options.suiteDir, 'before_after.py'), wakeTemplates.beforeAfterTemplate({}));
425
+ // test_crytic.py
426
+ const testContent = wakeTemplates.cryticTesterTemplate({});
427
+ await fs.writeFile(path.join(this.options.suiteDir, 'test_crytic.py'), testContent);
428
+ console.log(`✓ Generated Wake suite at ${this.options.suiteDir}`);
429
+ }
430
+ async listAvailable() {
431
+ await this.ensureBuild();
432
+ await this.runWakeUp();
433
+ const introspection = await this.inspectPytypes();
434
+ const filtered = introspection.contracts.filter(c => this.isContractAllowed(c.name));
435
+ console.log('Available contracts/functions (Wake):');
436
+ for (const c of filtered) {
437
+ console.log(`- ${c.name} (${c.module})`);
438
+ for (const m of c.methods) {
439
+ const sig = `${m.name}(${m.args.map(a => a.type).join(',')})`;
440
+ if (this.isFunctionIncluded(c.name, m)) {
441
+ console.log(` • ${sig}`);
442
+ }
443
+ }
444
+ }
445
+ }
446
+ }
447
+ exports.WakeGenerator = WakeGenerator;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recon-generate",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "description": "CLI to scaffold Recon fuzzing suite inside Foundry projects",
5
5
  "main": "dist/index.js",
6
6
  "bin": {