Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Additional CLI functionality #413

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# NodeJS
node_modules
coverage
build

# Editor
.vscode

# System
.DS_Store

# Testing
config.json
51 changes: 48 additions & 3 deletions src/bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
const { project } = require('../lib/project');
const { file } = require('../lib/file');
const { config } = require('../lib/config');
const { config, configShow } = require('../lib/config');
const { configSet } = require('../lib/configSet');
const { deploy } = require('../lib/deploy');
const { genKeyPair } = require('../lib/keypair');
const { example } = require('../lib/example');
const { system } = require('../lib/system');
const chalk = require('chalk');
Expand Down Expand Up @@ -60,7 +62,25 @@ yargs(hideBin(process.argv))
{ name: { demand: true, string: true, hidden: true } },
(argv) => file(argv.name)
)
.command(['config'], 'Add a new deploy alias', {}, config)
.command(
['config [show [alias]]'],
'Add a new deploy alias or display properties',
{
show: {
description: 'Display the config file',
demand: false,
boolean: true,
hidden: false,
},
alias: {
description: 'Display properties of the deploy alias',
demand: false,
string: true,
hidden: false,
},
},
(argv) => (argv.show ? configShow(argv.alias) : config())
)
.command(
['deploy [alias]'],
'Deploy or redeploy a zkApp',
Expand All @@ -77,6 +97,31 @@ yargs(hideBin(process.argv))
},
async (argv) => await deploy(argv)
)
.command(
['set <alias> <prop> <value>'],
'Set a new property value for the alias',
{
alias: { demand: true, string: true, hidden: false },
prop: { demand: true, string: true, hidden: false },
value: { demand: true, string: true, hidden: false },
},
async (argv) =>
await configSet({ alias: argv.alias, prop: argv.prop, value: argv.value })
)
.command(
[
'keypair <alias> [network]',
'key <alias> [network]',
'k <alias> [network]',
],
'Generate a new keypair for the given network and display the public key',
{
alias: { demand: true, string: true, hidden: false },
network: { demand: false, string: true, hidden: false },
},
async (argv) =>
await genKeyPair({ deployAliasName: argv.alias, network: argv.network })
)
.command(
['example [name]', 'e [name]'],
'Create an example project',
Expand All @@ -90,7 +135,7 @@ yargs(hideBin(process.argv))
},
async (argv) => await example(argv.name)
)
.command(['system', 'sys', 's'], 'Show system info', {}, () => system())
.command(['system', 'sys'], 'Show system info', {}, system)
.alias('h', 'help')
.alias('v', 'version')

Expand Down
94 changes: 53 additions & 41 deletions src/lib/config.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,27 @@
const fs = require('fs-extra');
const findPrefix = require('find-npm-prefix');
const { prompt } = require('enquirer');
const { table, getBorderCharacters } = require('table');
const { step } = require('./helpers');
const {
step,
configRead,
projRoot,
genKeys,
DEFAULT_GRAPHQL,
} = require('./helpers');
const { green, red, bold, gray, reset } = require('chalk');
const Client = require('mina-signer');

const log = console.log;

const DEFAULT_FEE = '0.1';

