diff --git a/.github/workflows/test-matrix.yml b/.github/workflows/test-matrix.yml index 2dd93eed..d04b8a97 100644 --- a/.github/workflows/test-matrix.yml +++ b/.github/workflows/test-matrix.yml @@ -7,6 +7,7 @@ on: jobs: tests: strategy: + fail-fast: false matrix: platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} diff --git a/bin/near-cli.js b/bin/near-cli.js index d72db0da..c757c20c 100644 --- a/bin/near-cli.js +++ b/bin/near-cli.js @@ -21,6 +21,8 @@ yargs // eslint-disable-line .command(require('../commands/account/state')) .command(require('../commands/contract/storage')) .command(require('../commands/transactions/status')) + .command(require('../commands/validators/stake')) + .command(require('../commands/validators/validators')) .command(require('../commands/contract/view')) .command(require('../commands/deprecated')) .option('verbose', { alias: ['v'], desc: 'Prints out verbose output', type: 'boolean', default: false }) diff --git a/commands/account/create.js b/commands/account/create.js index 7fca52a3..a69f3691 100644 --- a/commands/account/create.js +++ b/commands/account/create.js @@ -34,7 +34,7 @@ module.exports = { required: false }) .option('networkId', { - desc: 'Which network to use. Supports: mainnet, testnet', + desc: 'Which network to use. Supports: mainnet, testnet, custom', type: 'string', default: DEFAULT_NETWORK }), @@ -65,7 +65,7 @@ async function create(options) { } if (options.useFaucet) { - if (options.networkId !== 'testnet') throw new Error('Pre-funding accounts is only possible on testnet'); + if (options.networkId === 'mainnet') throw new Error('Pre-funding accounts is not possible on mainnet'); } else { if (!options.useAccount) throw new Error('Please specify an account to sign the transaction (--useAccount)'); await assertCredentials(options.useAccount, options.networkId, options.keyStore); @@ -117,11 +117,14 @@ async function create(options) { try { // Handle response const response = await promise; + if (keyPair) { storeCredentials(newAccountId, options.networkId, options.keyStore, keyPair, true); - } else { - console.log(chalk`{bold.white ${newAccountId}} created successfully, please add its credentials manually.`); } + + // The faucet does not throw on error, so we force it here + if (options.useFaucet) { await response.state(); } + inspectResponse.prettyPrintResponse(response, options); } catch (error) { // Handle errors @@ -144,6 +147,11 @@ async function create(options) { console.error(chalk`\nYou cannot create Top Level Accounts.`); process.exit(1); break; + case 'AccountDoesNotExist': + if (!options.useFaucet) throw error; + console.error(chalk`\nThe faucet reported {bold.white no errors}, but we {bold.red cannot} find ${options.newAccountId}. Check if it exists with "near state ${options.newAccountId} --networkId ${options.networkId}".\n`); + process.exit(1); + break; default: throw error; } diff --git a/commands/account/delete.js b/commands/account/delete.js index d162c077..a08d7fe4 100644 --- a/commands/account/delete.js +++ b/commands/account/delete.js @@ -11,7 +11,7 @@ module.exports = { desc: 'Delete account, sending remaining NEAR to a beneficiary', builder: (yargs) => yargs .option('networkId', { - desc: 'Which network to use. Supports: mainnet, testnet', + desc: 'Which network to use. Supports: mainnet, testnet, custom', type: 'string', default: DEFAULT_NETWORK }) @@ -25,7 +25,7 @@ module.exports = { const confirmDelete = function (accountId, beneficiaryId) { return askYesNoQuestion( - chalk`This will {bold.red delete ${accountId}}, transferring {bold.white the remaining NEAR tokens} to the {bold.green beneficiary ${beneficiaryId}}. This action will {bold.red NOT transfer} {bold.white FTs, NFTs} or other assets the account holds, make sure you to {bold.white manually transfer} them before deleting or they will be {bold.red lost}. Do you want to proceed? {bold.green (y/n)}`, + chalk`This will {bold.red delete ${accountId}}, transferring {bold.white the remaining NEAR tokens} to the {bold.green beneficiary ${beneficiaryId}}. This action will {bold.red NOT transfer} {bold.white FTs, NFTs} or other assets the account holds, make sure you to {bold.white manually transfer} them before deleting or they will be {bold.red lost}. Do you want to proceed? {bold.green (y/n)} `, false); }; @@ -46,13 +46,30 @@ async function deleteAccount(options) { } } - if (options.force || await confirmDelete(options.accountId, options.beneficiaryId)) { - const account = await near.account(options.accountId); - console.log(`Deleting account ${options.accountId}, beneficiary: ${options.beneficiaryId}`); + if (!options.force && !(await confirmDelete(options.accountId, options.beneficiaryId))) { + return console.log(chalk`{bold.white Deletion of account {bold.blue ${options.accountId}} was {bold.red cancelled}}`); + } + + const account = await near.account(options.accountId); + console.log(`Deleting account ${options.accountId}, beneficiary: ${options.beneficiaryId}`); + + try { const result = await account.deleteAccount(options.beneficiaryId); console.log(`Account ${options.accountId} for network "${options.networkId}" was deleted.`); inspectResponse.prettyPrintResponse(result, options); - } else { - console.log(chalk`{bold.white Deletion of account {bold.blue ${options.accountId}} was {bold.red cancelled}}`); + } catch (error) { + switch (error.type) { + case 'KeyNotFound': + console.log(chalk`\n{bold.white ${options.accountId}} was not found in the network ${options.networkId}\n`); + process.exit(1); + break; + case 'SignerDoesNotExist': + // On re-sending a transaction, the signer might have been deleted already + console.log('RPC returned an error, please check if the account is deleted and try again'); + process.exit(0); + break; + default: + throw error; + } } } \ No newline at end of file diff --git a/commands/account/login.js b/commands/account/login.js index dc1fb2b8..5eb99c4e 100644 --- a/commands/account/login.js +++ b/commands/account/login.js @@ -12,7 +12,7 @@ module.exports = { desc: 'Login through a web wallet (default: MyNearWallet)', builder: (yargs) => yargs .option('networkId', { - desc: 'Which network to use. Supports: mainnet, testnet', + desc: 'Which network to use. Supports: mainnet, testnet, custom', type: 'string', default: DEFAULT_NETWORK }), diff --git a/commands/account/state.js b/commands/account/state.js index a6a408c0..d248e6e6 100644 --- a/commands/account/state.js +++ b/commands/account/state.js @@ -8,7 +8,7 @@ module.exports = { desc: 'View account\'s state (balance, storage_usage, code_hash, etc...)', builder: (yargs) => yargs .option('networkId', { - desc: 'Which network to use. Supports: mainnet, testnet', + desc: 'Which network to use. Supports: mainnet, testnet, custom', type: 'string', default: DEFAULT_NETWORK }), diff --git a/commands/contract/call.js b/commands/contract/call.js index 47de1520..1f9c2797 100644 --- a/commands/contract/call.js +++ b/commands/contract/call.js @@ -17,7 +17,7 @@ module.exports = { type: 'string' }) .option('networkId', { - desc: 'Which network to use. Supports: mainnet, testnet', + desc: 'Which network to use. Supports: mainnet, testnet, custom', type: 'string', default: DEFAULT_NETWORK }) diff --git a/commands/contract/deploy.js b/commands/contract/deploy.js index 68fefec6..a5807e4b 100644 --- a/commands/contract/deploy.js +++ b/commands/contract/deploy.js @@ -35,7 +35,7 @@ module.exports = { default: '0' }) .option('networkId', { - desc: 'Which network to use. Supports: mainnet, testnet', + desc: 'Which network to use. Supports: mainnet, testnet, custom', type: 'string', default: DEFAULT_NETWORK }) diff --git a/commands/contract/storage.js b/commands/contract/storage.js index f0455599..dfbe1e37 100644 --- a/commands/contract/storage.js +++ b/commands/contract/storage.js @@ -8,7 +8,7 @@ module.exports = { desc: 'View contract\'s storage (stored key-value pairs)', builder: (yargs) => yargs .option('networkId', { - desc: 'Which network to use. Supports: mainnet, testnet', + desc: 'Which network to use. Supports: mainnet, testnet, custom', type: 'string', default: DEFAULT_NETWORK }) diff --git a/commands/contract/view.js b/commands/contract/view.js index 1c04d09c..fb6076a1 100644 --- a/commands/contract/view.js +++ b/commands/contract/view.js @@ -13,7 +13,7 @@ module.exports = { default: null }) .option('networkId', { - desc: 'Which network to use. Supports: mainnet, testnet', + desc: 'Which network to use. Supports: mainnet, testnet, custom', type: 'string', default: DEFAULT_NETWORK }), diff --git a/commands/credentials/add.js b/commands/credentials/add.js index 5c303825..867d6119 100644 --- a/commands/credentials/add.js +++ b/commands/credentials/add.js @@ -18,7 +18,7 @@ module.exports = { required: false, }) .option('networkId', { - desc: 'Which network to use. Supports: mainnet, testnet', + desc: 'Which network to use. Supports: mainnet, testnet, custom', type: 'string', default: DEFAULT_NETWORK }) diff --git a/commands/credentials/generate.js b/commands/credentials/generate.js index 865915a9..e3d35b29 100644 --- a/commands/credentials/generate.js +++ b/commands/credentials/generate.js @@ -20,7 +20,7 @@ module.exports = { default: false }) .option('networkId', { - desc: 'Which network to use. Supports: mainnet, testnet', + desc: 'Which network to use. Supports: mainnet, testnet, custom', type: 'string', default: DEFAULT_NETWORK }) diff --git a/commands/deprecated.js b/commands/deprecated.js index 4c5953ba..84923696 100644 --- a/commands/deprecated.js +++ b/commands/deprecated.js @@ -2,7 +2,7 @@ const chalk = require('chalk'); module.exports = [ { - command: ['clean', 'repl', 'stake', 'evm-call', 'evm-view', 'set-api-key', 'js', 'validators', 'proposals'], + command: ['clean', 'repl', 'evm-call', 'evm-view', 'set-api-key', 'js'], desc: false, handler: deprecated }, @@ -10,7 +10,13 @@ module.exports = [ command: 'dev-deploy [...]', desc: false, handler: deprecatedDevDeploy + }, + { + command: 'proposals', + desc: false, + handler: deprecatedProposals } + ]; async function deprecated() { @@ -20,4 +26,9 @@ async function deprecated() { async function deprecatedDevDeploy() { console.log(chalk`\nThis command has been {bold.red deprecated}`); console.log(chalk`Please use: {bold.white near create-account --useFaucet} to create a pre-funded account, and then {bold.white near deploy} to deploy the contract\n`); +} + +async function deprecatedProposals() { + console.log(chalk`\nThis command has been {bold.red deprecated}`); + console.log(chalk`Please use: {bold.white near validators proposals`); } \ No newline at end of file diff --git a/commands/keys/add.js b/commands/keys/add.js index 39ae2a77..f7296dfe 100644 --- a/commands/keys/add.js +++ b/commands/keys/add.js @@ -25,7 +25,7 @@ module.exports = { default: '0' }) .option('networkId', { - desc: 'Which network to use. Supports: mainnet, testnet', + desc: 'Which network to use. Supports: mainnet, testnet, custom', type: 'string', default: DEFAULT_NETWORK }), diff --git a/commands/keys/delete.js b/commands/keys/delete.js index a618e1d1..98540673 100644 --- a/commands/keys/delete.js +++ b/commands/keys/delete.js @@ -10,7 +10,7 @@ module.exports = { desc: 'Delete access key', builder: (yargs) => yargs .option('networkId', { - desc: 'Which network to use. Supports: mainnet, testnet', + desc: 'Which network to use. Supports: mainnet, testnet, custom', type: 'string', default: DEFAULT_NETWORK }) diff --git a/commands/keys/list.js b/commands/keys/list.js index eeedf909..1d8f096d 100644 --- a/commands/keys/list.js +++ b/commands/keys/list.js @@ -8,7 +8,7 @@ module.exports = { desc: 'Query public keys of an account', builder: (yargs) => yargs .option('networkId', { - desc: 'Which network to use. Supports: mainnet, testnet', + desc: 'Which network to use. Supports: mainnet, testnet, custom', type: 'string', default: DEFAULT_NETWORK }), diff --git a/commands/transactions/send.js b/commands/transactions/send.js index f2cf6119..a8521464 100644 --- a/commands/transactions/send.js +++ b/commands/transactions/send.js @@ -15,7 +15,7 @@ module.exports = { type: 'string', }) .option('networkId', { - desc: 'Which network to use. Supports: mainnet, testnet', + desc: 'Which network to use. Supports: mainnet, testnet, custom', type: 'string', default: DEFAULT_NETWORK }), diff --git a/commands/transactions/status.js b/commands/transactions/status.js index 8db19497..8257c067 100644 --- a/commands/transactions/status.js +++ b/commands/transactions/status.js @@ -17,7 +17,7 @@ module.exports = { type: 'string', }) .option('networkId', { - desc: 'Which network to use. Supports: mainnet, testnet', + desc: 'Which network to use. Supports: mainnet, testnet, custom', type: 'string', default: DEFAULT_NETWORK }), diff --git a/commands/validators/stake.js b/commands/validators/stake.js new file mode 100644 index 00000000..343dc63a --- /dev/null +++ b/commands/validators/stake.js @@ -0,0 +1,45 @@ +const qs = require('querystring'); +const { utils } = require('near-api-js'); + +const connect = require('../../utils/connect'); +const inspectResponse = require('../../utils/inspect-response'); +const { assertCredentials } = require('../../utils/credentials'); +const { DEFAULT_NETWORK } = require('../../config'); + +module.exports = { + command: 'validator-stake accountId stakingKey amount', + aliases: ['stake'], + desc: 'Create a staking transaction (for **validators** only)', + builder: (yargs) => yargs + .option('accountId', { + desc: 'Account that wants to become a network validator', + type: 'string', + required: true, + }) + .option('stakingKey', { + desc: 'Public key to stake with (base58 encoded)', + type: 'string', + required: true, + }) + .option('amount', { + desc: 'Amount to stake', + type: 'string', + required: true, + }) + .option('networkId', { + desc: 'Which network to use. Supports: mainnet, testnet, custom', + type: 'string', + default: DEFAULT_NETWORK + }), + handler: stake +}; + + +async function stake(options) { + await assertCredentials(options.accountId, options.networkId, options.keyStore); + console.log(`Staking ${options.amount} (${utils.format.parseNearAmount(options.amount)}N) on ${options.accountId} with public key = ${qs.unescape(options.stakingKey)}.`); + const near = await connect(options); + const account = await near.account(options.accountId); + const result = await account.stake(qs.unescape(options.stakingKey), utils.format.parseNearAmount(options.amount)); + inspectResponse.prettyPrintResponse(result, options); +} \ No newline at end of file diff --git a/commands/validators/validators.js b/commands/validators/validators.js new file mode 100644 index 00000000..4d84d807 --- /dev/null +++ b/commands/validators/validators.js @@ -0,0 +1,41 @@ +const { DEFAULT_NETWORK } = require('../../config'); +const connect = require('../../utils/connect'); +const validatorsInfo = require('../../utils/validators-info'); + +module.exports = { + command: 'validators ', + desc: 'Info on validators', + handler: validators, + builder: (yargs) => yargs + .option('query', + { + desc: 'current | next | | to lookup validators at a specific epoch, or "proposals" to see the current proposals', + type: 'string', + default: 'current' + } + ) + .option('networkId', { + desc: 'Which network to use. Supports: mainnet, testnet, custom', + type: 'string', + default: DEFAULT_NETWORK + }), +}; + +async function validators(options) { + const near = await connect(options); + + switch (options.query) { + case 'proposals': + await validatorsInfo.showProposalsTable(near); + break; + case 'current': + await validatorsInfo.showValidatorsTable(near, null); + break; + case 'next': + await validatorsInfo.showNextValidatorsTable(near); + break; + default: + await validatorsInfo.showValidatorsTable(near, options.query); + break; + } +} \ No newline at end of file diff --git a/config.js b/config.js index 748c6e6c..6f49f8c5 100644 --- a/config.js +++ b/config.js @@ -23,6 +23,15 @@ function getConfig(env) { helperAccount: 'testnet', }; break; + case 'custom': + config = { + networkId: 'custom', + nodeUrl: process.env.NEAR_CUSTOM_RPC, + walletUrl: process.env.NEAR_CUSTOM_WALLET, + helperUrl: process.env.NEAR_CUSTOM_HELPER, + helperAccount: process.env.NEAR_CUSTOM_TLA, + }; + break; default: throw Error(`Unconfigured environment '${env}'. Can be configured in src/config.js.`); } diff --git a/package.json b/package.json index fc9dce2b..316b8da4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "near-cli", - "version": "4.0.2", + "version": "4.0.4", "description": "Simple CLI for interacting with NEAR Protocol", "engines": { "node": ">= 16" @@ -36,6 +36,7 @@ "typescript": "^5.3.3" }, "dependencies": { + "ascii-table": "^0.0.9", "bs58": "^5.0.0", "chalk": "^4.1.2", "flagged-respawn": "^2.0.0", diff --git a/test/test_account_creation.sh b/test/test_account_creation.sh index 0424cd50..0aa813d8 100755 --- a/test/test_account_creation.sh +++ b/test/test_account_creation.sh @@ -2,12 +2,12 @@ set -e timestamp=$(date +%s) -testaccount1=testaccount${timestamp}-1.testnet -testaccount2=testaccount${timestamp}-2.testnet -testaccount3=testaccount${timestamp}-3.testnet -testaccount4=testaccount${timestamp}-4.testnet -zerobalance=testaccount${timestamp}-z.testnet -withbalance=testaccount${timestamp}-y.testnet +testaccount1=test-ac-${timestamp}-1.testnet +testaccount2=test-ac-${timestamp}-2.testnet +testaccount3=test-ac-${timestamp}-3.testnet +testaccount4=test-ac-${timestamp}-4.testnet +zerobalance=test-ac-${timestamp}-z.testnet +withbalance=test-ac-${timestamp}-y.testnet tla=${timestamp}${timestamp}${timestamp}12 @@ -85,7 +85,7 @@ fi # Cannot create a useFaucet account in mainnet ERROR=$(./bin/near create-account $testaccount4 --useFaucet --networkId mainnet 2>&1) -EXPECTED_ERROR=".+Pre-funding accounts is only possible on testnet.+" +EXPECTED_ERROR=".+Pre-funding accounts is not possible on mainnet.+" if [[ ! "$ERROR" =~ $EXPECTED_ERROR ]]; then echo FAILURE Unexpected output when funding pre-funded account in mainnet echo Got: $ERROR diff --git a/test/test_account_operations.sh b/test/test_account_operations.sh index e9fcacd0..56f63888 100755 --- a/test/test_account_operations.sh +++ b/test/test_account_operations.sh @@ -2,7 +2,7 @@ set -e timestamp=$(date +%s) -testaccount=testaccount$timestamp$RANDOM.testnet +testaccount=test-ao-$timestamp.testnet ./bin/near create-account $testaccount --useFaucet diff --git a/test/test_contract.sh b/test/test_contract.sh index cd0a04ce..2a7c1d26 100755 --- a/test/test_contract.sh +++ b/test/test_contract.sh @@ -2,8 +2,8 @@ set -e timestamp=$(date +%s) -contract=testaccount$timestamp-c.testnet -testaccount=testaccount$timestamp-t.testnet +contract=test-contract-$timestamp-c.testnet +testaccount=test-contract-$timestamp-t.testnet echo Creating account ./bin/near create $contract --useFaucet diff --git a/test/test_deploy_init_contract.sh b/test/test_deploy_init_contract.sh index 8b924464..23350971 100755 --- a/test/test_deploy_init_contract.sh +++ b/test/test_deploy_init_contract.sh @@ -1,7 +1,7 @@ #!/bin/bash timestamp=$(date +%s) -contract=testaccount$timestamp.testnet +contract=test-deploy-$timestamp.testnet echo Creating account ./bin/near create $contract --useFaucet diff --git a/test/test_keys.sh b/test/test_keys.sh index ca884946..8e8210e1 100755 --- a/test/test_keys.sh +++ b/test/test_keys.sh @@ -2,7 +2,7 @@ set -e timestamp=$(date +%s) -testaccount=testaccount${timestamp}.testnet +testaccount=test-keys-${timestamp}.testnet # Can create a pre-funded account ./bin/near create-account $testaccount --useFaucet diff --git a/utils/validators-info.js b/utils/validators-info.js new file mode 100644 index 00000000..c6e91fcf --- /dev/null +++ b/utils/validators-info.js @@ -0,0 +1,121 @@ +const { validators, utils } = require('near-api-js'); +const BN = require('bn.js'); +const AsciiTable = require('ascii-table'); + +async function validatorsInfo(near, blockNumberOrHash) { + // converts block number to integer + if (blockNumberOrHash && !isNaN(Number(blockNumberOrHash))) { + blockNumberOrHash = Number(blockNumberOrHash); + } + const genesisConfig = await near.connection.provider.sendJsonRpc('EXPERIMENTAL_genesis_config', {}); + const protocolConfig = await near.connection.provider.sendJsonRpc('EXPERIMENTAL_protocol_config', { 'finality': 'final' }); + const result = await near.connection.provider.sendJsonRpc('validators', [blockNumberOrHash]); + result.genesisConfig = genesisConfig; + result.protocolConfig = protocolConfig; + 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, blockNumberOrHash) { + const result = await validatorsInfo(near, blockNumberOrHash); + const seatPrice = validators.findSeatPrice( + result.current_validators, + result.numSeats, + result.genesisConfig.minimum_stake_ratio, + result.protocolConfig.protocol_version); + 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', 'Blocks produced', 'Blocks expected', 'Chunks produced', 'Chunks 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), + getNumberOfSeats(result.protocolConfig.protocol_version, validator.stake, seatPrice), + `${Math.floor((validator.num_produced_blocks + validator.num_produced_chunks) / (validator.num_expected_blocks + validator.num_expected_chunks) * 10000) / 100}%`, + validator.num_produced_blocks, + validator.num_expected_blocks, + validator.num_produced_chunks, + validator.num_expected_chunks); + }); + console.log(validatorsTable.toString()); +} + +async function showNextValidatorsTable(near) { + const result = await validatorsInfo(near, null); + const nextSeatPrice = validators.findSeatPrice( + result.next_validators, + result.numSeats, + result.genesisConfig.minimum_stake_ratio, + result.protocolConfig.protocol_version); + result.next_validators = result.next_validators.sort((a, b) => -new BN(a.stake).cmp(new BN(b.stake))); + 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), + getNumberOfSeats(result.protocolConfig.protocol_version, validator.stake, 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)}`, + getNumberOfSeats(result.protocolConfig.protocol_version, changeValidator.next.stake, 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, + result.genesisConfig.minimum_stake_ratio, + result.protocolConfig.protocol_version); + const combinedPassingProposals = combinedProposals.filter((p) => new BN(p.stake).gte(expectedSeatPrice)); + console.log(`Proposals for the epoch after next (new: ${proposals.size}, passing: ${combinedPassingProposals.length}, expected seat price = ${utils.format.formatNearAmount(expectedSeatPrice, 0)})`); + const proposalsTable = new AsciiTable(); + proposalsTable.setHeading('Status', 'Validator', 'Stake => New Stake', '# Seats'); + 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) ? 'Proposal(Accepted)' : 'Rollover'; + } else { + kind = proposals.has(proposal.account_id) ? 'Proposal(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, + getNumberOfSeats(result.protocolConfig.protocol_version, proposal.stake, expectedSeatPrice) + ); + }); + console.log(proposalsTable.toString()); + console.log('Expected seat price is calculated based on observed so far proposals and validators.'); + console.log('It can change from new proposals or some validators going offline.'); + console.log('Note: this currently doesn\'t account for offline kickouts and rewards for current epoch'); +} + +// starting from protocol version 49 each validator has 1 seat +function getNumberOfSeats(protocolVersion, stake, seatPrice) { + return protocolVersion < 49 ? new BN(stake).div(seatPrice) : new BN(1); +} + +module.exports = { showValidatorsTable, showNextValidatorsTable, showProposalsTable }; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index a5a18319..499d63b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1243,6 +1243,11 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +ascii-table@^0.0.9: + version "0.0.9" + resolved "https://registry.yarnpkg.com/ascii-table/-/ascii-table-0.0.9.tgz#06a6604d6a55d4bf41a9a47d9872d7a78da31e73" + integrity sha512-xpkr6sCDIYTPqzvjG8M3ncw1YOTaloWZOyrUmicoEifBEKzQzt+ooUpRpQ/AbOoJfO/p2ZKiyp79qHThzJDulQ== + async-retry@1.2.3: version "1.2.3" resolved "https://registry.npmjs.org/async-retry/-/async-retry-1.2.3.tgz"