genlayer 0.30.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/CLAUDE.md +55 -0
- package/README.md +121 -8
- package/dist/index.js +8424 -2489
- package/docs/delegator-guide.md +203 -0
- package/docs/validator-guide.md +260 -0
- package/package.json +2 -2
- package/src/commands/account/create.ts +23 -0
- package/src/commands/account/import.ts +81 -0
- package/src/commands/account/index.ts +67 -0
- package/src/commands/{keygen → account}/lock.ts +6 -7
- package/src/commands/account/send.ts +150 -0
- package/src/commands/account/show.ts +60 -0
- package/src/commands/{keygen → account}/unlock.ts +12 -12
- package/src/commands/contracts/code.ts +33 -0
- package/src/commands/contracts/index.ts +10 -0
- package/src/commands/network/setNetwork.ts +5 -4
- package/src/commands/staking/StakingAction.ts +125 -0
- package/src/commands/staking/delegatorClaim.ts +41 -0
- package/src/commands/staking/delegatorExit.ts +50 -0
- package/src/commands/staking/delegatorJoin.ts +42 -0
- package/src/commands/staking/index.ts +224 -0
- package/src/commands/staking/setIdentity.ts +61 -0
- package/src/commands/staking/setOperator.ts +40 -0
- package/src/commands/staking/stakingInfo.ts +292 -0
- package/src/commands/staking/validatorClaim.ts +38 -0
- package/src/commands/staking/validatorDeposit.ts +35 -0
- package/src/commands/staking/validatorExit.ts +44 -0
- package/src/commands/staking/validatorJoin.ts +47 -0
- package/src/commands/staking/validatorPrime.ts +35 -0
- package/src/index.ts +4 -2
- package/src/lib/actions/BaseAction.ts +43 -10
- package/tests/actions/code.test.ts +87 -0
- package/tests/actions/create.test.ts +18 -18
- package/tests/actions/lock.test.ts +7 -7
- package/tests/actions/setNetwork.test.ts +18 -57
- package/tests/actions/staking.test.ts +323 -0
- package/tests/actions/unlock.test.ts +12 -12
- package/tests/commands/account.test.ts +121 -0
- package/tests/commands/code.test.ts +69 -0
- package/tests/commands/staking.test.ts +211 -0
- package/tests/index.test.ts +6 -2
- package/tests/libs/baseAction.test.ts +7 -1
- package/src/commands/keygen/create.ts +0 -23
- package/src/commands/keygen/index.ts +0 -39
- package/tests/commands/keygen.test.ts +0 -123
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {StakingAction, StakingConfig} from "./StakingAction";
|
|
2
|
+
|
|
3
|
+
export interface ValidatorDepositOptions extends StakingConfig {
|
|
4
|
+
amount: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class ValidatorDepositAction extends StakingAction {
|
|
8
|
+
constructor() {
|
|
9
|
+
super();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async execute(options: ValidatorDepositOptions): Promise<void> {
|
|
13
|
+
this.startSpinner("Making validator deposit...");
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const client = await this.getStakingClient(options);
|
|
17
|
+
const amount = this.parseAmount(options.amount);
|
|
18
|
+
|
|
19
|
+
this.setSpinnerText(`Depositing ${this.formatAmount(amount)} to validator stake...`);
|
|
20
|
+
|
|
21
|
+
const result = await client.validatorDeposit({amount});
|
|
22
|
+
|
|
23
|
+
const output = {
|
|
24
|
+
transactionHash: result.transactionHash,
|
|
25
|
+
amount: this.formatAmount(amount),
|
|
26
|
+
blockNumber: result.blockNumber.toString(),
|
|
27
|
+
gasUsed: result.gasUsed.toString(),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
this.succeedSpinner("Deposit successful!", output);
|
|
31
|
+
} catch (error: any) {
|
|
32
|
+
this.failSpinner("Failed to make deposit", error.message || error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {StakingAction, StakingConfig} from "./StakingAction";
|
|
2
|
+
|
|
3
|
+
export interface ValidatorExitOptions extends StakingConfig {
|
|
4
|
+
shares: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class ValidatorExitAction extends StakingAction {
|
|
8
|
+
constructor() {
|
|
9
|
+
super();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async execute(options: ValidatorExitOptions): Promise<void> {
|
|
13
|
+
this.startSpinner("Initiating validator exit...");
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
let shares: bigint;
|
|
17
|
+
try {
|
|
18
|
+
shares = BigInt(options.shares);
|
|
19
|
+
if (shares <= 0n) throw new Error("must be positive");
|
|
20
|
+
} catch {
|
|
21
|
+
this.failSpinner(`Invalid shares value: "${options.shares}". Must be a positive whole number.`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const client = await this.getStakingClient(options);
|
|
26
|
+
|
|
27
|
+
this.setSpinnerText(`Exiting with ${shares} shares...`);
|
|
28
|
+
|
|
29
|
+
const result = await client.validatorExit({shares});
|
|
30
|
+
|
|
31
|
+
const output = {
|
|
32
|
+
transactionHash: result.transactionHash,
|
|
33
|
+
sharesWithdrawn: shares.toString(),
|
|
34
|
+
blockNumber: result.blockNumber.toString(),
|
|
35
|
+
gasUsed: result.gasUsed.toString(),
|
|
36
|
+
note: "Withdrawal will be claimable after the unbonding period",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
this.succeedSpinner("Exit initiated successfully!", output);
|
|
40
|
+
} catch (error: any) {
|
|
41
|
+
this.failSpinner("Failed to exit", error.message || error);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {StakingAction, StakingConfig} from "./StakingAction";
|
|
2
|
+
import type {Address} from "genlayer-js/types";
|
|
3
|
+
|
|
4
|
+
export interface ValidatorJoinOptions extends StakingConfig {
|
|
5
|
+
amount: string;
|
|
6
|
+
operator?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class ValidatorJoinAction extends StakingAction {
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async execute(options: ValidatorJoinOptions): Promise<void> {
|
|
15
|
+
this.startSpinner("Creating a new validator...");
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const client = await this.getStakingClient(options);
|
|
19
|
+
const amount = this.parseAmount(options.amount);
|
|
20
|
+
const signerAddress = await this.getSignerAddress();
|
|
21
|
+
|
|
22
|
+
this.setSpinnerText(`Creating validator with ${this.formatAmount(amount)} stake...`);
|
|
23
|
+
this.log(` From: ${signerAddress}`);
|
|
24
|
+
if (options.operator) {
|
|
25
|
+
this.log(` Operator: ${options.operator}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const result = await client.validatorJoin({
|
|
29
|
+
amount,
|
|
30
|
+
operator: options.operator as Address | undefined,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const output = {
|
|
34
|
+
transactionHash: result.transactionHash,
|
|
35
|
+
validatorWallet: result.validatorWallet,
|
|
36
|
+
amount: result.amount,
|
|
37
|
+
operator: result.operator,
|
|
38
|
+
blockNumber: result.blockNumber.toString(),
|
|
39
|
+
gasUsed: result.gasUsed.toString(),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
this.succeedSpinner("Validator created successfully!", output);
|
|
43
|
+
} catch (error: any) {
|
|
44
|
+
this.failSpinner("Failed to create validator", error.message || error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {StakingAction, StakingConfig} from "./StakingAction";
|
|
2
|
+
import type {Address} from "genlayer-js/types";
|
|
3
|
+
|
|
4
|
+
export interface ValidatorPrimeOptions extends StakingConfig {
|
|
5
|
+
validator: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class ValidatorPrimeAction extends StakingAction {
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async execute(options: ValidatorPrimeOptions): Promise<void> {
|
|
14
|
+
this.startSpinner("Priming validator...");
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const client = await this.getStakingClient(options);
|
|
18
|
+
|
|
19
|
+
this.setSpinnerText(`Priming validator ${options.validator}...`);
|
|
20
|
+
|
|
21
|
+
const result = await client.validatorPrime({validator: options.validator as Address});
|
|
22
|
+
|
|
23
|
+
const output = {
|
|
24
|
+
transactionHash: result.transactionHash,
|
|
25
|
+
validator: options.validator,
|
|
26
|
+
blockNumber: result.blockNumber.toString(),
|
|
27
|
+
gasUsed: result.gasUsed.toString(),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
this.succeedSpinner("Validator primed for next epoch!", output);
|
|
31
|
+
} catch (error: any) {
|
|
32
|
+
this.failSpinner("Failed to prime validator", error.message || error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import {program} from "commander";
|
|
|
3
3
|
import {version} from "../package.json";
|
|
4
4
|
import {CLI_DESCRIPTION} from "../src/lib/config/text";
|
|
5
5
|
import {initializeGeneralCommands} from "../src/commands/general";
|
|
6
|
-
import {
|
|
6
|
+
import {initializeAccountCommands} from "../src/commands/account";
|
|
7
7
|
import {initializeContractsCommands} from "../src/commands/contracts";
|
|
8
8
|
import {initializeConfigCommands} from "../src/commands/config";
|
|
9
9
|
import {initializeValidatorCommands} from "../src/commands/localnet";
|
|
@@ -11,11 +11,12 @@ import {initializeUpdateCommands} from "../src/commands/update";
|
|
|
11
11
|
import {initializeScaffoldCommands} from "../src/commands/scaffold";
|
|
12
12
|
import {initializeNetworkCommands} from "../src/commands/network";
|
|
13
13
|
import {initializeTransactionsCommands} from "../src/commands/transactions";
|
|
14
|
+
import {initializeStakingCommands} from "../src/commands/staking";
|
|
14
15
|
|
|
15
16
|
export function initializeCLI() {
|
|
16
17
|
program.version(version).description(CLI_DESCRIPTION);
|
|
17
18
|
initializeGeneralCommands(program);
|
|
18
|
-
|
|
19
|
+
initializeAccountCommands(program);
|
|
19
20
|
initializeContractsCommands(program);
|
|
20
21
|
initializeConfigCommands(program);
|
|
21
22
|
initializeUpdateCommands(program);
|
|
@@ -23,6 +24,7 @@ export function initializeCLI() {
|
|
|
23
24
|
initializeScaffoldCommands(program);
|
|
24
25
|
initializeNetworkCommands(program);
|
|
25
26
|
initializeTransactionsCommands(program);
|
|
27
|
+
initializeStakingCommands(program);
|
|
26
28
|
program.parse(process.argv);
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -5,8 +5,43 @@ import chalk from "chalk";
|
|
|
5
5
|
import inquirer from "inquirer";
|
|
6
6
|
import { inspect } from "util";
|
|
7
7
|
import {createClient, createAccount} from "genlayer-js";
|
|
8
|
-
import {localnet} from "genlayer-js/chains";
|
|
8
|
+
import {localnet, studionet, testnetAsimov} from "genlayer-js/chains";
|
|
9
9
|
import type {GenLayerClient, GenLayerChain, Hash, Address, Account} from "genlayer-js/types";
|
|
10
|
+
|
|
11
|
+
// Built-in networks - always resolve fresh from genlayer-js
|
|
12
|
+
export const BUILT_IN_NETWORKS: Record<string, GenLayerChain> = {
|
|
13
|
+
"localnet": localnet,
|
|
14
|
+
"studionet": studionet,
|
|
15
|
+
"testnet-asimov": testnetAsimov,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolves a stored network config to a fresh chain object.
|
|
20
|
+
* Handles both new format (alias string) and old format (JSON object) for backwards compat.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveNetwork(stored: string | undefined): GenLayerChain {
|
|
23
|
+
if (!stored) return localnet;
|
|
24
|
+
|
|
25
|
+
// Try as alias first (new format)
|
|
26
|
+
if (BUILT_IN_NETWORKS[stored]) {
|
|
27
|
+
return BUILT_IN_NETWORKS[stored];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Backwards compat: try parsing as JSON (old format)
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(stored);
|
|
33
|
+
// If it has a known name, use fresh version instead
|
|
34
|
+
const alias = Object.entries(BUILT_IN_NETWORKS)
|
|
35
|
+
.find(([_, chain]) => chain.name === parsed.name)?.[0];
|
|
36
|
+
if (alias) {
|
|
37
|
+
return BUILT_IN_NETWORKS[alias];
|
|
38
|
+
}
|
|
39
|
+
// Custom network - use as-is
|
|
40
|
+
return parsed;
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(`Unknown network: ${stored}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
10
45
|
import { ethers } from "ethers";
|
|
11
46
|
import { writeFileSync, existsSync, readFileSync } from "fs";
|
|
12
47
|
import { KeystoreData } from "../interfaces/KeystoreData";
|
|
@@ -38,7 +73,6 @@ export class BaseAction extends ConfigFileManager {
|
|
|
38
73
|
} catch (error) {
|
|
39
74
|
if (attempt >= BaseAction.MAX_PASSWORD_ATTEMPTS) {
|
|
40
75
|
this.failSpinner(`Maximum password attempts exceeded (${BaseAction.MAX_PASSWORD_ATTEMPTS}/${BaseAction.MAX_PASSWORD_ATTEMPTS}).`);
|
|
41
|
-
process.exit(1);
|
|
42
76
|
}
|
|
43
77
|
return await this.decryptKeystore(keystoreData, attempt + 1);
|
|
44
78
|
}
|
|
@@ -62,8 +96,7 @@ export class BaseAction extends ConfigFileManager {
|
|
|
62
96
|
|
|
63
97
|
protected async getClient(rpcUrl?: string, readOnly: boolean = false): Promise<GenLayerClient<GenLayerChain>> {
|
|
64
98
|
if (!this._genlayerClient) {
|
|
65
|
-
const
|
|
66
|
-
const network = networkConfig ? JSON.parse(networkConfig) : localnet;
|
|
99
|
+
const network = resolveNetwork(this.getConfig().network);
|
|
67
100
|
const account = await this.getAccount(readOnly);
|
|
68
101
|
this._genlayerClient = createClient({
|
|
69
102
|
chain: network,
|
|
@@ -88,7 +121,7 @@ export class BaseAction extends ConfigFileManager {
|
|
|
88
121
|
keystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
|
|
89
122
|
|
|
90
123
|
if (!this.isValidKeystoreFormat(keystoreData)) {
|
|
91
|
-
this.failSpinner("Invalid keystore format. Expected encrypted keystore file.");
|
|
124
|
+
this.failSpinner("Invalid keystore format. Expected encrypted keystore file.", undefined, false);
|
|
92
125
|
await this.confirmPrompt("Would you like to create a new keypair?");
|
|
93
126
|
decryptedPrivateKey = await this.createKeypair(BaseAction.DEFAULT_KEYSTORE_PATH, true);
|
|
94
127
|
keypairPath = this.getConfigByKey("keyPairPath")!;
|
|
@@ -116,7 +149,6 @@ export class BaseAction extends ConfigFileManager {
|
|
|
116
149
|
|
|
117
150
|
if (existsSync(finalOutputPath) && !overwrite) {
|
|
118
151
|
this.failSpinner(`The file at ${finalOutputPath} already exists. Use the '--overwrite' option to replace it.`);
|
|
119
|
-
process.exit(1);
|
|
120
152
|
}
|
|
121
153
|
|
|
122
154
|
const wallet = ethers.Wallet.createRandom();
|
|
@@ -126,12 +158,10 @@ export class BaseAction extends ConfigFileManager {
|
|
|
126
158
|
|
|
127
159
|
if (password !== confirmPassword) {
|
|
128
160
|
this.failSpinner("Passwords do not match");
|
|
129
|
-
process.exit(1);
|
|
130
161
|
}
|
|
131
162
|
|
|
132
163
|
if (password.length < BaseAction.MIN_PASSWORD_LENGTH) {
|
|
133
164
|
this.failSpinner(`Password must be at least ${BaseAction.MIN_PASSWORD_LENGTH} characters long`);
|
|
134
|
-
process.exit(1);
|
|
135
165
|
}
|
|
136
166
|
|
|
137
167
|
const encryptedJson = await wallet.encrypt(password);
|
|
@@ -220,10 +250,13 @@ export class BaseAction extends ConfigFileManager {
|
|
|
220
250
|
this.spinner.succeed(chalk.green(message));
|
|
221
251
|
}
|
|
222
252
|
|
|
223
|
-
protected failSpinner(message: string, error?:any): void {
|
|
253
|
+
protected failSpinner(message: string, error?: any, shouldExit = true): void {
|
|
224
254
|
if (error) this.log("Error:", error);
|
|
225
|
-
console.log(
|
|
255
|
+
console.log("");
|
|
226
256
|
this.spinner.fail(chalk.red(message));
|
|
257
|
+
if (shouldExit) {
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
227
260
|
}
|
|
228
261
|
|
|
229
262
|
protected stopSpinner(): void {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {describe, test, vi, beforeEach, afterEach, expect} from "vitest";
|
|
2
|
+
import {createClient, createAccount} from "genlayer-js";
|
|
3
|
+
import {CodeAction} from "../../src/commands/contracts/code";
|
|
4
|
+
|
|
5
|
+
vi.mock("genlayer-js");
|
|
6
|
+
|
|
7
|
+
describe("CodeAction", () => {
|
|
8
|
+
let codeAction: CodeAction;
|
|
9
|
+
const mockClient = {
|
|
10
|
+
getContractCode: vi.fn(),
|
|
11
|
+
initializeConsensusSmartContract: vi.fn(),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const mockPrivateKey = "mocked_private_key";
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
vi.mocked(createClient).mockReturnValue(mockClient as any);
|
|
19
|
+
vi.mocked(createAccount).mockReturnValue({privateKey: mockPrivateKey} as any);
|
|
20
|
+
codeAction = new CodeAction();
|
|
21
|
+
vi.spyOn(codeAction as any, "getAccount").mockResolvedValue({privateKey: mockPrivateKey});
|
|
22
|
+
|
|
23
|
+
vi.spyOn(codeAction as any, "startSpinner").mockImplementation(() => {});
|
|
24
|
+
vi.spyOn(codeAction as any, "succeedSpinner").mockImplementation(() => {});
|
|
25
|
+
vi.spyOn(codeAction as any, "failSpinner").mockImplementation(() => {});
|
|
26
|
+
vi.spyOn(codeAction as any, "log").mockImplementation(() => {});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
vi.restoreAllMocks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("gets contract code successfully", async () => {
|
|
34
|
+
const mockResult = "0x600160...";
|
|
35
|
+
|
|
36
|
+
vi.mocked(mockClient.getContractCode).mockResolvedValue(mockResult as any);
|
|
37
|
+
|
|
38
|
+
await codeAction.code({
|
|
39
|
+
contractAddress: "0xMockedContract",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(mockClient.getContractCode).toHaveBeenCalledWith("0xMockedContract");
|
|
43
|
+
expect(codeAction["succeedSpinner"]).toHaveBeenCalledWith(
|
|
44
|
+
"Contract code retrieved successfully",
|
|
45
|
+
mockResult,
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("handles getContractCode errors", async () => {
|
|
50
|
+
vi.mocked(mockClient.getContractCode).mockRejectedValue(new Error("Mocked code error"));
|
|
51
|
+
|
|
52
|
+
await codeAction.code({contractAddress: "0xMockedContract"});
|
|
53
|
+
|
|
54
|
+
expect(codeAction["failSpinner"]).toHaveBeenCalledWith("Error retrieving contract code", expect.any(Error));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("uses custom RPC URL when provided", async () => {
|
|
58
|
+
const mockResult = "0x600160...";
|
|
59
|
+
vi.mocked(mockClient.getContractCode).mockResolvedValue(mockResult as any);
|
|
60
|
+
|
|
61
|
+
await codeAction.code({
|
|
62
|
+
contractAddress: "0xMockedContract",
|
|
63
|
+
rpc: "https://custom-rpc-url.com",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(createClient).toHaveBeenCalledWith(
|
|
67
|
+
expect.objectContaining({
|
|
68
|
+
endpoint: "https://custom-rpc-url.com",
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
expect(mockClient.getContractCode).toHaveBeenCalledWith("0xMockedContract");
|
|
72
|
+
expect(codeAction["succeedSpinner"]).toHaveBeenCalledWith(
|
|
73
|
+
"Contract code retrieved successfully",
|
|
74
|
+
mockResult,
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("initializes consensus smart contract", async () => {
|
|
79
|
+
vi.mocked(mockClient.getContractCode).mockResolvedValue("0x" as any);
|
|
80
|
+
|
|
81
|
+
await codeAction.code({contractAddress: "0xMockedContract"});
|
|
82
|
+
|
|
83
|
+
expect(mockClient.initializeConsensusSmartContract).toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import {describe, test, vi, beforeEach, afterEach, expect} from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {CreateAccountAction} from "../../src/commands/account/create";
|
|
3
3
|
|
|
4
|
-
describe("
|
|
5
|
-
let
|
|
4
|
+
describe("CreateAccountAction", () => {
|
|
5
|
+
let createAction: CreateAccountAction;
|
|
6
6
|
|
|
7
7
|
beforeEach(() => {
|
|
8
8
|
vi.clearAllMocks();
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
createAction = new CreateAccountAction();
|
|
10
|
+
|
|
11
11
|
// Mock the BaseAction methods
|
|
12
|
-
vi.spyOn(
|
|
13
|
-
vi.spyOn(
|
|
14
|
-
vi.spyOn(
|
|
15
|
-
vi.spyOn(
|
|
12
|
+
vi.spyOn(createAction as any, "startSpinner").mockImplementation(() => {});
|
|
13
|
+
vi.spyOn(createAction as any, "succeedSpinner").mockImplementation(() => {});
|
|
14
|
+
vi.spyOn(createAction as any, "failSpinner").mockImplementation(() => {});
|
|
15
|
+
vi.spyOn(createAction as any, "createKeypair").mockResolvedValue("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
afterEach(() => {
|
|
@@ -22,24 +22,24 @@ describe("KeypairCreator", () => {
|
|
|
22
22
|
test("successfully creates and saves an encrypted keystore", async () => {
|
|
23
23
|
const options = {output: "keypair.json", overwrite: false};
|
|
24
24
|
|
|
25
|
-
await
|
|
25
|
+
await createAction.execute(options);
|
|
26
26
|
|
|
27
|
-
expect(
|
|
28
|
-
expect(
|
|
29
|
-
options.output,
|
|
27
|
+
expect(createAction["startSpinner"]).toHaveBeenCalledWith("Creating encrypted keystore...");
|
|
28
|
+
expect(createAction["createKeypair"]).toHaveBeenCalledWith(
|
|
29
|
+
options.output,
|
|
30
30
|
options.overwrite
|
|
31
31
|
);
|
|
32
|
-
expect(
|
|
33
|
-
"
|
|
32
|
+
expect(createAction["succeedSpinner"]).toHaveBeenCalledWith(
|
|
33
|
+
"Account created and saved to: keypair.json",
|
|
34
34
|
);
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
test("handles errors during keystore creation", async () => {
|
|
38
38
|
const mockError = new Error("Mocked creation error");
|
|
39
|
-
vi.spyOn(
|
|
39
|
+
vi.spyOn(createAction as any, "createKeypair").mockRejectedValue(mockError);
|
|
40
40
|
|
|
41
|
-
await
|
|
41
|
+
await createAction.execute({output: "keypair.json", overwrite: true});
|
|
42
42
|
|
|
43
|
-
expect(
|
|
43
|
+
expect(createAction["failSpinner"]).toHaveBeenCalledWith("Failed to create account", mockError);
|
|
44
44
|
});
|
|
45
45
|
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {describe, test, vi, beforeEach, afterEach, expect} from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {LockAccountAction} from "../../src/commands/account/lock";
|
|
3
3
|
|
|
4
|
-
describe("
|
|
5
|
-
let lockAction:
|
|
4
|
+
describe("LockAccountAction", () => {
|
|
5
|
+
let lockAction: LockAccountAction;
|
|
6
6
|
|
|
7
7
|
beforeEach(() => {
|
|
8
8
|
vi.clearAllMocks();
|
|
9
|
-
lockAction = new
|
|
9
|
+
lockAction = new LockAccountAction();
|
|
10
10
|
|
|
11
11
|
// Mock the BaseAction methods
|
|
12
12
|
vi.spyOn(lockAction as any, "startSpinner").mockImplementation(() => {});
|
|
@@ -33,7 +33,7 @@ describe("LockAction", () => {
|
|
|
33
33
|
expect(lockAction["keychainManager"].getPrivateKey).toHaveBeenCalled();
|
|
34
34
|
expect(lockAction["setSpinnerText"]).toHaveBeenCalledWith("Removing private key from OS keychain...");
|
|
35
35
|
expect(lockAction["keychainManager"].removePrivateKey).toHaveBeenCalled();
|
|
36
|
-
expect(lockAction["succeedSpinner"]).toHaveBeenCalledWith("
|
|
36
|
+
expect(lockAction["succeedSpinner"]).toHaveBeenCalledWith("Account locked! Private key removed from OS keychain.");
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
test("fails when keychain is not available", async () => {
|
|
@@ -51,7 +51,7 @@ describe("LockAction", () => {
|
|
|
51
51
|
|
|
52
52
|
await lockAction.execute();
|
|
53
53
|
|
|
54
|
-
expect(lockAction["succeedSpinner"]).toHaveBeenCalledWith("
|
|
54
|
+
expect(lockAction["succeedSpinner"]).toHaveBeenCalledWith("Account is already locked.");
|
|
55
55
|
expect(lockAction["keychainManager"].removePrivateKey).not.toHaveBeenCalled();
|
|
56
56
|
});
|
|
57
57
|
|
|
@@ -61,6 +61,6 @@ describe("LockAction", () => {
|
|
|
61
61
|
|
|
62
62
|
await lockAction.execute();
|
|
63
63
|
|
|
64
|
-
expect(lockAction["failSpinner"]).toHaveBeenCalledWith("Failed to lock
|
|
64
|
+
expect(lockAction["failSpinner"]).toHaveBeenCalledWith("Failed to lock account.", mockError);
|
|
65
65
|
});
|
|
66
66
|
});
|
|
@@ -26,7 +26,7 @@ describe("NetworkActions", () => {
|
|
|
26
26
|
test("setNetwork method sets network by valid name", async () => {
|
|
27
27
|
await networkActions.setNetwork(localnet.name);
|
|
28
28
|
|
|
29
|
-
expect(networkActions["writeConfig"]).toHaveBeenCalledWith("network",
|
|
29
|
+
expect(networkActions["writeConfig"]).toHaveBeenCalledWith("network", "localnet");
|
|
30
30
|
expect(networkActions["succeedSpinner"]).toHaveBeenCalledWith(
|
|
31
31
|
`Network successfully set to ${localnet.name}`,
|
|
32
32
|
);
|
|
@@ -35,7 +35,7 @@ describe("NetworkActions", () => {
|
|
|
35
35
|
test("setNetwork method sets network by valid alias", async () => {
|
|
36
36
|
await networkActions.setNetwork("localnet");
|
|
37
37
|
|
|
38
|
-
expect(networkActions["writeConfig"]).toHaveBeenCalledWith("network",
|
|
38
|
+
expect(networkActions["writeConfig"]).toHaveBeenCalledWith("network", "localnet");
|
|
39
39
|
expect(networkActions["succeedSpinner"]).toHaveBeenCalledWith(
|
|
40
40
|
`Network successfully set to ${localnet.name}`,
|
|
41
41
|
);
|
|
@@ -44,7 +44,7 @@ describe("NetworkActions", () => {
|
|
|
44
44
|
test("setNetwork method sets studionet by name", async () => {
|
|
45
45
|
await networkActions.setNetwork(studionet.name);
|
|
46
46
|
|
|
47
|
-
expect(networkActions["writeConfig"]).toHaveBeenCalledWith("network",
|
|
47
|
+
expect(networkActions["writeConfig"]).toHaveBeenCalledWith("network", "studionet");
|
|
48
48
|
expect(networkActions["succeedSpinner"]).toHaveBeenCalledWith(
|
|
49
49
|
`Network successfully set to ${studionet.name}`,
|
|
50
50
|
);
|
|
@@ -53,7 +53,7 @@ describe("NetworkActions", () => {
|
|
|
53
53
|
test("setNetwork method sets studionet by alias", async () => {
|
|
54
54
|
await networkActions.setNetwork("studionet");
|
|
55
55
|
|
|
56
|
-
expect(networkActions["writeConfig"]).toHaveBeenCalledWith("network",
|
|
56
|
+
expect(networkActions["writeConfig"]).toHaveBeenCalledWith("network", "studionet");
|
|
57
57
|
expect(networkActions["succeedSpinner"]).toHaveBeenCalledWith(
|
|
58
58
|
`Network successfully set to ${studionet.name}`,
|
|
59
59
|
);
|
|
@@ -62,7 +62,7 @@ describe("NetworkActions", () => {
|
|
|
62
62
|
test("setNetwork method sets testnet-asimov by name", async () => {
|
|
63
63
|
await networkActions.setNetwork(testnetAsimov.name);
|
|
64
64
|
|
|
65
|
-
expect(networkActions["writeConfig"]).toHaveBeenCalledWith("network",
|
|
65
|
+
expect(networkActions["writeConfig"]).toHaveBeenCalledWith("network", "testnet-asimov");
|
|
66
66
|
expect(networkActions["succeedSpinner"]).toHaveBeenCalledWith(
|
|
67
67
|
`Network successfully set to ${testnetAsimov.name}`,
|
|
68
68
|
);
|
|
@@ -71,7 +71,7 @@ describe("NetworkActions", () => {
|
|
|
71
71
|
test("setNetwork method sets testnet-asimov by alias", async () => {
|
|
72
72
|
await networkActions.setNetwork("testnet-asimov");
|
|
73
73
|
|
|
74
|
-
expect(networkActions["writeConfig"]).toHaveBeenCalledWith("network",
|
|
74
|
+
expect(networkActions["writeConfig"]).toHaveBeenCalledWith("network", "testnet-asimov");
|
|
75
75
|
expect(networkActions["succeedSpinner"]).toHaveBeenCalledWith(
|
|
76
76
|
`Network successfully set to ${testnetAsimov.name}`,
|
|
77
77
|
);
|
|
@@ -94,14 +94,8 @@ describe("NetworkActions", () => {
|
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
test("setNetwork method prompts user when no network name provided", async () => {
|
|
97
|
-
const mockSelectedNetwork = {
|
|
98
|
-
name: localnet.name,
|
|
99
|
-
alias: "localnet",
|
|
100
|
-
value: localnet,
|
|
101
|
-
};
|
|
102
|
-
|
|
103
97
|
vi.mocked(inquirer.prompt).mockResolvedValue({
|
|
104
|
-
selectedNetwork:
|
|
98
|
+
selectedNetwork: "localnet",
|
|
105
99
|
});
|
|
106
100
|
|
|
107
101
|
await networkActions.setNetwork();
|
|
@@ -112,74 +106,41 @@ describe("NetworkActions", () => {
|
|
|
112
106
|
name: "selectedNetwork",
|
|
113
107
|
message: "Select which network do you want to use:",
|
|
114
108
|
choices: [
|
|
115
|
-
{
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
value: localnet,
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
name: studionet.name,
|
|
122
|
-
alias: "studionet",
|
|
123
|
-
value: studionet,
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
name: testnetAsimov.name,
|
|
127
|
-
alias: "testnet-asimov",
|
|
128
|
-
value: testnetAsimov,
|
|
129
|
-
},
|
|
109
|
+
{name: localnet.name, value: "localnet"},
|
|
110
|
+
{name: studionet.name, value: "studionet"},
|
|
111
|
+
{name: testnetAsimov.name, value: "testnet-asimov"},
|
|
130
112
|
],
|
|
131
113
|
},
|
|
132
114
|
]);
|
|
133
|
-
expect(networkActions["writeConfig"]).toHaveBeenCalledWith(
|
|
134
|
-
"network",
|
|
135
|
-
JSON.stringify(mockSelectedNetwork),
|
|
136
|
-
);
|
|
115
|
+
expect(networkActions["writeConfig"]).toHaveBeenCalledWith("network", "localnet");
|
|
137
116
|
expect(networkActions["succeedSpinner"]).toHaveBeenCalledWith(
|
|
138
|
-
`Network successfully set to ${
|
|
117
|
+
`Network successfully set to ${localnet.name}`,
|
|
139
118
|
);
|
|
140
119
|
});
|
|
141
120
|
|
|
142
121
|
test("setNetwork method handles interactive selection of studionet", async () => {
|
|
143
|
-
const mockSelectedNetwork = {
|
|
144
|
-
name: studionet.name,
|
|
145
|
-
alias: "studionet",
|
|
146
|
-
value: studionet,
|
|
147
|
-
};
|
|
148
|
-
|
|
149
122
|
vi.mocked(inquirer.prompt).mockResolvedValue({
|
|
150
|
-
selectedNetwork:
|
|
123
|
+
selectedNetwork: "studionet",
|
|
151
124
|
});
|
|
152
125
|
|
|
153
126
|
await networkActions.setNetwork();
|
|
154
127
|
|
|
155
|
-
expect(networkActions["writeConfig"]).toHaveBeenCalledWith(
|
|
156
|
-
"network",
|
|
157
|
-
JSON.stringify(mockSelectedNetwork),
|
|
158
|
-
);
|
|
128
|
+
expect(networkActions["writeConfig"]).toHaveBeenCalledWith("network", "studionet");
|
|
159
129
|
expect(networkActions["succeedSpinner"]).toHaveBeenCalledWith(
|
|
160
|
-
`Network successfully set to ${
|
|
130
|
+
`Network successfully set to ${studionet.name}`,
|
|
161
131
|
);
|
|
162
132
|
});
|
|
163
133
|
|
|
164
134
|
test("setNetwork method handles interactive selection of testnet-asimov", async () => {
|
|
165
|
-
const mockSelectedNetwork = {
|
|
166
|
-
name: testnetAsimov.name,
|
|
167
|
-
alias: "testnet-asimov",
|
|
168
|
-
value: testnetAsimov,
|
|
169
|
-
};
|
|
170
|
-
|
|
171
135
|
vi.mocked(inquirer.prompt).mockResolvedValue({
|
|
172
|
-
selectedNetwork:
|
|
136
|
+
selectedNetwork: "testnet-asimov",
|
|
173
137
|
});
|
|
174
138
|
|
|
175
139
|
await networkActions.setNetwork();
|
|
176
140
|
|
|
177
|
-
expect(networkActions["writeConfig"]).toHaveBeenCalledWith(
|
|
178
|
-
"network",
|
|
179
|
-
JSON.stringify(mockSelectedNetwork),
|
|
180
|
-
);
|
|
141
|
+
expect(networkActions["writeConfig"]).toHaveBeenCalledWith("network", "testnet-asimov");
|
|
181
142
|
expect(networkActions["succeedSpinner"]).toHaveBeenCalledWith(
|
|
182
|
-
`Network successfully set to ${
|
|
143
|
+
`Network successfully set to ${testnetAsimov.name}`,
|
|
183
144
|
);
|
|
184
145
|
});
|
|
185
146
|
|