sol-classer 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/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # Classer: ABI to TypeScript/JavaScript Class Generator
2
+
3
+ Classer is a CLI tool that automatically generates strongly-typed TypeScript or vanilla JavaScript classes from Ethereum ABI JSON/JS/TS files. It streamlines contract interaction by providing:
4
+
5
+ - **Strict Typing**: TypeScript types for all function inputs and outputs (optional).
6
+ - **Read/Write Separation**: Clear distinction between view functions and state-modifying transactions.
7
+ - **Provider Flexibility**: Support for both RPC (server-side) and `window.ethereum` (browser).
8
+ - **ERC20 Helpers**: Automatic injection of `getBalance` and `transferTokens` helpers for ERC20 contracts.
9
+ - **Robustness**: Automatic contract initialization, `setSigner` helpers, and error handling wrapping.
10
+ - **Ethers.js v6 Integration**: Built on top of the latest Ethers.js for robust interaction.
11
+
12
+ ## Installation
13
+
14
+ To use it, you can install it globally or use `npx`:
15
+
16
+ ```bash
17
+ npm install -g sol-classer
18
+ # OR
19
+ npx sol-classer [options]
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ Basic usage:
25
+
26
+ ```bash
27
+ sol-classer \
28
+ --abi <path_to_abi> \
29
+ --name <ClassName> \
30
+ --address <ContractAddress> \
31
+ [options]
32
+ ```
33
+
34
+ ### Options
35
+
36
+ - `-a, --abi <path>`: **Required**. Path to the ABI file (`.json`, `.js`, `.ts`).
37
+ - `-n, --name <name>`: **Required**. Name of the class to generate.
38
+ - `-x, --address <address>`: **Required**. The default contract address.
39
+ - `-o, --output <path>`: Optional. Output file path. Defaults to `<ClassName>.[ts|js]`.
40
+ - `--provider <type>`: Optional. `'rpc'` (default) or `'window'`.
41
+ - `--lang <language>`: Optional. `'ts'` (default) or `'js'`.
42
+ - `--rpc <variable>`: Optional. Env var for RPC URL (only for RPC provider). Defaults to `NEXT_PUBLIC_RPC`.
43
+
44
+ ### Examples
45
+
46
+ **1. Standard TypeScript Class (RPC Provider)**
47
+ ```bash
48
+ sol-classer \
49
+ --abi test/samples/ERC20.json \
50
+ --name MyToken \
51
+ --address 0x123... \
52
+ --provider rpc \
53
+ --lang ts
54
+ ```
55
+
56
+ **2. Browser/Frontend Class (Window Provider)**
57
+ This generates a class that connects to `window.ethereum` and handles wallet connection.
58
+ ```bash
59
+ sol-classer \
60
+ --abi ABI/ABI.js \
61
+ --name FrontendToken \
62
+ --address 0x456... \
63
+ --provider window
64
+ ```
65
+
66
+ **Usage in Frontend:**
67
+ ```typescript
68
+ const token = new FrontendToken();
69
+ // Auto-initializes on first call, or call explicitly
70
+ await token.setSigner();
71
+ await token.transfer("0xUser", "10");
72
+ ```
73
+
74
+ **3. JavaScript Class (No Types)**
75
+ ```bash
76
+ sol-classer \
77
+ --abi test/samples/ERC20.json \
78
+ --name LegacyToken \
79
+ --address 0x789... \
80
+ --lang js
81
+ ```
82
+
83
+ ## Generated Code Features
84
+
85
+ - **Automatic Initialization**: All methods call `initializeContract()` internally to ensure provider/signer availability.
86
+ - **Error Handling**: All contract calls are wrapped in `try-catch` blocks with informative error logging.
87
+ - **Extensible**: The generated class can be easily extended or modified.
88
+ - **Isomorphic**: Works in Node.js (RPC) and Browser (Window) environments.
89
+ - **Zero Config**: Just pass the ABI and you are good to go.
@@ -0,0 +1,11 @@
1
+ export type ProviderType = 'rpc' | 'window';
2
+ export type Language = 'ts' | 'js';
3
+ export declare function solidityTypeToTs(type: string): string;
4
+ export declare function generateClass({ abi, className, contractAddress, rpcEnvVar, providerType, language }: {
5
+ abi: any[];
6
+ className: string;
7
+ contractAddress: string;
8
+ rpcEnvVar?: string;
9
+ providerType?: ProviderType;
10
+ language?: Language;
11
+ }): string;
@@ -0,0 +1,286 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.solidityTypeToTs = solidityTypeToTs;
4
+ exports.generateClass = generateClass;
5
+ function solidityTypeToTs(type) {
6
+ if (type.startsWith("uint") || type.startsWith("int")) {
7
+ return "string | number | bigint";
8
+ }
9
+ if (type === "address")
10
+ return "string";
11
+ if (type === "bool")
12
+ return "boolean";
13
+ if (type === "string")
14
+ return "string";
15
+ if (type.startsWith("bytes"))
16
+ return "string";
17
+ if (type === "tuple")
18
+ return "any"; // TODO: Handle tuples better if needed
19
+ if (type.endsWith("[]"))
20
+ return "any[]"; // Arrays
21
+ return "any";
22
+ }
23
+ function isReadFunction(fn) {
24
+ return (fn.stateMutability === "view" ||
25
+ fn.stateMutability === "pure");
26
+ }
27
+ function isWriteFunction(fn) {
28
+ return (fn.stateMutability === "nonpayable" ||
29
+ fn.stateMutability === "payable");
30
+ }
31
+ function generateParams(inputs, language) {
32
+ if (!inputs || inputs.length === 0)
33
+ return "";
34
+ return inputs
35
+ .map((i, idx) => {
36
+ const name = i.name || `arg${idx}`;
37
+ return language === 'ts'
38
+ ? `${name}: ${solidityTypeToTs(i.type)}`
39
+ : name;
40
+ })
41
+ .join(", ");
42
+ }
43
+ function generateReadMethod(fn, language) {
44
+ const params = generateParams(fn.inputs, language);
45
+ const argNames = fn.inputs ? fn.inputs.map((i, idx) => i.name || `arg${idx}`).join(", ") : "";
46
+ const returnType = language === 'ts' ? ": Promise<any>" : "";
47
+ return `
48
+ async ${fn.name}(${params})${returnType} {
49
+ try {
50
+ await this.initializeContract();
51
+ return await this.contract.${fn.name}(${argNames});
52
+ } catch (error) {
53
+ console.error("Failed to read ${fn.name}:", error);
54
+ throw error;
55
+ }
56
+ }
57
+ `;
58
+ }
59
+ function generateWriteMethod(fn, language) {
60
+ const params = generateParams(fn.inputs, language);
61
+ const argNames = fn.inputs ? fn.inputs.map((i, idx) => i.name || `arg${idx}`).join(", ") : "";
62
+ const returnType = language === 'ts' ? ": Promise<ethers.ContractTransactionResponse>" : "";
63
+ return `
64
+ async ${fn.name}(${params})${returnType} {
65
+ try {
66
+ await this.initializeContract();
67
+ if (!this.contract.runner) {
68
+ throw new Error("Signer required for ${fn.name}. Ensure you have connected a wallet.");
69
+ }
70
+
71
+ const tx = await this.contract.${fn.name}(${argNames});
72
+ await tx.wait();
73
+ return tx;
74
+ } catch (error) {
75
+ console.error("Failed to execute ${fn.name}:", error);
76
+ throw error;
77
+ }
78
+ }
79
+ `;
80
+ }
81
+ function isERC20(abi) {
82
+ const names = abi
83
+ .filter((x) => x.type === "function")
84
+ .map((x) => x.name);
85
+ return (names.includes("decimals") &&
86
+ names.includes("symbol") &&
87
+ names.includes("balanceOf") &&
88
+ names.includes("transfer"));
89
+ }
90
+ function generateClass({ abi, className, contractAddress, rpcEnvVar = "NEXT_PUBLIC_RPC", providerType = 'rpc', language = 'ts' }) {
91
+ const readFns = abi.filter((x) => x.type === "function" && isReadFunction(x));
92
+ const writeFns = abi.filter((x) => x.type === "function" && isWriteFunction(x));
93
+ const isErc20Token = isERC20(abi);
94
+ const isTs = language === 'ts';
95
+ let erc20Methods = "";
96
+ if (isErc20Token) {
97
+ const balanceReturnType = isTs ? ": Promise<string>" : "";
98
+ const txReturnType = isTs ? ": Promise<ethers.ContractTransactionResponse>" : "";
99
+ const addressType = isTs ? ": string" : "";
100
+ const amountType = isTs ? ": string" : "";
101
+ erc20Methods = `
102
+ // -------------------------
103
+ // ERC20 Helpers
104
+ // -------------------------
105
+
106
+ async getBalance(address${addressType})${balanceReturnType} {
107
+ try {
108
+ await this.initializeContract();
109
+ const balance = await this.contract.balanceOf(address);
110
+ const decimals = await this.contract.decimals();
111
+ return ethers.formatUnits(balance, decimals);
112
+ } catch (error) {
113
+ console.error("Failed to get balance:", error);
114
+ throw error;
115
+ }
116
+ }
117
+
118
+ async transferTokens(to${addressType}, amount${amountType})${txReturnType} {
119
+ try {
120
+ await this.initializeContract();
121
+ if (!this.contract.runner) {
122
+ throw new Error("Signer required for transferTokens");
123
+ }
124
+ const decimals = await this.contract.decimals();
125
+ const parsedAmount = ethers.parseUnits(amount, decimals);
126
+ const tx = await this.contract.transfer(to, parsedAmount);
127
+ await tx.wait();
128
+ return tx;
129
+ } catch (error) {
130
+ console.error("Failed to transfer tokens:", error);
131
+ throw error;
132
+ }
133
+ }
134
+ `;
135
+ }
136
+ // Type Definitions
137
+ const fieldTypes = isTs ? `
138
+ private provider: ethers.Provider | ethers.BrowserProvider | null;
139
+ private signer: ethers.Signer | null;
140
+ private contract: ethers.Contract | null;
141
+ private contractAddress: string;
142
+ ` : "";
143
+ // Constructor logic
144
+ let constructorCode = "";
145
+ let methodsCode = "";
146
+ if (providerType === 'rpc') {
147
+ const ctorParams = isTs
148
+ ? `contractAddress: string = "${contractAddress}", privateKey?: string, rpcUrl?: string`
149
+ : `contractAddress = "${contractAddress}", privateKey, rpcUrl`;
150
+ constructorCode = `
151
+ constructor(${ctorParams}) {
152
+ this.contractAddress = contractAddress;
153
+ this.contract = null;
154
+ this.signer = null;
155
+
156
+ const rpc = rpcUrl || process.env.${rpcEnvVar};
157
+ if (!rpc) {
158
+ throw new Error("RPC URL not found. Please provide it or set ${rpcEnvVar}");
159
+ }
160
+ this.provider = new ethers.JsonRpcProvider(rpc);
161
+
162
+ if (privateKey) {
163
+ this.signer = new ethers.Wallet(privateKey, this.provider);
164
+ }
165
+ }
166
+ `;
167
+ methodsCode = `
168
+ async initializeContract()${isTs ? ": Promise<void>" : ""} {
169
+ if (this.contract) return;
170
+
171
+ try {
172
+ // If we have a signer (private key), use it. Otherwise use provider (read-only)
173
+ const runner = this.signer || this.provider;
174
+ this.contract = new ethers.Contract(
175
+ this.contractAddress,
176
+ ABI,
177
+ runner
178
+ );
179
+ } catch (error) {
180
+ console.error("Failed to initialize contract:", error);
181
+ throw error;
182
+ }
183
+ }
184
+ `;
185
+ }
186
+ else {
187
+ // Window / Browser provider
188
+ const ctorParams = isTs
189
+ ? `contractAddress: string = "${contractAddress}"`
190
+ : `contractAddress = "${contractAddress}"`;
191
+ constructorCode = `
192
+ constructor(${ctorParams}) {
193
+ this.contractAddress = contractAddress;
194
+ this.provider = null;
195
+ this.signer = null;
196
+ this.contract = null;
197
+ }
198
+ `;
199
+ methodsCode = `
200
+ async init()${isTs ? ": Promise<void>" : ""} {
201
+ if (typeof window !== "undefined" && (window as any).ethereum) {
202
+ this.provider = new ethers.BrowserProvider((window as any).ethereum);
203
+ } else {
204
+ throw new Error("window.ethereum is not available");
205
+ }
206
+ }
207
+
208
+ async setSigner()${isTs ? ": Promise<void>" : ""} {
209
+ try {
210
+ if (!this.provider) await this.init();
211
+
212
+ // @ts-ignore
213
+ this.signer = await this.provider.getSigner();
214
+ } catch (error) {
215
+ console.error("Failed to set signer:", error);
216
+ throw error;
217
+ }
218
+ }
219
+
220
+ async getSignerAddress()${isTs ? ": Promise<string>" : ""} {
221
+ try {
222
+ if (!this.signer) await this.setSigner();
223
+ // @ts-ignore
224
+ return await this.signer.getAddress();
225
+ } catch (error) {
226
+ console.error("Failed to get signer address:", error);
227
+ throw error;
228
+ }
229
+ }
230
+
231
+ async initializeContract()${isTs ? ": Promise<void>" : ""} {
232
+ try {
233
+ if (!this.provider) await this.init();
234
+
235
+ if (!this.signer) {
236
+ // Try to get signer if possible, otherwise implementation might fail for usage requiring signer
237
+ // But we don't force it here to allow read-only if the user hasn't connected wallet yet?
238
+ // Actually, for consistency with 'setSigner', let's try to get it.
239
+ try {
240
+ // @ts-ignore
241
+ this.signer = await this.provider.getSigner();
242
+ } catch (e) {
243
+ console.warn("Could not get signer, defaulting to read-only provider if possible");
244
+ }
245
+ }
246
+
247
+ if (!this.contract) {
248
+ const runner = this.signer || this.provider;
249
+ this.contract = new ethers.Contract(
250
+ this.contractAddress,
251
+ ABI,
252
+ runner
253
+ );
254
+ }
255
+ } catch (error) {
256
+ console.error("Failed to initialize contract:", error);
257
+ throw error;
258
+ }
259
+ }
260
+ `;
261
+ }
262
+ return `
263
+ import { ethers } from "ethers";
264
+
265
+ const ABI = ${JSON.stringify(abi, null, 2)};
266
+
267
+ export class ${className} {
268
+ ${fieldTypes}
269
+ ${constructorCode}
270
+
271
+ ${methodsCode}
272
+
273
+ ${erc20Methods}
274
+
275
+ // -------------------------
276
+ // Write functions
277
+ // -------------------------
278
+ ${writeFns.map(fn => generateWriteMethod(fn, language)).join("\n")}
279
+
280
+ // -------------------------
281
+ // Read functions
282
+ // -------------------------
283
+ ${readFns.map(fn => generateReadMethod(fn, language)).join("\n")}
284
+ }
285
+ `;
286
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const commander_1 = require("commander");
8
+ const path_1 = __importDefault(require("path"));
9
+ const utils_1 = require("./utils");
10
+ const generator_1 = require("./generator");
11
+ const program = new commander_1.Command();
12
+ program
13
+ .version("1.0.0")
14
+ .description("Generate TypeScript class from ABI JSON")
15
+ .requiredOption("-a, --abi <path>", "Path to ABI JSON file")
16
+ .requiredOption("-n, --name <name>", "Name of the class to generate")
17
+ .requiredOption("-x, --address <address>", "Contract address")
18
+ .option("-o, --output <path>", "Output file path")
19
+ .option("--provider <type>", "Provider type: 'rpc' | 'window'", "rpc")
20
+ .option("--lang <language>", "Output language: 'ts' | 'js'", "ts")
21
+ .action(async (options) => {
22
+ try {
23
+ const abiPath = path_1.default.resolve(process.cwd(), options.abi);
24
+ const abi = await (0, utils_1.loadAbi)(abiPath);
25
+ // Validate options
26
+ if (!['rpc', 'window'].includes(options.provider)) {
27
+ throw new Error("Invalid provider type. Must be 'rpc' or 'window'.");
28
+ }
29
+ if (!['ts', 'js'].includes(options.lang)) {
30
+ throw new Error("Invalid language. Must be 'ts' or 'js'.");
31
+ }
32
+ const classCode = (0, generator_1.generateClass)({
33
+ abi,
34
+ className: options.name,
35
+ contractAddress: options.address,
36
+ rpcEnvVar: options.rpc,
37
+ providerType: options.provider,
38
+ language: options.lang
39
+ });
40
+ const extension = options.lang === 'js' ? 'js' : 'ts';
41
+ const outputPath = options.output
42
+ ? path_1.default.resolve(process.cwd(), options.output)
43
+ : path_1.default.join(process.cwd(), `${options.name}.${extension}`);
44
+ await (0, utils_1.writefile)(outputPath, classCode);
45
+ console.log(`Successfully generated ${options.name} at ${outputPath}`);
46
+ }
47
+ catch (error) {
48
+ console.error("Error generating class:", error);
49
+ process.exit(1);
50
+ }
51
+ });
52
+ program.parse(process.argv);
@@ -0,0 +1,2 @@
1
+ export declare function loadAbi(filePath: string): Promise<any>;
2
+ export declare function writefile(filePath: string, content: string): Promise<void>;
package/dist/utils.js ADDED
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadAbi = loadAbi;
7
+ exports.writefile = writefile;
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const path_1 = __importDefault(require("path"));
10
+ async function loadAbi(filePath) {
11
+ const ext = path_1.default.extname(filePath).toLowerCase();
12
+ if (ext === ".json") {
13
+ return await fs_extra_1.default.readJson(filePath);
14
+ }
15
+ else if (ext === ".js" || ext === ".ts") {
16
+ const absolutePath = path_1.default.resolve(filePath);
17
+ // Use jiti to handle both CJS and ESM, and TS files
18
+ const createJiti = require("jiti");
19
+ const jiti = createJiti(__filename);
20
+ const module = jiti(absolutePath);
21
+ // Try to find the ABI array
22
+ let candidate = module.default || module.ABI || module.abi || module.Abi;
23
+ // If default is an object containing ABI
24
+ if (module.default && !Array.isArray(module.default)) {
25
+ candidate = module.default.ABI || module.default.abi || module.default.Abi || candidate;
26
+ }
27
+ if (candidate && Array.isArray(candidate))
28
+ return candidate;
29
+ // If the module itself is the array (unlikely for named export but possible for default)
30
+ if (Array.isArray(module))
31
+ return module;
32
+ // Specific check for the 'ABI' key as seen in logs
33
+ if (Array.isArray(module.ABI))
34
+ return module.ABI;
35
+ throw new Error(`Could not find ABI array in ${filePath}. Loaded keys: ${Object.keys(module).join(", ")}`);
36
+ }
37
+ else {
38
+ throw new Error(`Unsupported file extension: ${ext}. Use .json, .js, or .ts`);
39
+ }
40
+ }
41
+ async function writefile(filePath, content) {
42
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(filePath));
43
+ await fs_extra_1.default.writeFile(filePath, content);
44
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "sol-classer",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "sol-classer": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "prepublishOnly": "npm run build",
15
+ "test": "echo \"Error: no test specified\" && exit 1"
16
+ },
17
+ "keywords": [],
18
+ "author": "larsalaxsanderson@gmail.com",
19
+ "license": "ISC",
20
+ "type": "commonjs",
21
+ "dependencies": {
22
+ "commander": "^14.0.3",
23
+ "ethers": "^6.16.0",
24
+ "fs-extra": "^11.3.3",
25
+ "jiti": "^2.6.1"
26
+ },
27
+ "devDependencies": {
28
+ "@types/fs-extra": "^11.0.4",
29
+ "@types/node": "^25.2.3",
30
+ "ts-node": "^10.9.2",
31
+ "typescript": "^5.9.3"
32
+ }
33
+ }