zkapp-cli
Version:
CLI to create zkApps (zero-knowledge apps) for Mina Protocol
320 lines (304 loc) • 9.87 kB
JavaScript
import chalk from 'chalk';
import { PrivateKey } from 'o1js';
import Constants from './constants.js';
import { capitalize } from './helpers.js';
// Module external API
export { prompts };
// Module internal API (exported for testing purposes)
export {
formatPrefixSymbol,
getFeepayorChoices,
sanitizeAliasName,
sanitizeCustomNetworkId,
};
/* istanbul ignore next */
const prompts = {
deployAliasPrompts: (config) => [
{
type: 'input',
name: 'deployAliasName',
message: (state) => {
const style =
state.submitted && !state.cancelled ? chalk.green : chalk.reset;
return style('Create a name (can be anything):');
},
prefix: formatPrefixSymbol,
validate: async (val) => {
if (!val || val.trim().length === 0)
return chalk.red('Name is required.');
const alias = sanitizeAliasName(val);
if (Object.keys(config.deployAliases).includes(alias)) {
return chalk.red('Name already exists.');
}
return true;
},
result: (val) => sanitizeAliasName(val),
},
{
type: 'select',
name: 'networkId',
initial: 0, // 0 = testnet, 1 = mainnet, change it to 1 after the HF
choices: Constants.networkIds
.map((networkId) => ({
name: capitalize(networkId),
value: networkId,
}))
.concat({
name: 'Custom network',
value: 'selectCustom',
}),
message: (state) => {
const style =
state.submitted && !state.cancelled ? chalk.green : chalk.reset;
return style('Choose the target network:');
},
result() {
return this.focused.value;
},
},
{
type() {
return this.answers.networkId === 'selectCustom' ? 'text' : null;
},
name: 'networkId',
message: (state) => {
const style =
state.submitted && !state.cancelled ? chalk.green : chalk.reset;
return style('Provide a custom network id:');
},
result(val) {
return sanitizeCustomNetworkId(val);
},
},
{
type: 'input',
name: 'url',
message: (state) => {
const style =
state.submitted && !state.cancelled ? chalk.green : chalk.reset;
return style('Set the Mina GraphQL API URL to deploy to:');
},
prefix: formatPrefixSymbol,
validate: (val) => {
if (!val || val.trim().length === 0)
return chalk.red('Url is required.');
try {
new URL(val);
} catch (err) {
return chalk.red('Enter a valid URL.');
}
return true;
},
result: (val) => val.trim().replace(/ /, ''),
},
{
type: 'input',
name: 'fee',
message: (state) => {
const style =
state.submitted && !state.cancelled ? chalk.green : chalk.reset;
return style('Set transaction fee to use when deploying (in MINA):');
},
prefix: formatPrefixSymbol,
validate: (val) => {
if (!val || val.trim().length === 0)
return chalk.red('Fee is required.');
if (isNaN(val)) return chalk.red('Fee must be a number.');
if (val < 0) return chalk.red("Fee can't be negative.");
return true;
},
result: (val) => val.trim().replace(/ /, '').replace(/-/, ''),
},
],
initialFeepayerPrompts: (
defaultFeepayerAlias,
defaultFeepayerAddress,
isFeepayerCached
) => [
{
type: 'select',
name: 'feepayer',
choices: [
{
name: `Recover fee payer account from an existing base58 private key`,
value: 'recover',
},
{
name: `Create a new fee payer key pair
NOTE: The private key is created on this computer and is stored in plain text.`,
value: 'create',
},
],
message: (state) => {
const style =
state.submitted && !state.cancelled ? chalk.green : chalk.reset;
return style('Choose an account to pay transaction fees:');
},
skip() {
return isFeepayerCached; // The prompt is only displayed if a feepayor has not been previously cached
},
result() {
// Workaround for a bug in enquirer that returns the first value of choices when the
// question is skipped https://github.com/enquirer/enquirer/issues/340 .
// This returns the previous prompt value if prompt is skipped.
if (isFeepayerCached) {
return this.state.answers.feepayer;
}
return this.focused.value;
},
},
{
type: 'select',
name: 'feepayer',
choices: [
{
name: `Use stored account ${chalk.bold(
defaultFeepayerAlias
)} (public key: ${chalk.bold(defaultFeepayerAddress)}) `,
value: 'defaultCache',
},
{
name: 'Use a different account (select to see options)',
value: 'other',
},
],
message: (state) => {
const style =
state.submitted && !state.cancelled ? chalk.green : chalk.reset;
return style('Choose an account to pay transaction fees:');
},
skip() {
return !isFeepayerCached; // Only display this prompt question if feeypayer is cached
},
result() {
// Workaround for a bug in enquirer that returns the first value of choices when the
// question is skipped https://github.com/enquirer/enquirer/issues/340 .
// This returns the previous prompt value if prompt is skipped.
if (!isFeepayerCached) {
return this.state.answers.feepayer;
}
return this.focused.value;
},
},
],
recoverFeepayerPrompts: (cachedFeepayerAliases) => [
{
type: 'input',
name: 'feepayerAlias',
message: (state) => {
const style =
state.submitted && !state.cancelled ? chalk.green : chalk.reset;
return style('Create an alias for this account');
},
validate: async (val) => {
if (!val || val.trim().length === 0)
return chalk.red('Fee payer alias is required.');
const alias = sanitizeAliasName(val);
if (cachedFeepayerAliases?.includes(alias))
return chalk.red(`Fee payer alias ${alias} already exists`);
return true;
},
result: (val) => sanitizeAliasName(val),
},
{
type: 'input',
name: 'feepayerKey',
message: (state) => {
const style =
state.submitted && !state.cancelled ? chalk.green : chalk.reset;
return style(`Account private key (base58):
NOTE: The private key is created on this computer and is stored in plain text.
Do NOT use an account which holds a substantial amount of MINA.`);
},
validate: async (val) => {
val = val.trim();
try {
PrivateKey.fromBase58(val);
} catch (err) {
return chalk.red('Enter a valid private key.');
}
return true;
},
result: (val) => val.trim(),
},
],
feepayerAliasPrompt: (cachedFeepayerAliases) => [
{
type: 'input',
name: 'feepayerAlias',
message: (state) => {
const style =
state.submitted && !state.cancelled ? chalk.green : chalk.reset;
return style('Create an alias for this account');
},
validate: async (val) => {
if (!val || val.trim().length === 0)
return chalk.red('Fee payer alias is required.');
const alias = sanitizeAliasName(val);
if (cachedFeepayerAliases?.includes(alias))
return chalk.red(`Fee payer alias ${alias} already exists`);
return true;
},
result: (val) => sanitizeAliasName(val),
},
],
otherFeepayerPrompts: (cachedFeepayerAliases) => [
{
type: 'select',
name: 'feepayer',
choices: getFeepayorChoices(cachedFeepayerAliases),
result() {
return this.focused.value;
},
},
{
type: 'select',
name: 'alternateCachedFeepayerAlias',
choices: cachedFeepayerAliases,
message: (state) => {
const style =
state.submitted && !state.cancelled ? chalk.green : chalk.reset;
return style('Choose another saved fee payer:');
},
skip() {
return this.state.answers.feepayer !== 'alternateCachedFeepayer';
},
},
],
};
function formatPrefixSymbol(state) {
// Shows a cyan question mark when not submitted.
// Shows a chalk.green check mark when submitted.
// Shows a red "x" if ctrl+C is pressed.
// Can't override the validating prefix or styling unfortunately
// https://github.com/enquirer/enquirer/blob/8d626c206733420637660ac7c2098d7de45e8590/lib/prompt.js#L125
// if (state.validating) return ''; // use no symbol, instead of pointer
if (!state.submitted) return state.symbols.question;
return state.cancelled ? chalk.red(state.symbols.cross) : state.symbols.check;
}
function getFeepayorChoices(cachedFeepayerAliases) {
const choices = [
{
name: `Recover fee payer account from an existing base58 private key`,
value: 'recover',
},
{
name: `Create a new fee payer key pair
NOTE: The private key is created on this computer and is stored in plain text.`,
value: 'create',
},
];
// Displays an additional prompt to select a different feepayer if more than one feepayer is cached
if (cachedFeepayerAliases?.length > 1)
choices.push({
name: 'Choose another saved fee payer',
value: 'alternateCachedFeepayer',
});
return choices;
}
function sanitizeAliasName(aliasName) {
return aliasName.toLowerCase().trim().replace(/\s+/g, '-');
}
function sanitizeCustomNetworkId(networkId) {
return networkId.trim().replace(/\s+/g, '-');
}