ethershell 0.1.0-alpha.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/bin/cli.js ADDED
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @fileoverview EtherShell - Interactive CLI for Ethereum smart contract development
5
+ * @description Main entry point for the EtherShell REPL environment that provides
6
+ * an interactive command-line interface for compiling, deploying, and managing
7
+ * Ethereum smart contracts and wallets.
8
+ * @module cli
9
+ */
10
+
11
+ import repl from 'repl';
12
+ import util from 'util';
13
+ import { customEval } from '../src/utils/replHelper.js';
14
+ import {
15
+ updateCompiler,
16
+ currentCompiler,
17
+ compilerOptions,
18
+ getCompilerOptions,
19
+ compile
20
+ } from '../src/services/build.js';
21
+ import { set, get, getDefault } from '../src/services/network.js';
22
+ import { deleteDirectory } from '../src/services/files.js';
23
+ import {
24
+ addAccounts,
25
+ getAccounts,
26
+ createAccounts,
27
+ deleteAccount,
28
+ createHD,
29
+ getHDAccounts,
30
+ addHD,
31
+ getAllAccounts,
32
+ connectWallet,
33
+ getWalletInfo
34
+ } from '../src/services/wallet.js';
35
+
36
+ import { deploy, add } from '../src/services/addContracts.js';
37
+ import { getContracts } from '../src/services/contracts.js';
38
+
39
+ /**
40
+ * REPL instance for EtherShell interactive environment
41
+ * @type {repl.REPLServer}
42
+ * @description Creates and configures the REPL server with custom evaluation
43
+ * and output formatting
44
+ */
45
+ export const r = repl.start({
46
+ prompt: 'EtherShell> ',
47
+ ignoreUndefined: true,
48
+ eval: customEval,
49
+ writer: output => {
50
+ return util.inspect(output, { colors: true, depth: null });
51
+ }
52
+ });
53
+
54
+ // Network commands
55
+ r.context.chain = set;
56
+ r.context.chain = get;
57
+ r.context.defaultChain = getDefault;
58
+
59
+ // Compile commands
60
+ r.context.compiler = currentCompiler;
61
+ r.context.compUpdate = updateCompiler;
62
+ r.context.compInfo = getCompilerOptions;
63
+ r.context.compOpts = compilerOptions;
64
+ r.context.build = compile;
65
+
66
+ // Clean build folder
67
+ r.context.clean = deleteDirectory;
68
+
69
+ // Set wallet
70
+ r.context.addWallet = addAccounts;
71
+ r.context.addHDWallet = addHD;
72
+ r.context.newWallet = createAccounts;
73
+ r.context.newHDWallet = createHD;
74
+ r.context.removeWallet = deleteAccount;
75
+ r.context.connectWallet = connectWallet;
76
+
77
+ // View wallets
78
+ r.context.wallets = getAccounts;
79
+ r.context.allWallets = getAllAccounts;
80
+ r.context.hdWallets = getHDAccounts;
81
+ r.context.walletInfo = getWalletInfo
82
+
83
+ // Add contract
84
+ r.context.deploy = deploy;
85
+ r.context.addContract = add;
86
+
87
+ // Contract
88
+ r.context.contracts = getContracts;
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "ethershell",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "Interactive JavaScript console for Ethereum smart contract management",
5
+ "author": "Alireza Kiakojouri (alirezaethdev@gmail.com)",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/AlirezaEthDev/EtherShell"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/AlirezaEthDev/EtherShell/issues"
12
+ },
13
+ "homepage": "https://github.com/AlirezaEthDev/EtherShell",
14
+ "type": "module",
15
+ "bin": {
16
+ "ethershell": "./bin/cli.js"
17
+ },
18
+ "scripts": {
19
+ "start": "node bin/cli.js",
20
+ "dev": "nodemon bin/cli.js",
21
+ "test": "echo \"No tests yet\" && exit 0",
22
+ "lint": "echo \"No linting configured\" && exit 0",
23
+ "prepublishOnly": "npm test"
24
+ },
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ },
28
+ "keywords": [
29
+ "ethereum",
30
+ "cli",
31
+ "smart-contracts",
32
+ "solidity",
33
+ "blockchain"
34
+ ],
35
+ "dependencies": {
36
+ "commander": "^12.1.0",
37
+ "ethers": "^6.13.0",
38
+ "node-localstorage": "^3.0.5",
39
+ "solc": "^0.8.29",
40
+ "vm": "^0.1.0"
41
+ },
42
+ "preferGlobal": true,
43
+ "files": [
44
+ "bin/",
45
+ "src/",
46
+ "LICENSE",
47
+ "README.md"
48
+ ]
49
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * @fileoverview Smart contract deployment and management
3
+ * @description Provides functions to deploy new smart contracts and add existing
4
+ * contracts to the EtherShell environment. Manages contract instances and integrates
5
+ * them with the REPL context.
6
+ * @module addContracts
7
+ */
8
+
9
+ import { ethers } from 'ethers';
10
+ import fs from 'fs';
11
+ import { provider } from './network.js';
12
+ import { allAccounts, accounts, hdAccounts } from './wallet.js';
13
+ import { LocalStorage } from 'node-localstorage';
14
+ import { r } from '../../bin/cli.js';
15
+
16
+ /**
17
+ * Local storage instance for persisting contract metadata
18
+ * @type {LocalStorage}
19
+ */
20
+ const localStorage = new LocalStorage('./localStorage');
21
+
22
+ /**
23
+ * Map of all deployed and added contracts
24
+ * @type {Map<string, ethers.Contract>}
25
+ */
26
+ export const contracts = new Map();
27
+
28
+ /**
29
+ * Deploy a new smart contract to the blockchain
30
+ * @async
31
+ * @param {string} contractName - Name of the contract to deploy
32
+ * @param {Array} [args=[]] - Constructor arguments for the contract
33
+ * @param {number} [accIndex=0] - Index of the account to deploy from
34
+ * @param {string} [chain] - Optional custom chain URL
35
+ * @param {string} [abiLoc] - Optional custom ABI file location
36
+ * @param {string} [bytecodeLoc] - Optional custom bytecode file location
37
+ * @returns {Promise<void>}
38
+ * @throws {Error} If contract name is empty, account index is out of range, or deployment fails
39
+ * @example
40
+ * deploy('MyToken', [1000000], 0);
41
+ */
42
+ export async function deploy(contractName, args, accIndex, chain, abiLoc, bytecodeLoc) {
43
+ try {
44
+ let currentProvider;
45
+ let connectedChain;
46
+
47
+ if(!contractName) {
48
+ throw new Error('Contract name is empty');
49
+ }
50
+
51
+ const contractArgs = args || [];
52
+
53
+ if(accIndex > allAccounts.length - 1) {
54
+ throw new Error('Wallet index is out of range');
55
+ }
56
+
57
+ if(!accIndex) {
58
+ accIndex = 0;
59
+ }
60
+
61
+ if(chain) {
62
+ currentProvider = new ethers.JsonRpcProvider(chain);
63
+ } else {
64
+ currentProvider = provider;
65
+ }
66
+
67
+ let wallet = new ethers.Wallet(allAccounts[accIndex].privateKey, currentProvider);
68
+ connectedChain = await currentProvider.getNetwork();
69
+
70
+ const abiPath = abiLoc || localStorage.getItem(`${contractName}_abi`);
71
+ const bytecodePath = bytecodeLoc || localStorage.getItem(`${contractName}_bytecode`);
72
+
73
+ const abi = JSON.parse(fs.readFileSync(abiPath, 'utf8'));
74
+ const bytecode = JSON.parse(fs.readFileSync(bytecodePath, 'utf8'));
75
+
76
+ const factory = new ethers.ContractFactory(abi, bytecode, wallet);
77
+ const deployTx = await factory.deploy(...contractArgs);
78
+ await deployTx.waitForDeployment();
79
+
80
+ // Update deployer contract list
81
+ const contSpec = {
82
+ address: deployTx.target,
83
+ deployedOn: connectedChain.name,
84
+ chainId: connectedChain.chainId
85
+ }
86
+
87
+ allAccounts[accIndex].contracts.push(contSpec);
88
+
89
+ const accountsIndex = accounts.findIndex(wallet => wallet.index == accIndex);
90
+ if(accountsIndex >= 0) {
91
+ accounts[accountsIndex].contracts.push(contSpec);
92
+ }
93
+
94
+ const hdIndex = hdAccounts.findIndex(wallet => wallet.index == accIndex);
95
+ if(hdIndex >= 0) {
96
+ hdAccounts[hdIndex].contracts.push(contSpec);
97
+ }
98
+
99
+ // Extend contract object
100
+ deployTx.index = Array.from(contracts.values()).length;
101
+ deployTx.name = contractName;
102
+ deployTx.chain = connectedChain.name;
103
+ deployTx.chainId = connectedChain.chainId;
104
+ deployTx.deployType = 'ethershell-deployed',
105
+ deployTx.provider = currentProvider;
106
+
107
+ // Add to contract list
108
+ contracts.set(contractName, deployTx);
109
+
110
+ // Add to REPL context
111
+ r.context[contractName] = deployTx;
112
+
113
+ const deployHash = deployTx.deploymentTransaction().hash;
114
+ const tx = await provider.getTransaction(deployHash);
115
+ delete tx.data;
116
+
117
+ // Extend transaction object
118
+ tx.ethershellIndex = deployTx.index;
119
+ tx.address = deployTx.target;
120
+ tx.name = deployTx.name;
121
+ tx.chain = deployTx.chain;
122
+ tx.deployType = deployTx.deployType;
123
+
124
+ console.log(tx);
125
+ } catch(err) {
126
+ console.error(err);
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Add an existing deployed contract to EtherShell
132
+ * @async
133
+ * @param {string} contractName - Name to assign to the contract
134
+ * @param {string} contractAddr - Address of the deployed contract
135
+ * @param {number} [accIndex=0] - Index of the account to interact with the contract
136
+ * @param {string} abiLoc - Path to the contract ABI file
137
+ * @param {string} [chain] - Optional custom chain URL
138
+ * @returns {Promise<void>}
139
+ * @throws {Error} If contract address or ABI location is null/undefined
140
+ * @example
141
+ * add('USDT', '0xdac17f958d2ee523a2206206994597c13d831ec7', 0, './abis/USDT.json');
142
+ */
143
+ export async function add(contractName, contractAddr, accIndex, abiLoc, chain) {
144
+ try {
145
+ let currentProvider;
146
+ let connectedChain;
147
+
148
+ if(!contractAddr) {
149
+ throw new Error('Contract address may not be null or undefined!');
150
+ }
151
+
152
+ if(!accIndex) {
153
+ accIndex = 0;
154
+ }
155
+
156
+ if(chain) {
157
+ currentProvider = new ethers.JsonRpcProvider(chain);
158
+ } else {
159
+ currentProvider = provider;
160
+ }
161
+
162
+ let wallet = new ethers.Wallet(allAccounts[accIndex].privateKey, currentProvider);
163
+ connectedChain = await currentProvider.getNetwork();
164
+
165
+ if(!abiLoc) {
166
+ throw new Error('ABI path may not be null or undefined!');
167
+ }
168
+
169
+ const abi = JSON.parse(fs.readFileSync(abiLoc, 'utf8'));
170
+
171
+ const newContract = new ethers.Contract(contractAddr, abi, wallet);
172
+
173
+ // Update deployer contract list
174
+ const contSpec = {
175
+ address: newContract.target,
176
+ deployedOn: connectedChain.name,
177
+ chainId: connectedChain.chainId
178
+ }
179
+
180
+ allAccounts[accIndex].contracts.push(contSpec);
181
+
182
+ const accountsIndex = accounts.findIndex(wallet => wallet.index == accIndex);
183
+ if(accountsIndex >= 0) {
184
+ accounts[accountsIndex].contracts.push(contSpec);
185
+ }
186
+
187
+ const hdIndex = hdAccounts.findIndex(wallet => wallet.index == accIndex);
188
+ if(hdIndex >= 0) {
189
+ hdAccounts[hdIndex].contracts.push(contSpec);
190
+ }
191
+
192
+ // Extend contract object
193
+ newContract.index = Array.from(contracts.values()).length;
194
+ newContract.name = contractName;
195
+ newContract.chain = connectedChain.name;
196
+ newContract.chainId = connectedChain.chainId;
197
+ newContract.deployType = 'pre-deployed',
198
+ newContract.provider = currentProvider;
199
+
200
+ // Add to contract list
201
+ contracts.set(contractName, newContract);
202
+
203
+ // Add to REPL context
204
+ r.context[contractName] = newContract;
205
+
206
+ // Add result
207
+ const result = {
208
+ index: newContract.index,
209
+ name: newContract.name,
210
+ address: newContract.target,
211
+ chain: newContract.chain,
212
+ chainId: newContract.chainId,
213
+ deployType: newContract.deployType,
214
+ provider: newContract.provider
215
+ }
216
+
217
+ console.log(result);
218
+ } catch(err) {
219
+ console.error(err);
220
+ }
221
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * @fileoverview Solidity compiler management and contract compilation
3
+ * @description Manages Solidity compiler versions, compilation settings, and
4
+ * provides functions to compile smart contracts with customizable options.
5
+ * @module build
6
+ */
7
+
8
+ import path from 'path';
9
+ import solc from 'solc';
10
+ import { check, collectSolFiles } from '../utils/dir.js';
11
+ import { setVersion, build } from '../utils/builder.js';
12
+
13
+ /**
14
+ * Current Solidity compiler instance
15
+ * @type {Object}
16
+ */
17
+ let currentSolcInstance = solc; // default local compiler
18
+
19
+ /**
20
+ * Global compiler configuration state
21
+ * @type {Object}
22
+ * @property {boolean} optimizer - Whether gas optimizer is enabled
23
+ * @property {number} optimizerRuns - Number of optimizer runs
24
+ * @property {boolean} viaIR - Whether to use IR-based code generation
25
+ */
26
+ let compilerConfig = {
27
+ optimizer: false,
28
+ optimizerRuns: 200,
29
+ viaIR: false
30
+ };
31
+
32
+ /**
33
+ * Update the Solidity compiler to a specific version
34
+ * @async
35
+ * @param {string} version - Solidity version (e.g., 'v0.8.20+commit.a1b79de6')
36
+ * @returns {Promise<void>}
37
+ * @throws {Error} If the specified version cannot be loaded
38
+ * @example
39
+ * await updateCompiler('v0.8.20+commit.a1b79de6');
40
+ */
41
+ export async function updateCompiler(version){
42
+ try{
43
+ currentSolcInstance = await setVersion(version, currentSolcInstance);
44
+ } catch(err) {
45
+ console.error(err);
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Get the current compiler version
51
+ * @returns {string} Current Solidity compiler version string
52
+ * @example
53
+ * const version = currentCompiler(); // Returns: "0.8.20+commit.a1b79de6.Emscripten.clang"
54
+ */
55
+ export function currentCompiler(){
56
+ return currentSolcInstance.version();
57
+ }
58
+
59
+ /**
60
+ * Configure compiler optimization options
61
+ * @param {boolean} gasOptimizer - Enable or disable gas optimization
62
+ * @param {boolean} viaIR - Enable or disable IR-based code generation
63
+ * @param {number} [optimizerRuns=200] - Number of times the optimizer should run
64
+ * @returns {Object|null} Updated compiler configuration or null on error
65
+ * @throws {Error} If parameters are invalid
66
+ * @example
67
+ * compilerOptions(true, false, 1000);
68
+ */
69
+ export function compilerOptions(gasOptimizer, viaIR, optimizerRuns = 200) {
70
+ try {
71
+ // Validate input parameters
72
+ if (typeof gasOptimizer !== 'boolean') {
73
+ throw new Error('Gas optimizer parameter must be a boolean');
74
+ }
75
+ if (typeof viaIR !== 'boolean') {
76
+ throw new Error('ViaIR parameter must be a boolean');
77
+ }
78
+ if (typeof optimizerRuns !== 'number' || optimizerRuns < 1) {
79
+ throw new Error('Optimizer runs must be a positive number');
80
+ }
81
+
82
+ // Update global configuration
83
+ compilerConfig.optimizer = gasOptimizer;
84
+ compilerConfig.viaIR = viaIR;
85
+ compilerConfig.optimizerRuns = optimizerRuns;
86
+
87
+ // Provide user feedback
88
+ console.log('✓ Compiler options updated:');
89
+ console.log(` Gas Optimizer: ${compilerConfig.optimizer ? 'Enabled' : 'Disabled'}`);
90
+ if (compilerConfig.optimizer) {
91
+ console.log(` Optimizer Runs: ${compilerConfig.optimizerRuns}`);
92
+ }
93
+ console.log(` ViaIR: ${compilerConfig.viaIR ? 'Enabled' : 'Disabled'}`);
94
+
95
+ return compilerConfig;
96
+ } catch (error) {
97
+ console.error('Error setting compiler options:', error.message);
98
+ return null;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Get current compiler options
104
+ * @returns {Object} Copy of current compiler configuration
105
+ * @example
106
+ * const opts = getCompilerOptions();
107
+ */
108
+ export function getCompilerOptions() {
109
+ return { ...compilerConfig };
110
+ }
111
+
112
+ /**
113
+ * Compile Solidity smart contract(s)
114
+ * @param {string} [fullPath] - Path to contract file or directory. Defaults to './contracts'
115
+ * @param {Array<string>} [selectedContracts] - Array of specific contract names to compile
116
+ * @param {string} [buildPath] - Output directory for compiled artifacts. Defaults to './build'
117
+ * @returns {void}
118
+ * @throws {Error} If no contracts found or compilation fails
119
+ * @example
120
+ * compile(); // Compile all contracts in './contracts'
121
+ * compile('./contracts/MyToken.sol'); // Compile specific file
122
+ * compile('./contracts', ['MyToken'], './output'); // Compile specific contracts to custom path
123
+ */
124
+ export function compile(fullPath, selectedContracts, buildPath){
125
+ try{
126
+ // Set default path if buildPath is undefined
127
+ if(!buildPath){
128
+ buildPath = path.resolve('.', 'build');
129
+ [buildPath].forEach(check);
130
+ }
131
+
132
+ let fileExt;
133
+ if(!fullPath) {
134
+ fullPath = path.resolve('.', 'contracts');
135
+ } else {
136
+ fileExt = path.extname(fullPath);
137
+ }
138
+
139
+ if(!fileExt){
140
+ const solFiles = collectSolFiles(fullPath);
141
+
142
+ if(!solFiles.length){
143
+ throw 'There is no smart contract in the directory!';
144
+ } else {
145
+ for(let i = 0; i < solFiles.length; i++){
146
+ build(solFiles[i], selectedContracts, buildPath);
147
+ }
148
+ console.log(`Contracts compiled into ${path.resolve(buildPath)}`);
149
+ }
150
+ } else {
151
+ build(fullPath, selectedContracts, buildPath);
152
+ console.log(`Contract compiled into ${path.resolve(buildPath)}`);
153
+ }
154
+ } catch(err){
155
+ console.error(err);
156
+ }
157
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @fileoverview Contract retrieval and management
3
+ * @description Provides functions to retrieve contract information from the
4
+ * contract registry by various identifiers (index, address, or name).
5
+ * @module contracts
6
+ */
7
+
8
+ import { ethers } from 'ethers';
9
+ import { getContArr } from '../utils/contractLister.js';
10
+
11
+ /**
12
+ * Get contract(s) information
13
+ * @async
14
+ * @param {number|string|null} [contPointer] - Contract identifier (index, address, or name).
15
+ * If omitted, returns all contracts.
16
+ * @returns {Promise<void>}
17
+ * @throws {Error} If the input is not valid
18
+ * @example
19
+ * await getContracts(); // Get all contracts
20
+ * await getContracts(0); // Get contract by index
21
+ * await getContracts('0x1234...'); // Get contract by address
22
+ * await getContracts('MyToken'); // Get contract by name
23
+ */
24
+ export async function getContracts(contPointer) {
25
+ const contArray = await getContArr();
26
+ let result;
27
+
28
+ if(!contPointer && contPointer != 0) {
29
+ result = contArray;
30
+ } else if(typeof contPointer === 'number') {
31
+ result = contArray[contPointer];
32
+ } else if(ethers.isAddress(contPointer)) {
33
+ const index = contArray.findIndex(contract => contract.address == contPointer);
34
+ result = contArray[index];
35
+ } else if(typeof contPointer === 'string') {
36
+ const index = contArray.findIndex(contract => contract.name == contPointer);
37
+ result = contArray[index];
38
+ } else {
39
+ throw new Error('Input is NOT valid!');
40
+ }
41
+
42
+ console.log(result);
43
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @fileoverview File system utilities for directory management
3
+ * @description Provides utilities for deleting directories recursively,
4
+ * primarily used for cleaning build directories.
5
+ * @module files
6
+ */
7
+
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+
11
+ /**
12
+ * Delete a directory recursively
13
+ * @param {string} [dirPath] - Path to the directory to delete. Defaults to './build'
14
+ * @returns {void}
15
+ * @example
16
+ * deleteDirectory('../build');
17
+ * deleteDirectory(); // Deletes default './build' directory
18
+ */
19
+ export function deleteDirectory(dirPath){
20
+ try {
21
+ if(!dirPath){
22
+ dirPath = path.resolve('..', 'build');
23
+ }
24
+ // Check if the directory exists
25
+ if(!fs.existsSync(dirPath)){
26
+ console.log('Path is not a directory');
27
+ return; }
28
+
29
+ // For Node.js 14.14.0+ (recommended)
30
+ fs.rmSync(dirPath, { recursive: true, force: true });
31
+
32
+ console.log('Directory deleted successfully');
33
+ } catch (err) {
34
+ if (err.code === 'ENOENT') {
35
+ console.log('Directory does not exist');
36
+ } else {
37
+ console.error('Error deleting directory:', err);
38
+ }
39
+ }
40
+ }