staker
Version:
Command line Utillity for Ethereum stakers
401 lines (352 loc) • 14.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.skillsValidator = exports.skillsETH2 = exports.skills = exports.skillValidatorPicker = exports.skillSearchBeaconscan = exports.skillSearchBeaconchain = exports.skillGetValidatorBalance = exports.skillEth2Stats = exports.skillBeaconLastBlock = exports.calcWaitTime = exports.calcAPR = void 0;
var _react = _interopRequireWildcard(require("react"));
var _ink = require("ink");
var _humanizeDuration = _interopRequireDefault(require("humanize-duration"));
var _commaNumber = _interopRequireDefault(require("comma-number"));
var _i18n = require("saihubot-cli-adapter/dist/i18n");
var _stakerFreenodes = require("staker-freenodes");
var _ValidatorBalances = _interopRequireDefault(require("./ValidatorBalances"));
var _ethRpc = require("../helpers/ethRpc");
var _utils = require("../utils");
var _i18n2 = require("../i18n");
var _useNativeTokenBalance = _interopRequireDefault(require("../eth/useNativeTokenBalance"));
var _useCoingeckoTokenStat = _interopRequireDefault(require("../eth/useCoingeckoTokenStat"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
// import AsciiBar from 'ascii-bar';
const ADDR = {
ETH2_DEPOSIT: '0x00000000219ab540356cbb839cbe05303d7705fa'
};
const statsI18n = {
"en": {
fetching: 'Fetching data...',
summary: `💰 Deposited ETH: {{balance}} (for {{validators}} 🧑🌾)
🪣 Trapped ETH: $\{{sum}}B ({{circulating}}% of circulating supply)`,
statistics: `🤑 Reward Rate: {{apr}}%
🌾 Participation Rate: {{participationRate}}%
💃 Active Validators: {{activeValidator}}
📦 Latest Epoch: {{epoch}}
👬 Queued Validators: {{queueValidator}}
⏳ Wait time: {{waitTime}}
`
},
"zh_TW": {
fetching: '取得資料中...',
summary: `💰 共存入 ETH: {{balance}} (可支持 {{validators}} 🧑🌾)
🪣 鎖倉的ETH: $\{{sum}}B (占總流通量 {{circulating}}%)
`,
statistics: `🤑 預估收益率: {{apr}}%
🌾 參與度: {{participationRate}}%
💃 活躍驗證者: {{activeValidator}}
📦 最近的 Epoch: {{epoch}}
👬 排隊中的驗證者: {{queueValidator}}
⏳ 預估等待時間: {{waitTime}}
`
},
props: ['apr', 'balance', 'validators', 'activeValidator', 'participationRate', 'epoch', 'queueValidator', 'waitTime', 'circulating', 'sum']
}; // https://github.com/TheRyanMiller/Eth2RewardsCalc/blob/master/getBeaconData.js
const calcWaitTime = (queueLength, activeValidator) => {
//225 Epochs per day (1 epoch = 32 * 12s slots)
//900 validators can be activated per day (4 per epoch)
// https://www.reddit.com/r/ethstaker/comments/k9wf4x/estimated_timeline_of_apr_and_eth_staked/gf7m0rf/
const daylyValidators = 225 * Math.max(Math.floor(activeValidator / 6500), 4); // need wait at least every 96 seconds per validator
const perWaitTime = Math.floor(24 * 60 * 60 / daylyValidators);
let time = 0;
if (queueLength > 0) time = perWaitTime * queueLength;
return (0, _humanizeDuration.default)(time * 1000, {
round: true,
units: ["d", "h"]
});
}; // https://www.reddit.com/r/ethstaker/comments/k7e9k0/what_will_be_the_minimum_apr_rate_for_eth2_stake/gexwpzq/
exports.calcWaitTime = calcWaitTime;
const calcAPR = validatorscount => (14300 / Math.sqrt(validatorscount)).toFixed(2);
exports.calcAPR = calcAPR;
const Eth2Stats = ({
fetch,
ethFetch
}) => {
const [beaconData, setBeaconData] = (0, _react.useState)({});
const [loading, balances] = (0, _useNativeTokenBalance.default)({
addresses: [ADDR.ETH2_DEPOSIT],
fetch,
networkFetch: ethFetch
});
const [tokenInfo] = (0, _useCoingeckoTokenStat.default)(fetch, 'ethereum');
(0, _react.useEffect)(() => {
async function fetchLatest() {
fetch('https://beaconcha.in/api/v1/epoch/latest').then(response => response.json()).then(json => setBeaconData(json.data));
}
fetchLatest();
}, [fetch]);
const balance = balances && balances[0] && balances[0]['balance'] && Math.floor(balances[0]['balance']) || 0; // const percent = balance/524288;
const validators = balance && Math.floor(balance / 32); // const message = `${parseFloat(percent * 100).toFixed(2)}%`;
// const barSize = 20;
// const bar = new AsciiBar({
// formatString: '#bar #message',
// undoneSymbol: "░",
// doneSymbol: "▓",
// width: barSize,
// total: barSize,
// start: Math.min(Math.round(percent * barSize), barSize),
// enableSpinner: false,
// lastUpdateForTiming: false,
// message,
// });
// more accurate: active - exit
const queueValidator = beaconData && validators - beaconData.validatorscount;
const stats = (0, _i18n.t)('statistics', {
i18n: statsI18n,
validators: balance && (0, _commaNumber.default)(validators),
activeValidator: beaconData && (0, _commaNumber.default)(beaconData.validatorscount),
participationRate: beaconData && Number(beaconData.globalparticipationrate * 100).toFixed(2),
epoch: beaconData && beaconData.epoch,
queueValidator: (0, _commaNumber.default)(queueValidator),
waitTime: beaconData && calcWaitTime(queueValidator, beaconData.validatorscount),
apr: beaconData && calcAPR(beaconData.totalvalidatorbalance / 10 ** 9)
});
const summary = (0, _i18n.t)('summary', {
i18n: statsI18n,
balance: (0, _commaNumber.default)(balance),
validators: balance && (0, _commaNumber.default)(validators),
circulating: balance && tokenInfo && tokenInfo[0] && Number(balance * 100 / tokenInfo[0].circulating_supply).toFixed(2),
sum: balance && tokenInfo && tokenInfo[0] && Number(balance * tokenInfo[0].current_price / 10 ** 9).toFixed(2)
});
return !loading ? /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_ink.Text, null, stats), /*#__PURE__*/_react.default.createElement(_ink.Text, null, summary)) : /*#__PURE__*/_react.default.createElement(_ink.Text, null, (0, _i18n.t)('fetching', {
i18n: statsI18n
}));
};
/**
* Get Eth2 stake state.
*
* 🤑 Reward Rate: 9.92%
* 🌾 Participation Rate: 99.05%
* 💃 Active Validators: 64,224
* 📦 Latest Epoch: 10844
*
* 👬 Queued Validators: 16,642
* ⏳ Wait time: 8 days, 2 hours
*
* 💰 Deposited ETH: 2,587,714 (for 80,866 🧑🌾)
* 🪣 Trapped ETH%: 2.26%
*/
const skillEth2Stats = {
name: 'stakestat',
help: '🗞 stats - latest Eth2 stake state',
requirements: {
addons: ['fetch']
},
rule: /^stats$|^stats-eth2$/i,
action: function (robot, msg) {
robot.sendComponent( /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(Eth2Stats, {
fetch: robot.addons.fetch,
ethFetch: _ethRpc.ethFetch
})));
robot.render();
}
};
/**
* Get the latest Eth2 block number.
*/
exports.skillEth2Stats = skillEth2Stats;
const skillBeaconLastBlock = {
name: 'lastBlockBeacon',
help: '🗂 lastblock-(eth2|beacon|validator)|block-(eth2|beacon|validator) - get the latest Eth2 block number',
requirements: {
addons: ['fetch']
},
i18n: {
'en': {
fetching: 'Fetching data...',
summary: 'The latest BeaconChain Epoch **#{{epoch}}** Slot **#{{slot}}** (proposed by **#{{proposer}}**)'
},
'zh_TW': {
fetching: '取得資料中...',
summary: '最新的 BeaconChain Epoch **#{{epoch}}**Slot **#{{slot}}** (出塊者 **#{{proposer}}**)'
},
props: ['epoch', 'proposer', 'slot']
},
rule: /^(last)?block-?(beacon|eth2|validator)$/i,
action: function (robot, msg) {
robot.send((0, _i18n.t)('fetching', {
i18n: this.i18n
}));
robot.render();
robot.addons.fetch('https://beaconcha.in/api/v1/block/latest').then(response => response.json()).then(json => {
const data = json.data;
const msg = (0, _i18n.t)('summary', {
i18n: this.i18n,
epoch: data.epoch,
slot: data.slot,
proposer: data.proposer
});
robot.send(msg);
robot.render();
});
}
};
/**
* Get Validator's balance of [key].
*
* can pass the validator key, or pre-define the
* SAIHUBOT_VALIDATOR environment variable
*/
exports.skillBeaconLastBlock = skillBeaconLastBlock;
const skillGetValidatorBalance = {
name: 'balance-validator',
help: '💰balance-(validator|eth2) - Show Validator\'s balance of [key]',
requirements: {
addons: ['fetch']
},
rule: /(^balance-(validator|eth2) )(.*)|^balance-(validator|eth2)$/i,
action: function (robot, msg) {
let validator = '';
if (msg[3] === undefined) {
validator = (0, _utils.getConfig)('VALIDATOR', '');
if (!validator) {
robot.send((0, _i18n.t)('needAddr', {
i18n: _i18n2.i18nValidator
}));
robot.render();
return;
}
}
const data = validator || (0, _utils.parseArg)(msg[3]);
robot.sendComponent( /*#__PURE__*/_react.default.createElement(_ValidatorBalances.default, {
validator: data,
fetch: robot.addons.fetch
}));
robot.render();
}
}; // ==== BEACON VALIDATOR ===
/**
* pick beacon validator explorer from the list
*
* can pass the validator index or address, or pre-define the
* SAIHUBOT_VALIDATOR environment variable
*/
exports.skillGetValidatorBalance = skillGetValidatorBalance;
const skillValidatorPicker = {
name: 'validator',
help: '🔎validator - Pick a beacon validator explorer from the list',
requirements: {
addons: ['confirm']
},
rule: /(^validator )(.*)|^validator$/i,
action: function (robot, msg) {
let validator = '';
if (msg[2] === undefined) {
validator = (0, _utils.getConfig)('VALIDATOR', '');
if (validator === '') {
robot.send((0, _i18n.t)('needAddr', {
i18n: _i18n2.i18nValidator
}));
robot.render();
return;
}
}
const data = validator || msg[2];
robot.addons.confirm((0, _i18n.t)('pick', {
i18n: _i18n2.i18nAddr
}), [{
title: (0, _i18n.t)('random', {
i18n: _i18n2.i18nAddr
}),
id: 'random',
rule: /^random/i,
action: () => robot.ask(`${(0, _stakerFreenodes.getRandomItem)(['beaconscan', 'beaconchain'])} ${data}`)
}, {
title: 'Beaconscan',
id: 'beaconscan',
rule: /^beaconscan/i,
action: () => robot.ask(`beaconscan ${data}`)
}, {
title: 'Beaconcha.in',
id: 'beaconchain',
rule: /^beaconchain/i,
action: () => robot.ask(`beaconchain ${data}`)
}]);
}
};
/**
* Check validator address on beaconscan.
*
* can pass the validator index or address, or pre-define the
* SAIHUBOT_VALIDATOR environment variable
*/
exports.skillValidatorPicker = skillValidatorPicker;
const skillSearchBeaconscan = {
name: 'beaconscan',
help: '📡beaconscan|scan [address] - check validator address or number on BeaconScan',
requirements: {
addons: ['search']
},
rule: /(^beaconscan )(.*)|^beaconscan/i,
action: function (robot, msg) {
let validator = '';
if (msg[2] === undefined) {
validator = (0, _utils.getConfig)('VALIDATOR', '');
if (validator === '') {
robot.send((0, _i18n.t)('needAddr', {
i18n: _i18n2.i18nValidator
}));
robot.render();
return;
}
}
const data = (0, _utils.singleAddr)(validator || msg[2]);
const url = 'https://beaconscan.com/validator/' + data;
robot.addons.search('Check', data, url, 'BeaconScan');
}
};
/**
* Check validator address on beaconcha.in.
*
* can pass the validator index or address, or pre-define the
* SAIHUBOT_VALIDATOR environment variable
*/
exports.skillSearchBeaconscan = skillSearchBeaconscan;
const skillSearchBeaconchain = {
name: 'beaconchain',
help: '📡beaconchain|beaconcha|beaconcha.in [address] - check validator address or number on beaconscan',
requirements: {
addons: ['search']
},
i18n: {
'en': {
needAddr: 'Please pass the index/address or define SAIHUBOT_VALIDATOR first'
},
'zh_TW': {
needAddr: '請傳入索引/地址,或是預先定義 SAIHUBOT_VALIDATOR 參數'
},
props: []
},
rule: /(^beaconcha(in|.in)? )(.*)|^beaconcha(in|.in)?$/i,
action: function (robot, msg) {
let validator = '';
if (msg[2] === undefined) {
validator = (0, _utils.getConfig)('VALIDATOR', '');
if (validator === '') {
robot.send((0, _i18n.t)('needAddr', {
i18n: _i18n2.i18nValidator
}));
robot.render();
return;
}
}
const data = (0, _utils.singleAddr)(validator.trim() || msg[3]);
const url = 'https://beaconcha.in/validator/' + data;
robot.addons.search('Check', data, url, 'beaconcha.in');
}
};
exports.skillSearchBeaconchain = skillSearchBeaconchain;
const skillsETH2 = [skillEth2Stats, skillBeaconLastBlock, skillGetValidatorBalance];
exports.skillsETH2 = skillsETH2;
const skillsValidator = [skillValidatorPicker, skillSearchBeaconchain, skillSearchBeaconscan];
exports.skillsValidator = skillsValidator;
const skills = [...skillsETH2, ...skillsValidator];
exports.skills = skills;