genlayer 0.31.0 → 0.32.1

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 (57) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/CLAUDE.md +55 -0
  3. package/README.md +121 -8
  4. package/dist/index.js +7161 -3706
  5. package/docs/delegator-guide.md +203 -0
  6. package/docs/validator-guide.md +291 -0
  7. package/package.json +2 -2
  8. package/src/commands/account/create.ts +29 -0
  9. package/src/commands/account/export.ts +106 -0
  10. package/src/commands/account/import.ts +135 -0
  11. package/src/commands/account/index.ts +126 -0
  12. package/src/commands/account/list.ts +34 -0
  13. package/src/commands/account/lock.ts +39 -0
  14. package/src/commands/account/remove.ts +30 -0
  15. package/src/commands/account/send.ts +156 -0
  16. package/src/commands/account/show.ts +74 -0
  17. package/src/commands/account/unlock.ts +51 -0
  18. package/src/commands/account/use.ts +21 -0
  19. package/src/commands/network/index.ts +18 -3
  20. package/src/commands/network/setNetwork.ts +43 -26
  21. package/src/commands/staking/StakingAction.ts +157 -0
  22. package/src/commands/staking/delegatorClaim.ts +41 -0
  23. package/src/commands/staking/delegatorExit.ts +50 -0
  24. package/src/commands/staking/delegatorJoin.ts +44 -0
  25. package/src/commands/staking/index.ts +251 -0
  26. package/src/commands/staking/setIdentity.ts +66 -0
  27. package/src/commands/staking/setOperator.ts +40 -0
  28. package/src/commands/staking/stakingInfo.ts +300 -0
  29. package/src/commands/staking/validatorClaim.ts +38 -0
  30. package/src/commands/staking/validatorDeposit.ts +35 -0
  31. package/src/commands/staking/validatorExit.ts +44 -0
  32. package/src/commands/staking/validatorJoin.ts +47 -0
  33. package/src/commands/staking/validatorPrime.ts +35 -0
  34. package/src/commands/staking/wizard.ts +802 -0
  35. package/src/index.ts +4 -2
  36. package/src/lib/actions/BaseAction.ts +114 -55
  37. package/src/lib/config/ConfigFileManager.ts +143 -0
  38. package/src/lib/config/KeychainManager.ts +23 -7
  39. package/tests/actions/create.test.ts +41 -21
  40. package/tests/actions/deploy.test.ts +7 -0
  41. package/tests/actions/lock.test.ts +33 -13
  42. package/tests/actions/setNetwork.test.ts +18 -57
  43. package/tests/actions/staking.test.ts +323 -0
  44. package/tests/actions/unlock.test.ts +51 -33
  45. package/tests/commands/account.test.ts +146 -0
  46. package/tests/commands/network.test.ts +10 -10
  47. package/tests/commands/staking.test.ts +333 -0
  48. package/tests/index.test.ts +6 -2
  49. package/tests/libs/baseAction.test.ts +71 -42
  50. package/tests/libs/configFileManager.test.ts +8 -1
  51. package/tests/libs/keychainManager.test.ts +56 -16
  52. package/src/commands/keygen/create.ts +0 -23
  53. package/src/commands/keygen/index.ts +0 -39
  54. package/src/commands/keygen/lock.ts +0 -31
  55. package/src/commands/keygen/unlock.ts +0 -41
  56. package/src/lib/interfaces/KeystoreData.ts +0 -5
  57. package/tests/commands/keygen.test.ts +0 -123
