genlayer 0.32.6 → 0.33.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 CHANGED
@@ -1,6 +1,12 @@
1
1
  # Changelog
2
2
 
3
- ## 0.32.6 (2025-12-12)
3
+ ## 0.33.0 (2026-01-13)
4
+
5
+ ### Features
6
+
7
+ * 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))
8
+
9
+ ## 0.32.8 (2025-12-12)
4
10
 
5
11
  ## 0.32.5 (2025-12-09)
6
12
 
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
@@ -399,20 +401,29 @@ EXAMPLES:
399
401
  genlayer staking validators
400
402
  genlayer staking validators --all # Include banned validators
401
403
 
402
- # Show validator slash/reward history (testnet only)
404
+ # Show validator slash/reward history (testnet only, default: last 10 epochs)
403
405
  genlayer staking validator-history 0x...
406
+ genlayer staking validator-history 0x... --epochs 5 # Last 5 epochs
407
+ genlayer staking validator-history 0x... --from-epoch 3 # From epoch 3
408
+ genlayer staking validator-history 0x... --all # Complete history (slow)
404
409
  genlayer staking validator-history 0x... --limit 20
405
410
  # Output:
406
- # ┌─────────────┬───────┬────────┬────────────────────────┬─────────┐
407
- # │ Time │ Epoch │ Type │ Details │ Block
408
- # ├─────────────┼───────┼────────┼────────────────────────┼─────────┤
409
- # │ 12-11 14:20 │ 5 │ REWARD │ Val: 0 GEN, Del: 0 GEN │ 4725136
410
- # │ 12-10 18:39 │ 4 │ SLASH │ 1.00% penalty 4717431
411
- # └─────────────┴───────┴────────┴────────────────────────┴─────────┘
411
+ # ┌─────────────┬───────┬────────┬────────┬────────────────────────────────────┐
412
+ # │ Time │ Epoch │ Type │ Details│ GL TxId / Block
413
+ # ├─────────────┼───────┼────────┼────────┼────────────────────────────────────┤
414
+ # │ 12-11 14:20 │ 5 │ REWARD │ Val: …│ block 4725136
415
+ # │ 12-10 18:39 │ 4 │ SLASH │ 1.00% │ 0x52db90a9...
416
+ # └─────────────┴───────┴────────┴────────┴────────────────────────────────────┘
412
417
 
413
418
  # Exit and claim (requires validator wallet address)
414
419
  genlayer staking validator-exit --validator 0x... --shares 100
