genlayer 0.32.6 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -1
- package/README.md +20 -9
- package/dist/index.js +142 -22
- 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 +18 -2
- package/src/commands/staking/stakingInfo.ts +36 -18
- package/src/commands/staking/validatorHistory.ts +42 -3
- package/src/commands/staking/validatorPrime.ts +38 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.33.0 (2026-01-13)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* validator tree traversal, primed column, prime-all command ([#270](https://github.com/yeagerai/genlayer-cli/issues/270)) ([75b9b06](https://github.com/yeagerai/genlayer-cli/commit/75b9b06a3bb80a24e7dabf422ceed44620765ccb))
|
|
8
|
+
|
|
9
|
+
## 0.32.8 (2025-12-12)
|
|
4
10
|
|
|
5
11
|
## 0.32.5 (2025-12-09)
|
|
6
12
|
|
package/README.md
CHANGED
|
@@ -306,14 +306,16 @@ COMMANDS:
|
|
|
306
306
|
validator-deposit [options] Make an additional deposit as a validator
|
|
307
307
|
validator-exit [options] Exit as a validator by withdrawing shares
|
|
308
308
|
validator-claim [options] Claim validator withdrawals after unbonding period
|
|
309
|
+
validator-prime [validator] Prime a validator for the next epoch
|
|
310
|
+
prime-all [options] Prime all validators that need priming
|
|
309
311
|
delegator-join [options] Join as a delegator by staking with a validator
|
|
310
312
|
delegator-exit [options] Exit as a delegator by withdrawing shares
|
|
311
313
|
delegator-claim [options] Claim delegator withdrawals after unbonding period
|
|
312
|
-
validator-info [validator] Get information about a validator
|
|
314
|
+
validator-info [validator] Get information about a validator (--debug for raw data)
|
|
313
315
|
validator-history [validator] Show slash and reward history for a validator
|
|
314
316
|
delegation-info [validator] Get delegation info for a delegator with a validator
|
|
315
317
|
epoch-info [options] Get current/previous epoch info (--epoch <n> for specific)
|
|
316
|
-
validators [options] Show validator set with stake, status, and weight
|
|
318
|
+
validators [options] Show validator set with stake, primed status, and weight
|
|
317
319
|
active-validators [options] List all active validators
|
|
318
320
|
quarantined-validators List all quarantined validators
|
|
319
321
|
banned-validators List all banned validators
|
|
@@ -399,20 +401,29 @@ EXAMPLES:
|
|
|
399
401
|
genlayer staking validators
|
|
400
402
|
genlayer staking validators --all # Include banned validators
|
|
401
403
|
|
|
402
|
-
# Show validator slash/reward history (testnet only)
|
|
404
|
+
# Show validator slash/reward history (testnet only, default: last 10 epochs)
|
|
403
405
|
genlayer staking validator-history 0x...
|
|
406
|
+
genlayer staking validator-history 0x... --epochs 5 # Last 5 epochs
|
|
407
|
+
genlayer staking validator-history 0x... --from-epoch 3 # From epoch 3
|
|
408
|
+
genlayer staking validator-history 0x... --all # Complete history (slow)
|
|
404
409
|
genlayer staking validator-history 0x... --limit 20
|
|
405
410
|
# Output:
|
|
406
|
-
#
|
|
407
|
-
# │ Time │ Epoch │ Type │ Details
|
|
408
|
-
#
|
|
409
|
-
# │ 12-11 14:20 │ 5 │ REWARD │ Val:
|
|
410
|
-
# │ 12-10 18:39 │ 4 │ SLASH │ 1.00%
|
|
411
|
-
#
|
|
411
|
+
# ┌─────────────┬───────┬────────┬────────┬────────────────────────────────────┐
|
|
412
|
+
# │ Time │ Epoch │ Type │ Details│ GL TxId / Block │
|
|
413
|
+
# ├─────────────┼───────┼────────┼────────┼────────────────────────────────────┤
|
|
414
|
+
# │ 12-11 14:20 │ 5 │ REWARD │ Val: …│ block 4725136 │
|
|
415
|
+
# │ 12-10 18:39 │ 4 │ SLASH │ 1.00% │ 0x52db90a9... │
|
|
416
|
+
# └─────────────┴───────┴────────┴────────┴────────────────────────────────────┘
|
|
412
417
|
|
|
413
418
|
# Exit and claim (requires validator wallet address)
|
|
414
419
|
genlayer staking validator-exit --validator 0x... --shares 100
|
|
415
420
|
genlayer staking validator-claim --validator 0x...
|
|
421
|
+
|
|
422
|
+
# Prime a validator for next epoch
|
|
423
|
+
genlayer staking validator-prime 0x...
|
|
424
|
+
|
|
425
|
+
# Prime all validators that need priming (anyone can call)
|
|
426
|
+
genlayer staking prime-all
|
|
416
427
|
```
|
|
417
428
|
|
|
418
429
|
### Running the CLI from the repository
|
package/dist/index.js
CHANGED
|
@@ -20078,7 +20078,7 @@ var require_cli_table3 = __commonJS({
|
|
|
20078
20078
|
import { program } from "commander";
|
|
20079
20079
|
|
|
20080
20080
|
// package.json
|
|
20081
|
-
var version = "0.
|
|
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
|
]);
|
|
@@ -52281,7 +52375,22 @@ var ValidatorHistoryAction = class extends StakingAction {
|
|
|
52281
52375
|
chain,
|
|
52282
52376
|
transport: http3(chain.rpcUrls.default.http[0])
|
|
52283
52377
|
});
|
|
52284
|
-
const
|
|
52378
|
+
const epochInfo = await client.getEpochInfo();
|
|
52379
|
+
const currentEpoch = epochInfo.currentEpoch;
|
|
52380
|
+
const defaultEpochs = 10n;
|
|
52381
|
+
let minEpoch = null;
|
|
52382
|
+
let fromBlock = "earliest";
|
|
52383
|
+
if (options.fromBlock) {
|
|
52384
|
+
fromBlock = BigInt(options.fromBlock);
|
|
52385
|
+
} else if (options.fromEpoch) {
|
|
52386
|
+
minEpoch = BigInt(options.fromEpoch);
|
|
52387
|
+
} else if (options.all) {
|
|
52388
|
+
console.log(source_default.yellow("Warning: Fetching all history from genesis. This may be slow for long-lived validators."));
|
|
52389
|
+
console.log(source_default.yellow("Consider using --epochs <n> or --from-epoch <n> for faster queries.\n"));
|
|
52390
|
+
} else {
|
|
52391
|
+
const numEpochs = options.epochs ? BigInt(options.epochs) : defaultEpochs;
|
|
52392
|
+
minEpoch = currentEpoch > numEpochs ? currentEpoch - numEpochs : 0n;
|
|
52393
|
+
}
|
|
52285
52394
|
const limit = options.limit ? parseInt(options.limit) : 50;
|
|
52286
52395
|
this.setSpinnerText("Fetching slash events...");
|
|
52287
52396
|
const slashLogs = await publicClient.getLogs({
|
|
@@ -52315,7 +52424,7 @@ var ValidatorHistoryAction = class extends StakingAction {
|
|
|
52315
52424
|
blockTimestamps.set(block.number, new Date(Number(block.timestamp) * 1e3));
|
|
52316
52425
|
});
|
|
52317
52426
|
}
|
|
52318
|
-
|
|
52427
|
+
let slashEvents = slashLogs.map((log) => ({
|
|
52319
52428
|
type: "slash",
|
|
52320
52429
|
epoch: log.args.epoch,
|
|
52321
52430
|
txId: log.args.txId,
|
|
@@ -52323,7 +52432,7 @@ var ValidatorHistoryAction = class extends StakingAction {
|
|
|
52323
52432
|
blockNumber: log.blockNumber,
|
|
52324
52433
|
timestamp: blockTimestamps.get(log.blockNumber) || /* @__PURE__ */ new Date(0)
|
|
52325
52434
|
}));
|
|
52326
|
-
|
|
52435
|
+
let rewardEvents = filteredRewardLogs.map((log) => ({
|
|
52327
52436
|
type: "reward",
|
|
52328
52437
|
epoch: log.args.epoch,
|
|
52329
52438
|
validatorRewards: log.args.validatorRewards,
|
|
@@ -52331,6 +52440,10 @@ var ValidatorHistoryAction = class extends StakingAction {
|
|
|
52331
52440
|
blockNumber: log.blockNumber,
|
|
52332
52441
|
timestamp: blockTimestamps.get(log.blockNumber) || /* @__PURE__ */ new Date(0)
|
|
52333
52442
|
}));
|
|
52443
|
+
if (minEpoch !== null) {
|
|
52444
|
+
slashEvents = slashEvents.filter((e2) => e2.epoch >= minEpoch);
|
|
52445
|
+
rewardEvents = rewardEvents.filter((e2) => e2.epoch >= minEpoch);
|
|
52446
|
+
}
|
|
52334
52447
|
const allEvents = [...slashEvents, ...rewardEvents];
|
|
52335
52448
|
allEvents.sort((a, b) => Number(b.blockNumber - a.blockNumber));
|
|
52336
52449
|
const limitedEvents = allEvents.slice(0, limit);
|
|
@@ -52384,7 +52497,9 @@ var ValidatorHistoryAction = class extends StakingAction {
|
|
|
52384
52497
|
console.log(source_default.bold(`History for ${validatorAddress}`));
|
|
52385
52498
|
console.log(table.toString());
|
|
52386
52499
|
console.log("");
|
|
52500
|
+
const epochRangeInfo = minEpoch !== null ? `epochs ${minEpoch}-${currentEpoch}` : options.fromBlock ? `from block ${options.fromBlock}` : "all epochs";
|
|
52387
52501
|
console.log(source_default.gray("Summary:"));
|
|
52502
|
+
console.log(source_default.gray(` Range: ${epochRangeInfo}`));
|
|
52388
52503
|
console.log(source_default.gray(` Slash events: ${slashEvents.length}`));
|
|
52389
52504
|
console.log(source_default.gray(` Reward events: ${rewardEvents.length}`));
|
|
52390
52505
|
console.log(source_default.gray(` Total validator rewards: ${client.formatStakingAmount(totalValidatorRewards)}`));
|
|
@@ -52392,6 +52507,7 @@ var ValidatorHistoryAction = class extends StakingAction {
|
|
|
52392
52507
|
if (allEvents.length > limit) {
|
|
52393
52508
|
console.log(source_default.gray(` (showing ${limit} of ${allEvents.length} events)`));
|
|
52394
52509
|
}
|
|
52510
|
+
console.log(source_default.gray(` Use --all to fetch complete history, --epochs <n> for last N epochs`));
|
|
52395
52511
|
console.log("");
|
|
52396
52512
|
} catch (error) {
|
|
52397
52513
|
this.failSpinner("Failed to get validator history", error.message || error);
|
|
@@ -53084,6 +53200,10 @@ function initializeStakingCommands(program2) {
|
|
|
53084
53200
|
const action = new ValidatorPrimeAction();
|
|
53085
53201
|
await action.execute({ ...options, validator });
|
|
53086
53202
|
});
|
|
53203
|
+
staking.command("prime-all").description("Prime all validators that need priming").option("--account <name>", "Account to use (pays gas)").option("--network <network>", "Network to use (localnet, testnet-asimov)").option("--rpc <rpcUrl>", "RPC URL for the network").option("--staking-address <address>", "Staking contract address (overrides chain config)").action(async (options) => {
|
|
53204
|
+
const action = new ValidatorPrimeAction();
|
|
53205
|
+
await action.primeAll(options);
|
|
53206
|
+
});
|
|
53087
53207
|
staking.command("set-operator [validator] [operator]").description("Change the operator address for a validator wallet").option("--validator <address>", "Validator wallet address (deprecated, use positional arg)").option("--operator <address>", "New operator address (deprecated, use positional arg)").option("--account <name>", "Account to use (must be validator owner)").option("--network <network>", "Network to use (localnet, testnet-asimov)").option("--rpc <rpcUrl>", "RPC URL for the network").action(async (validatorArg, operatorArg, options) => {
|
|
53088
53208
|
const validator = validatorArg || options.validator;
|
|
53089
53209
|
const operator = operatorArg || options.operator;
|
|
@@ -53130,7 +53250,7 @@ function initializeStakingCommands(program2) {
|
|
|
53130
53250
|
const action = new DelegatorClaimAction();
|
|
53131
53251
|
await action.execute({ ...options, validator });
|
|
53132
53252
|
});
|
|
53133
|
-
staking.command("validator-info [validator]").description("Get information about a validator").option("--validator <address>", "Validator address (deprecated, use positional arg)").option("--account <name>", "Account to use (for default validator address)").option("--network <network>", "Network to use (localnet, testnet-asimov)").option("--rpc <rpcUrl>", "RPC URL for the network").option("--staking-address <address>", "Staking contract address (overrides chain config)").action(async (validatorArg, options) => {
|
|
53253
|
+
staking.command("validator-info [validator]").description("Get information about a validator").option("--validator <address>", "Validator address (deprecated, use positional arg)").option("--account <name>", "Account to use (for default validator address)").option("--network <network>", "Network to use (localnet, testnet-asimov)").option("--rpc <rpcUrl>", "RPC URL for the network").option("--staking-address <address>", "Staking contract address (overrides chain config)").option("--debug", "Show raw unfiltered pending deposits/withdrawals").action(async (validatorArg, options) => {
|
|
53134
53254
|
const validator = validatorArg || options.validator;
|
|
53135
53255
|
const action = new StakingInfoAction();
|
|
53136
53256
|
await action.getValidatorInfo({ ...options, validator });
|
|
@@ -53164,7 +53284,7 @@ function initializeStakingCommands(program2) {
|
|
|
53164
53284
|
const action = new StakingInfoAction();
|
|
53165
53285
|
await action.listValidators(options);
|
|
53166
53286
|
});
|
|
53167
|
-
staking.command("validator-history [validator]").description("Show slash and reward history for a validator").option("--validator <address>", "Validator address (deprecated, use positional arg)").option("--from-block <block>", "Start from this block number").option("--limit <count>", "Maximum number of events to show (default: 50)").option("--account <name>", "Account to use (for default validator address)").option("--network <network>", "Network to use (localnet, testnet-asimov)").option("--rpc <rpcUrl>", "RPC URL for the network").option("--staking-address <address>", "Staking contract address (overrides chain config)").action(async (validatorArg, options) => {
|
|
53287
|
+
staking.command("validator-history [validator]").description("Show slash and reward history for a validator (default: last 10 epochs)").option("--validator <address>", "Validator address (deprecated, use positional arg)").option("--epochs <count>", "Number of recent epochs to fetch (default: 10)").option("--from-epoch <epoch>", "Start from this epoch number").option("--from-block <block>", "Start from this block number (advanced)").option("--all", "Fetch complete history from genesis (slow)").option("--limit <count>", "Maximum number of events to show (default: 50)").option("--account <name>", "Account to use (for default validator address)").option("--network <network>", "Network to use (localnet, testnet-asimov)").option("--rpc <rpcUrl>", "RPC URL for the network").option("--staking-address <address>", "Staking contract address (overrides chain config)").action(async (validatorArg, options) => {
|
|
53168
53288
|
const validator = validatorArg || options.validator;
|
|
53169
53289
|
const action = new ValidatorHistoryAction();
|
|
53170
53290
|
await action.execute({ ...options, validator });
|
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();
|
|
@@ -312,9 +325,12 @@ export function initializeStakingCommands(program: Command) {
|
|
|
312
325
|
|
|
313
326
|
staking
|
|
314
327
|
.command("validator-history [validator]")
|
|
315
|
-
.description("Show slash and reward history for a validator")
|
|
328
|
+
.description("Show slash and reward history for a validator (default: last 10 epochs)")
|
|
316
329
|
.option("--validator <address>", "Validator address (deprecated, use positional arg)")
|
|
317
|
-
.option("--
|
|
330
|
+
.option("--epochs <count>", "Number of recent epochs to fetch (default: 10)")
|
|
331
|
+
.option("--from-epoch <epoch>", "Start from this epoch number")
|
|
332
|
+
.option("--from-block <block>", "Start from this block number (advanced)")
|
|
333
|
+
.option("--all", "Fetch complete history from genesis (slow)")
|
|
318
334
|
.option("--limit <count>", "Maximum number of events to show (default: 50)")
|
|
319
335
|
.option("--account <name>", "Account to use (for default validator address)")
|
|
320
336
|
.option("--network <network>", "Network to use (localnet, testnet-asimov)")
|
|
@@ -9,6 +9,7 @@ const UNBONDING_PERIOD_EPOCHS = 7n;
|
|
|
9
9
|
|
|
10
10
|
export interface StakingInfoOptions extends StakingConfig {
|
|
11
11
|
validator?: string;
|
|
12
|
+
debug?: boolean;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export class StakingInfoAction extends StakingAction {
|
|
@@ -38,6 +39,7 @@ export class StakingInfoAction extends StakingAction {
|
|
|
38
39
|
const currentEpoch = epochInfo.currentEpoch;
|
|
39
40
|
|
|
40
41
|
const result: Record<string, any> = {
|
|
42
|
+
...(options.debug && {currentEpoch: currentEpoch.toString()}),
|
|
41
43
|
validator: info.address,
|
|
42
44
|
owner: info.owner,
|
|
43
45
|
operator: info.operator,
|
|
@@ -52,22 +54,27 @@ export class StakingInfoAction extends StakingAction {
|
|
|
52
54
|
live: info.live,
|
|
53
55
|
banned: info.banned ? info.bannedEpoch?.toString() : "Not banned",
|
|
54
56
|
selfStakePendingDeposits: (() => {
|
|
55
|
-
//
|
|
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
|
]);
|
|
@@ -30,7 +30,10 @@ const REWARD_EVENT_ABI = {
|
|
|
30
30
|
export interface ValidatorHistoryOptions extends StakingConfig {
|
|
31
31
|
validator?: string;
|
|
32
32
|
fromBlock?: string;
|
|
33
|
+
fromEpoch?: string;
|
|
34
|
+
epochs?: string;
|
|
33
35
|
limit?: string;
|
|
36
|
+
all?: boolean;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
interface SlashEvent {
|
|
@@ -107,7 +110,30 @@ export class ValidatorHistoryAction extends StakingAction {
|
|
|
107
110
|
transport: http(chain.rpcUrls.default.http[0]),
|
|
108
111
|
});
|
|
109
112
|
|
|
110
|
-
|
|
113
|
+
// Determine epoch range for filtering
|
|
114
|
+
const epochInfo = await client.getEpochInfo();
|
|
115
|
+
const currentEpoch = epochInfo.currentEpoch;
|
|
116
|
+
const defaultEpochs = 10n;
|
|
117
|
+
|
|
118
|
+
let minEpoch: bigint | null = null;
|
|
119
|
+
let fromBlock: bigint | "earliest" = "earliest";
|
|
120
|
+
|
|
121
|
+
if (options.fromBlock) {
|
|
122
|
+
// Explicit block takes precedence
|
|
123
|
+
fromBlock = BigInt(options.fromBlock);
|
|
124
|
+
} else if (options.fromEpoch) {
|
|
125
|
+
// Filter by starting epoch
|
|
126
|
+
minEpoch = BigInt(options.fromEpoch);
|
|
127
|
+
} else if (options.all) {
|
|
128
|
+
// Fetch all history (warn user)
|
|
129
|
+
console.log(chalk.yellow("Warning: Fetching all history from genesis. This may be slow for long-lived validators."));
|
|
130
|
+
console.log(chalk.yellow("Consider using --epochs <n> or --from-epoch <n> for faster queries.\n"));
|
|
131
|
+
} else {
|
|
132
|
+
// Default: last N epochs
|
|
133
|
+
const numEpochs = options.epochs ? BigInt(options.epochs) : defaultEpochs;
|
|
134
|
+
minEpoch = currentEpoch > numEpochs ? currentEpoch - numEpochs : 0n;
|
|
135
|
+
}
|
|
136
|
+
|
|
111
137
|
const limit = options.limit ? parseInt(options.limit) : 50;
|
|
112
138
|
|
|
113
139
|
this.setSpinnerText("Fetching slash events...");
|
|
@@ -156,7 +182,7 @@ export class ValidatorHistoryAction extends StakingAction {
|
|
|
156
182
|
}
|
|
157
183
|
|
|
158
184
|
// Transform to typed events
|
|
159
|
-
|
|
185
|
+
let slashEvents: SlashEvent[] = slashLogs.map(log => ({
|
|
160
186
|
type: "slash" as const,
|
|
161
187
|
epoch: (log.args as any).epoch as bigint,
|
|
162
188
|
txId: (log.args as any).txId as string,
|
|
@@ -165,7 +191,7 @@ export class ValidatorHistoryAction extends StakingAction {
|
|
|
165
191
|
timestamp: blockTimestamps.get(log.blockNumber) || new Date(0),
|
|
166
192
|
}));
|
|
167
193
|
|
|
168
|
-
|
|
194
|
+
let rewardEvents: RewardEvent[] = filteredRewardLogs.map(log => ({
|
|
169
195
|
type: "reward" as const,
|
|
170
196
|
epoch: (log.args as any).epoch as bigint,
|
|
171
197
|
validatorRewards: (log.args as any).validatorRewards as bigint,
|
|
@@ -174,6 +200,12 @@ export class ValidatorHistoryAction extends StakingAction {
|
|
|
174
200
|
timestamp: blockTimestamps.get(log.blockNumber) || new Date(0),
|
|
175
201
|
}));
|
|
176
202
|
|
|
203
|
+
// Filter by epoch if specified
|
|
204
|
+
if (minEpoch !== null) {
|
|
205
|
+
slashEvents = slashEvents.filter(e => e.epoch >= minEpoch!);
|
|
206
|
+
rewardEvents = rewardEvents.filter(e => e.epoch >= minEpoch!);
|
|
207
|
+
}
|
|
208
|
+
|
|
177
209
|
// Combine and sort by block number descending
|
|
178
210
|
const allEvents: HistoryEvent[] = [...slashEvents, ...rewardEvents];
|
|
179
211
|
allEvents.sort((a, b) => Number(b.blockNumber - a.blockNumber));
|
|
@@ -243,7 +275,13 @@ export class ValidatorHistoryAction extends StakingAction {
|
|
|
243
275
|
console.log("");
|
|
244
276
|
|
|
245
277
|
// Summary
|
|
278
|
+
const epochRangeInfo = minEpoch !== null
|
|
279
|
+
? `epochs ${minEpoch}-${currentEpoch}`
|
|
280
|
+
: options.fromBlock
|
|
281
|
+
? `from block ${options.fromBlock}`
|
|
282
|
+
: "all epochs";
|
|
246
283
|
console.log(chalk.gray("Summary:"));
|
|
284
|
+
console.log(chalk.gray(` Range: ${epochRangeInfo}`));
|
|
247
285
|
console.log(chalk.gray(` Slash events: ${slashEvents.length}`));
|
|
248
286
|
console.log(chalk.gray(` Reward events: ${rewardEvents.length}`));
|
|
249
287
|
console.log(chalk.gray(` Total validator rewards: ${client.formatStakingAmount(totalValidatorRewards)}`));
|
|
@@ -251,6 +289,7 @@ export class ValidatorHistoryAction extends StakingAction {
|
|
|
251
289
|
if (allEvents.length > limit) {
|
|
252
290
|
console.log(chalk.gray(` (showing ${limit} of ${allEvents.length} events)`));
|
|
253
291
|
}
|
|
292
|
+
console.log(chalk.gray(` Use --all to fetch complete history, --epochs <n> for last N epochs`));
|
|
254
293
|
console.log("");
|
|
255
294
|
} catch (error: any) {
|
|
256
295
|
this.failSpinner("Failed to get validator history", error.message || error);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {StakingAction, StakingConfig} from "./StakingAction";
|
|
2
2
|
import type {Address} from "genlayer-js/types";
|
|
3
|
+
import chalk from "chalk";
|
|
3
4
|
|
|
4
5
|
export interface ValidatorPrimeOptions extends StakingConfig {
|
|
5
6
|
validator: string;
|
|
@@ -32,4 +33,41 @@ export class ValidatorPrimeAction extends StakingAction {
|
|
|
32
33
|
this.failSpinner("Failed to prime validator", error.message || error);
|
|
33
34
|
}
|
|
34
35
|
}
|
|
36
|
+
|
|
37
|
+
async primeAll(options: StakingConfig): Promise<void> {
|
|
38
|
+
this.startSpinner("Fetching validators...");
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const client = await this.getStakingClient(options);
|
|
42
|
+
|
|
43
|
+
// Get all validators from tree
|
|
44
|
+
this.setSpinnerText("Fetching validators...");
|
|
45
|
+
const allValidators = await this.getAllValidatorsFromTree(options);
|
|
46
|
+
|
|
47
|
+
this.stopSpinner();
|
|
48
|
+
console.log(`\nPriming ${allValidators.length} validators:\n`);
|
|
49
|
+
|
|
50
|
+
let succeeded = 0;
|
|
51
|
+
let skipped = 0;
|
|
52
|
+
|
|
53
|
+
for (const addr of allValidators) {
|
|
54
|
+
process.stdout.write(` ${addr} ... `);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const result = await client.validatorPrime({validator: addr});
|
|
58
|
+
console.log(chalk.green(`primed ${result.transactionHash}`));
|
|
59
|
+
succeeded++;
|
|
60
|
+
} catch (error: any) {
|
|
61
|
+
const msg = error.message || String(error);
|
|
62
|
+
const shortErr = msg.length > 60 ? msg.slice(0, 57) + "..." : msg;
|
|
63
|
+
console.log(chalk.gray(`skipped: ${shortErr}`));
|
|
64
|
+
skipped++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(`\n${chalk.green(`${succeeded} primed`)}, ${chalk.gray(`${skipped} skipped`)}\n`);
|
|
69
|
+
} catch (error: any) {
|
|
70
|
+
this.failSpinner("Failed to prime validators", error.message || error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
35
73
|
}
|