create-fhevm-example 1.4.5 → 1.4.6

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.
@@ -58,7 +58,7 @@ const utils_1 = require("../shared/utils");
58
58
  function detectHardhatProject(targetDir) {
59
59
  const packageJsonPath = path.join(targetDir, "package.json");
60
60
  const hardhatConfigTs = path.join(targetDir, "hardhat.config.ts");
61
- const hardhatConfigJs = path.join(targetDir, "hardhat.config");
61
+ const hardhatConfigJs = path.join(targetDir, "hardhat.config.js");
62
62
  if (!fs.existsSync(packageJsonPath)) {
63
63
  return false;
64
64
  }
@@ -106,7 +106,7 @@ function updateHardhatConfig(targetDir) {
106
106
  ? configPathJs
107
107
  : null;
108
108
  if (!actualPath) {
109
- throw new Error("hardhat.config.ts or hardhat.config.js not found");
109
+ throw new Error(utils_1.ERROR_MESSAGES.CONFIG_NOT_FOUND);
110
110
  }
111
111
  let content = fs.readFileSync(actualPath, "utf-8");
112
112
  if (content.includes("@fhevm/hardhat-plugin")) {
@@ -139,12 +139,12 @@ function updateHardhatConfig(targetDir) {
139
139
  function addExampleFiles(exampleName, targetDir) {
140
140
  const example = config_1.EXAMPLES[exampleName];
141
141
  if (!example) {
142
- throw new Error(`Unknown example: ${exampleName}`);
142
+ throw new Error(utils_1.ERROR_MESSAGES.UNKNOWN_EXAMPLE(exampleName));
143
143
  }
144
144
  const rootDir = (0, utils_1.getRootDir)();
145
145
  const contractName = (0, utils_1.getContractName)(example.contract);
146
146
  if (!contractName) {
147
- throw new Error("Could not extract contract name");
147
+ throw new Error(utils_1.ERROR_MESSAGES.CONTRACT_NAME_FAILED);
148
148
  }
149
149
  const contractSource = path.join(rootDir, example.contract);
150
150
  const testSource = path.join(rootDir, example.test);
@@ -154,15 +154,9 @@ function addExampleFiles(exampleName, targetDir) {
154
154
  if (!fs.existsSync(contractsDir)) {
155
155
  fs.mkdirSync(contractsDir, { recursive: true });
156
156
  }
157
- if (fs.existsSync(contractDest)) {
158
- // File exists - will be handled by caller with prompts
159
- fs.copyFileSync(contractSource, contractDest);
160
- p.log.success(`Overwritten: ${contractName}.sol`);
161
- }
162
- else {
163
- fs.copyFileSync(contractSource, contractDest);
164
- p.log.success(`Added: ${contractName}.sol`);
165
- }
157
+ const isContractOverwrite = fs.existsSync(contractDest);
158
+ fs.copyFileSync(contractSource, contractDest);
159
+ p.log.success(`${isContractOverwrite ? "Overwritten" : "Added"}: ${contractName}.sol`);
166
160
  // Handle test file
167
161
  const testFileName = path.basename(example.test);
168
162
  const testDest = path.join(targetDir, "test", testFileName);
@@ -170,14 +164,9 @@ function addExampleFiles(exampleName, targetDir) {
170
164
  if (!fs.existsSync(testDir)) {
171
165
  fs.mkdirSync(testDir, { recursive: true });
172
166
  }
173
- if (fs.existsSync(testDest)) {
174
- fs.copyFileSync(testSource, testDest);
175
- p.log.success(`Overwritten: ${testFileName}`);
176
- }
177
- else {
178
- fs.copyFileSync(testSource, testDest);
179
- p.log.success(`Added: ${testFileName}`);
180
- }
167
+ const isTestOverwrite = fs.existsSync(testDest);
168
+ fs.copyFileSync(testSource, testDest);
169
+ p.log.success(`${isTestOverwrite ? "Overwritten" : "Added"}: ${testFileName}`);
181
170
  // Handle contract dependencies
182
171
  if (example.dependencies) {
183
172
  p.log.message("");
@@ -162,4 +162,7 @@ async function main() {
162
162
  p.outro(picocolors_1.default.green("✅ All checks passed! You are ready to develop. 🚀"));
163
163
  }
164
164
  }
165
- main().catch(console.error);
165
+ main().catch((err) => {
166
+ console.error(err);
167
+ process.exit(1);
168
+ });
@@ -187,5 +187,8 @@ async function main() {
187
187
  }
188
188
  const isMainModule = process.argv[1]?.includes("generate-docs");
189
189
  if (isMainModule) {
190
- main().catch(console.error);
190
+ main().catch((err) => {
191
+ console.error(err);
192
+ process.exit(1);
193
+ });
191
194
  }
@@ -56,6 +56,17 @@ const builders_1 = require("./shared/builders");
56
56
  const ui_1 = require("./shared/ui");
57
57
  const add_mode_1 = require("./commands/add-mode");
58
58
  const generate_docs_1 = require("./commands/generate-docs");
59
+ // =============================================================================
60
+ // INTERACTIVE MODE
61
+ // =============================================================================
62
+ /** Helper to check if user cancelled and show message */
63
+ function checkCancel(value) {
64
+ if (p.isCancel(value)) {
65
+ p.cancel("Operation cancelled.");
66
+ return true;
67
+ }
68
+ return false;
69
+ }
59
70
  /** Prompts user to select mode and returns project configuration */
60
71
  async function getProjectConfig() {
61
72
  // Build options - docs only available in local dev mode
@@ -83,22 +94,16 @@ async function getProjectConfig() {
83
94
  message: "What would you like to create?",
84
95
  options,
85
96
  });
86
- if (p.isCancel(mode)) {
87
- p.cancel("Operation cancelled.");
97
+ if (checkCancel(mode))
88
98
  return null;
89
- }
90
99
  let name;
91
100
  if (mode === "single") {
92
101
  const category = await (0, ui_1.promptSelectCategory)();
93
- if (p.isCancel(category)) {
94
- p.cancel("Operation cancelled.");
102
+ if (checkCancel(category))
95
103
  return null;
96
- }
97
104
  const example = await (0, ui_1.promptSelectExampleFromCategory)(category);
98
- if (p.isCancel(example)) {
99
- p.cancel("Operation cancelled.");
105
+ if (checkCancel(example))
100
106
  return null;
101
- }
102
107
  name = example;
103
108
  }
104
109
  else if (mode === "docs") {
@@ -108,10 +113,8 @@ async function getProjectConfig() {
108
113
  }
109
114
  else {
110
115
  const category = await (0, ui_1.promptSelectCategoryProject)();
111
- if (p.isCancel(category)) {
112
- p.cancel("Operation cancelled.");
116
+ if (checkCancel(category))
113
117
  return null;
114
- }
115
118
  name = category;
116
119
  }
117
120
  const projectName = await p.text({
@@ -119,27 +122,21 @@ async function getProjectConfig() {
119
122
  placeholder: `my-${name}-project`,
120
123
  defaultValue: `my-${name}-project`,
121
124
  });
122
- if (p.isCancel(projectName)) {
123
- p.cancel("Operation cancelled.");
125
+ if (checkCancel(projectName))
124
126
  return null;
125
- }
126
127
  const outputDir = await p.text({
127
128
  message: "Output directory:",
128
129
  placeholder: `./${projectName}`,
129
130
  defaultValue: `./${projectName}`,
130
131
  });
131
- if (p.isCancel(outputDir)) {
132
- p.cancel("Operation cancelled.");
132
+ if (checkCancel(outputDir))
133
133
  return null;
134
- }
135
134
  const shouldInstall = await p.confirm({
136
135
  message: "Install dependencies and run tests?",
137
136
  initialValue: false,
138
137
  });
139
- if (p.isCancel(shouldInstall)) {
140
- p.cancel("Operation cancelled.");
138
+ if (checkCancel(shouldInstall))
141
139
  return null;
142
- }
143
140
  return {
144
141
  mode: mode,
145
142
  name,
@@ -205,12 +202,22 @@ async function runInteractiveMode() {
205
202
  }
206
203
  function parseArgs(args) {
207
204
  const parsed = {};
205
+ // Short flag mappings
206
+ const shortFlags = {
207
+ "-e": "example",
208
+ "-c": "category",
209
+ "-o": "output",
210
+ "-a": "add",
211
+ "-i": "install",
212
+ "-h": "help",
213
+ };
208
214
  for (let i = 0; i < args.length; i++) {
209
215
  const arg = args[i];
216
+ // Handle long flags (--example, --category, etc.)
210
217
  if (arg.startsWith("--")) {
211
218
  const key = arg.slice(2);
212
219
  const nextArg = args[i + 1];
213
- if (nextArg && !nextArg.startsWith("--")) {
220
+ if (nextArg && !nextArg.startsWith("-")) {
214
221
  parsed[key] = nextArg;
215
222
  i++;
216
223
  }
@@ -218,8 +225,20 @@ function parseArgs(args) {
218
225
  parsed[key] = true;
219
226
  }
220
227
  }
221
- else if (arg === "-h") {
222
- parsed["help"] = true;
228
+ // Handle short flags (-e, -c, -o, -a, -i, -h)
229
+ else if (arg.startsWith("-") && shortFlags[arg]) {
230
+ const key = shortFlags[arg];
231
+ const nextArg = args[i + 1];
232
+ // Flags that take values: -e, -c, -o
233
+ if (["example", "category", "output"].includes(key) &&
234
+ nextArg &&
235
+ !nextArg.startsWith("-")) {
236
+ parsed[key] = nextArg;
237
+ i++;
238
+ }
239
+ else {
240
+ parsed[key] = true;
241
+ }
223
242
  }
224
243
  }
225
244
  return parsed;
@@ -1 +1 @@
1
- {"version":3,"file":"builders.d.ts","sourceRoot":"","sources":["../../../scripts/shared/builders.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA6DH;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,IAAI,CAkDN;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,GAChB,IAAI,CAwEN;AAMD;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,YAAY,EAAE,MAAM,EAAE,EACtB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAuEf"}
1
+ {"version":3,"file":"builders.d.ts","sourceRoot":"","sources":["../../../scripts/shared/builders.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAiEH;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,IAAI,CAkDN;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,GAChB,IAAI,CAwEN;AAMD;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,YAAY,EAAE,MAAM,EAAE,EACtB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CA6Df"}
@@ -69,7 +69,8 @@ function copyDependencies(dependencies, outputDir) {
69
69
  }
70
70
  }
71
71
  /**
72
- * Initializes git repository (optional, fails silently)
72
+ * Initializes git repository (optional, logs warning if skipped)
73
+ * @returns true if git init succeeded, false otherwise
73
74
  */
74
75
  function initGitRepo(outputDir) {
75
76
  try {
@@ -77,9 +78,11 @@ function initGitRepo(outputDir) {
77
78
  cwd: outputDir,
78
79
  stdio: "ignore",
79
80
  });
81
+ return true;
80
82
  }
81
83
  catch {
82
- // Git init is optional
84
+ utils_1.log.dim(" ⚠️ Git initialization skipped (git not available)");
85
+ return false;
83
86
  }
84
87
  }
85
88
  // =============================================================================
@@ -208,18 +211,9 @@ async function createLocalTestProject(exampleNames, outputDir) {
208
211
  Object.assign(allNpmDeps, example.npmDependencies);
209
212
  }
210
213
  }
211
- // 3. Copy dependencies
212
- for (const depPath of allContractDeps) {
213
- const depFullPath = path.join(rootDir, depPath);
214
- if (fs.existsSync(depFullPath)) {
215
- const relativePath = depPath.replace(/^contracts\//, "");
216
- const depDestPath = path.join(outputDir, "contracts", relativePath);
217
- const depDestDir = path.dirname(depDestPath);
218
- if (!fs.existsSync(depDestDir)) {
219
- fs.mkdirSync(depDestDir, { recursive: true });
220
- }
221
- fs.copyFileSync(depFullPath, depDestPath);
222
- }
214
+ // 3. Copy dependencies (reuse helper function)
215
+ if (allContractDeps.size > 0) {
216
+ copyDependencies(Array.from(allContractDeps), outputDir);
223
217
  }
224
218
  // 4. Finalize project
225
219
  (0, generators_1.updateProjectPackageJson)(outputDir, "fhevm-test-project", `Testing ${exampleNames.length} examples`, Object.keys(allNpmDeps).length > 0 ? allNpmDeps : undefined);
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../scripts/shared/config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,MAAM,WAAW,aAAa;IAC5B,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,uCAAuC;IACvC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,8CAA8C;IAC9C,UAAU,EAAE,MAAM,CAAC;IACnB,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,SAAS,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClD;AAMD,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CA6RlD,CAAC;AAMF,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAoJrD,CAAC;AAMF;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAE1C;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,EAAE,CAE3C;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAElE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAEpE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE3D"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../scripts/shared/config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,MAAM,WAAW,aAAa;IAC5B,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,uCAAuC;IACvC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,8CAA8C;IAC9C,UAAU,EAAE,MAAM,CAAC;IACnB,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,SAAS,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClD;AAMD,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CA+SlD,CAAC;AAMF,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAiKrD,CAAC;AAMF;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAE1C;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,EAAE,CAE3C;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAElE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAEpE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE3D"}
@@ -152,14 +152,6 @@ exports.EXAMPLES = {
152
152
  docsOutput: "docs/concepts/fhe-access-control.md",
153
153
  title: "FHE Access Control"
154
154
  },
155
- "fhe-anti-patterns": {
156
- contract: "contracts/concepts/FHEAntiPatterns.sol",
157
- test: "test/concepts/FHEAntiPatterns.ts",
158
- description: "Comprehensive guide to FHE anti-patterns and their solutions. Covers 9 critical mistakes: using if/else on encrypted values, incorrect permission patterns, require() statements that leak info, unbounded loops, noise accumulation, and deprecated APIs. Each pattern shows both ❌ WRONG and ✅ CORRECT implementations.",
159
- category: "Concepts",
160
- docsOutput: "docs/concepts/fhe-anti-patterns.md",
161
- title: "FHE Anti Patterns"
162
- },
163
155
  "fhe-handles": {
164
156
  contract: "contracts/concepts/FHEHandles.sol",
165
157
  test: "test/concepts/FHEHandles.ts",
@@ -176,6 +168,30 @@ exports.EXAMPLES = {
176
168
  docsOutput: "docs/concepts/fhe-input-proof.md",
177
169
  title: "FHE Input Proof"
178
170
  },
171
+ "control-flow": {
172
+ contract: "contracts/concepts/antipatterns/ControlFlow.sol",
173
+ test: "test/concepts/antipatterns/ControlFlow.ts",
174
+ description: "Control flow anti-patterns in FHE development. Demonstrates common mistakes when using conditional logic and loops with encrypted values.",
175
+ category: "Concepts - Antipatterns",
176
+ docsOutput: "docs/concepts/antipatterns/control-flow.md",
177
+ title: "Control Flow"
178
+ },
179
+ "operations-gas-noise": {
180
+ contract: "contracts/concepts/antipatterns/OperationsGasNoise.sol",
181
+ test: "test/concepts/antipatterns/OperationsGasNoise.ts",
182
+ description: "Operations, gas, and noise anti-patterns in FHE development. Demonstrates performance issues, side-channel leaks, and inefficient encrypted computation patterns.",
183
+ category: "Concepts - Antipatterns",
184
+ docsOutput: "docs/concepts/antipatterns/operations-gas-noise.md",
185
+ title: "Operations Gas Noise"
186
+ },
187
+ "permissions": {
188
+ contract: "contracts/concepts/antipatterns/Permissions.sol",
189
+ test: "test/concepts/antipatterns/Permissions.ts",
190
+ description: "Permission management anti-patterns in FHE development. Demonstrates common mistakes with allowThis, allow, and permission propagation across transfers and contracts.",
191
+ category: "Concepts - Antipatterns",
192
+ docsOutput: "docs/concepts/antipatterns/permissions.md",
193
+ title: "Permissions"
194
+ },
179
195
  "encrypted-lottery": {
180
196
  contract: "contracts/gaming/EncryptedLottery.sol",
181
197
  test: "test/gaming/EncryptedLottery.ts",
@@ -368,10 +384,6 @@ exports.CATEGORIES = {
368
384
  sol: "contracts/concepts/FHEAccessControl.sol",
369
385
  test: "test/concepts/FHEAccessControl.ts",
370
386
  },
371
- {
372
- sol: "contracts/concepts/FHEAntiPatterns.sol",
373
- test: "test/concepts/FHEAntiPatterns.ts",
374
- },
375
387
  {
376
388
  sol: "contracts/concepts/FHEHandles.sol",
377
389
  test: "test/concepts/FHEHandles.ts",
@@ -382,6 +394,23 @@ exports.CATEGORIES = {
382
394
  }
383
395
  ],
384
396
  },
397
+ conceptsantipatterns: {
398
+ name: "Concepts - Antipatterns Examples",
399
+ contracts: [
400
+ {
401
+ sol: "contracts/concepts/antipatterns/ControlFlow.sol",
402
+ test: "test/concepts/antipatterns/ControlFlow.ts",
403
+ },
404
+ {
405
+ sol: "contracts/concepts/antipatterns/OperationsGasNoise.sol",
406
+ test: "test/concepts/antipatterns/OperationsGasNoise.ts",
407
+ },
408
+ {
409
+ sol: "contracts/concepts/antipatterns/Permissions.sol",
410
+ test: "test/concepts/antipatterns/Permissions.ts",
411
+ }
412
+ ],
413
+ },
385
414
  gaming: {
386
415
  name: "Gaming Examples",
387
416
  contracts: [
@@ -108,8 +108,8 @@ exports.FHEVM_DEPENDENCIES = {
108
108
  "@fhevm/solidity": "^0.9.1",
109
109
  },
110
110
  devDependencies: {
111
- "@fhevm/hardhat-plugin": "^0.3.0-1",
112
- "@zama-fhe/relayer-sdk": "^0.3.0-5",
111
+ "@fhevm/hardhat-plugin": "^0.3.0-3",
112
+ "@zama-fhe/relayer-sdk": "^0.3.0-6",
113
113
  },
114
114
  };
115
115
  exports.CATEGORY_ORDER = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-fhevm-example",
3
- "version": "1.4.5",
3
+ "version": "1.4.6",
4
4
  "description": "Create FHEVM example projects with a single command - A comprehensive toolkit for building privacy-preserving smart contracts",
5
5
  "bin": {
6
6
  "create-fhevm-example": "./dist/scripts/index.js"
@@ -26,7 +26,8 @@ describe("FHEAntiPatterns", function () {
26
26
 
27
27
  before(async function () {
28
28
  if (!hre.fhevm.isMock) {
29
- throw new Error(`This hardhat test suite cannot run on Sepolia Testnet`);
29
+ console.warn(`This hardhat test suite cannot run on Sepolia Testnet`);
30
+ this.skip();
30
31
  }
31
32
  const ethSigners: HardhatEthersSigner[] = await ethers.getSigners();
32
33
  signers = { owner: ethSigners[0], alice: ethSigners[1] };
@@ -0,0 +1,125 @@
1
+ import {
2
+ FhevmType,
3
+ HardhatFhevmRuntimeEnvironment,
4
+ } from "@fhevm/hardhat-plugin";
5
+ import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
6
+ import { expect } from "chai";
7
+ import { ethers } from "hardhat";
8
+ import * as hre from "hardhat";
9
+
10
+ import {
11
+ FHEControlFlowAntiPatterns,
12
+ FHEControlFlowAntiPatterns__factory,
13
+ } from "../types";
14
+ import type { Signers } from "./types";
15
+
16
+ async function deployFixture() {
17
+ const factory = (await ethers.getContractFactory(
18
+ "FHEControlFlowAntiPatterns"
19
+ )) as FHEControlFlowAntiPatterns__factory;
20
+ const contract = (await factory.deploy()) as FHEControlFlowAntiPatterns;
21
+ const contractAddress = await contract.getAddress();
22
+ return { contract, contractAddress };
23
+ }
24
+
25
+ /**
26
+ * @notice Tests for FHE control flow anti-patterns
27
+ * Demonstrates wrong and correct patterns for conditional logic and loops
28
+ */
29
+ describe("FHEControlFlowAntiPatterns", function () {
30
+ let contract: FHEControlFlowAntiPatterns;
31
+ let contractAddress: string;
32
+ let signers: Signers;
33
+
34
+ before(async function () {
35
+ if (!hre.fhevm.isMock) {
36
+ throw new Error(`This hardhat test suite cannot run on Sepolia Testnet`);
37
+ }
38
+ const ethSigners: HardhatEthersSigner[] = await ethers.getSigners();
39
+ signers = { owner: ethSigners[0], alice: ethSigners[1] };
40
+ });
41
+
42
+ beforeEach(async function () {
43
+ const deployment = await deployFixture();
44
+ contractAddress = deployment.contractAddress;
45
+ contract = deployment.contract;
46
+
47
+ // Initialize contract with test values
48
+ const fhevm: HardhatFhevmRuntimeEnvironment = hre.fhevm;
49
+
50
+ // Create encrypted input for balance only (threshold is fixed at 100)
51
+ const input = await fhevm
52
+ .createEncryptedInput(contractAddress, signers.alice.address)
53
+ .add32(50) // balance
54
+ .encrypt();
55
+
56
+ await contract
57
+ .connect(signers.alice)
58
+ .initialize(input.handles[0], input.inputProof);
59
+ });
60
+
61
+ describe("Pattern 1: If/Else Branching", function () {
62
+ it("should execute correctConditional without leaking information", async function () {
63
+ const fhevm: HardhatFhevmRuntimeEnvironment = hre.fhevm;
64
+
65
+ await contract.connect(signers.alice).correctConditional();
66
+
67
+ const encrypted = await contract.getBalance();
68
+ const decrypted = await fhevm.userDecryptEuint(
69
+ FhevmType.euint32,
70
+ encrypted,
71
+ contractAddress,
72
+ signers.alice
73
+ );
74
+
75
+ // Balance (50) is not above threshold (100), so no penalty applied
76
+ expect(decrypted).to.equal(50);
77
+ });
78
+
79
+ it("wrongBranching should return placeholder value", async function () {
80
+ const result = await contract.wrongBranching.staticCall();
81
+ expect(result).to.equal(0);
82
+ });
83
+ });
84
+
85
+ describe("Pattern 2: Require/Revert", function () {
86
+ it("should return encrypted boolean for validation", async function () {
87
+ const fhevm: HardhatFhevmRuntimeEnvironment = hre.fhevm;
88
+
89
+ // Execute validation (stores result)
90
+ await contract.connect(signers.alice).correctValidation();
91
+
92
+ // Get result via getter
93
+ const encrypted = await contract.getValidationResult();
94
+ const decrypted = await fhevm.userDecryptEbool(
95
+ encrypted,
96
+ contractAddress,
97
+ signers.alice
98
+ );
99
+
100
+ // Balance (50) < 100, so should be false
101
+ expect(decrypted).to.equal(false);
102
+ });
103
+
104
+ it("wrongRequire should return explanation string", async function () {
105
+ const result = await contract.wrongRequire();
106
+ expect(result).to.include("doesn't work with encrypted values");
107
+ });
108
+ });
109
+
110
+ describe("Pattern 3: Encrypted Loop Iterations", function () {
111
+ it("should use fixed iterations with FHE.select", async function () {
112
+ const fhevm: HardhatFhevmRuntimeEnvironment = hre.fhevm;
113
+
114
+ await contract.connect(signers.alice).correctFixedIterations();
115
+
116
+ // Result should count up to min(balance, MAX_ITERATIONS)
117
+ // Balance is 50, MAX_ITERATIONS is 5, so result should be 5
118
+ });
119
+
120
+ it("wrongEncryptedLoop should return explanation string", async function () {
121
+ const result = await contract.wrongEncryptedLoop();
122
+ expect(result).to.include("Loop iterations leak");
123
+ });
124
+ });
125
+ });