genlayer 0.32.8 → 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,5 +1,11 @@
1
1
  # Changelog
2
2
 
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
+
3
9
  ## 0.32.8 (2025-12-12)
4
10
 
5
11
  ## 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.32.8";
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
  ]);
@@ -53106,6 +53200,10 @@ function initializeStakingCommands(program2) {
53106
53200
  const action = new ValidatorPrimeAction();
53107
53201
  await action.execute({ ...options, validator });
53108
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
+ });
53109
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) => {
53110
53208
  const validator = validatorArg || options.validator;
53111
53209
  const operator = operatorArg || options.operator;
@@ -53152,7 +53250,7 @@ function initializeStakingCommands(program2) {
53152
53250
  const action = new DelegatorClaimAction();
53153
53251
  await action.execute({ ...options, validator });
53154
53252
  });
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) => {
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) => {
53156
53254
  const validator = validatorArg || options.validator;
53157
53255
  const action = new StakingInfoAction();
53158
53256
  await action.getValidatorInfo({ ...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.8",
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();
@@ -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
  ]);
@@ -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
  }