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 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.32.8";
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.9",
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-V3MYVW3P.js
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://genlayer-testnet.rpc.caldera.xyz/http";
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: "0x03f410748EBdb4026a6b8299E9B6603A273709D1",
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: "0x67fd4aC71530FB220E0B7F90668BAF977B88fF07",
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: "0xB6E1316E57d47d82FDcEa5002028a554754EF243",
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("Continuing...");
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 pending = info.pendingDeposits.filter((d) => d.epoch + ACTIVATION_DELAY_EPOCHS > currentEpoch);
51832
- return pending.length > 0 ? pending.map((d) => {
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 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...`);
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 = activeAddresses.some((a) => a.toLowerCase() === addrLower);
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 });
@@ -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.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.9",
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("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
  }