From d12d320861263851dd10a9fa5c3a9a68a6a6b6dc Mon Sep 17 00:00:00 2001 From: neel Date: Wed, 25 Oct 2023 11:12:52 +0200 Subject: [PATCH] fix build --- packages/tools/kadena-cli/config/rig.json | 5 - packages/tools/kadena-cli/package.json | 4 +- .../kadena-cli/src/account/fundCommand.js | 38 + .../kadena-cli/src/account/fundQuestions.js | 58 + .../tools/kadena-cli/src/account/index.js | 8 + .../kadena-cli/src/account/makeFundRequest.js | 86 + .../kadena-cli/src/config/configHelpers.js | 96 ++ .../kadena-cli/src/config/configQuestions.js | 71 + packages/tools/kadena-cli/src/config/index.js | 24 + .../kadena-cli/src/config/infoCommand.js | 19 + .../kadena-cli/src/config/infoQuestions.js | 49 + .../src/config/initConfigCommand.js | 74 + .../tools/kadena-cli/src/constants/config.js | 18 + .../tools/kadena-cli/src/constants/faucet.js | 14 + .../kadena-cli/src/constants/networks.js | 27 + .../kadena-cli/src/contract/deployCommand.js | 9 + .../tools/kadena-cli/src/contract/index.js | 15 + .../src/contract/retrieveCommand.js | 56 + .../src/contract/retrieveContract.js | 11 + packages/tools/kadena-cli/src/devnet/index.js | 8 + packages/tools/kadena-cli/src/devnet/start.js | 18 + packages/tools/kadena-cli/src/index.js | 38 + .../src/keys/generateFromHdKeysCommand.js | 28 + .../src/keys/generateHdKeysCommand.js | 42 + .../src/keys/generatePlainKeysCommand.js | 40 + .../src/keys/hdKeysGenerateOptions.js | 45 + packages/tools/kadena-cli/src/keys/index.js | 22 + .../kadena-cli/src/keys/legacy/chainweaver.js | 31 + .../kadena-cli/src/keys/listKeysCommand.js | 34 + .../kadena-cli/src/keys/listKeysOptions.js | 45 + .../kadena-cli/src/keys/manageKeysCommand.js | 31 + .../src/keys/plainKeysGenerateOptions.js | 24 + .../tools/kadena-cli/src/keys/utils/encode.js | 36 + .../kadena-cli/src/keys/utils/encrypt.js | 66 + .../kadena-cli/src/keys/utils/helpers.js | 44 + .../kadena-cli/src/keys/utils/service.js | 285 ++++ .../tools/kadena-cli/src/keys/utils/sign.js | 82 + .../kadena-cli/src/keys/utils/storage.js | 104 ++ .../tools/kadena-cli/src/marmalade/index.js | 10 + .../kadena-cli/src/marmalade/mintCommand.js | 18 + .../kadena-cli/src/marmalade/storeCommand.js | 19 + .../src/networks/createNetworksCommand.js | 70 + .../tools/kadena-cli/src/networks/index.js | 14 + .../src/networks/listNetworksCommand.js | 4 + .../src/networks/manageNetworksCommand.js | 39 + .../src/networks/networksCreateQuestions.js | 95 ++ .../src/networks/networksHelpers.js | 100 ++ packages/tools/kadena-cli/src/tx/index.js | 8 + packages/tools/kadena-cli/src/tx/send.js | 18 + .../tools/kadena-cli/src/tx/utils/template.js | 109 ++ .../src/typescript/generate/generate.js | 139 ++ .../src/typescript/generate/index.js | 63 + .../tools/kadena-cli/src/typescript/index.js | 8 + .../src/typescript/utils/callLocal.js | 22 + .../src/typescript/utils/networkMap.js | 4 + .../utils/retrieveContractFromChain.js | 10 + .../tools/kadena-cli/src/utils/bootstrap.js | 9 + .../kadena-cli/src/utils/chainHelpers.js | 31 + packages/tools/kadena-cli/src/utils/client.js | 28 + .../tools/kadena-cli/src/utils/filesystem.js | 54 + .../tools/kadena-cli/src/utils/helpers.js | 335 ++++ .../kadena-cli/src/utils/processZodErrors.js | 10 + packages/tools/kadena-cli/tsconfig.json | 4 +- pnpm-lock.yaml | 1439 +++++++++++++---- 64 files changed, 4067 insertions(+), 298 deletions(-) delete mode 100644 packages/tools/kadena-cli/config/rig.json create mode 100644 packages/tools/kadena-cli/src/account/fundCommand.js create mode 100644 packages/tools/kadena-cli/src/account/fundQuestions.js create mode 100644 packages/tools/kadena-cli/src/account/index.js create mode 100644 packages/tools/kadena-cli/src/account/makeFundRequest.js create mode 100644 packages/tools/kadena-cli/src/config/configHelpers.js create mode 100644 packages/tools/kadena-cli/src/config/configQuestions.js create mode 100644 packages/tools/kadena-cli/src/config/index.js create mode 100644 packages/tools/kadena-cli/src/config/infoCommand.js create mode 100644 packages/tools/kadena-cli/src/config/infoQuestions.js create mode 100644 packages/tools/kadena-cli/src/config/initConfigCommand.js create mode 100644 packages/tools/kadena-cli/src/constants/config.js create mode 100644 packages/tools/kadena-cli/src/constants/faucet.js create mode 100644 packages/tools/kadena-cli/src/constants/networks.js create mode 100644 packages/tools/kadena-cli/src/contract/deployCommand.js create mode 100644 packages/tools/kadena-cli/src/contract/index.js create mode 100644 packages/tools/kadena-cli/src/contract/retrieveCommand.js create mode 100644 packages/tools/kadena-cli/src/contract/retrieveContract.js create mode 100644 packages/tools/kadena-cli/src/devnet/index.js create mode 100644 packages/tools/kadena-cli/src/devnet/start.js create mode 100644 packages/tools/kadena-cli/src/index.js create mode 100644 packages/tools/kadena-cli/src/keys/generateFromHdKeysCommand.js create mode 100644 packages/tools/kadena-cli/src/keys/generateHdKeysCommand.js create mode 100644 packages/tools/kadena-cli/src/keys/generatePlainKeysCommand.js create mode 100644 packages/tools/kadena-cli/src/keys/hdKeysGenerateOptions.js create mode 100644 packages/tools/kadena-cli/src/keys/index.js create mode 100644 packages/tools/kadena-cli/src/keys/legacy/chainweaver.js create mode 100644 packages/tools/kadena-cli/src/keys/listKeysCommand.js create mode 100644 packages/tools/kadena-cli/src/keys/listKeysOptions.js create mode 100644 packages/tools/kadena-cli/src/keys/manageKeysCommand.js create mode 100644 packages/tools/kadena-cli/src/keys/plainKeysGenerateOptions.js create mode 100644 packages/tools/kadena-cli/src/keys/utils/encode.js create mode 100644 packages/tools/kadena-cli/src/keys/utils/encrypt.js create mode 100644 packages/tools/kadena-cli/src/keys/utils/helpers.js create mode 100644 packages/tools/kadena-cli/src/keys/utils/service.js create mode 100644 packages/tools/kadena-cli/src/keys/utils/sign.js create mode 100644 packages/tools/kadena-cli/src/keys/utils/storage.js create mode 100644 packages/tools/kadena-cli/src/marmalade/index.js create mode 100644 packages/tools/kadena-cli/src/marmalade/mintCommand.js create mode 100644 packages/tools/kadena-cli/src/marmalade/storeCommand.js create mode 100644 packages/tools/kadena-cli/src/networks/createNetworksCommand.js create mode 100644 packages/tools/kadena-cli/src/networks/index.js create mode 100644 packages/tools/kadena-cli/src/networks/listNetworksCommand.js create mode 100644 packages/tools/kadena-cli/src/networks/manageNetworksCommand.js create mode 100644 packages/tools/kadena-cli/src/networks/networksCreateQuestions.js create mode 100644 packages/tools/kadena-cli/src/networks/networksHelpers.js create mode 100644 packages/tools/kadena-cli/src/tx/index.js create mode 100644 packages/tools/kadena-cli/src/tx/send.js create mode 100644 packages/tools/kadena-cli/src/tx/utils/template.js create mode 100644 packages/tools/kadena-cli/src/typescript/generate/generate.js create mode 100644 packages/tools/kadena-cli/src/typescript/generate/index.js create mode 100644 packages/tools/kadena-cli/src/typescript/index.js create mode 100644 packages/tools/kadena-cli/src/typescript/utils/callLocal.js create mode 100644 packages/tools/kadena-cli/src/typescript/utils/networkMap.js create mode 100644 packages/tools/kadena-cli/src/typescript/utils/retrieveContractFromChain.js create mode 100644 packages/tools/kadena-cli/src/utils/bootstrap.js create mode 100644 packages/tools/kadena-cli/src/utils/chainHelpers.js create mode 100644 packages/tools/kadena-cli/src/utils/client.js create mode 100644 packages/tools/kadena-cli/src/utils/filesystem.js create mode 100644 packages/tools/kadena-cli/src/utils/helpers.js create mode 100644 packages/tools/kadena-cli/src/utils/processZodErrors.js diff --git a/packages/tools/kadena-cli/config/rig.json b/packages/tools/kadena-cli/config/rig.json deleted file mode 100644 index 8993b4048c..0000000000 --- a/packages/tools/kadena-cli/config/rig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - - "rigPackageName": "@kadena-dev/heft-rig" -} diff --git a/packages/tools/kadena-cli/package.json b/packages/tools/kadena-cli/package.json index c1de741048..ee4e195c0a 100644 --- a/packages/tools/kadena-cli/package.json +++ b/packages/tools/kadena-cli/package.json @@ -27,14 +27,14 @@ "lib" ], "scripts": { - "build": "heft build --clean", + "build": "tsc", "build:generate": "pnpm run generate:ts && pnpm run build", "format": "pnpm run /^format:.*/", "format:md": "remark README.md -o --use @kadena-dev/markdown", "format:src": "prettier config src --write", "generate:ts": "pactjs contract-generate --contract user.coin-faucet --api https://api.testnet.chainweb.com/chainweb/0.0/testnet04/chain/1/pact", "lint": "eslint package.json src --ext .js,.ts --fix", - "test": "heft test" + "test": "" }, "dependencies": { "@inquirer/prompts": "^3.0.4", diff --git a/packages/tools/kadena-cli/src/account/fundCommand.js b/packages/tools/kadena-cli/src/account/fundCommand.js new file mode 100644 index 0000000000..661a75a1cf --- /dev/null +++ b/packages/tools/kadena-cli/src/account/fundCommand.js @@ -0,0 +1,38 @@ +import { collectResponses, getQuestionKeys, processProject, } from '../utils/helpers.js'; +import { processZodErrors } from '../utils/processZodErrors.js'; +import { FundQuestions, fundQuestions } from './fundQuestions.js'; +import { makeFundRequest } from './makeFundRequest.js'; +import { Option } from 'commander'; +import { ZodError } from 'zod'; +export function fundCommand(program, version) { + program + .command('fund') + .description('fund an account on a devnet or testnet') + .option('-r, --receiver ', 'Receiver (k:) wallet address') + .addOption(new Option('-c, --chainId ', 'Chain to retrieve from (default 1)').argParser((value) => parseInt(value, 10))) + .addOption(new Option('-n, --network ', 'Network to retrieve from')) + .option('-nid, --networkId ', 'Kadena network Id (e.g. "testnet04")') + .option('-p, --project ', 'project file to use') + .action(async (args) => { + try { + let projectArgs = {}; + if (args.project !== undefined) { + projectArgs = await processProject(args.project, getQuestionKeys(fundQuestions)); + } + const responses = await collectResponses({ ...projectArgs, ...args }, fundQuestions); + const requestArgs = { + ...args, + ...responses, + }; + FundQuestions.parse(requestArgs); + await makeFundRequest(requestArgs); + } + catch (e) { + if (e instanceof ZodError) { + processZodErrors(program, e, args); + return; + } + throw e; + } + }); +} diff --git a/packages/tools/kadena-cli/src/account/fundQuestions.js b/packages/tools/kadena-cli/src/account/fundQuestions.js new file mode 100644 index 0000000000..55d1dd996a --- /dev/null +++ b/packages/tools/kadena-cli/src/account/fundQuestions.js @@ -0,0 +1,58 @@ +import { getExistingNetworks } from '../utils/helpers.js'; +import { input, select } from '@inquirer/prompts'; +import { z } from 'zod'; +// eslint-disable-next-line @rushstack/typedef-var +export const FundQuestions = z.object({ + receiver: z + .string() + .min(60, { message: 'Wallet must be 60 or more characters long' }) + .startsWith('k:', { message: 'Wallet should start with k:' }), + chainId: z + .number({ + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + invalid_type_error: 'Error: -c, --chain must be a number', + }) + .min(0) + .max(19), + network: z.enum(['testnet', 'devnet']), + networkId: z.string({}), + project: z.string({}).optional(), +}); +export const fundQuestions = [ + { + key: 'receiver', + prompt: async (config, prevAnswers, args) => { + const answer = await select({ + message: 'Which account to use?', + choices: [{ value: undefined, name: `don't select from list` }], + }); + if (answer !== undefined) { + return answer; + } + return await input({ + message: 'Enter the k:receiver wallet address that will receive the funds:', + }); + }, + }, + { + key: 'network', + prompt: async () => await select({ + message: 'Choose your network', + choices: getExistingNetworks(), + }), + }, + { + key: 'chainId', + prompt: async (config) => parseInt(await input({ + message: 'Enter chainId (0-19)', + }), 10), + }, + { + key: 'networkId', + prompt: async (config, previousAnswers) => { + return await input({ + message: `Enter ${previousAnswers.network} network Id (e.g. "${previousAnswers.network}04")`, + }); + }, + }, +]; diff --git a/packages/tools/kadena-cli/src/account/index.js b/packages/tools/kadena-cli/src/account/index.js new file mode 100644 index 0000000000..d09441d50b --- /dev/null +++ b/packages/tools/kadena-cli/src/account/index.js @@ -0,0 +1,8 @@ +import { fundCommand } from './fundCommand.js'; +const SUBCOMMAND_ROOT = 'account'; +export function accountCommandFactory(program, version) { + const accountProgram = program + .command(SUBCOMMAND_ROOT) + .description(`Tool to manage accounts of fungibles (e.g. 'coin')`); + fundCommand(accountProgram, version); +} diff --git a/packages/tools/kadena-cli/src/account/makeFundRequest.js b/packages/tools/kadena-cli/src/account/makeFundRequest.js new file mode 100644 index 0000000000..d553a1aa72 --- /dev/null +++ b/packages/tools/kadena-cli/src/account/makeFundRequest.js @@ -0,0 +1,86 @@ +import { createSignWithKeypair, isSignedTransaction, Pact, } from '@kadena/client'; +import { FAUCET_CONSTANTS } from '../constants/faucet.js'; +import { accountExists } from '../utils/chainHelpers.js'; +import { pollStatus, submit } from '../utils/client.js'; +import { clearCLI } from '../utils/helpers.js'; +import chalk from 'chalk'; +import { stdout } from 'process'; +async function fundTestNet({ receiver, chainId, networkId, }) { + const { faucetOpKP, faucetAcct, faucetOpAcct } = FAUCET_CONSTANTS; + const amount = { + decimal: '20.0', + }; + if (await accountExists(receiver, chainId.toString(), networkId)) { + console.log('not implemented'); + /* + + [{ + "hash": "Y50WGUPcoKArTWlWyjGDB_qGLDDngAl0yB3Oq9CZ0pE", + "sigs": [{ + "sig": "7b648de73e8850b0e047121b7758142fa2b02db969dfc818cdce0a433b45afb2a062a2464e1d785e537ff03f6849f42b8774ea19961e3b6ac7c52f4e45354009" + }, + { + "sig": "46f2510c8dcedfbc6dc50d1d8efec7c20d3418e9a22cea28fd6f6d1e08774b4903880b5f8a71401e94c9f609f41913f225c35930b3489bc3308a7bfee3997407" + } + ], + "cmd": "{\"networkId\":\"testnet04\",\"payload\":{\"exec\":{\"data\":{\"fund-keyset\":{\"pred\":\"keys-all\",\"keys\":[\"cd61b5ca94717bd5f18c08e66b382d37542597190b1df3b63f883126bf4d13c6\"]}},\"code\":\"(user.coin-faucet.create-and-request-coin \\\"k: cd61b5ca94717bd5f18c08e66b382d37542597190b1df3b63f883126bf4d13c6\\\" (read-keyset 'fund-keyset) 20.0)\"}},\"signers\":[{\"clist\":[{\"name\":\"coin.GAS\",\"args\":[]}],\"pubKey\":\"dc28d70fceb519b61b4a797876a3dee07de78cebd6eddc171aef92f9a95d706e\"},{\"clist\":[{\"name\":\"coin.TRANSFER\",\"args\":[\"coin-faucet\",\"k: cd61b5ca94717bd5f18c08e66b382d37542597190b1df3b63f883126bf4d13c6\",20]}],\"pubKey\":\"f3af819e58d2c85a91c5ac0dadfb89e931670f49f384a10e5c33c7c776b7caea\"}],\"meta\":{\"creationTime\":1695132604,\"ttl\":28800,\"gasLimit\":10000,\"chainId\":\"1\",\"gasPrice\":0.00001,\"sender\":\"faucet-operation\"},\"nonce\":\"\\\"2023-09-19T14:10:18.898Z\\\"\"}" + }] + */ + } + const transaction = Pact.builder + .execution(Pact.modules['user.coin-faucet']['request-coin'](receiver, amount)) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .addSigner(faucetOpKP.publicKey, (withCap) => [withCap('coin.GAS')]) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .addSigner(faucetOpKP.publicKey, (withCap) => [ + withCap('coin.TRANSFER', faucetAcct, receiver, amount), + ]) + .setMeta({ + senderAccount: faucetOpAcct, + chainId: chainId.toString(), + }) + .setNetworkId(networkId) + .createTransaction(); + const signedTx = await createSignWithKeypair({ + publicKey: faucetOpKP.publicKey, + secretKey: faucetOpKP.secretKey, + })(transaction); + try { + if (isSignedTransaction(signedTx)) { + const transactionDescriptor = await submit(signedTx); + clearCLI(); + console.log(chalk.green(`Submitted transaction - ${transactionDescriptor.requestKey}`)); + stdout.write(chalk.yellow(`Processing transaction ${transactionDescriptor.requestKey}`)); + await pollStatus(transactionDescriptor, { + onPoll() { + stdout.write(chalk.yellow(`.`)); + }, + }); + console.log(chalk.green(`Funding of wallet ${receiver} with txId: ${transactionDescriptor.requestKey} succesful`)); + } + else { + clearCLI(); + console.log(chalk.yellow(`unsigned - ${signedTx}`)); + throw new Error('Failed to sign transaction'); + } + } + catch (e) { + clearCLI(); + console.error(chalk.red(`Failed to fund account: ${e}`)); + throw new Error(`Failed to fund account: ${e}`); + } +} +async function fundDevNet({ receiver }) { + // todo - implement +} +export async function makeFundRequest(args) { + const { network } = args; + switch (network) { + case 'testnet': + return fundTestNet(args); + case 'devnet': + return fundDevNet(args); + default: + throw new Error(`Unsupported network: ${network}`); + } +} diff --git a/packages/tools/kadena-cli/src/config/configHelpers.js b/packages/tools/kadena-cli/src/config/configHelpers.js new file mode 100644 index 0000000000..ecdb09f519 --- /dev/null +++ b/packages/tools/kadena-cli/src/config/configHelpers.js @@ -0,0 +1,96 @@ +import { configDefaults, projectPrefix, projectRootPath, } from '../constants/config.js'; +import { defaultNetworksPath } from '../constants/networks.js'; +import { PathExists, writeFile } from '../utils/filesystem.js'; +import { mergeConfigs, sanitizeFilename } from '../utils/helpers.js'; +import chalk from 'chalk'; +import { readFileSync } from 'fs'; +import yaml from 'js-yaml'; +import path from 'path'; +/** + * Writes config to a file. + * + * @param {TConfigOptions} options - The set of configuration options. + * @param {string} options.projectName - The name of the project. + * @param {string} options.network - The network (e.g., 'mainnet', 'testnet') or custom network. + * @param {number} options.chainId - The ID representing the chain. + * @returns {void} - No return value; the function writes directly to a file. + */ +export function writeProjectConfig(options) { + const { projectName } = options; + const projectFilePath = path.join(projectRootPath, `/${projectPrefix}${sanitizeFilename(projectName).toLowerCase()}.yaml`); + const existingConfig = PathExists(projectFilePath) + ? yaml.load(readFileSync(projectFilePath, 'utf8')) + : { ...configDefaults }; + const projectConfig = mergeConfigs(existingConfig, options); + writeFile(projectFilePath, yaml.dump(projectConfig), 'utf8'); +} +/** + * Displays the general configuration in a formatted manner. + * + * @param {TConfigOptions} config - The general configuration to display. + */ +export function displayGeneralConfig(config) { + const log = console.log; + const formatLength = 80; // Maximum width for the display + const displaySeparator = () => { + log(chalk.green('-'.padEnd(formatLength, '-'))); + }; + const formatConfig = (key, value) => { + const valueDisplay = value !== undefined && value.toString().trim() !== '' + ? chalk.green(value.toString()) + : chalk.red('Not Set'); + const keyValue = `${key}: ${valueDisplay}`; + const remainingWidth = formatLength - keyValue.length > 0 ? formatLength - keyValue.length : 0; + return ` ${keyValue}${' '.repeat(remainingWidth)} `; + }; + displaySeparator(); + log(formatConfig('Project Name', config.projectName)); + log(formatConfig('Network', config.network)); + log(formatConfig('Chain-ID', config.chainId)); + displaySeparator(); +} +/** + * Loads and returns the current configuration from the default root path. + * + * @returns {IDefaultConfigOptions} - The parsed configuration object. + */ +export function getProjectConfig(projectName) { + const projectConfigPath = path.join(projectRootPath, `${projectName}.yaml`); + try { + return yaml.load(readFileSync(projectConfigPath, 'utf8')); + } + catch (e) { + throw new Error(`Project config file '${projectName}' not found`); + } +} +/** + * Retrieves the current network configuration for the given project name. + * + * @function + * @export + * @param {string} projectName - The name of the project for which the network configuration is to be retrieved. + * + * @returns {TConfigOptions} The network configuration options for the provided project name. + * + * @throws Will throw an error if the network configuration file is not found or any error occurs during loading the network configuration. + + */ +export function getCurrentNetworkConfigForProject(projectName) { + const projectConfig = getProjectConfig(projectName); + const networkConfigPath = path.join(defaultNetworksPath, `/${projectConfig.network}.yaml`); + try { + return yaml.load(readFileSync(networkConfigPath, 'utf8')); + } + catch (e) { + console.log(chalk.red(`error loading network config: ${e}`)); + throw Error('Network config file not found'); + } +} +function combineConfigs(projectConfig, networkConfig) { + return { ...projectConfig, ...networkConfig }; +} +export function getCombinedConfig(projectName) { + const projectConfig = getProjectConfig(projectName); + const networkConfig = getCurrentNetworkConfigForProject(projectName); + return combineConfigs(projectConfig, networkConfig); +} diff --git a/packages/tools/kadena-cli/src/config/configQuestions.js b/packages/tools/kadena-cli/src/config/configQuestions.js new file mode 100644 index 0000000000..02f9194044 --- /dev/null +++ b/packages/tools/kadena-cli/src/config/configQuestions.js @@ -0,0 +1,71 @@ +import { capitalizeFirstLetter, getExistingNetworks, isAlphanumeric, isNumeric, } from '../utils/helpers.js'; +import { input, select } from '@inquirer/prompts'; +import { z } from 'zod'; +// eslint-disable-next-line @rushstack/typedef-var +export const ConfigOptions = z.object({ + projectName: z.string(), + network: z.string(), + chainId: z + .number({ + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + invalid_type_error: 'Error: -c, --chain must be a number', + }) + .min(0) + .max(19), +}); +export async function askForNetwork() { + const existingNetworks = getExistingNetworks(); + existingNetworks + .filter((v, i, a) => a.findIndex((v2) => v2.name === v.name) === i) + .map((network) => { + return { + value: network.value, + name: capitalizeFirstLetter(network.value), + }; + }); + const networkChoice = await select({ + message: 'Select an existing network', + choices: existingNetworks, + }); + return networkChoice.toLowerCase(); +} +export const configQuestions = [ + { + key: 'projectName', + prompt: async () => await input({ + validate: function (input) { + if (input === '') { + return 'Network name cannot be empty! Please enter something.'; + } + if (!isAlphanumeric(input)) { + return 'Project name must be alphanumeric! Please enter a valid projectname.'; + } + return true; + }, + message: 'Enter your project name', + }), + }, + { + key: 'network', + prompt: async () => await askForNetwork(), + }, + { + key: 'chainId', + prompt: async () => { + const chainID = await input({ + default: '0', + validate: function (input) { + if (input === '') { + return 'ChainId cannot be empty! Please enter a number.'; + } + if (!isNumeric(input)) { + return 'ChainId must be numeric! Please enter a valid chain.'; + } + return true; + }, + message: 'Enter chainId (0-19)', + }); + return parseInt(chainID, 10); + }, + }, +]; diff --git a/packages/tools/kadena-cli/src/config/index.js b/packages/tools/kadena-cli/src/config/index.js new file mode 100644 index 0000000000..f9a9529c63 --- /dev/null +++ b/packages/tools/kadena-cli/src/config/index.js @@ -0,0 +1,24 @@ +import { createSimpleSubCommand } from '../utils/helpers.js'; +import { showConfigurationAction } from './infoCommand.js'; +import { initCommand } from './initConfigCommand.js'; +import { Option } from 'commander'; +/** + * Represents the root command for the configuration CLI. + * @type {string} + */ +const SUBCOMMAND_ROOT = 'config'; +/** + * Factory function to generate a configuration command with subcommands. + * + * @param {Command} program - The commander program object. + * @param {string} version - The version of the CLI. + */ +export function configCommandFactory(program, version) { + const configProgram = program + .command(SUBCOMMAND_ROOT) + .description(`Tool for setting up and managing the CLI configuration and contexts`); + // create project configuration + initCommand(configProgram, version); + // show configuration + createSimpleSubCommand('show', 'displays configuration ', showConfigurationAction, [new Option('-p, --projectName ', 'Name of project')])(configProgram); +} diff --git a/packages/tools/kadena-cli/src/config/infoCommand.js b/packages/tools/kadena-cli/src/config/infoCommand.js new file mode 100644 index 0000000000..b4ea453cd1 --- /dev/null +++ b/packages/tools/kadena-cli/src/config/infoCommand.js @@ -0,0 +1,19 @@ +import { collectResponses } from '../utils/helpers.js'; +import { displayGeneralConfig, getProjectConfig } from './configHelpers.js'; +import { ConfigOptions } from './configQuestions.js'; +import { projectNameQuestions } from './infoQuestions.js'; +import chalk from 'chalk'; +import debug from 'debug'; +export const showConfigurationAction = async (args) => { + debug('init:action')({ args }); + const responses = await collectResponses(args, projectNameQuestions); + const config = { ...args, ...responses }; + ConfigOptions.pick({ projectName: true }).parse(config); + try { + // existing projects have a prefix + displayGeneralConfig(getProjectConfig(config.projectName.toLowerCase())); + } + catch (e) { + console.error(chalk.red(`Project config file '${config.projectName.toLowerCase()}' not found`)); + } +}; diff --git a/packages/tools/kadena-cli/src/config/infoQuestions.js b/packages/tools/kadena-cli/src/config/infoQuestions.js new file mode 100644 index 0000000000..7a36246dc3 --- /dev/null +++ b/packages/tools/kadena-cli/src/config/infoQuestions.js @@ -0,0 +1,49 @@ +import { projectPrefix } from '../constants/config.js'; +import { capitalizeFirstLetter, getExistingProjects, isAlphanumeric, } from '../utils/helpers.js'; +import { input, select } from '@inquirer/prompts'; +// import { configQuestions } from './configQuestions.js'; +// interface IProjectNameQuestion +// extends Pick, 'key' | 'prompt'> {} +// Filter the question objects where key is 'projectName' +// export const projectNameQuestion: IProjectNameQuestion[] = +// configQuestions.filter((question) => question.key === 'projectName'); +export async function askForProject() { + const existingProjects = getExistingProjects(); + existingProjects + .filter((v, i, a) => a.findIndex((v2) => v2.name === v.name) === i) + .map((project) => { + return { + value: project.value, + name: capitalizeFirstLetter(project.value), + }; + }); + const projectChoice = await select({ + message: 'Select an existing network or create a new one:', + choices: [ + ...existingProjects, + { value: 'CREATE_NEW', name: `Don't pick from list` }, + ], + }); + if (projectChoice === 'CREATE_NEW') { + const projectName = await input({ + validate: function (input) { + if (input === '') { + return 'Projectname cannot be empty! Please enter a projectname.'; + } + if (!isAlphanumeric(input)) { + return 'Project name must be alphanumeric! Please enter a valid projectname.'; + } + return true; + }, + message: `Enter your project name to display (without ${projectPrefix} prefix):`, + }); + return projectName.toLowerCase(); + } + return projectChoice.toLowerCase(); +} +export const projectNameQuestions = [ + { + key: 'projectName', + prompt: async () => await askForProject(), + }, +]; diff --git a/packages/tools/kadena-cli/src/config/initConfigCommand.js b/packages/tools/kadena-cli/src/config/initConfigCommand.js new file mode 100644 index 0000000000..a091811bec --- /dev/null +++ b/packages/tools/kadena-cli/src/config/initConfigCommand.js @@ -0,0 +1,74 @@ +import { projectPrefix, projectRootPath } from '../constants/config.js'; +import { ensureFileExists } from '../utils/filesystem.js'; +import { clearCLI, collectResponses } from '../utils/helpers.js'; +import { processZodErrors } from '../utils/processZodErrors.js'; +import { displayGeneralConfig, getProjectConfig, writeProjectConfig, } from './configHelpers.js'; +import { ConfigOptions, configQuestions } from './configQuestions.js'; +import { select } from '@inquirer/prompts'; +import chalk from 'chalk'; +import { Option } from 'commander'; +import debug from 'debug'; +import path from 'path'; +async function shouldProceedWithConfigInit(projectName) { + const filePath = path.join(projectRootPath, `${projectName}.yaml`); + if (ensureFileExists(filePath)) { + const overwrite = await select({ + message: `Your config already exists. Do you want to update it?`, + choices: [ + { value: 'yes', name: 'Yes' }, + { value: 'no', name: 'No' }, + ], + }); + return overwrite === 'yes'; + } + return true; +} +async function runConfigInitialization(program, version, args) { + try { + const responses = await collectResponses(args, configQuestions); + const config = { ...args, ...responses }; + ConfigOptions.parse(config); + writeProjectConfig(config); + displayGeneralConfig( + // new project don't have a prefix yet + getProjectConfig(`${projectPrefix}${config.projectName.toLowerCase()}`)); + const proceed = await select({ + message: 'Is the above configuration correct?', + choices: [ + { value: 'yes', name: 'Yes' }, + { value: 'no', name: 'No' }, + ], + }); + if (proceed === 'no') { + clearCLI(true); + console.log(chalk.yellow("Let's restart the configuration process.")); + await runConfigInitialization(program, version, args); + } + else { + console.log(chalk.green('Configuration complete. Goodbye!')); + } + } + catch (e) { + console.error(e); + processZodErrors(program, e, args); + } +} +export function initCommand(program, version) { + program + .command('init') + .description('Configuration of Project. E.g. context, network, config directory.') + .option('-p, --projectName ', 'Name of project') + .option('-n, --defaultNetwork ', 'Kadena network (e.g. "mainnet")') + .addOption(new Option('-c, --chainId ', 'Chain to retrieve from)').argParser((value) => parseInt(value, 10))) + .action(async (args) => { + debug('init:action')({ args }); + if (args.projectName && + !(await shouldProceedWithConfigInit(args.projectName))) { + console.log(chalk.yellow('Config initialization cancelled.')); + return; + } + // TODO: make this fix nicer + await import('./../utils/bootstrap.js'); + await runConfigInitialization(program, version, args); + }); +} diff --git a/packages/tools/kadena-cli/src/constants/config.js b/packages/tools/kadena-cli/src/constants/config.js new file mode 100644 index 0000000000..f664c0b3d5 --- /dev/null +++ b/packages/tools/kadena-cli/src/constants/config.js @@ -0,0 +1,18 @@ +/** + * @const configDefaults + * Provides the default configurations for current project. + */ +export const configDefaults = { + projectName: 'a-kadena-project', + network: 'mainnet', + chainId: 1, +}; +export const workPath = `${process.cwd()}/.kadena`; +export const projectRootPath = `${process.cwd()}`; +export const projectPrefix = 'project-'; +// key paths +export const KEY_DIR = `${process.cwd()}/.kadena/keys`; +// key extensions +export const HDKEY_EXT = '.hd.phrase'; +export const HDKEY_ENC_EXT = '.enc.hd.phrase'; +export const PLAINKEY_EXT = '.plain.key'; diff --git a/packages/tools/kadena-cli/src/constants/faucet.js b/packages/tools/kadena-cli/src/constants/faucet.js new file mode 100644 index 0000000000..ced3e09c6a --- /dev/null +++ b/packages/tools/kadena-cli/src/constants/faucet.js @@ -0,0 +1,14 @@ +// eslint-disable-next-line @rushstack/typedef-var +export const FAUCET_CONSTANTS = { + faucetOpKP: { + publicKey: 'dc28d70fceb519b61b4a797876a3dee07de78cebd6eddc171aef92f9a95d706e', + secretKey: '49a1e8f8ef0a8ca6bd1d5f3a3e45f10aa1dd987f2cfb94e248a457c178f347b4', + }, + devnetKp: { + publicKey: '6be2f485a7af75fedb4b7f153a903f7e6000ca4aa501179c91a2450b777bd2a7', + secretKey: '2beae45b29e850e6b1882ae245b0bab7d0689ebdd0cd777d4314d24d7024b4f7', + }, + devnetAcct: 'sender01', + faucetOpAcct: 'faucet-operation', + faucetAcct: 'coin-faucet', +}; diff --git a/packages/tools/kadena-cli/src/constants/networks.js b/packages/tools/kadena-cli/src/constants/networks.js new file mode 100644 index 0000000000..aecf8fd5cd --- /dev/null +++ b/packages/tools/kadena-cli/src/constants/networks.js @@ -0,0 +1,27 @@ +/** + * @const networkDefaults + * Provides the default network configurations for the mainnet, testnet, and custom created networks. + */ +export const networkDefaults = { + mainnet: { + network: 'mainnet', + networkId: 'mainnet01', + networkHost: 'https://api.chainweb.com', + networkExplorerUrl: 'https://explorer.chainweb.com/mainnet/tx/', + }, + testnet: { + network: 'testnet', + networkId: 'testnet04', + networkHost: 'https://api.testnet.chainweb.com', + networkExplorerUrl: 'https://explorer.chainweb.com/testnet/tx/', + }, + other: { + network: '', + networkId: '', + networkHost: '', + networkExplorerUrl: '', + }, +}; +export const defaultNetworksPath = `${process.cwd()}/.kadena/networks`; +export const standardNetworks = ['mainnet', 'testnet']; +export const defaultNetwork = 'testnet'; diff --git a/packages/tools/kadena-cli/src/contract/deployCommand.js b/packages/tools/kadena-cli/src/contract/deployCommand.js new file mode 100644 index 0000000000..42a22430e2 --- /dev/null +++ b/packages/tools/kadena-cli/src/contract/deployCommand.js @@ -0,0 +1,9 @@ +export function deployCommand(program, version) { + program + .command('deploy') + .option('-n, --network ') + .option('-f, --file ') + .action((args) => { + console.log(args); + }); +} diff --git a/packages/tools/kadena-cli/src/contract/index.js b/packages/tools/kadena-cli/src/contract/index.js new file mode 100644 index 0000000000..87c040fc15 --- /dev/null +++ b/packages/tools/kadena-cli/src/contract/index.js @@ -0,0 +1,15 @@ +import { deployCommand } from './deployCommand.js'; +import { retrieveCommand } from './retrieveCommand.js'; +const SUBCOMMAND_ROOT = 'contract'; +/** + * Create subcommand `kadena contract` + * `kadena contract retrieve` + * `kadena contract deploy` + */ +export function contractCommandFactory(program, version) { + const contractProgram = program + .command(SUBCOMMAND_ROOT) + .description(`Tool for managing smart-contracts`); + retrieveCommand(contractProgram, version); + deployCommand(contractProgram, version); +} diff --git a/packages/tools/kadena-cli/src/contract/retrieveCommand.js b/packages/tools/kadena-cli/src/contract/retrieveCommand.js new file mode 100644 index 0000000000..5508f5f2a6 --- /dev/null +++ b/packages/tools/kadena-cli/src/contract/retrieveCommand.js @@ -0,0 +1,56 @@ +import { processZodErrors } from '../utils/processZodErrors.js'; +import { retrieveContract } from './retrieveContract.js'; +import { Option } from 'commander'; +import debug from 'debug'; +import { z } from 'zod'; +// eslint-disable-next-line @rushstack/typedef-var +const Options = z.object({ + module: z.string({ + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + invalid_type_error: 'Error: -m, --module must be a string', + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + required_error: 'Error: -m, --module is required', + }), + out: z.string({ + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + invalid_type_error: 'Error: -o, --out must be a string', + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + required_error: 'Error: -o, --out is required', + }), + api: z.string({ + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + invalid_type_error: 'Error: --api must be a string', + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + required_error: 'Error: --api is required', + }), + network: z.enum(['mainnet', 'testnet']), + chain: z + .number({ + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + invalid_type_error: 'Error: -c, --chain must be a number', + }) + .min(0) + .max(19), +}); +export function retrieveCommand(program, version) { + program + .command('retrieve') + .description('Retrieve contract from a chainweb-api in a /local call (see also: https://github.com/kadena-io/chainweb-node#configuring-running-and-monitoring-the-health-of-a-chainweb-node).') + .option('-m, --module ', 'The module you want to retrieve (e.g. "coin")') + .option('-o, --out ', 'File to write the contract to') + .option('--api ', 'API to retrieve from (e.g. "https://api.chainweb.com/chainweb/0.0/mainnet01/chain/8/pact")') + .option('-n, --network ', 'Network to retrieve from (default "mainnet")', 'mainnet') + .addOption(new Option('-c, --chain ', 'Chain to retrieve from (default 1)') + .argParser((value) => parseInt(value, 10)) + .default(1)) + .action(async (args) => { + debug('retrieve-contract:action')({ args }); + try { + Options.parse(args); + } + catch (e) { + processZodErrors(program, e, args); + } + await retrieveContract(program, version)(args).catch(console.error); + }); +} diff --git a/packages/tools/kadena-cli/src/contract/retrieveContract.js b/packages/tools/kadena-cli/src/contract/retrieveContract.js new file mode 100644 index 0000000000..f25636e9ff --- /dev/null +++ b/packages/tools/kadena-cli/src/contract/retrieveContract.js @@ -0,0 +1,11 @@ +import { retrieveContractFromChain } from '../typescript/utils/retrieveContractFromChain.js'; +import { writeFileSync } from 'fs'; +import { join } from 'path'; +export function retrieveContract(__program, __version) { + return async function action({ module, out, network, chain, api }) { + const code = await retrieveContractFromChain(module, api, chain, network); + if (code !== undefined && code.length !== 0) { + writeFileSync(join(process.cwd(), out), code, 'utf8'); + } + }; +} diff --git a/packages/tools/kadena-cli/src/devnet/index.js b/packages/tools/kadena-cli/src/devnet/index.js new file mode 100644 index 0000000000..1c01d26063 --- /dev/null +++ b/packages/tools/kadena-cli/src/devnet/index.js @@ -0,0 +1,8 @@ +import { startCommand } from './start.js'; +const SUBCOMMAND_ROOT = 'devnet'; +export function devnetCommandFactory(program, version) { + const devnetProgram = program + .command(SUBCOMMAND_ROOT) + .description(`Tool for starting, stopping and managing the local devnet`); + startCommand(devnetProgram, version); +} diff --git a/packages/tools/kadena-cli/src/devnet/start.js b/packages/tools/kadena-cli/src/devnet/start.js new file mode 100644 index 0000000000..0f1d6380d8 --- /dev/null +++ b/packages/tools/kadena-cli/src/devnet/start.js @@ -0,0 +1,18 @@ +import { processZodErrors } from '../utils/processZodErrors.js'; +export function startCommand(program, version) { + program + .command('start') + .description('start the local devnet') + .action((args) => { + try { + // TODO: use @inquirer/prompts to interactively get missing flags + // TODO: create zod validator + // Options.parse(args); + } + catch (e) { + processZodErrors(program, e, args); + } + // TODO: implement + throw new Error('Not Implemented Yet'); + }); +} diff --git a/packages/tools/kadena-cli/src/index.js b/packages/tools/kadena-cli/src/index.js new file mode 100644 index 0000000000..bbfcdd2102 --- /dev/null +++ b/packages/tools/kadena-cli/src/index.js @@ -0,0 +1,38 @@ +#!/usr/bin/env node +import { accountCommandFactory } from './account/index.js'; +import { configCommandFactory } from './config/index.js'; +import { contractCommandFactory } from './contract/index.js'; +// import { dappCommandFactory } from './dapp/index.js'; +import { devnetCommandFactory } from './devnet/index.js'; +import { keysCommandFactory } from './keys/index.js'; +import { marmaladeCommandFactory } from './marmalade/index.js'; +import { networksCommandFactory } from './networks/index.js'; +import { txCommandFactory } from './tx/index.js'; +import { typescriptCommandFactory } from './typescript/index.js'; +import { clearCLI } from './utils/helpers.js'; +import { program } from 'commander'; +import { readFileSync } from 'node:fs'; +const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')); +// TODO: introduce root flag --no-interactive +// TODO: introduce root flag --ci +[ + configCommandFactory, + networksCommandFactory, + devnetCommandFactory, + keysCommandFactory, + accountCommandFactory, + txCommandFactory, + contractCommandFactory, + marmaladeCommandFactory, + typescriptCommandFactory, + // dappCommandFactory, +] + .flat() + .forEach(async (fn) => { + fn(program, packageJson.version); +}); +clearCLI(); +program + .description('CLI to interact with Kadena and its ecosystem') + .version(packageJson.version) + .parse(); diff --git a/packages/tools/kadena-cli/src/keys/generateFromHdKeysCommand.js b/packages/tools/kadena-cli/src/keys/generateFromHdKeysCommand.js new file mode 100644 index 0000000000..b7b4f06121 --- /dev/null +++ b/packages/tools/kadena-cli/src/keys/generateFromHdKeysCommand.js @@ -0,0 +1,28 @@ +import { clearCLI, collectResponses } from '../utils/helpers.js'; +import { processZodErrors } from '../utils/processZodErrors.js'; +import { HdKeygenOptions, hdKeygenQuestions } from './hdKeysGenerateOptions.js'; +// TO-DO: Implement this command +// choices: [ +// > Generate Public key from HD key +// > Generate Public and Private key from HD key +// > Generate Public and Private key from newly generated HD key +// 'Exit' +// ], +export function generateFromHdKeys(program, version) { + program + .command('gen-from-hd') + .description('generate keys from an HD-key') + .option('-f, --fileName ', 'Enter hd key file name to generate keys from') + .action(async (args) => { + try { + const responses = await collectResponses(args, hdKeygenQuestions); + const result = { ...args, ...responses }; + HdKeygenOptions.parse(result); + clearCLI(); + } + catch (e) { + console.log(e); + processZodErrors(program, e, args); + } + }); +} diff --git a/packages/tools/kadena-cli/src/keys/generateHdKeysCommand.js b/packages/tools/kadena-cli/src/keys/generateHdKeysCommand.js new file mode 100644 index 0000000000..e26959f6d4 --- /dev/null +++ b/packages/tools/kadena-cli/src/keys/generateHdKeysCommand.js @@ -0,0 +1,42 @@ +import { HDKEY_ENC_EXT, HDKEY_EXT } from '../constants/config.js'; +import { collectResponses } from '../utils/helpers.js'; // clearCLI +import { processZodErrors } from '../utils/processZodErrors.js'; +import { +// generateSeedPhrase, +getKeyPairsFromSeedPhrase, } from './legacy/chainweaver.js'; +import * as cryptoService from './utils/service.js'; +import * as storageService from './utils/storage.js'; +import { HdKeygenOptions, hdKeygenQuestions } from './hdKeysGenerateOptions.js'; +import chalk from 'chalk'; +export function generateHdKeys(program, version) { + program + .command('hd') + .description('generate an HD-key or public-private key-pair') + .option('-f, --fileName ', 'Enter a file name to store the key phrase in') + .option('-p, --password ', 'Enter a password to encrypt the key phrase with') + .action(async (args) => { + try { + const responses = await collectResponses(args, hdKeygenQuestions); + const result = { ...args, ...responses }; + HdKeygenOptions.parse(result); + const hasPassword = result.password !== undefined && result.password.trim() !== ''; + const { words, seed } = await cryptoService.generateSeed(result.password); + storageService.storeHdKey(words, seed, result.fileName, hasPassword); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const pairs = await getKeyPairsFromSeedPhrase('end mushroom modify drop talent eye bulk shop window leg flash knock'); + console.log(pairs.publicKey); + // console.log(generateSeedPhrase()); + // const testP1 = await cryptoService.processStoredSeed( + // seed, + // result.password, + // ); + // clearCLI(true); + console.log(chalk.green(`Generated HD Key: ${words}`)); + console.log(chalk.red(`The HD Key is stored within your keys folder under the filename: ${result.fileName}${hasPassword ? HDKEY_ENC_EXT : HDKEY_EXT} !`)); + } + catch (e) { + console.log(e); + processZodErrors(program, e, args); + } + }); +} diff --git a/packages/tools/kadena-cli/src/keys/generatePlainKeysCommand.js b/packages/tools/kadena-cli/src/keys/generatePlainKeysCommand.js new file mode 100644 index 0000000000..93529147f5 --- /dev/null +++ b/packages/tools/kadena-cli/src/keys/generatePlainKeysCommand.js @@ -0,0 +1,40 @@ +import { PLAINKEY_EXT } from '../constants/config.js'; +import { clearCLI, collectResponses } from '../utils/helpers.js'; +import { processZodErrors } from '../utils/processZodErrors.js'; +import * as cryptoService from './utils/service.js'; +import * as storageService from './utils/storage.js'; +import { +// PlainKeygenOptions, +plainKeygenQuestions, } from './plainKeysGenerateOptions.js'; +import chalk from 'chalk'; +export function generatePlainKeys(program, version) { + program + .command('plain') + .description('generate (plain) public-private key-pair') + .option('-al, --alias ', 'Enter an alias to store the key pair under (optional)') + .option('-a, --amount ', 'Enter the amount of key pairs you want to generate. (aliases can only be used when generating one key pair) (optional) (default: 1)') + .action(async (args) => { + try { + const responses = await collectResponses(args, plainKeygenQuestions); + const result = { ...args, ...responses }; + // PlainKeygenOptions.parse(result); + const plainKeyPairs = cryptoService.generateKeyPairsFromRandom(result.amount); + clearCLI(true); + console.log(chalk.green(`Generated Plain Key Pair(s): ${JSON.stringify(plainKeyPairs, null, 2)}`)); + if (result.alias !== undefined) { + storageService.savePlainKeyByAlias(result.alias, plainKeyPairs[0].publicKey, plainKeyPairs[0].secretKey, result.amount); + console.log(chalk.green('The Plain Key Pair is stored within your keys folder under the filename(s):')); + const totalKeys = result.amount === undefined ? 1 : result.amount; + for (let i = 0; i < totalKeys; i++) { + const keyName = i === 0 + ? `${result.alias}${PLAINKEY_EXT}` + : `${result.alias}-${i}${PLAINKEY_EXT}`; + console.log(chalk.green(`- ${keyName}`)); + } + } + } + catch (e) { + processZodErrors(program, e, args); + } + }); +} diff --git a/packages/tools/kadena-cli/src/keys/hdKeysGenerateOptions.js b/packages/tools/kadena-cli/src/keys/hdKeysGenerateOptions.js new file mode 100644 index 0000000000..fc617ba60e --- /dev/null +++ b/packages/tools/kadena-cli/src/keys/hdKeysGenerateOptions.js @@ -0,0 +1,45 @@ +import { isAlphanumeric } from '../utils/helpers.js'; +import { confirm, input, password } from '@inquirer/prompts'; +import { z } from 'zod'; +// eslint-disable-next-line @rushstack/typedef-var +export const HdKeygenOptions = z.object({ + fileName: z.string(), + password: z.string().optional(), +}); +export async function askForPassword() { + const usePassword = await confirm({ + message: 'Would you like to protect your seed with a password?', + }); + if (!usePassword) { + return undefined; + } + return await password({ + message: 'Enter a password for your HD key:', + validate: function (value) { + if (value.length < 8) { + return 'Password should be at least 6 characters long.'; + } + return true; + }, + }); +} +export const hdKeygenQuestions = [ + { + key: 'fileName', + prompt: async () => { + return await input({ + message: `Enter a filename for your HDkey:`, + validate: function (input) { + if (!isAlphanumeric(input)) { + return 'Filenames must be alphabetic! Please enter a valid name.'; + } + return true; + }, + }); + }, + }, + { + key: 'password', + prompt: async () => await askForPassword(), + }, +]; diff --git a/packages/tools/kadena-cli/src/keys/index.js b/packages/tools/kadena-cli/src/keys/index.js new file mode 100644 index 0000000000..846b6d66f0 --- /dev/null +++ b/packages/tools/kadena-cli/src/keys/index.js @@ -0,0 +1,22 @@ +import { generateFromHdKeys } from './generateFromHdKeysCommand.js'; +import { generateHdKeys } from './generateHdKeysCommand.js'; +import { generatePlainKeys } from './generatePlainKeysCommand.js'; +import { listKeys } from './listKeysCommand.js'; +import { manageKeys } from './manageKeysCommand.js'; +const SUBCOMMAND_ROOT = 'keys'; +export function generate(program, version) { + const generateProgram = program + .command('generate') + .description(`Generate keys`); + generateHdKeys(generateProgram, version); + generateFromHdKeys(generateProgram, version); + generatePlainKeys(generateProgram, version); +} +export function keysCommandFactory(program, version) { + const keysProgram = program + .command(SUBCOMMAND_ROOT) + .description(`Tool to generate and manage keys`); + generate(keysProgram, version); + listKeys(keysProgram, version); + manageKeys(keysProgram, version); +} diff --git a/packages/tools/kadena-cli/src/keys/legacy/chainweaver.js b/packages/tools/kadena-cli/src/keys/legacy/chainweaver.js new file mode 100644 index 0000000000..13d50cf4c2 --- /dev/null +++ b/packages/tools/kadena-cli/src/keys/legacy/chainweaver.js @@ -0,0 +1,31 @@ +import { binToHex, hexToBin } from '@kadena/cryptography-utils'; +import lib from 'cardano-crypto.js/kadena-crypto.js'; +// import lib from 'kadena-crypto.js/kadena-crypto.js'; +export function generateSeedPhrase() { + const seedPhrase = lib.kadenaGenMnemonic(); + return seedPhrase; +} +export const getKeyPairsFromSeedPhrase = (seedPhrase, index = 0) => { + const root = lib.kadenaMnemonicToRootKeypair('', seedPhrase); + const hardIndex = 0x80000000; + const newIndex = hardIndex + index; + const [privateRaw, pubRaw] = lib.kadenaGenKeypair('', root, newIndex); + const axprv = new Uint8Array(privateRaw); + const axpub = new Uint8Array(pubRaw); + const pub = binToHex(axpub); + const prv = binToHex(axprv); + return { + publicKey: pub, + secretKey: prv, + }; +}; +export function isValidSeedPhrase(seedPhrase) { + return lib.kadenaMnemonicCheck(seedPhrase); +} +export function getSignatureFromHash(hash, privateKey) { + const newHash = Buffer.from(hash, 'base64'); + const u8PrivateKey = hexToBin(privateKey); + const signature = lib.kadenaSign('', newHash, u8PrivateKey); + const s = new Uint8Array(signature); + return binToHex(s); +} diff --git a/packages/tools/kadena-cli/src/keys/listKeysCommand.js b/packages/tools/kadena-cli/src/keys/listKeysCommand.js new file mode 100644 index 0000000000..7000209ebf --- /dev/null +++ b/packages/tools/kadena-cli/src/keys/listKeysCommand.js @@ -0,0 +1,34 @@ +import { clearCLI, collectResponses } from '../utils/helpers.js'; +import { processZodErrors } from '../utils/processZodErrors.js'; +import { ListKeysOptions, listKeysQuestions } from './listKeysOptions.js'; +// TO-DO: Implement this command +// choices: [ +// 'List all keys', +// > HD keys encrypted with password +// > select and show key +// > HD keys unencrypted +// > select and show key +// > Plain keys +// > select and show key +// 'Exit' +// ], +export function listKeys(program, version) { + program + .command('list-keys') + .description('generate an HD-key or public-private key-pair') + .option('-k, --keyFile ', 'Select a key file to list keys from (HD or plain)') + .option('-a, --aliasedKeyFile ', 'Select a aliased key file to list keys from') + .option('-i, --index ', 'Select a key index to list keys from') + // .option('-c, --chainweaver ', 'Use chainweaver to list keys') + .action(async (args) => { + try { + const responses = await collectResponses(args, listKeysQuestions); + const result = { ...args, ...responses }; + clearCLI(); + ListKeysOptions.parse(result); + } + catch (e) { + processZodErrors(program, e, args); + } + }); +} diff --git a/packages/tools/kadena-cli/src/keys/listKeysOptions.js b/packages/tools/kadena-cli/src/keys/listKeysOptions.js new file mode 100644 index 0000000000..2d85b43337 --- /dev/null +++ b/packages/tools/kadena-cli/src/keys/listKeysOptions.js @@ -0,0 +1,45 @@ +import { capitalizeFirstLetter } from '../utils/helpers.js'; +import { password, select } from '@inquirer/prompts'; +import { z } from 'zod'; +// eslint-disable-next-line @rushstack/typedef-var +export const ListKeysOptions = z.object({ + keyType: z.string(), + password: z.string().optional(), +}); +export async function askForKeyType() { + const keyTypes = ['hd', 'plain'].map((type) => { + return { + value: type, + name: `${capitalizeFirstLetter(type)} key`, + }; + }); + const keyTypeChoice = await select({ + message: 'Select a key type to generate:', + choices: keyTypes, + }); + return keyTypeChoice.toLowerCase(); +} +export async function askForPassword(responses) { + if (responses.keyType === 'plain') { + return undefined; + } + return await password({ + message: 'Enter a password for your HD key:', + validate: function (value) { + if (value.length < 8) { + return 'Password should be at least 6 characters long.'; + } + return true; + }, + }); +} +export const listKeysQuestions = [ + { + key: 'keyType', + prompt: async () => await askForKeyType(), + }, + { + key: 'password', + prompt: async (config, responses) => await askForPassword(responses), + }, +]; diff --git a/packages/tools/kadena-cli/src/keys/manageKeysCommand.js b/packages/tools/kadena-cli/src/keys/manageKeysCommand.js new file mode 100644 index 0000000000..145d1d492b --- /dev/null +++ b/packages/tools/kadena-cli/src/keys/manageKeysCommand.js @@ -0,0 +1,31 @@ +import { processZodErrors } from '../utils/processZodErrors.js'; +// TO-DO: Implement this command +// choices: [ +// 'Encrypt an unencrypted HD key', +// '> List of unencrypted HD keys', +// '> select and encrypt an unencrypted HD key', +// 'Re-encrypt a key with a new password', +// '> List of encrypted HD keys', +// '> select and encrypt with given password', +// 'Delete a key', +// '> List of all keys', +// '> select and delete a key', +// 'Exit' +// ], +export function manageKeys(program, version) { + program + .command('manage') + .description('Manage key(s)') + .action((args) => { + try { + // TODO: use @inquirer/prompts to interactively get missing flags + // TODO: create zod validator + // Options.parse(args); + } + catch (e) { + processZodErrors(program, e, args); + } + // TODO: implement + throw new Error('Not Implemented Yet'); + }); +} diff --git a/packages/tools/kadena-cli/src/keys/plainKeysGenerateOptions.js b/packages/tools/kadena-cli/src/keys/plainKeysGenerateOptions.js new file mode 100644 index 0000000000..ed95ea8f4b --- /dev/null +++ b/packages/tools/kadena-cli/src/keys/plainKeysGenerateOptions.js @@ -0,0 +1,24 @@ +import { isAlphabetic } from '../utils/helpers.js'; +import { input } from '@inquirer/prompts'; +import { z } from 'zod'; +// eslint-disable-next-line @rushstack/typedef-var +export const PlainKeygenOptions = z.object({ + alias: z.string().optional(), + amount: z.number().optional(), +}); +export const plainKeygenQuestions = [ + { + key: 'alias', + prompt: async () => { + return await input({ + message: `Enter a alias for your key:`, + validate: function (input) { + if (!isAlphabetic(input)) { + return 'Alias must be alphabetic! Please enter a valid name.'; + } + return true; + }, + }); + }, + }, +]; diff --git a/packages/tools/kadena-cli/src/keys/utils/encode.js b/packages/tools/kadena-cli/src/keys/utils/encode.js new file mode 100644 index 0000000000..295d2f5742 --- /dev/null +++ b/packages/tools/kadena-cli/src/keys/utils/encode.js @@ -0,0 +1,36 @@ +/** + * Convert a Base64Url encoded string to a standard Base64 encoded string. + * @param {string} str - The Base64Url encoded string. + * @returns {string} - Returns the standard Base64 encoded representation of the input. + */ +function unescape(str) { + return (str + '==='.slice((str.length + 3) % 4)) + .replace(/-/g, '+') + .replace(/_/g, '/'); +} +/** + * Convert a standard Base64 encoded string to a Base64Url encoded string. + * @param {string} str - The Base64 encoded string. + * @returns {string} - Returns the Base64Url encoded representation of the input. + */ +function escape(str) { + return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); +} +/** + * Encode a string to a Base64Url encoded representation. + * @param {string} str - The input string to be encoded. + * @param {BufferEncoding} [encoding='utf8'] - The encoding of the input string. + * @returns {string} - Returns the Base64Url encoded representation of the input. + */ +export function encode(str, encoding = 'utf8') { + return escape(Buffer.from(str, encoding).toString('base64')); +} +/** + * Decode a Base64Url encoded string to its original representation. + * @param {string} str - The Base64Url encoded string to be decoded. + * @param {BufferEncoding} [encoding='utf8'] - The encoding to use for the output string. + * @returns {string} - Returns the decoded representation of the Base64Url encoded input. + */ +export function decode(str, encoding = 'utf8') { + return Buffer.from(unescape(str), 'base64').toString(encoding); +} diff --git a/packages/tools/kadena-cli/src/keys/utils/encrypt.js b/packages/tools/kadena-cli/src/keys/utils/encrypt.js new file mode 100644 index 0000000000..662d753fff --- /dev/null +++ b/packages/tools/kadena-cli/src/keys/utils/encrypt.js @@ -0,0 +1,66 @@ +import { createCipheriv, createDecipheriv, pbkdf2Sync, randomBytes, } from 'crypto'; +/** Constant salt value used for deriving keys */ +const SALT = Buffer.from('FkM5B23nB6LVY7mrwDeh'); +/** + * Derive a cryptographic key from the provided password. + * @param {string} password - User's password. + * @returns {Buffer} - Returns the derived cryptographic key. + */ +function deriveKey(password) { + return pbkdf2Sync(password, SALT, 1000, 32, 'sha256'); +} +/** + * Encrypt the provided text using AES-256-GCM algorithm. + * @param {Buffer} text - Text to encrypt. + * @param {string} password - User's password. + * @returns {{ cipherText: Buffer; iv: Buffer; tag: Buffer }} - Returns the encrypted text, initialization vector, and authentication tag. + */ +export function encrypt(text, password) { + const key = deriveKey(password); + const iv = randomBytes(12); + const cipher = createCipheriv('aes-256-gcm', key, iv); + const cipherText = Buffer.concat([cipher.update(text), cipher.final()]); + const tag = cipher.getAuthTag(); // Capture the authentication tag + return { + cipherText, + iv, + tag, + }; +} +/** + * Decrypt the provided encrypted text using AES-256-GCM algorithm. + * @param {Encrypted} encrypted - Encrypted text, initialization vector, and authentication tag. + * @param {string} password - User's password. + * @returns {Buffer | undefined} - Returns the decrypted text or undefined if decryption fails. + */ +export function decrypt(encrypted, password) { + const key = deriveKey(password); + const decipher = createDecipheriv('aes-256-gcm', key, encrypted.iv); + decipher.setAuthTag(encrypted.tag); // Set the authentication tag + try { + return Buffer.concat([ + decipher.update(encrypted.cipherText), + decipher.final(), + ]); + } + catch (err) { + console.warn('Failed to decrypt seed'); + return undefined; + } +} +/** + * Convert a Buffer to a Base64 encoded string. + * @param {Buffer} buffer - Buffer to convert. + * @returns {string} - Returns the Base64 encoded string. + */ +export function bufferToBase64(buffer) { + return buffer.toString('base64'); +} +/** + * Convert a Base64 encoded string to a Buffer. + * @param {string} base64 - Base64 encoded string to convert. + * @returns {Buffer} - Returns the resulting Buffer. + */ +export function base64ToBuffer(base64) { + return Buffer.from(base64, 'base64'); +} diff --git a/packages/tools/kadena-cli/src/keys/utils/helpers.js b/packages/tools/kadena-cli/src/keys/utils/helpers.js new file mode 100644 index 0000000000..b7bac2e884 --- /dev/null +++ b/packages/tools/kadena-cli/src/keys/utils/helpers.js @@ -0,0 +1,44 @@ +import { HDKEY_EXT, KEY_DIR, PLAINKEY_EXT } from '../../constants/config.js'; +import { existsSync, mkdirSync, readdirSync } from 'fs'; +import path from 'path'; +/** + * Fetches all plain key files from the specified directory. + * @returns {string[]} Array of plain key filenames without their extensions. + */ +export function getPlainKeys() { + return getFilesWithExtension(KEY_DIR, PLAINKEY_EXT); +} +/** + * Fetches all encrypted HD key files from the specified directory. + * @returns {string[]} Array of encrypted HD key filenames without their extensions. + */ +export function getEncryptedHDKeys() { + return getFilesWithExtension(KEY_DIR, HDKEY_EXT); +} +/** + * Fetches all unencrypted HD key files from the specified directory. + * @returns {string[]} Array of unencrypted HD key filenames without their extensions. + */ +export function getUnencryptedHDKeys() { + return getFilesWithExtension(KEY_DIR, HDKEY_EXT); +} +/** + * Fetches all files with a specific extension from a given directory. + * @param {string} dir - The directory path from which files are to be read. + * @param {string} extension - The file extension to filter by. + * @returns {string[]} Array of filenames with the specified extension, without the extension itself. + */ +export function getFilesWithExtension(dir, extension) { + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + try { + return readdirSync(dir) + .filter((filename) => filename.toLowerCase().endsWith(extension)) + .map((filename) => path.basename(filename.toLowerCase(), extension)); + } + catch (error) { + console.error(`Error reading directory for extension ${extension}:`, error); + return []; + } +} diff --git a/packages/tools/kadena-cli/src/keys/utils/service.js b/packages/tools/kadena-cli/src/keys/utils/service.js new file mode 100644 index 0000000000..93f1bfeb02 --- /dev/null +++ b/packages/tools/kadena-cli/src/keys/utils/service.js @@ -0,0 +1,285 @@ +import { sign as cryptoSign } from '@kadena/cryptography-utils'; +import { base64ToBuffer, bufferToBase64, decrypt, encrypt } from './encrypt.js'; +import { deriveKeyPair } from './sign.js'; +import * as bip39 from '@scure/bip39'; +import { wordlist } from '@scure/bip39/wordlists/english'; +import { randomBytes } from 'ed25519-keygen/utils'; +/** + * Convert a given mnemonic phrase into a seed buffer. + * + * @param {string} mnemonic - A mnemonic seed phrase to be converted into a seed buffer. + * @param {string} [password] - Optional password for encrypting the seed. + * @throws {Error} Throws an error if the provided mnemonic is not valid. + * @returns {Promise<{ seedBuffer: Uint8Array, seed: string }>} - Returns the seed buffer and processed seed. + */ +export async function setSeedFromMnemonic(mnemonic, password) { + if (!bip39.validateMnemonic(mnemonic, wordlist)) { + throw Error('Invalid mnemonic.'); + } + const seedBuffer = await bip39.mnemonicToSeed(mnemonic); + const seed = processSeedForStorage(seedBuffer, password); + return { + seedBuffer, + seed, + }; +} +/** + * Generates a seed based on a mnemonic phrase. The seed can be either encrypted or not, + * based on whether a password is provided. + * + * @param {string} [password] - Optional password for encrypting the seed. If not provided, the seed remains unencrypted. + * @returns {Promise<{ words: string, seed: string }>} An object containing the mnemonic words and the stored seed. + * @throws Will throw an error if mnemonic generation or validation fails, or if seed buffering fails. + */ +export async function generateSeed(password) { + const words = bip39.generateMnemonic(wordlist); // Assuming wordlist is globally defined or you can pass it as parameter if needed + if (!bip39.validateMnemonic(words, wordlist)) { + throw Error('Invalid mnemonic.'); + } + const seedBuffer = await bip39.mnemonicToSeed(words); + const seed = processSeedForStorage(seedBuffer, password); + return { + words, + seed, + }; +} +/** + * Function to get the public key at a specific index. + * + * @param {Uint8Array} seedBuffer - The seed buffer used for key generation. + * @param {number} index - The index of the public key to retrieve. + * @returns {string} The public key. + */ +export function getPublicKeyAtIndex(seedBuffer, index) { + return deriveKeyPair(seedBuffer, index).publicKey; +} +/** + * Function to get an array of public keys within a specified range of indices. + * + * @param {Uint8Array} seedBuffer - The seed buffer used for key generation. + * @param {number} startIndex - The starting index of the range (inclusive). + * @param {number} endIndex - The ending index of the range (exclusive). + * @returns {string[]} An array of public keys. + * @throws {Error} If startIndex is greater than or equal to endIndex. + */ +export function getPublicKeysInRange(seedBuffer, startIndex, endIndex) { + if (startIndex >= endIndex) { + throw new Error('startIndex must be less than endIndex'); + } + const publicKeys = []; + for (let index = startIndex; index < endIndex; index++) { + const publicKey = deriveKeyPair(seedBuffer, index).publicKey; + publicKeys.push(publicKey); + } + return publicKeys; +} +/** + * Function to restore a wallet using a stored seed. + * + * @param {string} storedSeed - The stored seed string, which may be encrypted. + * @param {string} password - The password for decrypting the seed. + * @param {number} keyLength - The number of public keys to generate. + * @returns {string[]} An array of public keys. + */ +export function restoreWallet(storedSeed, password, keyLength) { + try { + const seedBuffer = processStoredSeed(storedSeed, password); + if (seedBuffer === undefined) { + throw Error('Failed to set _seedBuffer.'); + } + return _generateKeys(seedBuffer, keyLength); + } + catch (error) { + console.error(error); + return []; + } +} +/** + * Function to generate a public key based on the provided seed buffer and public keys. + * + * @param {Uint8Array} seedBuffer - The seed buffer used for key generation. + * @param {string[]} publicKeys - An array of existing public keys. + * @returns {string} The generated public key. + */ +export function generatePublicKey(seedBuffer, publicKeys) { + const pair = deriveKeyPair(seedBuffer, publicKeys.length); + return pair.publicKey; +} +/** + * Generates a single key pair based on the provided seed buffer and the current number of public keys. + * + * @param {Uint8Array} seedBuffer - The seed buffer to use for key generation. + * @param {number} currentPublicKeyCount - The current number of public keys (to determine the next index). + * @returns {{ publicKey: string; secretKey: string }} The generated key pair. + * @throws {Error} Throws an error if the seed buffer is not provided. + */ +export function generateKeyPair(seedBuffer, currentPublicKeyCount) { + if (seedBuffer === undefined) + throw Error('No seed provided.'); + const pair = deriveKeyPair(seedBuffer, currentPublicKeyCount); + return { + publicKey: pair.publicKey, + secretKey: pair.privateKey, + }; +} +/** + * Generates multiple key pairs based on the provided seed buffer. + * + * @param {Uint8Array} seedBuffer - The seed buffer to use for key generation. + * @param {number} [count=1] - The number of key pairs to generate. + * @returns {{ publicKey: string; secretKey: string }[]} An array of generated key pairs. + * @throws {Error} Throws an error if the seed buffer is not provided. + */ +export function generateKeyPairsFromSeed(seedBuffer, count = 1) { + if (seedBuffer === undefined) + throw Error('No seed provided.'); + const keyPairs = []; + for (let i = 0; i < count; i++) { + const pair = deriveKeyPair(seedBuffer, i); + keyPairs.push({ + publicKey: pair.publicKey, + secretKey: pair.privateKey, + }); + } + return keyPairs; +} +/** + * Generates random key pairs without updating the internal state. + * + * @param {number} [count=1] - The number of key pairs to generate. + * @returns {{ publicKey: string; secretKey: string }[]} An array of generated key pairs. + */ +export function generateKeyPairsFromRandom(count = 1) { + const keyPairs = []; + for (let i = 0; i < count; i++) { + const randomSeedBuffer = randomBytes(32); + const pair = deriveKeyPair(randomSeedBuffer, keyPairs.length + i); + keyPairs.push({ + publicKey: pair.publicKey, + secretKey: pair.privateKey, + }); + } + return keyPairs; +} +/** + * Signs a given message using the specified public key. + * + * @param {string} msg - The message to be signed. + * @param {string} publicKey - The public key to use for signing. + * @param {string[]} publicKeys - An array of public keys to search in. + * @param {Uint8Array} seedBuffer - The seed buffer to derive keys from. + * @returns {ReturnType} The signature result. + * @throws {Error} Throws an error if the seed is not set, the public key is not found, or there's a public key mismatch. + */ +export function sign(msg, publicKey, publicKeys, seedBuffer) { + if (seedBuffer === undefined) + throw Error('No seed set.'); + const index = getPublicKeyIndex(publicKey, publicKeys); + const pair = deriveKeyPair(seedBuffer, index); + if (pair.publicKey !== publicKey) { + throw Error('Public key mismatch.'); + } + return cryptoSign(msg, { + secretKey: pair.privateKey, + publicKey: pair.publicKey, + }); +} +/** + * Signs a given transaction using a custom signing function. + * + * @param {IUnsignedCommand} tx - The unsigned transaction command. + * @param {string[]} publicKeys - An array of public keys to check against. + * @param {Uint8Array} seedBuffer - The seed buffer to derive keys from. + * @param {typeof sign} signFunction - A custom signing function that takes message, public key, public keys array, and seed buffer as arguments. + * @returns {IUnsignedCommand} The signed transaction command. + */ +export function signTransaction(tx, publicKeys, seedBuffer, signFunction) { + const command = JSON.parse(tx.cmd); + const sigs = command.signers.map((signer) => { + if (!publicKeys.includes(signer.pubKey)) { + return undefined; + } + const { sig } = signFunction(tx.cmd, signer.pubKey, publicKeys, seedBuffer); + if (sig === undefined) + return undefined; + return { sig, pubKey: signer.pubKey }; + }); + return { ...tx, sigs: sigs }; +} +/** + * Gets the index of a public key in the given array of public keys. + * + * @param {string} publicKey - The public key to search for. + * @param {string[]} publicKeys - An array of public keys to search in. + * @returns {number} The index of the public key in the array. + * @throws {Error} Throws an error if the public key is not found. + */ +function getPublicKeyIndex(publicKey, publicKeys) { + const index = publicKeys.indexOf(publicKey); + if (index === -1) { + throw Error(`No public key found. (${publicKey})`); + } + return index; +} +/** + * Helper function to generate an array of public keys based on the provided seed buffer and length. + * + * @param {Uint8Array} seedBuffer - The seed buffer to use for key generation. + * @param {number} length - The number of public keys to generate. + * @returns {string[]} An array of generated public keys. + */ +function _generateKeys(seedBuffer, length) { + const publicKeys = []; + for (let i = 0; i < length; i++) { + const pair = deriveKeyPair(seedBuffer, i); + publicKeys.push(pair.publicKey); + } + return publicKeys; +} +/** + * Abstracts the process of either encrypting the seed buffer or converting it to Base64 based on a provided password. + * + * @param {Uint8Array} seedBuffer - Seed buffer to be encrypted or converted. + * @param {string} [password] - Optional password for encrypting the seed buffer. + * @returns {string} - Returns either the encrypted seed string or the Base64 encoded seed string. + */ +function processSeedForStorage(seedBuffer, password) { + if (password !== undefined) { + const bufferSeed = Buffer.from(seedBuffer); + const encrypted = encrypt(bufferSeed, password); + const cipherText = bufferToBase64(encrypted.cipherText); + const iv = bufferToBase64(encrypted.iv); + const tag = bufferToBase64(encrypted.tag); // Convert the authentication tag to Base64 + return `${cipherText}.${iv}.${tag}`; + } + else { + return bufferToBase64(Buffer.from(seedBuffer)); + } +} +/** + * Processes a stored seed string to obtain a Uint8Array seed buffer. + * + * @param {string} storedSeed - The stored seed string, which may be encrypted or in Base64 format. + * @param {string} [password] - Optional password for decrypting the seed string if encrypted. + * @throws {Error} Throws an error if the stored seed is not provided, or if decryption fails. + * @returns {Uint8Array} The seed buffer obtained from the stored seed. + */ +export function processStoredSeed(storedSeed, password) { + if (!storedSeed) + throw Error('No seed provided.'); + if (password !== undefined) { + const [cipherTextBase64, ivBase64, tagBase64] = storedSeed.split('.'); // Split into three parts + const decrypted = decrypt({ + cipherText: base64ToBuffer(cipherTextBase64), + iv: base64ToBuffer(ivBase64), + tag: base64ToBuffer(tagBase64), // Convert the tag from Base64 to buffer + }, password); + if (!decrypted) { + throw Error('Failed to decrypt seed.'); + } + return Uint8Array.from(decrypted); + } + else { + return Uint8Array.from(base64ToBuffer(storedSeed)); + } +} diff --git a/packages/tools/kadena-cli/src/keys/utils/sign.js b/packages/tools/kadena-cli/src/keys/utils/sign.js new file mode 100644 index 0000000000..a40369c2b8 --- /dev/null +++ b/packages/tools/kadena-cli/src/keys/utils/sign.js @@ -0,0 +1,82 @@ +import { sign } from '@kadena/cryptography-utils'; +import { HDKey } from 'ed25519-keygen/hdkey'; +const KDA_COIN_TYPE = 626; +/** + * Convert a Uint8Array to a hexadecimal string. + * @param {Uint8Array} uint8Array - The array to convert. + * @returns {string} - Returns the hexadecimal representation of the input. + */ +const uint8ArrayToHex = (uint8Array) => { + if (uint8Array.length === 33 && uint8Array.at(0) === 0) { + uint8Array = uint8Array.slice(1); + } + return [...uint8Array].map((x) => x.toString(16).padStart(2, '0')).join(''); +}; +/** + * Derive a key pair using a seed and an index. + * @param {Uint8Array} seed - The seed for key derivation. + * @param {number} index - The index for key derivation. + * @returns {{ privateKey: string; publicKey: string }} - Returns the derived private and public keys. + */ +export const deriveKeyPair = (seed, index) => { + const key = HDKey.fromMasterSeed(seed).derive(`m/44'/${KDA_COIN_TYPE}'/0'/0/${index}`, true); + return { + privateKey: uint8ArrayToHex(key.privateKey), + publicKey: uint8ArrayToHex(key.publicKey), + }; +}; +/** + * Creates a signer function for a given public and secret key pair. + * + * @function + * @param {string} publicKey - The public key for signing. + * @param {string} [secretKey] - The optional secret key for signing. + * @returns {Function} A function that takes an unsigned command and returns the command with its signature. + * + * @example + * const signer = signWithKeyPair('myPublicKey', 'mySecretKey'); + * const signedCommand = signer(myUnsignedCommand); + * + * @throws {Error} Throws an error if the signature is undefined. + */ +export const signWithKeyPair = (publicKey, secretKey) => { + return (tx) => { + const { sig } = sign(tx.cmd, { publicKey, secretKey }); + if (sig === undefined) { + throw new Error('Signature is undefined'); + } + return { + ...tx, + sigs: [{ sig }], + }; + }; +}; +/** + * Generate a signer function using a seed and an index. + * @param {Uint8Array} seed - The seed for key derivation. + * @param {number} index - The index for key derivation. + * @returns {(tx: IUnsignedCommand) => { sigs: { sig: string }[] }} - Returns a function that can sign a transaction. + */ +export const signWithSeed = (seed, index) => { + const { publicKey, privateKey } = deriveKeyPair(seed, index); + return signWithKeyPair(publicKey, privateKey); +}; +// TO-DO: Implement. +/** + * Placeholder function for signing with Chainweaver. + */ +export const signWithChainweaver = () => { + return 'signWithChainweaver'; +}; +/** + * Placeholder function for signing with WalletConnect. + */ +export const signWithWalletConnect = () => { + return 'signWithWalletConnect'; +}; +/** + * Placeholder function for signing with Ledger. + */ +export const signWithLedger = () => { + return 'signWithWalletConnect'; +}; diff --git a/packages/tools/kadena-cli/src/keys/utils/storage.js b/packages/tools/kadena-cli/src/keys/utils/storage.js new file mode 100644 index 0000000000..775c7330ec --- /dev/null +++ b/packages/tools/kadena-cli/src/keys/utils/storage.js @@ -0,0 +1,104 @@ +import { HDKEY_ENC_EXT, HDKEY_EXT, KEY_DIR, PLAINKEY_EXT, } from '../../constants/config.js'; +import { ensureDirectoryExists } from '../../utils/filesystem.js'; +import { sanitizeFilename } from '../../utils/helpers.js'; +import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +/** + * Saves the given key pair to multiple files based on the provided amount. + * Only subsequent files will be postfixed with an index if the amount is greater than 1. + * + * @param {string} alias - The base alias for the key pair. + * @param {string} publicKey - The public key. + * @param {string} privateKey - The private key. + * @param {number} [amount=1] - The number of files to write. + */ +export function savePlainKeyByAlias(alias, publicKey, privateKey, amount = 1) { + ensureDirectoryExists(KEY_DIR); + const sanitizedAlias = sanitizeFilename(alias).toLocaleLowerCase(); + for (let i = 0; i < amount; i++) { + let fileName = sanitizedAlias; + // Append index to the filename if it's not the first file. + if (i > 0) { + fileName += `-${i}`; + } + fileName += PLAINKEY_EXT; + const filePath = join(KEY_DIR, fileName); + const data = { + publicKey, + privateKey, + }; + writeFileSync(filePath, JSON.stringify(data)); + } +} +/** + * Retrieves a key pair based on the given alias. + * + * @param {string} alias - The alias corresponding to the key file to be fetched. + * @returns {{publicKey: string; secretKey: string} | undefined} The key pair if found, otherwise undefined. + */ +export function getStoredPlainKeyByAlias(alias) { + const filePath = join(KEY_DIR, `${alias}${PLAINKEY_EXT}`); + if (existsSync(filePath)) { + const data = readFileSync(filePath, 'utf-8'); + const keyPair = JSON.parse(data); + return { + publicKey: keyPair.publicKey, + secretKey: keyPair.privateKey, + }; + } + return undefined; +} +/** + * Loads the public keys from key files based on their aliases. + * Iterates through files in the key directory, and if a file matches the '.key' extension, + * its content is parsed, and if it contains a valid public key, it's added to the returned array. + * + * @returns {string[]} Array of public keys. + */ +export function loadAllKeysFromAliasFiles() { + ensureDirectoryExists(KEY_DIR); + const publicKeys = []; + const files = readdirSync(KEY_DIR); + for (const file of files) { + if (file.endsWith('.key')) { + const filePath = join(KEY_DIR, file); + const data = readFileSync(filePath, 'utf-8'); + const keyPair = JSON.parse(data); + if (keyPair !== undefined && + typeof keyPair.publicKey === 'string' && + keyPair.publicKey.length > 0) { + publicKeys.push(keyPair.publicKey); + } + } + } + return publicKeys; +} +/** + * Stores the mnemonic phrase or seed to the filesystem. + * + * @param {string} words - The mnemonic phrase. + * @param {string} seed - The seed. + * @param {string} fileName - The name of the file to store the mnemonic or seed in. + * @param {boolean} hasPassword - Whether a password was used to generate the seed. + */ +export function storeHdKey(words, seed, fileName, hasPassword) { + ensureDirectoryExists(KEY_DIR); + const sanitizedFilename = sanitizeFilename(fileName).toLowerCase(); + const fileExtension = hasPassword ? HDKEY_ENC_EXT : HDKEY_EXT; + const dataToStore = hasPassword ? seed : words; + const storagePath = join(KEY_DIR, `${sanitizedFilename}${fileExtension}`); + writeFileSync(storagePath, dataToStore, 'utf8'); +} +/** + * Retrieves the stored mnemonic phrase from the filesystem. + * + * @param {string} fileName - The name of the file where the mnemonic is stored. + * @returns {string | undefined} The stored mnemonic phrase, or undefined if not found. + */ +export function getStoredHdKey(fileName) { + const storagePath = join(KEY_DIR, fileName); + if (existsSync(storagePath)) { + return readFileSync(storagePath, 'utf8'); + } + return undefined; +} diff --git a/packages/tools/kadena-cli/src/marmalade/index.js b/packages/tools/kadena-cli/src/marmalade/index.js new file mode 100644 index 0000000000..1ce3c5ca8a --- /dev/null +++ b/packages/tools/kadena-cli/src/marmalade/index.js @@ -0,0 +1,10 @@ +import { mintCommand } from './mintCommand.js'; +import { storeCommand } from './storeCommand.js'; +const SUBCOMMAND_ROOT = 'marmalade'; +export function marmaladeCommandFactory(program, version) { + const marmaladeProgram = program + .command(SUBCOMMAND_ROOT) + .description(`Tool for minting and managing NFTs with Marmalade`); + mintCommand(marmaladeProgram, version); + storeCommand(marmaladeProgram, version); +} diff --git a/packages/tools/kadena-cli/src/marmalade/mintCommand.js b/packages/tools/kadena-cli/src/marmalade/mintCommand.js new file mode 100644 index 0000000000..450814e87b --- /dev/null +++ b/packages/tools/kadena-cli/src/marmalade/mintCommand.js @@ -0,0 +1,18 @@ +import { processZodErrors } from '../utils/processZodErrors.js'; +export function mintCommand(program, version) { + program + .command('mint') + .description('mint a new NFT on Marmalade') + .action((args) => { + try { + // TODO: use @inquirer/prompts to interactively get missing flags + // TODO: create zod validator + // Options.parse(args); + } + catch (e) { + processZodErrors(program, e, args); + } + // TODO: implement + throw new Error('Not Implemented Yet'); + }); +} diff --git a/packages/tools/kadena-cli/src/marmalade/storeCommand.js b/packages/tools/kadena-cli/src/marmalade/storeCommand.js new file mode 100644 index 0000000000..6d02fafb2d --- /dev/null +++ b/packages/tools/kadena-cli/src/marmalade/storeCommand.js @@ -0,0 +1,19 @@ +import { processZodErrors } from '../utils/processZodErrors.js'; +export function storeCommand(program, version) { + program + .command('store') + .description('store') + .option('-p, --provider ') + .action((args) => { + try { + // TODO: use @inquirer/prompts to interactively get missing flags + // TODO: create zod validator + // Options.parse(args); + } + catch (e) { + processZodErrors(program, e, args); + } + // TODO: implement + throw new Error('Not Implemented Yet'); + }); +} diff --git a/packages/tools/kadena-cli/src/networks/createNetworksCommand.js b/packages/tools/kadena-cli/src/networks/createNetworksCommand.js new file mode 100644 index 0000000000..61576aacb5 --- /dev/null +++ b/packages/tools/kadena-cli/src/networks/createNetworksCommand.js @@ -0,0 +1,70 @@ +import { defaultNetworksPath } from '../constants/networks.js'; +import { ensureFileExists } from '../utils/filesystem.js'; +import { clearCLI, collectResponses } from '../utils/helpers.js'; +import { processZodErrors } from '../utils/processZodErrors.js'; +import { NetworksCreateOptions, networksCreateQuestions, } from './networksCreateQuestions.js'; +import { displayNetworkConfig, writeNetworks } from './networksHelpers.js'; +import { select } from '@inquirer/prompts'; +import chalk from 'chalk'; +import debug from 'debug'; +import path from 'path'; +async function shouldProceedWithNetworkCreate(network) { + const filePath = path.join(defaultNetworksPath, `${network}.yaml`); + if (ensureFileExists(filePath)) { + const overwrite = await select({ + message: `Your network (config) already exists. Do you want to update it?`, + choices: [ + { value: 'yes', name: 'Yes' }, + { value: 'no', name: 'No' }, + ], + }); + return overwrite === 'yes'; + } + return true; +} +async function runNetworksCreate(program, version, args) { + try { + const responses = await collectResponses(args, networksCreateQuestions); + const networkConfig = { ...args, ...responses }; + NetworksCreateOptions.parse(networkConfig); + writeNetworks(networkConfig); + displayNetworkConfig(networkConfig); + const proceed = await select({ + message: 'Is the above network configuration correct?', + choices: [ + { value: 'yes', name: 'Yes' }, + { value: 'no', name: 'No' }, + ], + }); + if (proceed === 'no') { + clearCLI(true); + console.log(chalk.yellow("Let's restart the configuration process.")); + await runNetworksCreate(program, version, args); + } + else { + console.log(chalk.green('Configuration complete. Goodbye!')); + } + } + catch (e) { + console.error(e); + processZodErrors(program, e, args); + } +} +export function createNetworksCommand(program, version) { + program + .command('create') + .description('Create new network') + .option('-n, --network ', 'Kadena network (e.g. "mainnet")') + .option('-nid, --networkId ', 'Kadena network Id (e.g. "mainnet01")') + .option('-h, --networkHost ', 'Kadena network host (e.g. "https://api.chainweb.com")') + .option('-e, --networkExplorerUrl ', 'Kadena network explorer (e.g. "https://explorer.chainweb.com/mainnet/tx/")') + .action(async (args) => { + debug('network-create:action')({ args }); + if (args.network && + !(await shouldProceedWithNetworkCreate(args.network.toLowerCase()))) { + console.log(chalk.red('Network creation aborted.')); + return; + } + await runNetworksCreate(program, version, args); + }); +} diff --git a/packages/tools/kadena-cli/src/networks/index.js b/packages/tools/kadena-cli/src/networks/index.js new file mode 100644 index 0000000000..93f0b7aef5 --- /dev/null +++ b/packages/tools/kadena-cli/src/networks/index.js @@ -0,0 +1,14 @@ +import { createSimpleSubCommand } from '../utils/helpers.js'; +import { createNetworksCommand } from './createNetworksCommand.js'; +import { listNetworksAction } from './listNetworksCommand.js'; +import { manageNetworks } from './manageNetworksCommand.js'; +const SUBCOMMAND_ROOT = 'networks'; +export function networksCommandFactory(program, version) { + const networksProgram = program + .command(SUBCOMMAND_ROOT) + .description(`Tool to create and manage networks`); + // Attach list subcommands to the networksProgram + createSimpleSubCommand('list', 'List all available networks', listNetworksAction)(networksProgram); + manageNetworks(networksProgram, version); + createNetworksCommand(networksProgram, version); +} diff --git a/packages/tools/kadena-cli/src/networks/listNetworksCommand.js b/packages/tools/kadena-cli/src/networks/listNetworksCommand.js new file mode 100644 index 0000000000..39db09da85 --- /dev/null +++ b/packages/tools/kadena-cli/src/networks/listNetworksCommand.js @@ -0,0 +1,4 @@ +import { displayNetworksConfig } from './networksHelpers.js'; +export const listNetworksAction = (args) => { + displayNetworksConfig(); +}; diff --git a/packages/tools/kadena-cli/src/networks/manageNetworksCommand.js b/packages/tools/kadena-cli/src/networks/manageNetworksCommand.js new file mode 100644 index 0000000000..4062072e18 --- /dev/null +++ b/packages/tools/kadena-cli/src/networks/manageNetworksCommand.js @@ -0,0 +1,39 @@ +import { defaultNetworksPath } from '../constants/networks.js'; +import { clearCLI, collectResponses, getExistingNetworks, } from '../utils/helpers.js'; +import { processZodErrors } from '../utils/processZodErrors.js'; +import { networkManageQuestions, NetworksCreateOptions, } from './networksCreateQuestions.js'; +import { writeNetworks } from './networksHelpers.js'; +import { select } from '@inquirer/prompts'; +import chalk from 'chalk'; +import { readFileSync } from 'fs'; +import yaml from 'js-yaml'; +import path from 'path'; +export function manageNetworks(program, version) { + program + .command('manage') + .description('Manage network(s)') + .action(async (args) => { + try { + const existingNetworks = getExistingNetworks(); + if (existingNetworks.length === 0) { + console.log(chalk.red('No existing networks found.')); + return; + } + const selectedNetwork = await select({ + message: 'Select the network you want to manage:', + choices: existingNetworks, + }); + const networkFilePath = path.join(defaultNetworksPath, `${selectedNetwork}.yaml`); + const existingConfig = yaml.load(readFileSync(networkFilePath, 'utf8')); + const responses = await collectResponses({ network: selectedNetwork }, networkManageQuestions); + const networkConfig = { ...existingConfig, ...responses }; + NetworksCreateOptions.parse(networkConfig); + writeNetworks(networkConfig); + clearCLI(); + console.log(chalk.green('Network configurations updated.')); + } + catch (e) { + processZodErrors(program, e, args); + } + }); +} diff --git a/packages/tools/kadena-cli/src/networks/networksCreateQuestions.js b/packages/tools/kadena-cli/src/networks/networksCreateQuestions.js new file mode 100644 index 0000000000..0e3dff9418 --- /dev/null +++ b/packages/tools/kadena-cli/src/networks/networksCreateQuestions.js @@ -0,0 +1,95 @@ +import { capitalizeFirstLetter, getExistingNetworks, isAlphabetic, isAlphanumeric, } from '../utils/helpers.js'; +import { input, select } from '@inquirer/prompts'; +import { z } from 'zod'; +// eslint-disable-next-line @rushstack/typedef-var +export const NetworksCreateOptions = z.object({ + network: z.string(), + networkId: z.string().optional(), + networkHost: z.string().optional(), + networkExplorerUrl: z.string().optional(), +}); +export async function askForNetwork() { + const existingNetworks = getExistingNetworks(); + // const prefixedStandardNetworks: ICustomChoice[] = standardNetworks.map( + // (network) => { + // return { + // value: network, + // name: network, + // } as ICustomChoice; + // }, + // ); + const allNetworkChoices = [ + ...existingNetworks, + // ...prefixedStandardNetworks, + ] + .filter((v, i, a) => a.findIndex((v2) => v2.name === v.name) === i) + .map((network) => { + return { + value: network.value, + name: capitalizeFirstLetter(network.value), + }; + }); + const networkChoice = await select({ + message: 'Select an (default) existing network or create a new one:', + choices: [ + ...allNetworkChoices, + { value: 'CREATE_NEW', name: 'Create a New Network' }, + ], + }); + if (networkChoice === 'CREATE_NEW') { + const newNetworkName = await input({ + default: 'testnet', + validate: function (input) { + if (input === '') { + return 'Network name cannot be empty! Please enter something.'; + } + if (!isAlphabetic(input)) { + return 'Network name must be alphabetic! Please enter a valid name.'; + } + return true; + }, + message: 'Enter the name for your new network:', + }); + return newNetworkName.toLowerCase(); + } + return networkChoice.toLowerCase(); +} +export const networksCreateQuestions = [ + { + key: 'network', + prompt: async () => await askForNetwork(), + }, + { + key: 'networkId', + prompt: async (config, previousAnswers, args) => { + const network = previousAnswers.network !== undefined + ? previousAnswers.network + : args.network; + return await input({ + default: `${network}01`, + message: `Enter ${network} network Id (e.g. "${network}01")`, + validate: function (input) { + if (!isAlphanumeric(input)) { + return 'NetworkId must be alphanumeric! Please enter a valid name.'; + } + return true; + }, + }); + }, + }, + { + key: 'networkHost', + prompt: async () => await input({ + message: 'Enter Kadena network host (e.g. "https://api.chainweb.com")', + }), + }, + { + key: 'networkExplorerUrl', + prompt: async () => await input({ + message: 'Enter Kadena network explorer URL (e.g. "https://explorer.chainweb.com/mainnet/tx/")', + }), + }, +]; +export const networkManageQuestions = [ + ...networksCreateQuestions.filter((question) => question.key !== 'network'), +]; diff --git a/packages/tools/kadena-cli/src/networks/networksHelpers.js b/packages/tools/kadena-cli/src/networks/networksHelpers.js new file mode 100644 index 0000000000..6a6f8b7330 --- /dev/null +++ b/packages/tools/kadena-cli/src/networks/networksHelpers.js @@ -0,0 +1,100 @@ +import { defaultNetworksPath, networkDefaults } from '../constants/networks.js'; +import { PathExists, writeFile } from '../utils/filesystem.js'; +import { getExistingNetworks, mergeConfigs, sanitizeFilename, } from '../utils/helpers.js'; +import chalk from 'chalk'; +import { existsSync, readFileSync } from 'fs'; +import yaml from 'js-yaml'; +import path from 'path'; +/** + * Writes the given network setting to the networks folder + * + * @param {TNetworksCreateOptions} options - The set of configuration options. + * @param {string} options.network - The network (e.g., 'mainnet', 'testnet') or custom network. + * @param {number} options.networkId - The ID representing the network. + * @param {string} options.networkHost - The hostname for the network. + * @param {string} options.networkExplorerUrl - The URL for the network explorer. + * @returns {void} - No return value; the function writes directly to a file. + */ +export function writeNetworks(options) { + const { network } = options; + const sanitizedNetwork = sanitizeFilename(network).toLowerCase(); + const networkFilePath = path.join(defaultNetworksPath, `${sanitizedNetwork}.yaml`); + let existingConfig; + if (PathExists(networkFilePath)) { + existingConfig = yaml.load(readFileSync(networkFilePath, 'utf8')); + } + else { + // Explicitly check if network key exists in networkDefaults and is not undefined + existingConfig = + typeof networkDefaults[network] !== 'undefined' + ? { ...networkDefaults[network] } + : { ...networkDefaults.other }; + } + const networkConfig = mergeConfigs(existingConfig, options); + writeFile(networkFilePath, yaml.dump(networkConfig), 'utf8'); +} +/** + * Displays the network configuration in a formatted manner. + * + * @param {TNetworksCreateOptions} networkConfig - The network configuration to display. + */ +export function displayNetworkConfig(networkConfig) { + const log = console.log; + const formatLength = 80; // Maximum width for the display + const displaySeparator = () => { + log(chalk.green('-'.padEnd(formatLength, '-'))); + }; + const formatConfig = (key, value) => { + const valueDisplay = value !== undefined && value.trim() !== '' + ? chalk.green(value) + : chalk.red('Not Set'); + const keyValue = `${key}: ${valueDisplay}`; + const remainingWidth = formatLength - keyValue.length > 0 ? formatLength - keyValue.length : 0; + return ` ${keyValue}${' '.repeat(remainingWidth)} `; + }; + displaySeparator(); + log(formatConfig('Network', networkConfig.network)); + log(formatConfig('Network ID', networkConfig.networkId)); + log(formatConfig('Network Host', networkConfig.networkHost)); + log(formatConfig('Network Explorer URL', networkConfig.networkExplorerUrl)); + displaySeparator(); +} +export function displayNetworksConfig() { + const log = console.log; + const formatLength = 80; // Maximum width for the display + const displaySeparator = () => { + log(chalk.green('-'.padEnd(formatLength, '-'))); + }; + const formatConfig = (key, value, isDefault) => { + const valueDisplay = (value?.trim() ?? '') !== '' ? chalk.green(value) : chalk.red('Not Set'); + const defaultIndicator = isDefault === true ? chalk.yellow(' (Using defaults)') : ''; + const keyValue = `${key}: ${valueDisplay}${defaultIndicator}`; + const remainingWidth = formatLength - keyValue.length > 0 ? formatLength - keyValue.length : 0; + return ` ${keyValue}${' '.repeat(remainingWidth)} `; + }; + const existingNetworks = getExistingNetworks(); + const standardNetworks = ['mainnet', 'testnet']; + existingNetworks.forEach(({ value }) => { + const networkFilePath = path.join(defaultNetworksPath, `${value}.yaml`); + const fileExists = existsSync(networkFilePath); + const networkConfig = fileExists + ? yaml.load(readFileSync(networkFilePath, 'utf8')) + : networkDefaults[value]; + displaySeparator(); + log(formatConfig('Network', value, !fileExists)); + log(formatConfig('Network ID', networkConfig.networkId, !fileExists)); + log(formatConfig('Network Host', networkConfig.networkHost, !fileExists)); + log(formatConfig('Network Explorer URL', networkConfig.networkExplorerUrl, !fileExists)); + }); + standardNetworks.forEach((network) => { + if (!existingNetworks.some(({ value }) => value === network)) { + const networkConfig = networkDefaults[network]; + displaySeparator(); + log(formatConfig('Network', network, true)); // as it is a standard network and does not exist in existingNetworks + log(formatConfig('Network ID', networkConfig.networkId, true)); + log(formatConfig('Network Host', networkConfig.networkHost, true)); + log(formatConfig('Network Explorer URL', networkConfig.networkExplorerUrl, true)); + } + }); + displaySeparator(); +} diff --git a/packages/tools/kadena-cli/src/tx/index.js b/packages/tools/kadena-cli/src/tx/index.js new file mode 100644 index 0000000000..9000c263e6 --- /dev/null +++ b/packages/tools/kadena-cli/src/tx/index.js @@ -0,0 +1,8 @@ +import { sendCommand } from './send.js'; +const SUBCOMMAND_ROOT = 'tx'; +export function txCommandFactory(program, version) { + const txProgram = program + .command(SUBCOMMAND_ROOT) + .description(`Tool for creating and managing transactions`); + sendCommand(txProgram, version); +} diff --git a/packages/tools/kadena-cli/src/tx/send.js b/packages/tools/kadena-cli/src/tx/send.js new file mode 100644 index 0000000000..83158f8864 --- /dev/null +++ b/packages/tools/kadena-cli/src/tx/send.js @@ -0,0 +1,18 @@ +import { processZodErrors } from '../utils/processZodErrors.js'; +export function sendCommand(program, version) { + program + .command('send') + .description('send a transaction to the blockchain') + .action((args) => { + try { + // TODO: use @inquirer/prompts to interactively get missing flags + // TODO: create zod validator + // Options.parse(args); + } + catch (e) { + processZodErrors(program, e, args); + } + // TODO: implement + throw new Error('Not Implemented Yet'); + }); +} diff --git a/packages/tools/kadena-cli/src/tx/utils/template.js b/packages/tools/kadena-cli/src/tx/utils/template.js new file mode 100644 index 0000000000..3f959e27a2 --- /dev/null +++ b/packages/tools/kadena-cli/src/tx/utils/template.js @@ -0,0 +1,109 @@ +import * as yaml from 'js-yaml'; +/** + * Substitutes placeholders in a string template with actual values. + * @param {string} template - The template with placeholders. + * @param {IValues} values - The actual values for the placeholders. + * @returns {string} The template string with placeholders substituted. + * + */ +function substitute(template, values) { + return template.replace(/\{\{(\{?[^}]+)\}\}\}?/g, (val, placeholder) => { + return values.hasOwnProperty(placeholder) + ? String(values[placeholder]) + : val; + }); +} +/** + * Fills a YAML template string using the provided values. + * @param {string} yamlTemplate - The YAML template to fill. + * @param {IValues} values - The values to use. + * @returns {string[]} An array of filled templates. + * @throws {Error} If there's an issue with filling the template. + */ +export function fillYAMLTemplate(yamlTemplate, values) { + const parsedTemplate = yaml.load(yamlTemplate); // Type assertion here + const processedTemplate = processItem(parsedTemplate, values); + return yaml.dump(processedTemplate); +} +/** + * Processes an item, which can be an object, array, or primitive, and replaces placeholders with provided values. + * @param {any} item - The item to process. + * @param {Values} values - The values to use for replacement. + * @returns {any} The processed item. + */ +function processItem(item, values) { + if (typeof item === 'object') { + if (Array.isArray(item)) { + return item.map((subItem) => processItem(subItem, values)); + } + else { + const result = {}; + for (const key in item) { + if (item.hasOwnProperty(key)) { + result[key] = processItem(item[key], values); + } + } + return result; + } + } + else if (typeof item === 'string') { + return substitute(item, values); + } + else { + return item; + } +} +/** + * Extract placeholders enclosed in {{{ }}} from a given template. + * @param {string} template - The template containing placeholders. + * @returns {string[]} An array of extracted placeholders. + */ +export function extractPlaceholders(template) { + const regex = /\{\{(\{?[^}]+)\}\}\}?/g; + let match; + const placeholders = new Set(); + while ((match = regex.exec(template)) !== null) { + placeholders.add(match[1]); + } + return [...placeholders]; +} +/** + * Fills a template with values provided interactively. + * @param {string} yamlTemplate - The YAML template to fill. + * @param {PromptFunction} prompter - The function to prompt for values. + * @returns {Promise} The filled template. + */ +export async function fillYAMLTemplateInteractive(yamlTemplate, prompter) { + const placeholders = extractPlaceholders(yamlTemplate); + /* Note + * + * The prompter that will be passed is collectResponses() from helpers.ts + * which is a function that takes in a list of questions and returns a list of responses + * + * It will also handle the defaults from the project by using processProject() from helpers.ts + * which takes a project file and gets the network/chain etc. from it + * + */ + const values = await prompter(placeholders); + return fillYAMLTemplate(yamlTemplate, values); +} +/** + * Identifies and returns placeholders in a template that have not been replaced. + * Each placeholder is returned with the value ''. + * + * @param {string} template - The template containing placeholders. + * @returns {IValues} An object with the placeholders as keys and '' as their values. + */ +export function findTemplateHoles(template) { + const placeholders = extractPlaceholders(template); + const holes = {}; + for (const placeholder of placeholders) { + holes[placeholder] = ''; // kda-tool uses null, but I think this is better + } + return holes; +} +// convert parsed template to TX +export function convertToTx(parsedTemplate) { + // TO-DO: Convert the parsed template to the appropriate transaction format, figure out how to do this + throw new Error('Function not implemented'); +} diff --git a/packages/tools/kadena-cli/src/typescript/generate/generate.js b/packages/tools/kadena-cli/src/typescript/generate/generate.js new file mode 100644 index 0000000000..4b9ba96dde --- /dev/null +++ b/packages/tools/kadena-cli/src/typescript/generate/generate.js @@ -0,0 +1,139 @@ +import { generateDts, pactParser } from '@kadena/pactjs-generator'; +import { retrieveContractFromChain } from '../utils/retrieveContractFromChain.js'; +import { existsSync, readFileSync, writeFileSync } from 'fs'; +import mkdirp from 'mkdirp'; +import { dirname, join } from 'path'; +import { rimraf } from 'rimraf'; +export const TARGET_PACKAGE = '.kadena/pactjs-generated'; +const shallowFindFile = (path, file) => { + while (!existsSync(join(path, file))) { + path = join(path, '..'); + if (path === '/') { + return; + } + } + return join(path, file); +}; +function verifyTsconfigTypings(tsconfigPath, program) { + if (tsconfigPath === undefined || tsconfigPath.length === 0) { + console.error('Could not find tsconfig.json, skipping types verification'); + } + else { + console.log(`\nVerifying tsconfig.json at \`${tsconfigPath}\``); + const tsconfig = readFileSync(tsconfigPath, 'utf8'); + if (!tsconfig.includes('.kadena/pactjs-generated')) { + console.log(`\n!!! WARNING: You have not added .kadena/pactjs-generated to tsconfig.json. Add it now. +{ "compilerOptions": { "types": [".kadena/pactjs-generated"] } }`); + } + } +} +async function generator(args) { + if (args.contract !== undefined) { + console.log(`Generating pact contracts from chainweb for ${args.contract.join(',')}`); + } + if (args.file !== undefined) { + console.log(`Generating pact contracts from files for ${args.file.join(',')}`); + } + const getContract = async (name) => { + console.log('fetching', name); + if (args.api !== undefined && + args.chain !== undefined && + args.network !== undefined) { + const content = await retrieveContractFromChain(name, args.api, args.chain, args.network); + return content ?? ''; + } + console.log(` + the generator tries to fetch ${name} from the blockchain but the api data is not presented. + this happen because ${name} mentioned via --contracts directly or it is a dependency of a module. + the scrip skips this module + `); + return ''; + }; + const files = args.file === undefined + ? [] + : args.file.map((file) => readFileSync(join(process.cwd(), file), 'utf-8')); + const modules = await pactParser({ + contractNames: args.contract, + files, + getContract, + namespace: args.namespace, + }); + if (process.env.DEBUG === 'dev') { + writeFileSync(join(process.cwd(), 'modules.json'), JSON.stringify(modules, undefined, 2)); + } + const moduleDtss = new Map(); + Object.keys(modules).map((name) => { + if (['', undefined, null].includes(modules[name].namespace)) { + console.log(` + WARNING: No namespace found for module "${name}". You can pass --namespace as a fallback. + `); + } + moduleDtss.set(name, generateDts(name, modules)); + }); + return moduleDtss; +} +export const generate = (program, version) => async (args) => { + // walk up in file tree from process.cwd() to get the package.json + const targetPackageJson = shallowFindFile(process.cwd(), 'package.json'); + if (targetPackageJson === undefined || + targetPackageJson.length === 0 || + targetPackageJson === '/') { + program.error('Could not find package.json'); + return; + } + const moduleDtss = await generator(args); + console.log(`Using package.json at ${targetPackageJson}`); + const targetDirectory = join(dirname(targetPackageJson), 'node_modules', TARGET_PACKAGE); + if (args.clean === true) { + console.log(`Cleaning ${targetDirectory}`); + rimraf.sync(targetDirectory); + } + if (!existsSync(targetDirectory)) { + console.log(`Creating directory ${targetDirectory}`); + mkdirp.sync(targetDirectory); + } + moduleDtss.forEach((dts, moduleName) => { + const targetFilePath = join(dirname(targetPackageJson), 'node_modules', TARGET_PACKAGE, `${moduleName}.d.ts`); + // write dts to index.d.ts to file + const indexPath = join(targetDirectory, 'index.d.ts'); + const exportStatement = `export * from './${moduleName}.js';`; + // always overwrite existing file + console.log(`Writing to new file ${targetFilePath}`); + writeFileSync(targetFilePath, dts); + // if indexPath exists, append export to existing file + if (existsSync(indexPath)) { + console.log(`Appending to existing file ${indexPath}`); + const indexDts = readFileSync(indexPath, 'utf8'); + // Append the export to the file if it's not already there. + if (!indexDts.includes(exportStatement)) { + const separator = indexDts.endsWith('\n') ? '' : '\n'; + const newIndexDts = [indexDts, exportStatement].join(separator); + writeFileSync(indexPath, newIndexDts); + } + } + else { + console.log(`Writing to new file ${indexPath}`); + writeFileSync(indexPath, exportStatement); + } + }); + // write npm init to package.json + const defaultPackageJsonPath = join(targetDirectory, 'package.json'); + // if exists, do nothing + if (existsSync(defaultPackageJsonPath)) { + console.log(`Package.json already exists at ${defaultPackageJsonPath}`); + } + else { + // write default contents to package.json + console.log(`Writing default package.json to ${defaultPackageJsonPath}`); + writeFileSync(defaultPackageJsonPath, JSON.stringify({ + name: TARGET_PACKAGE, + version: version, + description: 'TypeScript definitions for @kadena/client', + types: 'index.d.ts', + keywords: ['pact', 'contract', 'pactjs'], + author: `@kadena/pactjs-cli@${version}`, + }, null, 2)); + } + const tsconfigPath = shallowFindFile(join(process.cwd()), 'tsconfig.json'); + verifyTsconfigTypings(tsconfigPath, program); +}; diff --git a/packages/tools/kadena-cli/src/typescript/generate/index.js b/packages/tools/kadena-cli/src/typescript/generate/index.js new file mode 100644 index 0000000000..b657ce91c6 --- /dev/null +++ b/packages/tools/kadena-cli/src/typescript/generate/index.js @@ -0,0 +1,63 @@ +import { processZodErrors } from '../../utils/processZodErrors.js'; +import { generate } from './generate.js'; +import { Option } from 'commander'; +import { z } from 'zod'; +function asList(value, prev) { + if (prev === undefined) { + return [value]; + } + return [...prev, value]; +} +// eslint-disable-next-line @rushstack/typedef-var +const Options = z + .object({ + file: z.string().array().optional(), + contract: z.string().array().optional(), + clean: z.boolean().optional(), + capsInterface: z.string().optional(), + api: z.string().optional(), + chain: z.number().optional(), + namespace: z.string().optional(), + network: z.enum(['mainnet', 'testnet']), +}) + .refine(({ file, contract }) => { + if (file === undefined && contract === undefined) { + return false; + } + if (file !== undefined && contract !== undefined) { + return false; + } + return true; +}, 'Error: either file or contract must be specified') + .refine(({ contract, api: host }) => { + if (contract !== undefined && host === undefined) { + return false; + } + return true; +}, 'Error: when providing a contract a host must be specified'); +export function generateCommand(program, version) { + program + .command('contract-generate') + .description('Generate client based on a contract') + .option('-c, --clean', 'Clean existing generated files') + .option('-i, --caps-interface', 'Custom name for the interface of the caps. ' + + 'Can be used to create a type definition with a limited set of capabilities.') + .option('-f, --file ', 'Generate d.ts from Pact contract file', asList) + .option('--contract ', 'Generate d.ts from Pact contract from the blockchain', asList) + .option('--namespace ', 'use as the namespace of the contract if its not clear in the contract') + .option('--api ', 'The API to use for retrieving the contract, e.g. "https://api.chainweb.com/chainweb/0.0/mainnet01/chain/8/pact"') + .addOption(new Option('--chain ', 'The chainId to retrieve the contract from, e.g. 8. Defaults to 1.') + .argParser((value) => parseInt(value, 10)) + .default(1)) + .option('--network ', 'The networkId to retrieve the contract from, e.g. "testnet". Defaults to mainnet', 'mainnet') + .action((args) => { + try { + // TODO: use @inquirer/prompts to interactively get missing flags + Options.parse(args); + } + catch (e) { + processZodErrors(program, e, args); + } + generate(program, version)(args); + }); +} diff --git a/packages/tools/kadena-cli/src/typescript/index.js b/packages/tools/kadena-cli/src/typescript/index.js new file mode 100644 index 0000000000..42a907ae61 --- /dev/null +++ b/packages/tools/kadena-cli/src/typescript/index.js @@ -0,0 +1,8 @@ +import { generateCommand } from './generate/index.js'; +const SUBCOMMAND_ROOT = 'typescript'; +export function typescriptCommandFactory(program, version) { + const typescriptProgram = program + .command(SUBCOMMAND_ROOT) + .description(`Tool to generate and manage typescript definitions`); + generateCommand(typescriptProgram, version); +} diff --git a/packages/tools/kadena-cli/src/typescript/utils/callLocal.js b/packages/tools/kadena-cli/src/typescript/utils/callLocal.js new file mode 100644 index 0000000000..01c80e81b3 --- /dev/null +++ b/packages/tools/kadena-cli/src/typescript/utils/callLocal.js @@ -0,0 +1,22 @@ +import fetch from 'cross-fetch'; +export async function callLocal(apiHost, body) { + const response = await fetch(`${apiHost}/api/v1/local`, { + headers: { + accept: 'application/json;charset=utf-8, application/json', + 'cache-control': 'no-cache', + 'content-type': 'application/json;charset=utf-8', + pragma: 'no-cache', + }, + body, + method: 'POST', + }); + let jsonResponse; + let textResponse; + try { + jsonResponse = (await response.clone().json()); + } + catch (e) { + textResponse = await response.text(); + } + return { textResponse, jsonResponse, response }; +} diff --git a/packages/tools/kadena-cli/src/typescript/utils/networkMap.js b/packages/tools/kadena-cli/src/typescript/utils/networkMap.js new file mode 100644 index 0000000000..06ead6c094 --- /dev/null +++ b/packages/tools/kadena-cli/src/typescript/utils/networkMap.js @@ -0,0 +1,4 @@ +export const networkMap = { + mainnet: { network: 'mainnet01', api: 'api.chainweb.com' }, + testnet: { network: 'testnet04', api: 'api.testnet.chainweb.com' }, +}; diff --git a/packages/tools/kadena-cli/src/typescript/utils/retrieveContractFromChain.js b/packages/tools/kadena-cli/src/typescript/utils/retrieveContractFromChain.js new file mode 100644 index 0000000000..9f9ed225ce --- /dev/null +++ b/packages/tools/kadena-cli/src/typescript/utils/retrieveContractFromChain.js @@ -0,0 +1,10 @@ +import { callLocal } from './callLocal.js'; +import { networkMap } from './networkMap.js'; +export async function retrieveContractFromChain(module, apiHost, chain, network) { + const now = new Date(); + const createBody = (hash = '') => `{"cmd":"{\\"signers\\":[],\\"meta\\":{\\"creationTime\\":${now.getTime()},\\"ttl\\":600,\\"chainId\\":\\"${chain}\\",\\"gasPrice\\":1.0e-8,\\"gasLimit\\":2500,\\"sender\\":\\"sender00\\"},\\"nonce\\":\\"CW:${now.toUTCString()}\\",\\"networkId\\":\\"${networkMap[network].network}\\",\\"payload\\":{\\"exec\\":{\\"code\\":\\"(describe-module \\\\\\"${module}\\\\\\")\\",\\"data\\":{}}}}","hash":"${hash}","sigs":[]}`; + const { textResponse } = await callLocal(apiHost, createBody()); + const hashFromResponse = textResponse?.split(' ').splice(-1, 1)[0]; + const { jsonResponse } = await callLocal(apiHost, createBody(hashFromResponse)); + return jsonResponse?.result.data.code; +} diff --git a/packages/tools/kadena-cli/src/utils/bootstrap.js b/packages/tools/kadena-cli/src/utils/bootstrap.js new file mode 100644 index 0000000000..eb4ca0e52b --- /dev/null +++ b/packages/tools/kadena-cli/src/utils/bootstrap.js @@ -0,0 +1,9 @@ +// Bootstrapper for CLI +// Path: src/utils/bootstrap.ts +// write default networks to file +import { networkDefaults } from '../constants/networks.js'; +import { writeNetworks } from '../networks/networksHelpers.js'; +// create default mainnet +writeNetworks(networkDefaults.mainnet); +// create default testnet +writeNetworks(networkDefaults.testnet); diff --git a/packages/tools/kadena-cli/src/utils/chainHelpers.js b/packages/tools/kadena-cli/src/utils/chainHelpers.js new file mode 100644 index 0000000000..eb26a53ee9 --- /dev/null +++ b/packages/tools/kadena-cli/src/utils/chainHelpers.js @@ -0,0 +1,31 @@ +import { Pact } from '@kadena/client'; +import { dirtyRead } from './client.js'; +/** + * Fetches the balance of the given account on a specific chain and network. + * + * @param {string} accountName - The name of the account to fetch the balance for. + * @param {ChainId} chainId - The chain ID to fetch the balance from. + * @param {string} networkId - The network ID to fetch the balance from. + * @returns {Promise} - The result of the command. + */ +export function getBalance(accountName, chainId, networkId) { + const transaction = Pact.builder + .execution(Pact.modules.coin['get-balance'](accountName)) + .setMeta({ chainId }) + .setNetworkId(networkId) + .createTransaction(); + return dirtyRead(transaction); +} +/** + * Checks if a given account exists on a specific chain and network. + * + * @param {string} accountName - The name of the account to check for existence. + * @param {ChainId} chainId - The chain ID to check on. + * @param {string} networkId - The network ID to check on. + * @returns {Promise} - True if the account exists, false otherwise. + */ +export async function accountExists(accountName, chainId, networkId) { + const { result } = await getBalance(accountName, chainId, networkId); + return !(result.status === 'failure' && + result.error.message.includes('row not found')); +} diff --git a/packages/tools/kadena-cli/src/utils/client.js b/packages/tools/kadena-cli/src/utils/client.js new file mode 100644 index 0000000000..fed88d75f5 --- /dev/null +++ b/packages/tools/kadena-cli/src/utils/client.js @@ -0,0 +1,28 @@ +/* + TO-DO: + + needed for fundCommand.ts and probably others + probaby merge with other config + */ +import { createClient } from '@kadena/client'; +// you can edit this function if you want to use different network like dev-net or a private net +export const apiHostGenerator = ({ networkId, chainId, }) => { + switch (networkId) { + case 'mainnet01': + return `https://api.chainweb.com/chainweb/0.0/${networkId}/chain/${chainId ?? '1'}/pact`; + case 'fast-development': + return `http://localhost:8080/chainweb/0.0/${networkId}/chain/${chainId ?? '1'}/pact`; + case 'testnet04': + default: + return `https://api.testnet.chainweb.com/chainweb/0.0/${networkId}/chain/${chainId ?? '1'}/pact`; + } +}; +// configure the client and export the functions +export const { submit, +// preflight, +dirtyRead, pollCreateSpv, pollStatus, getStatus, createSpv, } = createClient(); +export const networkChoices = [ + { value: 'mainnet', name: 'Mainnet' }, + { value: 'testnet', name: 'Testnet' }, + { value: 'devnet', name: 'Devnet' }, +]; diff --git a/packages/tools/kadena-cli/src/utils/filesystem.js b/packages/tools/kadena-cli/src/utils/filesystem.js new file mode 100644 index 0000000000..95258bd0ca --- /dev/null +++ b/packages/tools/kadena-cli/src/utils/filesystem.js @@ -0,0 +1,54 @@ +import { accessSync, existsSync, mkdirSync, writeFileSync } from 'fs'; +import path from 'path'; +/** + * Checks if a given path exists. + * + * @param {PathLike} path - The path to check. + * @returns {Promise} - A promise that resolves to true if the path exists, otherwise false. + */ +export function PathExists(path) { + try { + accessSync(path); + return true; + } + catch { + return false; + } +} +/** + * Checks if a file exists at a given path. + * + * @param filePath - The path to the file. + */ +export function ensureFileExists(filePath) { + return existsSync(filePath); +} +/** + * Writes data to a file, creating any necessary directories along the path if they don't exist. + * + * @param {string} filePath - The path to the file. + * @param {string | NodeJS.ArrayBufferView} data - The data to be written to the file. Can be a string or a buffer view. + * @param {string | BaseEncodingOptions | undefined} options - Encoding options or a string specifying the encoding. Can be undefined. + */ +export function writeFile(filePath, data, options) { + const dirname = path.dirname(filePath); + if (!PathExists(dirname)) { + mkdirSync(dirname, { recursive: true }); + } + writeFileSync(filePath, data, options); +} +/** + * Ensures that a directory exists, and if it doesn't, creates it. + * + * @function + * @param {string} directoryPath - The path of the directory to ensure it exists. + * @throws Will throw an error if unable to create the directory. + * @example + * // Ensures that the 'myDirectory' exists, if not it will be created. + * ensureDirectoryExists('path/to/myDirectory'); + */ +export function ensureDirectoryExists(directoryPath) { + if (!PathExists(directoryPath)) { + mkdirSync(directoryPath, { recursive: true }); + } +} diff --git a/packages/tools/kadena-cli/src/utils/helpers.js b/packages/tools/kadena-cli/src/utils/helpers.js new file mode 100644 index 0000000000..fb88f24b26 --- /dev/null +++ b/packages/tools/kadena-cli/src/utils/helpers.js @@ -0,0 +1,335 @@ +import { getCombinedConfig } from '../config/configHelpers.js'; +import { projectPrefix, projectRootPath } from '../constants/config.js'; +import { defaultNetworksPath } from '../constants/networks.js'; +import chalk from 'chalk'; +import clear from 'clear'; +import { existsSync, mkdirSync, readdirSync } from 'fs'; +import path from 'path'; +/** + * Assigns a value to an object's property if the value is neither undefined nor an empty string. + * This function provides a type-safe way to conditionally update properties on an object. + * + * @template T - The type of the object to which the value might be assigned. + * @template K - The type of the property key on the object. + * @param {T} obj - The target object to which the value might be assigned. + * @param {K} key - The property key on the object where the value might be assigned. + * @param {T[K] | undefined} value - The value to be potentially assigned. If undefined or empty string, no assignment occurs. + */ +export function safeAssign(obj, key, value) { + if (value !== undefined && value !== '') { + obj[key] = value; + } +} +/** + * Merges properties from the source object into the target object, + * overwriting properties on the target only if they are defined in the source. + * + * @template T - The type of the target object and the source object. + * @param {T} target - The target object that will receive properties from the source. + * @param {Partial} source - The source object from which properties will be taken. + * @returns {T} - The merged object. + */ +export function mergeConfigs(target, source) { + for (const key in source) { + if (key in target) { + safeAssign(target, key, source[key]); + } + } + return target; +} +/** + * Generator function that iterates over a list of questions and yields questions that are yet to be answered. + * @template T The type of configuration options the questions correspond to. + * @param args The initial or provided answers for some of the questions. + * @param questions A list of questions to iterate over. + * @yields A question that is yet to be answered. + */ +export function* questionGenerator(args, questions) { + for (const question of questions) { + if (args[question.key] === undefined) { + yield question; + } + } +} +/** + * Collects user responses for a set of questions. + * @template T The type of configuration options the questions correspond to. + * @param args The initial or provided answers for some of the questions. + * @param questions A list of questions for which to collect responses. + * @returns A promise that resolves to an object of collected responses. + */ +export async function collectResponses(args, questions, config) { + const responses = { + ...args, + }; + const generator = questionGenerator(args, questions); + let result = generator.next(); + while (result.done === false) { + const question = result.value; + const currentConfig = config || {}; + responses[question.key] = await question.prompt(currentConfig, responses, args); + result = generator.next(); + } + return responses; +} +/** + * Extracts the public key from a given account string. + * + * @param {string} account - The account string in the format `[kctwu]:[a-zA-Z0-9]{64}`. + * + * @returns {string} - The extracted public key from the account. + * + * @throws {Error} - Throws an error if the account format is invalid. + * + * @example + * const account = 'k:abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz01'; + * const pubKey = getPubKeyFromAccount(account); + * console.log(pubKey); // Outputs: abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz01 + */ +export function getPubKeyFromAccount(account) { + if (!account.toLowerCase().match(/^[kctwu]:[a-zA-Z0-9]{64}$/)) { + throw new Error('Invalid account'); + } + const pubKey = account.toLowerCase().slice(2); + return pubKey; +} +/** + * Creates and attaches a new sub-command with specified arguments and options to the provided Commander program. + * + * @template T - The type of the arguments that the sub-command will receive. + * + * @param {string} name - The name identifier of the sub-command. + * @param {string} description - A brief description of the sub-command, displayed in help messages. + * @param {(args: T) => Promise | void} actionFn - A function defining the actions to be executed when the sub-command is invoked. + * It may be either synchronous or asynchronous and receives the arguments passed to the sub-command. + * @param {Array