@kadena/kadena-cli
Version:
Kadena CLI tool to interact with the Kadena blockchain (manage keys, transactions, etc.)
534 lines • 20.7 kB
JavaScript
import { z } from 'zod';
import { getTransactions, requestKeyValidation, } from '../commands/tx/utils/txHelpers.js';
import { basename } from 'node:path';
import { loadNetworkConfig } from '../commands/networks/utils/networkHelpers.js';
import { getTemplates } from '../commands/tx/commands/templates/templates.js';
import { MULTI_SELECT_INSTRUCTIONS } from '../constants/global.js';
import { services } from '../services/index.js';
import { CommandError } from '../utils/command.util.js';
import { isNotEmptyString, maskStringPreservingStartAndEnd, notEmpty, } from '../utils/globalHelpers.js';
import { getExistingNetworks } from '../utils/helpers.js';
import { log } from '../utils/logger.js';
import { checkbox, input, select } from '../utils/prompts.js';
import { tableFormatPrompt } from '../utils/tableDisplay.js';
import { networkSelectPrompt } from './network.js';
const CommandPayloadStringifiedJSONSchema = z.string();
const PactTransactionHashSchema = z.string();
const ISignatureJsonSchema = z.object({
sig: z.string(),
});
export const SignatureOrUndefinedOrNull = z.union([
ISignatureJsonSchema,
z.undefined(),
z.null(),
]);
const chainWeaverSignatureSchema = z.record(z.string(), z.string().nullable());
const ICommandSignatureSchema = z.array(SignatureOrUndefinedOrNull);
export const ICommandSchema = z.object({
cmd: CommandPayloadStringifiedJSONSchema,
hash: PactTransactionHashSchema,
sigs: ICommandSignatureSchema,
});
export const IUnsignedCommandSchema = z
.object({
cmd: CommandPayloadStringifiedJSONSchema,
hash: PactTransactionHashSchema,
sigs: ICommandSignatureSchema.or(chainWeaverSignatureSchema),
})
// Transform sings record to array
.transform((value) => {
if (Array.isArray(value.sigs)) {
return value;
}
const sigs = chainWeaverSignatureSchema.safeParse(value.sigs);
if (sigs.success) {
const cmd = z
.object({ signers: z.array(z.object({ pubKey: z.string() })) })
.safeParse(JSON.parse(value.cmd));
if (cmd.success) {
const keys = cmd.data.signers.map((signer) => signer.pubKey);
const result = {
...value,
sigs: keys.map((key) => sigs.data[key] !== null ? { sig: sigs.data[key] } : null),
};
return result;
}
}
throw new Error('Invalid signature schema');
});
export const ISignedCommandSchema = z.object({
cmd: CommandPayloadStringifiedJSONSchema,
hash: PactTransactionHashSchema,
sigs: z.array(ISignatureJsonSchema),
});
export async function txUnsignedCommandPrompt() {
const result = await input({
message: `Enter your transaction to sign:`,
validate: (inputString) => {
try {
const parsedInput = JSON.parse(inputString);
IUnsignedCommandSchema.parse(parsedInput);
return true;
}
catch (error) {
log.info('error', error);
return 'Incorrect Format. Please enter a valid Unsigned Command.';
}
},
});
return JSON.parse(result);
}
export const transactionSelectPrompt = async (args) => {
var _a, _b;
const signed = (_a = args.signed) !== null && _a !== void 0 ? _a : false;
const path = (_b = args.path) !== null && _b !== void 0 ? _b : args.directory;
const existingTransactions = await getTransactions(signed, path);
if (existingTransactions.length === 0) {
throw new CommandError({
warnings: [
'No transactions found. Use "kadena tx add" to create a transaction, and "kadena tx sign" to sign it.',
],
});
}
const choices = existingTransactions.map((transaction) => ({
value: transaction,
name: `Transaction: ${transaction}`,
}));
const selectedTransaction = await select({
message: 'Select a transaction file:',
choices: choices,
});
return selectedTransaction;
};
export const transactionsSelectPrompt = async (args) => {
var _a, _b, _c;
const signed = (_a = args.signed) !== null && _a !== void 0 ? _a : true;
const path = (_c = (_b = args.path) !== null && _b !== void 0 ? _b : args.directory) !== null && _c !== void 0 ? _c : process.cwd();
const fileExists = await services.filesystem.fileExists(path);
if (fileExists)
return [path];
const existingTransactions = await getTransactions(signed, path);
if (existingTransactions.length === 0) {
throw new CommandError({
warnings: [`No ${signed ? 'signed ' : ''}transactions found.`],
});
}
const choices = existingTransactions.map((transaction) => ({
value: transaction,
name: `Transaction: ${transaction}`,
}));
const selectedTransaction = await checkbox({
message: 'Select a transaction file:',
choices: choices,
pageSize: 10,
required: true,
instructions: MULTI_SELECT_INSTRUCTIONS,
});
return selectedTransaction;
};
// We don't want to prompt dir, but the flag is still available
// export async function txDirPrompt(): Promise<string> {
// return await input({
// message: `Enter your directory (default: working directory):`,
// validate: async (input) => {
// const dirExists = await services.filesystem.directoryExists(input);
// if (!dirExists) {
// return 'Directory or file not found. Please enter a valid directory or file path.';
// }
// return true;
// },
// default: `./`,
// });
// }
export const selectTemplate = async (args) => {
const stdin = args.stdin;
if (stdin !== undefined && stdin !== '')
return '-';
const templates = await getTemplates();
const choices = [
{
value: 'filepath',
name: 'Enter custom file path',
},
...templates.map((template) => ({
value: template.filename,
name: template.filename,
})),
];
const result = await select({
message: 'Which template do you want to use:',
choices,
});
if (result === 'filepath') {
const filePathResult = await input({
message: 'File path:',
});
return filePathResult;
}
const selectedTemplate = templates.find((template) => template.filename === result);
if (selectedTemplate) {
return selectedTemplate;
}
throw new Error(`Template "${result}" not found`);
};
// aliases in templates need to select aliases for keys and/or accounts
// in account, we need to know what value exactly is expected. like public key, account name, or keyset
// the idea is to expect specific naming for the variables, like "account-from" or "pk-from" or "keyset-from"
const promptVariableValue = async (key, variables) => {
if (key.startsWith('account:')) {
// search for account alias - needs account implementation
const accounts = await services.account.list();
const hasAccount = accounts.length > 0;
let value = null;
const choices = [
{
value: '_manual_',
name: 'Enter account manually',
},
...tableFormatPrompt([
...accounts.map((account) => ({
value: account.name,
name: [
basename(account.alias, '.yaml'),
maskStringPreservingStartAndEnd(account.name, 20),
account.fungible,
account.publicKeys
.map((x) => maskStringPreservingStartAndEnd(x))
.join(','),
account.predicate,
],
})),
]),
];
if (hasAccount) {
value = await select({
message: `Select account alias for template value ${key}:`,
choices,
});
}
if (value === '_manual_' || !hasAccount) {
const inputValue = await input({
message: `Manual entry for account for template value ${key}:`,
validate: (value) => {
if (value === '')
return `${key} cannot be empty`;
return true;
},
});
return inputValue;
}
if (value === null)
throw new Error('account not found');
log.info(`${log.color.green('>')} Using account name ${value}`);
return value;
}
else if (key.startsWith('key:')) {
const wallets = await services.wallet.list();
const walletKeysCount = wallets.reduce((acc, wallet) => acc + wallet.keys.length, 0);
const plainKeys = await services.plainKey.list();
const accounts = await services.account.list();
const hasKeys = walletKeysCount > 0 || plainKeys.length > 0;
const hasAccounts = accounts.length > 0;
let value = null;
let targetSelection = null;
//
// Handle match between account and key variables
//
const pkName = key.replace('key:', '');
const accountName = `account:${pkName}`;
const accountMatch = variables[`account:${pkName}`];
if (accountMatch) {
const accounts = await services.account.list();
const accountConfig = accounts.find((x) => x.name === accountMatch);
if (accountConfig) {
const selection = await select({
message: `Template key "${key}" matches account "${accountName}". Use public account's key?`,
choices: [
...accountConfig.publicKeys.map((key) => ({
value: key,
name: `Account public key: ${key}`,
})),
{
value: '_manual_',
name: 'Enter public key manually',
},
hasAccounts
? {
value: '_account_',
name: 'Pick a public key from another account',
}
: undefined,
hasKeys
? {
value: '_key_',
name: 'Pick a public key from keys',
}
: undefined,
].filter(notEmpty),
});
if (!selection.startsWith('_'))
return selection;
targetSelection = selection;
}
}
// Choices for where to select public key from
if (targetSelection === null) {
const choices = [
{
value: '_manual_',
name: 'Enter public key manually',
},
hasAccounts
? {
value: '_account_',
name: 'Pick a public key from another account',
}
: undefined,
hasKeys
? {
value: '_key_',
name: 'Pick a public key from keys',
}
: undefined,
].filter(notEmpty);
targetSelection =
choices.length === 1
? choices[0].value
: await select({
message: `Template value "${key}" public key:`,
choices: choices,
});
}
// Pick from wallet keys or plain keys
if (targetSelection === '_key_') {
const choices = [];
if (walletKeysCount > 0)
choices.push({ value: '_wallet_', name: 'Wallet keys' });
if (plainKeys.length > 0)
choices.push({ value: '_plain_', name: 'Plain keys' });
const target = choices.length === 1
? choices[0].value
: await select({
message: `Select public key alias for template value ${key}:`,
choices: choices,
});
if (target === '_wallet_') {
const wallet = wallets.length === 1
? wallets[0]
: await select({
message: `Select wallet for template value ${key}:`,
choices: wallets.map((wallet) => ({
value: wallet,
name: wallet.alias,
})),
});
// Purposely did not auto-select if 1 key for transparency
value = await select({
message: `Select public key from wallet ${wallet.alias}:`,
choices: [
...tableFormatPrompt([
...wallet.keys.map((key) => {
var _a;
return ({
value: key.publicKey,
name: [key.index.toString(), (_a = key.alias) !== null && _a !== void 0 ? _a : '', key.publicKey],
});
}),
]),
],
});
}
else if (target === '_plain_') {
// Purposely did not auto-select if 1 key for transparency
value = await select({
message: `Select public key from plain keys:`,
choices: plainKeys.map((key) => ({
value: key.publicKey,
name: key.publicKey,
})),
});
}
}
// Pick public key from accounts
if (targetSelection === '_account_') {
const accountName = await select({
message: `Select account alias for template value ${key}:`,
choices: [
...tableFormatPrompt([
...accounts.map((account) => ({
value: account.name,
name: [
basename(account.alias, '.yaml'),
maskStringPreservingStartAndEnd(account.name, 20),
account.fungible,
account.publicKeys
.map((x) => maskStringPreservingStartAndEnd(x))
.join(','),
account.predicate,
],
})),
]),
],
});
const account = accounts.find((x) => x.name === accountName);
if (account.publicKeys.length === 1) {
value = account.publicKeys[0];
}
else {
value = await select({
message: `Select public key for template value ${key}:`,
choices: [
...account.publicKeys.map((key) => ({
value: key,
name: `Account key: ${key}`,
})),
],
});
}
}
// Fallback: manual entry if nothing else is selected
if (value === null) {
value = await input({
message: `Manual entry for public key for template value ${key}:`,
validate: (value) => {
if (value === '')
return `${key} cannot be empty`;
return true;
},
});
}
if (value === null || value === '_manual_') {
throw new Error('public key not found');
}
log.info(`${log.color.green('>')} Using public key ${value}`);
return value;
}
else if (key.startsWith('keyset-')) {
// TODO: search for key alias - needs account implementation
const alias = await input({
message: `Template value for keyset ${key}:`,
validate: (value) => {
if (value === '')
return `${key} cannot be empty`;
return true;
},
});
log.info('keyset alias', alias);
return alias;
}
else if (key.startsWith('network:')) {
const keyName = key.substring('network:'.length);
const networks = await getExistingNetworks();
const networkName = await select({
message: `Select network id for template value ${keyName}:`,
choices: networks,
});
const network = await loadNetworkConfig(networkName);
return network.networkId;
}
const result = await input({
message: `Template value ${key}:`,
validate: (value) => {
if (value === '')
return `${key} cannot be empty`;
if (key.startsWith('decimal:') && !/^\d+\.\d+$/.test(value)) {
return 'Decimal value must be in the format 1.0';
}
return true;
},
});
return result;
};
export const templateVariables = async (args) => {
const values = args.values;
const variables = args.variables;
const data = args.data;
if (!values || !variables)
return {};
const variableValues = {};
for (const variable of variables) {
// Prioritize variables from data file
if (Object.hasOwn(data, variable)) {
variableValues[variable] = String(data[variable]);
continue;
}
// Find variables in cli arguments
const match = values.find((value) => value.startsWith(`--${variable}=`));
if (match !== undefined)
variableValues[variable] = match.split('=')[1];
else {
// Prompt for variable value
variableValues[variable] = await promptVariableValue(variable, variableValues);
}
}
return variableValues;
};
export const outFilePrompt = async (args) => {
const result = await input({
message: 'Where do you want to save the output:',
});
return result !== null && result !== void 0 ? result : null;
};
export const templateDataPrompt = async () => {
const result = await input({
message: 'File path of data to use for template .json or .yaml (optional):',
});
return result !== null && result !== void 0 ? result : null;
};
export async function selectSignMethodPrompt() {
return await select({
message: 'Select an action:',
choices: [
{
value: 'wallet',
name: 'Sign with wallet',
},
{
value: 'keyPair',
name: 'Sign with key pair',
},
],
});
}
function determineNetwork(networkId) {
const id = networkId !== null && networkId !== void 0 ? networkId : '';
if (id.includes('testnet')) {
return 'testnet';
}
else if (id.includes('mainnet')) {
return 'mainnet';
}
else if (id.includes('development')) {
return 'devnet';
}
return '';
}
export const txTransactionNetworks = async (args) => {
const commands = args.commands;
const networkPerTransaction = [];
for (const [index, command] of commands.entries()) {
const cmdPayload = JSON.parse(command.cmd);
let network = determineNetwork(cmdPayload.networkId);
if (network === '') {
network = await networkSelectPrompt({}, { networkText: `Select network for transaction ${index + 1}:` }, false);
}
networkPerTransaction.push(network);
}
return networkPerTransaction;
};
export const txRequestKeyPrompt = async () => {
return await input({
message: 'Enter transaction request key:',
validate: (value) => {
if (!isNotEmptyString(value.trim())) {
return 'Request key cannot be empty';
}
const parse = requestKeyValidation.safeParse(value);
if (parse.success)
return true;
const formatted = parse.error.format();
return formatted._errors[0];
},
});
};
//# sourceMappingURL=tx.js.map