genlayer 0.32.8 → 0.33.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 +12 -0
- package/README.md +10 -2
- package/dist/index.js +125 -25
- package/docs/validator-guide.md +27 -0
- package/package.json +2 -2
- package/src/commands/staking/StakingAction.ts +73 -2
- package/src/commands/staking/index.ts +13 -0
- package/src/commands/staking/stakingInfo.ts +36 -18
- package/src/commands/staking/validatorPrime.ts +38 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.33.1 (2026-02-07)
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
* bump genlayer-js to 0.18.10 with phase4 testnet addresses ([#274](https://github.com/yeagerai/genlayer-cli/issues/274)) ([faa9a9b](https://github.com/yeagerai/genlayer-cli/commit/faa9a9bfaadf3ff158e2b21457330bc629d977e9))
|
|
8
|
+
|
|
9
|
+
## 0.33.0 (2026-01-13)
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* validator tree traversal, primed column, prime-all command ([#270](https://github.com/yeagerai/genlayer-cli/issues/270)) ([75b9b06](https://github.com/yeagerai/genlayer-cli/commit/75b9b06a3bb80a24e7dabf422ceed44620765ccb))
|
|
14
|
+
|
|
3
15
|
## 0.32.8 (2025-12-12)
|
|
4
16
|
|
|
5
17
|
## 0.32.5 (2025-12-09)
|
package/README.md
CHANGED
|
@@ -306,14 +306,16 @@ COMMANDS:
|
|
|
306
306
|
validator-deposit [options] Make an additional deposit as a validator
|
|
307
307
|
validator-exit [options] Exit as a validator by withdrawing shares
|
|
308
308
|
validator-claim [options] Claim validator withdrawals after unbonding period
|
|
309
|
+
validator-prime [validator] Prime a validator for the next epoch
|
|
310
|
+
prime-all [options] Prime all validators that need priming
|
|
309
311
|
delegator-join [options] Join as a delegator by staking with a validator
|
|
310
312
|
delegator-exit [options] Exit as a delegator by withdrawing shares
|
|
311
313
|
delegator-claim [options] Claim delegator withdrawals after unbonding period
|
|
312
|
-
validator-info [validator] Get information about a validator
|
|
314
|
+
validator-info [validator] Get information about a validator (--debug for raw data)
|
|
313
315
|
validator-history [validator] Show slash and reward history for a validator
|
|
314
316
|
delegation-info [validator] Get delegation info for a delegator with a validator
|
|
315
317
|
epoch-info [options] Get current/previous epoch info (--epoch <n> for specific)
|
|
316
|
-
validators [options] Show validator set with stake, status, and weight
|
|
318
|
+
validators [options] Show validator set with stake, primed status, and weight
|
|
317
319
|
active-validators [options] List all active validators
|
|
318
320
|
quarantined-validators List all quarantined validators
|
|
319
321
|
banned-validators List all banned validators
|
|
@@ -416,6 +418,12 @@ EXAMPLES:
|
|
|
416
418
|
# Exit and claim (requires validator wallet address)
|
|
417
419
|
genlayer staking validator-exit --validator 0x... --shares 100
|
|
418
420
|
genlayer staking validator-claim --validator 0x...
|
|
421
|
+
|
|
422
|
+
# Prime a validator for next epoch
|
|
423
|
+
genlayer staking validator-prime 0x...
|
|
424
|
+
|
|
425
|
+
# Prime all validators that need priming (anyone can call)
|
|
426
|
+
genlayer staking prime-all
|
|
419
427
|
```
|
|
420
428
|
|
|
421
429
|
### Running the CLI from the repository
|
package/dist/index.js
CHANGED
|
@@ -20078,7 +20078,7 @@ var require_cli_table3 = __commonJS({
|
|
|
20078
20078
|
import { program } from "commander";
|
|
20079
20079
|
|
|
20080
20080
|
// package.json
|
|
20081
|
-
var version = "0.
|
|
20081
|
+
var version = "0.33.1";
|
|
20082
20082
|
var package_default = {
|
|
20083
20083
|
name: "genlayer",
|
|
20084
20084
|
version,
|
|
@@ -20147,7 +20147,7 @@ var package_default = {
|
|
|
20147
20147
|
dotenv: "^17.0.0",
|
|
20148
20148
|
ethers: "^6.13.4",
|
|
20149
20149
|
"fs-extra": "^11.3.0",
|
|
20150
|
-
"genlayer-js": "^0.18.
|
|
20150
|
+
"genlayer-js": "^0.18.10",
|
|
20151
20151
|
inquirer: "^12.0.0",
|
|
20152
20152
|
keytar: "^7.9.0",
|
|
20153
20153
|
"node-fetch": "^3.0.0",
|
|
@@ -33734,7 +33734,7 @@ init_fromHex();
|
|
|
33734
33734
|
init_toHex();
|
|
33735
33735
|
init_formatEther();
|
|
33736
33736
|
|
|
33737
|
-
// node_modules/genlayer-js/dist/chunk-
|
|
33737
|
+
// node_modules/genlayer-js/dist/chunk-WZNF2WK4.js
|
|
33738
33738
|
var chains_exports = {};
|
|
33739
33739
|
__export2(chains_exports, {
|
|
33740
33740
|
localnet: () => localnet,
|
|
@@ -42423,14 +42423,15 @@ var STAKING_ABI = [
|
|
|
42423
42423
|
]
|
|
42424
42424
|
}
|
|
42425
42425
|
];
|
|
42426
|
-
var TESTNET_JSON_RPC_URL = "https://
|
|
42426
|
+
var TESTNET_JSON_RPC_URL = "https://zksync-os-testnet-genlayer.zksync.dev";
|
|
42427
|
+
var TESTNET_WS_URL = "wss://zksync-os-testnet-alpha.zksync.dev/ws";
|
|
42427
42428
|
var STAKING_CONTRACT = {
|
|
42428
|
-
address: "
|
|
42429
|
+
address: "0x63Fa5E0bb10fb6fA98F44726C5518223F767687A",
|
|
42429
42430
|
abi: STAKING_ABI
|
|
42430
42431
|
};
|
|
42431
42432
|
var EXPLORER_URL2 = "https://explorer-asimov.genlayer.com/";
|
|
42432
42433
|
var CONSENSUS_MAIN_CONTRACT3 = {
|
|
42433
|
-
address: "
|
|
42434
|
+
address: "0x6CAFF6769d70824745AD895663409DC70aB5B28E",
|
|
42434
42435
|
abi: [
|
|
42435
42436
|
{
|
|
42436
42437
|
inputs: [],
|
|
@@ -43819,7 +43820,7 @@ var CONSENSUS_MAIN_CONTRACT3 = {
|
|
|
43819
43820
|
bytecode: ""
|
|
43820
43821
|
};
|
|
43821
43822
|
var CONSENSUS_DATA_CONTRACT3 = {
|
|
43822
|
-
address: "
|
|
43823
|
+
address: "0x0D9d1d74d72Fa5eB94bcf746C8FCcb312a722c9B",
|
|
43823
43824
|
abi: [
|
|
43824
43825
|
{
|
|
43825
43826
|
inputs: [],
|
|
@@ -46405,7 +46406,8 @@ var testnetAsimov = defineChain({
|
|
|
46405
46406
|
name: "Genlayer Asimov Testnet",
|
|
46406
46407
|
rpcUrls: {
|
|
46407
46408
|
default: {
|
|
46408
|
-
http: [TESTNET_JSON_RPC_URL]
|
|
46409
|
+
http: [TESTNET_JSON_RPC_URL],
|
|
46410
|
+
webSocket: [TESTNET_WS_URL]
|
|
46409
46411
|
}
|
|
46410
46412
|
},
|
|
46411
46413
|
nativeCurrency: {
|
|
@@ -51305,7 +51307,16 @@ function initializeTransactionsCommands(program2) {
|
|
|
51305
51307
|
|
|
51306
51308
|
// src/commands/staking/StakingAction.ts
|
|
51307
51309
|
import { readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
|
|
51308
|
-
import { ethers as ethers6 } from "ethers";
|
|
51310
|
+
import { ethers as ethers6, ZeroAddress } from "ethers";
|
|
51311
|
+
var STAKING_TREE_ABI = [
|
|
51312
|
+
{
|
|
51313
|
+
name: "validatorsRoot",
|
|
51314
|
+
type: "function",
|
|
51315
|
+
stateMutability: "view",
|
|
51316
|
+
inputs: [],
|
|
51317
|
+
outputs: [{ name: "", type: "address" }]
|
|
51318
|
+
}
|
|
51319
|
+
];
|
|
51309
51320
|
var StakingAction = class extends BaseAction {
|
|
51310
51321
|
constructor() {
|
|
51311
51322
|
super();
|
|
@@ -51392,7 +51403,7 @@ var StakingAction = class extends BaseAction {
|
|
|
51392
51403
|
}
|
|
51393
51404
|
this.stopSpinner();
|
|
51394
51405
|
const password = await this.promptPassword(`Enter password to unlock account '${accountName}':`);
|
|
51395
|
-
this.startSpinner("
|
|
51406
|
+
this.startSpinner("Unlocking account...");
|
|
51396
51407
|
const wallet = await ethers6.Wallet.fromEncryptedJson(keystoreJson, password);
|
|
51397
51408
|
return wallet.privateKey;
|
|
51398
51409
|
}
|
|
@@ -51439,6 +51450,52 @@ var StakingAction = class extends BaseAction {
|
|
|
51439
51450
|
signerAddress: account.address
|
|
51440
51451
|
};
|
|
51441
51452
|
}
|
|
51453
|
+
/**
|
|
51454
|
+
* Get all validators by traversing the validator tree.
|
|
51455
|
+
* This finds ALL validators including those not yet active/primed.
|
|
51456
|
+
*/
|
|
51457
|
+
async getAllValidatorsFromTree(config) {
|
|
51458
|
+
const network = this.getNetwork(config);
|
|
51459
|
+
const rpcUrl = config.rpc || network.rpcUrls.default.http[0];
|
|
51460
|
+
const stakingAddress = config.stakingAddress || network.stakingContract?.address;
|
|
51461
|
+
if (!stakingAddress) {
|
|
51462
|
+
throw new Error("Staking contract address not configured");
|
|
51463
|
+
}
|
|
51464
|
+
const publicClient = createPublicClient({
|
|
51465
|
+
chain: network,
|
|
51466
|
+
transport: http3(rpcUrl)
|
|
51467
|
+
});
|
|
51468
|
+
const root = await publicClient.readContract({
|
|
51469
|
+
address: stakingAddress,
|
|
51470
|
+
abi: STAKING_TREE_ABI,
|
|
51471
|
+
functionName: "validatorsRoot"
|
|
51472
|
+
});
|
|
51473
|
+
if (root === ZeroAddress) {
|
|
51474
|
+
return [];
|
|
51475
|
+
}
|
|
51476
|
+
const validators = [];
|
|
51477
|
+
const stack = [root];
|
|
51478
|
+
const visited = /* @__PURE__ */ new Set();
|
|
51479
|
+
while (stack.length > 0) {
|
|
51480
|
+
const addr = stack.pop();
|
|
51481
|
+
if (addr === ZeroAddress || visited.has(addr.toLowerCase())) continue;
|
|
51482
|
+
visited.add(addr.toLowerCase());
|
|
51483
|
+
validators.push(addr);
|
|
51484
|
+
const info = await publicClient.readContract({
|
|
51485
|
+
address: stakingAddress,
|
|
51486
|
+
abi: abi_exports.STAKING_ABI,
|
|
51487
|
+
functionName: "validatorView",
|
|
51488
|
+
args: [addr]
|
|
51489
|
+
});
|
|
51490
|
+
if (info.left !== ZeroAddress) {
|
|
51491
|
+
stack.push(info.left);
|
|
51492
|
+
}
|
|
51493
|
+
if (info.right !== ZeroAddress) {
|
|
51494
|
+
stack.push(info.right);
|
|
51495
|
+
}
|
|
51496
|
+
}
|
|
51497
|
+
return validators;
|
|
51498
|
+
}
|
|
51442
51499
|
};
|
|
51443
51500
|
|
|
51444
51501
|
// src/commands/staking/validatorJoin.ts
|
|
@@ -51605,6 +51662,38 @@ var ValidatorPrimeAction = class extends StakingAction {
|
|
|
51605
51662
|
this.failSpinner("Failed to prime validator", error.message || error);
|
|
51606
51663
|
}
|
|
51607
51664
|
}
|
|
51665
|
+
async primeAll(options) {
|
|
51666
|
+
this.startSpinner("Fetching validators...");
|
|
51667
|
+
try {
|
|
51668
|
+
const client = await this.getStakingClient(options);
|
|
51669
|
+
this.setSpinnerText("Fetching validators...");
|
|
51670
|
+
const allValidators = await this.getAllValidatorsFromTree(options);
|
|
51671
|
+
this.stopSpinner();
|
|
51672
|
+
console.log(`
|
|
51673
|
+
Priming ${allValidators.length} validators:
|
|
51674
|
+
`);
|
|
51675
|
+
let succeeded = 0;
|
|
51676
|
+
let skipped = 0;
|
|
51677
|
+
for (const addr of allValidators) {
|
|
51678
|
+
process.stdout.write(` ${addr} ... `);
|
|
51679
|
+
try {
|
|
51680
|
+
const result = await client.validatorPrime({ validator: addr });
|
|
51681
|
+
console.log(source_default.green(`primed ${result.transactionHash}`));
|
|
51682
|
+
succeeded++;
|
|
51683
|
+
} catch (error) {
|
|
51684
|
+
const msg = error.message || String(error);
|
|
51685
|
+
const shortErr = msg.length > 60 ? msg.slice(0, 57) + "..." : msg;
|
|
51686
|
+
console.log(source_default.gray(`skipped: ${shortErr}`));
|
|
51687
|
+
skipped++;
|
|
51688
|
+
}
|
|
51689
|
+
}
|
|
51690
|
+
console.log(`
|
|
51691
|
+
${source_default.green(`${succeeded} primed`)}, ${source_default.gray(`${skipped} skipped`)}
|
|
51692
|
+
`);
|
|
51693
|
+
} catch (error) {
|
|
51694
|
+
this.failSpinner("Failed to prime validators", error.message || error);
|
|
51695
|
+
}
|
|
51696
|
+
}
|
|
51608
51697
|
};
|
|
51609
51698
|
|
|
51610
51699
|
// src/commands/staking/setOperator.ts
|
|
@@ -51814,6 +51903,7 @@ var StakingInfoAction = class extends StakingAction {
|
|
|
51814
51903
|
]);
|
|
51815
51904
|
const currentEpoch = epochInfo.currentEpoch;
|
|
51816
51905
|
const result = {
|
|
51906
|
+
...options.debug && { currentEpoch: currentEpoch.toString() },
|
|
51817
51907
|
validator: info.address,
|
|
51818
51908
|
owner: info.owner,
|
|
51819
51909
|
operator: info.operator,
|
|
@@ -51828,19 +51918,20 @@ var StakingInfoAction = class extends StakingAction {
|
|
|
51828
51918
|
live: info.live,
|
|
51829
51919
|
banned: info.banned ? info.bannedEpoch?.toString() : "Not banned",
|
|
51830
51920
|
selfStakePendingDeposits: (() => {
|
|
51831
|
-
const
|
|
51832
|
-
return
|
|
51921
|
+
const deposits = options.debug ? info.pendingDeposits : info.pendingDeposits.filter((d) => d.epoch + ACTIVATION_DELAY_EPOCHS > currentEpoch);
|
|
51922
|
+
return deposits.length > 0 ? deposits.map((d) => {
|
|
51833
51923
|
const depositEpoch = d.epoch;
|
|
51834
51924
|
const activationEpoch = depositEpoch + ACTIVATION_DELAY_EPOCHS;
|
|
51835
51925
|
const epochsUntilActive = activationEpoch - currentEpoch;
|
|
51926
|
+
const isActivated = epochsUntilActive <= 0n;
|
|
51836
51927
|
return {
|
|
51837
51928
|
epoch: depositEpoch.toString(),
|
|
51838
51929
|
stake: d.stake,
|
|
51839
51930
|
shares: d.shares.toString(),
|
|
51840
51931
|
activatesAtEpoch: activationEpoch.toString(),
|
|
51841
|
-
epochsRemaining: epochsUntilActive.toString()
|
|
51932
|
+
...options.debug ? { status: isActivated ? "ACTIVATED" : `pending (${epochsUntilActive} epochs)` } : { epochsRemaining: epochsUntilActive.toString() }
|
|
51842
51933
|
};
|
|
51843
|
-
}) : "None";
|
|
51934
|
+
}) : options.debug ? `None (raw count: ${info.pendingDeposits.length})` : "None";
|
|
51844
51935
|
})(),
|
|
51845
51936
|
selfStakePendingWithdrawals: info.pendingWithdrawals.length > 0 ? info.pendingWithdrawals.map((w) => {
|
|
51846
51937
|
const exitEpoch = w.epoch;
|
|
@@ -52072,6 +52163,7 @@ var StakingInfoAction = class extends StakingAction {
|
|
|
52072
52163
|
myAddress = await this.getSignerAddress();
|
|
52073
52164
|
} catch {
|
|
52074
52165
|
}
|
|
52166
|
+
const allTreeAddresses = await this.getAllValidatorsFromTree(options);
|
|
52075
52167
|
const [activeAddresses, quarantinedList, bannedList, epochInfo] = await Promise.all([
|
|
52076
52168
|
client.getActiveValidators(),
|
|
52077
52169
|
client.getQuarantinedValidatorsDetailed(),
|
|
@@ -52080,12 +52172,9 @@ var StakingInfoAction = class extends StakingAction {
|
|
|
52080
52172
|
]);
|
|
52081
52173
|
const quarantinedSet = new Map(quarantinedList.map((v) => [v.validator.toLowerCase(), v]));
|
|
52082
52174
|
const bannedSet = new Map(bannedList.map((v) => [v.validator.toLowerCase(), v]));
|
|
52083
|
-
const
|
|
52084
|
-
|
|
52085
|
-
|
|
52086
|
-
...options.all ? bannedList.map((v) => v.validator) : []
|
|
52087
|
-
]);
|
|
52088
|
-
this.setSpinnerText(`Fetching details for ${allAddresses.size} validators...`);
|
|
52175
|
+
const activeSet = new Set(activeAddresses.map((a) => a.toLowerCase()));
|
|
52176
|
+
const allAddresses = options.all ? allTreeAddresses : allTreeAddresses.filter((addr) => !bannedSet.has(addr.toLowerCase()));
|
|
52177
|
+
this.setSpinnerText(`Fetching details for ${allAddresses.length} validators...`);
|
|
52089
52178
|
const BATCH_SIZE = 5;
|
|
52090
52179
|
const addressArray = Array.from(allAddresses);
|
|
52091
52180
|
const validatorInfos = [];
|
|
@@ -52103,7 +52192,7 @@ var StakingInfoAction = class extends StakingAction {
|
|
|
52103
52192
|
const addrLower = info.address.toLowerCase();
|
|
52104
52193
|
const isQuarantined = quarantinedSet.has(addrLower);
|
|
52105
52194
|
const isBanned = bannedSet.has(addrLower);
|
|
52106
|
-
const isActive =
|
|
52195
|
+
const isActive = activeSet.has(addrLower);
|
|
52107
52196
|
let status = "";
|
|
52108
52197
|
if (isBanned) {
|
|
52109
52198
|
const banInfo = bannedSet.get(addrLower);
|
|
@@ -52111,8 +52200,6 @@ var StakingInfoAction = class extends StakingAction {
|
|
|
52111
52200
|
} else if (isQuarantined) {
|
|
52112
52201
|
const qInfo = quarantinedSet.get(addrLower);
|
|
52113
52202
|
status = `quarant(e${qInfo.untilEpoch})`;
|
|
52114
|
-
} else if (info.needsPriming) {
|
|
52115
|
-
status = "prime!";
|
|
52116
52203
|
} else if (isActive) {
|
|
52117
52204
|
status = "active";
|
|
52118
52205
|
} else {
|
|
@@ -52156,6 +52243,7 @@ var StakingInfoAction = class extends StakingAction {
|
|
|
52156
52243
|
source_default.cyan("Self"),
|
|
52157
52244
|
source_default.cyan("Deleg"),
|
|
52158
52245
|
source_default.cyan("Pending"),
|
|
52246
|
+
source_default.cyan("Primed"),
|
|
52159
52247
|
source_default.cyan("Weight"),
|
|
52160
52248
|
source_default.cyan("Status")
|
|
52161
52249
|
],
|
|
@@ -52190,12 +52278,19 @@ var StakingInfoAction = class extends StakingAction {
|
|
|
52190
52278
|
if (moniker.length > 20) moniker = moniker.slice(0, 19) + "\u2026";
|
|
52191
52279
|
const validatorCell = moniker ? `${moniker}${roleTag}
|
|
52192
52280
|
${source_default.gray(info.address)}` : `${source_default.gray(info.address)}${roleTag}`;
|
|
52281
|
+
let primedStr;
|
|
52282
|
+
if (info.ePrimed >= currentEpoch) {
|
|
52283
|
+
primedStr = source_default.green(`e${info.ePrimed}`);
|
|
52284
|
+
} else if (info.ePrimed === currentEpoch - 1n) {
|
|
52285
|
+
primedStr = source_default.yellow(`e${info.ePrimed}`);
|
|
52286
|
+
} else {
|
|
52287
|
+
primedStr = source_default.red(`e${info.ePrimed}!`);
|
|
52288
|
+
}
|
|
52193
52289
|
let statusStr = status;
|
|
52194
52290
|
if (status === "active") statusStr = source_default.green(status);
|
|
52195
52291
|
else if (status === "BANNED") statusStr = source_default.red(status);
|
|
52196
52292
|
else if (status.startsWith("quarant")) statusStr = source_default.yellow(status);
|
|
52197
52293
|
else if (status.startsWith("banned")) statusStr = source_default.red(status);
|
|
52198
|
-
else if (status === "prime!") statusStr = source_default.magenta(status);
|
|
52199
52294
|
else if (status === "pending") statusStr = source_default.gray(status);
|
|
52200
52295
|
table.push([
|
|
52201
52296
|
(idx + 1).toString(),
|
|
@@ -52203,6 +52298,7 @@ ${source_default.gray(info.address)}` : `${source_default.gray(info.address)}${r
|
|
|
52203
52298
|
formatStake(info.vStake),
|
|
52204
52299
|
formatStake(info.dStake),
|
|
52205
52300
|
pendingStr,
|
|
52301
|
+
primedStr,
|
|
52206
52302
|
weightStr,
|
|
52207
52303
|
statusStr
|
|
52208
52304
|
]);
|
|
@@ -53106,6 +53202,10 @@ function initializeStakingCommands(program2) {
|
|
|
53106
53202
|
const action = new ValidatorPrimeAction();
|
|
53107
53203
|
await action.execute({ ...options, validator });
|
|
53108
53204
|
});
|
|
53205
|
+
staking.command("prime-all").description("Prime all validators that need priming").option("--account <name>", "Account to use (pays gas)").option("--network <network>", "Network to use (localnet, testnet-asimov)").option("--rpc <rpcUrl>", "RPC URL for the network").option("--staking-address <address>", "Staking contract address (overrides chain config)").action(async (options) => {
|
|
53206
|
+
const action = new ValidatorPrimeAction();
|
|
53207
|
+
await action.primeAll(options);
|
|
53208
|
+
});
|
|
53109
53209
|
staking.command("set-operator [validator] [operator]").description("Change the operator address for a validator wallet").option("--validator <address>", "Validator wallet address (deprecated, use positional arg)").option("--operator <address>", "New operator address (deprecated, use positional arg)").option("--account <name>", "Account to use (must be validator owner)").option("--network <network>", "Network to use (localnet, testnet-asimov)").option("--rpc <rpcUrl>", "RPC URL for the network").action(async (validatorArg, operatorArg, options) => {
|
|
53110
53210
|
const validator = validatorArg || options.validator;
|
|
53111
53211
|
const operator = operatorArg || options.operator;
|
|
@@ -53152,7 +53252,7 @@ function initializeStakingCommands(program2) {
|
|
|
53152
53252
|
const action = new DelegatorClaimAction();
|
|
53153
53253
|
await action.execute({ ...options, validator });
|
|
53154
53254
|
});
|
|
53155
|
-
staking.command("validator-info [validator]").description("Get information about a validator").option("--validator <address>", "Validator address (deprecated, use positional arg)").option("--account <name>", "Account to use (for default validator address)").option("--network <network>", "Network to use (localnet, testnet-asimov)").option("--rpc <rpcUrl>", "RPC URL for the network").option("--staking-address <address>", "Staking contract address (overrides chain config)").action(async (validatorArg, options) => {
|
|
53255
|
+
staking.command("validator-info [validator]").description("Get information about a validator").option("--validator <address>", "Validator address (deprecated, use positional arg)").option("--account <name>", "Account to use (for default validator address)").option("--network <network>", "Network to use (localnet, testnet-asimov)").option("--rpc <rpcUrl>", "RPC URL for the network").option("--staking-address <address>", "Staking contract address (overrides chain config)").option("--debug", "Show raw unfiltered pending deposits/withdrawals").action(async (validatorArg, options) => {
|
|
53156
53256
|
const validator = validatorArg || options.validator;
|
|
53157
53257
|
const action = new StakingInfoAction();
|
|
53158
53258
|
await action.getValidatorInfo({ ...options, validator });
|
package/docs/validator-guide.md
CHANGED
|
@@ -235,6 +235,23 @@ Output will include:
|
|
|
235
235
|
|
|
236
236
|
## Managing Your Validator
|
|
237
237
|
|
|
238
|
+
### Priming Your Validator
|
|
239
|
+
|
|
240
|
+
Validators must be "primed" each epoch to participate in consensus. Priming updates the validator's stake record for the upcoming epoch.
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
# Prime your validator
|
|
244
|
+
genlayer staking validator-prime 0xYourValidator...
|
|
245
|
+
|
|
246
|
+
# Or prime all validators at once (anyone can do this)
|
|
247
|
+
genlayer staking prime-all
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The `validators` command shows priming status:
|
|
251
|
+
- **Green** `e11` - Primed for current epoch
|
|
252
|
+
- **Yellow** `e10` - Needs priming before next epoch
|
|
253
|
+
- **Red** `e9!` - Urgently needs priming (behind)
|
|
254
|
+
|
|
238
255
|
### Add More Stake
|
|
239
256
|
|
|
240
257
|
```bash
|
|
@@ -247,6 +264,16 @@ genlayer staking validator-deposit --validator 0xYourValidatorWallet... --amount
|
|
|
247
264
|
genlayer staking active-validators
|
|
248
265
|
```
|
|
249
266
|
|
|
267
|
+
### View Validator Set
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
# Show all validators with stake, primed status, and weight
|
|
271
|
+
genlayer staking validators
|
|
272
|
+
|
|
273
|
+
# Include banned validators
|
|
274
|
+
genlayer staking validators --all
|
|
275
|
+
```
|
|
276
|
+
|
|
250
277
|
### Exit as Validator
|
|
251
278
|
|
|
252
279
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genlayer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.1",
|
|
4
4
|
"description": "GenLayer Command Line Tool",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"dotenv": "^17.0.0",
|
|
67
67
|
"ethers": "^6.13.4",
|
|
68
68
|
"fs-extra": "^11.3.0",
|
|
69
|
-
"genlayer-js": "^0.18.
|
|
69
|
+
"genlayer-js": "^0.18.10",
|
|
70
70
|
"inquirer": "^12.0.0",
|
|
71
71
|
"keytar": "^7.9.0",
|
|
72
72
|
"node-fetch": "^3.0.0",
|
|
@@ -2,10 +2,21 @@ import {BaseAction, BUILT_IN_NETWORKS, resolveNetwork} from "../../lib/actions/B
|
|
|
2
2
|
import {createClient, createAccount, formatStakingAmount, parseStakingAmount, abi} from "genlayer-js";
|
|
3
3
|
import type {GenLayerClient, GenLayerChain, Address} from "genlayer-js/types";
|
|
4
4
|
import {readFileSync, existsSync} from "fs";
|
|
5
|
-
import {ethers} from "ethers";
|
|
5
|
+
import {ethers, ZeroAddress} from "ethers";
|
|
6
6
|
import {createPublicClient, createWalletClient, http, type PublicClient, type WalletClient, type Chain, type Account} from "viem";
|
|
7
7
|
import {privateKeyToAccount} from "viem/accounts";
|
|
8
8
|
|
|
9
|
+
// Extended ABI for tree traversal (not in SDK)
|
|
10
|
+
const STAKING_TREE_ABI = [
|
|
11
|
+
{
|
|
12
|
+
name: "validatorsRoot",
|
|
13
|
+
type: "function",
|
|
14
|
+
stateMutability: "view",
|
|
15
|
+
inputs: [],
|
|
16
|
+
outputs: [{name: "", type: "address"}],
|
|
17
|
+
},
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
9
20
|
// Re-export for use by other staking commands
|
|
10
21
|
export {BUILT_IN_NETWORKS};
|
|
11
22
|
|
|
@@ -132,7 +143,7 @@ export class StakingAction extends BaseAction {
|
|
|
132
143
|
// Stop spinner before prompting for password
|
|
133
144
|
this.stopSpinner();
|
|
134
145
|
const password = await this.promptPassword(`Enter password to unlock account '${accountName}':`);
|
|
135
|
-
this.startSpinner("
|
|
146
|
+
this.startSpinner("Unlocking account...");
|
|
136
147
|
|
|
137
148
|
const wallet = await ethers.Wallet.fromEncryptedJson(keystoreJson, password);
|
|
138
149
|
return wallet.privateKey;
|
|
@@ -193,4 +204,64 @@ export class StakingAction extends BaseAction {
|
|
|
193
204
|
signerAddress: account.address as Address,
|
|
194
205
|
};
|
|
195
206
|
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get all validators by traversing the validator tree.
|
|
210
|
+
* This finds ALL validators including those not yet active/primed.
|
|
211
|
+
*/
|
|
212
|
+
protected async getAllValidatorsFromTree(config: StakingConfig): Promise<Address[]> {
|
|
213
|
+
const network = this.getNetwork(config);
|
|
214
|
+
const rpcUrl = config.rpc || network.rpcUrls.default.http[0];
|
|
215
|
+
const stakingAddress = config.stakingAddress || network.stakingContract?.address;
|
|
216
|
+
|
|
217
|
+
if (!stakingAddress) {
|
|
218
|
+
throw new Error("Staking contract address not configured");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const publicClient = createPublicClient({
|
|
222
|
+
chain: network,
|
|
223
|
+
transport: http(rpcUrl),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Get the root of the validator tree
|
|
227
|
+
const root = await publicClient.readContract({
|
|
228
|
+
address: stakingAddress as `0x${string}`,
|
|
229
|
+
abi: STAKING_TREE_ABI,
|
|
230
|
+
functionName: "validatorsRoot",
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (root === ZeroAddress) {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const validators: Address[] = [];
|
|
238
|
+
const stack: string[] = [root as string];
|
|
239
|
+
const visited = new Set<string>();
|
|
240
|
+
|
|
241
|
+
// Use validatorView from SDK's ABI (has left/right fields)
|
|
242
|
+
while (stack.length > 0) {
|
|
243
|
+
const addr = stack.pop()!;
|
|
244
|
+
|
|
245
|
+
if (addr === ZeroAddress || visited.has(addr.toLowerCase())) continue;
|
|
246
|
+
visited.add(addr.toLowerCase());
|
|
247
|
+
|
|
248
|
+
validators.push(addr as Address);
|
|
249
|
+
|
|
250
|
+
const info = await publicClient.readContract({
|
|
251
|
+
address: stakingAddress as `0x${string}`,
|
|
252
|
+
abi: abi.STAKING_ABI,
|
|
253
|
+
functionName: "validatorView",
|
|
254
|
+
args: [addr as `0x${string}`],
|
|
255
|
+
}) as {left: string; right: string};
|
|
256
|
+
|
|
257
|
+
if (info.left !== ZeroAddress) {
|
|
258
|
+
stack.push(info.left);
|
|
259
|
+
}
|
|
260
|
+
if (info.right !== ZeroAddress) {
|
|
261
|
+
stack.push(info.right);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return validators;
|
|
266
|
+
}
|
|
196
267
|
}
|
|
@@ -116,6 +116,18 @@ export function initializeStakingCommands(program: Command) {
|
|
|
116
116
|
await action.execute({...options, validator});
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
+
staking
|
|
120
|
+
.command("prime-all")
|
|
121
|
+
.description("Prime all validators that need priming")
|
|
122
|
+
.option("--account <name>", "Account to use (pays gas)")
|
|
123
|
+
.option("--network <network>", "Network to use (localnet, testnet-asimov)")
|
|
124
|
+
.option("--rpc <rpcUrl>", "RPC URL for the network")
|
|
125
|
+
.option("--staking-address <address>", "Staking contract address (overrides chain config)")
|
|
126
|
+
.action(async (options: StakingConfig) => {
|
|
127
|
+
const action = new ValidatorPrimeAction();
|
|
128
|
+
await action.primeAll(options);
|
|
129
|
+
});
|
|
130
|
+
|
|
119
131
|
staking
|
|
120
132
|
.command("set-operator [validator] [operator]")
|
|
121
133
|
.description("Change the operator address for a validator wallet")
|
|
@@ -228,6 +240,7 @@ export function initializeStakingCommands(program: Command) {
|
|
|
228
240
|
.option("--network <network>", "Network to use (localnet, testnet-asimov)")
|
|
229
241
|
.option("--rpc <rpcUrl>", "RPC URL for the network")
|
|
230
242
|
.option("--staking-address <address>", "Staking contract address (overrides chain config)")
|
|
243
|
+
.option("--debug", "Show raw unfiltered pending deposits/withdrawals")
|
|
231
244
|
.action(async (validatorArg: string | undefined, options: StakingInfoOptions) => {
|
|
232
245
|
const validator = validatorArg || options.validator;
|
|
233
246
|
const action = new StakingInfoAction();
|
|
@@ -9,6 +9,7 @@ const UNBONDING_PERIOD_EPOCHS = 7n;
|
|
|
9
9
|
|
|
10
10
|
export interface StakingInfoOptions extends StakingConfig {
|
|
11
11
|
validator?: string;
|
|
12
|
+
debug?: boolean;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export class StakingInfoAction extends StakingAction {
|
|
@@ -38,6 +39,7 @@ export class StakingInfoAction extends StakingAction {
|
|
|
38
39
|
const currentEpoch = epochInfo.currentEpoch;
|
|
39
40
|
|
|
40
41
|
const result: Record<string, any> = {
|
|
42
|
+
...(options.debug && {currentEpoch: currentEpoch.toString()}),
|
|
41
43
|
validator: info.address,
|
|
42
44
|
owner: info.owner,
|
|
43
45
|
operator: info.operator,
|
|
@@ -52,22 +54,27 @@ export class StakingInfoAction extends StakingAction {
|
|
|
52
54
|
live: info.live,
|
|
53
55
|
banned: info.banned ? info.bannedEpoch?.toString() : "Not banned",
|
|
54
56
|
selfStakePendingDeposits: (() => {
|
|
55
|
-
//
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
// In debug mode, show all deposits; otherwise filter to truly pending only
|
|
58
|
+
const deposits = options.debug
|
|
59
|
+
? info.pendingDeposits
|
|
60
|
+
: info.pendingDeposits.filter(d => d.epoch + ACTIVATION_DELAY_EPOCHS > currentEpoch);
|
|
61
|
+
return deposits.length > 0
|
|
62
|
+
? deposits.map(d => {
|
|
59
63
|
const depositEpoch = d.epoch;
|
|
60
64
|
const activationEpoch = depositEpoch + ACTIVATION_DELAY_EPOCHS;
|
|
61
65
|
const epochsUntilActive = activationEpoch - currentEpoch;
|
|
66
|
+
const isActivated = epochsUntilActive <= 0n;
|
|
62
67
|
return {
|
|
63
68
|
epoch: depositEpoch.toString(),
|
|
64
69
|
stake: d.stake,
|
|
65
70
|
shares: d.shares.toString(),
|
|
66
71
|
activatesAtEpoch: activationEpoch.toString(),
|
|
67
|
-
|
|
72
|
+
...(options.debug
|
|
73
|
+
? {status: isActivated ? "ACTIVATED" : `pending (${epochsUntilActive} epochs)`}
|
|
74
|
+
: {epochsRemaining: epochsUntilActive.toString()}),
|
|
68
75
|
};
|
|
69
76
|
})
|
|
70
|
-
: "None";
|
|
77
|
+
: options.debug ? `None (raw count: ${info.pendingDeposits.length})` : "None";
|
|
71
78
|
})(),
|
|
72
79
|
selfStakePendingWithdrawals:
|
|
73
80
|
info.pendingWithdrawals.length > 0
|
|
@@ -362,7 +369,10 @@ export class StakingInfoAction extends StakingAction {
|
|
|
362
369
|
// No account configured, that's fine
|
|
363
370
|
}
|
|
364
371
|
|
|
365
|
-
//
|
|
372
|
+
// Use tree traversal to get ALL validators (including not-yet-primed)
|
|
373
|
+
const allTreeAddresses = await this.getAllValidatorsFromTree(options);
|
|
374
|
+
|
|
375
|
+
// Also fetch status lists in parallel
|
|
366
376
|
const [activeAddresses, quarantinedList, bannedList, epochInfo] = await Promise.all([
|
|
367
377
|
client.getActiveValidators(),
|
|
368
378
|
client.getQuarantinedValidatorsDetailed(),
|
|
@@ -373,15 +383,14 @@ export class StakingInfoAction extends StakingAction {
|
|
|
373
383
|
// Build set of quarantined/banned for status lookup
|
|
374
384
|
const quarantinedSet = new Map(quarantinedList.map(v => [v.validator.toLowerCase(), v]));
|
|
375
385
|
const bannedSet = new Map(bannedList.map(v => [v.validator.toLowerCase(), v]));
|
|
386
|
+
const activeSet = new Set(activeAddresses.map(a => a.toLowerCase()));
|
|
376
387
|
|
|
377
|
-
//
|
|
378
|
-
const allAddresses =
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
...(options.all ? bannedList.map(v => v.validator) : []),
|
|
382
|
-
]);
|
|
388
|
+
// Filter out banned if not --all
|
|
389
|
+
const allAddresses = options.all
|
|
390
|
+
? allTreeAddresses
|
|
391
|
+
: allTreeAddresses.filter(addr => !bannedSet.has(addr.toLowerCase()));
|
|
383
392
|
|
|
384
|
-
this.setSpinnerText(`Fetching details for ${allAddresses.
|
|
393
|
+
this.setSpinnerText(`Fetching details for ${allAddresses.length} validators...`);
|
|
385
394
|
|
|
386
395
|
// Fetch detailed info in batches to avoid rate limiting
|
|
387
396
|
const BATCH_SIZE = 5;
|
|
@@ -411,7 +420,7 @@ export class StakingInfoAction extends StakingAction {
|
|
|
411
420
|
const addrLower = info.address.toLowerCase();
|
|
412
421
|
const isQuarantined = quarantinedSet.has(addrLower);
|
|
413
422
|
const isBanned = bannedSet.has(addrLower);
|
|
414
|
-
const isActive =
|
|
423
|
+
const isActive = activeSet.has(addrLower);
|
|
415
424
|
|
|
416
425
|
let status = "";
|
|
417
426
|
if (isBanned) {
|
|
@@ -420,8 +429,6 @@ export class StakingInfoAction extends StakingAction {
|
|
|
420
429
|
} else if (isQuarantined) {
|
|
421
430
|
const qInfo = quarantinedSet.get(addrLower)!;
|
|
422
431
|
status = `quarant(e${qInfo.untilEpoch})`;
|
|
423
|
-
} else if (info.needsPriming) {
|
|
424
|
-
status = "prime!";
|
|
425
432
|
} else if (isActive) {
|
|
426
433
|
status = "active";
|
|
427
434
|
} else {
|
|
@@ -485,6 +492,7 @@ export class StakingInfoAction extends StakingAction {
|
|
|
485
492
|
chalk.cyan("Self"),
|
|
486
493
|
chalk.cyan("Deleg"),
|
|
487
494
|
chalk.cyan("Pending"),
|
|
495
|
+
chalk.cyan("Primed"),
|
|
488
496
|
chalk.cyan("Weight"),
|
|
489
497
|
chalk.cyan("Status"),
|
|
490
498
|
],
|
|
@@ -533,13 +541,22 @@ export class StakingInfoAction extends StakingAction {
|
|
|
533
541
|
? `${moniker}${roleTag}\n${chalk.gray(info.address)}`
|
|
534
542
|
: `${chalk.gray(info.address)}${roleTag}`;
|
|
535
543
|
|
|
544
|
+
// Primed status - color based on how current it is
|
|
545
|
+
let primedStr: string;
|
|
546
|
+
if (info.ePrimed >= currentEpoch) {
|
|
547
|
+
primedStr = chalk.green(`e${info.ePrimed}`);
|
|
548
|
+
} else if (info.ePrimed === currentEpoch - 1n) {
|
|
549
|
+
primedStr = chalk.yellow(`e${info.ePrimed}`);
|
|
550
|
+
} else {
|
|
551
|
+
primedStr = chalk.red(`e${info.ePrimed}!`);
|
|
552
|
+
}
|
|
553
|
+
|
|
536
554
|
// Status coloring
|
|
537
555
|
let statusStr = status;
|
|
538
556
|
if (status === "active") statusStr = chalk.green(status);
|
|
539
557
|
else if (status === "BANNED") statusStr = chalk.red(status);
|
|
540
558
|
else if (status.startsWith("quarant")) statusStr = chalk.yellow(status);
|
|
541
559
|
else if (status.startsWith("banned")) statusStr = chalk.red(status);
|
|
542
|
-
else if (status === "prime!") statusStr = chalk.magenta(status);
|
|
543
560
|
else if (status === "pending") statusStr = chalk.gray(status);
|
|
544
561
|
|
|
545
562
|
table.push([
|
|
@@ -548,6 +565,7 @@ export class StakingInfoAction extends StakingAction {
|
|
|
548
565
|
formatStake(info.vStake),
|
|
549
566
|
formatStake(info.dStake),
|
|
550
567
|
pendingStr,
|
|
568
|
+
primedStr,
|
|
551
569
|
weightStr,
|
|
552
570
|
statusStr,
|
|
553
571
|
]);
|
|
@@ -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 ValidatorPrimeOptions extends StakingConfig {
|
|
5
6
|
validator: string;
|
|
@@ -32,4 +33,41 @@ export class ValidatorPrimeAction extends StakingAction {
|
|
|
32
33
|
this.failSpinner("Failed to prime validator", error.message || error);
|
|
33
34
|
}
|
|
34
35
|
}
|
|
36
|
+
|
|
37
|
+
async primeAll(options: StakingConfig): Promise<void> {
|
|
38
|
+
this.startSpinner("Fetching validators...");
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const client = await this.getStakingClient(options);
|
|
42
|
+
|
|
43
|
+
// Get all validators from tree
|
|
44
|
+
this.setSpinnerText("Fetching validators...");
|
|
45
|
+
const allValidators = await this.getAllValidatorsFromTree(options);
|
|
46
|
+
|
|
47
|
+
this.stopSpinner();
|
|
48
|
+
console.log(`\nPriming ${allValidators.length} validators:\n`);
|
|
49
|
+
|
|
50
|
+
let succeeded = 0;
|
|
51
|
+
let skipped = 0;
|
|
52
|
+
|
|
53
|
+
for (const addr of allValidators) {
|
|
54
|
+
process.stdout.write(` ${addr} ... `);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const result = await client.validatorPrime({validator: addr});
|
|
58
|
+
console.log(chalk.green(`primed ${result.transactionHash}`));
|
|
59
|
+
succeeded++;
|
|
60
|
+
} catch (error: any) {
|
|
61
|
+
const msg = error.message || String(error);
|
|
62
|
+
const shortErr = msg.length > 60 ? msg.slice(0, 57) + "..." : msg;
|
|
63
|
+
console.log(chalk.gray(`skipped: ${shortErr}`));
|
|
64
|
+
skipped++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(`\n${chalk.green(`${succeeded} primed`)}, ${chalk.gray(`${skipped} skipped`)}\n`);
|
|
69
|
+
} catch (error: any) {
|
|
70
|
+
this.failSpinner("Failed to prime validators", error.message || error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
35
73
|
}
|