@diamondslab/diamonds-hardhat-foundry 1.0.0
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/CHANGELOG.md +66 -0
- package/LICENSE +21 -0
- package/README.md +493 -0
- package/contracts/DiamondForgeHelpers.sol +96 -0
- package/contracts/DiamondFuzzBase.sol +128 -0
- package/package.json +78 -0
- package/src/foundry.ts +134 -0
- package/src/framework/DeploymentManager.ts +210 -0
- package/src/framework/ForgeFuzzingFramework.ts +194 -0
- package/src/framework/HelperGenerator.ts +246 -0
- package/src/index.ts +166 -0
- package/src/tasks/deploy.ts +110 -0
- package/src/tasks/generate-helpers.ts +101 -0
- package/src/tasks/init.ts +90 -0
- package/src/tasks/test.ts +108 -0
- package/src/templates/DiamondDeployment.sol.template +38 -0
- package/src/templates/ExampleFuzzTest.t.sol.template +109 -0
- package/src/templates/ExampleIntegrationTest.t.sol.template +79 -0
- package/src/templates/ExampleUnitTest.t.sol.template +59 -0
- package/src/types/config.ts +54 -0
- package/src/types/hardhat.ts +24 -0
- package/src/utils/foundry.ts +189 -0
- package/src/utils/logger.ts +66 -0
- package/src/utils/validation.ts +144 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration types for diamonds-hardhat-foundry plugin
|
|
3
|
+
* Will be fully implemented in Task 2.1
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface DiamondsFoundryConfig {
|
|
7
|
+
/**
|
|
8
|
+
* Output directory for generated helpers (relative to project root)
|
|
9
|
+
* @default "test/foundry/helpers"
|
|
10
|
+
*/
|
|
11
|
+
helpersDir?: string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Whether to generate example tests on init
|
|
15
|
+
* @default true
|
|
16
|
+
*/
|
|
17
|
+
generateExamples?: boolean;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Example test templates to generate
|
|
21
|
+
* @default ["unit", "integration", "fuzz"]
|
|
22
|
+
*/
|
|
23
|
+
exampleTests?: Array<"unit" | "integration" | "fuzz">;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Default network for deployments
|
|
27
|
+
* @default "hardhat"
|
|
28
|
+
*/
|
|
29
|
+
defaultNetwork?: string;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether to reuse existing deployment or deploy fresh
|
|
33
|
+
* @default true
|
|
34
|
+
*/
|
|
35
|
+
reuseDeployment?: boolean;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Additional forge test arguments
|
|
39
|
+
* @default []
|
|
40
|
+
*/
|
|
41
|
+
forgeTestArgs?: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Default configuration values
|
|
46
|
+
*/
|
|
47
|
+
export const DEFAULT_CONFIG: Required<DiamondsFoundryConfig> = {
|
|
48
|
+
helpersDir: "test/foundry/helpers",
|
|
49
|
+
generateExamples: true,
|
|
50
|
+
exampleTests: ["unit", "integration", "fuzz"],
|
|
51
|
+
defaultNetwork: "hardhat",
|
|
52
|
+
reuseDeployment: true,
|
|
53
|
+
forgeTestArgs: [],
|
|
54
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type extensions for Hardhat Runtime Environment
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import "hardhat/types/config";
|
|
6
|
+
import "hardhat/types/runtime";
|
|
7
|
+
import { DiamondsFoundryConfig } from "../types/config";
|
|
8
|
+
|
|
9
|
+
declare module "hardhat/types/config" {
|
|
10
|
+
export interface HardhatUserConfig {
|
|
11
|
+
diamondsFoundry?: Partial<DiamondsFoundryConfig>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface HardhatConfig {
|
|
15
|
+
diamondsFoundry: Required<DiamondsFoundryConfig>;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare module "hardhat/types/runtime" {
|
|
20
|
+
export interface HardhatRuntimeEnvironment {
|
|
21
|
+
diamondsFoundry: Required<DiamondsFoundryConfig>;
|
|
22
|
+
ethers: any; // Will be provided by @nomicfoundation/hardhat-ethers
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { execSync, spawn } from "child_process";
|
|
2
|
+
import { Logger } from "./logger";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Execute a Foundry command synchronously
|
|
6
|
+
* @param command - The foundry command (e.g., "forge test")
|
|
7
|
+
* @param args - Command arguments
|
|
8
|
+
* @param options - Execution options
|
|
9
|
+
*/
|
|
10
|
+
export function execForgeSync(
|
|
11
|
+
command: string,
|
|
12
|
+
args: string[] = [],
|
|
13
|
+
options: { cwd?: string; stdio?: "inherit" | "pipe" } = {}
|
|
14
|
+
): string {
|
|
15
|
+
const fullCommand = `${command} ${args.join(" ")}`;
|
|
16
|
+
Logger.step(`Running: ${fullCommand}`);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const output = execSync(fullCommand, {
|
|
20
|
+
cwd: options.cwd || process.cwd(),
|
|
21
|
+
stdio: options.stdio || "pipe",
|
|
22
|
+
encoding: "utf-8",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return output;
|
|
26
|
+
} catch (error: any) {
|
|
27
|
+
Logger.error(`Forge command failed: ${error.message}`);
|
|
28
|
+
if (error.stdout) Logger.info(error.stdout);
|
|
29
|
+
if (error.stderr) Logger.error(error.stderr);
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Execute a Foundry command asynchronously
|
|
36
|
+
* @param command - The foundry command (e.g., "forge")
|
|
37
|
+
* @param args - Command arguments
|
|
38
|
+
* @param options - Execution options
|
|
39
|
+
*/
|
|
40
|
+
export async function execForgeAsync(
|
|
41
|
+
command: string,
|
|
42
|
+
args: string[] = [],
|
|
43
|
+
options: { cwd?: string; verbose?: boolean } = {}
|
|
44
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
45
|
+
Logger.step(`Running: ${command} ${args.join(" ")}`);
|
|
46
|
+
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const child = spawn(command, args, {
|
|
49
|
+
cwd: options.cwd || process.cwd(),
|
|
50
|
+
shell: true,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
let stdout = "";
|
|
54
|
+
let stderr = "";
|
|
55
|
+
|
|
56
|
+
child.stdout?.on("data", (data) => {
|
|
57
|
+
const text = data.toString();
|
|
58
|
+
stdout += text;
|
|
59
|
+
if (options.verbose) {
|
|
60
|
+
process.stdout.write(text);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
child.stderr?.on("data", (data) => {
|
|
65
|
+
const text = data.toString();
|
|
66
|
+
stderr += text;
|
|
67
|
+
if (options.verbose) {
|
|
68
|
+
process.stderr.write(text);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
child.on("close", (code) => {
|
|
73
|
+
resolve({ stdout, stderr, exitCode: code || 0 });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
child.on("error", (error) => {
|
|
77
|
+
Logger.error(`Failed to spawn ${command}: ${error.message}`);
|
|
78
|
+
reject(error);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Run forge test with specified options
|
|
85
|
+
* @param options - Test execution options
|
|
86
|
+
*/
|
|
87
|
+
export async function runForgeTest(options: {
|
|
88
|
+
matchTest?: string;
|
|
89
|
+
matchContract?: string;
|
|
90
|
+
verbosity?: number;
|
|
91
|
+
gasReport?: boolean;
|
|
92
|
+
cwd?: string;
|
|
93
|
+
env?: Record<string, string>;
|
|
94
|
+
}): Promise<{ success: boolean; output: string }> {
|
|
95
|
+
const args: string[] = ["test"];
|
|
96
|
+
|
|
97
|
+
if (options.matchTest) {
|
|
98
|
+
args.push("--match-test", options.matchTest);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (options.matchContract) {
|
|
102
|
+
args.push("--match-contract", options.matchContract);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (options.verbosity) {
|
|
106
|
+
args.push("-" + "v".repeat(options.verbosity));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (options.gasReport) {
|
|
110
|
+
args.push("--gas-report");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const result = await execForgeAsync("forge", args, {
|
|
115
|
+
cwd: options.cwd,
|
|
116
|
+
verbose: true,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const success = result.exitCode === 0;
|
|
120
|
+
|
|
121
|
+
if (success) {
|
|
122
|
+
Logger.success("Forge tests passed!");
|
|
123
|
+
} else {
|
|
124
|
+
Logger.error("Forge tests failed");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { success, output: result.stdout + result.stderr };
|
|
128
|
+
} catch (error: any) {
|
|
129
|
+
Logger.error(`Test execution failed: ${error.message}`);
|
|
130
|
+
return { success: false, output: error.message };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Compile Forge contracts
|
|
136
|
+
* @param options - Compilation options
|
|
137
|
+
*/
|
|
138
|
+
export async function compileForge(options: {
|
|
139
|
+
cwd?: string;
|
|
140
|
+
verbose?: boolean;
|
|
141
|
+
}): Promise<{ success: boolean; output: string }> {
|
|
142
|
+
Logger.step("Compiling Forge contracts...");
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const result = await execForgeAsync("forge", ["build"], {
|
|
146
|
+
cwd: options.cwd,
|
|
147
|
+
verbose: options.verbose,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const success = result.exitCode === 0;
|
|
151
|
+
|
|
152
|
+
if (success) {
|
|
153
|
+
Logger.success("Forge compilation successful!");
|
|
154
|
+
} else {
|
|
155
|
+
Logger.error("Forge compilation failed");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { success, output: result.stdout + result.stderr };
|
|
159
|
+
} catch (error: any) {
|
|
160
|
+
Logger.error(`Compilation failed: ${error.message}`);
|
|
161
|
+
return { success: false, output: error.message };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check if Foundry is installed
|
|
167
|
+
*/
|
|
168
|
+
export function isFoundryInstalled(): boolean {
|
|
169
|
+
try {
|
|
170
|
+
execSync("forge --version", { stdio: "pipe" });
|
|
171
|
+
return true;
|
|
172
|
+
} catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get Foundry version
|
|
179
|
+
*/
|
|
180
|
+
export function getFoundryVersion(): string | null {
|
|
181
|
+
try {
|
|
182
|
+
const output = execSync("forge --version", { encoding: "utf-8", stdio: "pipe" });
|
|
183
|
+
// Extract version from output like "forge 0.2.0 (abc123 2024-01-01T00:00:00.000000000Z)"
|
|
184
|
+
const match = output.match(/forge\s+([\d.]+)/);
|
|
185
|
+
return match ? match[1] : null;
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import picocolors from "picocolors";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Logger utility for colored console output
|
|
5
|
+
*/
|
|
6
|
+
export class Logger {
|
|
7
|
+
/**
|
|
8
|
+
* Log an info message in cyan
|
|
9
|
+
*/
|
|
10
|
+
static info(message: string): void {
|
|
11
|
+
console.log(picocolors.cyan(`ℹ ${message}`));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Log a success message in green
|
|
16
|
+
*/
|
|
17
|
+
static success(message: string): void {
|
|
18
|
+
console.log(picocolors.green(`✓ ${message}`));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Log a warning message in yellow
|
|
23
|
+
*/
|
|
24
|
+
static warn(message: string): void {
|
|
25
|
+
console.log(picocolors.yellow(`⚠ ${message}`));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Log an error message in red
|
|
30
|
+
*/
|
|
31
|
+
static error(message: string): void {
|
|
32
|
+
console.log(picocolors.red(`✗ ${message}`));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Log a step message in blue
|
|
37
|
+
*/
|
|
38
|
+
static step(message: string): void {
|
|
39
|
+
console.log(picocolors.blue(`→ ${message}`));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Log a verbose/debug message in gray
|
|
44
|
+
*/
|
|
45
|
+
static debug(message: string): void {
|
|
46
|
+
console.log(picocolors.gray(` ${message}`));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Log a highlighted message in magenta
|
|
51
|
+
*/
|
|
52
|
+
static highlight(message: string): void {
|
|
53
|
+
console.log(picocolors.magenta(`★ ${message}`));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a formatted section header
|
|
58
|
+
*/
|
|
59
|
+
static section(title: string): void {
|
|
60
|
+
console.log("");
|
|
61
|
+
console.log(picocolors.bold(picocolors.cyan(`${"=".repeat(60)}`)));
|
|
62
|
+
console.log(picocolors.bold(picocolors.cyan(` ${title}`)));
|
|
63
|
+
console.log(picocolors.bold(picocolors.cyan(`${"=".repeat(60)}`)));
|
|
64
|
+
console.log("");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { DEFAULT_CONFIG, DiamondsFoundryConfig } from "../types/config";
|
|
4
|
+
import { Logger } from "./logger";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validation utilities for diamonds-hardhat-foundry plugin
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if Foundry is installed
|
|
12
|
+
*/
|
|
13
|
+
export function validateFoundryInstallation(): boolean {
|
|
14
|
+
try {
|
|
15
|
+
execSync("forge --version", { stdio: "ignore" });
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if Foundry is installed and throw error if not
|
|
24
|
+
*/
|
|
25
|
+
export function requireFoundry(): void {
|
|
26
|
+
if (!validateFoundryInstallation()) {
|
|
27
|
+
Logger.error("Foundry is not installed or not in PATH");
|
|
28
|
+
Logger.info("Install Foundry from: https://getfoundry.sh/");
|
|
29
|
+
throw new Error("Foundry is required but not found");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if a peer dependency is installed
|
|
35
|
+
*/
|
|
36
|
+
export function validatePeerDependency(packageName: string): boolean {
|
|
37
|
+
try {
|
|
38
|
+
require.resolve(packageName);
|
|
39
|
+
return true;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if required peer dependencies are installed
|
|
47
|
+
*/
|
|
48
|
+
export function validatePeerDependencies(): void {
|
|
49
|
+
const requiredDeps = [
|
|
50
|
+
"@diamondslab/diamonds",
|
|
51
|
+
"@diamondslab/hardhat-diamonds",
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const missing: string[] = [];
|
|
55
|
+
|
|
56
|
+
for (const dep of requiredDeps) {
|
|
57
|
+
if (!validatePeerDependency(dep)) {
|
|
58
|
+
missing.push(dep);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (missing.length > 0) {
|
|
63
|
+
Logger.error("Missing required peer dependencies:");
|
|
64
|
+
missing.forEach((dep) => Logger.error(` - ${dep}`));
|
|
65
|
+
Logger.info("\nInstall them with:");
|
|
66
|
+
Logger.info(` npm install ${missing.join(" ")}`);
|
|
67
|
+
throw new Error("Missing peer dependencies");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Validate and merge user config with defaults
|
|
73
|
+
*/
|
|
74
|
+
export function validateConfig(
|
|
75
|
+
userConfig?: Partial<DiamondsFoundryConfig>
|
|
76
|
+
): Required<DiamondsFoundryConfig> {
|
|
77
|
+
const config: Required<DiamondsFoundryConfig> = {
|
|
78
|
+
...DEFAULT_CONFIG,
|
|
79
|
+
...userConfig,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Validate helpersDir is a string
|
|
83
|
+
if (typeof config.helpersDir !== "string") {
|
|
84
|
+
throw new Error("diamondsFoundry.helpersDir must be a string");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Validate generateExamples is boolean
|
|
88
|
+
if (typeof config.generateExamples !== "boolean") {
|
|
89
|
+
throw new Error("diamondsFoundry.generateExamples must be a boolean");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Validate exampleTests is array
|
|
93
|
+
if (!Array.isArray(config.exampleTests)) {
|
|
94
|
+
throw new Error("diamondsFoundry.exampleTests must be an array");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Validate exampleTests values
|
|
98
|
+
const validTests = ["unit", "integration", "fuzz"];
|
|
99
|
+
for (const test of config.exampleTests) {
|
|
100
|
+
if (!validTests.includes(test)) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Invalid exampleTests value: ${test}. Must be one of: ${validTests.join(", ")}`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Validate defaultNetwork is string
|
|
108
|
+
if (typeof config.defaultNetwork !== "string") {
|
|
109
|
+
throw new Error("diamondsFoundry.defaultNetwork must be a string");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Validate reuseDeployment is boolean
|
|
113
|
+
if (typeof config.reuseDeployment !== "boolean") {
|
|
114
|
+
throw new Error("diamondsFoundry.reuseDeployment must be a boolean");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Validate forgeTestArgs is array
|
|
118
|
+
if (!Array.isArray(config.forgeTestArgs)) {
|
|
119
|
+
throw new Error("diamondsFoundry.forgeTestArgs must be an array");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return config;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check if a directory exists
|
|
127
|
+
*/
|
|
128
|
+
export function validateDirectoryExists(path: string): boolean {
|
|
129
|
+
return existsSync(path);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Validate output directory doesn't have conflicts
|
|
134
|
+
*/
|
|
135
|
+
export function validateOutputDirectory(
|
|
136
|
+
path: string,
|
|
137
|
+
force: boolean = false
|
|
138
|
+
): void {
|
|
139
|
+
if (!force && validateDirectoryExists(path)) {
|
|
140
|
+
Logger.warn(`Output directory already exists: ${path}`);
|
|
141
|
+
Logger.info("Use --force to overwrite");
|
|
142
|
+
throw new Error("Output directory exists");
|
|
143
|
+
}
|
|
144
|
+
}
|