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.
Files changed (39) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +1 -1
  3. package/dist/index.js +1425 -222
  4. package/docs/delegator-guide.md +6 -6
  5. package/docs/validator-guide.md +51 -18
  6. package/package.json +2 -2
  7. package/src/commands/account/create.ts +10 -4
  8. package/src/commands/account/export.ts +106 -0
  9. package/src/commands/account/import.ts +85 -31
  10. package/src/commands/account/index.ts +77 -18
  11. package/src/commands/account/list.ts +34 -0
  12. package/src/commands/account/lock.ts +16 -7
  13. package/src/commands/account/remove.ts +30 -0
  14. package/src/commands/account/send.ts +14 -8
  15. package/src/commands/account/show.ts +22 -8
  16. package/src/commands/account/unlock.ts +20 -10
  17. package/src/commands/account/use.ts +21 -0
  18. package/src/commands/network/index.ts +18 -3
  19. package/src/commands/network/setNetwork.ts +38 -22
  20. package/src/commands/staking/StakingAction.ts +51 -19
  21. package/src/commands/staking/delegatorJoin.ts +2 -0
  22. package/src/commands/staking/index.ts +29 -2
  23. package/src/commands/staking/setIdentity.ts +5 -0
  24. package/src/commands/staking/stakingInfo.ts +29 -21
  25. package/src/commands/staking/wizard.ts +809 -0
  26. package/src/lib/actions/BaseAction.ts +71 -45
  27. package/src/lib/config/ConfigFileManager.ts +143 -0
  28. package/src/lib/config/KeychainManager.ts +68 -8
  29. package/tests/actions/create.test.ts +30 -10
  30. package/tests/actions/deploy.test.ts +7 -0
  31. package/tests/actions/lock.test.ts +28 -8
  32. package/tests/actions/unlock.test.ts +44 -26
  33. package/tests/commands/account.test.ts +43 -18
  34. package/tests/commands/network.test.ts +10 -10
  35. package/tests/commands/staking.test.ts +122 -0
  36. package/tests/libs/baseAction.test.ts +64 -41
  37. package/tests/libs/configFileManager.test.ts +8 -1
  38. package/tests/libs/keychainManager.test.ts +62 -18
  39. package/src/lib/interfaces/KeystoreData.ts +0 -5
@@ -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 stake-info --validator 0xa8f1...130
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 stake-info --validator 0xa8f1...130
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 `stake-info`:
138
- ```
137
+ Check your pending withdrawals with `delegation-info`:
138
+ ```json
139
139
  pendingWithdrawals: [
140
140
  {
141
141
  epoch: '5',
@@ -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
- ## Step 1: Create an Account
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. You can:
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 on your validator server
113
- genlayer account create --output ./operator.json
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
- # Get operator address
116
- cat ./operator.json | jq -r .address
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 (run from your main wallet)
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.0",
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.5",
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
- output: string;
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("Creating encrypted keystore...");
16
- await this.createKeypair(options.output, options.overwrite);
16
+ this.startSpinner(`Creating account '${options.name}'...`);
17
+ await this.createKeypairByName(options.name, options.overwrite);
17
18
 
18
- this.succeedSpinner(`Account created and saved to: ${options.output}`);
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
- output: string;
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 privateKey = options.privateKey || await this.promptPrivateKey();
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(finalOutputPath) && !options.overwrite) {
29
- this.failSpinner(`File at ${finalOutputPath} already exists. Use '--overwrite' to replace.`);
24
+ if (existsSync(keystorePath) && !options.overwrite) {
25
+ this.failSpinner(`Account '${options.name}' already exists. Use '--overwrite' to replace.`);
30
26
  }
31
27
 
32
- const wallet = new ethers.Wallet(normalizedKey);
33
-
34
- const password = await this.promptPassword("Enter a password to encrypt your keystore (minimum 8 characters):");
35
- const confirmPassword = await this.promptPassword("Confirm password:");
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
- if (password !== confirmPassword) {
38
- this.failSpinner("Passwords do not match");
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 < ImportAccountAction.MIN_PASSWORD_LENGTH) {
42
- this.failSpinner(`Password must be at least ${ImportAccountAction.MIN_PASSWORD_LENGTH} characters long`);
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("Encrypting and saving keystore...");
60
+ this.startSpinner(`Importing account '${options.name}'...`);
46
61
 
47
62
  const encryptedJson = await wallet.encrypt(password);
48
63
 
49
- const keystoreData: KeystoreData = {
50
- version: 1,
51
- encrypted: encryptedJson,
52
- address: wallet.address,
53
- };
64
+ // Write standard web3 keystore format directly
65
+ writeFileSync(keystorePath, encryptedJson);
54
66
 
55
- writeFileSync(finalOutputPath, JSON.stringify(keystoreData, null, 2));
56
- this.writeConfig("keyPairPath", finalOutputPath);
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 and saved to: ${finalOutputPath}`);
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 {UnlockAccountAction} from "./unlock";
6
- import {LockAccountAction} from "./lock";
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 account (address, balance, keys)")
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
- .option("--output <path>", "Path to save the keystore", "./keypair.json")
23
- .option("--overwrite", "Overwrite existing file", false)
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
- .option("--private-key <key>", "Private key to import (will prompt if not provided)")
33
- .option("--output <path>", "Path to save the keystore", "./keypair.json")
34
- .option("--overwrite", "Overwrite existing file", false)
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
- .action(async (to: string, amount: string, options: {rpc?: string; network?: string}) => {
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
- .action(async () => {
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
- .action(async () => {
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
+ }