diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index af3ea8a..f1c2b57 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -4,9 +4,6 @@ on: push: branches: - master - pull_request: - branches: - - master jobs: test: diff --git a/README.md b/README.md index 8f9ee57..75f41b2 100644 --- a/README.md +++ b/README.md @@ -11,20 +11,17 @@

- | Branch | Tests | Coverage | -|--------------|-----------------|----------------| +| Branch | Tests | Coverage | +| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | `master` | ![Tests](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/AElfProject/aelf-command/feature/badge-json/master-test-results.json) | ![Coverage](https://AElfProject.github.io/aelf-command/badges.svg) | - - - ## Descriptions _A CLI tools built for AElf_ ## Features -- Get or Set common configs, `endpoint`, `account`, `datadir`, `password`. +- Get or Set common configs, `endpoint`, `account`, `datadir`, `password`, `csv`. - For new users who are not familiar with the CLI parameters, any missing parameters will be asked in a prompting way. - Create a new `account`. - Load an account from a given `private key` or `mnemonic`. @@ -157,6 +154,7 @@ Options: -a, --account The address of AElf wallet -p, --password The password of encrypted keyStore -d, --datadir The directory that contains the AElf related files. Defaults to {home}/.local/share/aelf + -c, --csv The location of the CSV file containing the parameters. -h, --help output usage information Commands: @@ -218,6 +216,7 @@ aelf-command console - `endpoint`: The endpoint for the RPC service. - `account`: The account to be used to interact with the blockchain `endpoint`. - `password`: The password for unlocking the given `account`. +- `csv>`: The location of the CSV file containing the parameters. You can specified options above in several ways, and the priority is in the order of low to high. diff --git a/jest.config.js b/jest.config.js index 58bb7d5..a2105fd 100644 --- a/jest.config.js +++ b/jest.config.js @@ -33,7 +33,7 @@ export default { coveragePathIgnorePatterns: ['/node_modules/', '/src/utils/constants.js', '/src/command/index.js'], // A list of reporter names that Jest uses when writing coverage reports - coverageReporters: ['text', 'json-summary'], + coverageReporters: ['text', 'json-summary', 'html'], // An object that configures minimum threshold enforcement for coverage results // coverageThreshold: null, @@ -146,7 +146,7 @@ export default { // The glob patterns Jest uses to detect test files testMatch: [ - // '**/test/utils/Logger.test.js' + // '**/test/utils/utils.test.js' '**/test/command/dappServer/socket-sign.test.js', '**/test/**/?(*.)+(spec|test).[jt]s?(x)' // "**/?(*.)+(spec|test).[tj]s?(x)" diff --git a/package.json b/package.json index 013cac6..048dc16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aelf-command", - "version": "0.1.48", + "version": "0.1.49-beta.0", "description": "A CLI tools for AElf", "main": "src/index.js", "type": "module", @@ -56,6 +56,7 @@ "check-node-version": "^4.2.1", "columnify": "^1.6.0", "commander": "^12.1.0", + "csv-parser": "^3.0.0", "elliptic": "^6.5.5", "inquirer": "^9.2.22", "inquirer-date-prompt": "^3.0.0", diff --git a/src/command/baseSubCommand.js b/src/command/baseSubCommand.js index 20752af..9e0ef64 100644 --- a/src/command/baseSubCommand.js +++ b/src/command/baseSubCommand.js @@ -6,7 +6,7 @@ import inquirer from 'inquirer'; import ora from 'ora'; import { logger } from '../utils/myLogger.js'; import { camelCase } from '../utils/utils.js'; -import { globalOptionsPrompts, strictGlobalOptionValidatorDesc } from '../utils/constants.js'; +import { commonGlobalOptionValidatorDesc, globalOptionsPrompts, strictGlobalOptionValidatorDesc } from '../utils/constants.js'; // Schema.warning = () => {}; // TypeError: Cannot add property warning, object is not extensible @@ -123,7 +123,7 @@ class BaseSubCommand { */ static getUniConfig(commander) { const result = {}; - ['password', 'endpoint', 'account', 'datadir'].forEach(v => { + Object.keys(commonGlobalOptionValidatorDesc).forEach(v => { const options = commander.opts(); if (options[v]) { result[v] = options[v]; diff --git a/src/command/call.js b/src/command/call.js index 3779bdb..3c85282 100644 --- a/src/command/call.js +++ b/src/command/call.js @@ -1,6 +1,8 @@ import AElf from 'aelf-sdk'; import inquirer from 'inquirer'; import chalk from 'chalk'; +import { createReadStream } from 'fs'; +import csv from 'csv-parser'; import BaseSubCommand from './baseSubCommand.js'; import { callCommandUsages, callCommandParameters } from '../utils/constants.js'; import { @@ -9,7 +11,8 @@ import { getMethod, promptTolerateSeveralTimes, getParams, - parseJSON + parseJSON, + parseCSV } from '../utils/utils.js'; import { getWallet } from '../utils/wallet.js'; import { logger } from '../utils/myLogger.js'; @@ -64,6 +67,19 @@ class CallCommand extends BaseSubCommand { return contractAddress; } + /** + * Calls a method with specified parameters. + * @param {any} method The method to call. + * @param {any} params The parameters for the method call. + * @returns {Promise} A promise that resolves with the result of the method call. + */ + async showRes(method, params) { + const result = await this.callMethod(method, params); + // @ts-ignore + logger.info(`\nResult:\n${JSON.stringify(result, null, 2)}`); + this.oraInstance.succeed('Succeed!'); + } + /** * Runs the command. * @param {Command} commander The Commander instance. @@ -75,7 +91,7 @@ class CallCommand extends BaseSubCommand { // @ts-ignore const { options, subOptions } = await super.run(commander, ...args); const subOptionsLength = Object.keys(subOptions).length; - const { endpoint, datadir, account, password } = options; + const { endpoint, datadir, account, password, csv } = options; const aelf = new AElf(new AElf.providers.HttpProvider(endpoint)); try { let { contractAddress, method, params } = subOptions; @@ -109,13 +125,16 @@ class CallCommand extends BaseSubCommand { break; case 'params': contractAddress = await getContractInstance(contractAddress, aelf, wallet, this.oraInstance); - method = getMethod(method, contractAddress); - - params = await getParams(method); - params = typeof params === 'string' ? params : BaseSubCommand.normalizeConfig(params); - if (Object.keys(params || {}).length > 0) { - console.log(chalk.hex('#3753d3')(`The params you entered is:\n${JSON.stringify(params, null, 2)}`)); + if (csv) { + const csvParams = await parseCSV(csv); + params = csvParams; + } else { + params = await getParams(method); + params = typeof params === 'string' ? params : BaseSubCommand.normalizeConfig(params); + if (Object.keys(params || {}).length > 0) { + console.log(chalk.hex('#3753d3')(`The params you entered is:\n${JSON.stringify(params, null, 2)}`)); + } } break; default: @@ -124,15 +143,23 @@ class CallCommand extends BaseSubCommand { } } contractAddress = await getContractInstance(contractAddress, aelf, wallet, this.oraInstance); - params = parseJSON(params); + if (Array.isArray(params)) { + params.forEach(param => parseJSON(param)); + } else { + params = parseJSON(params); + } + method = getMethod(method, contractAddress); if (method.inputTypeInfo && (Object.keys(method.inputTypeInfo.fields).length === 0 || !method.inputTypeInfo.fields)) { params = ''; } - const result = await this.callMethod(method, params); - // @ts-ignore - logger.info(`\nResult:\n${JSON.stringify(result, null, 2)}`); - this.oraInstance.succeed('Succeed!'); + if (Array.isArray(params)) { + for (const param of params) { + await this.showRes(method, param); + } + } else { + await this.showRes(method, params); + } } catch (e) { this.oraInstance.fail('Failed!'); // @ts-ignore diff --git a/src/index.js b/src/index.js index ccc1528..29b198c 100644 --- a/src/index.js +++ b/src/index.js @@ -51,11 +51,11 @@ function init(options) { commander.option('-e, --endpoint ', 'The URI of an AElf node. Eg: http://127.0.0.1:8000'); commander.option('-a, --account ', 'The address of AElf wallet'); commander.option('-p, --password ', 'The password of encrypted keyStore'); - commander.option( '-d, --datadir ', `The directory that contains the AElf related files. Default to be ${userHomeDir}/aelf` ); + commander.option('-c, --csv ', 'The location of the CSV file containing the parameters.'); const rc = new RC(); Object.values(commands).forEach(Value => { const command = new Value(rc); diff --git a/src/utils/constants.js b/src/utils/constants.js index 1448bb4..6a7859c 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -366,6 +366,11 @@ const commonGlobalOptionValidatorDesc = { type: 'string', required: false, message: 'set a valid account address in global config file or passed by -a
' + }, + csv: { + type: 'string', + required: false, + message: 'set params in csv file by -c ' } }; @@ -374,8 +379,8 @@ const strictGlobalOptionValidatorDesc = /**@type {CommonGlobalOptionValidatorDes // @ts-ignore Object.entries(commonGlobalOptionValidatorDesc).forEach((/** @type {[CommonGlobalOptionKey, any]} */ [key, value]) => { strictGlobalOptionValidatorDesc[key] = { - ...value, - required: true + ...value + // required: true }; }); diff --git a/src/utils/utils.js b/src/utils/utils.js index f9c510c..7649f8d 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -9,6 +9,8 @@ import _camelCase from 'camelcase'; import inquirer from 'inquirer'; import { plainLogger } from './myLogger.js'; import protobuf from '@aelfqueen/protobufjs'; +import { createReadStream } from 'fs'; +import csv from 'csv-parser'; const { load } = protobuf; /** @@ -462,6 +464,23 @@ async function deserializeLogs(aelf, logs = []) { return results; } +const parseCSV = async address => { + let results = []; + const stream = createReadStream(address).pipe(csv()); + for await (const data of stream) { + const cleanData = {}; + for (const key in data) { + const cleanKey = key.replace(/\n/g, '').trim(); + const cleanValue = typeof data[key] === 'string' ? data[key].replace(/\n/g, '').trim() : data[key]; + if (cleanValue !== '') { + cleanData[cleanKey] = cleanValue; + } + } + Object.keys(cleanData).length && results.push(cleanData); + } + return results; +}; + export { promisify, camelCase, @@ -474,5 +493,6 @@ export { parseJSON, randomId, getParams, - deserializeLogs + deserializeLogs, + parseCSV }; diff --git a/test/command/call.test.js b/test/command/call.test.js index e4e52af..dcfc744 100644 --- a/test/command/call.test.js +++ b/test/command/call.test.js @@ -7,7 +7,7 @@ import { callCommandUsages, callCommandParameters } from '../../src/utils/consta import { getContractInstance } from '../../src/utils/utils.js'; import { userHomeDir } from '../../src/utils/userHomeDir.js'; import { logger } from '../../src/utils/myLogger.js'; -import { endpoint as endPoint, account, password, dataDir } from '../constants.js'; +import { endpoint as endPoint, account, password, dataDir, csvDir } from '../constants.js'; const sampleRc = { getConfigs: jest.fn() }; jest.mock('../../src/utils/myLogger'); @@ -102,6 +102,26 @@ describe('CallCommand', () => { expect(logger.info).toHaveBeenCalled(); }); + test('should run with csv', async () => { + inquirer.prompt = questions => + Promise.resolve({ + symbol: 'ELF', + owner: 'GyQX6t18kpwaD9XHXe1ToKxfov8mSeTLE9q9NwUAeTE8tULZk' + }); + const commander = new Command(); + commander.option('-e, --endpoint ', 'The URI of an AElf node. Eg: http://127.0.0.1:8000'); + commander.option('-a, --account ', 'The address of AElf wallet'); + commander.option('-p, --password ', 'The password of encrypted keyStore'); + commander.option( + '-d, --datadir ', + `The directory that contains the AElf related files. Default to be ${userHomeDir}/aelf` + ); + commander.option('-c, --csv ', 'The location of the CSV file containing the parameters.'); + commander.parse([process.argv[0], '', 'call', '-e', endPoint, '-a', account, '-p', password, '-d', dataDir, '-c', csvDir]); + await callCommand.run(commander, 'AElf.ContractNames.Token', 'GetBalance'); + expect(logger.info).toHaveBeenCalled(); + }); + test('should run with invalid parameters', async () => { inquirer.prompt = backup; callCommand = new CallCommand(sampleRc, 'call', 'Call a read-only method on a contract.', [ diff --git a/test/command/proposal.test.js b/test/command/proposal.test.js index e1bf002..1703561 100644 --- a/test/command/proposal.test.js +++ b/test/command/proposal.test.js @@ -1,9 +1,7 @@ /* eslint-disable max-len */ import { Command } from 'commander'; -import path from 'path'; import inquirer from 'inquirer'; import AElf from 'aelf-sdk'; -import chalk from 'chalk'; import moment from 'moment'; import ProposalCommand from '../../src/command/proposal.js'; import { userHomeDir } from '../../src/utils/userHomeDir.js'; diff --git a/test/constants.js b/test/constants.js index 943dc4e..14f8e0e 100644 --- a/test/constants.js +++ b/test/constants.js @@ -4,3 +4,4 @@ export const endpoint = 'https://tdvw-test-node.aelf.io/'; export const account = 'GyQX6t18kpwaD9XHXe1ToKxfov8mSeTLE9q9NwUAeTE8tULZk'; export const password = '1234*Qwer'; export const dataDir = path.resolve(__dirname, './dataDir/aelf'); +export const csvDir = path.resolve(__dirname, './test.csv'); diff --git a/test/test.csv b/test/test.csv new file mode 100644 index 0000000..d326ec4 --- /dev/null +++ b/test/test.csv @@ -0,0 +1,3 @@ +symbol,"owner +" +ELF,GyQX6t18kpwaD9XHXe1ToKxfov8mSeTLE9q9NwUAeTE8tULZk diff --git a/test/utils/utils.test.js b/test/utils/utils.test.js index d6301e4..74a19ba 100644 --- a/test/utils/utils.test.js +++ b/test/utils/utils.test.js @@ -15,10 +15,11 @@ import { parseJSON, randomId, getParams, - deserializeLogs + deserializeLogs, + parseCSV } from '../../src/utils/utils'; import { plainLogger } from '../../src/utils/myLogger'; -import { endpoint, account, password, dataDir } from '../constants.js'; +import { endpoint, account, password, dataDir, csvDir } from '../constants.js'; jest.mock('inquirer'); @@ -391,4 +392,11 @@ describe('utils', () => { expect(result).toEqual(null); }); }); + + describe('parseCSV', () => { + test('test parse csv file', async () => { + const results = await parseCSV(csvDir); + expect(results).toEqual([{ owner: 'GyQX6t18kpwaD9XHXe1ToKxfov8mSeTLE9q9NwUAeTE8tULZk', symbol: 'ELF' }]); + }); + }); }); diff --git a/types/utils/constants.d.ts b/types/utils/constants.d.ts index 27b245d..f75d942 100644 --- a/types/utils/constants.d.ts +++ b/types/utils/constants.d.ts @@ -36,11 +36,17 @@ export interface AccountValidatorDesc { required: boolean; message: string; } +export interface CSVValidatorDesc { + type: string; + required: boolean; + message: string; +} export interface CommonGlobalOptionValidatorDesc { password: PasswordValidatorDesc; endpoint: EndpointValidatorDesc; datadir: DatadirValidatorDesc; account: AccountValidatorDesc; + csv: CSVValidatorDesc; } export const commonGlobalOptionValidatorDesc: CommonGlobalOptionValidatorDesc; diff --git a/yarn.lock b/yarn.lock index 142e902..9e95dd7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3085,6 +3085,13 @@ crypto-random-string@^4.0.0: dependencies: type-fest "^1.0.1" +csv-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/csv-parser/-/csv-parser-3.0.0.tgz#b88a6256d79e090a97a1b56451f9327b01d710e7" + integrity sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ== + dependencies: + minimist "^1.2.0" + cz-conventional-changelog@3.3.0, cz-conventional-changelog@^3.3.0: version "3.3.0" resolved "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.3.0.tgz#9246947c90404149b3fe2cf7ee91acad3b7d22d2"