genlayer 0.32.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.
- package/CHANGELOG.md +2 -0
- package/README.md +1 -1
- package/dist/index.js +1379 -221
- package/docs/delegator-guide.md +6 -6
- package/docs/validator-guide.md +49 -18
- package/package.json +2 -2
- package/src/commands/account/create.ts +10 -4
- package/src/commands/account/export.ts +106 -0
- package/src/commands/account/import.ts +85 -31
- package/src/commands/account/index.ts +77 -18
- package/src/commands/account/list.ts +34 -0
- package/src/commands/account/lock.ts +16 -7
- package/src/commands/account/remove.ts +30 -0
- package/src/commands/account/send.ts +14 -8
- package/src/commands/account/show.ts +22 -8
- package/src/commands/account/unlock.ts +20 -10
- package/src/commands/account/use.ts +21 -0
- package/src/commands/network/index.ts +18 -3
- package/src/commands/network/setNetwork.ts +38 -22
- package/src/commands/staking/StakingAction.ts +51 -19
- package/src/commands/staking/delegatorJoin.ts +2 -0
- package/src/commands/staking/index.ts +29 -2
- package/src/commands/staking/setIdentity.ts +5 -0
- package/src/commands/staking/stakingInfo.ts +29 -21
- package/src/commands/staking/wizard.ts +802 -0
- package/src/lib/actions/BaseAction.ts +71 -45
- package/src/lib/config/ConfigFileManager.ts +143 -0
- package/src/lib/config/KeychainManager.ts +23 -7
- package/tests/actions/create.test.ts +30 -10
- package/tests/actions/deploy.test.ts +7 -0
- package/tests/actions/lock.test.ts +28 -8
- package/tests/actions/unlock.test.ts +44 -26
- package/tests/commands/account.test.ts +43 -18
- package/tests/commands/network.test.ts +10 -10
- package/tests/commands/staking.test.ts +122 -0
- package/tests/libs/baseAction.test.ts +64 -41
- package/tests/libs/configFileManager.test.ts +8 -1
- package/tests/libs/keychainManager.test.ts +56 -16
- package/src/lib/interfaces/KeystoreData.ts +0 -5
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
import {StakingAction, StakingConfig, BUILT_IN_NETWORKS} from "./StakingAction";
|
|
2
|
+
import {CreateAccountAction} from "../account/create";
|
|
3
|
+
import {ExportAccountAction} from "../account/export";
|
|
4
|
+
import inquirer from "inquirer";
|
|
5
|
+
import type {Address} from "genlayer-js/types";
|
|
6
|
+
import {formatEther} from "viem";
|
|
7
|
+
import {createClient} from "genlayer-js";
|
|
8
|
+
import {readFileSync, existsSync} from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
|
|
11
|
+
export interface WizardOptions extends StakingConfig {
|
|
12
|
+
skipIdentity?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface WizardState {
|
|
16
|
+
accountName: string;
|
|
17
|
+
accountAddress: string;
|
|
18
|
+
networkAlias: string;
|
|
19
|
+
balance: bigint;
|
|
20
|
+
minStake: bigint;
|
|
21
|
+
operatorAddress?: string;
|
|
22
|
+
operatorAccountName?: string; // if operator is a CLI account
|
|
23
|
+
operatorKeystorePath?: string;
|
|
24
|
+
stakeAmount: string;
|
|
25
|
+
validatorWalletAddress?: string; // the validator contract address returned from validatorJoin
|
|
26
|
+
identity?: {
|
|
27
|
+
moniker: string;
|
|
28
|
+
logoUri?: string;
|
|
29
|
+
website?: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
email?: string;
|
|
32
|
+
twitter?: string;
|
|
33
|
+
telegram?: string;
|
|
34
|
+
github?: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Ensure address has 0x prefix
|
|
39
|
+
function ensureHexPrefix(address: string): string {
|
|
40
|
+
if (!address) return address;
|
|
41
|
+
return address.startsWith("0x") ? address : `0x${address}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class ValidatorWizardAction extends StakingAction {
|
|
45
|
+
constructor() {
|
|
46
|
+
super();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async execute(options: WizardOptions): Promise<void> {
|
|
50
|
+
console.log("\n========================================");
|
|
51
|
+
console.log(" GenLayer Validator Setup Wizard");
|
|
52
|
+
console.log("========================================\n");
|
|
53
|
+
|
|
54
|
+
const state: Partial<WizardState> = {};
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// Step 1: Account Setup
|
|
58
|
+
await this.stepAccountSetup(state, options);
|
|
59
|
+
|
|
60
|
+
// Step 2: Network Selection
|
|
61
|
+
await this.stepNetworkSelection(state, options);
|
|
62
|
+
|
|
63
|
+
// Step 3: Balance Check
|
|
64
|
+
await this.stepBalanceCheck(state, options);
|
|
65
|
+
|
|
66
|
+
// Step 4: Operator Setup
|
|
67
|
+
await this.stepOperatorSetup(state);
|
|
68
|
+
|
|
69
|
+
// Step 5: Stake Amount
|
|
70
|
+
await this.stepStakeAmount(state);
|
|
71
|
+
|
|
72
|
+
// Step 6: Join as Validator
|
|
73
|
+
await this.stepJoinValidator(state, options);
|
|
74
|
+
|
|
75
|
+
// Step 7: Identity Setup
|
|
76
|
+
if (!options.skipIdentity) {
|
|
77
|
+
await this.stepIdentitySetup(state, options);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Step 8: Summary
|
|
81
|
+
this.showSummary(state as WizardState);
|
|
82
|
+
} catch (error: any) {
|
|
83
|
+
if (error.message === "WIZARD_ABORTED") {
|
|
84
|
+
this.logError("Wizard aborted.");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.failSpinner("Wizard failed", error.message || error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private async stepAccountSetup(state: Partial<WizardState>, options: WizardOptions): Promise<void> {
|
|
92
|
+
console.log("Step 1: Account Setup");
|
|
93
|
+
console.log("---------------------\n");
|
|
94
|
+
|
|
95
|
+
// Check if account override provided
|
|
96
|
+
if (options.account) {
|
|
97
|
+
const keystorePath = this.getKeystorePath(options.account);
|
|
98
|
+
if (!this.accountExists(options.account)) {
|
|
99
|
+
this.failSpinner(`Account '${options.account}' not found.`);
|
|
100
|
+
}
|
|
101
|
+
state.accountName = options.account;
|
|
102
|
+
this.accountOverride = options.account;
|
|
103
|
+
const address = await this.getSignerAddress();
|
|
104
|
+
state.accountAddress = ensureHexPrefix(address);
|
|
105
|
+
console.log(`Using account: ${options.account} (${state.accountAddress})\n`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const accounts = this.listAccounts();
|
|
110
|
+
|
|
111
|
+
if (accounts.length === 0) {
|
|
112
|
+
// No accounts exist, create one
|
|
113
|
+
console.log("No accounts found. Let's create one.\n");
|
|
114
|
+
const {accountName} = await inquirer.prompt([
|
|
115
|
+
{
|
|
116
|
+
type: "input",
|
|
117
|
+
name: "accountName",
|
|
118
|
+
message: "Enter a name for your validator account:",
|
|
119
|
+
default: "validator",
|
|
120
|
+
validate: (input: string) => input.length > 0 || "Name cannot be empty",
|
|
121
|
+
},
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
const createAction = new CreateAccountAction();
|
|
125
|
+
await createAction.execute({name: accountName, overwrite: false, setActive: true});
|
|
126
|
+
|
|
127
|
+
state.accountName = accountName;
|
|
128
|
+
this.accountOverride = accountName;
|
|
129
|
+
const address = await this.getSignerAddress();
|
|
130
|
+
state.accountAddress = ensureHexPrefix(address);
|
|
131
|
+
} else {
|
|
132
|
+
// Accounts exist, choose or create
|
|
133
|
+
const choices = [
|
|
134
|
+
...accounts.map(a => ({
|
|
135
|
+
name: `${a.name} (${a.address})`,
|
|
136
|
+
value: a.name,
|
|
137
|
+
})),
|
|
138
|
+
{name: "Create new account", value: "__create_new__"},
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const {selectedAccount} = await inquirer.prompt([
|
|
142
|
+
{
|
|
143
|
+
type: "list",
|
|
144
|
+
name: "selectedAccount",
|
|
145
|
+
message: "Select an account that will be the owner of the validator:",
|
|
146
|
+
choices,
|
|
147
|
+
},
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
if (selectedAccount === "__create_new__") {
|
|
151
|
+
const {accountName} = await inquirer.prompt([
|
|
152
|
+
{
|
|
153
|
+
type: "input",
|
|
154
|
+
name: "accountName",
|
|
155
|
+
message: "Enter a name for your validator account:",
|
|
156
|
+
default: "validator",
|
|
157
|
+
validate: (input: string) => {
|
|
158
|
+
if (input.length === 0) return "Name cannot be empty";
|
|
159
|
+
if (accounts.find(a => a.name === input)) return "Account with this name already exists";
|
|
160
|
+
return true;
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
const createAction = new CreateAccountAction();
|
|
166
|
+
await createAction.execute({name: accountName, overwrite: false, setActive: true});
|
|
167
|
+
|
|
168
|
+
state.accountName = accountName;
|
|
169
|
+
this.accountOverride = accountName;
|
|
170
|
+
const address = await this.getSignerAddress();
|
|
171
|
+
state.accountAddress = ensureHexPrefix(address);
|
|
172
|
+
} else {
|
|
173
|
+
state.accountName = selectedAccount;
|
|
174
|
+
this.accountOverride = selectedAccount;
|
|
175
|
+
this.setActiveAccount(selectedAccount);
|
|
176
|
+
const address = await this.getSignerAddress();
|
|
177
|
+
state.accountAddress = ensureHexPrefix(address);
|
|
178
|
+
console.log(`\nUsing account: ${selectedAccount} (${state.accountAddress})`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log("");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private async stepNetworkSelection(state: Partial<WizardState>, options: WizardOptions): Promise<void> {
|
|
186
|
+
console.log("Step 2: Network Selection");
|
|
187
|
+
console.log("-------------------------\n");
|
|
188
|
+
|
|
189
|
+
if (options.network) {
|
|
190
|
+
const network = BUILT_IN_NETWORKS[options.network];
|
|
191
|
+
if (!network) {
|
|
192
|
+
this.failSpinner(`Unknown network: ${options.network}`);
|
|
193
|
+
}
|
|
194
|
+
state.networkAlias = options.network;
|
|
195
|
+
this.writeConfig("network", options.network);
|
|
196
|
+
console.log(`Using network: ${network.name}\n`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const currentNetwork = this.getConfigByKey("network");
|
|
201
|
+
// Exclude studionet - not compatible with staking
|
|
202
|
+
const excludedNetworks = ["studionet"];
|
|
203
|
+
const networks = Object.entries(BUILT_IN_NETWORKS)
|
|
204
|
+
.filter(([alias]) => !excludedNetworks.includes(alias))
|
|
205
|
+
.map(([alias, chain]) => ({
|
|
206
|
+
name: chain.name,
|
|
207
|
+
value: alias,
|
|
208
|
+
}));
|
|
209
|
+
|
|
210
|
+
const {selectedNetwork} = await inquirer.prompt([
|
|
211
|
+
{
|
|
212
|
+
type: "list",
|
|
213
|
+
name: "selectedNetwork",
|
|
214
|
+
message: "Select network:",
|
|
215
|
+
choices: networks,
|
|
216
|
+
default: currentNetwork || "testnet-asimov",
|
|
217
|
+
},
|
|
218
|
+
]);
|
|
219
|
+
|
|
220
|
+
state.networkAlias = selectedNetwork;
|
|
221
|
+
this.writeConfig("network", selectedNetwork);
|
|
222
|
+
console.log(`\nNetwork set to: ${BUILT_IN_NETWORKS[selectedNetwork].name}\n`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private async stepBalanceCheck(state: Partial<WizardState>, options: WizardOptions): Promise<void> {
|
|
226
|
+
console.log("Step 3: Balance Check");
|
|
227
|
+
console.log("---------------------\n");
|
|
228
|
+
|
|
229
|
+
this.startSpinner("Checking balance and staking requirements...");
|
|
230
|
+
|
|
231
|
+
const network = BUILT_IN_NETWORKS[state.networkAlias!];
|
|
232
|
+
const client = createClient({
|
|
233
|
+
chain: network,
|
|
234
|
+
account: state.accountAddress as Address,
|
|
235
|
+
endpoint: options.rpc,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const [balance, epochInfo] = await Promise.all([
|
|
239
|
+
client.getBalance({address: state.accountAddress as Address}),
|
|
240
|
+
client.getEpochInfo(),
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
this.stopSpinner();
|
|
244
|
+
|
|
245
|
+
const balanceFormatted = formatEther(balance);
|
|
246
|
+
const minStakeRaw = epochInfo.validatorMinStakeRaw;
|
|
247
|
+
const minStakeFormatted = epochInfo.validatorMinStake;
|
|
248
|
+
const currentEpoch = epochInfo.currentEpoch;
|
|
249
|
+
|
|
250
|
+
console.log(`Balance: ${balanceFormatted} GEN`);
|
|
251
|
+
console.log(`Minimum stake required: ${minStakeFormatted}`);
|
|
252
|
+
if (currentEpoch === 0n) {
|
|
253
|
+
console.log("(Epoch 0: minimum stake not enforced)");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Epoch 0 doesn't enforce min stake, so only check for non-zero epochs
|
|
257
|
+
if (currentEpoch !== 0n && balance < minStakeRaw) {
|
|
258
|
+
console.log("");
|
|
259
|
+
this.failSpinner(
|
|
260
|
+
`Insufficient balance. You need at least ${minStakeFormatted} to become a validator.\n` +
|
|
261
|
+
`Fund your account (${state.accountAddress}) and run the wizard again.`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
state.balance = balance;
|
|
266
|
+
state.minStake = currentEpoch === 0n ? 0n : minStakeRaw;
|
|
267
|
+
|
|
268
|
+
console.log("Balance sufficient!\n");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private async stepOperatorSetup(state: Partial<WizardState>): Promise<void> {
|
|
272
|
+
console.log("Step 4: Operator Setup");
|
|
273
|
+
console.log("----------------------\n");
|
|
274
|
+
|
|
275
|
+
console.log("Using a separate operator address is recommended for security:");
|
|
276
|
+
console.log("- Owner account: holds staked funds (keep secure)");
|
|
277
|
+
console.log("- Operator account: signs blocks (hot wallet on validator server)\n");
|
|
278
|
+
|
|
279
|
+
const {useOperator} = await inquirer.prompt([
|
|
280
|
+
{
|
|
281
|
+
type: "confirm",
|
|
282
|
+
name: "useOperator",
|
|
283
|
+
message: "Do you want to use a separate operator address?",
|
|
284
|
+
default: true,
|
|
285
|
+
},
|
|
286
|
+
]);
|
|
287
|
+
|
|
288
|
+
if (!useOperator) {
|
|
289
|
+
state.operatorAddress = ensureHexPrefix(state.accountAddress);
|
|
290
|
+
state.operatorAccountName = state.accountName;
|
|
291
|
+
console.log("\nOperator will be the same as owner address.\n");
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const accounts = this.listAccounts();
|
|
296
|
+
const otherAccounts = accounts.filter(a => a.name !== state.accountName);
|
|
297
|
+
|
|
298
|
+
const choices = [
|
|
299
|
+
{name: "Create new operator account", value: "create"},
|
|
300
|
+
...(otherAccounts.length > 0 ? [{name: "Select from my accounts", value: "select"}] : []),
|
|
301
|
+
{name: "Enter existing operator address", value: "existing"},
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
const {operatorChoice} = await inquirer.prompt([
|
|
305
|
+
{
|
|
306
|
+
type: "list",
|
|
307
|
+
name: "operatorChoice",
|
|
308
|
+
message: "How would you like to set up the operator?",
|
|
309
|
+
choices,
|
|
310
|
+
},
|
|
311
|
+
]);
|
|
312
|
+
|
|
313
|
+
if (operatorChoice === "existing") {
|
|
314
|
+
const {operatorAddress} = await inquirer.prompt([
|
|
315
|
+
{
|
|
316
|
+
type: "input",
|
|
317
|
+
name: "operatorAddress",
|
|
318
|
+
message: "Enter operator address (0x...):",
|
|
319
|
+
validate: (input: string) => {
|
|
320
|
+
if (!input.match(/^0x[a-fA-F0-9]{40}$/)) {
|
|
321
|
+
return "Invalid address format. Expected 0x followed by 40 hex characters.";
|
|
322
|
+
}
|
|
323
|
+
return true;
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
]);
|
|
327
|
+
state.operatorAddress = ensureHexPrefix(operatorAddress);
|
|
328
|
+
// No operatorAccountName - external address
|
|
329
|
+
console.log("");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (operatorChoice === "select") {
|
|
334
|
+
const {selectedOperator} = await inquirer.prompt([
|
|
335
|
+
{
|
|
336
|
+
type: "list",
|
|
337
|
+
name: "selectedOperator",
|
|
338
|
+
message: "Select an account to use as operator:",
|
|
339
|
+
choices: otherAccounts.map(a => ({
|
|
340
|
+
name: `${a.name} (${a.address})`,
|
|
341
|
+
value: a.name,
|
|
342
|
+
})),
|
|
343
|
+
},
|
|
344
|
+
]);
|
|
345
|
+
|
|
346
|
+
const operatorKeystorePath = this.getKeystorePath(selectedOperator);
|
|
347
|
+
const operatorData = JSON.parse(readFileSync(operatorKeystorePath, "utf-8"));
|
|
348
|
+
state.operatorAddress = ensureHexPrefix(operatorData.address);
|
|
349
|
+
state.operatorAccountName = selectedOperator;
|
|
350
|
+
|
|
351
|
+
// Export the selected operator keystore
|
|
352
|
+
const defaultFilename = `${selectedOperator}-keystore.json`;
|
|
353
|
+
const {outputFilename} = await inquirer.prompt([
|
|
354
|
+
{
|
|
355
|
+
type: "input",
|
|
356
|
+
name: "outputFilename",
|
|
357
|
+
message: "Export keystore filename:",
|
|
358
|
+
default: defaultFilename,
|
|
359
|
+
},
|
|
360
|
+
]);
|
|
361
|
+
|
|
362
|
+
let outputPath = path.resolve(`./${outputFilename}`);
|
|
363
|
+
|
|
364
|
+
// Check if file exists and ask to overwrite
|
|
365
|
+
if (existsSync(outputPath)) {
|
|
366
|
+
const {overwrite} = await inquirer.prompt([
|
|
367
|
+
{
|
|
368
|
+
type: "confirm",
|
|
369
|
+
name: "overwrite",
|
|
370
|
+
message: `File ${outputFilename} already exists. Overwrite?`,
|
|
371
|
+
default: false,
|
|
372
|
+
},
|
|
373
|
+
]);
|
|
374
|
+
if (!overwrite) {
|
|
375
|
+
const {newFilename} = await inquirer.prompt([
|
|
376
|
+
{
|
|
377
|
+
type: "input",
|
|
378
|
+
name: "newFilename",
|
|
379
|
+
message: "Enter new filename:",
|
|
380
|
+
},
|
|
381
|
+
]);
|
|
382
|
+
outputPath = path.resolve(`./${newFilename}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const {exportPassword} = await inquirer.prompt([
|
|
387
|
+
{
|
|
388
|
+
type: "password",
|
|
389
|
+
name: "exportPassword",
|
|
390
|
+
message: "Enter password for exported keystore (needed to import in node):",
|
|
391
|
+
mask: "*",
|
|
392
|
+
validate: (input: string) => input.length >= 8 || "Password must be at least 8 characters",
|
|
393
|
+
},
|
|
394
|
+
]);
|
|
395
|
+
|
|
396
|
+
const {confirmPassword} = await inquirer.prompt([
|
|
397
|
+
{
|
|
398
|
+
type: "password",
|
|
399
|
+
name: "confirmPassword",
|
|
400
|
+
message: "Confirm password:",
|
|
401
|
+
mask: "*",
|
|
402
|
+
},
|
|
403
|
+
]);
|
|
404
|
+
|
|
405
|
+
if (exportPassword !== confirmPassword) {
|
|
406
|
+
this.failSpinner("Passwords do not match");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const exportAction = new ExportAccountAction();
|
|
410
|
+
await exportAction.execute({
|
|
411
|
+
account: selectedOperator,
|
|
412
|
+
output: outputPath,
|
|
413
|
+
password: exportPassword,
|
|
414
|
+
overwrite: true,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
state.operatorKeystorePath = outputPath;
|
|
418
|
+
|
|
419
|
+
console.log("\n========================================");
|
|
420
|
+
console.log(" IMPORTANT: Transfer operator keystore");
|
|
421
|
+
console.log("========================================");
|
|
422
|
+
console.log(`File: ${outputPath}`);
|
|
423
|
+
console.log("Transfer this file to your validator server and import it");
|
|
424
|
+
console.log("into your validator node software.");
|
|
425
|
+
console.log("========================================\n");
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Create new operator account
|
|
430
|
+
const {operatorName} = await inquirer.prompt([
|
|
431
|
+
{
|
|
432
|
+
type: "input",
|
|
433
|
+
name: "operatorName",
|
|
434
|
+
message: "Enter a name for the operator account:",
|
|
435
|
+
default: "operator",
|
|
436
|
+
validate: (input: string) => {
|
|
437
|
+
if (input.length === 0) return "Name cannot be empty";
|
|
438
|
+
if (accounts.find(a => a.name === input)) return "Account with this name already exists";
|
|
439
|
+
return true;
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
]);
|
|
443
|
+
|
|
444
|
+
// Create the operator account
|
|
445
|
+
console.log("");
|
|
446
|
+
const createAction = new CreateAccountAction();
|
|
447
|
+
await createAction.execute({name: operatorName, overwrite: false, setActive: false});
|
|
448
|
+
|
|
449
|
+
// Get operator address
|
|
450
|
+
const operatorKeystorePath = this.getKeystorePath(operatorName);
|
|
451
|
+
const operatorData = JSON.parse(readFileSync(operatorKeystorePath, "utf-8"));
|
|
452
|
+
state.operatorAddress = ensureHexPrefix(operatorData.address);
|
|
453
|
+
state.operatorAccountName = operatorName;
|
|
454
|
+
|
|
455
|
+
// Export keystore
|
|
456
|
+
const defaultFilename = `${operatorName}-keystore.json`;
|
|
457
|
+
const {outputFilename} = await inquirer.prompt([
|
|
458
|
+
{
|
|
459
|
+
type: "input",
|
|
460
|
+
name: "outputFilename",
|
|
461
|
+
message: "Export keystore filename:",
|
|
462
|
+
default: defaultFilename,
|
|
463
|
+
},
|
|
464
|
+
]);
|
|
465
|
+
|
|
466
|
+
let outputPath = path.resolve(`./${outputFilename}`);
|
|
467
|
+
|
|
468
|
+
// Check if file exists and ask to overwrite
|
|
469
|
+
if (existsSync(outputPath)) {
|
|
470
|
+
const {overwrite} = await inquirer.prompt([
|
|
471
|
+
{
|
|
472
|
+
type: "confirm",
|
|
473
|
+
name: "overwrite",
|
|
474
|
+
message: `File ${outputFilename} already exists. Overwrite?`,
|
|
475
|
+
default: false,
|
|
476
|
+
},
|
|
477
|
+
]);
|
|
478
|
+
if (!overwrite) {
|
|
479
|
+
const {newFilename} = await inquirer.prompt([
|
|
480
|
+
{
|
|
481
|
+
type: "input",
|
|
482
|
+
name: "newFilename",
|
|
483
|
+
message: "Enter new filename:",
|
|
484
|
+
},
|
|
485
|
+
]);
|
|
486
|
+
outputPath = path.resolve(`./${newFilename}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const {exportPassword} = await inquirer.prompt([
|
|
491
|
+
{
|
|
492
|
+
type: "password",
|
|
493
|
+
name: "exportPassword",
|
|
494
|
+
message: "Enter password for exported keystore (needed to import in node):",
|
|
495
|
+
mask: "*",
|
|
496
|
+
validate: (input: string) => input.length >= 8 || "Password must be at least 8 characters",
|
|
497
|
+
},
|
|
498
|
+
]);
|
|
499
|
+
|
|
500
|
+
const {confirmPassword} = await inquirer.prompt([
|
|
501
|
+
{
|
|
502
|
+
type: "password",
|
|
503
|
+
name: "confirmPassword",
|
|
504
|
+
message: "Confirm password:",
|
|
505
|
+
mask: "*",
|
|
506
|
+
},
|
|
507
|
+
]);
|
|
508
|
+
|
|
509
|
+
if (exportPassword !== confirmPassword) {
|
|
510
|
+
this.failSpinner("Passwords do not match");
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const exportAction = new ExportAccountAction();
|
|
514
|
+
await exportAction.execute({
|
|
515
|
+
account: operatorName,
|
|
516
|
+
output: outputPath,
|
|
517
|
+
password: exportPassword,
|
|
518
|
+
overwrite: true,
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
state.operatorKeystorePath = outputPath;
|
|
522
|
+
|
|
523
|
+
console.log("\n========================================");
|
|
524
|
+
console.log(" IMPORTANT: Transfer operator keystore");
|
|
525
|
+
console.log("========================================");
|
|
526
|
+
console.log(`File: ${outputPath}`);
|
|
527
|
+
console.log("Transfer this file to your validator server and import it");
|
|
528
|
+
console.log("into your validator node software.");
|
|
529
|
+
console.log("========================================\n");
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private async stepStakeAmount(state: Partial<WizardState>): Promise<void> {
|
|
533
|
+
console.log("Step 5: Stake Amount");
|
|
534
|
+
console.log("--------------------\n");
|
|
535
|
+
|
|
536
|
+
const balanceGEN = formatEther(state.balance!);
|
|
537
|
+
const minStakeGEN = formatEther(state.minStake!);
|
|
538
|
+
const hasMinStake = state.minStake! > 0n;
|
|
539
|
+
|
|
540
|
+
const {stakeAmount} = await inquirer.prompt([
|
|
541
|
+
{
|
|
542
|
+
type: "input",
|
|
543
|
+
name: "stakeAmount",
|
|
544
|
+
message: hasMinStake
|
|
545
|
+
? `Enter stake amount (min: ${minStakeGEN}, max: ${balanceGEN} GEN):`
|
|
546
|
+
: `Enter stake amount (max: ${balanceGEN} GEN):`,
|
|
547
|
+
default: hasMinStake ? minStakeGEN : "1",
|
|
548
|
+
validate: (input: string) => {
|
|
549
|
+
const cleaned = input.toLowerCase().replace("gen", "").trim();
|
|
550
|
+
const num = parseFloat(cleaned);
|
|
551
|
+
if (isNaN(num) || num <= 0) {
|
|
552
|
+
return "Please enter a valid positive number";
|
|
553
|
+
}
|
|
554
|
+
const amountWei = BigInt(Math.floor(num * 1e18));
|
|
555
|
+
if (hasMinStake && amountWei < state.minStake!) {
|
|
556
|
+
return `Amount must be at least ${minStakeGEN} GEN`;
|
|
557
|
+
}
|
|
558
|
+
if (amountWei > state.balance!) {
|
|
559
|
+
return `Amount exceeds balance (${balanceGEN} GEN)`;
|
|
560
|
+
}
|
|
561
|
+
return true;
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
]);
|
|
565
|
+
|
|
566
|
+
// Normalize amount to always have "gen" suffix
|
|
567
|
+
const normalizedAmount = stakeAmount.toLowerCase().endsWith("gen") ? stakeAmount : `${stakeAmount}gen`;
|
|
568
|
+
state.stakeAmount = normalizedAmount;
|
|
569
|
+
|
|
570
|
+
const {confirm} = await inquirer.prompt([
|
|
571
|
+
{
|
|
572
|
+
type: "confirm",
|
|
573
|
+
name: "confirm",
|
|
574
|
+
message: `You will stake ${stakeAmount}. Continue?`,
|
|
575
|
+
default: true,
|
|
576
|
+
},
|
|
577
|
+
]);
|
|
578
|
+
|
|
579
|
+
if (!confirm) {
|
|
580
|
+
throw new Error("WIZARD_ABORTED");
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
console.log("");
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private async stepJoinValidator(state: Partial<WizardState>, options: WizardOptions): Promise<void> {
|
|
587
|
+
console.log("Step 6: Join as Validator");
|
|
588
|
+
console.log("-------------------------\n");
|
|
589
|
+
|
|
590
|
+
this.startSpinner("Creating validator...");
|
|
591
|
+
|
|
592
|
+
try {
|
|
593
|
+
const client = await this.getStakingClient({
|
|
594
|
+
...options,
|
|
595
|
+
account: state.accountName,
|
|
596
|
+
network: state.networkAlias,
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
const amount = this.parseAmount(state.stakeAmount!);
|
|
600
|
+
|
|
601
|
+
this.setSpinnerText(`Creating validator with ${this.formatAmount(amount)} stake...`);
|
|
602
|
+
|
|
603
|
+
const result = await client.validatorJoin({
|
|
604
|
+
amount,
|
|
605
|
+
operator: state.operatorAddress as Address,
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// Save the validator wallet address
|
|
609
|
+
state.validatorWalletAddress = ensureHexPrefix(result.validatorWallet);
|
|
610
|
+
|
|
611
|
+
this.succeedSpinner("Validator created successfully!", {
|
|
612
|
+
transactionHash: result.transactionHash,
|
|
613
|
+
validatorWallet: state.validatorWalletAddress,
|
|
614
|
+
amount: result.amount,
|
|
615
|
+
operator: result.operator,
|
|
616
|
+
blockNumber: result.blockNumber.toString(),
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
console.log("");
|
|
620
|
+
} catch (error: any) {
|
|
621
|
+
this.failSpinner("Failed to create validator", error.message || error);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
private async stepIdentitySetup(state: Partial<WizardState>, options: WizardOptions): Promise<void> {
|
|
626
|
+
console.log("Step 7: Identity Setup");
|
|
627
|
+
console.log("----------------------\n");
|
|
628
|
+
|
|
629
|
+
const {setupIdentity} = await inquirer.prompt([
|
|
630
|
+
{
|
|
631
|
+
type: "confirm",
|
|
632
|
+
name: "setupIdentity",
|
|
633
|
+
message: "Would you like to set up your validator identity now?",
|
|
634
|
+
default: true,
|
|
635
|
+
},
|
|
636
|
+
]);
|
|
637
|
+
|
|
638
|
+
if (!setupIdentity) {
|
|
639
|
+
console.log("\nYou can set up identity later with: genlayer staking set-identity\n");
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Collect all identity fields
|
|
644
|
+
const {moniker} = await inquirer.prompt([
|
|
645
|
+
{
|
|
646
|
+
type: "input",
|
|
647
|
+
name: "moniker",
|
|
648
|
+
message: "Enter validator display name (moniker):",
|
|
649
|
+
validate: (input: string) => input.length > 0 || "Moniker is required",
|
|
650
|
+
},
|
|
651
|
+
]);
|
|
652
|
+
|
|
653
|
+
const {logoUri} = await inquirer.prompt([
|
|
654
|
+
{
|
|
655
|
+
type: "input",
|
|
656
|
+
name: "logoUri",
|
|
657
|
+
message: "Enter logo URL (optional):",
|
|
658
|
+
},
|
|
659
|
+
]);
|
|
660
|
+
|
|
661
|
+
const {website} = await inquirer.prompt([
|
|
662
|
+
{
|
|
663
|
+
type: "input",
|
|
664
|
+
name: "website",
|
|
665
|
+
message: "Enter website URL (optional):",
|
|
666
|
+
},
|
|
667
|
+
]);
|
|
668
|
+
|
|
669
|
+
const {description} = await inquirer.prompt([
|
|
670
|
+
{
|
|
671
|
+
type: "input",
|
|
672
|
+
name: "description",
|
|
673
|
+
message: "Enter description (optional):",
|
|
674
|
+
},
|
|
675
|
+
]);
|
|
676
|
+
|
|
677
|
+
const {email} = await inquirer.prompt([
|
|
678
|
+
{
|
|
679
|
+
type: "input",
|
|
680
|
+
name: "email",
|
|
681
|
+
message: "Enter contact email (optional):",
|
|
682
|
+
},
|
|
683
|
+
]);
|
|
684
|
+
|
|
685
|
+
const {twitter} = await inquirer.prompt([
|
|
686
|
+
{
|
|
687
|
+
type: "input",
|
|
688
|
+
name: "twitter",
|
|
689
|
+
message: "Enter Twitter handle (optional):",
|
|
690
|
+
},
|
|
691
|
+
]);
|
|
692
|
+
|
|
693
|
+
const {telegram} = await inquirer.prompt([
|
|
694
|
+
{
|
|
695
|
+
type: "input",
|
|
696
|
+
name: "telegram",
|
|
697
|
+
message: "Enter Telegram handle (optional):",
|
|
698
|
+
},
|
|
699
|
+
]);
|
|
700
|
+
|
|
701
|
+
const {github} = await inquirer.prompt([
|
|
702
|
+
{
|
|
703
|
+
type: "input",
|
|
704
|
+
name: "github",
|
|
705
|
+
message: "Enter GitHub handle (optional):",
|
|
706
|
+
},
|
|
707
|
+
]);
|
|
708
|
+
|
|
709
|
+
state.identity = {
|
|
710
|
+
moniker,
|
|
711
|
+
logoUri: logoUri || undefined,
|
|
712
|
+
website: website || undefined,
|
|
713
|
+
description: description || undefined,
|
|
714
|
+
email: email || undefined,
|
|
715
|
+
twitter: twitter || undefined,
|
|
716
|
+
telegram: telegram || undefined,
|
|
717
|
+
github: github || undefined,
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
this.startSpinner("Setting validator identity...");
|
|
721
|
+
|
|
722
|
+
try {
|
|
723
|
+
const client = await this.getStakingClient({
|
|
724
|
+
...options,
|
|
725
|
+
account: state.accountName,
|
|
726
|
+
network: state.networkAlias,
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
// Use the validator wallet address (contract), not owner address
|
|
730
|
+
const validatorAddress = state.validatorWalletAddress || state.accountAddress;
|
|
731
|
+
|
|
732
|
+
await client.setIdentity({
|
|
733
|
+
validator: ensureHexPrefix(validatorAddress) as Address,
|
|
734
|
+
moniker,
|
|
735
|
+
logoUri: logoUri || undefined,
|
|
736
|
+
website: website || undefined,
|
|
737
|
+
description: description || undefined,
|
|
738
|
+
email: email || undefined,
|
|
739
|
+
twitter: twitter || undefined,
|
|
740
|
+
telegram: telegram || undefined,
|
|
741
|
+
github: github || undefined,
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
this.succeedSpinner("Validator identity set!");
|
|
745
|
+
console.log("");
|
|
746
|
+
} catch (error: any) {
|
|
747
|
+
this.stopSpinner();
|
|
748
|
+
this.logWarning(`Failed to set identity: ${error.message || error}`);
|
|
749
|
+
console.log("You can try again later with: genlayer staking set-identity\n");
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
private showSummary(state: WizardState): void {
|
|
754
|
+
console.log("\n========================================");
|
|
755
|
+
console.log(" Validator Setup Complete!");
|
|
756
|
+
console.log("========================================\n");
|
|
757
|
+
|
|
758
|
+
// Ensure all addresses have 0x prefix
|
|
759
|
+
const validatorWallet = ensureHexPrefix(state.validatorWalletAddress || state.accountAddress);
|
|
760
|
+
const ownerAddress = ensureHexPrefix(state.accountAddress);
|
|
761
|
+
const operatorAddress = ensureHexPrefix(state.operatorAddress || "");
|
|
762
|
+
|
|
763
|
+
console.log("Summary:");
|
|
764
|
+
// Validator wallet address first - most important
|
|
765
|
+
console.log(` Validator Wallet: ${validatorWallet}`);
|
|
766
|
+
console.log(` Owner: ${ownerAddress} (${state.accountName})`);
|
|
767
|
+
|
|
768
|
+
// Operator - show account name if it's a CLI account
|
|
769
|
+
if (state.operatorAccountName) {
|
|
770
|
+
console.log(` Operator: ${operatorAddress} (${state.operatorAccountName})`);
|
|
771
|
+
} else {
|
|
772
|
+
console.log(` Operator: ${operatorAddress}`);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
console.log(` Staked Amount: ${state.stakeAmount}`);
|
|
776
|
+
console.log(` Network: ${BUILT_IN_NETWORKS[state.networkAlias].name}`);
|
|
777
|
+
|
|
778
|
+
if (state.identity) {
|
|
779
|
+
console.log(` Identity:`);
|
|
780
|
+
console.log(` Moniker: ${state.identity.moniker}`);
|
|
781
|
+
if (state.identity.logoUri) console.log(` Logo: ${state.identity.logoUri}`);
|
|
782
|
+
if (state.identity.website) console.log(` Website: ${state.identity.website}`);
|
|
783
|
+
if (state.identity.description) console.log(` Description: ${state.identity.description}`);
|
|
784
|
+
if (state.identity.email) console.log(` Email: ${state.identity.email}`);
|
|
785
|
+
if (state.identity.twitter) console.log(` Twitter: ${state.identity.twitter}`);
|
|
786
|
+
if (state.identity.telegram) console.log(` Telegram: ${state.identity.telegram}`);
|
|
787
|
+
if (state.identity.github) console.log(` GitHub: ${state.identity.github}`);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
console.log("\nNext Steps:");
|
|
791
|
+
let step = 1;
|
|
792
|
+
if (state.operatorKeystorePath) {
|
|
793
|
+
console.log(` ${step++}. Transfer operator keystore to your validator server:`);
|
|
794
|
+
console.log(` ${state.operatorKeystorePath}`);
|
|
795
|
+
console.log(` ${step++}. Import it into your validator node software`);
|
|
796
|
+
}
|
|
797
|
+
console.log(` ${step++}. Monitor your validator:`);
|
|
798
|
+
console.log(` genlayer staking validator-info --validator ${validatorWallet}`);
|
|
799
|
+
console.log(` ${step++}. Lock your account when done: genlayer account lock`);
|
|
800
|
+
console.log("\n========================================\n");
|
|
801
|
+
}
|
|
802
|
+
}
|