katour 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/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
package/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # ⛓ Katour CLI
2
+
3
+ Multi-chain Solidity deployment CLI — compile, deploy, verify, and interact with contracts in one tool.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # Clone / copy the project
9
+ cd katour
10
+ npm install
11
+ npm link # makes `katour` available globally
12
+ ```
13
+
14
+ ## Quick Start
15
+
16
+ ```bash
17
+ # 1. Initialize a project
18
+ katour init
19
+
20
+ # 2. Edit .env — add your PRIVATE_KEY and API keys
21
+
22
+ # 3. Compile contracts
23
+ katour compile
24
+
25
+ # 4. Deploy
26
+ katour deploy
27
+
28
+ # 5. Interact with deployed contract
29
+ katour interact <address>
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Commands
35
+
36
+ ### `katour init`
37
+ Scaffolds the project structure:
38
+ ```
39
+ contracts/ ← put your .sol files here
40
+ artifacts/ ← compiled output (auto-generated)
41
+ deployments/ ← deployment records per network
42
+ .env ← config (private key, RPC, API keys)
43
+ ```
44
+
45
+ ---
46
+
47
+ ### `katour compile`
48
+ Compiles all contracts in `./contracts/`.
49
+
50
+ ```bash
51
+ katour compile # compile all
52
+ katour compile -f contracts/Token.sol # specific file
53
+ katour compile -o ./out # custom output dir
54
+ ```
55
+
56
+ ---
57
+
58
+ ### `katour deploy`
59
+ Deploys a compiled contract. Interactive prompts if no flags set.
60
+
61
+ ```bash
62
+ katour deploy # interactive
63
+ katour deploy -c Token -n sepolia # specific contract + network
64
+ katour deploy -c Token -n mainnet --verify # deploy + auto verify
65
+ katour deploy -c Token -n sepolia --dry-run # simulate only
66
+ katour deploy -c Token -n polygon --args "MyToken" "MTK" 1000000
67
+ ```
68
+
69
+ ---
70
+
71
+ ### `katour verify`
72
+ Verifies a deployed contract on the block explorer.
73
+
74
+ ```bash
75
+ katour verify 0xAbC... -c Token -n sepolia
76
+ katour verify 0xAbC... -c Token -n mainnet --args "MyToken" "MTK" 1000000
77
+ ```
78
+
79
+ ---
80
+
81
+ ### `katour interact`
82
+ Interactive REPL to call any contract function.
83
+
84
+ ```bash
85
+ katour interact 0xAbC... # interactive network + contract selection
86
+ katour interact 0xAbC... -n polygon -c Token
87
+ ```
88
+
89
+ Lists all read (`view/pure`) and write functions with prompts for arguments.
90
+
91
+ ---
92
+
93
+ ## Supported Networks
94
+
95
+ | Key | Name | Explorer |
96
+ |---------------|-----------------------|----------------------|
97
+ | `mainnet` | Ethereum Mainnet | etherscan.io |
98
+ | `sepolia` | Sepolia Testnet | sepolia.etherscan.io |
99
+ | `polygon` | Polygon Mainnet | polygonscan.com |
100
+ | `mumbai` | Polygon Mumbai | mumbai.polygonscan |
101
+ | `bsc` | BNB Smart Chain | bscscan.com |
102
+ | `bscTestnet` | BSC Testnet | testnet.bscscan.com |
103
+ | `arbitrum` | Arbitrum One | arbiscan.io |
104
+ | `optimism` | Optimism | optimistic.etherscan |
105
+ | `avalanche` | Avalanche C-Chain | snowtrace.io |
106
+ | `base` | Base | basescan.org |
107
+ | `baseSepolia` | Base Sepolia | sepolia.basescan.org |
108
+
109
+ ---
110
+
111
+ ## .env Reference
112
+
113
+ ```env
114
+ # Required
115
+ PRIVATE_KEY=your_wallet_private_key
116
+
117
+ # Optional - custom RPC endpoints
118
+ ETH_MAINNET_RPC=https://eth.llamarpc.com
119
+ ETH_SEPOLIA_RPC=https://rpc.sepolia.org
120
+ POLYGON_RPC=https://polygon-rpc.com
121
+ BSC_RPC=https://bsc-dataseed.binance.org
122
+
123
+ # For verification (get free keys from each explorer)
124
+ ETHERSCAN_API_KEY=...
125
+ POLYGONSCAN_API_KEY=...
126
+ BSCSCAN_API_KEY=...
127
+ ARBISCAN_API_KEY=...
128
+ BASESCAN_API_KEY=...
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Deployment Records
134
+
135
+ Every successful deployment is saved to `./deployments/<network>.json`:
136
+
137
+ ```json
138
+ {
139
+ "MyContract": {
140
+ "address": "0x...",
141
+ "txHash": "0x...",
142
+ "constructorArgs": ["Hello", "42"],
143
+ "deployedAt": "2024-01-01T00:00:00.000Z"
144
+ }
145
+ }
146
+ ```
147
+
148
+ ---
149
+
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "katour",
3
+ "version": "1.0.0",
4
+ "description": "Multi-chain Solidity contract deployment CLI — compile, deploy, verify, interact",
5
+ "type": "module",
6
+ "bin": {
7
+ "katour": "./src/index.js"
8
+ },
9
+ "scripts": {
10
+ "dev": "node src/index.js",
11
+ "link": "npm link"
12
+ },
13
+ "dependencies": {
14
+ "chalk": "^5.3.0",
15
+ "commander": "^12.1.0",
16
+ "ethers": "^6.13.0",
17
+ "figlet": "^1.7.0",
18
+ "inquirer": "^10.1.0",
19
+ "ora": "^8.0.1",
20
+ "solc": "^0.8.26",
21
+ "axios": "^1.7.0",
22
+ "dotenv": "^16.4.5",
23
+ "fs-extra": "^11.2.0",
24
+ "glob": "^11.0.0"
25
+ },
26
+ "engines": {
27
+ "node": ">=18.0.0"
28
+ }
29
+ }
@@ -0,0 +1,92 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { glob } from 'glob';
4
+ import solc from 'solc';
5
+ import chalk from 'chalk';
6
+ import ora from 'ora';
7
+
8
+ export async function compileCommand(options) {
9
+ const spinner = ora('Finding Solidity files...').start();
10
+
11
+ try {
12
+ const outputDir = options.output || './artifacts';
13
+ await fs.ensureDir(outputDir);
14
+
15
+ // Find contract files
16
+ let files = [];
17
+ if (options.file) {
18
+ if (!fs.existsSync(options.file)) {
19
+ spinner.fail(chalk.red(`File not found: ${options.file}`));
20
+ process.exit(1);
21
+ }
22
+ files = [options.file];
23
+ } else {
24
+ files = await glob('contracts/**/*.sol');
25
+ if (files.length === 0) {
26
+ spinner.fail(chalk.red('No .sol files found in ./contracts/'));
27
+ process.exit(1);
28
+ }
29
+ }
30
+
31
+ spinner.text = `Compiling ${files.length} contract(s)...`;
32
+
33
+ const compiled = [];
34
+
35
+ for (const filePath of files) {
36
+ const source = fs.readFileSync(filePath, 'utf8');
37
+ const fileName = path.basename(filePath);
38
+
39
+ // Build solc input
40
+ const input = {
41
+ language: 'Solidity',
42
+ sources: { [fileName]: { content: source } },
43
+ settings: {
44
+ outputSelection: {
45
+ '*': { '*': ['abi', 'evm.bytecode', 'evm.deployedBytecode', 'metadata'] },
46
+ },
47
+ optimizer: { enabled: true, runs: 200 },
48
+ },
49
+ };
50
+
51
+ const output = JSON.parse(solc.compile(JSON.stringify(input)));
52
+
53
+ // Check errors
54
+ if (output.errors) {
55
+ const errors = output.errors.filter(e => e.severity === 'error');
56
+ if (errors.length > 0) {
57
+ spinner.fail(chalk.red(`Compilation errors in ${fileName}:`));
58
+ errors.forEach(e => console.error(chalk.red(` ${e.formattedMessage}`)));
59
+ process.exit(1);
60
+ }
61
+ const warnings = output.errors.filter(e => e.severity === 'warning');
62
+ warnings.forEach(w => console.warn(chalk.yellow(` ⚠ ${w.message}`)));
63
+ }
64
+
65
+ // Save artifacts
66
+ for (const contractName of Object.keys(output.contracts[fileName] || {})) {
67
+ const artifact = {
68
+ contractName,
69
+ sourcePath: filePath,
70
+ abi: output.contracts[fileName][contractName].abi,
71
+ bytecode: output.contracts[fileName][contractName].evm.bytecode.object,
72
+ deployedBytecode: output.contracts[fileName][contractName].evm.deployedBytecode.object,
73
+ metadata: output.contracts[fileName][contractName].metadata,
74
+ compiledAt: new Date().toISOString(),
75
+ };
76
+
77
+ const outFile = path.join(outputDir, `${contractName}.json`);
78
+ await fs.writeJSON(outFile, artifact, { spaces: 2 });
79
+ compiled.push({ contractName, outFile });
80
+ }
81
+ }
82
+
83
+ spinner.succeed(chalk.green(`Compiled ${compiled.length} contract(s) successfully!`));
84
+ compiled.forEach(({ contractName, outFile }) => {
85
+ console.log(chalk.gray(` ✔ ${contractName} → ${outFile}`));
86
+ });
87
+
88
+ } catch (err) {
89
+ spinner.fail(chalk.red(`Compilation failed: ${err.message}`));
90
+ process.exit(1);
91
+ }
92
+ }
@@ -0,0 +1,162 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import inquirer from 'inquirer';
6
+ import { ethers } from 'ethers';
7
+ import { getNetwork, NETWORKS } from '../config/networks.js';
8
+ import { compileCommand } from './compile.js';
9
+ import { verifyContract } from '../utils/verify.js';
10
+ import { saveDeployment } from '../utils/deployments.js';
11
+ import dotenv from 'dotenv';
12
+ dotenv.config();
13
+
14
+ export async function deployCommand(options) {
15
+ console.log(chalk.cyan('\n🚀 Katour Deploy\n'));
16
+
17
+ try {
18
+ // 1. Pick network interactively if not specified
19
+ let networkName = options.network;
20
+ if (!networkName) {
21
+ const { net } = await inquirer.prompt([{
22
+ type: 'list',
23
+ name: 'net',
24
+ message: 'Select target network:',
25
+ choices: Object.entries(NETWORKS).map(([key, v]) => ({
26
+ name: `${v.name} (${v.currency})`,
27
+ value: key,
28
+ })),
29
+ }]);
30
+ networkName = net;
31
+ }
32
+
33
+ const network = getNetwork(networkName);
34
+ console.log(chalk.gray(` Network: ${network.name} (chainId: ${network.chainId})`));
35
+
36
+ // 2. Pick contract
37
+ let contractName = options.contract;
38
+ const artifactsDir = './artifacts';
39
+
40
+ if (!fs.existsSync(artifactsDir) || fs.readdirSync(artifactsDir).length === 0) {
41
+ console.log(chalk.yellow(' No artifacts found. Running compile first...\n'));
42
+ await compileCommand({ output: artifactsDir });
43
+ }
44
+
45
+ const artifacts = fs.readdirSync(artifactsDir)
46
+ .filter(f => f.endsWith('.json'))
47
+ .map(f => f.replace('.json', ''));
48
+
49
+ if (!contractName) {
50
+ if (artifacts.length === 1) {
51
+ contractName = artifacts[0];
52
+ console.log(chalk.gray(` Contract: ${contractName} (auto-selected)`));
53
+ } else {
54
+ const { contract } = await inquirer.prompt([{
55
+ type: 'list',
56
+ name: 'contract',
57
+ message: 'Select contract to deploy:',
58
+ choices: artifacts,
59
+ }]);
60
+ contractName = contract;
61
+ }
62
+ }
63
+
64
+ const artifactPath = path.join(artifactsDir, `${contractName}.json`);
65
+ if (!fs.existsSync(artifactPath)) {
66
+ console.error(chalk.red(`Artifact not found: ${artifactPath}`));
67
+ process.exit(1);
68
+ }
69
+
70
+ const artifact = await fs.readJSON(artifactPath);
71
+
72
+ // 3. Constructor args
73
+ const constructorAbi = artifact.abi.find(x => x.type === 'constructor');
74
+ let constructorArgs = options.args || [];
75
+
76
+ if (constructorAbi && constructorAbi.inputs.length > 0 && constructorArgs.length === 0) {
77
+ console.log(chalk.yellow(`\n Constructor requires ${constructorAbi.inputs.length} argument(s):`));
78
+ const answers = await inquirer.prompt(
79
+ constructorAbi.inputs.map((inp, i) => ({
80
+ type: 'input',
81
+ name: `arg_${i}`,
82
+ message: ` ${inp.name} (${inp.type}):`,
83
+ }))
84
+ );
85
+ constructorArgs = Object.values(answers);
86
+ }
87
+
88
+ // 4. Get private key
89
+ const privateKey = process.env.PRIVATE_KEY;
90
+ if (!privateKey) {
91
+ console.error(chalk.red('\n ❌ PRIVATE_KEY not found in .env'));
92
+ console.log(chalk.gray(' Create a .env file with: PRIVATE_KEY=0xyour_key'));
93
+ process.exit(1);
94
+ }
95
+
96
+ // 5. Dry run check
97
+ const provider = new ethers.JsonRpcProvider(network.rpc);
98
+ const wallet = new ethers.Wallet(privateKey, provider);
99
+ const balance = await provider.getBalance(wallet.address);
100
+ const balanceEth = ethers.formatEther(balance);
101
+
102
+ console.log(chalk.gray(`\n Deployer: ${wallet.address}`));
103
+ console.log(chalk.gray(` Balance: ${balanceEth} ${network.currency}`));
104
+
105
+ // Estimate gas
106
+ const factory = new ethers.ContractFactory(artifact.abi, artifact.bytecode, wallet);
107
+ const deployTx = await factory.getDeployTransaction(...constructorArgs);
108
+ const gasEstimate = await provider.estimateGas({ ...deployTx, from: wallet.address });
109
+ const feeData = await provider.getFeeData();
110
+ const estimatedCost = gasEstimate * (feeData.gasPrice || 0n);
111
+
112
+ console.log(chalk.gray(` Gas est: ${gasEstimate.toString()}`));
113
+ console.log(chalk.gray(` Est cost: ~${ethers.formatEther(estimatedCost)} ${network.currency}\n`));
114
+
115
+ if (options.dryRun) {
116
+ console.log(chalk.yellow(' 🔍 Dry run mode — no transaction sent.'));
117
+ return;
118
+ }
119
+
120
+ // 6. Confirm
121
+ const { confirmed } = await inquirer.prompt([{
122
+ type: 'confirm',
123
+ name: 'confirmed',
124
+ message: `Deploy ${contractName} to ${network.name}?`,
125
+ default: true,
126
+ }]);
127
+
128
+ if (!confirmed) {
129
+ console.log(chalk.yellow(' Deployment cancelled.'));
130
+ return;
131
+ }
132
+
133
+ // 7. Deploy
134
+ const spinner = ora(`Deploying ${contractName}...`).start();
135
+ const contract = await factory.deploy(...constructorArgs);
136
+ spinner.text = `Waiting for transaction... (${contract.deploymentTransaction().hash})`;
137
+
138
+ const receipt = await contract.deploymentTransaction().wait(1);
139
+ const address = await contract.getAddress();
140
+
141
+ spinner.succeed(chalk.green(`Deployed successfully!`));
142
+ console.log(chalk.green(`\n ✅ Contract address: ${chalk.bold(address)}`));
143
+ console.log(chalk.gray(` Tx hash: ${receipt.hash}`));
144
+ console.log(chalk.gray(` Block: ${receipt.blockNumber}`));
145
+ console.log(chalk.blue(` Explorer: ${network.explorer}/address/${address}\n`));
146
+
147
+ // 8. Save deployment record
148
+ await saveDeployment({ contractName, address, network: networkName, txHash: receipt.hash, args: constructorArgs });
149
+
150
+ // 9. Auto verify if flag set
151
+ if (options.verify) {
152
+ await verifyContract({ contractName, address, networkName, constructorArgs, artifact });
153
+ } else {
154
+ console.log(chalk.gray(` Tip: Run ${chalk.cyan(`katour verify ${address} -c ${contractName} -n ${networkName}`)} to verify\n`));
155
+ }
156
+
157
+ } catch (err) {
158
+ console.error(chalk.red(`\n Deploy failed: ${err.message}`));
159
+ if (process.env.DEBUG) console.error(err);
160
+ process.exit(1);
161
+ }
162
+ }
@@ -0,0 +1,109 @@
1
+ import fs from 'fs-extra';
2
+ import chalk from 'chalk';
3
+ import inquirer from 'inquirer';
4
+ import path from 'path';
5
+
6
+ const EXAMPLE_CONTRACT = `// SPDX-License-Identifier: MIT
7
+ pragma solidity ^0.8.20;
8
+
9
+ contract MyContract {
10
+ string public name;
11
+ address public owner;
12
+ uint256 public value;
13
+
14
+ event ValueUpdated(uint256 oldValue, uint256 newValue);
15
+
16
+ constructor(string memory _name, uint256 _initialValue) {
17
+ name = _name;
18
+ owner = msg.sender;
19
+ value = _initialValue;
20
+ }
21
+
22
+ modifier onlyOwner() {
23
+ require(msg.sender == owner, "Not owner");
24
+ _;
25
+ }
26
+
27
+ function setValue(uint256 _newValue) external onlyOwner {
28
+ emit ValueUpdated(value, _newValue);
29
+ value = _newValue;
30
+ }
31
+
32
+ function getValue() external view returns (uint256) {
33
+ return value;
34
+ }
35
+ }
36
+ `;
37
+
38
+ const ENV_TEMPLATE = `# ========================================
39
+ # Katour Environment Config
40
+ # ========================================
41
+
42
+ # Your deployer wallet private key (without 0x)
43
+ PRIVATE_KEY=your_private_key_here
44
+
45
+ # RPC endpoints (optional — defaults to public RPCs)
46
+ ETH_MAINNET_RPC=https://eth.llamarpc.com
47
+ ETH_SEPOLIA_RPC=https://rpc.sepolia.org
48
+ POLYGON_RPC=https://polygon-rpc.com
49
+ BSC_RPC=https://bsc-dataseed.binance.org
50
+
51
+ # Block explorer API keys (for verification)
52
+ ETHERSCAN_API_KEY=your_etherscan_key
53
+ POLYGONSCAN_API_KEY=your_polygonscan_key
54
+ BSCSCAN_API_KEY=your_bscscan_key
55
+ ARBISCAN_API_KEY=your_arbiscan_key
56
+ BASESCAN_API_KEY=your_basescan_key
57
+ `;
58
+
59
+ export async function initCommand() {
60
+ console.log(chalk.cyan('\n🛠 Katour Init\n'));
61
+
62
+ const { projectName } = await inquirer.prompt([{
63
+ type: 'input',
64
+ name: 'projectName',
65
+ message: 'Project name:',
66
+ default: path.basename(process.cwd()),
67
+ }]);
68
+
69
+ const { createExample } = await inquirer.prompt([{
70
+ type: 'confirm',
71
+ name: 'createExample',
72
+ message: 'Create example contract?',
73
+ default: true,
74
+ }]);
75
+
76
+ // Create folders
77
+ await fs.ensureDir('./contracts');
78
+ await fs.ensureDir('./artifacts');
79
+ await fs.ensureDir('./deployments');
80
+
81
+ // Create .env if not exists
82
+ if (!fs.existsSync('./.env')) {
83
+ await fs.writeFile('./.env', ENV_TEMPLATE);
84
+ console.log(chalk.green(' ✔ .env created'));
85
+ } else {
86
+ console.log(chalk.gray(' ℹ .env already exists, skipping'));
87
+ }
88
+
89
+ // .gitignore
90
+ const gitignore = `node_modules/\nartifacts/\n.env\ndeployments/\n`;
91
+ await fs.writeFile('./.gitignore', gitignore);
92
+ console.log(chalk.green(' ✔ .gitignore created'));
93
+
94
+ // Example contract
95
+ if (createExample) {
96
+ const contractPath = './contracts/MyContract.sol';
97
+ if (!fs.existsSync(contractPath)) {
98
+ await fs.writeFile(contractPath, EXAMPLE_CONTRACT);
99
+ console.log(chalk.green(' ✔ contracts/MyContract.sol created'));
100
+ }
101
+ }
102
+
103
+ console.log(chalk.green(`\n ✅ Project "${projectName}" initialized!\n`));
104
+ console.log(chalk.cyan(' Next steps:'));
105
+ console.log(chalk.gray(' 1. Edit .env and add your PRIVATE_KEY'));
106
+ console.log(chalk.gray(' 2. Add your contracts to ./contracts/'));
107
+ console.log(chalk.gray(' 3. Run: katour compile'));
108
+ console.log(chalk.gray(' 4. Run: katour deploy\n'));
109
+ }
@@ -0,0 +1,174 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import ora from 'ora';
4
+ import { ethers } from 'ethers';
5
+ import fs from 'fs-extra';
6
+ import path from 'path';
7
+ import { getNetwork, NETWORKS } from '../config/networks.js';
8
+ import dotenv from 'dotenv';
9
+ dotenv.config();
10
+
11
+ export async function interactCommand(address, options) {
12
+ console.log(chalk.cyan('\n⚡ Katour Interact\n'));
13
+
14
+ try {
15
+ // Network
16
+ let networkName = options.network;
17
+ if (!networkName) {
18
+ const { net } = await inquirer.prompt([{
19
+ type: 'list',
20
+ name: 'net',
21
+ message: 'Select network:',
22
+ choices: Object.entries(NETWORKS).map(([key, v]) => ({
23
+ name: `${v.name}`,
24
+ value: key,
25
+ })),
26
+ }]);
27
+ networkName = net;
28
+ }
29
+
30
+ const network = getNetwork(networkName);
31
+ const provider = new ethers.JsonRpcProvider(network.rpc);
32
+
33
+ // Load ABI
34
+ let contractName = options.contract;
35
+ if (!contractName) {
36
+ const artifacts = fs.readdirSync('./artifacts').filter(f => f.endsWith('.json'));
37
+ if (artifacts.length === 0) {
38
+ console.error(chalk.red(' No artifacts found. Run katour compile first.'));
39
+ process.exit(1);
40
+ }
41
+ const { c } = await inquirer.prompt([{
42
+ type: 'list',
43
+ name: 'c',
44
+ message: 'Select contract (for ABI):',
45
+ choices: artifacts.map(f => f.replace('.json', '')),
46
+ }]);
47
+ contractName = c;
48
+ }
49
+
50
+ const artifact = await fs.readJSON(path.join('./artifacts', `${contractName}.json`));
51
+ const abi = artifact.abi;
52
+
53
+ // Get all callable functions
54
+ const readFns = abi.filter(x => x.type === 'function' && (x.stateMutability === 'view' || x.stateMutability === 'pure'));
55
+ const writeFns = abi.filter(x => x.type === 'function' && x.stateMutability !== 'view' && x.stateMutability !== 'pure');
56
+
57
+ // Create contract instance (read-only first)
58
+ let contract = new ethers.Contract(address, abi, provider);
59
+
60
+ console.log(chalk.gray(` Contract: ${address}`));
61
+ console.log(chalk.gray(` Network: ${network.name}`));
62
+ console.log(chalk.gray(` Read fns: ${readFns.length} | Write fns: ${writeFns.length}\n`));
63
+
64
+ // Interactive loop
65
+ while (true) {
66
+ const allFunctions = [
67
+ ...readFns.map(f => ({ name: `📖 ${f.name}(${f.inputs.map(i => i.type).join(', ')}) [read]`, value: { fn: f, type: 'read' } })),
68
+ ...writeFns.map(f => ({ name: `✏️ ${f.name}(${f.inputs.map(i => i.type).join(', ')}) [write]`, value: { fn: f, type: 'write' } })),
69
+ new inquirer.Separator(),
70
+ { name: '🚪 Exit', value: null },
71
+ ];
72
+
73
+ const { selected } = await inquirer.prompt([{
74
+ type: 'list',
75
+ name: 'selected',
76
+ message: 'Select a function to call:',
77
+ choices: allFunctions,
78
+ pageSize: 20,
79
+ }]);
80
+
81
+ if (!selected) break;
82
+
83
+ const { fn, type } = selected;
84
+
85
+ // Collect inputs
86
+ let callArgs = [];
87
+ if (fn.inputs.length > 0) {
88
+ const answers = await inquirer.prompt(
89
+ fn.inputs.map((inp, i) => ({
90
+ type: 'input',
91
+ name: `arg_${i}`,
92
+ message: ` ${inp.name || `arg${i}`} (${inp.type}):`,
93
+ }))
94
+ );
95
+ callArgs = fn.inputs.map((inp, i) => {
96
+ const val = answers[`arg_${i}`];
97
+ // Parse arrays and tuples
98
+ if (inp.type.endsWith('[]') || inp.type === 'tuple') {
99
+ try { return JSON.parse(val); } catch { return val; }
100
+ }
101
+ return val;
102
+ });
103
+ }
104
+
105
+ if (type === 'read') {
106
+ // Read call
107
+ const spinner = ora(`Calling ${fn.name}...`).start();
108
+ try {
109
+ const result = await contract[fn.name](...callArgs);
110
+ spinner.succeed(chalk.green(`Result:`));
111
+ if (Array.isArray(result)) {
112
+ result.forEach((r, i) => console.log(chalk.cyan(` [${i}]: ${r.toString()}`)));
113
+ } else {
114
+ console.log(chalk.cyan(` → ${result.toString()}`));
115
+ }
116
+ } catch (err) {
117
+ spinner.fail(chalk.red(`Error: ${err.message}`));
118
+ }
119
+ } else {
120
+ // Write call - need wallet
121
+ const privateKey = process.env.PRIVATE_KEY;
122
+ if (!privateKey) {
123
+ console.error(chalk.red(' PRIVATE_KEY not set in .env'));
124
+ continue;
125
+ }
126
+
127
+ const wallet = new ethers.Wallet(privateKey, provider);
128
+ contract = contract.connect(wallet);
129
+
130
+ // Check if payable
131
+ let value = 0n;
132
+ if (fn.stateMutability === 'payable') {
133
+ const { ethValue } = await inquirer.prompt([{
134
+ type: 'input',
135
+ name: 'ethValue',
136
+ message: ` ETH to send (in ${network.currency}):`,
137
+ default: '0',
138
+ }]);
139
+ value = ethers.parseEther(ethValue || '0');
140
+ }
141
+
142
+ const { confirmed } = await inquirer.prompt([{
143
+ type: 'confirm',
144
+ name: 'confirmed',
145
+ message: `Send transaction to call ${fn.name}?`,
146
+ default: true,
147
+ }]);
148
+
149
+ if (!confirmed) continue;
150
+
151
+ const spinner = ora(`Sending transaction...`).start();
152
+ try {
153
+ const tx = await contract[fn.name](...callArgs, { value });
154
+ spinner.text = `Waiting for confirmation... (${tx.hash})`;
155
+ const receipt = await tx.wait(1);
156
+ spinner.succeed(chalk.green(`Transaction confirmed!`));
157
+ console.log(chalk.gray(` Tx hash: ${receipt.hash}`));
158
+ console.log(chalk.gray(` Block: ${receipt.blockNumber}`));
159
+ console.log(chalk.blue(` ${network.explorer}/tx/${receipt.hash}`));
160
+ } catch (err) {
161
+ spinner.fail(chalk.red(`Transaction failed: ${err.message}`));
162
+ }
163
+ }
164
+
165
+ console.log('');
166
+ }
167
+
168
+ console.log(chalk.gray('\n Goodbye! 👋\n'));
169
+
170
+ } catch (err) {
171
+ console.error(chalk.red(`\n Error: ${err.message}`));
172
+ process.exit(1);
173
+ }
174
+ }
@@ -0,0 +1,33 @@
1
+ import chalk from 'chalk';
2
+ import { getNetwork } from '../config/networks.js';
3
+ import { verifyContract } from '../utils/verify.js';
4
+ import fs from 'fs-extra';
5
+ import path from 'path';
6
+
7
+ export async function verifyCommand(address, options) {
8
+ console.log(chalk.cyan('\n🔍 Katour Verify\n'));
9
+
10
+ const networkName = options.network || 'mainnet';
11
+ const contractName = options.contract;
12
+
13
+ if (!contractName) {
14
+ console.error(chalk.red(' Please provide contract name with -c flag'));
15
+ process.exit(1);
16
+ }
17
+
18
+ const artifactPath = path.join('./artifacts', `${contractName}.json`);
19
+ if (!fs.existsSync(artifactPath)) {
20
+ console.error(chalk.red(` Artifact not found: ${artifactPath}. Run katour compile first.`));
21
+ process.exit(1);
22
+ }
23
+
24
+ const artifact = await fs.readJSON(artifactPath);
25
+
26
+ await verifyContract({
27
+ contractName,
28
+ address,
29
+ networkName,
30
+ constructorArgs: options.args || [],
31
+ artifact,
32
+ });
33
+ }
@@ -0,0 +1,127 @@
1
+ // Supported networks config
2
+ export const NETWORKS = {
3
+ // Ethereum
4
+ mainnet: {
5
+ name: 'Ethereum Mainnet',
6
+ chainId: 1,
7
+ rpc: process.env.ETH_MAINNET_RPC || 'https://eth.llamarpc.com',
8
+ explorer: 'https://etherscan.io',
9
+ explorerApi: 'https://api.etherscan.io/api',
10
+ explorerApiKeyEnv: 'ETHERSCAN_API_KEY',
11
+ currency: 'ETH',
12
+ },
13
+ sepolia: {
14
+ name: 'Sepolia Testnet',
15
+ chainId: 11155111,
16
+ rpc: process.env.ETH_SEPOLIA_RPC || 'https://rpc.sepolia.org',
17
+ explorer: 'https://sepolia.etherscan.io',
18
+ explorerApi: 'https://api-sepolia.etherscan.io/api',
19
+ explorerApiKeyEnv: 'ETHERSCAN_API_KEY',
20
+ currency: 'ETH',
21
+ },
22
+ goerli: {
23
+ name: 'Goerli Testnet',
24
+ chainId: 5,
25
+ rpc: process.env.ETH_GOERLI_RPC || 'https://rpc.ankr.com/eth_goerli',
26
+ explorer: 'https://goerli.etherscan.io',
27
+ explorerApi: 'https://api-goerli.etherscan.io/api',
28
+ explorerApiKeyEnv: 'ETHERSCAN_API_KEY',
29
+ currency: 'ETH',
30
+ },
31
+ // Polygon
32
+ polygon: {
33
+ name: 'Polygon Mainnet',
34
+ chainId: 137,
35
+ rpc: process.env.POLYGON_RPC || 'https://polygon-rpc.com',
36
+ explorer: 'https://polygonscan.com',
37
+ explorerApi: 'https://api.polygonscan.com/api',
38
+ explorerApiKeyEnv: 'POLYGONSCAN_API_KEY',
39
+ currency: 'MATIC',
40
+ },
41
+ mumbai: {
42
+ name: 'Polygon Mumbai',
43
+ chainId: 80001,
44
+ rpc: process.env.POLYGON_MUMBAI_RPC || 'https://rpc-mumbai.maticvigil.com',
45
+ explorer: 'https://mumbai.polygonscan.com',
46
+ explorerApi: 'https://api-testnet.polygonscan.com/api',
47
+ explorerApiKeyEnv: 'POLYGONSCAN_API_KEY',
48
+ currency: 'MATIC',
49
+ },
50
+ // BSC
51
+ bsc: {
52
+ name: 'BNB Smart Chain',
53
+ chainId: 56,
54
+ rpc: process.env.BSC_RPC || 'https://bsc-dataseed.binance.org',
55
+ explorer: 'https://bscscan.com',
56
+ explorerApi: 'https://api.bscscan.com/api',
57
+ explorerApiKeyEnv: 'BSCSCAN_API_KEY',
58
+ currency: 'BNB',
59
+ },
60
+ bscTestnet: {
61
+ name: 'BSC Testnet',
62
+ chainId: 97,
63
+ rpc: process.env.BSC_TESTNET_RPC || 'https://data-seed-prebsc-1-s1.binance.org:8545',
64
+ explorer: 'https://testnet.bscscan.com',
65
+ explorerApi: 'https://api-testnet.bscscan.com/api',
66
+ explorerApiKeyEnv: 'BSCSCAN_API_KEY',
67
+ currency: 'BNB',
68
+ },
69
+ // Arbitrum
70
+ arbitrum: {
71
+ name: 'Arbitrum One',
72
+ chainId: 42161,
73
+ rpc: process.env.ARBITRUM_RPC || 'https://arb1.arbitrum.io/rpc',
74
+ explorer: 'https://arbiscan.io',
75
+ explorerApi: 'https://api.arbiscan.io/api',
76
+ explorerApiKeyEnv: 'ARBISCAN_API_KEY',
77
+ currency: 'ETH',
78
+ },
79
+ // Optimism
80
+ optimism: {
81
+ name: 'Optimism',
82
+ chainId: 10,
83
+ rpc: process.env.OPTIMISM_RPC || 'https://mainnet.optimism.io',
84
+ explorer: 'https://optimistic.etherscan.io',
85
+ explorerApi: 'https://api-optimistic.etherscan.io/api',
86
+ explorerApiKeyEnv: 'OPTIMISM_API_KEY',
87
+ currency: 'ETH',
88
+ },
89
+ // Avalanche
90
+ avalanche: {
91
+ name: 'Avalanche C-Chain',
92
+ chainId: 43114,
93
+ rpc: process.env.AVALANCHE_RPC || 'https://api.avax.network/ext/bc/C/rpc',
94
+ explorer: 'https://snowtrace.io',
95
+ explorerApi: 'https://api.snowtrace.io/api',
96
+ explorerApiKeyEnv: 'SNOWTRACE_API_KEY',
97
+ currency: 'AVAX',
98
+ },
99
+ // Base
100
+ base: {
101
+ name: 'Base',
102
+ chainId: 8453,
103
+ rpc: process.env.BASE_RPC || 'https://mainnet.base.org',
104
+ explorer: 'https://basescan.org',
105
+ explorerApi: 'https://api.basescan.org/api',
106
+ explorerApiKeyEnv: 'BASESCAN_API_KEY',
107
+ currency: 'ETH',
108
+ },
109
+ baseSepolia: {
110
+ name: 'Base Sepolia',
111
+ chainId: 84532,
112
+ rpc: process.env.BASE_SEPOLIA_RPC || 'https://sepolia.base.org',
113
+ explorer: 'https://sepolia.basescan.org',
114
+ explorerApi: 'https://api-sepolia.basescan.org/api',
115
+ explorerApiKeyEnv: 'BASESCAN_API_KEY',
116
+ currency: 'ETH',
117
+ },
118
+ };
119
+
120
+ export function getNetwork(name) {
121
+ const net = NETWORKS[name];
122
+ if (!net) {
123
+ const available = Object.keys(NETWORKS).join(', ');
124
+ throw new Error(`Unknown network "${name}". Available: ${available}`);
125
+ }
126
+ return net;
127
+ }
package/src/index.js ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ import { program } from 'commander';
3
+ import chalk from 'chalk';
4
+ import figlet from 'figlet';
5
+ import { deployCommand } from './commands/deploy.js';
6
+ import { compileCommand } from './commands/compile.js';
7
+ import { verifyCommand } from './commands/verify.js';
8
+ import { interactCommand } from './commands/interact.js';
9
+ import { initCommand } from './commands/init.js';
10
+
11
+ // Banner
12
+ console.log(
13
+ chalk.cyan(figlet.textSync('katour', { font: 'Small' }))
14
+ );
15
+ console.log(chalk.gray(' ⛓ Multi-chain Solidity Deployment CLI\n'));
16
+
17
+ program
18
+ .name('katour')
19
+ .description('Deploy, compile, verify, and interact with Solidity contracts')
20
+ .version('1.0.0');
21
+
22
+ // katour init
23
+ program
24
+ .command('init')
25
+ .description('Initialize a new katour project')
26
+ .action(initCommand);
27
+
28
+ // katour compile
29
+ program
30
+ .command('compile')
31
+ .description('Compile Solidity contracts')
32
+ .option('-f, --file <path>', 'specific contract file to compile')
33
+ .option('-o, --output <dir>', 'output directory for artifacts', './artifacts')
34
+ .action(compileCommand);
35
+
36
+ // katour deploy
37
+ program
38
+ .command('deploy')
39
+ .description('Deploy a compiled contract')
40
+ .option('-c, --contract <name>', 'contract name to deploy')
41
+ .option('-n, --network <name>', 'target network (mainnet/sepolia/polygon/bsc/arbitrum...)')
42
+ .option('--args <args...>', 'constructor arguments')
43
+ .option('--verify', 'verify on Etherscan after deploy')
44
+ .option('--dry-run', 'simulate deployment without broadcasting')
45
+ .action(deployCommand);
46
+
47
+ // katour verify
48
+ program
49
+ .command('verify')
50
+ .description('Verify a deployed contract on block explorer')
51
+ .argument('<address>', 'deployed contract address')
52
+ .option('-c, --contract <name>', 'contract name')
53
+ .option('-n, --network <name>', 'network name')
54
+ .option('--args <args...>', 'constructor arguments used during deployment')
55
+ .action(verifyCommand);
56
+
57
+ // katour interact
58
+ program
59
+ .command('interact')
60
+ .description('Interact with a deployed contract')
61
+ .argument('<address>', 'deployed contract address')
62
+ .option('-c, --contract <name>', 'contract name (for ABI lookup)')
63
+ .option('-n, --network <name>', 'network name')
64
+ .action(interactCommand);
65
+
66
+ program.parse(process.argv);
@@ -0,0 +1,34 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ export async function saveDeployment({ contractName, address, network, txHash, args }) {
5
+ await fs.ensureDir('./deployments');
6
+
7
+ const filePath = `./deployments/${network}.json`;
8
+ let existing = {};
9
+
10
+ try {
11
+ if (fs.existsSync(filePath)) {
12
+ existing = await fs.readJSON(filePath);
13
+ }
14
+ } catch {}
15
+
16
+ existing[contractName] = {
17
+ address,
18
+ txHash,
19
+ constructorArgs: args,
20
+ deployedAt: new Date().toISOString(),
21
+ };
22
+
23
+ await fs.writeJSON(filePath, existing, { spaces: 2 });
24
+ }
25
+
26
+ export async function getDeployment(contractName, network) {
27
+ const filePath = `./deployments/${network}.json`;
28
+ try {
29
+ const data = await fs.readJSON(filePath);
30
+ return data[contractName] || null;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
@@ -0,0 +1,112 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import axios from 'axios';
4
+ import { getNetwork } from '../config/networks.js';
5
+ import { ethers } from 'ethers';
6
+ import dotenv from 'dotenv';
7
+ dotenv.config();
8
+
9
+ export async function verifyContract({ contractName, address, networkName, constructorArgs = [], artifact }) {
10
+ const network = getNetwork(networkName);
11
+ const apiKeyEnv = network.explorerApiKeyEnv;
12
+ const apiKey = process.env[apiKeyEnv];
13
+
14
+ if (!apiKey) {
15
+ console.warn(chalk.yellow(`\n ⚠ ${apiKeyEnv} not set — skipping verification`));
16
+ console.log(chalk.gray(` Get a key at ${network.explorer} and add to .env`));
17
+ return;
18
+ }
19
+
20
+ const spinner = ora(`Verifying ${contractName} on ${network.name}...`).start();
21
+
22
+ try {
23
+ // Encode constructor args
24
+ let encodedArgs = '';
25
+ if (constructorArgs.length > 0) {
26
+ const constructorAbi = artifact.abi.find(x => x.type === 'constructor');
27
+ if (constructorAbi) {
28
+ const types = constructorAbi.inputs.map(i => i.type);
29
+ encodedArgs = ethers.AbiCoder.defaultAbiCoder()
30
+ .encode(types, constructorArgs)
31
+ .slice(2); // remove 0x
32
+ }
33
+ }
34
+
35
+ // Extract solc version from metadata
36
+ let compilerVersion = '0.8.20';
37
+ try {
38
+ const metadata = JSON.parse(artifact.metadata || '{}');
39
+ compilerVersion = metadata.compiler?.version || compilerVersion;
40
+ } catch {}
41
+
42
+ // Submit verification
43
+ const params = new URLSearchParams({
44
+ apikey: apiKey,
45
+ module: 'contract',
46
+ action: 'verifysourcecode',
47
+ contractaddress: address,
48
+ sourceCode: artifact.metadata || '',
49
+ codeformat: 'solidity-single-file',
50
+ contractname: contractName,
51
+ compilerversion: `v${compilerVersion}`,
52
+ optimizationUsed: '1',
53
+ runs: '200',
54
+ constructorArguements: encodedArgs,
55
+ licenseType: '3', // MIT
56
+ });
57
+
58
+ const res = await axios.post(network.explorerApi, params.toString(), {
59
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
60
+ });
61
+
62
+ if (res.data.status === '1') {
63
+ const guid = res.data.result;
64
+ spinner.text = 'Waiting for verification result...';
65
+
66
+ // Poll for result
67
+ for (let i = 0; i < 10; i++) {
68
+ await sleep(5000);
69
+ const check = await axios.get(network.explorerApi, {
70
+ params: {
71
+ apikey: apiKey,
72
+ module: 'contract',
73
+ action: 'checkverifystatus',
74
+ guid,
75
+ },
76
+ });
77
+
78
+ if (check.data.result === 'Pass - Verified') {
79
+ spinner.succeed(chalk.green(`Contract verified!`));
80
+ console.log(chalk.blue(` ${network.explorer}/address/${address}#code\n`));
81
+ return;
82
+ }
83
+
84
+ if (check.data.result.includes('Fail')) {
85
+ spinner.fail(chalk.red(`Verification failed: ${check.data.result}`));
86
+ return;
87
+ }
88
+
89
+ spinner.text = `Checking... (${check.data.result})`;
90
+ }
91
+
92
+ spinner.warn(chalk.yellow('Verification pending — check explorer manually'));
93
+ console.log(chalk.gray(` ${network.explorer}/address/${address}`));
94
+
95
+ } else {
96
+ // Already verified is OK
97
+ if (res.data.result?.includes('already verified')) {
98
+ spinner.succeed(chalk.green('Already verified!'));
99
+ console.log(chalk.blue(` ${network.explorer}/address/${address}#code\n`));
100
+ } else {
101
+ spinner.fail(chalk.red(`Verification request failed: ${res.data.result}`));
102
+ }
103
+ }
104
+
105
+ } catch (err) {
106
+ spinner.fail(chalk.red(`Verification error: ${err.message}`));
107
+ }
108
+ }
109
+
110
+ function sleep(ms) {
111
+ return new Promise(resolve => setTimeout(resolve, ms));
112
+ }