@kadena/kadena-cli
Version:
Kadena CLI tool to interact with the Kadena blockchain (manage keys, transactions, etc.)
217 lines • 10.2 kB
JavaScript
import { defaultAccount, simulationDefaults, } from '../../../../../constants/devnets.js';
import { log } from '../../../../../utils/logger.js';
import { appendToLogFile, createLogFile } from '../file.js';
import { generateAccount, getAccountBalance, getRandomNumber, getRandomOption, isEqualChainAccounts, seedRandom, stringifyProperty, } from '../helper.js';
import { crossChainTransfer } from './crosschain-transfer.js';
import { safeTransfer } from './safe-transfer.js';
import { transfer } from './transfer.js';
const simulationTransferOptions = [
'cross-chain-transfer',
'transfer',
'safe-transfer',
];
export async function simulateCoin({ network, numberOfAccounts, transferInterval, maxAmount, tokenPool, logFolder, seed, defaultChain, maxTime, }) {
var _a;
const accounts = [];
// Parameters validation
if (tokenPool < maxAmount) {
log.error('The max transfer amount cant be greater than the total token pool');
return;
}
if (numberOfAccounts <= 1) {
log.error('Number of accounts must be greater than 1');
return;
}
log.info('Seed value: ', seed);
const filepath = await createLogFile(logFolder, `coin-${Date.now()}-${seed}.csv`);
try {
// Create accounts
for (let i = 0; i < numberOfAccounts; i++) {
// This will determine if the account has 1 or 2 keys (even = 1 key, odd = 2 keys)
const noOfKeys = i % 2 === 0 ? 1 : 2;
let account = await generateAccount(noOfKeys, defaultChain, network.host);
log.info(`Generated KeyPair\nAccount: ${account.account}\nPublic Key: ${stringifyProperty(account.keys, 'publicKey')}\nSecret Key: ${stringifyProperty(account.keys, 'secretKey')}\n`);
/* To diversify the initial testing sample, we cycle through all transfer types for the first funding transfers.
Subsequent transfers will be of the 'transfer' type to simulate normal operations. */
const fundingType = i < simulationTransferOptions.length
? simulationTransferOptions[i]
: 'transfer';
let result;
const sender = { ...defaultAccount, chainId: '0' };
if (fundingType === 'cross-chain-transfer') {
account = {
...account,
chainId: '1',
};
result = await crossChainTransfer({
network,
sender,
receiver: account,
amount: tokenPool / numberOfAccounts,
gasPayer: defaultAccount,
});
}
else if (fundingType === 'safe-transfer') {
result = await safeTransfer({
network,
chainId: defaultChain,
receiver: account,
amount: tokenPool / numberOfAccounts,
sender,
});
}
else {
result = await transfer({
network,
receiver: account,
chainId: defaultChain,
amount: tokenPool / numberOfAccounts,
sender,
});
}
// If the account is not in the accountlist, add it
if (accounts.includes(account)) {
throw Error('Duplicate account');
}
accounts.push(account);
await appendToLogFile(filepath, {
timestamp: Date.now(),
from: defaultAccount.account,
to: account.account,
amount: tokenPool / numberOfAccounts,
requestKey: result.reqKey,
action: 'fund',
});
}
// Generate first seeded random number
let seededRandomNo = seedRandom(seed);
let counter = 0;
const startTime = Date.now();
// eslint-disable-next-line no-constant-condition
while (true) {
// Transfer between accounts
for (let i = 0; i < accounts.length; i++) {
const account = accounts[i];
const amount = getRandomNumber(seededRandomNo, maxAmount);
// To avoid underflowing the token pool, we fund an account when there has been more iterations than total amount of circulating tokens divided by max amount
if (counter >= tokenPool / maxAmount) {
await transfer({
network,
receiver: account,
chainId: defaultChain,
amount: tokenPool / numberOfAccounts,
sender: defaultAccount,
});
counter = 0;
}
// If not enough balance, continue
if (!(await validateBalance(account, amount, network.host, defaultChain, seededRandomNo))) {
continue;
}
// Generate seeded random number based on the previous number
seededRandomNo = seedRandom(`${seededRandomNo}`);
// Randomly choose next account
let nextAccount = accounts[getRandomNumber(seededRandomNo, accounts.length)];
// Random select a transfer type
const transferType = getRandomOption(seededRandomNo, simulationTransferOptions);
let result;
// This is to simulate cross chain transfers
if (transferType === 'cross-chain-transfer' &&
simulationDefaults.CHAIN_COUNT > 1) {
// Make sure the chain id is different
if (account.chainId === nextAccount.chainId) {
nextAccount = {
...nextAccount,
chainId: `${getRandomNumber(seededRandomNo, simulationDefaults.CHAIN_COUNT)}`,
};
}
if (account.chainId === nextAccount.chainId) {
log.warning('Skipping cross chain transfer to same chain');
continue;
}
// Get a random account to potentially pay for the gas
const possibleGasPayer = getRandomOption(seededRandomNo, accounts);
result = await crossChainTransfer({
network,
sender: account,
receiver: nextAccount,
amount,
gasPayer: possibleGasPayer.chainId === nextAccount.chainId
? possibleGasPayer
: defaultAccount,
});
}
else {
// Make sure the chain id is the same if the transfer type is transfer or safe-transfer
if (account.chainId !== nextAccount.chainId) {
nextAccount = { ...nextAccount, chainId: account.chainId };
}
if (isEqualChainAccounts(account, nextAccount)) {
log.warning('Skipping transfer to self');
continue;
}
// Using a random number to determine if the transfer is a safe transfer or not
if (transferType === 'transfer') {
result = await transfer({
network,
receiver: nextAccount,
sender: account,
amount,
chainId: account.chainId || defaultChain,
});
}
if (transferType === 'safe-transfer') {
result = await safeTransfer({
network,
receiver: nextAccount,
sender: account,
amount,
chainId: account.chainId || defaultChain,
});
}
}
await appendToLogFile(filepath, {
timestamp: Date.now(),
from: account.account,
to: nextAccount.account,
amount,
requestKey: (_a = result === null || result === void 0 ? void 0 : result.reqKey) !== null && _a !== void 0 ? _a : '',
action: transferType,
});
// If the account is not in the accountlist, add it
const accountExists = accounts.some((existingAccount) => isEqualChainAccounts(nextAccount, existingAccount));
if (!accountExists) {
accounts.push(nextAccount);
}
await new Promise((resolve) => setTimeout(resolve, transferInterval));
const simulatedTime = Date.now() - startTime;
if (simulatedTime > maxTime) {
log.info(`Simulation stopped after ${maxTime}ms. Please wait for the last transactions to complete.`);
return;
}
}
counter++;
}
}
catch (error) {
log.error(error);
await appendToLogFile(filepath, { error });
throw error;
}
}
async function validateBalance(account, amount, networkHost, defaultChain, seededRandomNo) {
const balance = await getAccountBalance({
account: account.account,
chainId: account.chainId || defaultChain,
networkHost: networkHost,
});
// using a random number safety gap to avoid underflowing the account
const amountWithSafetyGap = amount + getRandomNumber(seededRandomNo, 1);
if (amountWithSafetyGap > parseFloat(balance)) {
log.warning(`Insufficient funds for ${account.account}\nFunds necessary: ${amountWithSafetyGap}\nFunds available: ${balance}`);
log.info('Skipping transfer');
return false;
}
return true;
}
//# sourceMappingURL=simulate.js.map