415
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
416
427
  ```
417
428
 
418
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.32.6";
20081
+ var version = "0.33.0";
20082
20082
  var package_default = {
20083
20083
  name: "genlayer",
20084
20084
  version,
@@ -51305,7 +51305,16 @@ function initializeTransactionsCommands(program2) {
51305
51305
 
51306
51306
  // src/commands/staking/StakingAction.ts
51307
51307
  import { readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
51308
- import { ethers as ethers6 } from "ethers";
51308
+ import { ethers as ethers6, ZeroAddress } from "ethers";
51309
+ var STAKING_TREE_ABI = [
51310
+ {
51311
+ name: "validatorsRoot",
51312
+ type: "function",
51313
+ stateMutability: "view",
51314
+ inputs: [],
51315
+ outputs: [{ name: "", type: "address" }]
51316
+ }
51317
+ ];
51309
51318
  var StakingAction = class extends BaseAction {
51310
51319
  constructor() {
51311
51320
  super();
@@ -51392,7 +51401,7 @@ var StakingAction = class extends BaseAction {
51392
51401
  }
51393
51402
  this.stopSpinner();
51394
51403
  const password = await this.promptPassword(`Enter password to unlock account '${accountName}':`);
51395
- this.startSpinner("Continuing...");
51404
+ this.startSpinner("Unlocking account...");
51396
51405
  const wallet = await ethers6.Wallet.fromEncryptedJson(keystoreJson, password);
51397
51406
  return wallet.privateKey;
51398
51407
  }
@@ -51439,6 +51448,52 @@ var StakingAction = class extends BaseAction {
51439
51448
  signerAddress: account.address
51440
51449
  };
51441
51450
  }
51451
+ /**
51452
+ * Get all validators by traversing the validator tree.
51453
+ * This finds ALL validators including those not yet active/primed.
51454
+ */
51455
+ async getAllValidatorsFromTree(config) {
51456
+ const network = this.getNetwork(config);
51457
+ const rpcUrl = config.rpc || network.rpcUrls.default.http[0];
51458
+ const stakingAddress = config.stakingAddress || network.stakingContract?.address;
51459
+ if (!stakingAddress) {
51460
+ throw new Error("Staking contract address not configured");
51461
+ }
51462
+ const publicClient = createPublicClient({
51463
+ chain: network,
51464
+ transport: http3(rpcUrl)
51465
+ });
51466
+ const root = await publicClient.readContract({
51467
+ address: stakingAddress,
51468
+ abi: STAKING_TREE_ABI,
51469
+ functionName: "validatorsRoot"
51470
+ });
51471
+ if (root === ZeroAddress) {
51472
+ return [];
51473
+ }
51474
+ const validators = [];
51475
+ const stack = [root];
51476
+ const visited = /* @__PURE__ */ new Set();
51477
+ while (stack.length > 0) {
51478
+ const addr = stack.pop();
51479
+ if (addr === ZeroAddress || visited.has(addr.toLowerCase())) continue;
51480
+ visited.add(addr.toLowerCase());
51481
+ validators.push(addr);
51482
+ const info = await publicClient.readContract({
51483
+ address: stakingAddress,
51484
+ abi: abi_exports.STAKING_ABI,
51485
+ functionName: "validatorView",
51486
+ args: [addr]
51487
+ });
51488
+ if (info.left !== ZeroAddress) {
51489
+ stack.push(info.left);
51490
+ }
51491
+ if (info.right !== ZeroAddress) {
51492
+ stack.push(info.right);
51493
+ }
51494
+ }
51495
+ return validators;
51496
+ }
51442
51497
  };
51443
51498
 
51444
51499
  // src/commands/staking/validatorJoin.ts
@@ -51605,6 +51660,38 @@ var ValidatorPrimeAction = class extends StakingAction {
51605
51660
  this.failSpinner("Failed to prime validator", error.message || error);
51606
51661
  }
51607
51662
  }
51663
+ async primeAll(options) {
51664
+ this.startSpinner("Fetching validators...");
51665
+ try {
51666
+ const client = await this.getStakingClient(options);
51667
+ this.setSpinnerText("Fetching validators...");
51668
+ const allValidators = await this.getAllValidatorsFromTree(options);
51669
+ this.stopSpinner();
51670
+ console.log(`
51671
+ Priming ${allValidators.length} validators:
51672
+ `);
51673
+ let succeeded = 0;
51674
+ let skipped = 0;
51675
+ for (const addr of allValidators) {
51676
+ process.stdout.write(` ${addr} ... `);
51677
+ try {
51678
+ const result = await client.validatorPrime({ validator: addr });
51679
+ console.log(source_default.green(`primed ${result.transactionHash}`));
51680
+ succeeded++;
51681
+ } catch (error) {
51682
+ const msg = error.message || String(error);
51683
+ const shortErr = msg.length > 60 ? msg.slice(0, 57) + "..." : msg;
51684
+ console.log(source_default.gray(`skipped: ${shortErr}`));
51685
+ skipped++;
51686
+ }
51687
+ }
51688
+ console.log(`
51689
+ ${source_default.green(`${succeeded} primed`)}, ${source_default.gray(`${skipped} skipped`)}
51690
+ `);
51691
+ } catch (error) {
51692
+ this.failSpinner("Failed to prime validators", error.message || error);
51693
+ }
51694
+ }
51608
51695
  };
51609
51696
 
51610
51697
  // src/commands/staking/setOperator.ts
@@ -51814,6 +51901,7 @@ var StakingInfoAction = class extends StakingAction {
51814
51901
  ]);
51815
51902
  const currentEpoch = epochInfo.currentEpoch;
51816
51903
  const result = {
51904
+ ...options.debug && { currentEpoch: currentEpoch.toString() },
51817
51905
  validator: info.address,
51818
51906
  owner: info.owner,
51819
51907
  operator: info.operator,
@@ -51828,19 +51916,20 @@ var StakingInfoAction = class extends StakingAction {
51828
51916
  live: info.live,
51829
51917
  banned: info.banned ? info.bannedEpoch?.toString() : "Not banned",
51830
51918
  selfStakePendingDeposits: (() => {
51831
- const pending = info.pendingDeposits.filter((d) => d.epoch + ACTIVATION_DELAY_EPOCHS > currentEpoch);
51832
- return pending.length > 0 ? pending.map((d) => {
51919
+ const deposits = options.debug ? info.pendingDeposits : info.pendingDeposits.filter((d) => d.epoch + ACTIVATION_DELAY_EPOCHS > currentEpoch);
51920
+ return deposits.length > 0 ? deposits.map((d) => {
51833
51921
  const depositEpoch = d.epoch;
51834
51922
  const activationEpoch = depositEpoch + ACTIVATION_DELAY_EPOCHS;
51835
51923
  const epochsUntilActive = activationEpoch - currentEpoch;
51924
+ const isActivated = epochsUntilActive <= 0n;
51836
51925
  return {
51837
51926
  epoch: depositEpoch.toString(),
51838
51927
  stake: d.stake,
51839
51928
  shares: d.shares.toString(),
51840
51929
  activatesAtEpoch: activationEpoch.toString(),
51841
- epochsRemaining: epochsUntilActive.toString()
51930
+ ...options.debug ? { status: isActivated ? "ACTIVATED" : `pending (${epochsUntilActive} epochs)` } : { epochsRemaining: epochsUntilActive.toString() }
51842
51931
  };
51843
- }) : "None";
51932
+ }) : options.debug ? `None (raw count: ${info.pendingDeposits.length})` : "None";
51844
51933
  })(),
51845
51934
  selfStakePendingWithdrawals: info.pendingWithdrawals.length > 0 ? info.pendingWithdrawals.map((w) => {
51846
51935
  const exitEpoch = w.epoch;
@@ -52072,6 +52161,7 @@ var StakingInfoAction = class extends StakingAction {
52072
52161
  myAddress = await this.getSignerAddress();
52073
52162
  } catch {
52074
52163
  }
52164
+ const allTreeAddresses = await this.getAllValidatorsFromTree(options);
52075
52165
  const [activeAddresses, quarantinedList, bannedList, epochInfo] = await Promise.all([
52076
52166
  client.getActiveValidators(),
52077
52167
  client.getQuarantinedValidatorsDetailed(),
@@ -52080,12 +52170,9 @@ var StakingInfoAction = class extends StakingAction {
52080
52170
  ]);
52081
52171
  const quarantinedSet = new Map(quarantinedList.map((v) => [v.validator.toLowerCase(), v]));
52082
52172
  const bannedSet = new Map(bannedList.map((v) => [v.validator.toLowerCase(), v]));
52083
- const allAddresses = /* @__PURE__ */ new Set([
52084
- ...activeAddresses,
52085
- ...quarantinedList.map((v) => v.validator),
52086
- ...options.all ? bannedList.map((v) => v.validator) : []
52087
- ]);
52088
- this.setSpinnerText(`Fetching details for ${allAddresses.size} validators...`);
52173
+ const activeSet = new Set(activeAddresses.map((a) => a.toLowerCase()));
52174
+ const allAddresses = options.all ? allTreeAddresses : allTreeAddresses.filter((addr) => !bannedSet.has(addr.toLowerCase()));
52175
+ this.setSpinnerText(`Fetching details for ${allAddresses.length} validators...`);
52089
52176
  const BATCH_SIZE = 5;
52090
52177
  const addressArray = Array.from(allAddresses);
52091
52178
  const validatorInfos = [];
@@ -52103,7 +52190,7 @@ var StakingInfoAction = class extends StakingAction {
52103
52190
  const addrLower = info.address.toLowerCase();
52104
52191
  const isQuarantined = quarantinedSet.has(addrLower);
52105
52192
  const isBanned = bannedSet.has(addrLower);
52106
- const isActive = activeAddresses.some((a) => a.toLowerCase() === addrLower);
52193
+ const isActive = activeSet.has(addrLower);
52107
52194
  let status = "";
52108
52195
  if (isBanned) {
52109
52196
  const banInfo = bannedSet.get(addrLower);
@@ -52111,8 +52198,6 @@ var StakingInfoAction = class extends StakingAction {
52111
52198
  } else if (isQuarantined) {
52112
52199
  const qInfo = quarantinedSet.get(addrLower);
52113
52200
  status = `quarant(e${qInfo.untilEpoch})`;
52114
- } else if (info.needsPriming) {
52115
- status = "prime!";
52116
52201
  } else if (isActive) {
52117
52202
  status = "active";
52118
52203
  } else {
@@ -52156,6 +52241,7 @@ var StakingInfoAction = class extends StakingAction {
52156
52241
  source_default.cyan("Self"),
52157
52242
  source_default.cyan("Deleg"),
52158
52243
  source_default.cyan("Pending"),
52244
+ source_default.cyan("Primed"),
52159
52245
  source_default.cyan("Weight"),
52160
52246
  source_default.cyan("Status")
52161
52247
  ],
@@ -52190,12 +52276,19 @@ var StakingInfoAction = class extends StakingAction {
52190
52276
  if (moniker.length > 20) moniker = moniker.slice(0, 19) + "\u2026";
52191
52277
  const validatorCell = moniker ? `${moniker}${roleTag}
