recon-generate 0.0.2 → 0.0.4
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 +5 -0
- package/dist/generator.d.ts +3 -0
- package/dist/generator.js +50 -13
- package/dist/index.js +5 -0
- package/dist/templateManager.d.ts +2 -1
- package/dist/templateManager.js +8 -2
- package/dist/templates/handlebars-helpers.js +14 -5
- package/dist/templates/setup.js +15 -0
- package/dist/templates/target-functions.js +13 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ Key options:
|
|
|
31
31
|
- `--exclude "A:{foo(uint256)},D"` — exclude these contracts/functions.
|
|
32
32
|
- `--admin "A:{fnSig,fnSig}"` — mark listed functions as admin (`asAdmin`).
|
|
33
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
|
+
- `--dynamic-deploy "Foo,Bar"` — keep dynamic lists for the listed contracts: deploys one instance, tracks addresses in an array, exposes `_getRandom<Contract>` and `switch<Contract>(entropy)` helpers to rotate among deployed instances.
|
|
34
35
|
- `--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
36
|
- `--force` — replace existing generated suite output under `--output` (does **not** rebuild `.recon/out`).
|
|
36
37
|
- `--force-build` — delete `.recon/out` to force a fresh compile before generation.
|
|
@@ -63,6 +64,9 @@ recon-generate --name foo --force
|
|
|
63
64
|
|
|
64
65
|
# Generate mocks and include them in targets
|
|
65
66
|
recon-generate --mock "Morpho,Other" --force
|
|
67
|
+
|
|
68
|
+
# Enable dynamic deploy helpers for Foo and Bar
|
|
69
|
+
recon-generate --dynamic-deploy "Foo,Bar" --force
|
|
66
70
|
```
|
|
67
71
|
|
|
68
72
|
### Behavior
|
|
@@ -71,6 +75,7 @@ recon-generate --mock "Morpho,Other" --force
|
|
|
71
75
|
- `--force` replaces the suite output folder; it does **not** rebuild `.recon/out`.
|
|
72
76
|
- `--force-build` deletes `.recon/out` to force a fresh compile before generation.
|
|
73
77
|
- 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.
|
|
78
|
+
- When `--dynamic-deploy` is used, generated `Setup.sol` keeps an `address[]` per listed contract, pushes the first deployment, and emits `_getRandom<Contract>` helper plus `switch<Contract>(entropy)` in `TargetFunctions.sol` to retarget to a previously deployed instance.
|
|
74
79
|
- `recon.json` is regenerated every run from current filters/contracts.
|
|
75
80
|
- Include/exclude/admin matching is tolerant of fully qualified struct names vs short names (e.g., `Morpho.MarketParams` vs `MarketParams`).
|
|
76
81
|
- Admin list sets `actor: admin` for matched functions; others default to `actor`.
|
package/dist/generator.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface GeneratorOptions {
|
|
|
13
13
|
forceBuild?: boolean;
|
|
14
14
|
admin?: Map<string, Set<string>>;
|
|
15
15
|
mocks?: Set<string>;
|
|
16
|
+
dynamicDeploy?: Set<string>;
|
|
16
17
|
suiteNameSnake: string;
|
|
17
18
|
suiteNamePascal: string;
|
|
18
19
|
}
|
|
@@ -21,6 +22,7 @@ export declare class ReconGenerator {
|
|
|
21
22
|
private options;
|
|
22
23
|
private generatedMocks;
|
|
23
24
|
private allowedMockNames;
|
|
25
|
+
private contractKindCache;
|
|
24
26
|
constructor(foundryRoot: string, options: GeneratorOptions);
|
|
25
27
|
private logDebug;
|
|
26
28
|
private outDir;
|
|
@@ -33,6 +35,7 @@ export declare class ReconGenerator {
|
|
|
33
35
|
private installSetupHelpers;
|
|
34
36
|
private updateRemappings;
|
|
35
37
|
private updateGitignore;
|
|
38
|
+
private getContractKind;
|
|
36
39
|
private findSourceContracts;
|
|
37
40
|
private matchesSignatureOrName;
|
|
38
41
|
private paramType;
|
package/dist/generator.js
CHANGED
|
@@ -41,6 +41,7 @@ const child_process_1 = require("child_process");
|
|
|
41
41
|
const fs = __importStar(require("fs/promises"));
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
43
|
const abi_to_mock_1 = __importDefault(require("abi-to-mock"));
|
|
44
|
+
const parser_1 = __importDefault(require("@solidity-parser/parser"));
|
|
44
45
|
const templateManager_1 = require("./templateManager");
|
|
45
46
|
const types_1 = require("./types");
|
|
46
47
|
const utils_1 = require("./utils");
|
|
@@ -50,6 +51,7 @@ class ReconGenerator {
|
|
|
50
51
|
this.options = options;
|
|
51
52
|
this.generatedMocks = new Set();
|
|
52
53
|
this.allowedMockNames = new Set();
|
|
54
|
+
this.contractKindCache = new Map();
|
|
53
55
|
}
|
|
54
56
|
logDebug(message, obj) {
|
|
55
57
|
if (!this.options.debug)
|
|
@@ -153,6 +155,45 @@ class ReconGenerator {
|
|
|
153
155
|
await fs.writeFile(gitignorePath, content);
|
|
154
156
|
}
|
|
155
157
|
}
|
|
158
|
+
async getContractKind(sourcePath, contractName) {
|
|
159
|
+
const absPath = path.isAbsolute(sourcePath) ? sourcePath : path.join(this.foundryRoot, sourcePath);
|
|
160
|
+
let fileCache = this.contractKindCache.get(absPath);
|
|
161
|
+
if (fileCache && fileCache.has(contractName)) {
|
|
162
|
+
return fileCache.get(contractName);
|
|
163
|
+
}
|
|
164
|
+
let content;
|
|
165
|
+
try {
|
|
166
|
+
content = await fs.readFile(absPath, 'utf8');
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
this.logDebug('Failed to read source when determining contract kind', { sourcePath, error: String(e) });
|
|
170
|
+
return 'unknown';
|
|
171
|
+
}
|
|
172
|
+
let ast;
|
|
173
|
+
try {
|
|
174
|
+
ast = parser_1.default.parse(content, { tolerant: true });
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
this.logDebug('Failed to parse source when determining contract kind', { sourcePath, error: String(e) });
|
|
178
|
+
return 'unknown';
|
|
179
|
+
}
|
|
180
|
+
fileCache = fileCache !== null && fileCache !== void 0 ? fileCache : new Map();
|
|
181
|
+
let kind = 'unknown';
|
|
182
|
+
const children = ast === null || ast === void 0 ? void 0 : ast.children;
|
|
183
|
+
if (Array.isArray(children)) {
|
|
184
|
+
for (const node of children) {
|
|
185
|
+
if ((node === null || node === void 0 ? void 0 : node.type) === 'ContractDefinition' && node.name === contractName) {
|
|
186
|
+
if (node.kind === 'contract' || node.kind === 'interface' || node.kind === 'library') {
|
|
187
|
+
kind = node.kind;
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
fileCache.set(contractName, kind);
|
|
194
|
+
this.contractKindCache.set(absPath, fileCache);
|
|
195
|
+
return kind;
|
|
196
|
+
}
|
|
156
197
|
async findSourceContracts() {
|
|
157
198
|
var _a;
|
|
158
199
|
const contracts = [];
|
|
@@ -181,18 +222,14 @@ class ReconGenerator {
|
|
|
181
222
|
for (const [sourcePath, contractName] of Object.entries(metadata.settings.compilationTarget)) {
|
|
182
223
|
const lowerPath = sourcePath.toLowerCase();
|
|
183
224
|
const lowerName = String(contractName).toLowerCase();
|
|
184
|
-
// Skip interfaces, libraries, and mocks by convention, unless explicitly allowed (generated mocks)
|
|
185
225
|
const isGeneratedMock = this.generatedMocks.has(String(contractName));
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
lowerName.includes('mock')) {
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
226
|
+
const contractKind = await this.getContractKind(sourcePath, String(contractName));
|
|
227
|
+
if (contractKind === 'interface' || contractKind === 'library') {
|
|
228
|
+
this.logDebug('Skipping non-contract artifact', { contractName, sourcePath, contractKind });
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (!isGeneratedMock && (lowerPath.includes('/mock') || lowerName.includes('mock'))) {
|
|
232
|
+
continue;
|
|
196
233
|
}
|
|
197
234
|
const relativeJson = path.relative(this.foundryRoot, filePath);
|
|
198
235
|
let displayedPath = sourcePath;
|
|
@@ -427,7 +464,7 @@ class ReconGenerator {
|
|
|
427
464
|
return config;
|
|
428
465
|
}
|
|
429
466
|
async run() {
|
|
430
|
-
var _a;
|
|
467
|
+
var _a, _b;
|
|
431
468
|
await this.ensureFoundryConfigExists();
|
|
432
469
|
const outPath = this.outDir();
|
|
433
470
|
const mockTargets = (_a = this.options.mocks) !== null && _a !== void 0 ? _a : new Set();
|
|
@@ -471,7 +508,7 @@ class ReconGenerator {
|
|
|
471
508
|
this.logDebug('No contracts left after include/exclude filters');
|
|
472
509
|
}
|
|
473
510
|
const reconConfig = await this.loadReconConfig(filteredContracts);
|
|
474
|
-
const tm = new templateManager_1.TemplateManager(this.foundryRoot, this.options.suiteDir, this.options.suiteNameSnake, this.options.suiteNamePascal);
|
|
511
|
+
const tm = new templateManager_1.TemplateManager(this.foundryRoot, this.options.suiteDir, this.options.suiteNameSnake, this.options.suiteNamePascal, (_b = this.options.dynamicDeploy) !== null && _b !== void 0 ? _b : new Set());
|
|
475
512
|
await tm.generateTemplates(filteredContracts, reconConfig);
|
|
476
513
|
}
|
|
477
514
|
async listAvailable() {
|
package/dist/index.js
CHANGED
|
@@ -114,6 +114,7 @@ async function main() {
|
|
|
114
114
|
.option('--admin <spec>', 'Mark functions as admin: e.g. "A:{foo(uint256),bar()},B:{baz(address)}"')
|
|
115
115
|
.option('--name <suite>', 'Suite name; affects folder (recon-name) and Crytic artifacts')
|
|
116
116
|
.option('--mock <names>', 'Comma-separated contract names to generate mocks for')
|
|
117
|
+
.option('--dynamic-deploy <names>', 'Comma-separated contract names to enable dynamic deploy lists')
|
|
117
118
|
.option('--debug', 'Print debug info about filters and selection')
|
|
118
119
|
.option('--list', 'List available contracts/functions (after filters) and exit')
|
|
119
120
|
.option('--force', 'Replace existing generated suite output (under --output). Does not rebuild .recon/out.')
|
|
@@ -130,6 +131,9 @@ async function main() {
|
|
|
130
131
|
const mockSet = opts.mock
|
|
131
132
|
? new Set(String(opts.mock).split(',').map((s) => s.trim()).filter(Boolean))
|
|
132
133
|
: undefined;
|
|
134
|
+
const dynamicDeploySet = opts.dynamicDeploy
|
|
135
|
+
? new Set(String(opts.dynamicDeploy).split(',').map((s) => s.trim()).filter(Boolean))
|
|
136
|
+
: undefined;
|
|
133
137
|
const suiteRaw = opts.name ? String(opts.name).trim() : '';
|
|
134
138
|
const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
|
|
135
139
|
const suitePascal = suiteRaw ? (0, case_1.pascal)(suiteRaw) : '';
|
|
@@ -154,6 +158,7 @@ async function main() {
|
|
|
154
158
|
force: !!opts.force,
|
|
155
159
|
forceBuild: !!opts.forceBuild,
|
|
156
160
|
mocks: mockSet,
|
|
161
|
+
dynamicDeploy: dynamicDeploySet,
|
|
157
162
|
suiteNameSnake: suiteSnake,
|
|
158
163
|
suiteNamePascal: suitePascal,
|
|
159
164
|
});
|
|
@@ -4,7 +4,8 @@ export declare class TemplateManager {
|
|
|
4
4
|
private suiteDir;
|
|
5
5
|
private suiteNameSnake;
|
|
6
6
|
private suiteNamePascal;
|
|
7
|
-
|
|
7
|
+
private dynamicDeploy;
|
|
8
|
+
constructor(foundryRoot: string, suiteDir: string, suiteNameSnake: string, suiteNamePascal: string, dynamicDeploy?: Set<string>);
|
|
8
9
|
private skipList;
|
|
9
10
|
private shouldGenerateFile;
|
|
10
11
|
private updateTargetFile;
|
package/dist/templateManager.js
CHANGED
|
@@ -39,11 +39,12 @@ const fs = __importStar(require("fs/promises"));
|
|
|
39
39
|
const templates = __importStar(require("./templates"));
|
|
40
40
|
const types_1 = require("./types");
|
|
41
41
|
class TemplateManager {
|
|
42
|
-
constructor(foundryRoot, suiteDir, suiteNameSnake, suiteNamePascal) {
|
|
42
|
+
constructor(foundryRoot, suiteDir, suiteNameSnake, suiteNamePascal, dynamicDeploy = new Set()) {
|
|
43
43
|
this.foundryRoot = foundryRoot;
|
|
44
44
|
this.suiteDir = suiteDir;
|
|
45
45
|
this.suiteNameSnake = suiteNameSnake;
|
|
46
46
|
this.suiteNamePascal = suiteNamePascal;
|
|
47
|
+
this.dynamicDeploy = dynamicDeploy;
|
|
47
48
|
}
|
|
48
49
|
skipList() {
|
|
49
50
|
const suiteRel = path.relative(this.foundryRoot, this.suiteDir);
|
|
@@ -180,6 +181,10 @@ class TemplateManager {
|
|
|
180
181
|
}
|
|
181
182
|
async generateTemplates(contracts, reconConfig) {
|
|
182
183
|
const { enabledContracts, adminFunctions, nonSeparatedFunctions, separatedByContract, allContractNames, } = this.collectFunctions(contracts, reconConfig);
|
|
184
|
+
const contractsForSetup = enabledContracts.map(contract => ({ ...contract, isDynamic: this.dynamicDeploy.has(contract.name) }));
|
|
185
|
+
const dynamicContracts = enabledContracts
|
|
186
|
+
.filter(contract => this.dynamicDeploy.has(contract.name))
|
|
187
|
+
.map(c => ({ name: c.name, path: c.path }));
|
|
183
188
|
const suiteRel = path.relative(this.foundryRoot, this.suiteDir);
|
|
184
189
|
const suffix = this.suiteNameSnake ? `-${this.suiteNameSnake}` : '';
|
|
185
190
|
const cryticTesterName = `CryticTester${this.suiteNamePascal}`;
|
|
@@ -193,10 +198,11 @@ class TemplateManager {
|
|
|
193
198
|
[path.join(suiteRel, `${cryticTesterName}.sol`)]: templates.cryticTesterTemplate({ cryticTesterName, suiteNameSuffix: suffix }),
|
|
194
199
|
[path.join(suiteRel, `${cryticToFoundryName}.sol`)]: templates.cryticToFoundryTemplate({ cryticToFoundryName }),
|
|
195
200
|
[path.join(suiteRel, 'Properties.sol')]: templates.propertiesTemplate({}),
|
|
196
|
-
[path.join(suiteRel, 'Setup.sol')]: templates.setupTemplate({ contracts:
|
|
201
|
+
[path.join(suiteRel, 'Setup.sol')]: templates.setupTemplate({ contracts: contractsForSetup }),
|
|
197
202
|
[path.join(suiteRel, 'TargetFunctions.sol')]: templates.targetFunctionsTemplate({
|
|
198
203
|
functions: nonSeparatedFunctions,
|
|
199
204
|
contracts: allContractNames,
|
|
205
|
+
dynamicContracts,
|
|
200
206
|
}),
|
|
201
207
|
[path.join(suiteTargetsDir, 'AdminTargets.sol')]: templates.adminTargetsTemplate({ functions: adminFunctions }),
|
|
202
208
|
[path.join(suiteTargetsDir, 'DoomsdayTargets.sol')]: templates.doomsdayTargetsTemplate({}),
|
|
@@ -86,13 +86,22 @@ function registerHelpers(handlebars) {
|
|
|
86
86
|
: `tempValue${index}`};`)
|
|
87
87
|
.join("\n ")
|
|
88
88
|
: "";
|
|
89
|
+
const inputNames = abi.inputs.map((input, index) => {
|
|
90
|
+
if (input.name && input.name.trim() !== "") {
|
|
91
|
+
return input.name;
|
|
92
|
+
}
|
|
93
|
+
if (input.type === "tuple") {
|
|
94
|
+
return `empty${index}`;
|
|
95
|
+
}
|
|
96
|
+
return `arg${index}`;
|
|
97
|
+
});
|
|
89
98
|
if (mode === types_1.Mode.NORMAL || mode === types_1.Mode.FAIL) {
|
|
90
99
|
return `
|
|
91
100
|
function ${contractName}_${abi.name}(${abi.inputs
|
|
92
|
-
.map((input) => `${conditionallyAddMemoryLocation(input.type, extractType(input))} ${input
|
|
101
|
+
.map((input) => `${conditionallyAddMemoryLocation(input.type, extractType(input))} ${inputNames[abi.inputs.indexOf(input)]}`)
|
|
93
102
|
.join(", ")}) public ${modifiersStr}{
|
|
94
103
|
${contractName}.${abi.name}${valueStr}(${abi.inputs
|
|
95
|
-
.map((input) =>
|
|
104
|
+
.map((input, index) => inputNames[index] || getDefaultValue(input.type))
|
|
96
105
|
.join(", ")});${mode === 'fail'
|
|
97
106
|
? `
|
|
98
107
|
t(false, "${contractName}_${abi.name}");`
|
|
@@ -102,16 +111,16 @@ function registerHelpers(handlebars) {
|
|
|
102
111
|
else {
|
|
103
112
|
return `
|
|
104
113
|
function ${contractName}_${abi.name}(${abi.inputs
|
|
105
|
-
.map((input) => `${conditionallyAddMemoryLocation(input.type, extractType(input))} ${input
|
|
114
|
+
.map((input) => `${conditionallyAddMemoryLocation(input.type, extractType(input))} ${inputNames[abi.inputs.indexOf(input)]}`)
|
|
106
115
|
.join(", ")}) public ${modifiersStr}{
|
|
107
116
|
${hasOutputs ? `${outputs}
|
|
108
117
|
try ${contractName}.${abi.name}${valueStr}(${abi.inputs
|
|
109
|
-
.map((input) =>
|
|
118
|
+
.map((input, index) => inputNames[index] || getDefaultValue(input.type))
|
|
110
119
|
.join(", ")}) returns (${returnTypes}) {
|
|
111
120
|
${assignValues}
|
|
112
121
|
}`
|
|
113
122
|
: `try ${contractName}.${abi.name}(${abi.inputs
|
|
114
|
-
.map((input) =>
|
|
123
|
+
.map((input, index) => inputNames[index] || getDefaultValue(input.type))
|
|
115
124
|
.join(", ")}) {}`} catch {
|
|
116
125
|
${hasOutputs ? " " : " "}t(false, "${contractName}_${abi.name}");
|
|
117
126
|
${hasOutputs ? " " : " "}}
|
package/dist/templates/setup.js
CHANGED
|
@@ -29,6 +29,8 @@ import "{{this.path}}";
|
|
|
29
29
|
abstract contract Setup is BaseSetup, ActorManager, AssetManager, Utils {
|
|
30
30
|
{{#each contracts}}
|
|
31
31
|
{{this.name}} {{camel this.name}};
|
|
32
|
+
{{#if this.isDynamic}}address[] {{camel this.name}}_s;
|
|
33
|
+
{{/if}}
|
|
32
34
|
{{/each}}
|
|
33
35
|
|
|
34
36
|
/// === Setup === ///
|
|
@@ -36,9 +38,22 @@ abstract contract Setup is BaseSetup, ActorManager, AssetManager, Utils {
|
|
|
36
38
|
function setup() internal virtual override {
|
|
37
39
|
{{#each contracts}}
|
|
38
40
|
{{camel this.name}} = new {{this.name}}(); // TODO: Add parameters here
|
|
41
|
+
{{#if this.isDynamic}}
|
|
42
|
+
{{camel this.name}}_s.push(address({{camel this.name}}));
|
|
43
|
+
{{/if}}
|
|
39
44
|
{{/each}}
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
/// === Dynamic deploy helpers === ///
|
|
48
|
+
{{#each contracts}}
|
|
49
|
+
{{#if this.isDynamic}}
|
|
50
|
+
function _getRandom{{pascal this.name}}(uint8 index) internal view returns (address) {
|
|
51
|
+
return {{camel this.name}}_s[index % {{camel this.name}}_s.length];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
{{/if}}
|
|
55
|
+
{{/each}}
|
|
56
|
+
|
|
42
57
|
/// === MODIFIERS === ///
|
|
43
58
|
/// Prank admin and actor
|
|
44
59
|
|
|
@@ -22,6 +22,11 @@ import {Panic} from "@recon/Panic.sol";
|
|
|
22
22
|
import { {{pascal this}}Targets } from "./targets/{{pascal this}}Targets.sol";
|
|
23
23
|
{{/each}}
|
|
24
24
|
|
|
25
|
+
// Dynamic deploy contract types
|
|
26
|
+
{{#each dynamicContracts}}
|
|
27
|
+
import "{{this.path}}";
|
|
28
|
+
{{/each}}
|
|
29
|
+
|
|
25
30
|
abstract contract TargetFunctions is
|
|
26
31
|
{{#each contracts}}{{pascal this}}Targets{{#unless @last}},
|
|
27
32
|
{{/unless}}{{/each}}
|
|
@@ -32,6 +37,14 @@ abstract contract TargetFunctions is
|
|
|
32
37
|
/// AUTO GENERATED TARGET FUNCTIONS - WARNING: DO NOT DELETE OR MODIFY THIS LINE ///
|
|
33
38
|
{{#each functions}}
|
|
34
39
|
{{functionDefinition this}}
|
|
40
|
+
{{/each}}
|
|
41
|
+
|
|
42
|
+
/// AUTO GENERATED DYNAMIC DEPLOY SWITCHES ///
|
|
43
|
+
{{#each dynamicContracts}}
|
|
44
|
+
function switch{{pascal this.name}}(uint8 entropy) public {
|
|
45
|
+
{{camel this.name}} = {{this.name}}(_getRandom{{pascal this.name}}(entropy));
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
{{/each}}
|
|
36
49
|
}
|
|
37
50
|
`, { noEscape: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "recon-generate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "CLI to scaffold Recon fuzzing suite inside Foundry projects",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
"package.json"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
+
"@solidity-parser/parser": "^0.20.2",
|
|
21
22
|
"abi-to-mock": "^1.0.4",
|
|
22
23
|
"case": "^1.6.3",
|
|
23
|
-
"commander": "^
|
|
24
|
+
"commander": "^14.0.2",
|
|
24
25
|
"handlebars": "^4.7.8"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|