diff --git a/.changeset/khaki-swans-look.md b/.changeset/khaki-swans-look.md new file mode 100644 index 0000000000..26c3da9482 --- /dev/null +++ b/.changeset/khaki-swans-look.md @@ -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. diff --git a/packages/tools/kadena-cli/src/account/accountOptions.ts b/packages/tools/kadena-cli/src/account/accountOptions.ts index dd3b2fd112..4d36db2f41 100644 --- a/packages/tools/kadena-cli/src/account/accountOptions.ts +++ b/packages/tools/kadena-cli/src/account/accountOptions.ts @@ -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'; @@ -149,14 +150,31 @@ export const accountOptions = { invalid_type_error: 'Error: -m, --amount must be a positive number', }), option: new Option('-m, --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}"`); } }, }), diff --git a/packages/tools/kadena-cli/src/account/commands/accountDetails.ts b/packages/tools/kadena-cli/src/account/commands/accountDetails.ts index 655d433c39..09995a56fa 100644 --- a/packages/tools/kadena-cli/src/account/commands/accountDetails.ts +++ b/packages/tools/kadena-cli/src/account/commands/accountDetails.ts @@ -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'; @@ -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}`; }; diff --git a/packages/tools/kadena-cli/src/account/commands/accountFund.ts b/packages/tools/kadena-cli/src/account/commands/accountFund.ts index 2945009251..cb42e52939 100644 --- a/packages/tools/kadena-cli/src/account/commands/accountFund.ts +++ b/packages/tools/kadena-cli/src/account/commands/accountFund.ts @@ -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'; @@ -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, @@ -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) => { @@ -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.`, @@ -87,7 +95,7 @@ export const createAccountFundCommand = createCommand( chainIds, ); - const undeployedChainIdsStr = undeployedChainIds.join(', '); + const undeployedChainIdsStr = sortChainIds(undeployedChainIds).join(', '); if (undeployedChainIds.length > 0) { log.warning( diff --git a/packages/tools/kadena-cli/src/account/tests/accountFund.test.ts b/packages/tools/kadena-cli/src/account/tests/accountFund.test.ts index e26b9e7f37..406549c1ef 100644 --- a/packages/tools/kadena-cli/src/account/tests/accountFund.test.ts +++ b/packages/tools/kadena-cli/src/account/tests/accountFund.test.ts @@ -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', diff --git a/packages/tools/kadena-cli/src/account/utils/accountHelpers.ts b/packages/tools/kadena-cli/src/account/utils/accountHelpers.ts index 85ebb000dc..d2fab0048d 100644 --- a/packages/tools/kadena-cli/src/account/utils/accountHelpers.ts +++ b/packages/tools/kadena-cli/src/account/utils/accountHelpers.ts @@ -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'; @@ -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, @@ -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; + return ( + typeof obj.maxAmount === 'number' && typeof obj.numberOfChains === 'number' + ); +}; diff --git a/packages/tools/kadena-cli/src/account/utils/fund.ts b/packages/tools/kadena-cli/src/account/utils/fund.ts index 3330fe5225..3ea20f642d 100644 --- a/packages/tools/kadena-cli/src/account/utils/fund.ts +++ b/packages/tools/kadena-cli/src/account/utils/fund.ts @@ -4,6 +4,7 @@ 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'; @@ -11,7 +12,8 @@ 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).`; }; diff --git a/packages/tools/kadena-cli/src/constants/account.ts b/packages/tools/kadena-cli/src/constants/account.ts index 4c84373ed8..0aceb20123 100644 --- a/packages/tools/kadena-cli/src/constants/account.ts +++ b/packages/tools/kadena-cli/src/constants/account.ts @@ -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; diff --git a/packages/tools/kadena-cli/src/prompts/account.ts b/packages/tools/kadena-cli/src/prompts/account.ts index 8df212f175..a798ef9dd5 100644 --- a/packages/tools/kadena-cli/src/prompts/account.ts +++ b/packages/tools/kadena-cli/src/prompts/account.ts @@ -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'; @@ -83,12 +83,20 @@ export const accountKdnNamePrompt: IPrompt = async () => message: 'Enter an .kda name:', }); -export const fundAmountPrompt: IPrompt = async () => +export const fundAmountPrompt: IPrompt = 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]}`; @@ -96,6 +104,7 @@ export const fundAmountPrompt: IPrompt = async () => return true; }, message: 'Enter an amount:', + default: previousQuestions?.maxAmount as string, }); export const fungiblePrompt: IPrompt = async () =>