Skip to content

Commit a1f7ac4

Browse files
authored
Add Third Party Feepayer (#424)
1 parent f6fdcd2 commit a1f7ac4

File tree

6 files changed

+569
-103
lines changed

6 files changed

+569
-103
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "zkapp-cli",
3-
"version": "0.9.1",
3+
"version": "0.10.0",
44
"description": "CLI to create a zkApp (\"zero-knowledge app\") for Mina Protocol.",
55
"keywords": [
66
"cli",

src/lib/config.js

Lines changed: 226 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
const fs = require('fs-extra');
22
const findPrefix = require('find-npm-prefix');
3+
const os = require('os');
34
const { prompt } = require('enquirer');
45
const { table, getBorderCharacters } = require('table');
56
const { step } = require('./helpers');
6-
const { green, red, bold, gray, reset } = require('chalk');
7+
const { green, red, bold, gray } = require('chalk');
78
const Client = require('mina-signer');
8-
9+
const { prompts } = require('./prompts');
10+
const { PrivateKey, PublicKey } = require('snarkyjs');
11+
const HOME_DIR = os.homedir();
912
const log = console.log;
1013

1114
/**
@@ -32,14 +35,34 @@ async function config() {
3235
return;
3336
}
3437

38+
let isFeepayerCached = false;
39+
let defaultFeepayerAlias;
40+
let cachedFeepayerAliases;
41+
let defaultFeepayerAddress;
42+
43+
try {
44+
cachedFeepayerAliases = getCachedFeepayerAliases(HOME_DIR);
45+
defaultFeepayerAlias = cachedFeepayerAliases[0];
46+
defaultFeepayerAddress = getCachedFeepayerAddress(
47+
HOME_DIR,
48+
defaultFeepayerAlias
49+
);
50+
51+
isFeepayerCached = true;
52+
} catch (err) {
53+
if (err.code !== 'ENOENT') {
54+
console.error(err);
55+
}
56+
}
57+
3558
// Checks if developer has the legacy networks in config.json and renames it to deploy aliases.
3659
if (Object.prototype.hasOwnProperty.call(config, 'networks')) {
3760
Object.assign(config, { deployAliases: config.networks });
3861
delete config.networks;
3962
}
4063

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

77-
console.log('Add a new deploy alias:');
100+
console.log('Enter values to create a deploy alias:');
78101

79-
// TODO: Later, show pre-configured list to choose from or let user
80-
// add a custom deploy alias.
102+
const {
103+
deployAliasPrompts,
104+
initialFeepayerPrompts,
105+
recoverFeepayerPrompts,
106+
otherFeepayerPrompts,
107+
feepayerAliasPrompt,
108+
} = prompts;
81109

82-
function formatPrefixSymbol(state) {
83-
// Shows a cyan question mark when not submitted.
84-
// Shows a green check mark when submitted.
85-
// Shows a red "x" if ctrl+C is pressed.
110+
const initialPromptResponse = await prompt([
111+
...deployAliasPrompts(config),
112+
...initialFeepayerPrompts(
113+
defaultFeepayerAlias,
114+
defaultFeepayerAddress,
115+
isFeepayerCached
116+
),
117+
]);
86118

87-
// Can't override the validating prefix or styling unfortunately
88-
// https://github.com/enquirer/enquirer/blob/8d626c206733420637660ac7c2098d7de45e8590/lib/prompt.js#L125
89-
// if (state.validating) return ''; // use no symbol, instead of pointer
119+
let recoverFeepayerResponse;
120+
let feepayerAliasResponse;
121+
let otherFeepayerResponse;
90122

91-
if (!state.submitted) return state.symbols.question;
92-
return state.cancelled ? red(state.symbols.cross) : state.symbols.check;
123+
if (initialPromptResponse.feepayer === 'recover') {
124+
recoverFeepayerResponse = await prompt(
125+
recoverFeepayerPrompts(cachedFeepayerAliases)
126+
);
93127
}
94128

95-
const response = await prompt([
96-
{
97-
type: 'input',
98-
name: 'deployAliasName',
99-
message: (state) => {
100-
const style = state.submitted && !state.cancelled ? green : reset;
101-
return style('Choose a name (can be anything):');
102-
},
103-
prefix: formatPrefixSymbol,
104-
validate: async (val) => {
105-
val = val.toLowerCase().trim().replace(' ', '-');
106-
if (!val) return red('Name is required.');
107-
if (Object.keys(config.deployAliases).includes(val)) {
108-
return red('Name already exists.');
109-
}
110-
return true;
111-
},
112-
result: (val) => val.toLowerCase().trim().replace(' ', '-'),
113-
},
114-
{
115-
type: 'input',
116-
name: 'url',
117-
message: (state) => {
118-
const style = state.submitted && !state.cancelled ? green : reset;
119-
return style('Set the Mina GraphQL API URL to deploy to:');
120-
},
121-
prefix: formatPrefixSymbol,
122-
validate: (val) => {
123-
if (!val) return red('Url is required.');
124-
return true;
125-
},
126-
result: (val) => val.trim().replace(/ /, ''),
127-
},
128-
{
129-
type: 'input',
130-
name: 'fee',
131-
message: (state) => {
132-
const style = state.submitted && !state.cancelled ? green : reset;
133-
return style('Set transaction fee to use when deploying (in MINA):');
134-
},
135-
prefix: formatPrefixSymbol,
136-
validate: (val) => {
137-
if (!val) return red('Fee is required.');
138-
if (isNaN(val)) return red('Fee must be a number.');
139-
if (val < 0) return red("Fee can't be negative.");
140-
return true;
141-
},
142-
result: (val) => val.trim().replace(/ /, ''),
143-
},
144-
]);
129+
if (initialPromptResponse?.feepayer === 'create') {
130+
console.log('inside create if');
131+
feepayerAliasResponse = await prompt(
132+
feepayerAliasPrompt(cachedFeepayerAliases)
133+
);
134+
}
135+
136+
if (initialPromptResponse.feepayer === 'other') {
137+
otherFeepayerResponse = await prompt(
138+
otherFeepayerPrompts(cachedFeepayerAliases)
139+
);
140+
141+
if (otherFeepayerResponse.feepayer === 'recover') {
142+
recoverFeepayerResponse = await prompt(
143+
recoverFeepayerPrompts(cachedFeepayerAliases)
144+
);
145+
}
146+
147+
if (otherFeepayerResponse.feepayer === 'create') {
148+
feepayerAliasResponse = await prompt(
149+
feepayerAliasPrompt(cachedFeepayerAliases)
150+
);
151+
}
152+
}
153+
154+
const promptResponse = {
155+
...initialPromptResponse,
156+
...recoverFeepayerResponse,
157+
...otherFeepayerResponse,
158+
...feepayerAliasResponse,
159+
};
145160

146161
// If user presses "ctrl + c" during interactive prompt, exit.
147-
const { deployAliasName, url, fee } = response;
162+
let {
163+
deployAliasName,
164+
url,
165+
fee,
166+
feepayer,
167+
feepayerAlias,
168+
feepayerKey,
169+
alternateCachedFeepayerAlias,
170+
} = promptResponse;
171+
148172
if (!deployAliasName || !url || !fee) return;
149173

150-
const keyPair = await step(
151-
`Create key pair at keys/${deployAliasName}.json`,
174+
let feepayerKeyPair;
175+
switch (feepayer) {
176+
case 'create':
177+
feepayerKeyPair = await createKeyPairStep(HOME_DIR, feepayerAlias);
178+
break;
179+
case 'recover':
180+
feepayerKeyPair = await recoverKeyPairStep(
181+
HOME_DIR,
182+
feepayerKey,
183+
feepayerAlias
184+
);
185+
break;
186+
case 'defaultCache':
187+
feepayerAlias = defaultFeepayerAlias;
188+
feepayerKeyPair = await savedKeyPairStep(
189+
HOME_DIR,
190+
defaultFeepayerAlias,
191+
defaultFeepayerAddress
192+
);
193+
break;
194+
case 'alternateCachedFeepayer':
195+
feepayerAlias = alternateCachedFeepayerAlias;
196+
feepayerKeyPair = await savedKeyPairStep(
197+
HOME_DIR,
198+
alternateCachedFeepayerAlias
199+
);
200+
break;
201+
default:
202+
break;
203+
}
204+
205+
await step(
206+
`Create zkApp key pair at keys/${deployAliasName}.json`,
152207
async () => {
153-
const client = new Client({ network: 'testnet' }); // TODO: Make this configurable for mainnet and testnet.
154-
let keyPair = client.genKeys();
208+
const keyPair = createKeyPair('testnet');
155209
fs.outputJsonSync(`${DIR}/keys/${deployAliasName}.json`, keyPair, {
156210
spaces: 2,
157211
});
@@ -160,9 +214,16 @@ async function config() {
160214
);
161215

162216
await step(`Add deploy alias to config.json`, async () => {
217+
if (!feepayerAlias) {
218+
// No fee payer alias, return early to prevent creating a deploy alias with invalid fee payer
219+
log(red(`Invalid fee payer alias ${feepayerAlias}" .`));
220+
process.exit(1);
221+
}
163222
config.deployAliases[deployAliasName] = {
164223
url,
165224
keyPath: `keys/${deployAliasName}.json`,
225+
feepayerKeyPath: `${HOME_DIR}/.cache/zkapp-cli/keys/${feepayerAlias}.json`,
226+
feepayerAlias,
166227
fee,
167228
};
168229
fs.outputJsonSync(`${DIR}/config.json`, config, { spaces: 2 });
@@ -176,18 +237,112 @@ async function config() {
176237
`\nSuccess!\n` +
177238
`\nNext steps:` +
178239
`\n - If this is a testnet, request tMINA at:\n https://faucet.minaprotocol.com/?address=${encodeURIComponent(
179-
keyPair.publicKey
240+
feepayerKeyPair.publicKey
180241
)}&?explorer=${explorerName}` +
181242
`\n - To deploy, run: \`zk deploy ${deployAliasName}\``;
182243

183244
log(green(str));
184245
}
185246

247+
// Creates a new feepayer key pair
248+
async function createKeyPairStep(directory, feepayerAlias) {
249+
if (!feepayerAlias) {
250+
// No fee payer alias, return early to prevent generating key pair with undefined alias
251+
log(red(`Invalid fee payer alias ${feepayerAlias}.`));
252+
return;
253+
}
254+
return await step(
255+
`Create fee payer key pair at ${HOME_DIR}/.cache/zkapp-cli/keys/${feepayerAlias}.json`,
256+
async () => {
257+
const keyPair = createKeyPair('testnet');
258+
259+
fs.outputJsonSync(
260+
`${directory}/.cache/zkapp-cli/keys/${feepayerAlias}.json`,
261+
keyPair,
262+
{
263+
spaces: 2,
264+
}
265+
);
266+
return keyPair;
267+
}
268+
);
269+
}
270+
271+
async function recoverKeyPairStep(directory, feepayerKey, feepayerAlias) {
272+
return await step(
273+
`Recover fee payer keypair from ${feepayerKey} and add to ${HOME_DIR}/.cache/zkapp-cli/keys/${feepayerAlias}.json`,
274+
async () => {
275+
const feepayorPrivateKey = PrivateKey.fromBase58(feepayerKey);
276+
const feepayerAddress = feepayorPrivateKey.toPublicKey();
277+
278+
const keyPair = {
279+
privateKey: feepayerKey,
280+
publicKey: PublicKey.toBase58(feepayerAddress),
281+
};
282+
283+
fs.outputJsonSync(
284+
`${directory}/.cache/zkapp-cli/keys/${feepayerAlias}.json`,
285+
keyPair,
286+
{
287+
spaces: 2,
288+
}
289+
);
290+
return keyPair;
291+
}
292+
);
293+
}
294+
// Returns a cached keypair from a given feepayer alias
295+
async function savedKeyPairStep(directory, feepayerAlias, address) {
296+
if (!feepayerAlias) {
297+
// No fee payer alias, return early to prevent generating key pair with undefined alias
298+
log(red(`Invalid fee payer alias: ${feepayerAlias}.`));
299+
process.exit(1);
300+
}
301+
const keyPair = fs.readJSONSync(
302+
`${directory}/.cache/zkapp-cli/keys/${feepayerAlias}.json`
303+
);
304+
305+
if (!address) address = keyPair.publicKey;
306+
307+
return await step(
308+
`Use stored fee payer ${feepayerAlias} (public key: ${address}) `,
309+
310+
async () => {
311+
return keyPair;
312+
}
313+
);
314+
}
315+
316+
// Check if feepayer alias/aliases are stored on users machine and returns an array of them.
317+
function getCachedFeepayerAliases(directory) {
318+
let aliases = fs.readdirSync(`${directory}/.cache/zkapp-cli/keys/`);
319+
320+
aliases = aliases
321+
.filter((fileName) => fileName.includes('json'))
322+
.map((name) => name.slice(0, -5));
323+
324+
return aliases;
325+
}
326+
327+
function getCachedFeepayerAddress(directory, feePayorAlias) {
328+
const address = fs.readJSONSync(
329+
`${directory}/.cache/zkapp-cli/keys/${feePayorAlias}.json`
330+
).publicKey;
331+
332+
return address;
333+
}
334+
335+
function createKeyPair(network) {
336+
const client = new Client({ network });
337+
return client.genKeys();
338+
}
339+
186340
function getExplorerName(graphQLUrl) {
187341
return new URL(graphQLUrl).hostname
188342
.split('.')
189343
.filter((item) => item === 'minascan' || item === 'minaexplorer')?.[0];
190344
}
345+
191346
module.exports = {
192347
config,
193348
};

0 commit comments

Comments
 (0)