/**
* Show existing deploy aliases in `config.json` and allow a user to add a new
* deploy alias and url--and generate a key pair for it.
* @returns {Promise<void>}
*/
async function config() {
// Get project root, so the CLI command can be run anywhere inside their proj.
const DIR = await findPrefix(process.cwd());

let config;
try {
config = fs.readJSONSync(`${DIR}/config.json`);
} catch (err) {
let str;
if (err.code === 'ENOENT') {
str = `config.json not found. Make sure you're in a zkApp project.`;
} else {
str = 'Unable to read config.json.';
console.error(err);
}
log(red(str));
return;
}
const DIR = await projRoot();
const config = await configRead();

// Checks if developer has the legacy networks in config.json and renames it to deploy aliases.
if (Object.prototype.hasOwnProperty.call(config, 'networks')) {
Expand Down Expand Up @@ -74,7 +65,7 @@ async function config() {
const msg = '\n ' + table(tableData, tableConfig).replaceAll('\n', '\n ');
log(msg);

console.log('Add a new deploy alias:');
log('Add a new deploy alias:');

// TODO: Later, show pre-configured list to choose from or let user
// add a custom deploy alias.
Expand Down Expand Up @@ -116,47 +107,40 @@ async function config() {
name: 'url',
message: (state) => {
const style = state.submitted && !state.cancelled ? green : reset;
return style('Set the Mina GraphQL API URL to deploy to:');
return style(`Set the Mina GraphQL API URL to deploy to
Press enter for default ${DEFAULT_GRAPHQL}:`);
},
prefix: formatPrefixSymbol,
validate: (val) => {
if (!val) return red('Url is required.');
return true;
},
result: (val) => val.trim().replace(/ /, ''),
result: (val) => (val ? val.trim().replace(/ /, '') : DEFAULT_GRAPHQL),
},
{
type: 'input',
name: 'fee',
message: (state) => {
const style = state.submitted && !state.cancelled ? green : reset;
return style('Set transaction fee to use when deploying (in MINA):');
return style(
`Set transaction fee to use when deploying (in MINA)\n Press enter for defualt ${DEFAULT_FEE}`
);
},
prefix: formatPrefixSymbol,
validate: (val) => {
if (!val) return red('Fee is required.');
if (!val) return true;
if (isNaN(val)) return red('Fee must be a number.');
if (val < 0) return red("Fee can't be negative.");
return true;
},
result: (val) => val.trim().replace(/ /, ''),
result: (val) => (val ? val.trim().replace(/ /, '') : DEFAULT_FEE),
},
]);

// If user presses "ctrl + c" during interactive prompt, exit.
const { deployAliasName, url, fee } = response;
if (!deployAliasName || !url || !fee) return;

// TODO allow user to choose an existing key or generate new
const keyPair = await step(
`Create key pair at keys/${deployAliasName}.json`,
async () => {
const client = new Client({ network: 'testnet' }); // TODO: Make this configurable for mainnet and testnet.
let keyPair = client.genKeys();
fs.outputJsonSync(`${DIR}/keys/${deployAliasName}.json`, keyPair, {
spaces: 2,
});
return keyPair;
}
async () => await genKeys({ deployAliasName }) // TODO: Make this configurable for mainnet and testnet.
);

await step(`Add deploy alias to config.json`, async () => {
Expand All @@ -172,22 +156,50 @@ async function config() {
config.deployAliases[deployAliasName]?.url
);

const str =
`\nSuccess!\n` +
const success = `\nSuccess!\n` + `\nNew deploy alias: ${deployAliasName}`;
log(green(success));
log(config.deployAliases[deployAliasName]);

const nextSteps =
`\nNext steps:` +
`\n - If this is a testnet, request tMINA at:\n https://faucet.minaprotocol.com/?address=${encodeURIComponent(
keyPair.publicKey
)}&?explorer=${explorerName}` +
`\n - To deploy, run: \`zk deploy ${deployAliasName}\``;

log(green(str));
log(green(nextSteps));
}

/**
* Display the contents of `config.json`
* @param {string} alias Name of the deploy alias
* @returns {Promise<void>}
*/
async function configShow(alias) {
const config = await configRead();
if (!alias) {
log(config);
return;
}
// deploy alias must exist to display
const aliases = Object.keys(config.deployAliases);
if (!aliases.includes(alias)) {
console.error(red(`Invalid deploy alias: ${alias}`));
log('Available deploy aliases:', aliases);
return;
}
log('Deploy alias:', alias);
log(config.deployAliases[alias]);
return;
}

function getExplorerName(graphQLUrl) {
return new URL(graphQLUrl).hostname
.split('.')
.filter((item) => item === 'minascan' || item === 'minaexplorer')?.[0];
}

module.exports = {
config,
configShow,
};
38 changes: 38 additions & 0 deletions src/lib/configSet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const fs = require('fs-extra');
const { green, red } = require('chalk');
const { configRead, projRoot } = require('./helpers');

// config set existing alias property
async function configSet({ alias, prop, value }) {
const config = await configRead();

// deploy alias must exist to change a value
const aliases = Object.keys(config.deployAliases);
if (!aliases.includes(alias)) {
console.error(red(`Invalid deploy alias: ${alias}`));
console.log('Available deploy aliases:', aliases);
return;
}

// all aliases have the same properties, we just need one
// property must be valid
const props = Object.keys(config.deployAliases[aliases[0]]);
if (!props.includes(prop)) {
console.error(red(`Invalid property: ${prop}`));
console.log('Available properties:', props);
return;
}

// TODO validation of the value given the property

// set value and overwrite config file
config.deployAliases[alias][prop] = value;
fs.writeJSONSync(`${await projRoot()}/config.json`, config, { spaces: 2 });
console.log(
green(`Alias '${alias}' property '${prop}' successfully set to '${value}'`)
);
}

module.exports = {
configSet,
};
Loading