@@ -0,0 +1,135 @@
1
+ import {BaseAction} from "../../lib/actions/BaseAction";
2
+ import {ethers} from "ethers";
3
+ import {writeFileSync, existsSync, readFileSync} from "fs";
4
+
5
+ export interface ImportAccountOptions {
6
+ privateKey?: string;
7
+ keystore?: string;
8
+ name: string;
9
+ overwrite: boolean;
10
+ setActive?: boolean;
11
+ password?: string;
12
+ sourcePassword?: string;
13
+ }
14
+
15
+ export class ImportAccountAction extends BaseAction {
16
+ constructor() {
17
+ super();
18
+ }
19
+
20
+ async execute(options: ImportAccountOptions): Promise<void> {
21
+ try {
22
+ const keystorePath = this.getKeystorePath(options.name);
23
+
24
+ if (existsSync(keystorePath) && !options.overwrite) {
25
+ this.failSpinner(`Account '${options.name}' already exists. Use '--overwrite' to replace.`);
26
+ }
27
+
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
+ }
42
+
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
+ }
54
+ }
55
+
56
+ if (password.length < BaseAction.MIN_PASSWORD_LENGTH) {
57
+ this.failSpinner(`Password must be at least ${BaseAction.MIN_PASSWORD_LENGTH} characters long`);
58
+ }
59
+
60
+ this.startSpinner(`Importing account '${options.name}'...`);
61
+
62
+ const encryptedJson = await wallet.encrypt(password);
63
+
64
+ // Write standard web3 keystore format directly
65
+ writeFileSync(keystorePath, encryptedJson);
66
+
67
+ if (options.setActive !== false) {
68
+ this.setActiveAccount(options.name);
69
+ }
70
+
71
+ await this.keychainManager.removePrivateKey(options.name);
72
+
73
+ this.succeedSpinner(`Account '${options.name}' imported to: ${keystorePath}`);
74
+ this.logInfo(`Address: ${wallet.address}`);
75
+ } catch (error) {
76
+ this.failSpinner("Failed to import account", error);
77
+ }
78
+ }
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
+
121
+ private async promptPrivateKey(): Promise<string> {
122
+ return this.promptPassword("Enter private key to import:");
123
+ }
124
+
125
+ private normalizePrivateKey(key: string): string {
126
+ const trimmed = key.trim();
127
+ return trimmed.startsWith("0x") ? trimmed : `0x${trimmed}`;
128
+ }
129
+
130
+ private validatePrivateKey(key: string): void {
131
+ if (!/^0x[0-9a-fA-F]{64}$/.test(key)) {
132
+ this.failSpinner("Invalid private key format. Expected 64 hex characters (with or without 0x prefix).");
133
+ }
134
+ }
135
+ }
@@ -0,0 +1,126 @@
1
+ import {Command} from "commander";
2
+ import {ShowAccountAction, ShowAccountOptions} from "./show";
3
+ import {CreateAccountAction, CreateAccountOptions} from "./create";
4
+ import {ImportAccountAction, ImportAccountOptions} from "./import";
5
+ import {ExportAccountAction, ExportAccountOptions} from "./export";
6
+ import {UnlockAccountAction, UnlockAccountOptions} from "./unlock";
7
+ import {LockAccountAction, LockAccountOptions} from "./lock";
8
+ import {SendAction, SendOptions} from "./send";
9
+ import {ListAccountsAction} from "./list";
10
+ import {UseAccountAction} from "./use";
11
+ import {RemoveAccountAction} from "./remove";
12
+
13
+ export function initializeAccountCommands(program: Command) {
14
+ const accountCommand = program
15
+ .command("account")
16
+ .description("Manage your accounts (address, balance, keys)")
17
+ .action(async () => {
18
+ // Default action: show account info (use 'account show' for options)
19
+ const showAction = new ShowAccountAction();
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);
39
+ });
40
+
41
+ accountCommand
42
+ .command("create")
43
+ .description("Create a new account with encrypted keystore")
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")
47
+ .action(async (options: CreateAccountOptions) => {
48
+ const createAction = new CreateAccountAction();
49
+ await createAction.execute(options);
50
+ });
51
+
52
+ accountCommand
53
+ .command("import")
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")
62
+ .action(async (options: ImportAccountOptions) => {
63
+ const importAction = new ImportAccountAction();
64
+ await importAction.execute(options);
65
+ });
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
+
96
+ accountCommand
97
+ .command("send <to> <amount>")
98
+ .description("Send GEN to an address")
99
+ .option("--rpc <rpcUrl>", "RPC URL for the network")
100
+ .option("--network <network>", "Network to use (localnet, testnet-asimov)")
101
+ .option("--account <name>", "Account to send from")
102
+ .action(async (to: string, amount: string, options: {rpc?: string; network?: string; account?: string}) => {
103
+ const sendAction = new SendAction();
104
+ await sendAction.execute({to, amount, rpc: options.rpc, network: options.network, account: options.account});
105
+ });
106
+
107
+ accountCommand
108
+ .command("unlock")
109
+ .description("Unlock account by caching private key in OS keychain")
110
+ .option("--account <name>", "Account to unlock")
111
+ .action(async (options: UnlockAccountOptions) => {
112
+ const unlockAction = new UnlockAccountAction();
113
+ await unlockAction.execute(options);
114
+ });
115
+
116
+ accountCommand
117
+ .command("lock")
118
+ .description("Lock account by removing private key from OS keychain")
119
+ .option("--account <name>", "Account to lock")
120
+ .action(async (options: LockAccountOptions) => {
121
+ const lockAction = new LockAccountAction();
122
+ await lockAction.execute(options);
123
+ });
124
+
125
+ return program;
126
+ }
@@ -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
+ }
@@ -0,0 +1,39 @@
1
+ import {BaseAction} from "../../lib/actions/BaseAction";
2
+
3
+ export interface LockAccountOptions {
4
+ account?: string;
5
+ }
6
+
7
+ export class LockAccountAction extends BaseAction {
8
+ async execute(options?: LockAccountOptions): Promise<void> {
9
+ this.startSpinner("Checking keychain availability...");
10
+
11
+ const keychainAvailable = await this.keychainManager.isKeychainAvailable();
12
+ if (!keychainAvailable) {
13
+ this.failSpinner("OS keychain is not available. This command requires a supported keychain (e.g. macOS Keychain, Windows Credential Manager, or GNOME Keyring).");
14
+ return;
15
+ }
16
+
17
+ if (options?.account) {
18
+ this.accountOverride = options.account;
19
+ }
20
+
21
+ const accountName = this.resolveAccountName();
22
+ this.setSpinnerText(`Checking for cached private key for '${accountName}'...`);
23
+
24
+ const hasCachedKey = await this.keychainManager.getPrivateKey(accountName);
25
+ if (!hasCachedKey) {
26
+ this.succeedSpinner(`Account '${accountName}' is already locked.`);
27
+ return;
28
+ }
29
+
30
+ this.setSpinnerText(`Removing private key for '${accountName}' from OS keychain...`);
31
+
32
+ try {
33
+ await this.keychainManager.removePrivateKey(accountName);
34
+ this.succeedSpinner(`Account '${accountName}' locked! Private key removed from OS keychain.`);
35
+ } catch (error) {
36
+ this.failSpinner("Failed to lock account.", error);
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,30 @@
1
+ import {BaseAction} from "../../lib/actions/BaseAction";
2
+
3
+ export class RemoveAccountAction extends BaseAction {
4
+ constructor() {
5
+ super();
6
+ }
7
+
8
+ async execute(name: string, options: {force?: boolean}): Promise<void> {
9
+ try {
10
+ if (!this.accountExists(name)) {
11
+ this.failSpinner(`Account '${name}' does not exist.`);
12
+ return;
13
+ }
14
+
15
+ if (!options.force) {
16
+ await this.confirmPrompt(`Are you sure you want to remove account '${name}'? This cannot be undone.`);
17
+ }
18
+
19
+ // Remove from keychain if unlocked
20
+ await this.keychainManager.removePrivateKey(name);
21
+
22
+ // Remove keystore file
23
+ this.removeAccount(name);
24
+
25
+ this.logSuccess(`Account '${name}' removed`);
26
+ } catch (error) {
27
+ this.failSpinner("Failed to remove account", error);
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,156 @@
1
+ import {BaseAction, BUILT_IN_NETWORKS, resolveNetwork} from "../../lib/actions/BaseAction";
2
+ import {parseEther, formatEther} from "viem";
3
+ import {createClient, createAccount} from "genlayer-js";
4
+ import type {GenLayerChain, Address, Hash} from "genlayer-js/types";
5
+ import {readFileSync, existsSync} from "fs";
6
+ import {ethers} from "ethers";
7
+
8
+ export interface SendOptions {
9
+ to: string;
10
+ amount: string;
11
+ rpc?: string;
12
+ network?: string;
13
+ account?: string;
14
+ }
15
+
16
+ export class SendAction extends BaseAction {
17
+ constructor() {
18
+ super();
19
+ }
20
+
21
+ private getNetwork(networkOption?: string): GenLayerChain {
22
+ if (networkOption) {
23
+ const network = BUILT_IN_NETWORKS[networkOption];
24
+ if (!network) {
25
+ throw new Error(`Unknown network: ${networkOption}. Available: ${Object.keys(BUILT_IN_NETWORKS).join(", ")}`);
26
+ }
27
+ return network;
28
+ }
29
+ return resolveNetwork(this.getConfig().network);
30
+ }
31
+
32
+ private parseAmount(amount: string): bigint {
33
+ // Support "10gen" or "10" (assumes gen) or wei values
34
+ const lowerAmount = amount.toLowerCase();
35
+ if (lowerAmount.endsWith("gen")) {
36
+ const value = lowerAmount.slice(0, -3);
37
+ return parseEther(value);
38
+ }
39
+ // If it's a large number (likely wei), use as-is
40
+ if (BigInt(amount) > 1_000_000_000_000n) {
41
+ return BigInt(amount);
42
+ }
43
+ // Otherwise assume it's in GEN
44
+ return parseEther(amount);
45
+ }
46
+
47
+ async execute(options: SendOptions): Promise<void> {
48
+ this.startSpinner("Preparing transfer...");
49
+
50
+ try {
51
+ if (options.account) {
52
+ this.accountOverride = options.account;
53
+ }
54
+
55
+ const accountName = this.resolveAccountName();
56
+ const keystorePath = this.getKeystorePath(accountName);
57
+
58
+ if (!existsSync(keystorePath)) {
59
+ this.failSpinner(`Account '${accountName}' not found. Run 'genlayer account create --name ${accountName}' first.`);
60
+ return;
61
+ }
62
+
63
+ const keystoreJson = readFileSync(keystorePath, "utf-8");
64
+ const keystoreData = JSON.parse(keystoreJson);
65
+
66
+ if (!this.isValidKeystoreFormat(keystoreData)) {
67
+ this.failSpinner("Invalid keystore format.");
68
+ return;
69
+ }
70
+
71
+ // Get private key
72
+ const cachedKey = await this.keychainManager.getPrivateKey(accountName);
73
+ let privateKey: string;
74
+
75
+ if (cachedKey) {
76
+ privateKey = cachedKey;
77
+ } else {
78
+ this.stopSpinner();
79
+ const password = await this.promptPassword(`Enter password to unlock account '${accountName}':`);
80
+ this.startSpinner("Preparing transfer...");
81
+ const wallet = await ethers.Wallet.fromEncryptedJson(keystoreJson, password);
82
+ privateKey = wallet.privateKey;
83
+ }
84
+
85
+ const network = this.getNetwork(options.network);
86
+ const account = createAccount(privateKey as Hash);
87
+ const amount = this.parseAmount(options.amount);
88
+
89
+ const client = createClient({
90
+ chain: network,
91
+ account,
92
+ endpoint: options.rpc,
93
+ });
94
+
95
+ this.setSpinnerText(`Sending ${formatEther(amount)} GEN to ${options.to}...`);
96
+
97
+ // Get nonce
98
+ const nonce = await client.getCurrentNonce({address: account.address});
99
+
100
+ // Prepare and sign transaction (let prepareTransactionRequest estimate gas)
101
+ const transactionRequest = await client.prepareTransactionRequest({
102
+ account,
103
+ to: options.to as Address,
104
+ value: amount,
105
+ type: "legacy",
106
+ nonce: Number(nonce),
107
+ });
108
+
109
+ const serializedTransaction = await account.signTransaction(transactionRequest);
110
+ const txHash = await client.sendRawTransaction({serializedTransaction});
111
+
112
+ this.setSpinnerText(`Transaction submitted: ${txHash}\nWaiting for confirmation...`);
113
+
114
+ // Poll for receipt (standard ETH transfer, not GenVM tx)
115
+ let receipt = null;
116
+ for (let i = 0; i < 60; i++) {
117
+ try {
118
+ receipt = await client.getTransactionReceipt({hash: txHash});
119
+ if (receipt) break;
120
+ } catch {
121
+ // Receipt not available yet, continue polling
122
+ }
123
+ await new Promise((r) => setTimeout(r, 2000));
124
+ }
125
+
126
+ if (!receipt) {
127
+ // Tx submitted but receipt not found yet - still success
128
+ this.succeedSpinner("Transfer submitted (pending confirmation)", {
129
+ transactionHash: txHash,
130
+ from: account.address,
131
+ to: options.to,
132
+ amount: `${formatEther(amount)} GEN`,
133
+ });
134
+ return;
135
+ }
136
+
137
+ if (receipt.status === "reverted") {
138
+ this.failSpinner("Transaction reverted");
139
+ return;
140
+ }
141
+
142
+ const result = {
143
+ transactionHash: txHash,
144
+ from: account.address,
145
+ to: options.to,
146
+ amount: `${formatEther(amount)} GEN`,
147
+ blockNumber: receipt.blockNumber.toString(),
148
+ gasUsed: receipt.gasUsed.toString(),
149
+ };
150
+
151
+ this.succeedSpinner("Transfer successful!", result);
152
+ } catch (error: any) {
153
+ this.failSpinner("Transfer failed", error.message || error);
154
+ }
155
+ }
156
+ }
@@ -0,0 +1,74 @@
1
+ import {BaseAction, resolveNetwork} from "../../lib/actions/BaseAction";
2
+ import {formatEther} from "viem";
3
+ import {createClient} from "genlayer-js";
4
+ import type {GenLayerChain, Address} from "genlayer-js/types";
5
+ import {readFileSync, existsSync} from "fs";
6
+
7
+ export interface ShowAccountOptions {
8
+ rpc?: string;
9
+ account?: string;
10
+ }
11
+
12
+ export class ShowAccountAction extends BaseAction {
13
+ constructor() {
14
+ super();
15
+ }
16
+
17
+ private getNetwork(): GenLayerChain {
18
+ return resolveNetwork(this.getConfig().network);
19
+ }
20
+
21
+ async execute(options?: ShowAccountOptions): Promise<void> {
22
+ this.startSpinner("Fetching account info...");
23
+
24
+ try {
25
+ if (options?.account) {
26
+ this.accountOverride = options.account;
27
+ }
28
+
29
+ const accountName = this.resolveAccountName();
30
+ const keystorePath = this.getKeystorePath(accountName);
31
+
32
+ if (!existsSync(keystorePath)) {
33
+ this.failSpinner(`Account '${accountName}' not found. Run 'genlayer account create --name ${accountName}' first.`);
34
+ return;
35
+ }
36
+
37
+ const keystoreData = JSON.parse(readFileSync(keystorePath, "utf-8"));
38
+
39
+ if (!this.isValidKeystoreFormat(keystoreData)) {
40
+ this.failSpinner("Invalid keystore format.");
41
+ return;
42
+ }
43
+
44
+ const rawAddr = keystoreData.address;
45
+ const address = (rawAddr.startsWith("0x") ? rawAddr : `0x${rawAddr}`) as Address;
46
+ const network = this.getNetwork();
47
+
48
+ const client = createClient({
49
+ chain: network,
50
+ account: address,
51
+ endpoint: options?.rpc,
52
+ });
53
+
54
+ const balance = await client.getBalance({address});
55
+ const formattedBalance = formatEther(balance);
56
+
57
+ const isUnlocked = await this.keychainManager.isAccountUnlocked(accountName);
58
+ const isActive = this.getActiveAccount() === accountName;
59
+
60
+ const result = {
61
+ name: accountName,
62
+ address,
63
+ balance: `${formattedBalance} GEN`,
64
+ network: network.name || "localnet",
65
+ status: isUnlocked ? "unlocked" : "locked",
66
+ active: isActive,
67
+ };
68
+
69
+ this.succeedSpinner("Account info", result);
70
+ } catch (error: any) {
71
+ this.failSpinner("Failed to get account info", error.message || error);
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,51 @@
1
+ import {BaseAction} from "../../lib/actions/BaseAction";
2
+ import {readFileSync, existsSync} from "fs";
3
+ import {ethers} from "ethers";
4
+
5
+ export interface UnlockAccountOptions {
6
+ account?: string;
7
+ }
8
+
9
+ export class UnlockAccountAction extends BaseAction {
10
+ async execute(options?: UnlockAccountOptions): Promise<void> {
11
+ this.startSpinner("Checking keychain availability...");
12
+
13
+ const keychainAvailable = await this.keychainManager.isKeychainAvailable();
14
+ if (!keychainAvailable) {
15
+ this.failSpinner("OS keychain is not available. This command requires a supported keychain (e.g. macOS Keychain, Windows Credential Manager, or GNOME Keyring).");
16
+ return;
17
+ }
18
+
19
+ if (options?.account) {
20
+ this.accountOverride = options.account;
21
+ }
22
+
23
+ const accountName = this.resolveAccountName();
24
+ this.setSpinnerText(`Checking for account '${accountName}'...`);
25
+
26
+ const keystorePath = this.getKeystorePath(accountName);
27
+ if (!existsSync(keystorePath)) {
28
+ this.failSpinner(`Account '${accountName}' not found. Run 'genlayer account create --name ${accountName}' first.`);
29
+ return;
30
+ }
31
+
32
+ const keystoreJson = readFileSync(keystorePath, "utf-8");
33
+ const keystoreData = JSON.parse(keystoreJson);
34
+ if (!this.isValidKeystoreFormat(keystoreData)) {
35
+ this.failSpinner("Invalid keystore format.");
36
+ return;
37
+ }
38
+
39
+ this.stopSpinner();
40
+
41
+ try {
42
+ const password = await this.promptPassword(`Enter password to unlock '${accountName}':`);
43
+ const wallet = await ethers.Wallet.fromEncryptedJson(keystoreJson, password);
44
+
45
+ await this.keychainManager.storePrivateKey(accountName, wallet.privateKey);
46
+ this.succeedSpinner(`Account '${accountName}' unlocked! Private key cached in OS keychain.`);
47
+ } catch (error) {
48
+ this.failSpinner("Failed to unlock account.", error);
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,21 @@
1
+ import {BaseAction} from "../../lib/actions/BaseAction";
2
+
3
+ export class UseAccountAction extends BaseAction {
4
+ constructor() {
5
+ super();
6
+ }
7
+
8
+ async execute(name: string): Promise<void> {
9
+ try {
10
+ if (!this.accountExists(name)) {
11
+ this.failSpinner(`Account '${name}' does not exist. Run 'genlayer account list' to see available accounts.`);
12
+ return;
13
+ }
14
+
15
+ this.setActiveAccount(name);
16
+ this.logSuccess(`Active account set to '${name}'`);
17
+ } catch (error) {
18
+ this.failSpinner("Failed to set active account", error);
19
+ }
20
+ }
21
+ }