52192
52278
  ${source_default.gray(info.address)}` : `${source_default.gray(info.address)}${roleTag}`;
52279
+ let primedStr;
52280
+ if (info.ePrimed >= currentEpoch) {
52281
+ primedStr = source_default.green(`e${info.ePrimed}`);
52282
+ } else if (info.ePrimed === currentEpoch - 1n) {
52283
+ primedStr = source_default.yellow(`e${info.ePrimed}`);
52284
+ } else {
52285
+ primedStr = source_default.red(`e${info.ePrimed}!`);
52286
+ }
52193
52287
  let statusStr = status;
52194
52288
  if (status === "active") statusStr = source_default.green(status);
52195
52289
  else if (status === "BANNED") statusStr = source_default.red(status);
52196
52290
  else if (status.startsWith("quarant")) statusStr = source_default.yellow(status);
52197
52291
  else if (status.startsWith("banned")) statusStr = source_default.red(status);
52198
- else if (status === "prime!") statusStr = source_default.magenta(status);
52199
52292
  else if (status === "pending") statusStr = source_default.gray(status);
52200
52293
  table.push([
52201
52294
  (idx + 1).toString(),
@@ -52203,6 +52296,7 @@ ${source_default.gray(info.address)}` : `${source_default.gray(info.address)}${r
52203
52296
  formatStake(info.vStake),
52204
52297
  formatStake(info.dStake),
52205
52298
  pendingStr,
52299
+ primedStr,
52206
52300
  weightStr,
52207
52301
  statusStr
52208
52302
  ]);
@@ -52281,7 +52375,22 @@ var ValidatorHistoryAction = class extends StakingAction {
52281
52375
  chain,
52282
52376
  transport: http3(chain.rpcUrls.default.http[0])
52283
52377
  });
52284
- const fromBlock = options.fromBlock ? BigInt(options.fromBlock) : 0n;
52378
+ const epochInfo = await client.getEpochInfo();
52379
+ const currentEpoch = epochInfo.currentEpoch;
52380
+ const defaultEpochs = 10n;
52381
+ let minEpoch = null;
52382
+ let fromBlock = "earliest";
52383
+ if (options.fromBlock) {
52384
+ fromBlock = BigInt(options.fromBlock);
52385
+ } else if (options.fromEpoch) {
52386
+ minEpoch = BigInt(options.fromEpoch);
52387
+ } else if (options.all) {
52388
+ console.log(source_default.yellow("Warning: Fetching all history from genesis. This may be slow for long-lived validators."));
52389
+ console.log(source_default.yellow("Consider using --epochs <n> or --from-epoch <n> for faster queries.\n"));
52390
+ } else {
52391
+ const numEpochs = options.epochs ? BigInt(options.epochs) : defaultEpochs;
52392
+ minEpoch = currentEpoch > numEpochs ? currentEpoch - numEpochs : 0n;
52393
+ }
52285
52394
  const limit = options.limit ? parseInt(options.limit) : 50;
52286
52395
  this.setSpinnerText("Fetching slash events...");
52287
52396
  const slashLogs = await publicClient.getLogs({
@@ -52315,7 +52424,7 @@ var ValidatorHistoryAction = class extends StakingAction {
52315
52424
  blockTimestamps.set(block.number, new Date(Number(block.timestamp) * 1e3));
52316
52425
  });
52317
52426
  }
52318
- const slashEvents = slashLogs.map((log) => ({
52427
+ let slashEvents = slashLogs.map((log) => ({
52319
52428
  type: "slash",
52320
52429
  epoch: log.args.epoch,
52321
52430
  txId: log.args.txId,
@@ -52323,7 +52432,7 @@ var ValidatorHistoryAction = class extends StakingAction {
52323
52432
  blockNumber: log.blockNumber,
52324
52433
  timestamp: blockTimestamps.get(log.blockNumber) || /* @__PURE__ */ new Date(0)
52325
52434
  }));
52326
- const rewardEvents = filteredRewardLogs.map((log) => ({
52435
+ let rewardEvents = filteredRewardLogs.map((log) => ({
52327
52436
  type: "reward",
52328
52437
  epoch: log.args.epoch,
52329
52438
  validatorRewards: log.args.validatorRewards,
@@ -52331,6 +52440,10 @@ var ValidatorHistoryAction = class extends StakingAction {
52331
52440
  blockNumber: log.blockNumber,
52332
52441
  timestamp: blockTimestamps.get(log.blockNumber) || /* @__PURE__ */ new Date(0)
52333
52442
  }));
52443
+ if (minEpoch !== null) {
52444
+ slashEvents = slashEvents.filter((e2) => e2.epoch >= minEpoch);
52445
+ rewardEvents = rewardEvents.filter((e2) => e2.epoch >= minEpoch);
52446
+ }
52334
52447
  const allEvents = [...slashEvents, ...rewardEvents];
52335
52448
  allEvents.sort((a, b) => Number(b.blockNumber - a.blockNumber));
52336
52449
  const limitedEvents = allEvents.slice(0, limit);
@@ -52384,7 +52497,9 @@ var ValidatorHistoryAction = class extends StakingAction {
52384
52497
  console.log(source_default.bold(`History for ${validatorAddress}`));
52385
52498
  console.log(table.toString());
52386
52499
  console.log("");
52500
+ const epochRangeInfo = minEpoch !== null ? `epochs ${minEpoch}-${currentEpoch}` : options.fromBlock ? `from block ${options.fromBlock}` : "all epochs";
52387
52501
  console.log(source_default.gray("Summary:"));
52502
+ console.log(source_default.gray(` Range: ${epochRangeInfo}`));
52388
52503
  console.log(source_default.gray(` Slash events: ${slashEvents.length}`));
52389
52504
  console.log(source_default.gray(` Reward events: ${rewardEvents.length}`));
52390
52505
  console.log(source_default.gray(` Total validator rewards: ${client.formatStakingAmount(totalValidatorRewards)}`));
@@ -52392,6 +52507,7 @@ var ValidatorHistoryAction = class extends StakingAction {
52392
52507
  if (allEvents.length > limit) {
52393
52508
  console.log(source_default.gray(` (showing ${limit} of ${allEvents.length} events)`));
52394
52509
  }
52510
+ console.log(source_default.gray(` Use --all to fetch complete history, --epochs <n> for last N epochs`));
52395
52511
  console.log("");
52396
52512
  } catch (error) {
52397
52513
  this.failSpinner("Failed to get validator history", error.message || error);
@@ -53084,6 +53200,10 @@ function initializeStakingCommands(program2) {
53084
53200
  const action = new ValidatorPrimeAction();
53085
53201
  await action.execute({ ...options, validator });
53086
53202
  });
53203
+ 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) => {
53204
+ const action = new ValidatorPrimeAction();
53205
+ await action.primeAll(options);
53206
+ });
53087
53207
  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) => {
53088
53208
  const validator = validatorArg || options.validator;
53089
53209
  const operator = operatorArg || options.operator;
@@ -53130,7 +53250,7 @@ function initializeStakingCommands(program2) {
53130
53250
  const action = new DelegatorClaimAction();
53131
53251
  await action.execute({ ...options, validator });
53132
53252
  });
53133
- 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) => {
53253
+ 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) => {
53134
53254
  const validator = validatorArg || options.validator;
53135
53255
  const action = new StakingInfoAction();
53136
53256
  await action.getValidatorInfo({ ...options, validator });
@@ -53164,7 +53284,7 @@ function initializeStakingCommands(program2) {
53164
53284
  const action = new StakingInfoAction();
53165
53285
  await action.listValidators(options);
53166
53286
  });
53167
- staking.command("validator-history [validator]").description("Show slash and reward history for a validator").option("--validator <address>", "Validator address (deprecated, use positional arg)").option("--from-block <block>", "Start from this block number").option("--limit <count>", "Maximum number of events to show (default: 50)").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) => {
53287
+ staking.command("validator-history [validator]").description("Show slash and reward history for a validator (default: last 10 epochs)").option("--validator <address>", "Validator address (deprecated, use positional arg)").option("--epochs <count>", "Number of recent epochs to fetch (default: 10)").option("--from-epoch <epoch>", "Start from this epoch number").option("--from-block <block>", "Start from this block number (advanced)").option("--all", "Fetch complete history from genesis (slow)").option("--limit <count>", "Maximum number of events to show (default: 50)").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) => {
53168
53288
  const validator = validatorArg || options.validator;
53169
53289
  const action = new ValidatorHistoryAction();
53170
53290
  await action.execute({ ...options, validator });
@@ -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.32.6",
3
+ "version": "0.33.0",
4
4
  "description": "GenLayer Command Line Tool",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -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("Continuing...");
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();
@@ -312,9 +325,12 @@ export function initializeStakingCommands(program: Command) {
312
325
 
313
326
  staking
314
327
  .command("validator-history [validator]")
315
- .description("Show slash and reward history for a validator")
328
+ .description("Show slash and reward history for a validator (default: last 10 epochs)")
316
329
  .option("--validator <address>", "Validator address (deprecated, use positional arg)")
317
- .option("--from-block <block>", "Start from this block number")
330
+ .option("--epochs <count>", "Number of recent epochs to fetch (default: 10)")
331
+ .option("--from-epoch <epoch>", "Start from this epoch number")
332
+ .option("--from-block <block>", "Start from this block number (advanced)")
333
+ .option("--all", "Fetch complete history from genesis (slow)")
318
334
  .option("--limit <count>", "Maximum number of events to show (default: 50)")
319
335
  .option("--account <name>", "Account to use (for default validator address)")
320
336
  .option("--network <network>", "Network to use (localnet, testnet-asimov)")
@@ -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
- // Filter to only truly pending deposits (not yet active)
56
- const pending = info.pendingDeposits.filter(d => d.epoch + ACTIVATION_DELAY_EPOCHS > currentEpoch);
57
- return pending.length > 0
58
- ? pending.map(d => {
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
- epochsRemaining: epochsUntilActive.toString(),
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
- // Fetch all data in parallel
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
- // Combine all validators
378
- const allAddresses = new Set([
379
- ...activeAddresses,
380
- ...quarantinedList.map(v => v.validator),
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.size} validators...`);
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 = activeAddresses.some(a => a.toLowerCase() === addrLower);
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
  ]);
