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

Add Third Party Feepayer #424

Merged
merged 90 commits into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
69413f8
feat: prompt user to pick an acount to pay transaction fees
ymekuria May 5, 2023
5c1ae19
refactor: reword prompt text
ymekuria May 5, 2023
ca12e94
add value property to fee payor response
ymekuria May 6, 2023
9f434eb
feat: prompt user to add base58 key if recover existing fee-payer acc…
ymekuria May 6, 2023
da7128f
feat: prompt user to choose a fee-payer alias
ymekuria May 6, 2023
835a1a8
feat: validate fee-payer alias input
ymekuria May 7, 2023
aa42fb8
feat: prompt user to enter existing base58 key only if recover option…
ymekuria May 7, 2023
ce66c06
feat: add create feepayer keypair step
ymekuria May 7, 2023
778a7cd
feat: generate feepayor keypayer
ymekuria May 8, 2023
c5d61fe
refactor: create feepayer keypayer in function
ymekuria May 8, 2023
c5e404a
refactor: create zkapp key pair in helper function
ymekuria May 8, 2023
b1d70b0
refactor: add network argument to create keypair helper
ymekuria May 8, 2023
6a5566c
feat: get users home directory path
ymekuria May 8, 2023
18a4c33
feat: add fee payer key pair to cache after create
ymekuria May 8, 2023
fafbb4a
feat: link feepayer public key to faucet link
ymekuria May 8, 2023
35359e0
feat: add feepayer alias to config.json
ymekuria May 8, 2023
9b41a42
feat: read feepayor key from users dir for deploy
ymekuria May 8, 2023
8ded5b1
feat: deploy using 3rd party feepayor
ymekuria May 8, 2023
40def10
feat: display feepayor key on error if account not found
ymekuria May 8, 2023
174e76e
feat: add feepayer key path to config.json
ymekuria May 8, 2023
dc59684
feat: display feepayer alais during deploy
ymekuria May 8, 2023
3f4e135
feat: check if feepayor is cached
ymekuria May 8, 2023
2b4e6d3
feat: prompt user to use cached feepayor as an option
ymekuria May 8, 2023
0ab58be
feat: only prompt for feepayer alias if not in cache
ymekuria May 11, 2023
c34434a
Update Next UI scaffold defaults (#402)
ymekuria May 9, 2023
aa6c579
use true key path instead misleading key path (#404)
wizicer May 9, 2023
eee3715
Clean up deploy and log full errors to the console (#408)
mitschabaude May 11, 2023
006d816
feat: read feepayor key from users dir for deploy
ymekuria May 8, 2023
70f01b4
refactor: get cached feepayer aliases in function
ymekuria May 11, 2023
ee77f13
refactor: get cached feepayer address in function
ymekuria May 11, 2023
66090fb
feat:display all cached fee payers in prompt
ymekuria May 16, 2023
bd2cc00
feat: skip alternative cached feepayer prompt if default selected
ymekuria May 16, 2023
fd09bef
refactor: get feepayor prompt options in function
ymekuria May 16, 2023
f8d9078
refactor: updated feepayor prompt
ymekuria Jun 5, 2023
1cdd8ee
refactor: display correct prompt if no feepayer is cached
ymekuria Jun 8, 2023
86d310b
refactor: seperate deploy alias prompts
ymekuria Jun 9, 2023
13890ee
refactor: move feepayor prompts to seperate variable
ymekuria Jun 9, 2023
3020cb0
refactor: move recover feepayor prompts to seperate variable
ymekuria Jun 9, 2023
f2bc51c
refactor: seperate other feepayor prompts
ymekuria Jun 9, 2023
3cc2272
refactor: seperate feepayor alias prompts
ymekuria Jun 9, 2023
bc9701c
refactor: consitent method names
ymekuria Jun 12, 2023
aab8d7d
feat: add option to select from multiple saved feepayors
ymekuria Jun 12, 2023
cbc6428
feat: do not prompt user for fee-payer alias if saved alias selected
ymekuria Jun 12, 2023
1dd6e8a
refactor: add config prompts file
ymekuria Jun 12, 2023
0e97124
refactor: move deploy alias prompts to seperate file
ymekuria Jun 12, 2023
b02bed1
refactor: move recover fee-payer prompts to seperate file
ymekuria Jun 13, 2023
6aba713
feat: reorder recover feepayor prompts
ymekuria Jun 13, 2023
feaa8ad
feat: update prompt to create a deploy alias
ymekuria Jun 13, 2023
c2f0820
feat: update feepayor alias prompt
ymekuria Jun 13, 2023
87a5fd1
fix: workaround for enquirer skip prompt bug
ymekuria Jun 13, 2023
26bfc60
refactor: rename fee-payer -> feepayer in prompts
ymekuria Jun 13, 2023
3b073f2
feat: capitilze url in deploy alias table
ymekuria Jun 13, 2023
8d224ba
feat: replace choose -> create in alias prompts
ymekuria Jun 13, 2023
983a04c
feat: update feepayer -> fee payer in prompts
ymekuria Jun 13, 2023
5a4bc23
refactor: move fee payer alias prompt to prompts file
ymekuria Jun 14, 2023
e934029
feat: update branch prompt order
ymekuria Jun 14, 2023
059184b
refactor: move other fee payer prompts to prompts file
ymekuria Jun 14, 2023
7331ad7
feat: move create key pair step to function
ymekuria Jun 14, 2023
2d0f61f
add recover feepayor step to function
ymekuria Jun 14, 2023
ce8bc73
feat: add cached keypair step to a seperate function
ymekuria Jun 14, 2023
b46a9ed
feat: update key pair steps
ymekuria Jun 14, 2023
53023c8
feat: add account recover warning prompt
ymekuria Jun 14, 2023
4ab7f46
refactor: remove logs
ymekuria Jun 14, 2023
9d05792
merge branch main into feature/third-party-feepayer
ymekuria Jun 14, 2023
2003915
refactor: remove unused code
ymekuria Jun 14, 2023
b6f4bdd
feat: add keypair storage note to create prompt
ymekuria Jun 14, 2023
d1c111a
refactor: capitalize url field in deploy prompt
ymekuria Jun 14, 2023
722a0fa
feat: deploy with fee payer public key
ymekuria Jun 14, 2023
1414d37
feat: sanitize fee payer alais input
ymekuria Jun 14, 2023
645b960
feat distinguish stored fee payer alias in prompt
ymekuria Jun 15, 2023
4b383b7
feat: add incorrect private key input validation
ymekuria Jun 15, 2023
5338fd0
feat: add validation if fee payer alias already exits
ymekuria Jun 15, 2023
f55ff99
feat: update fee payer alias check
ymekuria Jun 15, 2023
0095110
feat: display error and exit config flow if invalid fee payer alias
ymekuria Jun 15, 2023
7479108
refactor: format invalid fee payer alais error message
ymekuria Jun 15, 2023
f5bcd3a
feat: update existing feepayer alias check
ymekuria Jun 15, 2023
a92b6bb
feat: sign deploy txn withfee payer key
ymekuria Jun 15, 2023
9428dcd
feat: update config type in interact script
ymekuria Jun 15, 2023
ef0b83c
feat: read fee payer key in interact script
ymekuria Jun 15, 2023
66a5b08
feat: add fee payer as txn sender
ymekuria Jun 16, 2023
08d35cb
feat: sign interact txn with fee payer key
ymekuria Jun 16, 2023
5771334
feat: pay zkapp account creation fee with feepayer
ymekuria Jun 16, 2023
3f081be
feat: replace groups of whitespace user input with single dash
ymekuria Jun 20, 2023
c8d0b6a
feat: rename to fee payer alias in config
ymekuria Jun 20, 2023
bdd9fe3
feat: rename to fee payer alias in interact script
ymekuria Jun 20, 2023
98b8edf
feat: use fee from config in interact script
ymekuria Jun 20, 2023
d54c517
feat: log errors in interact script
ymekuria Jun 20, 2023
768e7c5
feat: sanitize whitespaces for deploy alias input
ymekuria Jun 20, 2023
7ceb0b8
bump version => 0.10.0
ymekuria Jun 20, 2023
b441027
Merge branch 'main' into feature/third-party-feepayer
ymekuria Jun 20, 2023
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: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zkapp-cli",
"version": "0.9.1",
"version": "0.10.0",
"description": "CLI to create a zkApp (\"zero-knowledge app\") for Mina Protocol.",
"keywords": [
"cli",
Expand Down
297 changes: 226 additions & 71 deletions src/lib/config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
const fs = require('fs-extra');
const findPrefix = require('find-npm-prefix');
const os = require('os');
const { prompt } = require('enquirer');
const { table, getBorderCharacters } = require('table');
const { step } = require('./helpers');
const { green, red, bold, gray, reset } = require('chalk');
const { green, red, bold, gray } = require('chalk');
const Client = require('mina-signer');

const { prompts } = require('./prompts');
const { PrivateKey, PublicKey } = require('snarkyjs');
const HOME_DIR = os.homedir();
const log = console.log;

/**
Expand All @@ -32,14 +35,34 @@ async function config() {
return;
}

let isFeepayerCached = false;
let defaultFeepayerAlias;
let cachedFeepayerAliases;
let defaultFeepayerAddress;

try {
cachedFeepayerAliases = getCachedFeepayerAliases(HOME_DIR);
defaultFeepayerAlias = cachedFeepayerAliases[0];
defaultFeepayerAddress = getCachedFeepayerAddress(
HOME_DIR,
defaultFeepayerAlias
);

isFeepayerCached = true;
} catch (err) {
if (err.code !== 'ENOENT') {
console.error(err);
}
}

// Checks if developer has the legacy networks in config.json and renames it to deploy aliases.
if (Object.prototype.hasOwnProperty.call(config, 'networks')) {
Object.assign(config, { deployAliases: config.networks });
delete config.networks;
}

// Build table of existing deploy aliases found in their config.json
let tableData = [[bold('Name'), bold('Url'), bold('Smart Contract')]];
let tableData = [[bold('Name'), bold('URL'), bold('Smart Contract')]];
for (const deployAliasName in config.deployAliases) {
const { url, smartContract } = config.deployAliases[deployAliasName];
tableData.push([
Expand Down Expand Up @@ -74,84 +97,115 @@ async function config() {
const msg = '\n ' + table(tableData, tableConfig).replaceAll('\n', '\n ');
log(msg);

console.log('Add a new deploy alias:');
console.log('Enter values to create a deploy alias:');

// TODO: Later, show pre-configured list to choose from or let user
// add a custom deploy alias.
const {
deployAliasPrompts,
initialFeepayerPrompts,
recoverFeepayerPrompts,
otherFeepayerPrompts,
feepayerAliasPrompt,
} = prompts;

function formatPrefixSymbol(state) {
// Shows a cyan question mark when not submitted.
// Shows a green check mark when submitted.
// Shows a red "x" if ctrl+C is pressed.
const initialPromptResponse = await prompt([
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chose this approach of feeding the response of the initial prompts into the subsequent branching prompt options because there is not a good api and way to handle nested prompts that depend on previous answers with the package we are using in the cli.

...deployAliasPrompts(config),
...initialFeepayerPrompts(
defaultFeepayerAlias,
defaultFeepayerAddress,
isFeepayerCached
),
]);

// 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
let recoverFeepayerResponse;
let feepayerAliasResponse;
let otherFeepayerResponse;

if (!state.submitted) return state.symbols.question;
return state.cancelled ? red(state.symbols.cross) : state.symbols.check;
if (initialPromptResponse.feepayer === 'recover') {
recoverFeepayerResponse = await prompt(
recoverFeepayerPrompts(cachedFeepayerAliases)
);
}

const response = await prompt([
{
type: 'input',
name: 'deployAliasName',
message: (state) => {
const style = state.submitted && !state.cancelled ? green : reset;
return style('Choose a name (can be anything):');
},
prefix: formatPrefixSymbol,
validate: async (val) => {
val = val.toLowerCase().trim().replace(' ', '-');
if (!val) return red('Name is required.');
if (Object.keys(config.deployAliases).includes(val)) {
return red('Name already exists.');
}
return true;
},
result: (val) => val.toLowerCase().trim().replace(' ', '-'),
},
{
type: 'input',
name: 'url',
message: (state) => {
const style = state.submitted && !state.cancelled ? green : reset;
return style('Set the Mina GraphQL API URL to deploy to:');
},
prefix: formatPrefixSymbol,
validate: (val) => {
if (!val) return red('Url is required.');
return true;
},
result: (val) => val.trim().replace(/ /, ''),
},
{
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):');
},
prefix: formatPrefixSymbol,
validate: (val) => {
if (!val) return red('Fee is required.');
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(/ /, ''),
},
]);
if (initialPromptResponse?.feepayer === 'create') {
console.log('inside create if');
feepayerAliasResponse = await prompt(
feepayerAliasPrompt(cachedFeepayerAliases)
);
}

if (initialPromptResponse.feepayer === 'other') {
otherFeepayerResponse = await prompt(
otherFeepayerPrompts(cachedFeepayerAliases)
);

if (otherFeepayerResponse.feepayer === 'recover') {
recoverFeepayerResponse = await prompt(
recoverFeepayerPrompts(cachedFeepayerAliases)
);
}

if (otherFeepayerResponse.feepayer === 'create') {
feepayerAliasResponse = await prompt(
feepayerAliasPrompt(cachedFeepayerAliases)
);
}
}

const promptResponse = {
...initialPromptResponse,
...recoverFeepayerResponse,
...otherFeepayerResponse,
...feepayerAliasResponse,
};

// If user presses "ctrl + c" during interactive prompt, exit.
const { deployAliasName, url, fee } = response;
let {
deployAliasName,
url,
fee,
feepayer,
feepayerAlias,
feepayerKey,
alternateCachedFeepayerAlias,
} = promptResponse;

if (!deployAliasName || !url || !fee) return;

const keyPair = await step(
`Create key pair at keys/${deployAliasName}.json`,
let feepayerKeyPair;
switch (feepayer) {
case 'create':
feepayerKeyPair = await createKeyPairStep(HOME_DIR, feepayerAlias);
break;
case 'recover':
feepayerKeyPair = await recoverKeyPairStep(
HOME_DIR,
feepayerKey,
feepayerAlias
);
break;
case 'defaultCache':
feepayerAlias = defaultFeepayerAlias;
feepayerKeyPair = await savedKeyPairStep(
HOME_DIR,
defaultFeepayerAlias,
defaultFeepayerAddress
);
break;
case 'alternateCachedFeepayer':
feepayerAlias = alternateCachedFeepayerAlias;
feepayerKeyPair = await savedKeyPairStep(
HOME_DIR,
alternateCachedFeepayerAlias
);
break;
default:
break;
}

await step(
`Create zkApp 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();
const keyPair = createKeyPair('testnet');
fs.outputJsonSync(`${DIR}/keys/${deployAliasName}.json`, keyPair, {
spaces: 2,
});
Expand All @@ -160,9 +214,16 @@ async function config() {
);

await step(`Add deploy alias to config.json`, async () => {
if (!feepayerAlias) {
// No fee payer alias, return early to prevent creating a deploy alias with invalid fee payer
log(red(`Invalid fee payer alias ${feepayerAlias}" .`));
process.exit(1);
}
config.deployAliases[deployAliasName] = {
url,
keyPath: `keys/${deployAliasName}.json`,
feepayerKeyPath: `${HOME_DIR}/.cache/zkapp-cli/keys/${feepayerAlias}.json`,
feepayerAlias,
fee,
};
fs.outputJsonSync(`${DIR}/config.json`, config, { spaces: 2 });
Expand All @@ -176,18 +237,112 @@ async function config() {
`\nSuccess!\n` +
`\nNext steps:` +
`\n - If this is a testnet, request tMINA at:\n https://faucet.minaprotocol.com/?address=${encodeURIComponent(
keyPair.publicKey
feepayerKeyPair.publicKey
)}&?explorer=${explorerName}` +
`\n - To deploy, run: \`zk deploy ${deployAliasName}\``;

log(green(str));
}

// Creates a new feepayer key pair
async function createKeyPairStep(directory, feepayerAlias) {
if (!feepayerAlias) {
// No fee payer alias, return early to prevent generating key pair with undefined alias
log(red(`Invalid fee payer alias ${feepayerAlias}.`));
return;
}
return await step(
`Create fee payer key pair at ${HOME_DIR}/.cache/zkapp-cli/keys/${feepayerAlias}.json`,
async () => {
const keyPair = createKeyPair('testnet');

fs.outputJsonSync(
`${directory}/.cache/zkapp-cli/keys/${feepayerAlias}.json`,
keyPair,
{
spaces: 2,
}
);
return keyPair;
}
);
}

async function recoverKeyPairStep(directory, feepayerKey, feepayerAlias) {
return await step(
`Recover fee payer keypair from ${feepayerKey} and add to ${HOME_DIR}/.cache/zkapp-cli/keys/${feepayerAlias}.json`,
async () => {
const feepayorPrivateKey = PrivateKey.fromBase58(feepayerKey);
const feepayerAddress = feepayorPrivateKey.toPublicKey();

const keyPair = {
privateKey: feepayerKey,
publicKey: PublicKey.toBase58(feepayerAddress),
};

fs.outputJsonSync(
`${directory}/.cache/zkapp-cli/keys/${feepayerAlias}.json`,
keyPair,
{
spaces: 2,
}
);
return keyPair;
}
);
}
// Returns a cached keypair from a given feepayer alias
async function savedKeyPairStep(directory, feepayerAlias, address) {
if (!feepayerAlias) {
// No fee payer alias, return early to prevent generating key pair with undefined alias
log(red(`Invalid fee payer alias: ${feepayerAlias}.`));
process.exit(1);
}
const keyPair = fs.readJSONSync(
`${directory}/.cache/zkapp-cli/keys/${feepayerAlias}.json`
);

if (!address) address = keyPair.publicKey;

return await step(
`Use stored fee payer ${feepayerAlias} (public key: ${address}) `,

async () => {
return keyPair;
}
);
}

// Check if feepayer alias/aliases are stored on users machine and returns an array of them.
function getCachedFeepayerAliases(directory) {
let aliases = fs.readdirSync(`${directory}/.cache/zkapp-cli/keys/`);

aliases = aliases
.filter((fileName) => fileName.includes('json'))
.map((name) => name.slice(0, -5));

return aliases;
}

function getCachedFeepayerAddress(directory, feePayorAlias) {
const address = fs.readJSONSync(
`${directory}/.cache/zkapp-cli/keys/${feePayorAlias}.json`
).publicKey;

return address;
}

function createKeyPair(network) {
const client = new Client({ network });
return client.genKeys();
}

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

module.exports = {
config,
};
Loading