diff --git a/.gitignore b/.gitignore index fa986070..ef8a731b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,13 @@ # NodeJS node_modules coverage +build # Editor .vscode # System .DS_Store + +# Testing +config.json diff --git a/src/bin/index.js b/src/bin/index.js index bd487a66..82ee2553 100755 --- a/src/bin/index.js +++ b/src/bin/index.js @@ -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'); @@ -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', @@ -77,6 +97,31 @@ yargs(hideBin(process.argv)) }, async (argv) => await deploy(argv) ) + .command( + ['set '], + '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 [network]', + 'key [network]', + 'k [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', @@ -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') diff --git a/src/lib/config.js b/src/lib/config.js index 254589d1..9c7a4c65 100644 --- a/src/lib/config.js +++ b/src/lib/config.js @@ -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} */ 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')) { @@ -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. @@ -116,30 +107,29 @@ 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), }, ]); @@ -147,16 +137,10 @@ async function config() { 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 () => { @@ -172,15 +156,41 @@ 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} + */ +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) { @@ -188,6 +198,8 @@ function getExplorerName(graphQLUrl) { .split('.') .filter((item) => item === 'minascan' || item === 'minaexplorer')?.[0]; } + module.exports = { config, + configShow, }; diff --git a/src/lib/configSet.js b/src/lib/configSet.js new file mode 100644 index 00000000..3c5bc949 --- /dev/null +++ b/src/lib/configSet.js @@ -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, +}; diff --git a/src/lib/deploy.js b/src/lib/deploy.js index 3988eb15..9d4c93a8 100644 --- a/src/lib/deploy.js +++ b/src/lib/deploy.js @@ -1,19 +1,16 @@ const sh = require('child_process').execSync; const fs = require('fs-extra'); const path = require('path'); -const findPrefix = require('find-npm-prefix'); const { prompt } = require('enquirer'); const { table, getBorderCharacters } = require('table'); const glob = require('fast-glob'); -const { step } = require('./helpers'); +const { step, configRead, projRoot, DEFAULT_GRAPHQL } = require('./helpers'); const fetch = require('node-fetch'); const util = require('util'); const { red, green, bold, reset } = require('chalk'); const log = console.log; -const DEFAULT_GRAPHQL = 'https://proxy.berkeley.minaexplorer.com/graphql'; // The endpoint used to interact with the network - /** * Deploy a smart contract to the specified deploy alias. If no deploy alias param is * provided, yargs will tell the user that the deploy alias param is required. @@ -23,23 +20,8 @@ const DEFAULT_GRAPHQL = 'https://proxy.berkeley.minaexplorer.com/graphql'; // Th */ async function deploy({ alias, yes }) { // 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(); const latestCliVersion = await getLatestCliVersion(); const installedCliVersion = await getInstalledCliVersion(); @@ -102,7 +84,6 @@ async function deploy({ alias, yes }) { log(red(`Please correct your config.json and try again.`)); process.exit(1); - return; } await step('Build project', async () => { @@ -225,7 +206,6 @@ async function deploy({ alias, yes }) { ); process.exit(1); - return; } // Find the users file to import the smart contract from @@ -249,7 +229,6 @@ async function deploy({ alias, yes }) { ); process.exit(1); - return; } // Attempt to import the smart contract class to deploy from the user's file. @@ -262,7 +241,6 @@ async function deploy({ alias, yes }) { ); process.exit(1); - return; } // Attempt to import the private key from the `keys` directory. This private key will be used to deploy the zkApp. @@ -279,7 +257,6 @@ async function deploy({ alias, yes }) { ); process.exit(1); - return; } let zkApp = smartContractImports[contractName]; // The specified zkApp class to deploy @@ -337,7 +314,6 @@ async function deploy({ alias, yes }) { ); process.exit(1); - return; } fee = `${Number(fee) * 1e9}`; // in nanomina (1 billion = 1.0 mina) @@ -354,7 +330,6 @@ async function deploy({ alias, yes }) { ); process.exit(1); - return; } let transaction = await step('Build transaction', async () => { @@ -454,7 +429,6 @@ async function deploy({ alias, yes }) { // Note that the thrown error object is already console logged via step(). log(red(getErrorMessage(txn))); process.exit(1); - return; } const str = diff --git a/src/lib/helpers.js b/src/lib/helpers.js index bf9191e7..53c67b65 100644 --- a/src/lib/helpers.js +++ b/src/lib/helpers.js @@ -1,5 +1,10 @@ const ora = require('ora'); +const fs = require('fs-extra'); +const Client = require('mina-signer'); const { green, red } = require('chalk'); +const findPrefix = require('find-npm-prefix'); + +const DEFAULT_GRAPHQL = 'https://proxy.berkeley.minaexplorer.com/graphql'; // The endpoint used to interact with the network /** * Helper for any steps for a consistent UX. @@ -24,6 +29,84 @@ async function step(str, fn) { } } +/** + * Read and return the config file + */ +async function configRead(path) { + const configPath = path ? path : await projRoot(); + let config; + try { + config = fs.readJSONSync(`${configPath}/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); + } + console.log(red(str)); + return; + } + return config; +} + +/** + * Root of the zkapp project + * @returns {Promise} + */ +async function projRoot() { + const DIR = await findPrefix(process.cwd()); + return DIR; +} + +/** + * Key names + * @param {string} path Path to keys directory. Default to ./keys + * @returns {Promise>} + */ +async function keyNames(path) { + const keysDir = path ? path : `${await projRoot()}/keys`; + const keys = fs + .readdirSync(keysDir) + .map((fname) => fname.substring(0, fname.lastIndexOf('.'))); + return keys; +} + +/** + * Generate a keypair for the given network and write to the specified path/deployAliasName + * @param {string} network Default: 'testnet' + * @param {string} path Default: ./keys + * @param {string} deployAliasName Required + * @returns {Promise} + */ +async function genKeys({ network, path, deployAliasName }) { + const keyDir = path ? path : `${await projRoot()}/keys`; + + // make sure we don't overwrite a key + const keys = await keyNames(); + if (keys.includes(deployAliasName)) { + console.error(red(`keys/${deployAliasName}.json already exists!`)); + console.error( + red(`It must be deleted to create a new keypair under this name.`) + ); + process.exit(1); + } + + const net = network ? network : 'testnet'; + const client = new Client({ network: net }); + let keyPair = client.genKeys(); + fs.outputJsonSync(`${keyDir}/${deployAliasName}.json`, keyPair, { + spaces: 2, + }); + return keyPair; +} + module.exports = { step, + genKeys, + keyNames, + projRoot, + configRead, + DEFAULT_GRAPHQL, }; diff --git a/src/lib/keypair.js b/src/lib/keypair.js new file mode 100644 index 00000000..100da31a --- /dev/null +++ b/src/lib/keypair.js @@ -0,0 +1,15 @@ +const { genKeys } = require('../lib/helpers'); +const { green } = require('chalk'); + +/** + * Generate a new key pair, write to file, and display the public key + */ +async function genKeyPair({ network, deployAliasName }) { + const keyPair = await genKeys({ network, deployAliasName }); + console.log(green('Written to file:', `keys/${deployAliasName}.json`)); + console.log('Public key:', keyPair.publicKey); +} + +module.exports = { + genKeyPair, +};