@@ -30,7 +30,10 @@ const REWARD_EVENT_ABI = {
30
30
  export interface ValidatorHistoryOptions extends StakingConfig {
31
31
  validator?: string;
32
32
  fromBlock?: string;
33
+ fromEpoch?: string;
34
+ epochs?: string;
33
35
  limit?: string;
36
+ all?: boolean;
34
37
  }
35
38
 
36
39
  interface SlashEvent {
@@ -107,7 +110,30 @@ export class ValidatorHistoryAction extends StakingAction {
107
110
  transport: http(chain.rpcUrls.default.http[0]),
108
111
  });
109
112
 
110
- const fromBlock = options.fromBlock ? BigInt(options.fromBlock) : 0n;
113
+ // Determine epoch range for filtering
114
+ const epochInfo = await client.getEpochInfo();
115
+ const currentEpoch = epochInfo.currentEpoch;
116
+ const defaultEpochs = 10n;
117
+
118
+ let minEpoch: bigint | null = null;
119
+ let fromBlock: bigint | "earliest" = "earliest";
120
+
121
+ if (options.fromBlock) {
122
+ // Explicit block takes precedence
123
+ fromBlock = BigInt(options.fromBlock);
124
+ } else if (options.fromEpoch) {
125
+ // Filter by starting epoch
126
+ minEpoch = BigInt(options.fromEpoch);
127
+ } else if (options.all) {
128
+ // Fetch all history (warn user)
129
+ console.log(chalk.yellow("Warning: Fetching all history from genesis. This may be slow for long-lived validators."));
130
+ console.log(chalk.yellow("Consider using --epochs <n> or --from-epoch <n> for faster queries.\n"));
131
+ } else {
132
+ // Default: last N epochs
133
+ const numEpochs = options.epochs ? BigInt(options.epochs) : defaultEpochs;
134
+ minEpoch = currentEpoch > numEpochs ? currentEpoch - numEpochs : 0n;
135
+ }
136
+
111
137
  const limit = options.limit ? parseInt(options.limit) : 50;
112
138
 
113
139
  this.setSpinnerText("Fetching slash events...");
@@ -156,7 +182,7 @@ export class ValidatorHistoryAction extends StakingAction {
156
182
  }
157
183
 
158
184
  // Transform to typed events
159
- const slashEvents: SlashEvent[] = slashLogs.map(log => ({
185
+ let slashEvents: SlashEvent[] = slashLogs.map(log => ({
160
186
  type: "slash" as const,
161
187
  epoch: (log.args as any).epoch as bigint,
162
188
  txId: (log.args as any).txId as string,
@@ -165,7 +191,7 @@ export class ValidatorHistoryAction extends StakingAction {
165
191
  timestamp: blockTimestamps.get(log.blockNumber) || new Date(0),
166
192
  }));
167
193
 
168
- const rewardEvents: RewardEvent[] = filteredRewardLogs.map(log => ({
194
+ let rewardEvents: RewardEvent[] = filteredRewardLogs.map(log => ({
169
195
  type: "reward" as const,
170
196
  epoch: (log.args as any).epoch as bigint,
171
197
  validatorRewards: (log.args as any).validatorRewards as bigint,
@@ -174,6 +200,12 @@ export class ValidatorHistoryAction extends StakingAction {
174
200
  timestamp: blockTimestamps.get(log.blockNumber) || new Date(0),
175
201
  }));
176
202
 
203
+ // Filter by epoch if specified
204
+ if (minEpoch !== null) {
205
+ slashEvents = slashEvents.filter(e => e.epoch >= minEpoch!);
206
+ rewardEvents = rewardEvents.filter(e => e.epoch >= minEpoch!);
207
+ }
208
+
177
209
  // Combine and sort by block number descending
178
210
  const allEvents: HistoryEvent[] = [...slashEvents, ...rewardEvents];
179
211
  allEvents.sort((a, b) => Number(b.blockNumber - a.blockNumber));
@@ -243,7 +275,13 @@ export class ValidatorHistoryAction extends StakingAction {
243
275
  console.log("");
244
276
 
245
277
  // Summary
278
+ const epochRangeInfo = minEpoch !== null
279
+ ? `epochs ${minEpoch}-${currentEpoch}`
280
+ : options.fromBlock
281
+ ? `from block ${options.fromBlock}`
282
+ : "all epochs";
246
283
  console.log(chalk.gray("Summary:"));
284
+ console.log(chalk.gray(` Range: ${epochRangeInfo}`));
247
285
  console.log(chalk.gray(` Slash events: ${slashEvents.length}`));
248
286
  console.log(chalk.gray(` Reward events: ${rewardEvents.length}`));
249
287
  console.log(chalk.gray(` Total validator rewards: ${client.formatStakingAmount(totalValidatorRewards)}`));
@@ -251,6 +289,7 @@ export class ValidatorHistoryAction extends StakingAction {
251
289
  if (allEvents.length > limit) {
252
290
  console.log(chalk.gray(` (showing ${limit} of ${allEvents.length} events)`));
253
291
  }
292
+ console.log(chalk.gray(` Use --all to fetch complete history, --epochs <n> for last N epochs`));
254
293
  console.log("");
255
294
  } catch (error: any) {
256
295
  this.failSpinner("Failed to get validator history", error.message || error);
@@ -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
  }