-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.ts
More file actions
194 lines (165 loc) · 5.58 KB
/
index.ts
File metadata and controls
194 lines (165 loc) · 5.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
#!/usr/bin/env node
import { glob } from 'glob';
import { readFileSync } from 'fs';
import { resolve, join } from 'path';
import { Command } from 'commander';
import keccak256 from 'keccak256';
import { statSync } from 'fs';
interface ErrorInfo {
name: string;
signature: string;
selector: string;
filePath: string;
}
interface CommandOptions {
directory: string;
list: boolean;
}
/**
* Calculates the selector for a Solidity error
* @param signature - The error signature (e.g., "CustomError(uint256,address)")
* @returns The selector as a hex string (e.g., "0x82b42900")
*/
function calculateSelector(signature: string): string {
// Use keccak256 package to compute the hash
const hash = keccak256(signature).toString('hex');
return '0x' + hash.substring(0, 8);
}
/**
* Checks if a path is a file (not a directory)
* @param path - Path to check
* @returns True if the path is a file, false otherwise
*/
function isFile(path: string): boolean {
try {
return statSync(path).isFile();
} catch (error) {
return false;
}
}
/**
* Extracts custom errors from a Solidity file
* @param filePath - Path to the Solidity file
* @returns Array of error information objects
*/
function extractErrorsFromFile(filePath: string): ErrorInfo[] {
// Skip if not a file
if (!isFile(filePath)) {
return [];
}
const content = readFileSync(filePath, 'utf-8');
const errors: ErrorInfo[] = [];
// Regular expression to match Solidity custom errors
// This matches "error ErrorName(type1 param1, type2 param2, ...);"
const errorRegex = /error\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)\s*;/g;
let match;
while ((match = errorRegex.exec(content)) !== null) {
const errorName = match[1];
// Parse parameters (if any)
const params = match[2].trim();
// Create param signature (e.g., "uint256,address" from "uint256 amount, address recipient")
let paramSignature = '';
if (params) {
paramSignature = params
.split(',')
.map(param => param.trim().split(/\s+/)[0]) // Extract type from "type name"
.join(',');
}
const signature = `${errorName}(${paramSignature})`;
const selector = calculateSelector(signature);
errors.push({
name: errorName,
signature,
selector,
filePath
});
}
return errors;
}
/**
* Scans a directory for Solidity files and extracts all custom errors
* @param directory - Directory to scan
* @returns Array of error information objects
*/
async function scanDirectoryForErrors(directory: string): Promise<ErrorInfo[]> {
const absPath = resolve(directory);
const solidityFiles = await glob(`${absPath}/**/*.sol`);
let allErrors: ErrorInfo[] = [];
for (const file of solidityFiles) {
try {
const errors = extractErrorsFromFile(file);
allErrors = [...allErrors, ...errors];
} catch (error) {
console.error(`Error processing file ${file}:`, error);
}
}
return allErrors;
}
/**
* Finds an error by its selector
* @param errors - Array of error information objects
* @param selector - Error selector to find
* @returns Matching error or undefined if not found
*/
function findErrorBySelector(errors: ErrorInfo[], selector: string): ErrorInfo | undefined {
return errors.find(error => error.selector.toLowerCase() === selector.toLowerCase());
}
/**
* Main CLI function
*/
async function main() {
const program = new Command();
program
.name('sol-errors-decoder')
.description('CLI tool to decode Solidity custom error selectors')
.version('1.0.0');
program
.argument('[selector]', 'Error selector to decode (e.g., 0x82b42900)')
.option('-d, --directory <directory>', 'Directory to scan for Solidity files', process.cwd())
.option('-l, --list', 'List all found errors without decoding')
.option('--skip-node-modules', 'Skip node_modules directories', false)
.action(async (selector: string | undefined, options: CommandOptions & { skipNodeModules?: boolean }) => {
try {
const directory = options.directory;
console.log(`Scanning ${directory} for Solidity files...`);
const errors = await scanDirectoryForErrors(directory);
console.log(`Found ${errors.length} custom errors in ${directory}`);
if (options.list) {
// List all errors
console.table(errors.map(e => ({
Name: e.name,
Signature: e.signature,
Selector: e.selector,
File: e.filePath
})));
return;
}
if (!selector) {
console.log('No selector provided. Use --list to see all errors or provide a selector to decode.');
program.help();
return;
}
// Make sure selector has 0x prefix
const normalizedSelector = selector.startsWith('0x') ? selector : `0x${selector}`;
const matchingError = findErrorBySelector(errors, normalizedSelector);
if (matchingError) {
console.log('\nFound matching error:');
console.log(`Name: ${matchingError.name}`);
console.log(`Signature: ${matchingError.signature}`);
console.log(`Selector: ${matchingError.selector}`);
console.log(`File: ${matchingError.filePath}`);
} else {
console.log(`No matching error found for selector: ${normalizedSelector}`);
}
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
});
program.parse();
}
// Run the CLI
main().catch(error => {
console.error('Unhandled error:', error);
process.exit(1);
});