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
@@ -1,7 +1,11 @@
1
1
  import {BaseAction} from "../../lib/actions/BaseAction";
2
2
 
3
+ export interface LockAccountOptions {
4
+ account?: string;
5
+ }
6
+
3
7
  export class LockAccountAction extends BaseAction {
4
- async execute(): Promise<void> {
8
+ async execute(options?: LockAccountOptions): Promise<void> {
5
9
  this.startSpinner("Checking keychain availability...");
6
10
 
7
11
  const keychainAvailable = await this.keychainManager.isKeychainAvailable();
@@ -10,19 +14,24 @@ export class LockAccountAction extends BaseAction {
10
14
  return;
11
15
  }
12
16
 
13
- this.setSpinnerText("Checking for cached private key...");
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}'...`);
14
23
 
15
- const hasCachedKey = await this.keychainManager.getPrivateKey();
24
+ const hasCachedKey = await this.keychainManager.getPrivateKey(accountName);
16
25
  if (!hasCachedKey) {
17
- this.succeedSpinner("Account is already locked.");
26
+ this.succeedSpinner(`Account '${accountName}' is already locked.`);
18
27
  return;
19
28
  }
20
29
 
21
- this.setSpinnerText("Removing private key from OS keychain...");
30
+ this.setSpinnerText(`Removing private key for '${accountName}' from OS keychain...`);
22
31
 
23
32
  try {
24
- await this.keychainManager.removePrivateKey();
25
- this.succeedSpinner("Account locked! Private key removed from OS keychain.");
33
+ await this.keychainManager.removePrivateKey(accountName);
34
+ this.succeedSpinner(`Account '${accountName}' locked! Private key removed from OS keychain.`);
26
35
  } catch (error) {
27
36
  this.failSpinner("Failed to lock account.", error);
28
37
  }
@@ -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
+ }
@@ -4,13 +4,13 @@ import {createClient, createAccount} from "genlayer-js";
4
4
  import type {GenLayerChain, Address, Hash} from "genlayer-js/types";
5
5
  import {readFileSync, existsSync} from "fs";
6
6
  import {ethers} from "ethers";
7
- import {KeystoreData} from "../../lib/interfaces/KeystoreData";
8
7
 
9
8
  export interface SendOptions {
10
9
  to: string;
11
10
  amount: string;
12
11
  rpc?: string;
13
12
  network?: string;
13
+ account?: string;
14
14
  }
15
15
 
16
16
  export class SendAction extends BaseAction {
@@ -48,14 +48,20 @@ export class SendAction extends BaseAction {
48
48
  this.startSpinner("Preparing transfer...");
49
49
 
50
50
  try {
51
- const keypairPath = this.getConfigByKey("keyPairPath");
51
+ if (options.account) {
52
+ this.accountOverride = options.account;
53
+ }
54
+
55
+ const accountName = this.resolveAccountName();
56
+ const keystorePath = this.getKeystorePath(accountName);
52
57
 
53
- if (!keypairPath || !existsSync(keypairPath)) {
54
- this.failSpinner("No account found. Run 'genlayer account create' first.");
58
+ if (!existsSync(keystorePath)) {
59
+ this.failSpinner(`Account '${accountName}' not found. Run 'genlayer account create --name ${accountName}' first.`);
55
60
  return;
56
61
  }
57
62
 
58
- const keystoreData: KeystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
63
+ const keystoreJson = readFileSync(keystorePath, "utf-8");
64
+ const keystoreData = JSON.parse(keystoreJson);
59
65
 
60
66
  if (!this.isValidKeystoreFormat(keystoreData)) {
61
67
  this.failSpinner("Invalid keystore format.");
@@ -63,16 +69,16 @@ export class SendAction extends BaseAction {
63
69
  }
64
70
 
65
71
  // Get private key
66
- const cachedKey = await this.keychainManager.getPrivateKey();
72
+ const cachedKey = await this.keychainManager.getPrivateKey(accountName);
67
73
  let privateKey: string;
68
74
 
69
75
  if (cachedKey) {
70
76
  privateKey = cachedKey;
71
77
  } else {
72
78
  this.stopSpinner();
73
- const password = await this.promptPassword("Enter password to unlock account:");
79
+ const password = await this.promptPassword(`Enter password to unlock account '${accountName}':`);
74
80
  this.startSpinner("Preparing transfer...");
75
- const wallet = await ethers.Wallet.fromEncryptedJson(keystoreData.encrypted, password);
81
+ const wallet = await ethers.Wallet.fromEncryptedJson(keystoreJson, password);
76
82
  privateKey = wallet.privateKey;
77
83
  }
78
84
 
@@ -3,7 +3,11 @@ import {formatEther} from "viem";
3
3
  import {createClient} from "genlayer-js";
4
4
  import type {GenLayerChain, Address} from "genlayer-js/types";
5
5
  import {readFileSync, existsSync} from "fs";
6
- import {KeystoreData} from "../../lib/interfaces/KeystoreData";
6
+
7
+ export interface ShowAccountOptions {
8
+ rpc?: string;
9
+ account?: string;
10
+ }
7
11
 
8
12
  export class ShowAccountAction extends BaseAction {
9
13
  constructor() {
@@ -14,42 +18,52 @@ export class ShowAccountAction extends BaseAction {
14
18
  return resolveNetwork(this.getConfig().network);
15
19
  }
16
20
 
17
- async execute(): Promise<void> {
21
+ async execute(options?: ShowAccountOptions): Promise<void> {
18
22
  this.startSpinner("Fetching account info...");
19
23
 
20
24
  try {
21
- const keypairPath = this.getConfigByKey("keyPairPath");
25
+ if (options?.account) {
26
+ this.accountOverride = options.account;
27
+ }
28
+
29
+ const accountName = this.resolveAccountName();
30
+ const keystorePath = this.getKeystorePath(accountName);
22
31
 
23
- if (!keypairPath || !existsSync(keypairPath)) {
24
- this.failSpinner("No account found. Run 'genlayer account create' first.");
32
+ if (!existsSync(keystorePath)) {
33
+ this.failSpinner(`Account '${accountName}' not found. Run 'genlayer account create --name ${accountName}' first.`);
25
34
  return;
26
35
  }
27
36
 
28
- const keystoreData: KeystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
37
+ const keystoreData = JSON.parse(readFileSync(keystorePath, "utf-8"));
29
38
 
30
39
  if (!this.isValidKeystoreFormat(keystoreData)) {
31
40
  this.failSpinner("Invalid keystore format.");
32
41
  return;
33
42
  }
34
43
 
35
- const address = keystoreData.address as Address;
44
+ const rawAddr = keystoreData.address;
45
+ const address = (rawAddr.startsWith("0x") ? rawAddr : `0x${rawAddr}`) as Address;
36
46
  const network = this.getNetwork();
37
47
 
38
48
  const client = createClient({
39
49
  chain: network,
40
50
  account: address,
51
+ endpoint: options?.rpc,
41
52
  });
42
53
 
43
54
  const balance = await client.getBalance({address});
44
55
  const formattedBalance = formatEther(balance);
45
56
 
46
- const isUnlocked = await this.keychainManager.getPrivateKey();
57
+ const isUnlocked = await this.keychainManager.isAccountUnlocked(accountName);
58
+ const isActive = this.getActiveAccount() === accountName;
47
59
 
48
60
  const result = {
61
+ name: accountName,
49
62
  address,
50
63
  balance: `${formattedBalance} GEN`,
51
64
  network: network.name || "localnet",
52
65
  status: isUnlocked ? "unlocked" : "locked",
66
+ active: isActive,
53
67
  };
54
68
 
55
69
  this.succeedSpinner("Account info", result);
@@ -2,8 +2,12 @@ import {BaseAction} from "../../lib/actions/BaseAction";
2
2
  import {readFileSync, existsSync} from "fs";
3
3
  import {ethers} from "ethers";
4
4
 
5
+ export interface UnlockAccountOptions {
6
+ account?: string;
7
+ }
8
+
5
9
  export class UnlockAccountAction extends BaseAction {
6
- async execute(): Promise<void> {
10
+ async execute(options?: UnlockAccountOptions): Promise<void> {
7
11
  this.startSpinner("Checking keychain availability...");
8
12
 
9
13
  const keychainAvailable = await this.keychainManager.isKeychainAvailable();
@@ -12,15 +16,21 @@ export class UnlockAccountAction extends BaseAction {
12
16
  return;
13
17
  }
14
18
 
15
- this.setSpinnerText("Checking for existing account...");
19
+ if (options?.account) {
20
+ this.accountOverride = options.account;
21
+ }
22
+
23
+ const accountName = this.resolveAccountName();
24
+ this.setSpinnerText(`Checking for account '${accountName}'...`);
16
25
 
17
- const keypairPath = this.getConfigByKey("keyPairPath");
18
- if (!keypairPath || !existsSync(keypairPath)) {
19
- this.failSpinner("No account found. Run 'genlayer account create' first.");
26
+ const keystorePath = this.getKeystorePath(accountName);
27
+ if (!existsSync(keystorePath)) {
28
+ this.failSpinner(`Account '${accountName}' not found. Run 'genlayer account create --name ${accountName}' first.`);
20
29
  return;
21
30
  }
22
31
 
23
- const keystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
32
+ const keystoreJson = readFileSync(keystorePath, "utf-8");
33
+ const keystoreData = JSON.parse(keystoreJson);
24
34
  if (!this.isValidKeystoreFormat(keystoreData)) {
25
35
  this.failSpinner("Invalid keystore format.");
26
36
  return;
@@ -29,11 +39,11 @@ export class UnlockAccountAction extends BaseAction {
29
39
  this.stopSpinner();
30
40
 
31
41
  try {
32
- const password = await this.promptPassword("Enter password to unlock account:");
33
- const wallet = await ethers.Wallet.fromEncryptedJson(keystoreData.encrypted, password);
42
+ const password = await this.promptPassword(`Enter password to unlock '${accountName}':`);
43
+ const wallet = await ethers.Wallet.fromEncryptedJson(keystoreJson, password);
34
44
 
35
- await this.keychainManager.storePrivateKey(wallet.privateKey);
36
- this.succeedSpinner("Account unlocked! Private key cached in OS keychain.");
45
+ await this.keychainManager.storePrivateKey(accountName, wallet.privateKey);
46
+ this.succeedSpinner(`Account '${accountName}' unlocked! Private key cached in OS keychain.`);
37
47
  } catch (error) {
38
48
  this.failSpinner("Failed to unlock account.", error);
39
49
  }
@@ -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
+ }
@@ -4,11 +4,26 @@ import {NetworkActions} from "./setNetwork";
4
4
  export function initializeNetworkCommands(program: Command) {
5
5
  const networkActions = new NetworkActions();
6
6
 
7
- program
8
- .command("network")
7
+ const network = program.command("network").description("Network configuration");
8
+
9
+ // genlayer network set [name]
10
+ network
11
+ .command("set")
9
12
  .description("Set the network to use")
10
- .argument("[network]", "The network to use")
13
+ .argument("[network]", "The network to set")
11
14
  .action((networkName?: string) => networkActions.setNetwork(networkName));
12
15
 
16
+ // genlayer network info
17
+ network
18
+ .command("info")
19
+ .description("Show current network configuration and contract addresses")
20
+ .action(() => networkActions.showInfo());
21
+
22
+ // genlayer network list
23
+ network
24
+ .command("list")
25
+ .description("List available networks")
26
+ .action(() => networkActions.listNetworks());
27
+
13
28
  return program;
14
29
  }
@@ -1,32 +1,48 @@
1
- import {AiProviders} from "@/lib/config/simulator";
2
- import {BaseAction} from "../../lib/actions/BaseAction";
1
+ import {BaseAction, BUILT_IN_NETWORKS, resolveNetwork} from "../../lib/actions/BaseAction";
3
2
  import inquirer, {DistinctQuestion} from "inquirer";
4
- import {localnet, studionet, testnetAsimov} from "genlayer-js/chains";
5
- import {} from "genlayer-js/chains";
6
-
7
- const networks = [
8
- {
9
- name: localnet.name,
10
- alias: "localnet",
11
- value: localnet,
12
- },
13
- {
14
- name: studionet.name,
15
- alias: "studionet",
16
- value: studionet,
17
- },
18
- {
19
- name: testnetAsimov.name,
20
- alias: "testnet-asimov",
21
- value: testnetAsimov,
22
- },
23
- ];
3
+
4
+ const networks = Object.entries(BUILT_IN_NETWORKS).map(([alias, network]) => ({
5
+ name: network.name,
6
+ alias,
7
+ value: network,
8
+ }));
24
9
 
25
10
  export class NetworkActions extends BaseAction {
26
11
  constructor() {
27
12
  super();
28
13
  }
29
14
 
15
+ async showInfo(): Promise<void> {
16
+ const storedNetwork = this.getConfigByKey("network") || "localnet";
17
+ const network = resolveNetwork(storedNetwork);
18
+
19
+ const info: Record<string, string> = {
20
+ alias: storedNetwork,
21
+ name: network.name,
22
+ chainId: network.id?.toString() || "unknown",
23
+ rpc: network.rpcUrls?.default?.http?.[0] || "unknown",
24
+ mainContract: network.consensusMainContract?.address || "not set",
25
+ stakingContract: network.stakingContract?.address || "not set",
26
+ };
27
+
28
+ if (network.blockExplorers?.default?.url) {
29
+ info.explorer = network.blockExplorers.default.url;
30
+ }
31
+
32
+ this.succeedSpinner("Current network", info);
33
+ }
34
+
35
+ async listNetworks(): Promise<void> {
36
+ const currentNetwork = this.getConfigByKey("network") || "localnet";
37
+
38
+ console.log("");
39
+ for (const net of networks) {
40
+ const marker = net.alias === currentNetwork ? "*" : " ";
41
+ console.log(`${marker} ${net.alias.padEnd(16)} ${net.name}`);
42
+ }
43
+ console.log("");
44
+ }
45
+
30
46
  async setNetwork(networkName?: string): Promise<void> {
31
47
  if (networkName || networkName === "") {
32
48
  if (!networks.some(n => n.name === networkName || n.alias === networkName)) {
@@ -3,12 +3,15 @@ import {createClient, createAccount, formatStakingAmount, parseStakingAmount, ab
3
3
  import type {GenLayerClient, GenLayerChain, Address} from "genlayer-js/types";
4
4
  import {readFileSync, existsSync} from "fs";
5
5
  import {ethers} from "ethers";
6
- import {KeystoreData} from "../../lib/interfaces/KeystoreData";
6
+
7
+ // Re-export for use by other staking commands
8
+ export {BUILT_IN_NETWORKS};
7
9
 
8
10
  export interface StakingConfig {
9
11
  rpc?: string;
10
12
  stakingAddress?: string;
11
13
  network?: string;
14
+ account?: string;
12
15
  }
13
16
 
14
17
  export class StakingAction extends BaseAction {
@@ -33,6 +36,11 @@ export class StakingAction extends BaseAction {
33
36
 
34
37
  protected async getStakingClient(config: StakingConfig): Promise<GenLayerClient<GenLayerChain>> {
35
38
  if (!this._stakingClient) {
39
+ // Set account override if provided
40
+ if (config.account) {
41
+ this.accountOverride = config.account;
42
+ }
43
+
36
44
  const network = this.getNetwork(config);
37
45
 
38
46
  // Override staking address if provided
@@ -56,6 +64,11 @@ export class StakingAction extends BaseAction {
56
64
  }
57
65
 
58
66
  protected async getReadOnlyStakingClient(config: StakingConfig): Promise<GenLayerClient<GenLayerChain>> {
67
+ // Set account override if provided
68
+ if (config.account) {
69
+ this.accountOverride = config.account;
70
+ }
71
+
59
72
  const network = this.getNetwork(config);
60
73
 
61
74
  if (config.stakingAddress) {
@@ -65,44 +78,61 @@ export class StakingAction extends BaseAction {
65
78
  };
66
79
  }
67
80
 
68
- const keypairPath = this.getConfigByKey("keyPairPath");
69
- if (!keypairPath || !existsSync(keypairPath)) {
70
- throw new Error("No account found. Run 'genlayer account create' first.");
81
+ const accountName = this.resolveAccountName();
82
+ const keystorePath = this.getKeystorePath(accountName);
83
+
84
+ if (!existsSync(keystorePath)) {
85
+ throw new Error(`Account '${accountName}' not found. Run 'genlayer account create --name ${accountName}' first.`);
71
86
  }
72
87
 
73
- const keystoreData: KeystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
88
+ const keystoreData = JSON.parse(readFileSync(keystorePath, "utf-8"));
89
+ const addr = keystoreData.address as string;
90
+ const normalizedAddress = (addr.startsWith("0x") ? addr : `0x${addr}`) as Address;
74
91
 
75
92
  return createClient({
76
93
  chain: network,
77
94
  endpoint: config.rpc,
78
- account: keystoreData.address as Address,
95
+ account: normalizedAddress,
79
96
  });
80
97
  }
81
98
 
82
99
  private async getPrivateKeyForStaking(): Promise<string> {
83
- const keypairPath = this.getConfigByKey("keyPairPath");
100
+ const accountName = this.resolveAccountName();
101
+ const keystorePath = this.getKeystorePath(accountName);
84
102
 
85
- if (!keypairPath || !existsSync(keypairPath)) {
86
- throw new Error("No account found. Run 'genlayer account create' first.");
103
+ if (!existsSync(keystorePath)) {
104
+ throw new Error(`Account '${accountName}' not found. Run 'genlayer account create --name ${accountName}' first.`);
87
105
  }
88
106
 
89
- const keystoreData: KeystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
107
+ const keystoreJson = readFileSync(keystorePath, "utf-8");
108
+ const keystoreData = JSON.parse(keystoreJson);
90
109
 
91
110
  if (!this.isValidKeystoreFormat(keystoreData)) {
92
111
  throw new Error("Invalid keystore format.");
93
112
  }
94
113
 
95
- const cachedKey = await this.keychainManager.getPrivateKey();
114
+ const cachedKey = await this.keychainManager.getPrivateKey(accountName);
96
115
  if (cachedKey) {
97
- return cachedKey;
116
+ // Verify cached key matches keystore address - safety check
117
+ const tempAccount = createAccount(cachedKey as `0x${string}`);
118
+ const cachedAddress = tempAccount.address.toLowerCase();
119
+ const keystoreAddress = `0x${keystoreData.address.toLowerCase().replace(/^0x/, '')}`;
120
+
121
+ if (cachedAddress !== keystoreAddress) {
122
+ // Cached key doesn't match keystore - invalidate it
123
+ await this.keychainManager.removePrivateKey(accountName);
124
+ // Fall through to prompt for password
125
+ } else {
126
+ return cachedKey;
127
+ }
98
128
  }
99
129
 
100
130
  // Stop spinner before prompting for password
101
131
  this.stopSpinner();
102
- const password = await this.promptPassword("Enter password to unlock account:");
132
+ const password = await this.promptPassword(`Enter password to unlock account '${accountName}':`);
103
133
  this.startSpinner("Continuing...");
104
134
 
105
- const wallet = await ethers.Wallet.fromEncryptedJson(keystoreData.encrypted, password);
135
+ const wallet = await ethers.Wallet.fromEncryptedJson(keystoreJson, password);
106
136
  return wallet.privateKey;
107
137
  }
108
138
 
@@ -115,11 +145,13 @@ export class StakingAction extends BaseAction {
115
145
  }
116
146
 
117
147
  protected async getSignerAddress(): Promise<Address> {
118
- const keypairPath = this.getConfigByKey("keyPairPath");
119
- if (!keypairPath || !existsSync(keypairPath)) {
120
- throw new Error("Keypair file not found.");
148
+ const accountName = this.resolveAccountName();
149
+ const keystorePath = this.getKeystorePath(accountName);
150
+ if (!existsSync(keystorePath)) {
151
+ throw new Error(`Account '${accountName}' not found.`);
121
152
  }
122
- const keystoreData: KeystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
123
- return keystoreData.address as Address;
153
+ const keystoreData = JSON.parse(readFileSync(keystorePath, "utf-8"));
154
+ const addr = keystoreData.address as string;
155
+ return (addr.startsWith("0x") ? addr : `0x${addr}`) as Address;
124
156
  }
125
157
  }
@@ -1,5 +1,6 @@
1
1
  import {StakingAction, StakingConfig} from "./StakingAction";
2
2
  import type {Address} from "genlayer-js/types";
3
+ import chalk from "chalk";
3
4
 
4
5
  export interface DelegatorJoinOptions extends StakingConfig {
5
6
  validator: string;
@@ -35,6 +36,7 @@ export class DelegatorJoinAction extends StakingAction {
35
36
  };
36
37
 
37
38
  this.succeedSpinner("Successfully joined as delegator!", output);
39
+ console.log(chalk.dim(`\nTo view your delegation: genlayer staking delegation-info --validator ${options.validator}`));
38
40
  } catch (error: any) {
39
41
  this.failSpinner("Failed to join as delegator", error.message || error);
40
42
  }