From e5f5619000e92b57f5249b5d5864ef954208da4a Mon Sep 17 00:00:00 2001 From: Edgars Date: Thu, 11 Dec 2025 23:51:17 +0000 Subject: [PATCH 1/7] feat: improve epoch-info display with current/previous sections - Show current epoch with validators, weight, slashed - Show previous epoch with inflation, claimed, unclaimed, slashed - Add --epoch option to query specific epoch data - Clean text output instead of JSON - Update docs with new output format --- README.md | 30 ++- docs/validator-guide.md | 35 ++- package-lock.json | 67 +++++ package.json | 1 + src/commands/staking/index.ts | 174 +++++++++---- src/commands/staking/stakingInfo.ts | 301 +++++++++++++++++++++-- src/commands/staking/validatorHistory.ts | 259 +++++++++++++++++++ 7 files changed, 776 insertions(+), 91 deletions(-) create mode 100644 src/commands/staking/validatorHistory.ts diff --git a/README.md b/README.md index ca3865c4..cb534b6a 100644 --- a/README.md +++ b/README.md @@ -311,7 +311,7 @@ COMMANDS: delegator-claim [options] Claim delegator withdrawals after unbonding period validator-info [options] Get information about a validator delegation-info [options] Get delegation info for a delegator with a validator - epoch-info [options] Get epoch info with timing estimates + epoch-info [options] Get current/previous epoch info (--epoch for specific) active-validators [options] List all active validators COMMON OPTIONS (all commands): @@ -360,20 +360,24 @@ EXAMPLES: # banned: 'Not banned' # } - # Get current epoch info (includes timing estimates) + # Get current epoch info (shows current + previous epoch) genlayer staking epoch-info # Output: - # { - # currentEpoch: '2', - # epochStarted: '2025-11-28T09:57:49.000Z', - # epochEnded: 'Not ended', - # nextEpochEstimate: '2025-11-29T09:57:49.000Z', - # timeUntilNextEpoch: '19h 56m', - # minEpochDuration: '24h 0m', - # validatorMinStake: '0.01 GEN', - # delegatorMinStake: '42 GEN', - # activeValidatorsCount: '6' - # } + # ✔ Epoch info + # + # Current Epoch: 5 (started 9h 30m ago) + # Next Epoch: in 14h 30m + # Validators: 33 + # ... + # + # Previous Epoch: 4 (finalized) + # Inflation: 1732904.66 GEN + # Claimed: 0 GEN + # Unclaimed: 1732904.66 GEN + # ... + + # Query specific epoch data + genlayer staking epoch-info --epoch 4 # List active validators genlayer staking active-validators diff --git a/docs/validator-guide.md b/docs/validator-guide.md index 88fa9cd0..72246534 100644 --- a/docs/validator-guide.md +++ b/docs/validator-guide.md @@ -88,19 +88,28 @@ genlayer staking epoch-info Output: ``` -{ - currentEpoch: '2', - epochStarted: '2025-01-15T00:00:00.000Z', - nextEpochEstimate: '2025-01-16T00:00:00.000Z', - timeUntilNextEpoch: '12h 30m', - minEpochDuration: '24h 0m', - validatorMinStake: '42000 GEN', - delegatorMinStake: '42 GEN', - activeValidatorsCount: '6', - epochInflation: '1000 GEN', - totalWeight: '500000000000000000000000', - totalClaimed: '500 GEN' -} +✔ Epoch info + + Current Epoch: 5 (started 9h 30m ago) + Next Epoch: in 14h 30m + Validators: 33 + Weight: 6061746783417938774454 + Slashed: 0 GEN + + Previous Epoch: 4 (finalized) + Inflation: 1732904.66 GEN + Claimed: 0 GEN + Unclaimed: 1732904.66 GEN + Slashed: 0 GEN + + Min Epoch Duration: 24h 0m + Validator Min Stake: 42000 GEN + Delegator Min Stake: 42 GEN +``` + +You can also query a specific epoch: +```bash +genlayer staking epoch-info --epoch 4 ``` Note the `validatorMinStake` - you need at least this amount. diff --git a/package-lock.json b/package-lock.json index b0c8b3b2..c7ac22ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "license": "MIT", "dependencies": { "chalk": "^5.4.1", + "cli-table3": "^0.6.5", "commander": "^14.0.0", "dockerode": "^4.0.2", "dotenv": "^17.0.0", @@ -176,6 +177,16 @@ "node": ">=18" } }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@conventional-changelog/git-client": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-1.0.1.tgz", @@ -3595,6 +3606,62 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-table3/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-table3/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cli-table3/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-table3/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cli-width": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", diff --git a/package.json b/package.json index e65054ec..4f666da8 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ }, "dependencies": { "chalk": "^5.4.1", + "cli-table3": "^0.6.5", "commander": "^14.0.0", "dockerode": "^4.0.2", "dotenv": "^17.0.0", diff --git a/src/commands/staking/index.ts b/src/commands/staking/index.ts index c8fc80d5..30e1ec39 100644 --- a/src/commands/staking/index.ts +++ b/src/commands/staking/index.ts @@ -10,6 +10,7 @@ import {DelegatorJoinAction, DelegatorJoinOptions} from "./delegatorJoin"; import {DelegatorExitAction, DelegatorExitOptions} from "./delegatorExit"; import {DelegatorClaimAction, DelegatorClaimOptions} from "./delegatorClaim"; import {StakingInfoAction, StakingInfoOptions} from "./stakingInfo"; +import {ValidatorHistoryAction, ValidatorHistoryOptions} from "./validatorHistory"; import {ValidatorWizardAction, WizardOptions} from "./wizard"; export function initializeStakingCommands(program: Command) { @@ -45,73 +46,99 @@ export function initializeStakingCommands(program: Command) { }); staking - .command("validator-deposit") + .command("validator-deposit [validator]") .description("Make an additional deposit to a validator wallet") - .requiredOption("--validator
", "Validator wallet contract address to deposit to") + .option("--validator
", "Validator wallet contract address (deprecated, use positional arg)") .requiredOption("--amount ", "Amount to deposit (in wei or with 'eth'/'gen' suffix)") .option("--account ", "Account to use (must be validator owner)") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") - .action(async (options: ValidatorDepositOptions) => { + .action(async (validatorArg: string | undefined, options: ValidatorDepositOptions) => { + const validator = validatorArg || options.validator; + if (!validator) { + console.error("Error: validator address is required"); + process.exit(1); + } const action = new ValidatorDepositAction(); - await action.execute(options); + await action.execute({...options, validator}); }); staking - .command("validator-exit") + .command("validator-exit [validator]") .description("Exit as a validator by withdrawing shares") - .requiredOption("--validator
", "Validator wallet contract address") + .option("--validator
", "Validator wallet contract address (deprecated, use positional arg)") .requiredOption("--shares ", "Number of shares to withdraw") .option("--account ", "Account to use (must be validator owner)") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") - .action(async (options: ValidatorExitOptions) => { + .action(async (validatorArg: string | undefined, options: ValidatorExitOptions) => { + const validator = validatorArg || options.validator; + if (!validator) { + console.error("Error: validator address is required"); + process.exit(1); + } const action = new ValidatorExitAction(); - await action.execute(options); + await action.execute({...options, validator}); }); staking - .command("validator-claim") + .command("validator-claim [validator]") .description("Claim validator withdrawals after unbonding period") - .requiredOption("--validator
", "Validator wallet contract address") + .option("--validator
", "Validator wallet contract address (deprecated, use positional arg)") .option("--account ", "Account to use") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") - .action(async (options: ValidatorClaimOptions) => { + .action(async (validatorArg: string | undefined, options: ValidatorClaimOptions) => { + const validator = validatorArg || options.validator; + if (!validator) { + console.error("Error: validator address is required"); + process.exit(1); + } const action = new ValidatorClaimAction(); - await action.execute(options); + await action.execute({...options, validator}); }); staking - .command("validator-prime") + .command("validator-prime [validator]") .description("Prime a validator to prepare their stake record for the next epoch") - .requiredOption("--validator
", "Validator address to prime") + .option("--validator
", "Validator address to prime (deprecated, use positional arg)") .option("--account ", "Account to use") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") .option("--staking-address
", "Staking contract address (overrides chain config)") - .action(async (options: ValidatorPrimeOptions) => { + .action(async (validatorArg: string | undefined, options: ValidatorPrimeOptions) => { + const validator = validatorArg || options.validator; + if (!validator) { + console.error("Error: validator address is required"); + process.exit(1); + } const action = new ValidatorPrimeAction(); - await action.execute(options); + await action.execute({...options, validator}); }); staking - .command("set-operator") + .command("set-operator [validator] [operator]") .description("Change the operator address for a validator wallet") - .requiredOption("--validator
", "Validator wallet address") - .requiredOption("--operator
", "New operator address") + .option("--validator
", "Validator wallet address (deprecated, use positional arg)") + .option("--operator
", "New operator address (deprecated, use positional arg)") .option("--account ", "Account to use (must be validator owner)") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") - .action(async (options: SetOperatorOptions) => { + .action(async (validatorArg: string | undefined, operatorArg: string | undefined, options: SetOperatorOptions) => { + const validator = validatorArg || options.validator; + const operator = operatorArg || options.operator; + if (!validator || !operator) { + console.error("Error: validator and operator addresses are required"); + process.exit(1); + } const action = new SetOperatorAction(); - await action.execute(options); + await action.execute({...options, validator, operator}); }); staking - .command("set-identity") + .command("set-identity [validator]") .description("Set validator identity metadata (moniker, website, socials, etc.)") - .requiredOption("--validator
", "Validator wallet address") + .option("--validator
", "Validator wallet address (deprecated, use positional arg)") .requiredOption("--moniker ", "Validator display name") .option("--logo-uri ", "Logo URI") .option("--website ", "Website URL") @@ -124,89 +151,116 @@ export function initializeStakingCommands(program: Command) { .option("--account ", "Account to use (must be validator operator)") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") - .action(async (options: SetIdentityOptions) => { + .action(async (validatorArg: string | undefined, options: SetIdentityOptions) => { + const validator = validatorArg || options.validator; + if (!validator) { + console.error("Error: validator address is required"); + process.exit(1); + } const action = new SetIdentityAction(); - await action.execute(options); + await action.execute({...options, validator}); }); // Delegator commands staking - .command("delegator-join") + .command("delegator-join [validator]") .description("Join as a delegator by staking with a validator") - .requiredOption("--validator
", "Validator address to delegate to") + .option("--validator
", "Validator address to delegate to (deprecated, use positional arg)") .requiredOption("--amount ", "Amount to stake (in wei or with 'eth'/'gen' suffix)") .option("--account ", "Account to use") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") .option("--staking-address
", "Staking contract address (overrides chain config)") - .action(async (options: DelegatorJoinOptions) => { + .action(async (validatorArg: string | undefined, options: DelegatorJoinOptions) => { + const validator = validatorArg || options.validator; + if (!validator) { + console.error("Error: validator address is required"); + process.exit(1); + } const action = new DelegatorJoinAction(); - await action.execute(options); + await action.execute({...options, validator}); }); staking - .command("delegator-exit") + .command("delegator-exit [validator]") .description("Exit as a delegator by withdrawing shares from a validator") - .requiredOption("--validator
", "Validator address to exit from") + .option("--validator
", "Validator address to exit from (deprecated, use positional arg)") .requiredOption("--shares ", "Number of shares to withdraw") .option("--account ", "Account to use") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") .option("--staking-address
", "Staking contract address (overrides chain config)") - .action(async (options: DelegatorExitOptions) => { + .action(async (validatorArg: string | undefined, options: DelegatorExitOptions) => { + const validator = validatorArg || options.validator; + if (!validator) { + console.error("Error: validator address is required"); + process.exit(1); + } const action = new DelegatorExitAction(); - await action.execute(options); + await action.execute({...options, validator}); }); staking - .command("delegator-claim") + .command("delegator-claim [validator]") .description("Claim delegator withdrawals after unbonding period") - .requiredOption("--validator
", "Validator address") + .option("--validator
", "Validator address (deprecated, use positional arg)") .option("--delegator
", "Delegator address (defaults to signer)") .option("--account ", "Account to use") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") .option("--staking-address
", "Staking contract address (overrides chain config)") - .action(async (options: DelegatorClaimOptions) => { + .action(async (validatorArg: string | undefined, options: DelegatorClaimOptions) => { + const validator = validatorArg || options.validator; + if (!validator) { + console.error("Error: validator address is required"); + process.exit(1); + } const action = new DelegatorClaimAction(); - await action.execute(options); + await action.execute({...options, validator}); }); // Info commands staking - .command("validator-info") + .command("validator-info [validator]") .description("Get information about a validator") - .option("--validator
", "Validator address (defaults to signer)") + .option("--validator
", "Validator address (deprecated, use positional arg)") .option("--account ", "Account to use (for default validator address)") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") .option("--staking-address
", "Staking contract address (overrides chain config)") - .action(async (options: StakingInfoOptions) => { + .action(async (validatorArg: string | undefined, options: StakingInfoOptions) => { + const validator = validatorArg || options.validator; const action = new StakingInfoAction(); - await action.getValidatorInfo(options); + await action.getValidatorInfo({...options, validator}); }); staking - .command("delegation-info") + .command("delegation-info [validator]") .description("Get delegation info for a delegator with a validator") - .requiredOption("--validator
", "Validator address") + .option("--validator
", "Validator address (deprecated, use positional arg)") .option("--delegator
", "Delegator address (defaults to signer)") .option("--account ", "Account to use (for default delegator address)") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") .option("--staking-address
", "Staking contract address (overrides chain config)") - .action(async (options: StakingInfoOptions & {delegator?: string}) => { + .action(async (validatorArg: string | undefined, options: StakingInfoOptions & {delegator?: string}) => { + const validator = validatorArg || options.validator; + if (!validator) { + console.error("Error: validator address is required"); + process.exit(1); + } const action = new StakingInfoAction(); - await action.getStakeInfo(options); + await action.getStakeInfo({...options, validator}); }); staking .command("epoch-info") .description("Get current epoch and staking parameters") + .option("--epoch ", "Show data for specific epoch (current or previous only)") .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") .option("--staking-address
", "Staking contract address (overrides chain config)") - .action(async (options: StakingInfoOptions) => { + .action(async (options: StakingInfoOptions & {epoch?: string}) => { const action = new StakingInfoAction(); await action.getEpochInfo(options); }); @@ -244,5 +298,33 @@ export function initializeStakingCommands(program: Command) { await action.listBannedValidators(options); }); + staking + .command("validators") + .description("Show validator set with stake, status, and voting power") + .option("--all", "Include banned validators") + .option("--network ", "Network to use (localnet, testnet-asimov)") + .option("--rpc ", "RPC URL for the network") + .option("--staking-address
", "Staking contract address (overrides chain config)") + .action(async (options: StakingInfoOptions & {all?: boolean}) => { + const action = new StakingInfoAction(); + await action.listValidators(options); + }); + + staking + .command("validator-history [validator]") + .description("Show slash and reward history for a validator") + .option("--validator
", "Validator address (deprecated, use positional arg)") + .option("--from-block ", "Start from this block number") + .option("--limit ", "Maximum number of events to show (default: 50)") + .option("--account ", "Account to use (for default validator address)") + .option("--network ", "Network to use (localnet, testnet-asimov)") + .option("--rpc ", "RPC URL for the network") + .option("--staking-address
", "Staking contract address (overrides chain config)") + .action(async (validatorArg: string | undefined, options: ValidatorHistoryOptions) => { + const validator = validatorArg || options.validator; + const action = new ValidatorHistoryAction(); + await action.execute({...options, validator}); + }); + return program; } diff --git a/src/commands/staking/stakingInfo.ts b/src/commands/staking/stakingInfo.ts index 30a37e87..e11a3711 100644 --- a/src/commands/staking/stakingInfo.ts +++ b/src/commands/staking/stakingInfo.ts @@ -1,5 +1,7 @@ import {StakingAction, StakingConfig} from "./StakingAction"; -import type {Address} from "genlayer-js/types"; +import type {Address, ValidatorInfo} from "genlayer-js/types"; +import Table from "cli-table3"; +import chalk from "chalk"; // Epoch-related constants const ACTIVATION_DELAY_EPOCHS = 2n; @@ -189,12 +191,11 @@ export class StakingInfoAction extends StakingAction { } } - async getEpochInfo(options: StakingConfig): Promise { + async getEpochInfo(options: StakingConfig & {epoch?: string}): Promise { this.startSpinner("Fetching epoch info..."); try { const client = await this.getReadOnlyStakingClient(options); - const info = await client.getEpochInfo(); const formatDuration = (ms: number): string => { @@ -208,26 +209,75 @@ export class StakingInfoAction extends StakingAction { return `${hours}h ${minutes}m`; }; + const formatAmount = client.formatStakingAmount; + + // If specific epoch requested, show just that epoch's data + if (options.epoch !== undefined) { + const epochNum = BigInt(options.epoch); + const epochData = await client.getEpochData(epochNum); + const isFinalized = info.lastFinalizedEpoch >= epochNum; + const startDate = new Date(Number(epochData.start) * 1000); + const endDate = epochData.end > 0n ? new Date(Number(epochData.end) * 1000) : null; + + this.succeedSpinner(`Epoch ${epochNum}`); + console.log(`\n Epoch: ${epochNum}`); + console.log(` Started: ${startDate.toISOString()}`); + console.log(` Ended: ${endDate?.toISOString() || "Not yet"}`); + console.log(` Finalized: ${isFinalized ? "Yes" : "No"}`); + console.log(` Validators: ${epochData.vcount}`); + console.log(` Weight: ${epochData.weight}`); + console.log(` Inflation: ${formatAmount(epochData.inflation)}`); + console.log(` Claimed: ${formatAmount(epochData.claimed)}`); + console.log(` Slashed: ${formatAmount(epochData.slashed)}`); + console.log(); + return; + } + + // Default: show current + previous epoch + const currentEpochData = await client.getEpochData(info.currentEpoch); + const currentStart = new Date(Number(currentEpochData.start) * 1000); const now = Date.now(); + const timeSinceStart = now - currentStart.getTime(); const timeUntilNext = info.nextEpochEstimate ? info.nextEpochEstimate.getTime() - now : null; - const result = { - currentEpoch: info.currentEpoch.toString(), - epochStarted: info.currentEpochStart.toISOString(), - epochEnded: info.currentEpochEnd?.toISOString() || "Not ended", - nextEpochEstimate: info.nextEpochEstimate?.toISOString() || "N/A", - timeUntilNextEpoch: timeUntilNext && timeUntilNext > 0 ? formatDuration(timeUntilNext) : "N/A", - minEpochDuration: formatDuration(Number(info.epochMinDuration) * 1000), - validatorMinStake: info.validatorMinStake, - delegatorMinStake: info.delegatorMinStake, - activeValidatorsCount: info.activeValidatorsCount.toString(), - // Inflation/rewards - epochInflation: info.inflation, - totalWeight: info.totalWeight.toString(), - totalClaimed: info.totalClaimed, - }; + this.succeedSpinner("Epoch info"); + + const nextEstimate = timeUntilNext && timeUntilNext > 0 + ? `in ${formatDuration(timeUntilNext)}` + : currentEpochData.end > 0n ? "Next epoch started" : "N/A"; + + console.log(`\n Current Epoch: ${info.currentEpoch} (started ${formatDuration(timeSinceStart)} ago)`); + console.log(` Next Epoch: ${nextEstimate}`); + console.log(` Validators: ${info.activeValidatorsCount}`); + console.log(` Weight: ${currentEpochData.weight}`); + console.log(` Slashed: ${formatAmount(currentEpochData.slashed)}`); + + // Previous epoch (has the actual inflation/rewards data) + if (info.currentEpoch > 0n) { + const prevEpoch = info.currentEpoch - 1n; + const prevData = await client.getEpochData(prevEpoch); + const isFinalized = info.lastFinalizedEpoch >= prevEpoch; + const prevEnd = prevData.end > 0n; + + let status: string; + if (!prevEnd) { + status = "still active"; + } else if (isFinalized) { + status = "finalized"; + } else { + status = "finalizing txs..."; + } + + console.log(`\n Previous Epoch: ${prevEpoch} (${status})`); + console.log(` Inflation: ${formatAmount(prevData.inflation)}`); + console.log(` Claimed: ${formatAmount(prevData.claimed)}`); + console.log(` Unclaimed: ${formatAmount(prevData.inflation - prevData.claimed)}`); + console.log(` Slashed: ${formatAmount(prevData.slashed)}`); + } - this.succeedSpinner("Epoch info retrieved", result); + console.log(`\n Min Epoch Duration: ${formatDuration(Number(info.epochMinDuration) * 1000)}`); + console.log(` Validator Min Stake: ${info.validatorMinStake}`); + console.log(` Delegator Min Stake: ${info.delegatorMinStake}\n`); } catch (error: any) { this.failSpinner("Failed to get epoch info", error.message || error); } @@ -297,4 +347,217 @@ export class StakingInfoAction extends StakingAction { this.failSpinner("Failed to get banned validators", error.message || error); } } + + async listValidators(options: StakingConfig & {all?: boolean}): Promise { + this.startSpinner("Fetching validator set..."); + + try { + const client = await this.getReadOnlyStakingClient(options); + + // Get current user's address to mark "mine" + let myAddress: Address | null = null; + try { + myAddress = await this.getSignerAddress(); + } catch { + // No account configured, that's fine + } + + // Fetch all data in parallel + const [activeAddresses, quarantinedList, bannedList, epochInfo] = await Promise.all([ + client.getActiveValidators(), + client.getQuarantinedValidatorsDetailed(), + options.all ? client.getBannedValidators() : Promise.resolve([]), + client.getEpochInfo(), + ]); + + // Build set of quarantined/banned for status lookup + const quarantinedSet = new Map(quarantinedList.map(v => [v.validator.toLowerCase(), v])); + const bannedSet = new Map(bannedList.map(v => [v.validator.toLowerCase(), v])); + + // Combine all validators + const allAddresses = new Set([ + ...activeAddresses, + ...quarantinedList.map(v => v.validator), + ...(options.all ? bannedList.map(v => v.validator) : []), + ]); + + this.setSpinnerText(`Fetching details for ${allAddresses.size} validators...`); + + // Fetch detailed info in batches to avoid rate limiting + const BATCH_SIZE = 5; + const addressArray = Array.from(allAddresses); + const validatorInfos: ValidatorInfo[] = []; + + for (let i = 0; i < addressArray.length; i += BATCH_SIZE) { + const batch = addressArray.slice(i, i + BATCH_SIZE); + const batchResults = await Promise.all( + batch.map(addr => client.getValidatorInfo(addr as Address)) + ); + validatorInfos.push(...batchResults); + if (i + BATCH_SIZE < addressArray.length) { + this.setSpinnerText(`Fetching details... ${Math.min(i + BATCH_SIZE, addressArray.length)}/${addressArray.length}`); + } + } + + // Build table rows + type ValidatorRow = { + info: ValidatorInfo; + status: string; + isMine: boolean; + totalStakeRaw: bigint; + }; + + const rows: ValidatorRow[] = validatorInfos.map(info => { + const addrLower = info.address.toLowerCase(); + const isQuarantined = quarantinedSet.has(addrLower); + const isBanned = bannedSet.has(addrLower); + const isActive = activeAddresses.some(a => a.toLowerCase() === addrLower); + + let status = ""; + if (isBanned) { + const banInfo = bannedSet.get(addrLower)!; + status = banInfo.permanentlyBanned ? "BANNED" : `banned(e${banInfo.untilEpoch})`; + } else if (isQuarantined) { + const qInfo = quarantinedSet.get(addrLower)!; + status = `quarant(e${qInfo.untilEpoch})`; + } else if (info.needsPriming) { + status = "prime!"; + } else if (isActive) { + status = "active"; + } else { + status = "pending"; + } + + const isMine = myAddress + ? info.owner.toLowerCase() === myAddress.toLowerCase() || + info.operator.toLowerCase() === myAddress.toLowerCase() + : false; + + return { + info, + status, + isMine, + totalStakeRaw: info.vStakeRaw + info.dStakeRaw, + }; + }); + + // Calculate validator weight using the contract formula: + // weight = (vStake * alpha + dStake * (1 - alpha)) ^ beta + // Default: alpha = 0.6, beta = 0.5 (square root) + const ALPHA = 0.6; + const BETA = 0.5; + const calcWeight = (vStakeRaw: bigint, dStakeRaw: bigint): number => { + const vStake = Number(vStakeRaw) / 1e18; + const dStake = Number(dStakeRaw) / 1e18; + const util = vStake * ALPHA + dStake * (1 - ALPHA); + return Math.pow(util, BETA); + }; + + // Add weight to rows and sort by weight descending + const rowsWithWeight = rows.map(r => ({ + ...r, + weight: calcWeight(r.info.vStakeRaw, r.info.dStakeRaw), + })); + rowsWithWeight.sort((a, b) => b.weight - a.weight); + + // Calculate total weight for active validators only (for power %) + const totalWeight = rowsWithWeight + .filter(r => r.status === "active") + .reduce((sum, r) => sum + r.weight, 0); + + this.stopSpinner(); + + // Format stake - shorten large numbers + const formatStake = (s: string) => { + const num = parseFloat(s.replace(" GEN", "")); + if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`; + if (num >= 1000) return `${(num / 1000).toFixed(1)}K`; + if (num >= 1) return num.toFixed(1); + if (num > 0) return num.toPrecision(2); + return "0"; + }; + + // Create table (no fixed widths - let it auto-size) + const table = new Table({ + head: [ + chalk.cyan("#"), + chalk.cyan("Validator"), + chalk.cyan("Self"), + chalk.cyan("Deleg"), + chalk.cyan("Pending"), + chalk.cyan("Weight"), + chalk.cyan("Status"), + ], + style: {head: [], border: []}, + }); + + rowsWithWeight.forEach((row, idx) => { + const {info, status, isMine, weight} = row; + + // Weight percentage (share of active set weight) + const weightPct = totalWeight > 0 ? (weight / totalWeight) * 100 : 0; + const weightStr = status === "active" ? `${weightPct.toFixed(1)}%` : chalk.gray("-"); + + // Pending deposits/withdrawals - sum amounts + const pendingDepositSum = info.pendingDeposits.reduce((sum, d) => sum + d.stakeRaw, 0n); + const pendingWithdrawSum = info.pendingWithdrawals.reduce((sum, w) => sum + w.stakeRaw, 0n); + let pendingStr = "-"; + if (pendingDepositSum > 0n && pendingWithdrawSum > 0n) { + pendingStr = chalk.green(`+${formatStake(`${Number(pendingDepositSum) / 1e18} GEN`)}`) + + " " + chalk.red(`-${formatStake(`${Number(pendingWithdrawSum) / 1e18} GEN`)}`); + } else if (pendingDepositSum > 0n) { + pendingStr = chalk.green(`+${formatStake(`${Number(pendingDepositSum) / 1e18} GEN`)}`); + } else if (pendingWithdrawSum > 0n) { + pendingStr = chalk.red(`-${formatStake(`${Number(pendingWithdrawSum) / 1e18} GEN`)}`) + } + + // Role indicator (colored) + let roleTag = ""; + if (isMine) { + if (myAddress && info.owner.toLowerCase() === myAddress.toLowerCase()) { + roleTag = info.operator.toLowerCase() === myAddress.toLowerCase() + ? chalk.cyan(" [own+op]") + : chalk.cyan(" [owner]"); + } else { + roleTag = chalk.cyan(" [operator]"); + } + } + + // Moniker + role + full address on second line + let moniker = info.identity?.moniker || ""; + if (moniker.length > 20) moniker = moniker.slice(0, 19) + "…"; + const validatorCell = moniker + ? `${moniker}${roleTag}\n${chalk.gray(info.address)}` + : `${chalk.gray(info.address)}${roleTag}`; + + // Status coloring + let statusStr = status; + if (status === "active") statusStr = chalk.green(status); + else if (status === "BANNED") statusStr = chalk.red(status); + else if (status.startsWith("quarant")) statusStr = chalk.yellow(status); + else if (status.startsWith("banned")) statusStr = chalk.red(status); + else if (status === "prime!") statusStr = chalk.magenta(status); + else if (status === "pending") statusStr = chalk.gray(status); + + table.push([ + (idx + 1).toString(), + validatorCell, + formatStake(info.vStake), + formatStake(info.dStake), + pendingStr, + weightStr, + statusStr, + ]); + }); + + console.log(""); + console.log(table.toString()); + console.log(""); + const activeCount = rowsWithWeight.filter(r => r.status === "active").length; + console.log(chalk.gray(`Total: ${rowsWithWeight.length} validators (${activeCount} active)`)); + console.log(""); + } catch (error: any) { + this.failSpinner("Failed to list validators", error.message || error); + } + } } diff --git a/src/commands/staking/validatorHistory.ts b/src/commands/staking/validatorHistory.ts new file mode 100644 index 00000000..b8c01d88 --- /dev/null +++ b/src/commands/staking/validatorHistory.ts @@ -0,0 +1,259 @@ +import {StakingAction, StakingConfig, BUILT_IN_NETWORKS} from "./StakingAction"; +import type {Address, GenLayerChain} from "genlayer-js/types"; +import {createPublicClient, http} from "viem"; +import Table from "cli-table3"; +import chalk from "chalk"; + +// Event ABIs for log fetching +const SLASH_EVENT_ABI = { + type: "event", + name: "SlashedFromIdleness", + inputs: [ + {name: "validator", type: "address", indexed: true}, + {name: "txId", type: "bytes32", indexed: false}, + {name: "epoch", type: "uint256", indexed: false}, + {name: "percentage", type: "uint256", indexed: false}, + ], +} as const; + +const REWARD_EVENT_ABI = { + type: "event", + name: "ValidatorPrime", + inputs: [ + {name: "validator", type: "address", indexed: false}, + {name: "epoch", type: "uint256", indexed: false}, + {name: "validatorRewards", type: "uint256", indexed: false}, + {name: "delegatorRewards", type: "uint256", indexed: false}, + ], +} as const; + +export interface ValidatorHistoryOptions extends StakingConfig { + validator?: string; + fromBlock?: string; + limit?: string; +} + +interface SlashEvent { + type: "slash"; + epoch: bigint; + txId: string; + percentage: bigint; + blockNumber: bigint; + timestamp: Date; +} + +interface RewardEvent { + type: "reward"; + epoch: bigint; + validatorRewards: bigint; + delegatorRewards: bigint; + blockNumber: bigint; + timestamp: Date; +} + +type HistoryEvent = SlashEvent | RewardEvent; + +export class ValidatorHistoryAction extends StakingAction { + constructor() { + super(); + } + + private getNetworkForHistory(config: StakingConfig): GenLayerChain { + if (config.network) { + const network = BUILT_IN_NETWORKS[config.network]; + if (!network) { + throw new Error(`Unknown network: ${config.network}`); + } + return network; + } + // Check global config + const globalNetwork = this.getConfig().network; + if (globalNetwork && BUILT_IN_NETWORKS[globalNetwork]) { + return BUILT_IN_NETWORKS[globalNetwork]; + } + return BUILT_IN_NETWORKS["localnet"]; + } + + async execute(options: ValidatorHistoryOptions): Promise { + this.startSpinner("Fetching validator history..."); + + try { + // Check network - localnet doesn't support eth_getLogs + const chain = this.getNetworkForHistory(options); + if (chain.id === 808080) { + this.failSpinner("validator-history requires testnet-asimov (localnet doesn't support event logs)"); + return; + } + + const client = await this.getReadOnlyStakingClient(options); + const validatorAddress = options.validator || (await this.getSignerAddress()); + + // Verify it's a validator + const isValidator = await client.isValidator(validatorAddress as Address); + if (!isValidator) { + this.failSpinner(`Address ${validatorAddress} is not a validator`); + return; + } + + this.setSpinnerText("Fetching contract addresses..."); + + // Get addresses + const stakingAddress = client.getStakingContract().address; + const slashingAddress = await client.getSlashingAddress(); + + // Create public client for log fetching + const publicClient = createPublicClient({ + chain, + transport: http(chain.rpcUrls.default.http[0]), + }); + + const fromBlock = options.fromBlock ? BigInt(options.fromBlock) : 0n; + const limit = options.limit ? parseInt(options.limit) : 50; + + this.setSpinnerText("Fetching slash events..."); + + // Fetch slash events (indexed by validator) + const slashLogs = await publicClient.getLogs({ + address: slashingAddress as `0x${string}`, + event: SLASH_EVENT_ABI, + args: {validator: validatorAddress as `0x${string}`}, + fromBlock, + toBlock: "latest", + }); + + this.setSpinnerText("Fetching reward events..."); + + // Fetch reward events (not indexed, need to filter client-side) + const rewardLogs = await publicClient.getLogs({ + address: stakingAddress, + event: REWARD_EVENT_ABI, + fromBlock, + toBlock: "latest", + }); + + // Filter rewards to this validator + const filteredRewardLogs = rewardLogs.filter( + log => (log.args as any).validator?.toLowerCase() === validatorAddress.toLowerCase() + ); + + // Get unique block numbers to fetch timestamps + const allLogs = [...slashLogs, ...filteredRewardLogs]; + const uniqueBlocks = [...new Set(allLogs.map(l => l.blockNumber))]; + + this.setSpinnerText("Fetching block timestamps..."); + + // Fetch block timestamps in batches + const blockTimestamps = new Map(); + const BATCH_SIZE = 10; + for (let i = 0; i < uniqueBlocks.length; i += BATCH_SIZE) { + const batch = uniqueBlocks.slice(i, i + BATCH_SIZE); + const blocks = await Promise.all( + batch.map(blockNumber => publicClient.getBlock({blockNumber})) + ); + blocks.forEach(block => { + blockTimestamps.set(block.number, new Date(Number(block.timestamp) * 1000)); + }); + } + + // Transform to typed events + const slashEvents: SlashEvent[] = slashLogs.map(log => ({ + type: "slash" as const, + epoch: (log.args as any).epoch as bigint, + txId: (log.args as any).txId as string, + percentage: (log.args as any).percentage as bigint, + blockNumber: log.blockNumber, + timestamp: blockTimestamps.get(log.blockNumber) || new Date(0), + })); + + const rewardEvents: RewardEvent[] = filteredRewardLogs.map(log => ({ + type: "reward" as const, + epoch: (log.args as any).epoch as bigint, + validatorRewards: (log.args as any).validatorRewards as bigint, + delegatorRewards: (log.args as any).delegatorRewards as bigint, + blockNumber: log.blockNumber, + timestamp: blockTimestamps.get(log.blockNumber) || new Date(0), + })); + + // Combine and sort by block number descending + const allEvents: HistoryEvent[] = [...slashEvents, ...rewardEvents]; + allEvents.sort((a, b) => Number(b.blockNumber - a.blockNumber)); + + // Apply limit + const limitedEvents = allEvents.slice(0, limit); + + // Calculate totals + const totalValidatorRewards = rewardEvents.reduce((sum, e) => sum + e.validatorRewards, 0n); + const totalDelegatorRewards = rewardEvents.reduce((sum, e) => sum + e.delegatorRewards, 0n); + + this.stopSpinner(); + + // Display results + if (limitedEvents.length === 0) { + console.log(chalk.yellow("\nNo history events found for this validator.\n")); + return; + } + + // Format timestamp as datetime + const formatTime = (date: Date): string => { + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + const hours = String(date.getHours()).padStart(2, "0"); + const minutes = String(date.getMinutes()).padStart(2, "0"); + return `${month}-${day} ${hours}:${minutes}`; + }; + + // Create table + const table = new Table({ + head: [ + chalk.cyan("Time"), + chalk.cyan("Epoch"), + chalk.cyan("Type"), + chalk.cyan("Details"), + chalk.cyan("Block"), + ], + style: {head: [], border: []}, + }); + + for (const event of limitedEvents) { + if (event.type === "slash") { + const pct = Number(event.percentage) / 100; // basis points to % + table.push([ + formatTime(event.timestamp), + event.epoch.toString(), + chalk.red("SLASH"), + `${pct.toFixed(2)}% penalty`, + event.blockNumber.toString(), + ]); + } else { + const valReward = client.formatStakingAmount(event.validatorRewards); + const delReward = client.formatStakingAmount(event.delegatorRewards); + table.push([ + formatTime(event.timestamp), + event.epoch.toString(), + chalk.green("REWARD"), + `Val: ${valReward}, Del: ${delReward}`, + event.blockNumber.toString(), + ]); + } + } + + console.log(""); + console.log(chalk.bold(`History for ${validatorAddress}`)); + console.log(table.toString()); + console.log(""); + + // Summary + console.log(chalk.gray("Summary:")); + console.log(chalk.gray(` Slash events: ${slashEvents.length}`)); + console.log(chalk.gray(` Reward events: ${rewardEvents.length}`)); + console.log(chalk.gray(` Total validator rewards: ${client.formatStakingAmount(totalValidatorRewards)}`)); + console.log(chalk.gray(` Total delegator rewards: ${client.formatStakingAmount(totalDelegatorRewards)}`)); + if (allEvents.length > limit) { + console.log(chalk.gray(` (showing ${limit} of ${allEvents.length} events)`)); + } + console.log(""); + } catch (error: any) { + this.failSpinner("Failed to get validator history", error.message || error); + } + } +} From 40098b047f0462456e8bc346f8388a234b1e5fa1 Mon Sep 17 00:00:00 2001 From: Edgars Date: Thu, 11 Dec 2025 23:52:47 +0000 Subject: [PATCH 2/7] feat: add validators table and validator-history commands - Add `staking validators` command showing validator set table with: - Moniker, address, self stake, delegated stake - Pending deposits/withdrawals with amounts - Weight percentage (voting power) - Status (active, quarantined, banned, primed, pending) - Owner/operator role indicators - Add `staking validator-history` command showing: - Slash events (idleness penalties) with percentage - Reward events (ValidatorPrime) with validator/delegator rewards - Timestamps, epochs, and block numbers - Summary with totals - Update staking commands to use positional args (backwards compatible): - `validator-info 0x...` instead of `--validator 0x...` - Same for deposit, exit, claim, prime, delegator commands - Add slashed data to epoch-info output --- README.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cb534b6a..2c5960b2 100644 --- a/README.md +++ b/README.md @@ -309,9 +309,11 @@ COMMANDS: delegator-join [options] Join as a delegator by staking with a validator delegator-exit [options] Exit as a delegator by withdrawing shares delegator-claim [options] Claim delegator withdrawals after unbonding period - validator-info [options] Get information about a validator - delegation-info [options] Get delegation info for a delegator with a validator + validator-info [validator] Get information about a validator + validator-history [validator] Show slash and reward history for a validator + delegation-info [validator] Get delegation info for a delegator with a validator epoch-info [options] Get current/previous epoch info (--epoch for specific) + validators [options] Show validator set with stake, status, and weight active-validators [options] List all active validators COMMON OPTIONS (all commands): @@ -391,6 +393,21 @@ EXAMPLES: # ] # } + # Show validator set table with stake, status, weight + genlayer staking validators + genlayer staking validators --all # Include banned validators + + # Show validator slash/reward history (testnet only) + genlayer staking validator-history 0x... + genlayer staking validator-history 0x... --limit 20 + # Output: + # ┌─────────────┬───────┬────────┬────────────────────────┬─────────┐ + # │ Time │ Epoch │ Type │ Details │ Block │ + # ├─────────────┼───────┼────────┼────────────────────────┼─────────┤ + # │ 12-11 14:20 │ 5 │ REWARD │ Val: 0 GEN, Del: 0 GEN │ 4725136 │ + # │ 12-10 18:39 │ 4 │ SLASH │ 1.00% penalty │ 4717431 │ + # └─────────────┴───────┴────────┴────────────────────────┴─────────┘ + # Exit and claim (requires validator wallet address) genlayer staking validator-exit --validator 0x... --shares 100 genlayer staking validator-claim --validator 0x... From 5c31bae748a19d2dcced00ab996ffad05ca0fb7a Mon Sep 17 00:00:00 2001 From: Edgars Date: Thu, 11 Dec 2025 23:57:39 +0000 Subject: [PATCH 3/7] fix: filter pending deposits/withdrawals by epoch in validators table Only show truly pending deposits (epoch + 2 > current) and withdrawals (epoch + 7 > current) instead of all historical ones. Also add quarantined-validators and banned-validators commands to docs. --- README.md | 2 ++ src/commands/staking/stakingInfo.ts | 9 ++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2c5960b2..2d84851c 100644 --- a/README.md +++ b/README.md @@ -315,6 +315,8 @@ COMMANDS: epoch-info [options] Get current/previous epoch info (--epoch for specific) validators [options] Show validator set with stake, status, and weight active-validators [options] List all active validators + quarantined-validators List all quarantined validators + banned-validators List all banned validators COMMON OPTIONS (all commands): --network Network to use (localnet, testnet-asimov) diff --git a/src/commands/staking/stakingInfo.ts b/src/commands/staking/stakingInfo.ts index e11a3711..9f00729f 100644 --- a/src/commands/staking/stakingInfo.ts +++ b/src/commands/staking/stakingInfo.ts @@ -498,9 +498,12 @@ export class StakingInfoAction extends StakingAction { const weightPct = totalWeight > 0 ? (weight / totalWeight) * 100 : 0; const weightStr = status === "active" ? `${weightPct.toFixed(1)}%` : chalk.gray("-"); - // Pending deposits/withdrawals - sum amounts - const pendingDepositSum = info.pendingDeposits.reduce((sum, d) => sum + d.stakeRaw, 0n); - const pendingWithdrawSum = info.pendingWithdrawals.reduce((sum, w) => sum + w.stakeRaw, 0n); + // Pending deposits/withdrawals - sum amounts (filter to truly pending only) + const currentEpoch = epochInfo.currentEpoch; + const trulyPendingDeposits = info.pendingDeposits.filter(d => d.epoch + ACTIVATION_DELAY_EPOCHS > currentEpoch); + const trulyPendingWithdrawals = info.pendingWithdrawals.filter(w => w.epoch + UNBONDING_PERIOD_EPOCHS > currentEpoch); + const pendingDepositSum = trulyPendingDeposits.reduce((sum, d) => sum + d.stakeRaw, 0n); + const pendingWithdrawSum = trulyPendingWithdrawals.reduce((sum, w) => sum + w.stakeRaw, 0n); let pendingStr = "-"; if (pendingDepositSum > 0n && pendingWithdrawSum > 0n) { pendingStr = chalk.green(`+${formatStake(`${Number(pendingDepositSum) / 1e18} GEN`)}`) + From b68ceb3f94677bb5f982dfcd7293d1b541e78992 Mon Sep 17 00:00:00 2001 From: Edgars Date: Fri, 12 Dec 2025 14:33:16 +0000 Subject: [PATCH 4/7] feat: show tx hash in validator-history slash events --- src/commands/staking/validatorHistory.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/staking/validatorHistory.ts b/src/commands/staking/validatorHistory.ts index b8c01d88..9c252f13 100644 --- a/src/commands/staking/validatorHistory.ts +++ b/src/commands/staking/validatorHistory.ts @@ -217,11 +217,12 @@ export class ValidatorHistoryAction extends StakingAction { for (const event of limitedEvents) { if (event.type === "slash") { const pct = Number(event.percentage) / 100; // basis points to % + const shortTxId = `${event.txId.slice(0, 10)}...`; table.push([ formatTime(event.timestamp), event.epoch.toString(), chalk.red("SLASH"), - `${pct.toFixed(2)}% penalty`, + `${pct.toFixed(2)}% (tx: ${shortTxId})`, event.blockNumber.toString(), ]); } else { From ce88afba975070e6ba9851220bc79915c7fe2635 Mon Sep 17 00:00:00 2001 From: Edgars Date: Fri, 12 Dec 2025 14:36:56 +0000 Subject: [PATCH 5/7] chore: upgrade genlayer-js to v0.18.9 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index c7ac22ce..cbfefc6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "dotenv": "^17.0.0", "ethers": "^6.13.4", "fs-extra": "^11.3.0", - "genlayer-js": "^0.18.8", + "genlayer-js": "^0.18.9", "inquirer": "^12.0.0", "keytar": "^7.9.0", "node-fetch": "^3.0.0", @@ -5581,9 +5581,9 @@ } }, "node_modules/genlayer-js": { - "version": "0.18.8", - "resolved": "https://registry.npmjs.org/genlayer-js/-/genlayer-js-0.18.8.tgz", - "integrity": "sha512-zxcwbKiJF09oNCKf1RcHm3vlcYECdDRbrWZEzwZgplIp0gtQu+vfMmBavWzd5saja5TVin4EWHdzuPOwpuxI/w==", + "version": "0.18.9", + "resolved": "https://registry.npmjs.org/genlayer-js/-/genlayer-js-0.18.9.tgz", + "integrity": "sha512-KMKLCKU4v7FCQhJBwC3Nx+Y6ezN7UC4ATCJozKoHHhsC9rk9fcac7TIinonPfwQa8ghXMYBLCcqkhNy7BltREA==", "license": "MIT", "dependencies": { "eslint-plugin-import": "^2.30.0", diff --git a/package.json b/package.json index 4f666da8..ff48830e 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "dotenv": "^17.0.0", "ethers": "^6.13.4", "fs-extra": "^11.3.0", - "genlayer-js": "^0.18.8", + "genlayer-js": "^0.18.9", "inquirer": "^12.0.0", "keytar": "^7.9.0", "node-fetch": "^3.0.0", From a68a9a165a9e3468e97c48ab86416fea50bdb3e2 Mon Sep 17 00:00:00 2001 From: Edgars Date: Fri, 12 Dec 2025 14:39:05 +0000 Subject: [PATCH 6/7] feat: show full GenLayer txId in validator-history slash events --- src/commands/staking/validatorHistory.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/commands/staking/validatorHistory.ts b/src/commands/staking/validatorHistory.ts index 9c252f13..5530b25a 100644 --- a/src/commands/staking/validatorHistory.ts +++ b/src/commands/staking/validatorHistory.ts @@ -209,7 +209,7 @@ export class ValidatorHistoryAction extends StakingAction { chalk.cyan("Epoch"), chalk.cyan("Type"), chalk.cyan("Details"), - chalk.cyan("Block"), + chalk.cyan("GL TxId / Block"), ], style: {head: [], border: []}, }); @@ -217,13 +217,12 @@ export class ValidatorHistoryAction extends StakingAction { for (const event of limitedEvents) { if (event.type === "slash") { const pct = Number(event.percentage) / 100; // basis points to % - const shortTxId = `${event.txId.slice(0, 10)}...`; table.push([ formatTime(event.timestamp), event.epoch.toString(), chalk.red("SLASH"), - `${pct.toFixed(2)}% (tx: ${shortTxId})`, - event.blockNumber.toString(), + `${pct.toFixed(2)}%`, + event.txId, ]); } else { const valReward = client.formatStakingAmount(event.validatorRewards); @@ -233,7 +232,7 @@ export class ValidatorHistoryAction extends StakingAction { event.epoch.toString(), chalk.green("REWARD"), `Val: ${valReward}, Del: ${delReward}`, - event.blockNumber.toString(), + `block ${event.blockNumber}`, ]); } } From c2ada191dc84f0e9fcc1dff5feacaa8028bbeeae Mon Sep 17 00:00:00 2001 From: Edgars Date: Fri, 12 Dec 2025 14:43:59 +0000 Subject: [PATCH 7/7] fix: update staking tests for epoch-info output changes --- tests/actions/staking.test.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/actions/staking.test.ts b/tests/actions/staking.test.ts index 922dcdc4..56fffc76 100644 --- a/tests/actions/staking.test.ts +++ b/tests/actions/staking.test.ts @@ -64,7 +64,9 @@ const mockClient = { getValidatorInfo: vi.fn(), getStakeInfo: vi.fn(), getEpochInfo: vi.fn(), + getEpochData: vi.fn(), getActiveValidators: vi.fn(), + formatStakingAmount: vi.fn((val: bigint) => `${Number(val) / 1e18} GEN`), }; function setupActionMocks(action: any) { @@ -207,6 +209,15 @@ describe("StakingInfoAction", () => { action = new StakingInfoAction(); setupActionMocks(action); mockClient.getEpochInfo.mockResolvedValue(mockEpochInfo); + mockClient.getEpochData.mockResolvedValue({ + start: BigInt(Math.floor(Date.now() / 1000) - 3600), + end: 0n, + vcount: 5n, + weight: 100000n, + inflation: 1000n * BigInt(1e18), + claimed: 500n * BigInt(1e18), + slashed: 0n, + }); }); test("gets validator info", async () => { @@ -252,7 +263,7 @@ describe("StakingInfoAction", () => { test("gets epoch info", async () => { await action.getEpochInfo({stakingAddress: "0xStaking"}); - expect(action["succeedSpinner"]).toHaveBeenCalledWith("Epoch info retrieved", expect.any(Object)); + expect(action["succeedSpinner"]).toHaveBeenCalledWith("Epoch info"); }); test("lists active validators", async () => {