genlayer 0.32.0 → 0.32.2
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/CHANGELOG.md +4 -0
- package/README.md +1 -1
- package/dist/index.js +1425 -222
- package/docs/delegator-guide.md +6 -6
- package/docs/validator-guide.md +51 -18
- package/package.json +2 -2
- package/src/commands/account/create.ts +10 -4
- package/src/commands/account/export.ts +106 -0
- package/src/commands/account/import.ts +85 -31
- package/src/commands/account/index.ts +77 -18
- package/src/commands/account/list.ts +34 -0
- package/src/commands/account/lock.ts +16 -7
- package/src/commands/account/remove.ts +30 -0
- package/src/commands/account/send.ts +14 -8
- package/src/commands/account/show.ts +22 -8
- package/src/commands/account/unlock.ts +20 -10
- package/src/commands/account/use.ts +21 -0
- package/src/commands/network/index.ts +18 -3
- package/src/commands/network/setNetwork.ts +38 -22
- package/src/commands/staking/StakingAction.ts +51 -19
- package/src/commands/staking/delegatorJoin.ts +2 -0
- package/src/commands/staking/index.ts +29 -2
- package/src/commands/staking/setIdentity.ts +5 -0
- package/src/commands/staking/stakingInfo.ts +29 -21
- package/src/commands/staking/wizard.ts +809 -0
- package/src/lib/actions/BaseAction.ts +71 -45
- package/src/lib/config/ConfigFileManager.ts +143 -0
- package/src/lib/config/KeychainManager.ts +68 -8
- package/tests/actions/create.test.ts +30 -10
- package/tests/actions/deploy.test.ts +7 -0
- package/tests/actions/lock.test.ts +28 -8
- package/tests/actions/unlock.test.ts +44 -26
- package/tests/commands/account.test.ts +43 -18
- package/tests/commands/network.test.ts +10 -10
- package/tests/commands/staking.test.ts +122 -0
- package/tests/libs/baseAction.test.ts +64 -41
- package/tests/libs/configFileManager.test.ts +8 -1
- package/tests/libs/keychainManager.test.ts +62 -18
- package/src/lib/interfaces/KeystoreData.ts +0 -5
package/docs/delegator-guide.md
CHANGED
|
@@ -51,7 +51,7 @@ genlayer staking active-validators
|
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
Output:
|
|
54
|
-
```
|
|
54
|
+
```json
|
|
55
55
|
{
|
|
56
56
|
count: 6,
|
|
57
57
|
validators: [
|
|
@@ -94,11 +94,11 @@ Options:
|
|
|
94
94
|
## Step 8: Verify Your Delegation
|
|
95
95
|
|
|
96
96
|
```bash
|
|
97
|
-
genlayer staking
|
|
97
|
+
genlayer staking delegation-info --validator 0xa8f1...130
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
Output:
|
|
101
|
-
```
|
|
101
|
+
```json
|
|
102
102
|
{
|
|
103
103
|
delegator: '0x86D0d159483CBf01E920ECfF8bB7F0Cd7E964E7E',
|
|
104
104
|
validator: '0xa8f1BF1e5e709593b4468d7ac5DC315Ea3CAe130',
|
|
@@ -117,7 +117,7 @@ The `projectedReward` shows your estimated earnings per epoch based on current i
|
|
|
117
117
|
### Check Your Stake
|
|
118
118
|
|
|
119
119
|
```bash
|
|
120
|
-
genlayer staking
|
|
120
|
+
genlayer staking delegation-info --validator 0xa8f1...130
|
|
121
121
|
```
|
|
122
122
|
|
|
123
123
|
### Withdraw (Exit) Delegation
|
|
@@ -134,8 +134,8 @@ Options:
|
|
|
134
134
|
|
|
135
135
|
This initiates a withdrawal. Your tokens enter an **unbonding period of 7 epochs** before they can be claimed.
|
|
136
136
|
|
|
137
|
-
Check your pending withdrawals with `
|
|
138
|
-
```
|
|
137
|
+
Check your pending withdrawals with `delegation-info`:
|
|
138
|
+
```json
|
|
139
139
|
pendingWithdrawals: [
|
|
140
140
|
{
|
|
141
141
|
epoch: '5',
|
package/docs/validator-guide.md
CHANGED
|
@@ -2,33 +2,62 @@
|
|
|
2
2
|
|
|
3
3
|
This guide walks you through becoming a validator on the GenLayer testnet using the CLI.
|
|
4
4
|
|
|
5
|
+
For a deeper understanding of how staking works in GenLayer, see the [Staking documentation](/understand-genlayer-protocol/core-concepts/optimistic-democracy/staking).
|
|
6
|
+
|
|
5
7
|
## Prerequisites
|
|
6
8
|
|
|
7
9
|
- Node.js installed
|
|
8
10
|
- GenLayer CLI installed (`npm install -g genlayer`)
|
|
9
11
|
- GEN tokens for staking (minimum stake required)
|
|
10
12
|
|
|
11
|
-
##
|
|
13
|
+
## Quick Start: Validator Wizard
|
|
14
|
+
|
|
15
|
+
The easiest way to become a validator is using the interactive wizard:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
genlayer staking wizard
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The wizard guides you through all steps:
|
|
22
|
+
1. Account setup (create or select)
|
|
23
|
+
2. Network selection
|
|
24
|
+
3. Balance verification
|
|
25
|
+
4. Operator setup (optional, recommended for security)
|
|
26
|
+
5. Stake amount selection
|
|
27
|
+
6. Validator creation
|
|
28
|
+
7. Identity setup (moniker, website, etc.)
|
|
29
|
+
|
|
30
|
+
If you prefer manual setup, follow the steps below.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Manual Setup
|
|
35
|
+
|
|
36
|
+
## Step 1: Create an Owner Account
|
|
12
37
|
|
|
13
38
|
```bash
|
|
14
|
-
genlayer account create
|
|
39
|
+
genlayer account create --name owner
|
|
15
40
|
```
|
|
16
41
|
|
|
17
|
-
You'll be prompted to set a password. This creates an encrypted keystore file.
|
|
42
|
+
You'll be prompted to set a password. This creates an encrypted keystore file in standard web3 format.
|
|
43
|
+
|
|
44
|
+
The owner account holds your staked funds and controls the validator. Keep it secure.
|
|
18
45
|
|
|
19
46
|
## Step 2: View Your Account
|
|
20
47
|
|
|
21
48
|
```bash
|
|
22
|
-
genlayer account
|
|
49
|
+
genlayer account show
|
|
23
50
|
```
|
|
24
51
|
|
|
25
52
|
Output:
|
|
26
53
|
```
|
|
27
54
|
{
|
|
55
|
+
name: 'owner',
|
|
28
56
|
address: '0x86D0d159483CBf01E920ECfF8bB7F0Cd7E964E7E',
|
|
29
57
|
balance: '0 GEN',
|
|
30
58
|
network: 'localnet',
|
|
31
|
-
status: 'locked'
|
|
59
|
+
status: 'locked',
|
|
60
|
+
active: true
|
|
32
61
|
}
|
|
33
62
|
```
|
|
34
63
|
|
|
@@ -40,19 +69,16 @@ genlayer network testnet-asimov
|
|
|
40
69
|
|
|
41
70
|
Verify with:
|
|
42
71
|
```bash
|
|
43
|
-
genlayer account
|
|
72
|
+
genlayer account show
|
|
44
73
|
```
|
|
45
74
|
|
|
46
75
|
You should see `network: 'Asimov Testnet'`.
|
|
47
76
|
|
|
48
77
|
## Step 4: Fund Your Account
|
|
49
78
|
|
|
50
|
-
Transfer GEN tokens to your address
|
|
79
|
+
Transfer GEN tokens to your address:
|
|
51
80
|
- Use the faucet (if available)
|
|
52
|
-
- Transfer from another account
|
|
53
|
-
```bash
|
|
54
|
-
genlayer account send <your-address> 50000gen
|
|
55
|
-
```
|
|
81
|
+
- Transfer from another funded account using `genlayer account send`
|
|
56
82
|
|
|
57
83
|
## Step 5: Check Staking Requirements
|
|
58
84
|
|
|
@@ -108,18 +134,25 @@ Options:
|
|
|
108
134
|
|
|
109
135
|
This way, if your operator server is compromised, your staked funds remain safe.
|
|
110
136
|
|
|
137
|
+
If you already have an operator wallet (e.g., from geth, foundry, or another tool), you can use its address directly. Otherwise, create one:
|
|
138
|
+
|
|
111
139
|
```bash
|
|
112
|
-
# Create operator account
|
|
113
|
-
genlayer account create --
|
|
140
|
+
# Create operator account (skip if you already have one)
|
|
141
|
+
genlayer account create --name operator
|
|
142
|
+
|
|
143
|
+
# View operator address
|
|
144
|
+
genlayer account show --account operator
|
|
145
|
+
# Address: 0xOperator123...
|
|
114
146
|
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
# 0xOperator123...
|
|
147
|
+
# Export keystore for validator node software (standard web3 format)
|
|
148
|
+
genlayer account export --account operator --output ./operator-keystore.json
|
|
118
149
|
|
|
119
|
-
# Join with separate operator
|
|
150
|
+
# Join as validator with separate operator
|
|
120
151
|
genlayer staking validator-join --amount 42000gen --operator 0xOperator123...
|
|
121
152
|
```
|
|
122
153
|
|
|
154
|
+
Transfer `operator-keystore.json` to your validator server and import it into your validator node software. The keystore is in standard web3 format, compatible with geth, foundry, and other Ethereum tools.
|
|
155
|
+
|
|
123
156
|
You can change the operator later:
|
|
124
157
|
|
|
125
158
|
```bash
|
|
@@ -240,7 +273,7 @@ genlayer staking validator-claim
|
|
|
240
273
|
Run `genlayer account create` first.
|
|
241
274
|
|
|
242
275
|
### "Insufficient balance"
|
|
243
|
-
Ensure you have enough GEN. Check with `genlayer account`.
|
|
276
|
+
Ensure you have enough GEN. Check with `genlayer account show`.
|
|
244
277
|
|
|
245
278
|
### "Below minimum stake"
|
|
246
279
|
Check minimum with `genlayer staking epoch-info` and increase your stake amount.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genlayer",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.2",
|
|
4
4
|
"description": "GenLayer Command Line Tool",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"dotenv": "^17.0.0",
|
|
66
66
|
"ethers": "^6.13.4",
|
|
67
67
|
"fs-extra": "^11.3.0",
|
|
68
|
-
"genlayer-js": "^0.18.
|
|
68
|
+
"genlayer-js": "^0.18.7",
|
|
69
69
|
"inquirer": "^12.0.0",
|
|
70
70
|
"keytar": "^7.9.0",
|
|
71
71
|
"node-fetch": "^3.0.0",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {BaseAction} from "../../lib/actions/BaseAction";
|
|
2
2
|
|
|
3
3
|
export interface CreateAccountOptions {
|
|
4
|
-
|
|
4
|
+
name: string;
|
|
5
5
|
overwrite: boolean;
|
|
6
|
+
setActive?: boolean;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
export class CreateAccountAction extends BaseAction {
|
|
@@ -12,10 +13,15 @@ export class CreateAccountAction extends BaseAction {
|
|
|
12
13
|
|
|
13
14
|
async execute(options: CreateAccountOptions): Promise<void> {
|
|
14
15
|
try {
|
|
15
|
-
this.startSpinner(
|
|
16
|
-
await this.
|
|
16
|
+
this.startSpinner(`Creating account '${options.name}'...`);
|
|
17
|
+
await this.createKeypairByName(options.name, options.overwrite);
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
if (options.setActive !== false) {
|
|
20
|
+
this.setActiveAccount(options.name);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const keystorePath = this.getKeystorePath(options.name);
|
|
24
|
+
this.succeedSpinner(`Account '${options.name}' created at: ${keystorePath}`);
|
|
19
25
|
} catch (error) {
|
|
20
26
|
this.failSpinner("Failed to create account", error);
|
|
21
27
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {BaseAction} from "../../lib/actions/BaseAction";
|
|
2
|
+
import {ethers} from "ethers";
|
|
3
|
+
import {writeFileSync, existsSync, readFileSync} from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
export interface ExportAccountOptions {
|
|
7
|
+
account?: string;
|
|
8
|
+
output: string;
|
|
9
|
+
password?: string;
|
|
10
|
+
sourcePassword?: string;
|
|
11
|
+
overwrite?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ExportAccountAction extends BaseAction {
|
|
15
|
+
constructor() {
|
|
16
|
+
super();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async execute(options: ExportAccountOptions): Promise<void> {
|
|
20
|
+
try {
|
|
21
|
+
if (options.account) {
|
|
22
|
+
this.accountOverride = options.account;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const accountName = this.resolveAccountName();
|
|
26
|
+
const keystorePath = this.getKeystorePath(accountName);
|
|
27
|
+
|
|
28
|
+
if (!existsSync(keystorePath)) {
|
|
29
|
+
this.failSpinner(`Account '${accountName}' not found.`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const outputPath = path.resolve(options.output);
|
|
33
|
+
|
|
34
|
+
if (existsSync(outputPath) && !options.overwrite) {
|
|
35
|
+
this.failSpinner(`Output file already exists: ${outputPath}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Get the private key
|
|
39
|
+
const privateKey = await this.getPrivateKeyForExport(accountName, keystorePath, options.sourcePassword);
|
|
40
|
+
|
|
41
|
+
// Get password for the exported keystore
|
|
42
|
+
let password: string;
|
|
43
|
+
if (options.password) {
|
|
44
|
+
password = options.password;
|
|
45
|
+
} else {
|
|
46
|
+
password = await this.promptPassword("Enter password for exported keystore (minimum 8 characters):");
|
|
47
|
+
const confirmPassword = await this.promptPassword("Confirm password:");
|
|
48
|
+
if (password !== confirmPassword) {
|
|
49
|
+
this.failSpinner("Passwords do not match");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (password.length < BaseAction.MIN_PASSWORD_LENGTH) {
|
|
54
|
+
this.failSpinner(`Password must be at least ${BaseAction.MIN_PASSWORD_LENGTH} characters long`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.startSpinner(`Exporting account '${accountName}'...`);
|
|
58
|
+
|
|
59
|
+
const wallet = new ethers.Wallet(privateKey);
|
|
60
|
+
const encryptedJson = await wallet.encrypt(password);
|
|
61
|
+
|
|
62
|
+
// Write standard web3 keystore format (compatible with geth, foundry, etc.)
|
|
63
|
+
writeFileSync(outputPath, encryptedJson);
|
|
64
|
+
|
|
65
|
+
this.succeedSpinner(`Account '${accountName}' exported to: ${outputPath}`);
|
|
66
|
+
this.logInfo(`Address: ${wallet.address}`);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
this.failSpinner("Failed to export account", error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async getPrivateKeyForExport(
|
|
73
|
+
accountName: string,
|
|
74
|
+
keystorePath: string,
|
|
75
|
+
sourcePassword?: string
|
|
76
|
+
): Promise<string> {
|
|
77
|
+
// First check if key is cached in keychain
|
|
78
|
+
const isAvailable = await this.keychainManager.isKeychainAvailable();
|
|
79
|
+
if (isAvailable) {
|
|
80
|
+
const cachedKey = await this.keychainManager.getPrivateKey(accountName);
|
|
81
|
+
if (cachedKey) {
|
|
82
|
+
return cachedKey;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Need to decrypt the keystore
|
|
87
|
+
const fileContent = readFileSync(keystorePath, "utf-8");
|
|
88
|
+
const parsed = JSON.parse(fileContent);
|
|
89
|
+
|
|
90
|
+
const encryptedJson = parsed.encrypted || fileContent;
|
|
91
|
+
|
|
92
|
+
const password = sourcePassword || await this.promptPassword(`Enter password to unlock '${accountName}':`);
|
|
93
|
+
|
|
94
|
+
this.startSpinner("Decrypting keystore...");
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const wallet = await ethers.Wallet.fromEncryptedJson(encryptedJson, password);
|
|
98
|
+
this.stopSpinner();
|
|
99
|
+
return wallet.privateKey;
|
|
100
|
+
} catch {
|
|
101
|
+
this.failSpinner("Failed to decrypt keystore. Wrong password?");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
throw new Error("Unreachable");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -1,69 +1,123 @@
|
|
|
1
1
|
import {BaseAction} from "../../lib/actions/BaseAction";
|
|
2
2
|
import {ethers} from "ethers";
|
|
3
|
-
import {writeFileSync, existsSync} from "fs";
|
|
4
|
-
import {KeystoreData} from "../../lib/interfaces/KeystoreData";
|
|
3
|
+
import {writeFileSync, existsSync, readFileSync} from "fs";
|
|
5
4
|
|
|
6
5
|
export interface ImportAccountOptions {
|
|
7
6
|
privateKey?: string;
|
|
8
|
-
|
|
7
|
+
keystore?: string;
|
|
8
|
+
name: string;
|
|
9
9
|
overwrite: boolean;
|
|
10
|
+
setActive?: boolean;
|
|
11
|
+
password?: string;
|
|
12
|
+
sourcePassword?: string;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
export class ImportAccountAction extends BaseAction {
|
|
13
|
-
private static readonly MIN_PASSWORD_LENGTH = 8;
|
|
14
|
-
|
|
15
16
|
constructor() {
|
|
16
17
|
super();
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
async execute(options: ImportAccountOptions): Promise<void> {
|
|
20
21
|
try {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const normalizedKey = this.normalizePrivateKey(privateKey);
|
|
24
|
-
this.validatePrivateKey(normalizedKey);
|
|
25
|
-
|
|
26
|
-
const finalOutputPath = this.getFilePath(options.output);
|
|
22
|
+
const keystorePath = this.getKeystorePath(options.name);
|
|
27
23
|
|
|
28
|
-
if (existsSync(
|
|
29
|
-
this.failSpinner(`
|
|
24
|
+
if (existsSync(keystorePath) && !options.overwrite) {
|
|
25
|
+
this.failSpinner(`Account '${options.name}' already exists. Use '--overwrite' to replace.`);
|
|
30
26
|
}
|
|
31
27
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
let privateKey: string;
|
|
29
|
+
|
|
30
|
+
if (options.keystore) {
|
|
31
|
+
privateKey = await this.importFromKeystore(options.keystore, options.sourcePassword);
|
|
32
|
+
} else if (options.privateKey) {
|
|
33
|
+
const normalizedKey = this.normalizePrivateKey(options.privateKey);
|
|
34
|
+
this.validatePrivateKey(normalizedKey);
|
|
35
|
+
privateKey = normalizedKey;
|
|
36
|
+
} else {
|
|
37
|
+
const inputKey = await this.promptPrivateKey();
|
|
38
|
+
const normalizedKey = this.normalizePrivateKey(inputKey);
|
|
39
|
+
this.validatePrivateKey(normalizedKey);
|
|
40
|
+
privateKey = normalizedKey;
|
|
41
|
+
}
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
const wallet = new ethers.Wallet(privateKey);
|
|
44
|
+
|
|
45
|
+
let password: string;
|
|
46
|
+
if (options.password) {
|
|
47
|
+
password = options.password;
|
|
48
|
+
} else {
|
|
49
|
+
password = await this.promptPassword("Enter a password to encrypt your keystore (minimum 8 characters):");
|
|
50
|
+
const confirmPassword = await this.promptPassword("Confirm password:");
|
|
51
|
+
if (password !== confirmPassword) {
|
|
52
|
+
this.failSpinner("Passwords do not match");
|
|
53
|
+
}
|
|
39
54
|
}
|
|
40
55
|
|
|
41
|
-
if (password.length <
|
|
42
|
-
this.failSpinner(`Password must be at least ${
|
|
56
|
+
if (password.length < BaseAction.MIN_PASSWORD_LENGTH) {
|
|
57
|
+
this.failSpinner(`Password must be at least ${BaseAction.MIN_PASSWORD_LENGTH} characters long`);
|
|
43
58
|
}
|
|
44
59
|
|
|
45
|
-
this.startSpinner(
|
|
60
|
+
this.startSpinner(`Importing account '${options.name}'...`);
|
|
46
61
|
|
|
47
62
|
const encryptedJson = await wallet.encrypt(password);
|
|
48
63
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
encrypted: encryptedJson,
|
|
52
|
-
address: wallet.address,
|
|
53
|
-
};
|
|
64
|
+
// Write standard web3 keystore format directly
|
|
65
|
+
writeFileSync(keystorePath, encryptedJson);
|
|
54
66
|
|
|
55
|
-
|
|
56
|
-
|
|
67
|
+
if (options.setActive !== false) {
|
|
68
|
+
this.setActiveAccount(options.name);
|
|
69
|
+
}
|
|
57
70
|
|
|
58
|
-
await this.keychainManager.removePrivateKey();
|
|
71
|
+
await this.keychainManager.removePrivateKey(options.name);
|
|
59
72
|
|
|
60
|
-
this.succeedSpinner(`Account imported
|
|
73
|
+
this.succeedSpinner(`Account '${options.name}' imported to: ${keystorePath}`);
|
|
61
74
|
this.logInfo(`Address: ${wallet.address}`);
|
|
62
75
|
} catch (error) {
|
|
63
76
|
this.failSpinner("Failed to import account", error);
|
|
64
77
|
}
|
|
65
78
|
}
|
|
66
79
|
|
|
80
|
+
private async importFromKeystore(keystorePath: string, sourcePassword?: string): Promise<string> {
|
|
81
|
+
if (!existsSync(keystorePath)) {
|
|
82
|
+
this.failSpinner(`Keystore file not found: ${keystorePath}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const fileContent = readFileSync(keystorePath, "utf-8");
|
|
86
|
+
let encryptedJson: string;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(fileContent);
|
|
90
|
+
|
|
91
|
+
// Check if it's our format (with 'encrypted' field) or standard web3 keystore
|
|
92
|
+
if (parsed.encrypted) {
|
|
93
|
+
// Our format
|
|
94
|
+
encryptedJson = parsed.encrypted;
|
|
95
|
+
} else if (parsed.crypto || parsed.Crypto) {
|
|
96
|
+
// Standard web3 keystore format (geth, foundry, etc.)
|
|
97
|
+
encryptedJson = fileContent;
|
|
98
|
+
} else {
|
|
99
|
+
this.failSpinner("Invalid keystore format. Expected encrypted keystore file.");
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
this.failSpinner("Invalid keystore file. Could not parse JSON.");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const password = sourcePassword || await this.promptPassword("Enter password to decrypt keystore:");
|
|
106
|
+
|
|
107
|
+
this.startSpinner("Decrypting keystore...");
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const wallet = await ethers.Wallet.fromEncryptedJson(encryptedJson!, password);
|
|
111
|
+
this.stopSpinner();
|
|
112
|
+
return wallet.privateKey;
|
|
113
|
+
} catch {
|
|
114
|
+
this.failSpinner("Failed to decrypt keystore. Wrong password?");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// This line is unreachable but TypeScript needs it
|
|
118
|
+
throw new Error("Unreachable");
|
|
119
|
+
}
|
|
120
|
+
|
|
67
121
|
private async promptPrivateKey(): Promise<string> {
|
|
68
122
|
return this.promptPassword("Enter private key to import:");
|
|
69
123
|
}
|
|
@@ -1,26 +1,49 @@
|
|
|
1
1
|
import {Command} from "commander";
|
|
2
|
-
import {ShowAccountAction} from "./show";
|
|
2
|
+
import {ShowAccountAction, ShowAccountOptions} from "./show";
|
|
3
3
|
import {CreateAccountAction, CreateAccountOptions} from "./create";
|
|
4
4
|
import {ImportAccountAction, ImportAccountOptions} from "./import";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import {ExportAccountAction, ExportAccountOptions} from "./export";
|
|
6
|
+
import {UnlockAccountAction, UnlockAccountOptions} from "./unlock";
|
|
7
|
+
import {LockAccountAction, LockAccountOptions} from "./lock";
|
|
7
8
|
import {SendAction, SendOptions} from "./send";
|
|
9
|
+
import {ListAccountsAction} from "./list";
|
|
10
|
+
import {UseAccountAction} from "./use";
|
|
11
|
+
import {RemoveAccountAction} from "./remove";
|
|
8
12
|
|
|
9
13
|
export function initializeAccountCommands(program: Command) {
|
|
10
14
|
const accountCommand = program
|
|
11
15
|
.command("account")
|
|
12
|
-
.description("Manage your
|
|
16
|
+
.description("Manage your accounts (address, balance, keys)")
|
|
13
17
|
.action(async () => {
|
|
14
|
-
// Default action: show account info
|
|
18
|
+
// Default action: show account info (use 'account show' for options)
|
|
15
19
|
const showAction = new ShowAccountAction();
|
|
16
|
-
await showAction.execute();
|
|
20
|
+
await showAction.execute({});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
accountCommand
|
|
24
|
+
.command("list")
|
|
25
|
+
.description("List all accounts")
|
|
26
|
+
.action(async () => {
|
|
27
|
+
const listAction = new ListAccountsAction();
|
|
28
|
+
await listAction.execute();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
accountCommand
|
|
32
|
+
.command("show")
|
|
33
|
+
.description("Show account details (address, balance)")
|
|
34
|
+
.option("--rpc <rpcUrl>", "RPC URL for the network")
|
|
35
|
+
.option("--account <name>", "Account to show")
|
|
36
|
+
.action(async (options: ShowAccountOptions) => {
|
|
37
|
+
const showAction = new ShowAccountAction();
|
|
38
|
+
await showAction.execute(options);
|
|
17
39
|
});
|
|
18
40
|
|
|
19
41
|
accountCommand
|
|
20
42
|
.command("create")
|
|
21
43
|
.description("Create a new account with encrypted keystore")
|
|
22
|
-
.
|
|
23
|
-
.option("--overwrite", "Overwrite existing
|
|
44
|
+
.requiredOption("--name <name>", "Name for the account")
|
|
45
|
+
.option("--overwrite", "Overwrite existing account", false)
|
|
46
|
+
.option("--no-set-active", "Do not set as active account")
|
|
24
47
|
.action(async (options: CreateAccountOptions) => {
|
|
25
48
|
const createAction = new CreateAccountAction();
|
|
26
49
|
await createAction.execute(options);
|
|
@@ -28,39 +51,75 @@ export function initializeAccountCommands(program: Command) {
|
|
|
28
51
|
|
|
29
52
|
accountCommand
|
|
30
53
|
.command("import")
|
|
31
|
-
.description("Import an account from a private key")
|
|
32
|
-
.
|
|
33
|
-
.option("--
|
|
34
|
-
.option("--
|
|
54
|
+
.description("Import an account from a private key or keystore file")
|
|
55
|
+
.requiredOption("--name <name>", "Name for the account")
|
|
56
|
+
.option("--private-key <key>", "Private key to import")
|
|
57
|
+
.option("--keystore <path>", "Path to keystore file to import (geth, foundry, etc.)")
|
|
58
|
+
.option("--password <password>", "Password for the new keystore (skips confirmation prompt)")
|
|
59
|
+
.option("--source-password <password>", "Password to decrypt source keystore (with --keystore)")
|
|
60
|
+
.option("--overwrite", "Overwrite existing account", false)
|
|
61
|
+
.option("--no-set-active", "Do not set as active account")
|
|
35
62
|
.action(async (options: ImportAccountOptions) => {
|
|
36
63
|
const importAction = new ImportAccountAction();
|
|
37
64
|
await importAction.execute(options);
|
|
38
65
|
});
|
|
39
66
|
|
|
67
|
+
accountCommand
|
|
68
|
+
.command("export")
|
|
69
|
+
.description("Export an account to a keystore file (web3/geth/foundry compatible)")
|
|
70
|
+
.requiredOption("--output <path>", "Output path for the keystore file")
|
|
71
|
+
.option("--account <name>", "Account to export (defaults to active account)")
|
|
72
|
+
.option("--password <password>", "Password for exported keystore (skips confirmation)")
|
|
73
|
+
.option("--source-password <password>", "Password to decrypt account (if not unlocked)")
|
|
74
|
+
.action(async (options: ExportAccountOptions) => {
|
|
75
|
+
const exportAction = new ExportAccountAction();
|
|
76
|
+
await exportAction.execute(options);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
accountCommand
|
|
80
|
+
.command("use <name>")
|
|
81
|
+
.description("Set the active account")
|
|
82
|
+
.action(async (name: string) => {
|
|
83
|
+
const useAction = new UseAccountAction();
|
|
84
|
+
await useAction.execute(name);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
accountCommand
|
|
88
|
+
.command("remove <name>")
|
|
89
|
+
.description("Remove an account")
|
|
90
|
+
.option("--force", "Skip confirmation prompt", false)
|
|
91
|
+
.action(async (name: string, options: {force?: boolean}) => {
|
|
92
|
+
const removeAction = new RemoveAccountAction();
|
|
93
|
+
await removeAction.execute(name, options);
|
|
94
|
+
});
|
|
95
|
+
|
|
40
96
|
accountCommand
|
|
41
97
|
.command("send <to> <amount>")
|
|
42
98
|
.description("Send GEN to an address")
|
|
43
99
|
.option("--rpc <rpcUrl>", "RPC URL for the network")
|
|
44
100
|
.option("--network <network>", "Network to use (localnet, testnet-asimov)")
|
|
45
|
-
.
|
|
101
|
+
.option("--account <name>", "Account to send from")
|
|
102
|
+
.action(async (to: string, amount: string, options: {rpc?: string; network?: string; account?: string}) => {
|
|
46
103
|
const sendAction = new SendAction();
|
|
47
|
-
await sendAction.execute({to, amount, rpc: options.rpc, network: options.network});
|
|
104
|
+
await sendAction.execute({to, amount, rpc: options.rpc, network: options.network, account: options.account});
|
|
48
105
|
});
|
|
49
106
|
|
|
50
107
|
accountCommand
|
|
51
108
|
.command("unlock")
|
|
52
109
|
.description("Unlock account by caching private key in OS keychain")
|
|
53
|
-
.
|
|
110
|
+
.option("--account <name>", "Account to unlock")
|
|
111
|
+
.action(async (options: UnlockAccountOptions) => {
|
|
54
112
|
const unlockAction = new UnlockAccountAction();
|
|
55
|
-
await unlockAction.execute();
|
|
113
|
+
await unlockAction.execute(options);
|
|
56
114
|
});
|
|
57
115
|
|
|
58
116
|
accountCommand
|
|
59
117
|
.command("lock")
|
|
60
118
|
.description("Lock account by removing private key from OS keychain")
|
|
61
|
-
.
|
|
119
|
+
.option("--account <name>", "Account to lock")
|
|
120
|
+
.action(async (options: LockAccountOptions) => {
|
|
62
121
|
const lockAction = new LockAccountAction();
|
|
63
|
-
await lockAction.execute();
|
|
122
|
+
await lockAction.execute(options);
|
|
64
123
|
});
|
|
65
124
|
|
|
66
125
|
return program;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {BaseAction} from "../../lib/actions/BaseAction";
|
|
2
|
+
|
|
3
|
+
export class ListAccountsAction extends BaseAction {
|
|
4
|
+
constructor() {
|
|
5
|
+
super();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async execute(): Promise<void> {
|
|
9
|
+
try {
|
|
10
|
+
const accounts = this.listAccounts();
|
|
11
|
+
const activeAccount = this.getActiveAccount();
|
|
12
|
+
const unlockedAccounts = await this.keychainManager.listUnlockedAccounts();
|
|
13
|
+
|
|
14
|
+
if (accounts.length === 0) {
|
|
15
|
+
this.logInfo("No accounts found. Run 'genlayer account create --name <name>' to create one.");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log("");
|
|
20
|
+
for (const account of accounts) {
|
|
21
|
+
const isActive = account.name === activeAccount;
|
|
22
|
+
const isUnlocked = unlockedAccounts.includes(account.name);
|
|
23
|
+
const marker = isActive ? "*" : " ";
|
|
24
|
+
const status = isUnlocked ? "(unlocked)" : "";
|
|
25
|
+
const activeLabel = isActive ? "(active)" : "";
|
|
26
|
+
|
|
27
|
+
console.log(`${marker} ${account.name.padEnd(16)} ${account.address} ${activeLabel} ${status}`.trim());
|
|
28
|
+
}
|
|
29
|
+
console.log("");
|
|
30
|
+
} catch (error) {
|
|
31
|
+
this.failSpinner("Failed to list accounts", error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|