diff --git a/bin/near-cli.js b/bin/near-cli.js index 22459e1f..6caa5188 100644 --- a/bin/near-cli.js +++ b/bin/near-cli.js @@ -196,6 +196,7 @@ yargs // eslint-disable-line .command(require('../commands/repl')) .command(require('../commands/generate-key')) .command(require('../commands/validators')) + .command(require('../commands/proposals')) .config(config) .alias({ 'accountId': ['account_id'], diff --git a/commands/proposals.js b/commands/proposals.js new file mode 100644 index 00000000..dfa35caf --- /dev/null +++ b/commands/proposals.js @@ -0,0 +1,12 @@ +const exitOnError = require('../utils/exit-on-error'); +const connect = require('../utils/connect'); +const validatorsInfo = require('../utils/validators-info'); + +module.exports = { + command: 'proposals', + desc: 'lookup current proposals', + handler: exitOnError(async (argv) => { + const near = await connect(argv); + await validatorsInfo.showProposalsTable(near); + }) +}; diff --git a/commands/validators.js b/commands/validators.js index a12d692d..86e0a73a 100644 --- a/commands/validators.js +++ b/commands/validators.js @@ -1,66 +1,29 @@ const exitOnError = require('../utils/exit-on-error'); const connect = require('../utils/connect'); -const { validators, utils } = require('near-api-js'); -const BN = require('bn.js'); -const AsciiTable = require('ascii-table'); +const validatorsInfo = require('../utils/validators-info'); module.exports = { - command: 'EXPERIMENTAL_validators', - desc: 'lookup validators and proposals', + command: 'validators ', + desc: 'lookup validators for given epoch (or current / next)', + builder: (yargs) => yargs + .option('epoch', { + desc: 'epoch defined by block number or current / next', + type: 'string', + required: true + }), handler: exitOnError(async (argv) => { const near = await connect(argv); - const genesisConfig = await near.connection.provider.sendJsonRpc('EXPERIMENTAL_genesis_config', {}); - const result = await near.connection.provider.sendJsonRpc('validators', [null]); - - // Calculate all required data. - const numSeats = genesisConfig.num_block_producer_seats + genesisConfig.avg_hidden_validator_seats_per_shard.reduce((a, b) => a + b); - const seatPrice = validators.findSeatPrice(result.current_validators, numSeats); - const nextSeatPrice = validators.findSeatPrice(result.next_validators, numSeats); - - // Sort validators by their stake. - result.current_validators = result.current_validators.sort((a, b) => -new BN(a.stake).cmp(new BN(b.stake))); - result.next_validators = result.next_validators.sort((a, b) => -new BN(a.stake).cmp(new BN(b.stake))); - - var validatorsTable = new AsciiTable(); - validatorsTable.setHeading('Validator Id', 'Stake', '# seats', '% online', 'bls produced', 'bls expected'); - console.log(`Validators (total: ${result.current_validators.length}, seat price: ${utils.format.formatNearAmount(seatPrice, 0)}):`); - result.current_validators.forEach((validator) => { - validatorsTable.addRow( - validator.account_id, - utils.format.formatNearAmount(validator.stake, 0), - new BN(validator.stake).divRound(seatPrice), - Math.floor(validator.num_produced_blocks / validator.num_expected_blocks * 100), - validator.num_produced_blocks, - validator.num_expected_blocks); - }); - console.log(validatorsTable.toString()); - - if (result.current_fishermen) { - console.log(`\nFishermen (total: ${result.current_fishermen.length}):`); - var fishermenTable = new AsciiTable(); - fishermenTable.setHeading('Fisherman Id', 'Stake'); - result.current_fishermen.forEach((fisherman) => { - fishermenTable.addRow(fisherman.account_id, utils.format.formatNearAmount(fisherman.stake, 0)); - }); - console.log(fishermenTable.toString()); + switch (argv.epoch) { + case 'current': + await validatorsInfo.showValidatorsTable(near, null); + break; + case 'next': + await validatorsInfo.showNextValidatorsTable(near); + break; + default: + await validatorsInfo.showValidatorsTable(near, argv.epoch); + break; } - - const diff = validators.diffEpochValidators(result.current_validators, result.next_validators); - console.log(`\nNext validators (total: ${result.next_validators.length}, seat price: ${utils.format.formatNearAmount(nextSeatPrice, 0)}):`); - let nextValidatorsTable = new AsciiTable(); - nextValidatorsTable.setHeading('Status', 'Validator', 'Stake', '# seats'); - diff.newValidators.map((validator) => nextValidatorsTable.addRow( - 'New', - validator.account_id, - utils.format.formatNearAmount(validator.stake, 0), - new BN(validator.stake).divRound(nextSeatPrice))); - diff.changedValidators.map((changeValidator) => nextValidatorsTable.addRow( - 'Rewarded', - changeValidator.next.account_id, - `${utils.format.formatNearAmount(changeValidator.current.stake, 0)} -> ${utils.format.formatNearAmount(changeValidator.next.stake, 0)}`, - new BN(changeValidator.next.stake).divRound(nextSeatPrice))); - diff.removedValidators.map((validator) => nextValidatorsTable.addRow('Kicked out', validator.account_id, '-', '-')); - console.log(nextValidatorsTable.toString()); }) }; diff --git a/utils/validators-info.js b/utils/validators-info.js new file mode 100644 index 00000000..b380a497 --- /dev/null +++ b/utils/validators-info.js @@ -0,0 +1,91 @@ +const { validators, utils } = require('near-api-js'); +const BN = require('bn.js'); +const AsciiTable = require('ascii-table'); + +async function validatorsInfo(near, epochId) { + const genesisConfig = await near.connection.provider.sendJsonRpc('EXPERIMENTAL_genesis_config', {}); + const result = await near.connection.provider.sendJsonRpc('validators', [epochId]); + result.genesisConfig = genesisConfig; + result.numSeats = genesisConfig.num_block_producer_seats + genesisConfig.avg_hidden_validator_seats_per_shard.reduce((a, b) => a + b); + return result; +} + +async function showValidatorsTable(near, epochId) { + const result = await validatorsInfo(near, epochId); + const seatPrice = validators.findSeatPrice(result.current_validators, result.numSeats); + result.current_validators = result.current_validators.sort((a, b) => -new BN(a.stake).cmp(new BN(b.stake))); + var validatorsTable = new AsciiTable(); + validatorsTable.setHeading('Validator Id', 'Stake', '# seats', '% online', 'bls produced', 'bls expected'); + console.log(`Validators (total: ${result.current_validators.length}, seat price: ${utils.format.formatNearAmount(seatPrice, 0)}):`); + result.current_validators.forEach((validator) => { + validatorsTable.addRow( + validator.account_id, + utils.format.formatNearAmount(validator.stake, 0), + new BN(validator.stake).divRound(seatPrice), + Math.floor(validator.num_produced_blocks / validator.num_expected_blocks * 100), + validator.num_produced_blocks, + validator.num_expected_blocks); + }); + console.log(validatorsTable.toString()); +} + +async function showNextValidatorsTable(near) { + const result = await validatorsInfo(near, null); + const nextSeatPrice = validators.findSeatPrice(result.next_validators, result.numSeats); + const diff = validators.diffEpochValidators(result.current_validators, result.next_validators); + console.log(`\nNext validators (total: ${result.next_validators.length}, seat price: ${utils.format.formatNearAmount(nextSeatPrice, 0)}):`); + let nextValidatorsTable = new AsciiTable(); + nextValidatorsTable.setHeading('Status', 'Validator', 'Stake', '# seats'); + diff.newValidators.forEach((validator) => nextValidatorsTable.addRow( + 'New', + validator.account_id, + utils.format.formatNearAmount(validator.stake, 0), + new BN(validator.stake).divRound(nextSeatPrice))); + diff.changedValidators.forEach((changeValidator) => nextValidatorsTable.addRow( + 'Rewarded', + changeValidator.next.account_id, + `${utils.format.formatNearAmount(changeValidator.current.stake, 0)} -> ${utils.format.formatNearAmount(changeValidator.next.stake, 0)}`, + new BN(changeValidator.next.stake).divRound(nextSeatPrice))); + diff.removedValidators.forEach((validator) => nextValidatorsTable.addRow('Kicked out', validator.account_id, '-', '-')); + console.log(nextValidatorsTable.toString()); +} + +function combineValidatorsAndProposals(validators, proposalsMap) { + // TODO: filter out all kicked out validators. + let result = validators.filter((validator) => !proposalsMap.has(validator.account_id)); + return result.concat([...proposalsMap.values()]); +} + +async function showProposalsTable(near) { + const result = await validatorsInfo(near, null); + let currentValidators = new Map(); + result.current_validators.forEach((v) => currentValidators.set(v.account_id, v)); + let proposals = new Map(); + result.current_proposals.forEach((p) => proposals.set(p.account_id, p)); + const combinedProposals = combineValidatorsAndProposals(result.current_validators, proposals); + const expectedSeatPrice = validators.findSeatPrice(combinedProposals, result.numSeats); + console.log(`Proposals (total: ${proposals.size})`); + console.log(`Expected seat price = ${utils.format.formatNearAmount(expectedSeatPrice, 0)}`); + const proposalsTable = new AsciiTable(); + combinedProposals.sort((a, b) => -new BN(a.stake).cmp(new BN(b.stake))).forEach((proposal) => { + let kind = ''; + if (new BN(proposal.stake).gte(expectedSeatPrice)) { + kind = proposals.has(proposal.account_id) ? 'New' : 'Rollover'; + } else { + kind = proposals.has(proposal.account_id) ? 'Declined' : 'Kicked out'; + } + let stake_fmt = utils.format.formatNearAmount(proposal.stake, 0); + if (currentValidators.has(proposal.account_id) && proposals.has(proposal.account_id)) { + stake_fmt = `${utils.format.formatNearAmount(currentValidators.get(proposal.account_id).stake, 0)} => ${stake_fmt}`; + } + proposalsTable.addRow( + kind, + proposal.account_id, + stake_fmt + ); + }); + console.log(proposalsTable.toString()); + console.log("Note: this currently doesn't account for offline kickouts and rewards for current epoch"); +} + +module.exports = { showValidatorsTable, showNextValidatorsTable, showProposalsTable }; \ No newline at end of file