Skip to content

Commit

Permalink
feat(kadena-cli): dynamically split max amount based on number of cha…
Browse files Browse the repository at this point in the history
…in ids to fund account (#2148)

* feat(kadena-cli): dynamically split max amount based on number of chain ids to fund account

* chore: add changeset

* fix(kadena-cli): fix typo in the fund amount validation error message

* feat(account): add additional check on rest params type
  • Loading branch information
realdreamer committed May 28, 2024
1 parent b57c36b commit 6b940f9
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 36 deletions.
7 changes: 7 additions & 0 deletions .changeset/khaki-swans-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@kadena/kadena-cli": minor
---

Fixed reducing the overall maximum fund amount to 20 and dynamically split the max amount per chain when funding multiple chains.

Fixed sorting of chain ids in the log.
26 changes: 22 additions & 4 deletions packages/tools/kadena-cli/src/account/accountOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import { log } from '../utils/logger.js';
import type { IAliasAccountData } from './types.js';
import {
chainIdRangeValidation,
createFundAmountValidation,
formatZodFieldErrors,
fundAmountValidation,
isValidMaxAccountFundParams,
parseChainIdRange,
readAccountFromFile,
} from './utils/accountHelpers.js';
Expand Down Expand Up @@ -149,14 +150,31 @@ export const accountOptions = {
invalid_type_error: 'Error: -m, --amount must be a positive number',
}),
option: new Option('-m, --amount <amount>', 'Amount to fund your account'),
transform: (amount: string) => {
transform: (amount: string, ...rest) => {
if (
!(
Array.isArray(rest) &&
rest.length > 0 &&
isValidMaxAccountFundParams(rest[0])
)
) {
throw new Error(
'Invalid rest parameters. Ensure that maxAmount and numberOfChains are provided and are numbers',
);
}

const maxAmount = rest[0].maxAmount;
const numberOfChains = rest[0].numberOfChains;

try {
const parsedAmount = Number(amount);
fundAmountValidation.parse(parsedAmount);
createFundAmountValidation(numberOfChains, maxAmount).parse(
parsedAmount,
);
return amount;
} catch (error) {
const errorMessage = formatZodFieldErrors(error);
throw new Error(`Error: -m, --amount ${errorMessage}`);
throw new Error(`Error: -m, --amount "${errorMessage}"`);
}
},
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { log } from '../../utils/logger.js';
import { createTable } from '../../utils/table.js';
import { accountOptions } from '../accountOptions.js';
import type { IAccountDetailsResult } from '../types.js';
import { sortChainIds } from '../utils/accountHelpers.js';
import type { IGetAccountDetailsParams } from '../utils/getAccountDetails.js';
import { getAccountDetailsFromChain } from '../utils/getAccountDetails.js';

Expand All @@ -30,9 +31,7 @@ interface IAccountDetails {
const formatWarnings = (warnings: string[]): string | null => {
if (warnings.length === 0) return null;
const [prefix, suffix, ...chainIds] = warnings;
const sortedChainIds = chainIds.sort(
(a, b) => parseInt(a, 10) - parseInt(b, 10),
);
const sortedChainIds = sortChainIds(chainIds as ChainId[]);
return `${prefix} ${sortedChainIds.join(',')} ${suffix}`;
};

Expand Down
18 changes: 13 additions & 5 deletions packages/tools/kadena-cli/src/account/commands/accountFund.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ora from 'ora';
import {
CHAIN_ID_ACTION_ERROR_MESSAGE,
MAX_FUND_AMOUNT,
NO_ACCOUNTS_FOUND_ERROR_MESSAGE,
} from '../../constants/account.js';
import { FAUCET_MODULE_NAME } from '../../constants/devnets.js';
Expand All @@ -11,7 +12,10 @@ import { notEmpty } from '../../utils/globalHelpers.js';
import { globalOptions } from '../../utils/globalOptions.js';
import { log } from '../../utils/logger.js';
import { accountOptions } from '../accountOptions.js';
import { ensureAccountAliasFilesExists } from '../utils/accountHelpers.js';
import {
ensureAccountAliasFilesExists,
sortChainIds,
} from '../utils/accountHelpers.js';
import { fund } from '../utils/fund.js';
import {
deployFaucetsToChains,
Expand All @@ -26,9 +30,9 @@ export const createAccountFundCommand = createCommand(
'Fund an existing/new account',
[
accountOptions.accountSelect(),
accountOptions.fundAmount(),
globalOptions.networkSelect(),
accountOptions.chainIdRange(),
accountOptions.fundAmount(),
accountOptions.deployFaucet(),
],
async (option) => {
Expand All @@ -39,16 +43,20 @@ export const createAccountFundCommand = createCommand(
}

const { account, accountConfig } = await option.account();
const { amount } = await option.amount();
const { network, networkConfig } = await option.network({
allowedNetworkIds: ['testnet', 'development'],
});
const { chainIds } = await option.chainIds();

if (!notEmpty(chainIds)) {
return log.error(CHAIN_ID_ACTION_ERROR_MESSAGE);
}

const maxAmount = Math.floor(MAX_FUND_AMOUNT / chainIds.length);
const { amount } = await option.amount({
maxAmount: maxAmount,
numberOfChains: chainIds.length,
});

if (!notEmpty(accountConfig)) {
return log.error(
`Account details are missing. Please check "${account}" account alias file.`,
Expand Down Expand Up @@ -87,7 +95,7 @@ export const createAccountFundCommand = createCommand(
chainIds,
);

const undeployedChainIdsStr = undeployedChainIds.join(', ');
const undeployedChainIdsStr = sortChainIds(undeployedChainIds).join(', ');

if (undeployedChainIds.length > 0) {
log.warning(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ describe('account fund', () => {
);
});

it('should throw max amount error when user tries to enter more than max amount split for multi chains', async () => {
const res = await runCommand(
'account fund --account=account-add-test-manual --amount=20 --network=testnet --chain-ids=0,1,2 --quiet',
);
expect(res.stderr).toContain(
'Error: -m, --amount "With 3 chains to fund, the max amount per chain is 6 coin(s)."',
);
});

it('should exit with invalid chain id error message when user passes invalid chain id with quiet flag', async () => {
const res = await runCommand(
'account fund --account=account-add-test-manual --amount=1 --network=testnet --chain-ids=-1 --quiet',
Expand Down
65 changes: 46 additions & 19 deletions packages/tools/kadena-cli/src/account/utils/accountHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import type {
import { z } from 'zod';
import type { IAliasAccountData } from './../types.js';

import type { ChainId } from '@kadena/types';
import { MAX_FUND_AMOUNT } from '../../constants/account.js';
import { ACCOUNT_DIR, MAX_CHAIN_VALUE } from '../../constants/config.js';
import { services } from '../../services/index.js';
import { KadenaError } from '../../services/service-error.js';
Expand Down Expand Up @@ -162,28 +164,32 @@ export const chainIdRangeValidation = z
)
.nonempty();

export const fundAmountValidation = z
.number({
errorMap: (error) => {
if (error.code === 'too_small') {
return {
message: 'must be greater than or equal to 1',
};
}
export const createFundAmountValidation = (
numberOfChains: number,
maxValue = MAX_FUND_AMOUNT,
): z.ZodNumber =>
z
.number({
errorMap: (error) => {
if (error.code === 'too_small') {
return {
message: 'must be greater than or equal to 1',
};
}

if (error.code === 'too_big') {
return {
message: `With ${numberOfChains} chains to fund, the max amount per chain is ${maxValue} coin(s).`,
};
}

if (error.code === 'too_big') {
return {
message: 'must be less than or equal to 100',
message: `must be a positive number (1 - ${maxValue})`,
};
}

return {
message: 'must be a positive number (1 - 100)',
};
},
})
.min(1)
.max(100);
},
})
.min(1)
.max(maxValue);

export const getChainIdRangeSeparator = (
input: string,
Expand Down Expand Up @@ -237,3 +243,24 @@ export const isValidForOnlyKeysAllPredicate = (
accountName: string,
publicKeys: string[],
): boolean => isKAccount(accountName) && publicKeys.length === 1;

export const sortChainIds = (chainIds: ChainId[]): ChainId[] =>
chainIds.sort((a, b) => parseInt(a, 10) - parseInt(b, 10));

interface IMaxAccountFundParams {
maxAmount: number;
numberOfChains: number;
}

export const isValidMaxAccountFundParams = (
param: unknown,
): param is IMaxAccountFundParams => {
if (typeof param !== 'object' || param === null) {
return false;
}

const obj = param as Record<string, unknown>;
return (
typeof obj.maxAmount === 'number' && typeof obj.numberOfChains === 'number'
);
};
4 changes: 3 additions & 1 deletion packages/tools/kadena-cli/src/account/utils/fund.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import type { INetworkCreateOptions } from '../../networks/utils/networkHelpers.
import type { CommandResult } from '../../utils/command.util.js';
import { isNotEmptyString, notEmpty } from '../../utils/globalHelpers.js';
import type { IAliasAccountData } from '../types.js';
import { sortChainIds } from './accountHelpers.js';
import { createAndTransferFund } from './createAndTransferFunds.js';
import { getAccountDetails } from './getAccountDetails.js';
import { transferFund } from './transferFund.js';

const formatAccountCreatedMsgs = (msgs: string[]): string | null => {
if (msgs.length === 0) return null;
const [pre, ...chainIds] = msgs;
return `${pre} ${chainIds.join(
const sortedChainIds = sortChainIds(chainIds as ChainId[]);
return `${pre} ${sortedChainIds.join(
', ',
)}. So the account will be created on these Chain ID(s).`;
};
Expand Down
2 changes: 2 additions & 0 deletions packages/tools/kadena-cli/src/constants/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ export const KEYS_ALL_PRED_ERROR_MESSAGE =
'Only "keys-all" predicate is allowed for the given public keys and account name';
export const MAINNET_FUND_TRANSFER_ERROR_MESSAGE =
'Funding operations are not allowed on mainnet network with network ID:';

export const MAX_FUND_AMOUNT = 20;
17 changes: 13 additions & 4 deletions packages/tools/kadena-cli/src/prompts/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ChainId } from '@kadena/types';
import { basename, parse } from 'node:path';
import {
chainIdRangeValidation,
fundAmountValidation,
createFundAmountValidation,
getAllAccountNames,
parseChainIdRange,
} from '../account/utils/accountHelpers.js';
Expand Down Expand Up @@ -83,19 +83,28 @@ export const accountKdnNamePrompt: IPrompt<string> = async () =>
message: 'Enter an .kda name:',
});

export const fundAmountPrompt: IPrompt<string> = async () =>
export const fundAmountPrompt: IPrompt<string> = async (
previousQuestions,
args,
isOptional,
) =>
await input({
validate(value: string) {
const parsedValue = parseFloat(value.trim().replace(',', '.'));

const parseResult = fundAmountValidation.safeParse(parsedValue);
const maxAmountString = Number(previousQuestions.maxAmount as string);
const numberOfChains = previousQuestions.numberOfChains as number;
const parseResult = createFundAmountValidation(
numberOfChains,
maxAmountString,
).safeParse(parsedValue);
if (!parseResult.success) {
const formatted = parseResult.error.format();
return `Amount: ${formatted._errors[0]}`;
}
return true;
},
message: 'Enter an amount:',
default: previousQuestions?.maxAmount as string,
});

export const fungiblePrompt: IPrompt<string> = async () =>
Expand Down

0 comments on commit 6b940f9

Please sign in to comment.