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 +2 -0
- package/README.md +149 -0
- package/package.json +29 -0
- package/src/commands/compile.js +92 -0
- package/src/commands/deploy.js +162 -0
- package/src/commands/init.js +109 -0
- package/src/commands/interact.js +174 -0
- package/src/commands/verify.js +33 -0
- package/src/config/networks.js +127 -0
- package/src/index.js +66 -0
- package/src/utils/deployments.js +34 -0
- package/src/utils/verify.js +112 -0
package/.gitattributes
ADDED
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
|
+
}
|