|
1 | | -import { beforeAll, describe, expect, it } from "vitest"; |
2 | | - |
3 | | -import { resolve } from "node:path"; |
4 | | - |
5 | | -import type { Entry } from "./types.js"; |
6 | | - |
7 | | -import { |
8 | | - extractEtherscanUrls, |
9 | | - findProtocolConstantsFiles, |
10 | | - hasEtherscanExample, |
11 | | - hasSolUrl, |
12 | | - hasSolUrlWithLineNumber, |
13 | | - parseConstantsFile, |
14 | | -} from "./utils.js"; |
| 1 | +import { describe, expect, it } from "vitest"; |
| 2 | + |
| 3 | +import { readdirSync } from "node:fs"; |
| 4 | +import { join, resolve } from "node:path"; |
| 5 | + |
| 6 | +import { FLUIDKEY_CONFIG } from "../fluidkey/constants.js"; |
| 7 | +import { HINKAL_CONFIG } from "../hinkal/constants.js"; |
| 8 | +import { INTMAX_CONFIG } from "../intmax/constants.js"; |
| 9 | +import { PRIVACY_POOLS_CONFIG } from "../privacy-pools/constants.js"; |
| 10 | +import { RAILGUN_CONFIG } from "../railgun/constants.js"; |
| 11 | +import { TORNADO_CASH_CONFIG } from "../tornado-cash/constants.js"; |
| 12 | + |
| 13 | +const PROTOCOL_CONFIGS = [ |
| 14 | + { name: "fluidkey", config: FLUIDKEY_CONFIG }, |
| 15 | + { name: "hinkal", config: HINKAL_CONFIG }, |
| 16 | + { name: "intmax", config: INTMAX_CONFIG }, |
| 17 | + { name: "privacy-pools", config: PRIVACY_POOLS_CONFIG }, |
| 18 | + { name: "railgun", config: RAILGUN_CONFIG }, |
| 19 | + { name: "tornado-cash", config: TORNADO_CASH_CONFIG }, |
| 20 | +]; |
| 21 | + |
| 22 | +/** Directories excluded from protocol config checks */ |
| 23 | +const EXCLUDED_DIRS = new Set(["__tests__", "utils", "monero"]); |
15 | 24 |
|
16 | 25 | describe("constants.ts files", () => { |
17 | | - let files: Entry[] = []; |
18 | | - |
19 | | - beforeAll(() => { |
20 | | - const srcDir = resolve(__dirname, ".."); |
21 | | - |
22 | | - const filePaths = findProtocolConstantsFiles(srcDir); |
23 | | - |
24 | | - files = filePaths.map((filePath) => ({ |
25 | | - filePath, |
26 | | - ...parseConstantsFile(filePath), |
27 | | - })); |
28 | | - }); |
29 | | - |
30 | | - it("should have at least one contract address", () => { |
31 | | - files |
32 | | - .filter(({ filePath }) => !filePath.includes("monero")) |
33 | | - .forEach(({ filePath, addresses }) => { |
34 | | - expect( |
35 | | - addresses.length, |
36 | | - `${filePath} must have at least one contract address (Address-typed constant)`, |
37 | | - ).toBeGreaterThanOrEqual(1); |
38 | | - }); |
39 | | - }); |
40 | | - |
41 | | - it("should have at least two events array", () => { |
42 | | - files |
43 | | - .filter(({ filePath }) => !filePath.includes("monero")) |
44 | | - .forEach(({ filePath, events }) => { |
45 | | - expect( |
46 | | - events.length, |
47 | | - `${filePath} must have at least two events arrays (array with parseAbiItem calls)`, |
48 | | - ).toBeGreaterThanOrEqual(2); |
49 | | - }); |
50 | | - }); |
51 | | - |
52 | | - it("should have a URL to a .sol source file for each contract address", () => { |
53 | | - files.forEach(({ filePath, addresses }) => { |
54 | | - addresses.forEach(({ name, comment }) => { |
55 | | - expect(hasSolUrl(comment), `JSDoc for ${name} in ${filePath} must have a URL ending in .sol`).toBe(true); |
56 | | - }); |
| 26 | + it("should have a config entry for every protocol directory with a constants.ts file", () => { |
| 27 | + const srcDirectory = resolve(__dirname, ".."); |
| 28 | + const allDirectories = readdirSync(srcDirectory, { withFileTypes: true }) |
| 29 | + .filter((entry) => entry.isDirectory() && !EXCLUDED_DIRS.has(entry.name)) |
| 30 | + .map((entry) => entry.name); |
| 31 | + |
| 32 | + const protocolDirectories = allDirectories.filter((directory) => { |
| 33 | + const protocolDirectoryPath = join(srcDirectory, directory); |
| 34 | + const files = readdirSync(protocolDirectoryPath); |
| 35 | + return files.includes("constants.ts"); |
57 | 36 | }); |
58 | | - }); |
59 | 37 |
|
60 | | - it("should have a URL to a .sol source file with line reference for each events array indicating the function that emits the events", () => { |
61 | | - files.forEach(({ filePath, events }) => { |
62 | | - events.forEach(({ name, comment }) => { |
63 | | - expect( |
64 | | - hasSolUrlWithLineNumber(comment), |
65 | | - `JSDoc for ${name} in ${filePath} must have a URL to a .sol file with a line reference (e.g., #L123 or start=30)`, |
66 | | - ).toBe(true); |
67 | | - }); |
68 | | - }); |
69 | | - }); |
| 38 | + const configuredNames = new Set(PROTOCOL_CONFIGS.map(({ name }) => name)); |
| 39 | + const missing = protocolDirectories.filter((directory) => !configuredNames.has(directory)); |
70 | 40 |
|
71 | | - it("should have a Emits: section describing all events for each events array", () => { |
72 | | - files.forEach(({ filePath, events }) => { |
73 | | - events.forEach(({ name, arrayEventCount, emitsEventCount }) => { |
74 | | - expect( |
75 | | - emitsEventCount, |
76 | | - `JSDoc for ${name} in ${filePath} must describe all ${arrayEventCount} event(s) in the Emits: section`, |
77 | | - ).toBe(arrayEventCount); |
78 | | - }); |
79 | | - }); |
| 41 | + expect(missing, `Missing PROTOCOL_CONFIGS entries for: ${missing.join(", ")}`).toHaveLength(0); |
80 | 42 | }); |
81 | 43 |
|
82 | | - it("should have an Example: section with an etherscan transaction URL for each events array", () => { |
83 | | - files.forEach(({ filePath, events }) => { |
84 | | - events.forEach(({ name, comment }) => { |
85 | | - expect( |
86 | | - hasEtherscanExample(comment), |
87 | | - `JSDoc for ${name} in ${filePath} must contain an etherscan.io/tx/0x123... example URL`, |
88 | | - ).toBe(true); |
89 | | - }); |
| 44 | + it("should have at least two distinct event arrays per protocol", () => { |
| 45 | + PROTOCOL_CONFIGS.forEach(({ name, config }) => { |
| 46 | + const uniqueEventArrays = new Set(config.operations.map(({ events }) => events)); |
| 47 | + expect(uniqueEventArrays.size, `${name} must have at least two distinct event arrays`).toBeGreaterThanOrEqual(2); |
90 | 48 | }); |
91 | 49 | }); |
92 | 50 |
|
93 | | - it("should not repeat etherscan URL examples across events arrays", () => { |
94 | | - const allUrls: string[] = []; |
95 | | - const urlToLocations: Record<string, string[] | undefined> = {}; |
96 | | - |
97 | | - files.forEach(({ filePath, events }) => { |
98 | | - events.forEach(({ name, comment }) => { |
99 | | - const urls = extractEtherscanUrls(comment); |
100 | | - urls.forEach((url) => { |
101 | | - allUrls.push(url); |
102 | | - if (!urlToLocations[url]) { |
103 | | - urlToLocations[url] = []; |
104 | | - } |
105 | | - urlToLocations[url].push(`${filePath} (${name})`); |
106 | | - }); |
107 | | - }); |
108 | | - }); |
109 | | - |
110 | | - const duplicates = Object.entries(urlToLocations) |
111 | | - .filter(([, locations]) => locations && locations.length > 1) |
112 | | - .map(([url, locations]) => `${url} found in: ${locations?.join(", ")}`); |
| 51 | + it("should not repeat example transaction URLs across operations", () => { |
| 52 | + const allExampleTxUrls = PROTOCOL_CONFIGS.flatMap(({ config }) => |
| 53 | + config.operations.map(({ exampleTxUrl }) => exampleTxUrl), |
| 54 | + ); |
| 55 | + const uniqueExampleTxUrls = new Set(allExampleTxUrls); |
113 | 56 |
|
114 | | - expect( |
115 | | - duplicates, |
116 | | - `Etherscan URLs must not be repeated across events arrays:\n${duplicates.join("\n")}`, |
117 | | - ).toHaveLength(0); |
| 57 | + expect(uniqueExampleTxUrls.size, "Duplicate example transaction URLs found").toBe(allExampleTxUrls.length); |
118 | 58 | }); |
119 | 59 | }); |
0 commit comments