recon-generate 0.0.2
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 +84 -0
- package/dist/generator.d.ts +52 -0
- package/dist/generator.js +512 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +169 -0
- package/dist/templateManager.d.ts +14 -0
- package/dist/templateManager.js +236 -0
- package/dist/templates/before-after.d.ts +1 -0
- package/dist/templates/before-after.js +37 -0
- package/dist/templates/crytic-tester.d.ts +1 -0
- package/dist/templates/crytic-tester.js +23 -0
- package/dist/templates/crytic-to-foundry.d.ts +1 -0
- package/dist/templates/crytic-to-foundry.js +32 -0
- package/dist/templates/echidna-config.d.ts +1 -0
- package/dist/templates/echidna-config.js +20 -0
- package/dist/templates/halmos-config.d.ts +1 -0
- package/dist/templates/halmos-config.js +14 -0
- package/dist/templates/handlebars-helpers.d.ts +2 -0
- package/dist/templates/handlebars-helpers.js +142 -0
- package/dist/templates/index.d.ts +14 -0
- package/dist/templates/index.js +29 -0
- package/dist/templates/medusa-config.d.ts +1 -0
- package/dist/templates/medusa-config.js +112 -0
- package/dist/templates/properties.d.ts +1 -0
- package/dist/templates/properties.js +18 -0
- package/dist/templates/setup.d.ts +1 -0
- package/dist/templates/setup.js +55 -0
- package/dist/templates/target-functions.d.ts +1 -0
- package/dist/templates/target-functions.js +37 -0
- package/dist/templates/targets/admin-targets.d.ts +1 -0
- package/dist/templates/targets/admin-targets.js +33 -0
- package/dist/templates/targets/doomsday-targets.d.ts +1 -0
- package/dist/templates/targets/doomsday-targets.js +33 -0
- package/dist/templates/targets/managers-targets.d.ts +1 -0
- package/dist/templates/targets/managers-targets.js +61 -0
- package/dist/templates/targets/targets.d.ts +1 -0
- package/dist/templates/targets/targets.js +35 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.js +14 -0
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +106 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
## recon-generate
|
|
2
|
+
|
|
3
|
+
CLI to scaffold the Recon fuzzing suite (Chimera/Echidna/Medusa/Halmos targets) inside a Foundry project.
|
|
4
|
+
|
|
5
|
+
### Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g recon-generate
|
|
9
|
+
# or from this repo
|
|
10
|
+
npm install -g .
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For local dev:
|
|
14
|
+
```bash
|
|
15
|
+
npm install
|
|
16
|
+
npm run build
|
|
17
|
+
npm link # optional, to use globally
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Usage
|
|
21
|
+
|
|
22
|
+
Run from your Foundry project root (where `foundry.toml` is):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
recon-generate [options]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Key options:
|
|
29
|
+
|
|
30
|
+
- `--include "A,B:{foo(uint256)|bar(address)},C"` — include-only these contracts/functions.
|
|
31
|
+
- `--exclude "A:{foo(uint256)},D"` — exclude these contracts/functions.
|
|
32
|
+
- `--admin "A:{fnSig,fnSig}"` — mark listed functions as admin (`asAdmin`).
|
|
33
|
+
- `--mock "A,B"` — generate mocks for the listed contracts (compiled ABIs required); mocks go under `recon[-name]/mocks` and are added to targets.
|
|
34
|
+
- `--name <suite>` — name the suite; affects folder (`recon-<name>`), config filenames (`echidna-<name>.yaml`, `medusa-<name>.json`, `halmos-<name>.toml`), and Crytic tester/runner names.
|
|
35
|
+
- `--force` — replace existing generated suite output under `--output` (does **not** rebuild `.recon/out`).
|
|
36
|
+
- `--force-build` — delete `.recon/out` to force a fresh compile before generation.
|
|
37
|
+
- `--list` — show available contracts/functions after filters, then exit.
|
|
38
|
+
- `--debug` — print filter/matching/debug info.
|
|
39
|
+
- `--recon <path>` — custom recon.json path (defaults to `./recon.json` or `./.recon/recon.json`).
|
|
40
|
+
- `--output <path>` — base path where the suite folder will be placed; we append `/recon` or `/recon-<name>`. Example: `--output test` → `<foundry root>/test/recon-<name>`.
|
|
41
|
+
- `--foundry-config <path>` — path to `foundry.toml` (default `./foundry.toml`).
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Default scaffolding
|
|
47
|
+
recon-generate
|
|
48
|
+
|
|
49
|
+
# Exclude a contract
|
|
50
|
+
recon-generate --exclude "Morpho" --force
|
|
51
|
+
|
|
52
|
+
# Exclude a single function only
|
|
53
|
+
recon-generate --exclude "Morpho:{setOwner(address)}" --force
|
|
54
|
+
|
|
55
|
+
# Mark specific functions as admin
|
|
56
|
+
recon-generate --admin "Morpho:{setOwner(address),enableIrm(address),setFee(MarketParams,uint256),setFeeRecipient(address)}" --force
|
|
57
|
+
|
|
58
|
+
# List what would be generated (no files written)
|
|
59
|
+
recon-generate --list --include "Morpho"
|
|
60
|
+
|
|
61
|
+
# Generate a named suite (outputs under recon-foo, configs echidna-foo.yaml, etc.)
|
|
62
|
+
recon-generate --name foo --force
|
|
63
|
+
|
|
64
|
+
# Generate mocks and include them in targets
|
|
65
|
+
recon-generate --mock "Morpho,Other" --force
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Behavior
|
|
69
|
+
|
|
70
|
+
- If the suite output folder exists and `--force` is not provided, the command aborts.
|
|
71
|
+
- `--force` replaces the suite output folder; it does **not** rebuild `.recon/out`.
|
|
72
|
+
- `--force-build` deletes `.recon/out` to force a fresh compile before generation.
|
|
73
|
+
- When `--mock` is used, a fresh build is triggered, mocks are generated under `recon[-name]/mocks`, and a second build picks them up into `.recon/out` and targets.
|
|
74
|
+
- `recon.json` is regenerated every run from current filters/contracts.
|
|
75
|
+
- Include/exclude/admin matching is tolerant of fully qualified struct names vs short names (e.g., `Morpho.MarketParams` vs `MarketParams`).
|
|
76
|
+
- Admin list sets `actor: admin` for matched functions; others default to `actor`.
|
|
77
|
+
|
|
78
|
+
### Development
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm install
|
|
82
|
+
npm run build
|
|
83
|
+
npm link # to test globally
|
|
84
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type FilterSpec = {
|
|
2
|
+
contractOnly: Set<string>;
|
|
3
|
+
functions: Map<string, Set<string>>;
|
|
4
|
+
};
|
|
5
|
+
export interface GeneratorOptions {
|
|
6
|
+
suiteDir: string;
|
|
7
|
+
reconConfigPath: string;
|
|
8
|
+
foundryConfigPath: string;
|
|
9
|
+
include?: FilterSpec;
|
|
10
|
+
exclude?: FilterSpec;
|
|
11
|
+
debug?: boolean;
|
|
12
|
+
force?: boolean;
|
|
13
|
+
forceBuild?: boolean;
|
|
14
|
+
admin?: Map<string, Set<string>>;
|
|
15
|
+
mocks?: Set<string>;
|
|
16
|
+
suiteNameSnake: string;
|
|
17
|
+
suiteNamePascal: string;
|
|
18
|
+
}
|
|
19
|
+
export declare class ReconGenerator {
|
|
20
|
+
private foundryRoot;
|
|
21
|
+
private options;
|
|
22
|
+
private generatedMocks;
|
|
23
|
+
private allowedMockNames;
|
|
24
|
+
constructor(foundryRoot: string, options: GeneratorOptions);
|
|
25
|
+
private logDebug;
|
|
26
|
+
private outDir;
|
|
27
|
+
private ensureReconFolder;
|
|
28
|
+
private removePath;
|
|
29
|
+
private runCmd;
|
|
30
|
+
private ensureFoundryConfigExists;
|
|
31
|
+
private ensureBuild;
|
|
32
|
+
private installChimera;
|
|
33
|
+
private installSetupHelpers;
|
|
34
|
+
private updateRemappings;
|
|
35
|
+
private updateGitignore;
|
|
36
|
+
private findSourceContracts;
|
|
37
|
+
private matchesSignatureOrName;
|
|
38
|
+
private paramType;
|
|
39
|
+
private simplifyType;
|
|
40
|
+
private simplifySignature;
|
|
41
|
+
private isAdminFunction;
|
|
42
|
+
private isContractAllowed;
|
|
43
|
+
private mockNameFor;
|
|
44
|
+
private findAbiPath;
|
|
45
|
+
private generateMocks;
|
|
46
|
+
private copyAndCleanupMocks;
|
|
47
|
+
private isFunctionIncluded;
|
|
48
|
+
private buildDefaultReconConfig;
|
|
49
|
+
private loadReconConfig;
|
|
50
|
+
run(): Promise<void>;
|
|
51
|
+
listAvailable(): Promise<void>;
|
|
52
|
+
}
|
|
@@ -0,0 +1,512 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.ReconGenerator = void 0;
|
|
40
|
+
const child_process_1 = require("child_process");
|
|
41
|
+
const fs = __importStar(require("fs/promises"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const abi_to_mock_1 = __importDefault(require("abi-to-mock"));
|
|
44
|
+
const templateManager_1 = require("./templateManager");
|
|
45
|
+
const types_1 = require("./types");
|
|
46
|
+
const utils_1 = require("./utils");
|
|
47
|
+
class ReconGenerator {
|
|
48
|
+
constructor(foundryRoot, options) {
|
|
49
|
+
this.foundryRoot = foundryRoot;
|
|
50
|
+
this.options = options;
|
|
51
|
+
this.generatedMocks = new Set();
|
|
52
|
+
this.allowedMockNames = new Set();
|
|
53
|
+
}
|
|
54
|
+
logDebug(message, obj) {
|
|
55
|
+
if (!this.options.debug)
|
|
56
|
+
return;
|
|
57
|
+
if (obj !== undefined) {
|
|
58
|
+
console.info(`[recon-generate] ${message}`, obj);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.info(`[recon-generate] ${message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
outDir() {
|
|
65
|
+
return path.join(this.foundryRoot, '.recon', 'out');
|
|
66
|
+
}
|
|
67
|
+
async ensureReconFolder() {
|
|
68
|
+
await fs.mkdir(path.join(this.foundryRoot, '.recon'), { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
async removePath(p) {
|
|
71
|
+
try {
|
|
72
|
+
await fs.rm(p, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
/* ignore */
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
runCmd(cmd, cwd) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
(0, child_process_1.exec)(cmd, { cwd, env: { ...process.env, PATH: (0, utils_1.getEnvPath)() } }, (err, stdout, stderr) => {
|
|
81
|
+
if (err) {
|
|
82
|
+
reject(new Error(stderr || err.message));
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
resolve();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async ensureFoundryConfigExists() {
|
|
91
|
+
if (!(await (0, utils_1.fileExists)(this.options.foundryConfigPath))) {
|
|
92
|
+
throw new Error(`foundry.toml not found at ${this.options.foundryConfigPath}. Run recon-generate from a Foundry project root or pass --foundry-config <path>.`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async ensureBuild() {
|
|
96
|
+
const outPath = this.outDir();
|
|
97
|
+
if (await (0, utils_1.fileExists)(outPath)) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
await this.ensureReconFolder();
|
|
101
|
+
const skipArg = '--skip */test/** */tests/** */script/** */scripts/** */contracts/test/**';
|
|
102
|
+
const cmd = `forge build --build-info ${skipArg} --out .recon/out`.replace(/\s+/g, ' ').trim();
|
|
103
|
+
await this.runCmd(cmd, this.foundryRoot);
|
|
104
|
+
}
|
|
105
|
+
async installChimera() {
|
|
106
|
+
const chimeraPath = path.join(this.foundryRoot, 'lib', 'chimera');
|
|
107
|
+
if (await (0, utils_1.fileExists)(chimeraPath)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
await this.runCmd('forge install Recon-Fuzz/chimera --no-git', this.foundryRoot);
|
|
111
|
+
}
|
|
112
|
+
async installSetupHelpers() {
|
|
113
|
+
const setupPath = path.join(this.foundryRoot, 'lib', 'setup-helpers');
|
|
114
|
+
if (await (0, utils_1.fileExists)(setupPath)) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
await this.runCmd('forge install Recon-Fuzz/setup-helpers --no-git', this.foundryRoot);
|
|
118
|
+
}
|
|
119
|
+
async updateRemappings() {
|
|
120
|
+
const remappingsPath = path.join(this.foundryRoot, 'remappings.txt');
|
|
121
|
+
if (!(await (0, utils_1.fileExists)(remappingsPath))) {
|
|
122
|
+
await this.runCmd('forge remappings > remappings.txt', this.foundryRoot);
|
|
123
|
+
}
|
|
124
|
+
const current = await fs.readFile(remappingsPath, 'utf8');
|
|
125
|
+
const chimeraMapping = '@chimera/=lib/chimera/src/';
|
|
126
|
+
const setupToolsMapping = '@recon/=lib/setup-helpers/src/';
|
|
127
|
+
const remappings = current
|
|
128
|
+
.split('\n')
|
|
129
|
+
.map(line => line.trim())
|
|
130
|
+
.filter(line => line && !line.startsWith('@chimera') && !line.startsWith('@recon'));
|
|
131
|
+
remappings.push(chimeraMapping);
|
|
132
|
+
remappings.push(setupToolsMapping);
|
|
133
|
+
await fs.writeFile(remappingsPath, remappings.join('\n'));
|
|
134
|
+
}
|
|
135
|
+
async updateGitignore() {
|
|
136
|
+
const gitignorePath = path.join(this.foundryRoot, '.gitignore');
|
|
137
|
+
const additions = ['crytic-export', 'echidna', 'medusa', '.recon'];
|
|
138
|
+
let content = '';
|
|
139
|
+
try {
|
|
140
|
+
content = await fs.readFile(gitignorePath, 'utf8');
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// start empty
|
|
144
|
+
}
|
|
145
|
+
let changed = false;
|
|
146
|
+
for (const line of additions) {
|
|
147
|
+
if (!content.includes(line)) {
|
|
148
|
+
content += (content.endsWith('\n') || content.length === 0 ? '' : '\n') + line + '\n';
|
|
149
|
+
changed = true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (changed) {
|
|
153
|
+
await fs.writeFile(gitignorePath, content);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async findSourceContracts() {
|
|
157
|
+
var _a;
|
|
158
|
+
const contracts = [];
|
|
159
|
+
const outDir = this.outDir();
|
|
160
|
+
try {
|
|
161
|
+
if (!(await (0, utils_1.fileExists)(outDir))) {
|
|
162
|
+
throw new Error(`Compiled output not found at ${outDir}. Please ensure forge build runs successfully (we attempt it automatically).`);
|
|
163
|
+
}
|
|
164
|
+
const entries = await fs.readdir(outDir, { withFileTypes: true });
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
if (!entry.isDirectory()) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const contractDir = path.join(outDir, entry.name);
|
|
170
|
+
const files = await fs.readdir(contractDir);
|
|
171
|
+
for (const file of files) {
|
|
172
|
+
if (!file.endsWith('.json')) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const filePath = path.join(contractDir, file);
|
|
176
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
177
|
+
const json = JSON.parse(content);
|
|
178
|
+
if (json.metadata && json.abi) {
|
|
179
|
+
const metadata = json.metadata;
|
|
180
|
+
if ((_a = metadata.settings) === null || _a === void 0 ? void 0 : _a.compilationTarget) {
|
|
181
|
+
for (const [sourcePath, contractName] of Object.entries(metadata.settings.compilationTarget)) {
|
|
182
|
+
const lowerPath = sourcePath.toLowerCase();
|
|
183
|
+
const lowerName = String(contractName).toLowerCase();
|
|
184
|
+
// Skip interfaces, libraries, and mocks by convention, unless explicitly allowed (generated mocks)
|
|
185
|
+
const isGeneratedMock = this.generatedMocks.has(String(contractName));
|
|
186
|
+
if (!isGeneratedMock) {
|
|
187
|
+
if (lowerPath.includes('/interface') ||
|
|
188
|
+
lowerPath.includes('/interfaces') ||
|
|
189
|
+
lowerPath.includes('/lib/') ||
|
|
190
|
+
lowerPath.includes('/libraries') ||
|
|
191
|
+
lowerPath.includes('/library') ||
|
|
192
|
+
lowerPath.includes('/mock') ||
|
|
193
|
+
lowerName.includes('mock')) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const relativeJson = path.relative(this.foundryRoot, filePath);
|
|
198
|
+
let displayedPath = sourcePath;
|
|
199
|
+
if (isGeneratedMock) {
|
|
200
|
+
displayedPath = path.relative(this.foundryRoot, path.join(this.options.suiteDir, 'mocks', `${contractName}.sol`));
|
|
201
|
+
}
|
|
202
|
+
contracts.push({
|
|
203
|
+
path: displayedPath,
|
|
204
|
+
name: contractName,
|
|
205
|
+
jsonPath: relativeJson,
|
|
206
|
+
abi: json.abi,
|
|
207
|
+
enabled: true,
|
|
208
|
+
separated: true,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
this.logDebug('Discovered contracts', contracts.map(c => ({ name: c.name, jsonPath: c.jsonPath, path: c.path })));
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
throw new Error(`Failed to read compiled contracts at ${outDir}: ${e}`);
|
|
219
|
+
}
|
|
220
|
+
return contracts;
|
|
221
|
+
}
|
|
222
|
+
matchesSignatureOrName(signature, set) {
|
|
223
|
+
if (!set || set.size === 0) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
const nameOnly = signature.split('(')[0];
|
|
227
|
+
const simplifiedSig = this.simplifySignature(signature);
|
|
228
|
+
const simplifiedName = simplifiedSig.split('(')[0];
|
|
229
|
+
if (set.has(signature) || set.has(nameOnly) || set.has(simplifiedSig) || set.has(simplifiedName)) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
// Also allow matching on prefix of param types (struct fully qualified vs short)
|
|
233
|
+
const tryParts = (sig) => {
|
|
234
|
+
const m = sig.match(/^(.*?)\((.*)\)$/);
|
|
235
|
+
if (!m) {
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
const name = m[1];
|
|
239
|
+
const params = m[2];
|
|
240
|
+
const parts = params ? params.split(',').map(p => p.trim()) : [];
|
|
241
|
+
const shortened = parts.map(p => this.simplifyType(p));
|
|
242
|
+
return [`${name}(${shortened.join(',')})`, name];
|
|
243
|
+
};
|
|
244
|
+
for (const candidate of tryParts(signature).concat(tryParts(simplifiedSig))) {
|
|
245
|
+
if (set.has(candidate)) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
paramType(p) {
|
|
252
|
+
if (p.internalType) {
|
|
253
|
+
const parts = p.internalType.trim().split(/\s+/);
|
|
254
|
+
if (parts.length > 1) {
|
|
255
|
+
return parts[1];
|
|
256
|
+
}
|
|
257
|
+
return parts[0];
|
|
258
|
+
}
|
|
259
|
+
return p.type;
|
|
260
|
+
}
|
|
261
|
+
simplifyType(t) {
|
|
262
|
+
const arraySuffixMatch = t.match(/(\[.*\])$/);
|
|
263
|
+
const arraySuffix = arraySuffixMatch ? arraySuffixMatch[1] : '';
|
|
264
|
+
const base = arraySuffix ? t.slice(0, -arraySuffix.length) : t;
|
|
265
|
+
const segments = base.split('.');
|
|
266
|
+
const simple = segments[segments.length - 1];
|
|
267
|
+
return `${simple}${arraySuffix}`;
|
|
268
|
+
}
|
|
269
|
+
simplifySignature(signature) {
|
|
270
|
+
const match = signature.match(/^(.*?)\((.*)\)$/);
|
|
271
|
+
if (!match) {
|
|
272
|
+
return signature;
|
|
273
|
+
}
|
|
274
|
+
const name = match[1];
|
|
275
|
+
const params = match[2];
|
|
276
|
+
if (!params) {
|
|
277
|
+
return `${name}()`;
|
|
278
|
+
}
|
|
279
|
+
const simplifiedParams = params.split(',').map(p => this.simplifyType(p.trim())).join(',');
|
|
280
|
+
return `${name}(${simplifiedParams})`;
|
|
281
|
+
}
|
|
282
|
+
isAdminFunction(contractName, signature) {
|
|
283
|
+
if (!this.options.admin || this.options.admin.size === 0) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
const set = this.options.admin.get(contractName);
|
|
287
|
+
if (!set) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
const matched = this.matchesSignatureOrName(signature, set);
|
|
291
|
+
if (matched && this.options.debug) {
|
|
292
|
+
this.logDebug('Matched admin function', { contractName, signature, set: Array.from(set) });
|
|
293
|
+
}
|
|
294
|
+
return matched;
|
|
295
|
+
}
|
|
296
|
+
isContractAllowed(name) {
|
|
297
|
+
const { include, exclude } = this.options;
|
|
298
|
+
if (this.options.mocks && this.options.mocks.has(name)) {
|
|
299
|
+
this.logDebug(`Contract filtered because it is mocked`, { name });
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
// Include logic
|
|
303
|
+
if (include && (include.contractOnly.size > 0 || include.functions.size > 0)) {
|
|
304
|
+
const inInclude = include.contractOnly.has(name) || include.functions.has(name);
|
|
305
|
+
if (!inInclude) {
|
|
306
|
+
this.logDebug(`Contract filtered by include`, { name });
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// Exclude logic
|
|
311
|
+
if (exclude && exclude.contractOnly.has(name)) {
|
|
312
|
+
this.logDebug(`Contract filtered by exclude`, { name });
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
mockNameFor(contract) {
|
|
318
|
+
return `${contract}Mock`;
|
|
319
|
+
}
|
|
320
|
+
async findAbiPath(contractName) {
|
|
321
|
+
const outDir = this.outDir();
|
|
322
|
+
const entries = await fs.readdir(outDir, { withFileTypes: true });
|
|
323
|
+
for (const entry of entries) {
|
|
324
|
+
if (!entry.isDirectory()) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const candidate = path.join(outDir, entry.name, `${contractName}.json`);
|
|
328
|
+
if (await (0, utils_1.fileExists)(candidate)) {
|
|
329
|
+
return candidate;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
throw new Error(`ABI for contract ${contractName} not found in .recon/out. Build may have failed.`);
|
|
333
|
+
}
|
|
334
|
+
async generateMocks() {
|
|
335
|
+
if (!this.options.mocks || this.options.mocks.size === 0) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
const tempSrc = path.join(this.foundryRoot, 'src', '__recon');
|
|
339
|
+
await fs.mkdir(tempSrc, { recursive: true });
|
|
340
|
+
for (const name of this.options.mocks) {
|
|
341
|
+
const mockName = this.mockNameFor(name);
|
|
342
|
+
const abiPath = await this.findAbiPath(name);
|
|
343
|
+
await (0, abi_to_mock_1.default)(abiPath, tempSrc, mockName);
|
|
344
|
+
this.generatedMocks.add(mockName);
|
|
345
|
+
this.allowedMockNames.add(mockName);
|
|
346
|
+
this.logDebug('Generated mock', { name, mockName, abiPath, output: tempSrc });
|
|
347
|
+
}
|
|
348
|
+
return tempSrc;
|
|
349
|
+
}
|
|
350
|
+
async copyAndCleanupMocks(tempSrc) {
|
|
351
|
+
if (!tempSrc) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const mocksDir = path.join(this.options.suiteDir, 'mocks');
|
|
355
|
+
await fs.mkdir(mocksDir, { recursive: true });
|
|
356
|
+
const tempFiles = await fs.readdir(tempSrc);
|
|
357
|
+
for (const f of tempFiles) {
|
|
358
|
+
if (!f.endsWith('.sol')) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const srcPath = path.join(tempSrc, f);
|
|
362
|
+
const destPath = path.join(mocksDir, f);
|
|
363
|
+
await fs.copyFile(srcPath, destPath);
|
|
364
|
+
}
|
|
365
|
+
await this.removePath(tempSrc);
|
|
366
|
+
}
|
|
367
|
+
isFunctionIncluded(contractName, signature) {
|
|
368
|
+
const { include, exclude } = this.options;
|
|
369
|
+
// Include functions filter (if provided for this contract)
|
|
370
|
+
if (include && include.functions.size > 0) {
|
|
371
|
+
const fnSet = include.functions.get(contractName);
|
|
372
|
+
if (fnSet) {
|
|
373
|
+
if (!this.matchesSignatureOrName(signature, fnSet)) {
|
|
374
|
+
this.logDebug('Function filtered by include', { contractName, signature });
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// Exclude functions filter
|
|
380
|
+
if (exclude && exclude.functions.size > 0) {
|
|
381
|
+
const fnSet = exclude.functions.get(contractName);
|
|
382
|
+
if (fnSet && this.matchesSignatureOrName(signature, fnSet)) {
|
|
383
|
+
this.logDebug('Function filtered by exclude', { contractName, signature });
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
async buildDefaultReconConfig(contracts) {
|
|
390
|
+
var _a;
|
|
391
|
+
const config = {};
|
|
392
|
+
for (const contract of contracts) {
|
|
393
|
+
if (!this.isContractAllowed(contract.name)) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const funcs = [];
|
|
397
|
+
for (const item of contract.abi) {
|
|
398
|
+
if (item.type === 'function' && !((_a = item.stateMutability) === null || _a === void 0 ? void 0 : _a.match(/^(view|pure)$/))) {
|
|
399
|
+
const signature = `${item.name}(${item.inputs.map((i) => this.paramType(i)).join(',')})`;
|
|
400
|
+
if (this.isFunctionIncluded(contract.name, signature)) {
|
|
401
|
+
const actor = this.isAdminFunction(contract.name, signature) ? types_1.Actor.ADMIN : types_1.Actor.ACTOR;
|
|
402
|
+
if (this.options.debug && actor === types_1.Actor.ADMIN) {
|
|
403
|
+
this.logDebug('Assigning admin actor in default config', { contract: contract.name, signature });
|
|
404
|
+
}
|
|
405
|
+
funcs.push({ signature, actor, mode: types_1.Mode.NORMAL });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (funcs.length === 0) {
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
config[contract.jsonPath] = {
|
|
413
|
+
enabled: true,
|
|
414
|
+
functions: funcs,
|
|
415
|
+
separated: true,
|
|
416
|
+
};
|
|
417
|
+
this.logDebug('Added contract to recon config (default)', { contract: contract.name, functions: funcs.map(f => f.signature) });
|
|
418
|
+
}
|
|
419
|
+
return config;
|
|
420
|
+
}
|
|
421
|
+
async loadReconConfig(contracts) {
|
|
422
|
+
const reconPath = this.options.reconConfigPath;
|
|
423
|
+
const config = await this.buildDefaultReconConfig(contracts);
|
|
424
|
+
await fs.mkdir(path.dirname(reconPath), { recursive: true });
|
|
425
|
+
await fs.writeFile(reconPath, JSON.stringify(config, null, 2));
|
|
426
|
+
this.logDebug('Wrote new recon config', config);
|
|
427
|
+
return config;
|
|
428
|
+
}
|
|
429
|
+
async run() {
|
|
430
|
+
var _a;
|
|
431
|
+
await this.ensureFoundryConfigExists();
|
|
432
|
+
const outPath = this.outDir();
|
|
433
|
+
const mockTargets = (_a = this.options.mocks) !== null && _a !== void 0 ? _a : new Set();
|
|
434
|
+
this.generatedMocks.clear();
|
|
435
|
+
this.allowedMockNames = new Set([...mockTargets].map(n => this.mockNameFor(n)));
|
|
436
|
+
const suiteExists = await (0, utils_1.fileExists)(this.options.suiteDir);
|
|
437
|
+
if (suiteExists) {
|
|
438
|
+
if (!this.options.force) {
|
|
439
|
+
throw new Error(`${this.options.suiteDir} already exists. Re-run with --force to replace generated suite (use --force-build to rebuild .recon/out).`);
|
|
440
|
+
}
|
|
441
|
+
this.logDebug('Force enabled: removing existing generated output');
|
|
442
|
+
await this.removePath(this.options.suiteDir);
|
|
443
|
+
}
|
|
444
|
+
if (this.options.forceBuild || mockTargets.size > 0 || !(await (0, utils_1.fileExists)(outPath))) {
|
|
445
|
+
if (await (0, utils_1.fileExists)(outPath)) {
|
|
446
|
+
this.logDebug('Rebuilding: removing existing .recon/out');
|
|
447
|
+
await this.removePath(outPath);
|
|
448
|
+
}
|
|
449
|
+
await this.ensureBuild(); // first build to get ABIs
|
|
450
|
+
}
|
|
451
|
+
// Generate mocks if requested, then rebuild to pick them up
|
|
452
|
+
let tempMockSrc = null;
|
|
453
|
+
if (mockTargets.size > 0) {
|
|
454
|
+
tempMockSrc = await this.generateMocks();
|
|
455
|
+
if (await (0, utils_1.fileExists)(outPath)) {
|
|
456
|
+
await this.removePath(outPath);
|
|
457
|
+
}
|
|
458
|
+
await this.ensureBuild();
|
|
459
|
+
}
|
|
460
|
+
await this.installChimera();
|
|
461
|
+
await this.installSetupHelpers();
|
|
462
|
+
await this.updateRemappings();
|
|
463
|
+
await this.updateGitignore();
|
|
464
|
+
// After all builds, copy mocks to suite folder and clean temp
|
|
465
|
+
if (mockTargets.size > 0) {
|
|
466
|
+
await this.copyAndCleanupMocks(tempMockSrc);
|
|
467
|
+
}
|
|
468
|
+
const contracts = await this.findSourceContracts();
|
|
469
|
+
const filteredContracts = contracts.filter(c => this.isContractAllowed(c.name));
|
|
470
|
+
if (filteredContracts.length === 0) {
|
|
471
|
+
this.logDebug('No contracts left after include/exclude filters');
|
|
472
|
+
}
|
|
473
|
+
const reconConfig = await this.loadReconConfig(filteredContracts);
|
|
474
|
+
const tm = new templateManager_1.TemplateManager(this.foundryRoot, this.options.suiteDir, this.options.suiteNameSnake, this.options.suiteNamePascal);
|
|
475
|
+
await tm.generateTemplates(filteredContracts, reconConfig);
|
|
476
|
+
}
|
|
477
|
+
async listAvailable() {
|
|
478
|
+
var _a;
|
|
479
|
+
await this.ensureFoundryConfigExists();
|
|
480
|
+
await this.ensureBuild();
|
|
481
|
+
const contracts = await this.findSourceContracts();
|
|
482
|
+
const filtered = [];
|
|
483
|
+
for (const contract of contracts) {
|
|
484
|
+
if (!this.isContractAllowed(contract.name)) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
const fns = [];
|
|
488
|
+
for (const item of contract.abi) {
|
|
489
|
+
if (item.type === 'function' && !((_a = item.stateMutability) === null || _a === void 0 ? void 0 : _a.match(/^(view|pure)$/))) {
|
|
490
|
+
const signature = `${item.name}(${item.inputs.map((i) => this.paramType(i)).join(',')})`;
|
|
491
|
+
if (this.isFunctionIncluded(contract.name, signature)) {
|
|
492
|
+
fns.push(signature);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (fns.length > 0) {
|
|
497
|
+
filtered.push({ name: contract.name, path: contract.path, functions: fns });
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
console.log('Available contracts and functions (after filters):');
|
|
501
|
+
for (const c of filtered) {
|
|
502
|
+
console.log(`- ${c.name} :: ${c.path}`);
|
|
503
|
+
for (const fn of c.functions) {
|
|
504
|
+
console.log(` • ${fn}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (filtered.length === 0) {
|
|
508
|
+
console.log('No contracts/functions match the current include/exclude filters.');
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
exports.ReconGenerator = ReconGenerator;
|
package/dist/index.d.ts
ADDED