pandoras-box
Version:
A small and simple stress testing tool for Ethereum-compatible blockchain networks
173 lines (172 loc) • 8.38 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.distributeAccount = exports.Distributor = void 0;
const bignumber_1 = require("@ethersproject/bignumber");
const providers_1 = require("@ethersproject/providers");
const units_1 = require("@ethersproject/units");
const wallet_1 = require("@ethersproject/wallet");
const cli_progress_1 = require("cli-progress");
const cli_table3_1 = __importDefault(require("cli-table3"));
const heap_1 = __importDefault(require("heap"));
const logger_1 = __importDefault(require("../logger/logger"));
const errors_1 = __importDefault(require("./errors"));
class distributeAccount {
constructor(missingFunds, address, index) {
this.missingFunds = missingFunds;
this.address = address;
this.mnemonicIndex = index;
}
}
exports.distributeAccount = distributeAccount;
class runtimeCosts {
constructor(accDistributionCost, subAccount) {
this.accDistributionCost = accDistributionCost;
this.subAccount = subAccount;
}
}
// Manages the fund distribution before each run-cycle
class Distributor {
constructor(mnemonic, subAccounts, totalTx, runtimeEstimator, url) {
this.requestedSubAccounts = subAccounts;
this.totalTx = totalTx;
this.mnemonic = mnemonic;
this.runtimeEstimator = runtimeEstimator;
this.readyMnemonicIndexes = [];
this.provider = new providers_1.JsonRpcProvider(url);
this.ethWallet = wallet_1.Wallet.fromMnemonic(mnemonic, `m/44'/60'/0'/0/0`).connect(this.provider);
}
distribute() {
return __awaiter(this, void 0, void 0, function* () {
logger_1.default.title('💸 Fund distribution initialized 💸');
const baseCosts = yield this.calculateRuntimeCosts();
this.printCostTable(baseCosts);
// Check if there are any addresses that need funding
const shortAddresses = yield this.findAccountsForDistribution(baseCosts.subAccount);
const initialAccCount = shortAddresses.size();
if (initialAccCount == 0) {
// Nothing to distribute
logger_1.default.success('Accounts are fully funded for the cycle');
return this.readyMnemonicIndexes;
}
// Get a list of accounts that can be funded
const fundableAccounts = yield this.getFundableAccounts(baseCosts, shortAddresses);
if (fundableAccounts.length != initialAccCount) {
logger_1.default.warn(`Unable to fund all sub-accounts. Funding ${fundableAccounts.length}`);
}
// Fund the accounts
yield this.fundAccounts(baseCosts, fundableAccounts);
logger_1.default.success('Fund distribution finished!');
return this.readyMnemonicIndexes;
});
}
calculateRuntimeCosts() {
return __awaiter(this, void 0, void 0, function* () {
const inherentValue = this.runtimeEstimator.GetValue();
const baseTxEstimate = yield this.runtimeEstimator.EstimateBaseTx();
const baseGasPrice = yield this.runtimeEstimator.GetGasPrice();
const baseTxCost = baseGasPrice.mul(baseTxEstimate).add(inherentValue);
// Calculate how much each sub-account needs
// to execute their part of the run cycle.
// Each account needs at least numTx * (gasPrice * gasLimit + value)
const subAccountCost = bignumber_1.BigNumber.from(this.totalTx).mul(baseTxCost);
// Calculate the cost of the single distribution transaction
const singleDistributionCost = yield this.provider.estimateGas({
from: wallet_1.Wallet.fromMnemonic(this.mnemonic, `m/44'/60'/0'/0/0`)
.address,
to: wallet_1.Wallet.fromMnemonic(this.mnemonic, `m/44'/60'/0'/0/1`).address,
value: subAccountCost,
});
return new runtimeCosts(singleDistributionCost, subAccountCost);
});
}
findAccountsForDistribution(singleRunCost) {
return __awaiter(this, void 0, void 0, function* () {
const balanceBar = new cli_progress_1.SingleBar({
barCompleteChar: '\u2588',
barIncompleteChar: '\u2591',
hideCursor: true,
});
logger_1.default.info('\nFetching sub-account balances...');
const shortAddresses = new heap_1.default();
balanceBar.start(this.requestedSubAccounts, 0, {
speed: 'N/A',
});
for (let i = 1; i <= this.requestedSubAccounts; i++) {
const addrWallet = wallet_1.Wallet.fromMnemonic(this.mnemonic, `m/44'/60'/0'/0/${i}`).connect(this.provider);
const balance = yield addrWallet.getBalance();
balanceBar.increment();
if (balance.lt(singleRunCost)) {
// Address doesn't have enough funds, make sure it's
// on the list to get topped off
shortAddresses.push(new distributeAccount(singleRunCost.sub(balance), addrWallet.address, i));
continue;
}
// Address has enough funds already, mark it as ready
this.readyMnemonicIndexes.push(i);
}
balanceBar.stop();
return shortAddresses;
});
}
printCostTable(costs) {
logger_1.default.info('\nCycle Cost Table:');
const costTable = new cli_table3_1.default({
head: ['Name', 'Cost [eth]'],
});
costTable.push(['Required acc. balance', (0, units_1.formatEther)(costs.subAccount)], ['Single distribution cost', (0, units_1.formatEther)(costs.accDistributionCost)]);
logger_1.default.info(costTable.toString());
}
getFundableAccounts(costs, initialSet) {
return __awaiter(this, void 0, void 0, function* () {
// Check if the root wallet has enough funds to distribute
const accountsToFund = [];
let distributorBalance = bignumber_1.BigNumber.from(yield this.ethWallet.getBalance());
while (distributorBalance.gt(costs.accDistributionCost) &&
initialSet.size() > 0) {
const acc = initialSet.pop();
distributorBalance = distributorBalance.sub(acc.missingFunds);
accountsToFund.push(acc);
}
// Check if there are accounts to fund
if (accountsToFund.length == 0) {
throw errors_1.default.errNotEnoughFunds;
}
return accountsToFund;
});
}
fundAccounts(costs, accounts) {
return __awaiter(this, void 0, void 0, function* () {
logger_1.default.info('\nFunding accounts...');
const fundBar = new cli_progress_1.SingleBar({
barCompleteChar: '\u2588',
barIncompleteChar: '\u2591',
hideCursor: true,
});
fundBar.start(accounts.length, 0, {
speed: 'N/A',
});
for (const acc of accounts) {
yield this.ethWallet.sendTransaction({
to: acc.address,
value: acc.missingFunds,
});
fundBar.increment();
this.readyMnemonicIndexes.push(acc.mnemonicIndex);
}
fundBar.stop();
});
}
}
exports.Distributor = Distributor;