aelf-command
Version:
A CLI tools for AElf
511 lines (480 loc) • 15.6 kB
JavaScript
import AElf from 'aelf-sdk';
import moment from 'moment';
import chalk from 'chalk';
import path from 'path';
import { v4 as uuid } from 'uuid';
import fs from 'fs';
import { fileURLToPath } from 'url';
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;
/**
* @typedef {import('ora').Ora} Ora
* @typedef {import('inquirer').DistinctQuestion} DistinctQuestion
*/
/**
* Promisifies a function.
* @param {Function} fn - The function to promisify.
* @param {boolean} [firstData] - Whether to pass first data.
* @returns {Function} A promisified function.
*/
function promisify(fn, firstData) {
return (...args) =>
new Promise((resolve, reject) => {
args.push((err, ...result) => {
let res = result;
let error = err;
if (result.length <= 1) {
res = result[0];
}
if (firstData) {
res = error;
error = null;
}
if (error) {
reject(error);
} else {
resolve(res);
}
});
fn.call(null, ...args);
});
}
/**
* Converts a string to camelCase.
* @param {string} str - The input string.
* @returns {string} The camelCase version of the string.
*/
function camelCase(str) {
return _camelCase(str);
}
// todo: repository aelf-sdk, add a method that return all contract's name
// so that we can develop a better method to help us identify the aelf's contract
function isAElfContract(str) {
return str.trim().toLowerCase().startsWith('aelf.');
}
/**
* @description judge if the input is regular expression
* @param {*} o
* @returns boolean flag
*/
function isRegExp(o) {
return o && Object.prototype.toString.call(o) === '[object RegExp]';
}
/**
* Retrieves the list of methods of a contract.
* @param {Object.<string, any>} [contract] - The contract object.
* @returns {string[]} An array of method names.
*/
function getContractMethods(contract = {}) {
if (!contract) {
// @ts-ignore
plainLogger.fatal('There is no such contract');
process.exit(1);
}
return Object.keys(contract)
.filter(v => /^[A-Z]/.test(v))
.sort();
}
/**
* Retrieves an instance of a contract.
* @param {string} contractAddress - The address of the contract.
* @param {any} aelf - The AElf instance.
* @param {any} wallet - The wallet instance.
* @param {Ora} oraInstance - The ora instance for logging.
* @returns {Promise<any>} A promise that resolves to the contract instance.
*/
async function getContractInstance(contractAddress, aelf, wallet, oraInstance) {
if (typeof contractAddress !== 'string') {
return contractAddress;
}
oraInstance.start('Fetching contract');
let contract = null;
try {
if (!isAElfContract(contractAddress)) {
contract = await aelf.chain.contractAt(contractAddress, wallet);
} else {
const { GenesisContractAddress } = await aelf.chain.getChainStatus();
const genesisContract = await aelf.chain.contractAt(GenesisContractAddress, wallet);
const address = await genesisContract.GetContractAddressByName.call(AElf.utils.sha256(contractAddress));
contract = await aelf.chain.contractAt(address, wallet);
}
} catch (e) {
// @ts-ignore
oraInstance.fail(plainLogger.error('Failed to find the contract, please enter the correct contract name!'));
process.exit(1);
}
oraInstance.succeed('Fetching contract successfully!');
return contract;
}
function getMethod(method, contract) {
if (typeof method !== 'string') {
return method;
}
if (contract[method]) {
return contract[method];
}
throw new Error(`Not exist method ${method}`);
}
/**
* Prompts with tolerance for multiple attempts.
* @param {Object} options - Prompt options.
* @param {Function} options.processAfterPrompt - Function to process after prompt.
* @param {string | RegExp} options.pattern - Pattern for the prompt.
* @param {number} options.times - Number of times to prompt.
* @param {DistinctQuestion} options.prompt - prompt.
* @param {Ora} oraInstance - The ora instance for logging.
* @returns {Promise<Object.<string, any>>} The result of the prompt.
*/
async function promptTolerateSeveralTimes({ processAfterPrompt = () => {}, pattern, times = 3, prompt = [] }, oraInstance) {
if (pattern && !isRegExp(pattern)) {
throw new Error("param 'pattern' must be a regular expression!");
}
if (processAfterPrompt && typeof processAfterPrompt !== 'function') {
throw new Error("Param 'processAfterPrompt' must be a function!");
}
let askTimes = 0;
let answerInput;
while (askTimes < times) {
try {
answerInput = await inquirer.prompt(prompt);
answerInput = await processAfterPrompt(answerInput);
// @ts-ignore
if (!pattern || pattern.test(answerInput)) {
break;
}
askTimes++;
} catch (e) {
oraInstance.fail('Failed');
break;
}
}
if (askTimes >= times && answerInput === null) {
// @ts-ignore
oraInstance.fail(plainLogger.fatal(`You has entered wrong message ${times} times!`));
process.exit(1);
}
return answerInput;
}
function isFilePath(val) {
if (!val) {
return false;
}
const filePath = path.resolve(process.cwd(), val);
try {
const stat = fs.statSync(filePath);
return stat.isFile();
} catch (e) {
return false;
}
}
/**
* Retrieves the result of a transaction.
* @param {*} aelf - The AElf instance.
* @param {string} txId - The transaction ID.
* @param {number} [times] - Number of times to retry.
* @param {number} [delay] - Delay between retries.
* @param {number} [timeLimit] - Time limit for retries.
* @returns {Promise<any>} The transaction result.
*/
async function getTxResult(aelf, txId, times = 0, delay = 3000, timeLimit = 3) {
const currentTime = times + 1;
await /** @type {Promise<void>} */ (
new Promise(resolve => {
setTimeout(() => {
resolve();
}, delay);
})
);
const tx = await aelf.chain.getTxResult(txId);
if (tx.Status === 'PENDING' && currentTime <= timeLimit) {
const result = await getTxResult(aelf, txId, currentTime, delay, timeLimit);
return result;
}
if (tx.Status === 'PENDING' && currentTime > timeLimit) {
return tx;
}
if (tx.Status === 'MINED') {
return tx;
}
throw tx;
}
/**
* Parses a JSON string.
* @param {string} [str] - The JSON string to parse.
* @returns {*} The parsed JSON object.
*/
function parseJSON(str = '') {
let result = null;
try {
result = JSON.parse(str);
if (typeof result === 'number' && /^-?\d+(\.\d+)?[eE][+-]?\d+$/.test(String(result))) {
result = str;
}
} catch (e) {
result = str;
}
return result;
}
/**
* Generates a random ID.
* @returns {string} The random ID.
*/
function randomId() {
return uuid().replace(/-/g, '');
}
const PROTO_TYPE_PROMPT_TYPE = {
'.google.protobuf.Timestamp': {
type: 'datetime',
format: ['yyyy', '/', 'mm', '/', 'dd', ' ', 'HH', ':', 'MM'],
initial: moment()
.add({
hours: 1,
minutes: 5
})
.toDate(),
transformFunc(time) {
return {
seconds: moment(time).unix(),
nanos: moment(time).milliseconds() * 1000
};
}
},
default: {
type: 'input',
transformFunc: v => v
}
};
function isSpecialParameters(inputType) {
return (
inputType.fieldsArray &&
inputType.fieldsArray.length === 1 &&
['Hash', 'Address'].includes(inputType.name) &&
inputType.fieldsArray[0].type === 'bytes'
);
}
async function getParamValue(type, fieldName, rule) {
let prompts = PROTO_TYPE_PROMPT_TYPE[type] || PROTO_TYPE_PROMPT_TYPE.default;
const fieldNameWithoutDot = fieldName.replace('.', '');
prompts = {
...prompts,
name: fieldNameWithoutDot,
message: `Enter the required param <${fieldName}>:`
};
const promptValue = (await inquirer.prompt(prompts))[fieldNameWithoutDot];
if (rule === 'repeated') {
prompts.transformFunc = v => JSON.parse(v.replace(/'/g, '"'));
}
let value = parseJSON(await prompts.transformFunc(promptValue));
if (typeof value === 'string' && isFilePath(value)) {
const filePath = path.resolve(process.cwd(), value);
const { read } = await inquirer.prompt({
type: 'confirm',
name: 'read',
// eslint-disable-next-line max-len
message: `It seems that you have entered a file path, do you want to read the file content and take it as the value of <${fieldName}>`
});
if (read) {
try {
fs.accessSync(filePath, fs.constants.R_OK);
} catch (err) {
throw new Error(`permission denied, no read access to file ${filePath}!`);
}
value = fs.readFileSync(filePath).toString('base64');
}
}
return value;
}
/**
* Retrieves parameters of a method.
* @param {*} method - The method.
* @returns {Promise<Object.<string, any>>} A promise that resolves to the parameters object.
*/
async function getParams(method) {
const fields = Object.entries(method.inputTypeInfo.fields || {});
let result = {};
if (fields.length > 0) {
console.log(
chalk.yellow(
'\nIf you need to pass file contents as a parameter, you can enter the relative or absolute path of the file\n'
)
);
console.log('Enter the params one by one, type `Enter` to skip optional param:');
if (isSpecialParameters(method.inputType)) {
/**
* @type {Object.<string, any>}
*/
let prompts = PROTO_TYPE_PROMPT_TYPE.default;
prompts = {
...prompts,
name: 'value',
message: 'Enter the required param <value>:'
};
const promptValue = (await inquirer.prompt(prompts)).value;
result = parseJSON(promptValue);
} else {
for (const [fieldName, fieldType] of fields) {
const { type, rule } = fieldType;
let innerType = null;
try {
innerType = method.inputType.lookupType(type);
} catch (e) {}
let paramValue;
// todo: use recursion
if (
rule !== 'repeated' &&
innerType &&
!isSpecialParameters(innerType) &&
(type || '').indexOf('google.protobuf.Timestamp') === -1
) {
let innerResult = {};
const innerInputTypeInfo = innerType.toJSON();
const innerFields = Object.entries(innerInputTypeInfo.fields || {});
if (isSpecialParameters(innerFields)) {
/**
* @type {Object.<string, any>}
*/
let prompts = PROTO_TYPE_PROMPT_TYPE.default;
prompts = {
...prompts,
name: 'value',
message: `Enter the required param <${fieldName}.value>:`
};
innerResult = (await inquirer.prompt(prompts)).value;
} else {
for (const [innerFieldName, innerFieldType] of innerFields) {
innerResult[innerFieldName] = parseJSON(await getParamValue(innerFieldType.type, `${fieldName}.${innerFieldName}`));
}
}
paramValue = innerResult;
} else {
paramValue = await getParamValue(type, fieldName, rule);
}
result[fieldName] = parseJSON(paramValue);
}
}
}
return result;
}
async function getProto(aelf, address) {
return AElf.pbjs.Root.fromDescriptor(await aelf.chain.getContractFileDescriptorSet(address));
}
function decodeBase64(str) {
const { util } = AElf.pbjs;
const buffer = util.newBuffer(util.base64.length(str));
util.base64.decode(str, buffer, 0);
return buffer;
}
function getDeserializeLogResult(serializedData, dataType) {
let deserializeLogResult = serializedData.reduce((acc, v) => {
let deserialize = dataType.decode(decodeBase64(v));
deserialize = dataType.toObject(deserialize, {
enums: String, // enums as string names
longs: String, // longs as strings (requires long.js)
bytes: String, // bytes as base64 encoded strings
defaults: false, // includes default values
arrays: true, // populates empty arrays (repeated fields) even if defaults=false
objects: true, // populates empty objects (map fields) even if defaults=false
oneofs: true // includes virtual oneof fields set to the present field's name
});
return {
...acc,
...deserialize
};
}, {});
deserializeLogResult = AElf.utils.transform.transform(dataType, deserializeLogResult, AElf.utils.transform.OUTPUT_TRANSFORMERS);
deserializeLogResult = AElf.utils.transform.transformArrayToMap(dataType, deserializeLogResult);
return deserializeLogResult;
}
/**
* Deserializes logs from AElf.
* @param {*} aelf - The AElf instance.
* @param {Array} [logs] - The logs array to deserialize.
* @returns {Promise<any>} A promise that resolves to the deserialized logs.
*/
async function deserializeLogs(aelf, logs = []) {
if (!logs || logs.length === 0) {
return null;
}
let dirname;
try {
// for test as we cannot use import.meta.url in Jest
dirname = __dirname;
} catch {
const __filename = fileURLToPath(import.meta.url);
dirname = path.dirname(__filename);
}
const filePath = path.resolve(dirname, '../package.json');
const Root = await load(path.resolve(dirname, '../protobuf/virtual_transaction.proto'));
let results = await Promise.all(logs.map(v => getProto(aelf, v.Address)));
results = results.map((proto, index) => {
const { Name, NonIndexed, Indexed = [] } = logs[index];
const serializedData = [...Indexed];
if (NonIndexed) {
serializedData.push(NonIndexed);
}
if (Name === 'VirtualTransactionCreated') {
// VirtualTransactionCreated is system-default
try {
// @ts-ignore
const dataType = Root.VirtualTransactionCreated;
return getDeserializeLogResult(serializedData, dataType);
} catch (e) {
// if normal contract has a method called VirtualTransactionCreated
const dataType = proto.lookupType(Name);
return getDeserializeLogResult(serializedData, dataType);
}
} else {
// other method
const dataType = proto.lookupType(Name);
return getDeserializeLogResult(serializedData, dataType);
}
});
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;
};
const parseJSONFile = async address => {
try {
const absolutePath = path.resolve(address);
const data = await fs.readFileSync(absolutePath);
const jsonObject = JSON.parse(data.toString('utf8'));
return jsonObject;
} catch (error) {
throw new Error(`An error occurred while reading or parsing the JSON file: ${error.message}`);
}
};
export {
promisify,
camelCase,
getContractMethods,
getContractInstance,
getMethod,
promptTolerateSeveralTimes,
isAElfContract,
getTxResult,
parseJSON,
randomId,
getParams,
deserializeLogs,
parseCSV,
parseJSONFile
};