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 +6 -0
- package/README.md +10 -2
- package/dist/index.js +116 -18
- package/docs/validator-guide.md +27 -0
- package/package.json +1 -1
- package/src/commands/staking/StakingAction.ts +73 -2
- package/src/commands/staking/index.ts +13 -0
- package/src/commands/staking/stakingInfo.ts +36 -18
- package/src/commands/staking/validatorPrime.ts +38 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,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.
|
|
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("
|
|
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
|
|
51832
|
-
return
|
|
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
|
|
52084
|
-
|
|
52085
|
-
|
|
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 =
|
|
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 });
|
package/docs/validator-guide.md
CHANGED
|
@@ -235,6 +235,23 @@ Output will include:
|
|
|
235
235
|
|
|
236
236
|
## Managing Your Validator
|
|
237
237
|
|
|
238
|
+
### Priming Your Validator
|
|
239
|
+
|
|
240
|
+
Validators must be "primed" each epoch to participate in consensus. Priming updates the validator's stake record for the upcoming epoch.
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
# Prime your validator
|
|
244
|
+
genlayer staking validator-prime 0xYourValidator...
|
|
245
|
+
|
|
246
|
+
# Or prime all validators at once (anyone can do this)
|
|
247
|
+
genlayer staking prime-all
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The `validators` command shows priming status:
|
|
251
|
+
- **Green** `e11` - Primed for current epoch
|
|
252
|
+
- **Yellow** `e10` - Needs priming before next epoch
|
|
253
|
+
- **Red** `e9!` - Urgently needs priming (behind)
|
|
254
|
+
|
|
238
255
|
### Add More Stake
|
|
239
256
|
|
|
240
257
|
```bash
|
|
@@ -247,6 +264,16 @@ genlayer staking validator-deposit --validator 0xYourValidatorWallet... --amount
|
|
|
247
264
|
genlayer staking active-validators
|
|
248
265
|
```
|
|
249
266
|
|
|
267
|
+
### View Validator Set
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
# Show all validators with stake, primed status, and weight
|
|
271
|
+
genlayer staking validators
|
|
272
|
+
|
|
273
|
+
# Include banned validators
|
|
274
|
+
genlayer staking validators --all
|
|
275
|
+
```
|
|
276
|
+
|
|
250
277
|
### Exit as Validator
|
|
251
278
|
|
|
252
279
|
```bash
|
package/package.json
CHANGED
|
@@ -2,10 +2,21 @@ import {BaseAction, BUILT_IN_NETWORKS, resolveNetwork} from "../../lib/actions/B
|
|
|
2
2
|
import {createClient, createAccount, formatStakingAmount, parseStakingAmount, abi} from "genlayer-js";
|
|
3
3
|
import type {GenLayerClient, GenLayerChain, Address} from "genlayer-js/types";
|
|
4
4
|
import {readFileSync, existsSync} from "fs";
|
|
5
|
-
import {ethers} from "ethers";
|
|
5
|
+
import {ethers, ZeroAddress} from "ethers";
|
|
6
6
|
import {createPublicClient, createWalletClient, http, type PublicClient, type WalletClient, type Chain, type Account} from "viem";
|
|
7
7
|
import {privateKeyToAccount} from "viem/accounts";
|
|
8
8
|
|
|
9
|
+
// Extended ABI for tree traversal (not in SDK)
|
|
10
|
+
const STAKING_TREE_ABI = [
|
|
11
|
+
{
|
|
12
|
+
name: "validatorsRoot",
|
|
13
|
+
type: "function",
|
|
14
|
+
stateMutability: "view",
|
|
15
|
+
inputs: [],
|
|
16
|
+
outputs: [{name: "", type: "address"}],
|
|
17
|
+
},
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
9
20
|
// Re-export for use by other staking commands
|
|
10
21
|
export {BUILT_IN_NETWORKS};
|
|
11
22
|
|
|
@@ -132,7 +143,7 @@ export class StakingAction extends BaseAction {
|
|
|
132
143
|
// Stop spinner before prompting for password
|
|
133
144
|
this.stopSpinner();
|
|
134
145
|
const password = await this.promptPassword(`Enter password to unlock account '${accountName}':`);
|
|
135
|
-
this.startSpinner("
|
|
146
|
+
this.startSpinner("Unlocking account...");
|
|
136
147
|
|
|
137
148
|
const wallet = await ethers.Wallet.fromEncryptedJson(keystoreJson, password);
|
|
138
149
|
return wallet.privateKey;
|
|
@@ -193,4 +204,64 @@ export class StakingAction extends BaseAction {
|
|
|
193
204
|
signerAddress: account.address as Address,
|
|
194
205
|
};
|
|
195
206
|
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get all validators by traversing the validator tree.
|
|
210
|
+
* This finds ALL validators including those not yet active/primed.
|
|
211
|
+
*/
|
|
212
|
+
protected async getAllValidatorsFromTree(config: StakingConfig): Promise<Address[]> {
|
|
213
|
+
const network = this.getNetwork(config);
|
|
214
|
+
const rpcUrl = config.rpc || network.rpcUrls.default.http[0];
|
|
215
|
+
const stakingAddress = config.stakingAddress || network.stakingContract?.address;
|
|
216
|
+
|
|
217
|
+
if (!stakingAddress) {
|
|
218
|
+
throw new Error("Staking contract address not configured");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const publicClient = createPublicClient({
|
|
222
|
+
chain: network,
|
|
223
|
+
transport: http(rpcUrl),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Get the root of the validator tree
|
|
227
|
+
const root = await publicClient.readContract({
|
|
228
|
+
address: stakingAddress as `0x${string}`,
|
|
229
|
+
abi: STAKING_TREE_ABI,
|
|
230
|
+
functionName: "validatorsRoot",
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (root === ZeroAddress) {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const validators: Address[] = [];
|
|
238
|
+
const stack: string[] = [root as string];
|
|
239
|
+
const visited = new Set<string>();
|
|
240
|
+
|
|
241
|
+
// Use validatorView from SDK's ABI (has left/right fields)
|
|
242
|
+
while (stack.length > 0) {
|
|
243
|
+
const addr = stack.pop()!;
|
|
244
|
+
|
|
245
|
+
if (addr === ZeroAddress || visited.has(addr.toLowerCase())) continue;
|
|
246
|
+
visited.add(addr.toLowerCase());
|
|
247
|
+
|
|
248
|
+
validators.push(addr as Address);
|
|
249
|
+
|
|
250
|
+
const info = await publicClient.readContract({
|
|
251
|
+
address: stakingAddress as `0x${string}`,
|
|
252
|
+
abi: abi.STAKING_ABI,
|
|
253
|
+
functionName: "validatorView",
|
|
254
|
+
args: [addr as `0x${string}`],
|
|
255
|
+
}) as {left: string; right: string};
|
|
256
|
+
|
|
257
|
+
if (info.left !== ZeroAddress) {
|
|
258
|
+
stack.push(info.left);
|
|
259
|
+
}
|
|
260
|
+
if (info.right !== ZeroAddress) {
|
|
261
|
+
stack.push(info.right);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return validators;
|
|
266
|
+
}
|
|
196
267
|
}
|
|
@@ -116,6 +116,18 @@ export function initializeStakingCommands(program: Command) {
|
|
|
116
116
|
await action.execute({...options, validator});
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
+
staking
|
|
120
|
+
.command("prime-all")
|
|
121
|
+
.description("Prime all validators that need priming")
|
|
122
|
+
.option("--account <name>", "Account to use (pays gas)")
|
|
123
|
+
.option("--network <network>", "Network to use (localnet, testnet-asimov)")
|
|
124
|
+
.option("--rpc <rpcUrl>", "RPC URL for the network")
|
|
125
|
+
.option("--staking-address <address>", "Staking contract address (overrides chain config)")
|
|
126
|
+
.action(async (options: StakingConfig) => {
|
|
127
|
+
const action = new ValidatorPrimeAction();
|
|
128
|
+
await action.primeAll(options);
|
|
129
|
+
});
|
|
130
|
+
|
|
119
131
|
staking
|
|
120
132
|
.command("set-operator [validator] [operator]")
|
|
121
133
|
.description("Change the operator address for a validator wallet")
|
|
@@ -228,6 +240,7 @@ export function initializeStakingCommands(program: Command) {
|
|
|
228
240
|
.option("--network <network>", "Network to use (localnet, testnet-asimov)")
|
|
229
241
|
.option("--rpc <rpcUrl>", "RPC URL for the network")
|
|
230
242
|
.option("--staking-address <address>", "Staking contract address (overrides chain config)")
|
|
243
|
+
.option("--debug", "Show raw unfiltered pending deposits/withdrawals")
|
|
231
244
|
.action(async (validatorArg: string | undefined, options: StakingInfoOptions) => {
|
|
232
245
|
const validator = validatorArg || options.validator;
|
|
233
246
|
const action = new StakingInfoAction();
|
|
@@ -9,6 +9,7 @@ const UNBONDING_PERIOD_EPOCHS = 7n;
|
|
|
9
9
|
|
|
10
10
|
export interface StakingInfoOptions extends StakingConfig {
|
|
11
11
|
validator?: string;
|
|
12
|
+
debug?: boolean;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export class StakingInfoAction extends StakingAction {
|
|
@@ -38,6 +39,7 @@ export class StakingInfoAction extends StakingAction {
|
|
|
38
39
|
const currentEpoch = epochInfo.currentEpoch;
|
|
39
40
|
|
|
40
41
|
const result: Record<string, any> = {
|
|
42
|
+
...(options.debug && {currentEpoch: currentEpoch.toString()}),
|
|
41
43
|
validator: info.address,
|
|
42
44
|
owner: info.owner,
|
|
43
45
|
operator: info.operator,
|
|
@@ -52,22 +54,27 @@ export class StakingInfoAction extends StakingAction {
|
|
|
52
54
|
live: info.live,
|
|
53
55
|
banned: info.banned ? info.bannedEpoch?.toString() : "Not banned",
|
|
54
56
|
selfStakePendingDeposits: (() => {
|
|
55
|
-
//
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
// In debug mode, show all deposits; otherwise filter to truly pending only
|
|
58
|
+
const deposits = options.debug
|
|
59
|
+
? info.pendingDeposits
|
|
60
|
+
: info.pendingDeposits.filter(d => d.epoch + ACTIVATION_DELAY_EPOCHS > currentEpoch);
|
|
61
|
+
return deposits.length > 0
|
|
62
|
+
? deposits.map(d => {
|
|
59
63
|
const depositEpoch = d.epoch;
|
|
60
64
|
const activationEpoch = depositEpoch + ACTIVATION_DELAY_EPOCHS;
|
|
61
65
|
const epochsUntilActive = activationEpoch - currentEpoch;
|
|
66
|
+
const isActivated = epochsUntilActive <= 0n;
|
|
62
67
|
return {
|
|
63
68
|
epoch: depositEpoch.toString(),
|
|
64
69
|
stake: d.stake,
|
|
65
70
|
shares: d.shares.toString(),
|
|
66
71
|
activatesAtEpoch: activationEpoch.toString(),
|
|
67
|
-
|
|
72
|
+
...(options.debug
|
|
73
|
+
? {status: isActivated ? "ACTIVATED" : `pending (${epochsUntilActive} epochs)`}
|
|
74
|
+
: {epochsRemaining: epochsUntilActive.toString()}),
|
|
68
75
|
};
|
|
69
76
|
})
|
|
70
|
-
: "None";
|
|
77
|
+
: options.debug ? `None (raw count: ${info.pendingDeposits.length})` : "None";
|
|
71
78
|
})(),
|
|
72
79
|
selfStakePendingWithdrawals:
|
|
73
80
|
info.pendingWithdrawals.length > 0
|
|
@@ -362,7 +369,10 @@ export class StakingInfoAction extends StakingAction {
|
|
|
362
369
|
// No account configured, that's fine
|
|
363
370
|
}
|
|
364
371
|
|
|
365
|
-
//
|
|
372
|
+
// Use tree traversal to get ALL validators (including not-yet-primed)
|
|
373
|
+
const allTreeAddresses = await this.getAllValidatorsFromTree(options);
|
|
374
|
+
|
|
375
|
+
// Also fetch status lists in parallel
|
|
366
376
|
const [activeAddresses, quarantinedList, bannedList, epochInfo] = await Promise.all([
|
|
367
377
|
client.getActiveValidators(),
|
|
368
378
|
client.getQuarantinedValidatorsDetailed(),
|
|
@@ -373,15 +383,14 @@ export class StakingInfoAction extends StakingAction {
|
|
|
373
383
|
// Build set of quarantined/banned for status lookup
|
|
374
384
|
const quarantinedSet = new Map(quarantinedList.map(v => [v.validator.toLowerCase(), v]));
|
|
375
385
|
const bannedSet = new Map(bannedList.map(v => [v.validator.toLowerCase(), v]));
|
|
386
|
+
const activeSet = new Set(activeAddresses.map(a => a.toLowerCase()));
|
|
376
387
|
|
|
377
|
-
//
|
|
378
|
-
const allAddresses =
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
...(options.all ? bannedList.map(v => v.validator) : []),
|
|
382
|
-
]);
|
|
388
|
+
// Filter out banned if not --all
|
|
389
|
+
const allAddresses = options.all
|
|
390
|
+
? allTreeAddresses
|
|
391
|
+
: allTreeAddresses.filter(addr => !bannedSet.has(addr.toLowerCase()));
|
|
383
392
|
|
|
384
|
-
this.setSpinnerText(`Fetching details for ${allAddresses.
|
|
393
|
+
this.setSpinnerText(`Fetching details for ${allAddresses.length} validators...`);
|
|
385
394
|
|
|
386
395
|
// Fetch detailed info in batches to avoid rate limiting
|
|
387
396
|
const BATCH_SIZE = 5;
|
|
@@ -411,7 +420,7 @@ export class StakingInfoAction extends StakingAction {
|
|
|
411
420
|
const addrLower = info.address.toLowerCase();
|
|
412
421
|
const isQuarantined = quarantinedSet.has(addrLower);
|
|
413
422
|
const isBanned = bannedSet.has(addrLower);
|
|
414
|
-
const isActive =
|
|
423
|
+
const isActive = activeSet.has(addrLower);
|
|
415
424
|
|
|
416
425
|
let status = "";
|
|
417
426
|
if (isBanned) {
|
|
@@ -420,8 +429,6 @@ export class StakingInfoAction extends StakingAction {
|
|
|
420
429
|
} else if (isQuarantined) {
|
|
421
430
|
const qInfo = quarantinedSet.get(addrLower)!;
|
|
422
431
|
status = `quarant(e${qInfo.untilEpoch})`;
|
|
423
|
-
} else if (info.needsPriming) {
|
|
424
|
-
status = "prime!";
|
|
425
432
|
} else if (isActive) {
|
|
426
433
|
status = "active";
|
|
427
434
|
} else {
|
|
@@ -485,6 +492,7 @@ export class StakingInfoAction extends StakingAction {
|
|
|
485
492
|
chalk.cyan("Self"),
|
|
486
493
|
chalk.cyan("Deleg"),
|
|
487
494
|
chalk.cyan("Pending"),
|
|
495
|
+
chalk.cyan("Primed"),
|
|
488
496
|
chalk.cyan("Weight"),
|
|
489
497
|
chalk.cyan("Status"),
|
|
490
498
|
],
|
|
@@ -533,13 +541,22 @@ export class StakingInfoAction extends StakingAction {
|
|
|
533
541
|
? `${moniker}${roleTag}\n${chalk.gray(info.address)}`
|
|
534
542
|
: `${chalk.gray(info.address)}${roleTag}`;
|
|
535
543
|
|
|
544
|
+
// Primed status - color based on how current it is
|
|
545
|
+
let primedStr: string;
|
|
546
|
+
if (info.ePrimed >= currentEpoch) {
|
|
547
|
+
primedStr = chalk.green(`e${info.ePrimed}`);
|
|
548
|
+
} else if (info.ePrimed === currentEpoch - 1n) {
|
|
549
|
+
primedStr = chalk.yellow(`e${info.ePrimed}`);
|
|
550
|
+
} else {
|
|
551
|
+
primedStr = chalk.red(`e${info.ePrimed}!`);
|
|
552
|
+
}
|
|
553
|
+
|
|
536
554
|
// Status coloring
|
|
537
555
|
let statusStr = status;
|
|
538
556
|
if (status === "active") statusStr = chalk.green(status);
|
|
539
557
|
else if (status === "BANNED") statusStr = chalk.red(status);
|
|
540
558
|
else if (status.startsWith("quarant")) statusStr = chalk.yellow(status);
|
|
541
559
|
else if (status.startsWith("banned")) statusStr = chalk.red(status);
|
|
542
|
-
else if (status === "prime!") statusStr = chalk.magenta(status);
|
|
543
560
|
else if (status === "pending") statusStr = chalk.gray(status);
|
|
544
561
|
|
|
545
562
|
table.push([
|
|
@@ -548,6 +565,7 @@ export class StakingInfoAction extends StakingAction {
|
|
|
548
565
|
formatStake(info.vStake),
|
|
549
566
|
formatStake(info.dStake),
|
|
550
567
|
pendingStr,
|
|
568
|
+
primedStr,
|
|
551
569
|
weightStr,
|
|
552
570
|
statusStr,
|
|
553
571
|
]);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {StakingAction, StakingConfig} from "./StakingAction";
|
|
2
2
|
import type {Address} from "genlayer-js/types";
|
|
3
|
+
import chalk from "chalk";
|
|
3
4
|
|
|
4
5
|
export interface ValidatorPrimeOptions extends StakingConfig {
|
|
5
6
|
validator: string;
|
|
@@ -32,4 +33,41 @@ export class ValidatorPrimeAction extends StakingAction {
|
|
|
32
33
|
this.failSpinner("Failed to prime validator", error.message || error);
|
|
33
34
|
}
|
|
34
35
|
}
|
|
36
|
+
|
|
37
|
+
async primeAll(options: StakingConfig): Promise<void> {
|
|
38
|
+
this.startSpinner("Fetching validators...");
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const client = await this.getStakingClient(options);
|
|
42
|
+
|
|
43
|
+
// Get all validators from tree
|
|
44
|
+
this.setSpinnerText("Fetching validators...");
|
|
45
|
+
const allValidators = await this.getAllValidatorsFromTree(options);
|
|
46
|
+
|
|
47
|
+
this.stopSpinner();
|
|
48
|
+
console.log(`\nPriming ${allValidators.length} validators:\n`);
|
|
49
|
+
|
|
50
|
+
let succeeded = 0;
|
|
51
|
+
let skipped = 0;
|
|
52
|
+
|
|
53
|
+
for (const addr of allValidators) {
|
|
54
|
+
process.stdout.write(` ${addr} ... `);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const result = await client.validatorPrime({validator: addr});
|
|
58
|
+
console.log(chalk.green(`primed ${result.transactionHash}`));
|
|
59
|
+
succeeded++;
|
|
60
|
+
} catch (error: any) {
|
|
61
|
+
const msg = error.message || String(error);
|
|
62
|
+
const shortErr = msg.length > 60 ? msg.slice(0, 57) + "..." : msg;
|
|
63
|
+
console.log(chalk.gray(`skipped: ${shortErr}`));
|
|
64
|
+
skipped++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(`\n${chalk.green(`${succeeded} primed`)}, ${chalk.gray(`${skipped} skipped`)}\n`);
|
|
69
|
+
} catch (error: any) {
|
|
70
|
+
this.failSpinner("Failed to prime validators", error.message || error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
35
73
|
}
|