@devtion/devcli 0.0.0-dev → 0.0.0-e312890

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -1
  3. package/dist/.env +54 -39
  4. package/dist/index.js +553 -121
  5. package/dist/public/mini-semaphore.wasm +0 -0
  6. package/dist/public/mini-semaphore.zkey +0 -0
  7. package/dist/types/commands/authBandada.d.ts +2 -0
  8. package/dist/types/commands/authSIWE.d.ts +7 -0
  9. package/dist/types/commands/ceremony/index.d.ts +3 -0
  10. package/dist/types/commands/ceremony/listParticipants.d.ts +2 -0
  11. package/dist/types/commands/contribute.d.ts +1 -1
  12. package/dist/types/commands/finalize.d.ts +4 -3
  13. package/dist/types/commands/index.d.ts +2 -0
  14. package/dist/types/commands/observe.d.ts +1 -1
  15. package/dist/types/commands/setup.d.ts +4 -4
  16. package/dist/types/lib/bandada.d.ts +6 -0
  17. package/dist/types/lib/files.d.ts +1 -0
  18. package/dist/types/lib/localConfigs.d.ts +38 -0
  19. package/dist/types/lib/prompts.d.ts +7 -7
  20. package/dist/types/lib/utils.d.ts +3 -2
  21. package/dist/types/types/index.d.ts +69 -0
  22. package/package.json +106 -97
  23. package/src/commands/auth.ts +24 -8
  24. package/src/commands/authBandada.ts +120 -0
  25. package/src/commands/authSIWE.ts +185 -0
  26. package/src/commands/ceremony/index.ts +20 -0
  27. package/src/commands/ceremony/listParticipants.ts +56 -0
  28. package/src/commands/contribute.ts +56 -29
  29. package/src/commands/finalize.ts +27 -14
  30. package/src/commands/index.ts +3 -1
  31. package/src/commands/listCeremonies.ts +2 -3
  32. package/src/commands/logout.ts +3 -1
  33. package/src/commands/observe.ts +8 -4
  34. package/src/commands/setup.ts +59 -48
  35. package/src/commands/validate.ts +2 -3
  36. package/src/index.ts +35 -13
  37. package/src/lib/bandada.ts +51 -0
  38. package/src/lib/errors.ts +2 -2
  39. package/src/lib/localConfigs.ts +55 -1
  40. package/src/lib/prompts.ts +23 -26
  41. package/src/lib/services.ts +39 -16
  42. package/src/lib/utils.ts +53 -15
  43. package/src/types/index.ts +75 -0
package/dist/index.js CHANGED
@@ -2,28 +2,27 @@
2
2
 
3
3
  /**
4
4
  * @module @p0tion/phase2cli
5
- * @version 1.0.5
5
+ * @version 1.2.5
6
6
  * @file All-in-one interactive command-line for interfacing with zkSNARK Phase 2 Trusted Setup ceremonies
7
7
  * @copyright Ethereum Foundation 2022
8
8
  * @license MIT
9
9
  * @see [Github]{@link https://github.com/privacy-scaling-explorations/p0tion}
10
10
  */
11
11
  import { createCommand } from 'commander';
12
- import fs, { readFileSync, createWriteStream, renameSync } from 'fs';
13
- import { dirname } from 'path';
12
+ import fs, { readFileSync, createWriteStream, existsSync, renameSync } from 'fs';
13
+ import path, { dirname } from 'path';
14
14
  import { fileURLToPath } from 'url';
15
- import { zKey } from 'snarkjs';
15
+ import { zKey, groth16 } from 'snarkjs';
16
16
  import boxen from 'boxen';
17
17
  import { pipeline } from 'node:stream';
18
18
  import { promisify } from 'node:util';
19
19
  import fetch$1 from 'node-fetch';
20
- import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
21
- import { commonTerms, formatZkeyIndex, getZkeyStorageFilePath, finalContributionIndex, createCustomLoggerForFile, getBucketName, progressToNextContributionStep, permanentlyStoreCurrentContributionTimeAndHash, convertToDoubleDigits, multiPartUpload, verifyContribution, generateGetObjectPreSignedUrl, convertBytesOrKbToGb, numExpIterations, getDocumentById, getParticipantsCollectionPath, fromQueryToFirebaseDocumentInfo, getAllCollectionDocs, extractPrefix, autoGenerateEntropy, vmConfigurationTypes, initializeFirebaseCoreServices, signInToFirebaseWithCredentials, getCurrentFirebaseAuthUser, isCoordinator, parseCeremonyFile, blake512FromPath, checkIfObjectExist, setupCeremony, genesisZkeyIndex, getR1csStorageFilePath, getWasmStorageFilePath, getPotStorageFilePath, extractPoTFromFilename, potFileDownloadMainUrl, createS3Bucket, potFilenameTemplate, getR1CSInfo, getOpenedCeremonies, getCeremonyCircuits, checkParticipantForCeremony, getCurrentActiveParticipantTimeout, getCircuitBySequencePosition, getCircuitContributionsFromContributor, progressToNextCircuitForContribution, resumeContributionAfterTimeoutExpiration, generateValidContributionsAttestation, getContributionsValidityForContributor, getClosedCeremonies, checkAndPrepareCoordinatorForFinalization, computeSHA256ToHex, finalizeCeremony, getVerificationKeyStorageFilePath, verificationKeyAcronym, getVerifierContractStorageFilePath, verifierSmartContractAcronym, finalizeCircuit, exportVkey, exportVerifierContract } from '@p0tion/actions';
20
+ import { commonTerms, formatZkeyIndex, getZkeyStorageFilePath, finalContributionIndex, createCustomLoggerForFile, getBucketName, progressToNextContributionStep, contribHashRegex, permanentlyStoreCurrentContributionTimeAndHash, convertToDoubleDigits, multiPartUpload, verifyContribution, generateGetObjectPreSignedUrl, convertBytesOrKbToGb, numExpIterations, getDocumentById, getParticipantsCollectionPath, fromQueryToFirebaseDocumentInfo, getAllCollectionDocs, extractPrefix, autoGenerateEntropy, vmConfigurationTypes, initializeFirebaseCoreServices, signInToFirebaseWithCredentials, getCurrentFirebaseAuthUser, isCoordinator, parseCeremonyFile, blake512FromPath, checkIfObjectExist, setupCeremony, genesisZkeyIndex, getR1csStorageFilePath, getWasmStorageFilePath, getPotStorageFilePath, extractPoTFromFilename, potFileDownloadMainUrl, createS3Bucket, potFilenameTemplate, getR1CSInfo, getOpenedCeremonies, getCeremonyCircuits, checkParticipantForCeremony, getCurrentActiveParticipantTimeout, getCircuitBySequencePosition, getCircuitContributionsFromContributor, progressToNextCircuitForContribution, resumeContributionAfterTimeoutExpiration, generateValidContributionsAttestation, getContributionsValidityForContributor, getClosedCeremonies, checkAndPrepareCoordinatorForFinalization, computeSHA256ToHex, finalizeCeremony, getVerificationKeyStorageFilePath, verificationKeyAcronym, getVerifierContractStorageFilePath, verifierSmartContractAcronym, finalizeCircuit, exportVkey, exportVerifierContract, getAllCeremonies } from '@devtion/actions';
22
21
  import fetch from '@adobe/node-fetch-retry';
23
22
  import { request } from '@octokit/request';
24
23
  import { SingleBar, Presets } from 'cli-progress';
25
24
  import dotenv from 'dotenv';
26
- import { GithubAuthProvider, getAuth, signOut } from 'firebase/auth';
25
+ import { GithubAuthProvider, signInWithCustomToken, getAuth, signOut } from 'firebase/auth';
27
26
  import { getDiskInfoSync } from 'node-disk-info';
28
27
  import ora from 'ora';
29
28
  import { Timer } from 'timer-node';
@@ -34,11 +33,13 @@ import Conf from 'conf';
34
33
  import prompts from 'prompts';
35
34
  import clear from 'clear';
36
35
  import figlet from 'figlet';
37
- import { Readable } from 'stream';
38
36
  import { createOAuthDeviceAuth } from '@octokit/auth-oauth-device';
39
37
  import clipboard from 'clipboardy';
40
38
  import open from 'open';
41
- import { Timestamp, onSnapshot } from 'firebase/firestore';
39
+ import { Identity } from '@semaphore-protocol/identity';
40
+ import { httpsCallable } from 'firebase/functions';
41
+ import { ApiSdk } from '@bandada/api-sdk';
42
+ import { Timestamp, onSnapshot, doc, collection, getDocs } from 'firebase/firestore';
42
43
  import readline from 'readline';
43
44
 
44
45
  /**
@@ -99,7 +100,7 @@ const CORE_SERVICES_ERRORS = {
99
100
  FIREBASE_TOKEN_EXPIRED_REMOVED_PERMISSIONS: `The Github authorization has failed due to lack of association between your account and the CLI`,
100
101
  FIREBASE_USER_DISABLED: `The Github account has been suspended by the ceremony coordinator(s), blocking the possibility of contribution. Please, contact them to understand the motivation behind it.`,
101
102
  FIREBASE_FAILED_CREDENTIALS_VERIFICATION: `Firebase cannot verify your Github credentials due to network errors. Please, try once again later.`,
102
- FIREBASE_NETWORK_ERROR: `Unable to reach Firebase due to network erros. Please, try once again later and make sure your Internet connection is stable.`,
103
+ FIREBASE_NETWORK_ERROR: `Unable to reach Firebase due to network errors. Please, try once again later and make sure your Internet connection is stable.`,
103
104
  FIREBASE_CEREMONY_NOT_OPENED: `There are no ceremonies opened to contributions`,
104
105
  FIREBASE_CEREMONY_NOT_CLOSED: `There are no ceremonies ready to finalization`,
105
106
  AWS_CEREMONY_BUCKET_CREATION: `Unable to create a new bucket for the ceremony. Something went wrong during the creation. Please, repeat the process by providing a new ceremony name of the ceremony.`,
@@ -121,7 +122,7 @@ const COMMAND_ERRORS = {
121
122
  COMMAND_SETUP_NO_R1CS: `Unable to retrieve R1CS files from current working directory. Please, run this command from a working directory where the R1CS files are located to continue with the setup process. We kindly ask you to run the command from an empty directory containing only the R1CS and WASM files.`,
122
123
  COMMAND_SETUP_NO_WASM: `Unable to retrieve WASM files from current working directory. Please, run this command from a working directory where the WASM files are located to continue with the setup process. We kindly ask you to run the command from an empty directory containing only the WASM and R1CS files.`,
123
124
  COMMAND_SETUP_MISMATCH_R1CS_WASM: `The folder contains more R1CS files than WASM files (or vice versa). Please, run this command from a working directory where each R1CS is paired with its corresponding file WASM.`,
124
- COMMAND_SETUP_DOWNLOAD_PTAU: `Unable to download Powers of Tau file from Hermez Cryptography Phase 1 Trusted Setup. Possible causes may involve an error while making the request (be sure to have a stable internet connection). Please, we kindly ask you to terminate the current session and repeat the process.`,
125
+ COMMAND_SETUP_DOWNLOAD_PTAU: `Unable to download Powers of Tau file from PPoT Phase 1 Trusted Setup. Possible causes may involve an error while making the request (be sure to have a stable internet connection). Please, we kindly ask you to terminate the current session and repeat the process.`,
125
126
  COMMAND_SETUP_ABORT: `You chose to abort the setup process.`,
126
127
  COMMAND_CONTRIBUTE_NO_OPENED_CEREMONIES: `Unfortunately, there is no ceremony for which you can make a contribution at this time. Please, try again later.`,
127
128
  COMMAND_CONTRIBUTE_NO_PARTICIPANT_DATA: `Unable to retrieve your data as ceremony participant. Please, terminate the current session and try again later. If the error persists, please contact the ceremony coordinator.`,
@@ -237,6 +238,14 @@ const checkAndMakeNewDirectoryIfNonexistent = (directoryLocalPath) => {
237
238
  const writeLocalJsonFile = (filePath, data) => {
238
239
  fs.writeFileSync(filePath, JSON.stringify(data), "utf-8");
239
240
  };
241
+ /**
242
+ * Return the local current project directory name.
243
+ * @returns <string> - the local project (e.g., dist/) directory name.
244
+ */
245
+ const getLocalDirname = () => {
246
+ const filename = fileURLToPath(import.meta.url);
247
+ return path.dirname(filename);
248
+ };
240
249
 
241
250
  // Get npm package name.
242
251
  const packagePath$4 = `${dirname(fileURLToPath(import.meta.url))}/..`;
@@ -252,6 +261,14 @@ const config = new Conf({
252
261
  accessToken: {
253
262
  type: "string",
254
263
  default: ""
264
+ },
265
+ bandadaIdentity: {
266
+ type: "string",
267
+ default: ""
268
+ },
269
+ authMethod: {
270
+ type: "string",
271
+ default: ""
255
272
  }
256
273
  }
257
274
  });
@@ -312,6 +329,39 @@ const setLocalAccessToken = (token) => config.set("accessToken", token);
312
329
  * Delete the stored access token.
313
330
  */
314
331
  const deleteLocalAccessToken = () => config.delete("accessToken");
332
+ /**
333
+ * Return the Bandada identity, if present.
334
+ * @returns <string | undefined> - the Bandada identity if present, otherwise undefined.
335
+ */
336
+ const getLocalBandadaIdentity = () => config.get("bandadaIdentity");
337
+ /**
338
+ * Check if the Bandada identity exists in the local storage.
339
+ * @returns <boolean>
340
+ */
341
+ const checkLocalBandadaIdentity = () => config.has("bandadaIdentity") && !!config.get("bandadaIdentity");
342
+ /**
343
+ * Set the Bandada identity.
344
+ * @param identity <string> - the Bandada identity to be stored.
345
+ */
346
+ const setLocalBandadaIdentity = (identity) => config.set("bandadaIdentity", identity);
347
+ /**
348
+ * Delete the stored Bandada identity.
349
+ */
350
+ const deleteLocalBandadaIdentity = () => config.delete("bandadaIdentity");
351
+ /**
352
+ * Return the authentication method, if present.
353
+ * @returns <string | undefined> - the authentication method if present, otherwise undefined.
354
+ */
355
+ const getLocalAuthMethod = () => config.get("authMethod");
356
+ /**
357
+ * Set the authentication method.
358
+ * @param method <string> - the authentication method to be stored.
359
+ */
360
+ const setLocalAuthMethod = (method) => config.set("authMethod", method);
361
+ /**
362
+ * Delete the stored authentication method.
363
+ */
364
+ const deleteLocalAuthMethod = () => config.delete("authMethod");
315
365
  /**
316
366
  * Get the complete local file path.
317
367
  * @param cwd <string> - the current working directory path.
@@ -367,6 +417,12 @@ const getVerificationKeyLocalFilePath = (completeFilename) => `${verificationKey
367
417
  * @returns <string> - the complete final verifier contract path to the file.
368
418
  */
369
419
  const getVerifierContractLocalFilePath = (completeFilename) => `${verifierContractsLocalFolderPath}/${completeFilename}`;
420
+ /**
421
+ * Get the complete final attestation file path.
422
+ * @param completeFilename <string> - the complete filename of the file (name.ext).
423
+ * @returns <string> - the complete final final attestation path to the file.
424
+ */
425
+ const getFinalAttestationLocalFilePath = (completeFilename) => `${finalAttestationsLocalFolderPath}/${completeFilename}`;
370
426
  /**
371
427
  * Get the final transcript file path.
372
428
  * @param completeFilename <string> - the complete filename of the file (name.ext).
@@ -416,7 +472,7 @@ const getGithubAuthenticatedUserGists = async (githubToken, params) => {
416
472
  headers: {
417
473
  authorization: `token ${githubToken}`
418
474
  },
419
- per_page: params.perPage,
475
+ per_page: params.perPage, // max items per page = 100.
420
476
  page: params.page
421
477
  });
422
478
  if (response && response.status === 200)
@@ -464,9 +520,10 @@ const getPublicAttestationGist = async (githubToken, publicAttestationFilename)
464
520
  * @returns <string> - the third-party provider handle of the user.
465
521
  */
466
522
  const getUserHandleFromProviderUserId = (providerUserId) => {
467
- if (providerUserId.indexOf("-") === -1)
468
- showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_GET_GITHUB_ACCOUNT_INFO, true);
469
- return providerUserId.split("-")[0];
523
+ if (providerUserId.indexOf("-") === -1) {
524
+ return providerUserId;
525
+ }
526
+ return providerUserId.substring(0, providerUserId.lastIndexOf("-"));
470
527
  };
471
528
  /**
472
529
  * Return a custom spinner.
@@ -580,9 +637,22 @@ const publishGist = async (token, content, ceremonyTitle, ceremonyPrefix) => {
580
637
  * @param isFinalizing <boolean> - flag to discriminate between ceremony finalization (true) and contribution (false).
581
638
  * @returns <string> - the ready to share tweet url.
582
639
  */
583
- const generateCustomUrlToTweetAboutParticipation = (ceremonyName, gistUrl, isFinalizing) => isFinalizing
584
- ? `https://twitter.com/intent/tweet?text=I%20have%20finalized%20the%20${ceremonyName}%20Phase%202%20Trusted%20Setup%20ceremony!%20You%20can%20view%20my%20final%20attestation%20here:%20${gistUrl}%20#Ethereum%20#ZKP%20#PSE`
585
- : `https://twitter.com/intent/tweet?text=I%20contributed%20to%20the%20${ceremonyName}%20Phase%202%20Trusted%20Setup%20ceremony!%20You%20can%20contribute%20here:%20https://github.com/privacy-scaling-explorations/p0tion%20You%20can%20view%20my%20attestation%20here:%20${gistUrl}%20#Ethereum%20#ZKP`;
640
+ const generateCustomUrlToTweetAboutParticipation = (ceremonyName, gistUrl, isFinalizing) => {
641
+ ceremonyName = ceremonyName.replace(/ /g, "%20");
642
+ return isFinalizing
643
+ ? `https://twitter.com/intent/tweet?text=I%20have%20finalized%20the%20${ceremonyName}${ceremonyName.toLowerCase().includes("trusted") ||
644
+ ceremonyName.toLowerCase().includes("setup") ||
645
+ ceremonyName.toLowerCase().includes("phase2") ||
646
+ ceremonyName.toLowerCase().includes("ceremony")
647
+ ? "!"
648
+ : "%20Phase%202%20Trusted%20Setup%20ceremony!"}%20You%20can%20view%20my%20final%20attestation%20here:%20${gistUrl}%20#Ethereum%20#ZKP%20#PSE`
649
+ : `https://twitter.com/intent/tweet?text=I%20contributed%20to%20the%20${ceremonyName}${ceremonyName.toLowerCase().includes("trusted") ||
650
+ ceremonyName.toLowerCase().includes("setup") ||
651
+ ceremonyName.toLowerCase().includes("phase2") ||
652
+ ceremonyName.toLowerCase().includes("ceremony")
653
+ ? "!"
654
+ : "%20Phase%202%20Trusted%20Setup%20ceremony!"}%20You%20can%20view%20the%20steps%20to%20contribute%20here:%20https://ceremony.pse.dev%20You%20can%20view%20my%20attestation%20here:%20${gistUrl}%20#Ethereum%20#ZKP`;
655
+ };
586
656
  /**
587
657
  * Return a custom progress bar.
588
658
  * @param type <ProgressBarType> - the type of the progress bar.
@@ -710,13 +780,14 @@ const getLatestUpdatesFromParticipant = async (firestoreDatabase, ceremonyId, pa
710
780
  * @param entropyOrBeaconHash <string> - the entropy or beacon hash (only when finalizing) for the contribution.
711
781
  * @param contributorOrCoordinatorIdentifier <string> - the identifier of the contributor or coordinator (only when finalizing).
712
782
  * @param isFinalizing <boolean> - flag to discriminate between ceremony finalization (true) and contribution (false).
783
+ * @param circuitsLength <number> - the total number of circuits in the ceremony.
713
784
  */
714
- const handleStartOrResumeContribution = async (cloudFunctions, firestoreDatabase, ceremony, circuit, participant, entropyOrBeaconHash, contributorOrCoordinatorIdentifier, isFinalizing) => {
785
+ const handleStartOrResumeContribution = async (cloudFunctions, firestoreDatabase, ceremony, circuit, participant, entropyOrBeaconHash, contributorOrCoordinatorIdentifier, isFinalizing, circuitsLength) => {
715
786
  // Extract data.
716
787
  const { prefix: ceremonyPrefix } = ceremony.data;
717
788
  const { waitingQueue, avgTimings, prefix: circuitPrefix, sequencePosition } = circuit.data;
718
789
  const { completedContributions } = waitingQueue; // = current progress.
719
- console.log(`${theme.text.bold(`\n- Circuit # ${theme.colors.magenta(`${sequencePosition}`)}`)} (Contribution Steps)`);
790
+ console.log(`${theme.text.bold(`\n- Circuit # ${theme.colors.magenta(`${sequencePosition}/${circuitsLength}`)}`)} (Contribution Steps)`);
720
791
  // Get most up-to-date data from the participant document.
721
792
  let participantData = await getLatestUpdatesFromParticipant(firestoreDatabase, ceremony.id, participant.id);
722
793
  const spinner = customSpinner(`${participantData.contributionStep === "DOWNLOADING" /* ParticipantContributionStep.DOWNLOADING */
@@ -762,6 +833,7 @@ const handleStartOrResumeContribution = async (cloudFunctions, firestoreDatabase
762
833
  // Download the latest contribution from bucket.
763
834
  await downloadCeremonyArtifact(cloudFunctions, bucketName, lastZkeyStorageFilePath, lastZkeyLocalFilePath);
764
835
  console.log(`${theme.symbols.success} Contribution ${theme.text.bold(`#${lastZkeyIndex}`)} correctly downloaded`);
836
+ await sleep(3000);
765
837
  // Advance to next contribution step (COMPUTING) if not finalizing.
766
838
  if (!isFinalizing) {
767
839
  spinner.text = `Preparing for contribution computation...`;
@@ -784,16 +856,19 @@ const handleStartOrResumeContribution = async (cloudFunctions, firestoreDatabase
784
856
  spinner.start();
785
857
  // Read local transcript file info to get the contribution hash.
786
858
  const transcriptContents = readFile(transcriptLocalFilePath);
787
- const matchContributionHash = transcriptContents.match(/Contribution.+Hash.+\n\t\t.+\n\t\t.+\n.+\n\t\t.+\n/);
859
+ const matchContributionHash = transcriptContents.match(contribHashRegex);
788
860
  if (!matchContributionHash)
789
861
  showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_FINALIZE_NO_TRANSCRIPT_CONTRIBUTION_HASH_MATCH, true);
790
862
  // Format contribution hash.
791
863
  const contributionHash = matchContributionHash?.at(0)?.replace("\n\t\t", "");
864
+ await sleep(500);
792
865
  // Make request to cloud functions to permanently store the information.
793
866
  await permanentlyStoreCurrentContributionTimeAndHash(cloudFunctions, ceremony.id, computingTime, contributionHash);
794
867
  // Format computing time.
795
868
  const { seconds: computationSeconds, minutes: computationMinutes, hours: computationHours } = getSecondsMinutesHoursFromMillis(computingTime);
796
869
  spinner.succeed(`${isFinalizing ? "Contribution" : `Contribution ${theme.text.bold(`#${nextZkeyIndex}`)}`} computation took ${theme.text.bold(`${convertToDoubleDigits(computationHours)}:${convertToDoubleDigits(computationMinutes)}:${convertToDoubleDigits(computationSeconds)}`)}`);
870
+ // ensure the previous step is completed
871
+ await sleep(5000);
797
872
  // Advance to next contribution step (UPLOADING) if not finalizing.
798
873
  if (!isFinalizing) {
799
874
  spinner.text = `Preparing for uploading the contribution...`;
@@ -809,12 +884,17 @@ const handleStartOrResumeContribution = async (cloudFunctions, firestoreDatabase
809
884
  console.log(`${theme.symbols.success} Contribution ${theme.text.bold(`#${nextZkeyIndex}`)} already computed`);
810
885
  // Contribution step = UPLOADING.
811
886
  if (isFinalizing || participantData.contributionStep === "UPLOADING" /* ParticipantContributionStep.UPLOADING */) {
812
- spinner.text = `Uploading ${isFinalizing ? "final" : "your"} contribution ${!isFinalizing ? theme.text.bold(`#${nextZkeyIndex}`) : ""} to storage.\n${theme.symbols.warning} This step may take a while based on circuit size and your contribution speed. Everything's fine, just be patient.`;
887
+ spinner.text = `Uploading ${isFinalizing ? "final" : "your"} contribution ${!isFinalizing ? theme.text.bold(`#${nextZkeyIndex}`) : ""} to storage.\n${theme.symbols.warning} This step may take a while based on circuit size and your internet speed. Everything's fine, just be patient.`;
813
888
  spinner.start();
814
- if (!isFinalizing)
815
- await multiPartUpload(cloudFunctions, bucketName, nextZkeyStorageFilePath, nextZkeyLocalFilePath, Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB), ceremony.id, participantData.tempContributionData);
889
+ const progressBar = customProgressBar(ProgressBarType.UPLOAD, `your contribution`);
890
+ if (!isFinalizing) {
891
+ await multiPartUpload(cloudFunctions, bucketName, nextZkeyStorageFilePath, nextZkeyLocalFilePath, Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB), ceremony.id, participantData.tempContributionData, progressBar);
892
+ progressBar.stop();
893
+ }
816
894
  else
817
895
  await multiPartUpload(cloudFunctions, bucketName, nextZkeyStorageFilePath, nextZkeyLocalFilePath, Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB));
896
+ // small sleep to ensure the previous step is completed
897
+ await sleep(5000);
818
898
  spinner.succeed(`${isFinalizing ? `Contribution` : `Contribution ${theme.text.bold(`#${nextZkeyIndex}`)}`} correctly saved to storage`);
819
899
  // Advance to next contribution step (VERIFYING) if not finalizing.
820
900
  if (!isFinalizing) {
@@ -992,7 +1072,7 @@ const promptCircomCompiler = async () => {
992
1072
  * Shows a list of circuits for a single option selection.
993
1073
  * @dev the circuit names are derived from local R1CS files.
994
1074
  * @param options <Array<string>> - an array of circuits names.
995
- * @returns Promise<string> - the name of the choosen circuit.
1075
+ * @returns Promise<string> - the name of the chosen circuit.
996
1076
  */
997
1077
  const promptCircuitSelector = async (options) => {
998
1078
  const { circuitFilename } = await prompts({
@@ -1010,7 +1090,7 @@ const promptCircuitSelector = async (options) => {
1010
1090
  * Shows a list of standard EC2 VM instance types for a single option selection.
1011
1091
  * @notice the suggested VM configuration type is calculated based on circuit constraint size.
1012
1092
  * @param constraintSize <number> - the amount of circuit constraints
1013
- * @returns Promise<string> - the name of the choosen VM type.
1093
+ * @returns Promise<string> - the name of the chosen VM type.
1014
1094
  */
1015
1095
  const promptVMTypeSelector = async (constraintSize) => {
1016
1096
  let suggestedConfiguration = 0;
@@ -1107,7 +1187,7 @@ const promptVMDiskTypeSelector = async () => {
1107
1187
  /**
1108
1188
  * Show a series of questions about the circuits.
1109
1189
  * @param constraintSize <number> - the amount of circuit constraints.
1110
- * @param timeoutMechanismType <CeremonyTimeoutType> - the choosen timeout mechanism type for the ceremony.
1190
+ * @param timeoutMechanismType <CeremonyTimeoutType> - the chosen timeout mechanism type for the ceremony.
1111
1191
  * @param needPromptCircomCompiler <boolean> - a boolean value indicating if the questions related to the Circom compiler version and commit hash must be asked.
1112
1192
  * @param enforceVM <boolean> - a boolean value indicating if the contribution verification could be supported by VM-only approach or not.
1113
1193
  * @returns Promise<Array<Circuit>> - circuit info prompted by the coordinator.
@@ -1120,7 +1200,7 @@ const promptCircuitInputData = async (constraintSize, timeoutMechanismType, same
1120
1200
  let circomVersion = "";
1121
1201
  let circomCommitHash = "";
1122
1202
  let circuitInputData;
1123
- let useCfOrVm;
1203
+ let cfOrVm;
1124
1204
  let vmDiskType;
1125
1205
  let vmConfigurationType = "";
1126
1206
  const questions = [
@@ -1175,18 +1255,21 @@ const promptCircuitInputData = async (constraintSize, timeoutMechanismType, same
1175
1255
  circomVersion = version;
1176
1256
  circomCommitHash = commitHash;
1177
1257
  }
1178
- // Ask for prefered contribution verification method (CF vs VM).
1258
+ // Ask for preferred contribution verification method (CF vs VM).
1179
1259
  if (!enforceVM) {
1180
1260
  const { confirmation } = await askForConfirmation(`The contribution verification can be performed using Cloud Functions (CF, cheaper for small contributions but limited to 1M constraints) or custom virtual machines (expensive but could scale up to 30M constraints). Be aware about VM costs and if you wanna learn more, please visit the documentation to have a complete overview about cost estimation of the two mechanisms.\nChoose the contribution verification mechanism`, `CF`, // eq. true.
1181
1261
  `VM` // eq. false.
1182
1262
  );
1183
- useCfOrVm = confirmation;
1263
+ cfOrVm = confirmation
1264
+ ? "CF" /* CircuitContributionVerificationMechanism.CF */
1265
+ : "VM" /* CircuitContributionVerificationMechanism.VM */;
1184
1266
  }
1185
- else
1186
- useCfOrVm = "VM" /* CircuitContributionVerificationMechanism.VM */;
1187
- if (useCfOrVm === undefined)
1267
+ else {
1268
+ cfOrVm = "VM" /* CircuitContributionVerificationMechanism.VM */;
1269
+ }
1270
+ if (cfOrVm === undefined)
1188
1271
  showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
1189
- if (!useCfOrVm) {
1272
+ if (cfOrVm === "VM" /* CircuitContributionVerificationMechanism.VM */) {
1190
1273
  // Ask for selecting the specific VM configuration type.
1191
1274
  vmConfigurationType = await promptVMTypeSelector(constraintSize);
1192
1275
  // Ask for selecting the specific VM disk (volume) type.
@@ -1220,9 +1303,7 @@ const promptCircuitInputData = async (constraintSize, timeoutMechanismType, same
1220
1303
  paramsConfiguration: circuitConfigurationValues
1221
1304
  },
1222
1305
  verification: {
1223
- cfOrVm: useCfOrVm
1224
- ? "CF" /* CircuitContributionVerificationMechanism.CF */
1225
- : "VM" /* CircuitContributionVerificationMechanism.VM */,
1306
+ cfOrVm,
1226
1307
  vm: {
1227
1308
  vmConfigurationType,
1228
1309
  vmDiskType
@@ -1258,9 +1339,7 @@ const promptCircuitInputData = async (constraintSize, timeoutMechanismType, same
1258
1339
  paramsConfiguration: circuitConfigurationValues
1259
1340
  },
1260
1341
  verification: {
1261
- cfOrVm: useCfOrVm
1262
- ? "CF" /* CircuitContributionVerificationMechanism.CF */
1263
- : "VM" /* CircuitContributionVerificationMechanism.VM */,
1342
+ cfOrVm,
1264
1343
  vm: {
1265
1344
  vmConfigurationType,
1266
1345
  vmDiskType
@@ -1304,7 +1383,7 @@ const promptCircuitAddition = async () => {
1304
1383
  * Shows a list of pre-computed zKeys for a single option selection.
1305
1384
  * @dev the names are derived from local zKeys files.
1306
1385
  * @param options <Array<string>> - an array of pre-computed zKeys names.
1307
- * @returns Promise<string> - the name of the choosen pre-computed zKey.
1386
+ * @returns Promise<string> - the name of the chosen pre-computed zKey.
1308
1387
  */
1309
1388
  const promptPreComputedZkeySelector = async (options) => {
1310
1389
  const { preComputedZkeyFilename } = await prompts({
@@ -1342,13 +1421,13 @@ const promptNeededPowersForCircuit = async (suggestedSmallestNeededPowers) => {
1342
1421
  * Shows a list of PoT files for a single option selection.
1343
1422
  * @dev the names are derived from local PoT files.
1344
1423
  * @param options <Array<string>> - an array of PoT file names.
1345
- * @returns Promise<string> - the name of the choosen PoT.
1424
+ * @returns Promise<string> - the name of the chosen PoT.
1346
1425
  */
1347
1426
  const promptPotSelector = async (options) => {
1348
1427
  const { potFilename } = await prompts({
1349
1428
  type: "select",
1350
1429
  name: "potFilename",
1351
- message: theme.text.bold("Select the Powers of Tau file choosen for the circuit"),
1430
+ message: theme.text.bold("Select the Powers of Tau file chosen for the circuit"),
1352
1431
  choices: options.map((option) => {
1353
1432
  console.log(option);
1354
1433
  return { title: option, value: option };
@@ -1366,7 +1445,7 @@ const promptPotSelector = async (options) => {
1366
1445
  * @param isFinalizing <boolean> - true when the coordinator must select a ceremony for finalization; otherwise false (participant selects a ceremony for contribution).
1367
1446
  * @returns Promise<FirebaseDocumentInfo> - the Firestore document of the selected ceremony.
1368
1447
  */
1369
- const promptForCeremonySelection = async (ceremoniesDocuments, isFinalizing) => {
1448
+ const promptForCeremonySelection = async (ceremoniesDocuments, isFinalizing, messageToDisplay) => {
1370
1449
  // Prepare state.
1371
1450
  const choices = [];
1372
1451
  // Prepare choices x ceremony.
@@ -1384,9 +1463,7 @@ const promptForCeremonySelection = async (ceremoniesDocuments, isFinalizing) =>
1384
1463
  const { ceremony } = await prompts({
1385
1464
  type: "select",
1386
1465
  name: "ceremony",
1387
- message: theme.text.bold(!isFinalizing
1388
- ? "Which ceremony would you like to contribute to?"
1389
- : "Which ceremony would you like to finalize?"),
1466
+ message: theme.text.bold(messageToDisplay),
1390
1467
  choices,
1391
1468
  initial: 0
1392
1469
  });
@@ -1418,7 +1495,7 @@ const promptToTypeEntropyOrBeacon = async (isEntropy = true) => {
1418
1495
  * @return <Promise<string>> - the entropy.
1419
1496
  */
1420
1497
  const promptForEntropy = async () => {
1421
- // Prompt for entropy generation prefered method.
1498
+ // Prompt for entropy generation preferred method.
1422
1499
  const { confirmation } = await askForConfirmation(`Do you prefer to type your entropy or generate it randomly?`, "Manually", "Randomly");
1423
1500
  if (confirmation === undefined)
1424
1501
  showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
@@ -1537,16 +1614,37 @@ const checkAuth = async (firebaseApp) => {
1537
1614
  showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_NOT_AUTHENTICATED, true);
1538
1615
  // Retrieve local access token.
1539
1616
  const token = String(getLocalAccessToken());
1540
- // Get credentials.
1541
- const credentials = exchangeGithubTokenForCredentials(token);
1542
- // Sign in to Firebase using credentials.
1543
- await signInToFirebase(firebaseApp, credentials);
1617
+ let providerUserId;
1618
+ let username;
1619
+ const authMethod = getLocalAuthMethod();
1620
+ switch (authMethod) {
1621
+ case "github": {
1622
+ // Get credentials.
1623
+ const credentials = exchangeGithubTokenForCredentials(token);
1624
+ // Sign in to Firebase using credentials.
1625
+ await signInToFirebase(firebaseApp, credentials);
1626
+ // Get Github unique identifier (handle-id).
1627
+ providerUserId = await getGithubProviderUserId(String(token));
1628
+ username = getUserHandleFromProviderUserId(providerUserId);
1629
+ break;
1630
+ }
1631
+ case "bandada": {
1632
+ const userCredentials = await signInWithCustomToken(getAuth(), token);
1633
+ providerUserId = userCredentials.user.uid;
1634
+ username = providerUserId;
1635
+ break;
1636
+ }
1637
+ case "siwe": {
1638
+ const userCredentials = await signInWithCustomToken(getAuth(), token);
1639
+ providerUserId = userCredentials.user.uid;
1640
+ username = providerUserId;
1641
+ break;
1642
+ }
1643
+ }
1544
1644
  // Get current authenticated user.
1545
1645
  const user = getCurrentFirebaseAuthUser(firebaseApp);
1546
- // Get Github unique identifier (handle-id).
1547
- const providerUserId = await getGithubProviderUserId(String(token));
1548
1646
  // Greet the user.
1549
- console.log(`Greetings, @${theme.text.bold(getUserHandleFromProviderUserId(providerUserId))} ${theme.emojis.wave}\n`);
1647
+ console.log(`Greetings, @${theme.text.bold(username)} ${theme.emojis.wave}\n`);
1550
1648
  return {
1551
1649
  user,
1552
1650
  token,
@@ -1635,7 +1733,7 @@ const handleAdditionOfCircuitsToCeremony = async (r1csOptions, wasmOptions, cere
1635
1733
  wasmFilename.split(`.${commonTerms.foldersAndPathsTerms.wasm}`)[0]);
1636
1734
  if (matchingWasms.length !== 1)
1637
1735
  showError(COMMAND_ERRORS.COMMAND_SETUP_MISMATCH_R1CS_WASM, true);
1638
- // Get input data for choosen circuit.
1736
+ // Get input data for chosen circuit.
1639
1737
  const circuitInputData = await getInputDataToAddCircuitToCeremony(choosenCircuitFilename, matchingWasms[0], ceremonyTimeoutMechanismType, sameCircomCompiler, circuitSequencePosition, sharedCircomCompilerData);
1640
1738
  // Store circuit data.
1641
1739
  inputDataForCircuits.push(circuitInputData);
@@ -1688,7 +1786,7 @@ const displayCeremonySummary = (ceremonyInputData, circuits) => {
1688
1786
  };
1689
1787
  /**
1690
1788
  * Check if the smallest Powers of Tau has already been downloaded/stored in the correspondent local path
1691
- * @dev we are downloading the Powers of Tau file from Hermez Cryptography Phase 1 Trusted Setup.
1789
+ * @dev we are downloading the Powers of Tau file from Perpetual Powers of Tau Phase 1 Trusted Setup.
1692
1790
  * @param powers <string> - the smallest amount of powers needed for the given circuit (should be in a 'XY' stringified form).
1693
1791
  * @param ptauCompleteFilename <string> - the complete file name of the powers of tau file to be downloaded.
1694
1792
  * @returns <Promise<void>>
@@ -1702,7 +1800,7 @@ const checkAndDownloadSmallestPowersOfTau = async (powers, ptauCompleteFilename)
1702
1800
  .map((dirent) => dirent.name);
1703
1801
  // Check if already downloaded or not.
1704
1802
  if (smallestPtauFileForGivenPowers.length === 0) {
1705
- const spinner = customSpinner(`Downloading the ${theme.text.bold(`#${powers}`)} smallest PoT file needed from the Hermez Cryptography Phase 1 Trusted Setup...`, `clock`);
1803
+ const spinner = customSpinner(`Downloading the ${theme.text.bold(`#${powers}`)} smallest PoT file needed from the Perpetual Powers of Tau Phase 1 Trusted Setup...`, `clock`);
1706
1804
  spinner.start();
1707
1805
  // Download smallest Powers of Tau file from remote server.
1708
1806
  const streamPipeline = promisify(pipeline);
@@ -1725,7 +1823,7 @@ const checkAndDownloadSmallestPowersOfTau = async (powers, ptauCompleteFilename)
1725
1823
  * number of powers greater than or equal to the powers needed by the zKey), the coordinator will be asked
1726
1824
  * to provide a number of powers manually, ranging from the smallest possible to the largest.
1727
1825
  * @param neededPowers <number> - the smallest amount of powers needed by the zKey.
1728
- * @returns Promise<string, string> - the information about the choosen Powers of Tau file for the pre-computed zKey
1826
+ * @returns Promise<string, string> - the information about the chosen Powers of Tau file for the pre-computed zKey
1729
1827
  * along with related powers.
1730
1828
  */
1731
1829
  const handlePreComputedZkeyPowersOfTauSelection = async (neededPowers) => {
@@ -1817,7 +1915,7 @@ const handleCircuitArtifactUploadToStorage = async (firebaseFunctions, bucketNam
1817
1915
  * @notice The setup command allows the coordinator of the ceremony to prepare the next ceremony by interacting with the CLI.
1818
1916
  * @dev For proper execution, the command must be run in a folder containing the R1CS files related to the circuits
1819
1917
  * for which the coordinator wants to create the ceremony. The command will download the necessary Tau powers
1820
- * from Hermez's ceremony Phase 1 Reliable Setup Ceremony.
1918
+ * from PPoT ceremony Phase 1 Setup Ceremony.
1821
1919
  * @param cmd? <any> - the path to the ceremony setup file.
1822
1920
  */
1823
1921
  const setup = async (cmd) => {
@@ -1826,7 +1924,9 @@ const setup = async (cmd) => {
1826
1924
  let ceremonyId = ""; // The unique identifier of the ceremony.
1827
1925
  const { firebaseApp, firebaseFunctions, firestoreDatabase } = await bootstrapCommandExecutionAndServices();
1828
1926
  // Check for authentication.
1829
- const { user, providerUserId } = cmd.auth ? await authWithToken(firebaseApp, cmd.auth) : await checkAuth(firebaseApp);
1927
+ const { user, providerUserId } = cmd.auth
1928
+ ? await authWithToken(firebaseApp, cmd.auth)
1929
+ : await checkAuth(firebaseApp);
1830
1930
  // Preserve command execution only for coordinators.
1831
1931
  if (!(await isCoordinator(user)))
1832
1932
  showError(COMMAND_ERRORS.COMMAND_NOT_COORDINATOR, true);
@@ -1843,7 +1943,7 @@ const setup = async (cmd) => {
1843
1943
  // if there is the file option, then set up the non interactively
1844
1944
  if (cmd.template) {
1845
1945
  // 1. parse the file
1846
- // tmp data - do not cleanup files as we need them
1946
+ // tmp data - do not cleanup files as we need them
1847
1947
  const spinner = customSpinner(`Parsing ${theme.text.bold(cmd.template)} setup configuration file...`, `clock`);
1848
1948
  spinner.start();
1849
1949
  const setupCeremonyData = await parseCeremonyFile(cmd.template);
@@ -1853,8 +1953,6 @@ const setup = async (cmd) => {
1853
1953
  // create a new bucket
1854
1954
  const bucketName = await handleCeremonyBucketCreation(firebaseFunctions, ceremonySetupData.ceremonyPrefix);
1855
1955
  console.log(`\n${theme.symbols.success} Ceremony bucket name: ${theme.text.bold(bucketName)}`);
1856
- // create S3 clienbt
1857
- const s3 = new S3Client({ region: 'us-east-1' });
1858
1956
  // loop through each circuit
1859
1957
  for await (const circuit of setupCeremonyData.circuits) {
1860
1958
  // Local paths.
@@ -1864,25 +1962,24 @@ const setup = async (cmd) => {
1864
1962
  const potLocalPathAndFileName = getPotLocalFilePath(circuit.files.potFilename);
1865
1963
  const zkeyLocalPathAndFileName = getZkeyLocalFilePath(circuit.files.initialZkeyFilename);
1866
1964
  // 2. download the pot and wasm files
1867
- const streamPipeline = promisify(pipeline);
1868
1965
  await checkAndDownloadSmallestPowersOfTau(convertToDoubleDigits(circuit.metadata?.pot), circuit.files.potFilename);
1869
- // download the wasm to calculate the hash
1870
- const spinner = customSpinner(`Downloading the ${theme.text.bold(`#${circuit.name}`)} WASM file from the project's bucket...`, `clock`);
1966
+ // 3. generate the zKey
1967
+ const spinner = customSpinner(`Generating genesis zKey for circuit ${theme.text.bold(circuit.name)}...`, `clock`);
1871
1968
  spinner.start();
1872
- const command = new GetObjectCommand({ Bucket: ceremonySetupData.circuitArtifacts[index].artifacts.bucket, Key: ceremonySetupData.circuitArtifacts[index].artifacts.wasmStoragePath });
1873
- const response = await s3.send(command);
1874
- if (response.$metadata.httpStatusCode !== 200) {
1875
- throw new Error("There was an error while trying to download the wasm file. Please check that the file has the correct permissions (public) set.");
1969
+ if (existsSync(zkeyLocalPathAndFileName)) {
1970
+ spinner.succeed(`The genesis zKey for circuit ${theme.text.bold(circuit.name)} is already present on disk`);
1876
1971
  }
1877
- if (response.Body instanceof Readable)
1878
- await streamPipeline(response.Body, createWriteStream(wasmLocalPathAndFileName));
1879
- spinner.stop();
1880
- // 3. generate the zKey
1881
- await zKey.newZKey(r1csLocalPathAndFileName, getPotLocalFilePath(circuit.files.potFilename), zkeyLocalPathAndFileName, undefined);
1972
+ else {
1973
+ await zKey.newZKey(r1csLocalPathAndFileName, getPotLocalFilePath(circuit.files.potFilename), zkeyLocalPathAndFileName, undefined);
1974
+ spinner.succeed(`Generation of the genesis zKey for circuit ${theme.text.bold(circuit.name)} completed successfully`);
1975
+ }
1976
+ const hashSpinner = customSpinner(`Calculating hashes for circuit ${theme.text.bold(circuit.name)}...`, `clock`);
1977
+ hashSpinner.start();
1882
1978
  // 4. calculate the hashes
1883
1979
  const wasmBlake2bHash = await blake512FromPath(wasmLocalPathAndFileName);
1884
1980
  const potBlake2bHash = await blake512FromPath(getPotLocalFilePath(circuit.files.potFilename));
1885
1981
  const initialZkeyBlake2bHash = await blake512FromPath(zkeyLocalPathAndFileName);
1982
+ hashSpinner.succeed(`Hashes for circuit ${theme.text.bold(circuit.name)} calculated successfully`);
1886
1983
  // 5. upload the artifacts
1887
1984
  // Upload zKey to Storage.
1888
1985
  await handleCircuitArtifactUploadToStorage(firebaseFunctions, bucketName, circuit.files.initialZkeyStoragePath, zkeyLocalPathAndFileName, circuit.files.initialZkeyFilename);
@@ -1900,9 +1997,9 @@ const setup = async (cmd) => {
1900
1997
  // 6 update the setup data object
1901
1998
  ceremonySetupData.circuits[index].files = {
1902
1999
  ...circuit.files,
1903
- potBlake2bHash: potBlake2bHash,
1904
- wasmBlake2bHash: wasmBlake2bHash,
1905
- initialZkeyBlake2bHash: initialZkeyBlake2bHash
2000
+ potBlake2bHash,
2001
+ wasmBlake2bHash,
2002
+ initialZkeyBlake2bHash
1906
2003
  };
1907
2004
  ceremonySetupData.circuits[index].zKeySizeInBytes = getFileStats(zkeyLocalPathAndFileName).size;
1908
2005
  }
@@ -2110,17 +2207,29 @@ const expirationCountdownForGithubOAuth = (expirationInSeconds) => {
2110
2207
  */
2111
2208
  const onVerification = async (verification) => {
2112
2209
  // Copy code to clipboard.
2113
- clipboard.writeSync(verification.user_code);
2114
- clipboard.readSync();
2210
+ let noClipboard = false;
2211
+ try {
2212
+ clipboard.writeSync(verification.user_code);
2213
+ clipboard.readSync();
2214
+ }
2215
+ catch (error) {
2216
+ noClipboard = true;
2217
+ }
2115
2218
  // Display data.
2116
2219
  console.log(`${theme.symbols.warning} Visit ${theme.text.bold(theme.text.underlined(verification.verification_uri))} on this device to generate a new token and authenticate\n`);
2117
- console.log(theme.colors.magenta(figlet.textSync(verification.user_code, { font: "ANSI Shadow" })), '\n');
2118
- console.log(`${theme.symbols.info} Your auth code: ${theme.text.bold(verification.user_code)} has been copied to your clipboard (${theme.emojis.clipboard} ${theme.symbols.success})\n`);
2220
+ console.log(theme.colors.magenta(figlet.textSync("Code is Below", { font: "ANSI Shadow" })), "\n");
2221
+ const message = !noClipboard ? `has been copied to your clipboard (${theme.emojis.clipboard})` : ``;
2222
+ console.log(`${theme.symbols.info} Your auth code: ${theme.text.bold(verification.user_code)} ${message} ${theme.symbols.success}\n`);
2119
2223
  const spinner = customSpinner(`Redirecting to Github...`, `clock`);
2120
2224
  spinner.start();
2121
2225
  await sleep(10000); // ~10s to make users able to read the CLI.
2122
- // Automatically open the page (# Step 2).
2123
- await open(verification.verification_uri);
2226
+ try {
2227
+ // Automatically open the page (# Step 2).
2228
+ await open(verification.verification_uri);
2229
+ }
2230
+ catch (error) {
2231
+ console.log(`${theme.symbols.info} Please authenticate via GitHub at ${verification.verification_uri}`);
2232
+ }
2124
2233
  spinner.stop();
2125
2234
  // Countdown for time expiration.
2126
2235
  expirationCountdownForGithubOAuth(verification.expires_in);
@@ -2171,6 +2280,7 @@ const auth = async () => {
2171
2280
  // Generate a new access token using Github Device Flow (OAuth 2.0).
2172
2281
  const newToken = await executeGithubDeviceFlow(String(process.env.AUTH_GITHUB_CLIENT_ID));
2173
2282
  // Store the new access token.
2283
+ setLocalAuthMethod("github");
2174
2284
  setLocalAccessToken(newToken);
2175
2285
  }
2176
2286
  else
@@ -2191,6 +2301,249 @@ const auth = async () => {
2191
2301
  terminate(providerUserId);
2192
2302
  };
2193
2303
 
2304
+ const { BANDADA_API_URL } = process.env;
2305
+ const bandadaApi = new ApiSdk(BANDADA_API_URL);
2306
+ const addMemberToGroup = async (groupId, dashboardUrl, identity) => {
2307
+ const commitment = identity.commitment.toString();
2308
+ const group = await bandadaApi.getGroup(groupId);
2309
+ const providerName = group.credentials.id.split("_")[0].toLowerCase();
2310
+ // 6. open a new window with the url:
2311
+ const url = `${dashboardUrl}credentials?group=${groupId}&member=${commitment}&provider=${providerName}`;
2312
+ console.log(`${theme.text.bold(`Verification URL:`)} ${theme.text.underlined(url)}`);
2313
+ open(url);
2314
+ const { confirmation } = await askForConfirmation("Did you join the Bandada group in the browser?");
2315
+ if (!confirmation)
2316
+ showError("You must join the Bandada group to continue the login process", true);
2317
+ };
2318
+ const isGroupMember = async (groupId, identity) => {
2319
+ const commitment = identity.commitment.toString();
2320
+ const isMember = await bandadaApi.isGroupMember(groupId, commitment);
2321
+ return isMember;
2322
+ };
2323
+
2324
+ const { BANDADA_DASHBOARD_URL, BANDADA_GROUP_ID } = process.env;
2325
+ const authBandada = async () => {
2326
+ try {
2327
+ const { firebaseFunctions } = await bootstrapCommandExecutionAndServices();
2328
+ const spinner = customSpinner(`Checking identity string for Semaphore...`, `clock`);
2329
+ spinner.start();
2330
+ // 1. check if _identity string exists in local storage
2331
+ let identityString;
2332
+ const isIdentityStringStored = checkLocalBandadaIdentity();
2333
+ if (isIdentityStringStored) {
2334
+ identityString = getLocalBandadaIdentity();
2335
+ spinner.succeed(`Identity seed found\n`);
2336
+ }
2337
+ else {
2338
+ spinner.warn(`Identity seed not found\n`);
2339
+ // 2. generate a random _identity string and save it in local storage
2340
+ const { seed } = await prompts({
2341
+ type: "text",
2342
+ name: "seed",
2343
+ message: theme.text.bold(`Enter a secret string to use as your identity seed in Semaphore:`),
2344
+ initial: false
2345
+ });
2346
+ identityString = seed;
2347
+ setLocalBandadaIdentity(identityString);
2348
+ }
2349
+ // 3. create a semaphore identity with _identity string as a seed
2350
+ const identity = new Identity(identityString);
2351
+ // 4. check if the user is a member of the group
2352
+ console.log(`Checking Bandada membership...`);
2353
+ const isMember = await isGroupMember(BANDADA_GROUP_ID, identity);
2354
+ if (!isMember) {
2355
+ await addMemberToGroup(BANDADA_GROUP_ID, BANDADA_DASHBOARD_URL, identity);
2356
+ }
2357
+ // 5. generate a proof that the user owns the commitment.
2358
+ spinner.text = `Generating proof of identity...`;
2359
+ spinner.start();
2360
+ // publicSignals = [hash(externalNullifier, identityNullifier), commitment]
2361
+ const initDirectoryName = getLocalDirname();
2362
+ const directoryName = initDirectoryName.includes("/src") ? "." : initDirectoryName;
2363
+ const { proof, publicSignals } = await groth16.fullProve({
2364
+ identityTrapdoor: identity.trapdoor,
2365
+ identityNullifier: identity.nullifier,
2366
+ externalNullifier: BANDADA_GROUP_ID
2367
+ }, `${directoryName}/public/mini-semaphore.wasm`, `${directoryName}/public/mini-semaphore.zkey`);
2368
+ spinner.succeed(`Proof generated.\n`);
2369
+ spinner.text = `Sending proof to verification...`;
2370
+ spinner.start();
2371
+ // 6. send proof to a cloud function that verifies it and checks membership
2372
+ const cf = httpsCallable(firebaseFunctions, commonTerms.cloudFunctionsNames.bandadaValidateProof);
2373
+ const result = await cf({
2374
+ proof,
2375
+ publicSignals
2376
+ });
2377
+ const { valid, token, message } = result.data;
2378
+ if (!valid) {
2379
+ showError(message, true);
2380
+ deleteLocalAuthMethod();
2381
+ deleteLocalAccessToken();
2382
+ deleteLocalBandadaIdentity();
2383
+ }
2384
+ spinner.succeed(`Proof verified.\n`);
2385
+ spinner.text = `Authenticating...`;
2386
+ spinner.start();
2387
+ // 7. Auth to p0tion firebase
2388
+ const credentials = await signInWithCustomToken(getAuth(), token);
2389
+ setLocalAuthMethod("bandada");
2390
+ setLocalAccessToken(token);
2391
+ spinner.succeed(`Authenticated as ${theme.text.bold(credentials.user.uid)}.`);
2392
+ console.log(`\n${theme.symbols.warning} You can always log out by running the ${theme.text.bold(`phase2cli logout`)} command`);
2393
+ }
2394
+ catch (error) {
2395
+ // Delete local token.
2396
+ console.log("An error crashed the process. Deleting local token and identity.");
2397
+ console.error(error);
2398
+ deleteLocalAuthMethod();
2399
+ deleteLocalAccessToken();
2400
+ deleteLocalBandadaIdentity();
2401
+ }
2402
+ process.exit(0);
2403
+ };
2404
+
2405
+ const showVerificationCodeAndUri = async (OAuthDeviceCode) => {
2406
+ // Copy code to clipboard.
2407
+ let noClipboard = false;
2408
+ try {
2409
+ clipboard.writeSync(OAuthDeviceCode.user_code);
2410
+ clipboard.readSync();
2411
+ }
2412
+ catch (error) {
2413
+ noClipboard = true;
2414
+ }
2415
+ // Display data.
2416
+ console.log(`${theme.symbols.warning} Visit ${theme.text.bold(theme.text.underlined(OAuthDeviceCode.verification_uri_complete))} on this device to generate a new token and authenticate\n`);
2417
+ console.log(theme.colors.magenta(figlet.textSync("Code is Below", { font: "ANSI Shadow" })), "\n");
2418
+ const message = !noClipboard ? `has been copied to your clipboard (${theme.emojis.clipboard})` : ``;
2419
+ console.log(`${theme.symbols.info} Your auth code: ${theme.text.bold(OAuthDeviceCode.user_code)} ${message} ${theme.symbols.success}\n`);
2420
+ const spinner = customSpinner(`Redirecting to Sign In With Ethereum...`, `clock`);
2421
+ spinner.start();
2422
+ await sleep(10000); // ~10s to make users able to read the CLI.
2423
+ try {
2424
+ // Automatically open the page (# Step 2).
2425
+ await open(OAuthDeviceCode.verification_uri_complete);
2426
+ }
2427
+ catch (error) {
2428
+ console.log(`${theme.symbols.info} Please authenticate via SIWE at ${OAuthDeviceCode.verification_uri_complete}`);
2429
+ }
2430
+ spinner.stop();
2431
+ };
2432
+ /**
2433
+ * Return the token to sign in to Firebase after passing the SIWE Device Flow
2434
+ * @param clientId <string> - The client id of the Auth0 application.
2435
+ * @param firebaseFunctions <any> - The Firebase functions instance to call the cloud function
2436
+ * @returns <string> - The token to sign in to Firebase
2437
+ */
2438
+ const executeSIWEDeviceFlow = async (clientId, firebaseFunctions) => {
2439
+ // Call Auth0 endpoint to request device code uri
2440
+ const OAuthDeviceCode = (await fetch$1(`${process.env.AUTH0_APPLICATION_URL}/oauth/device/code`, {
2441
+ method: "POST",
2442
+ headers: { "content-type": "application/json" },
2443
+ body: JSON.stringify({
2444
+ client_id: clientId,
2445
+ scope: "openid",
2446
+ audience: `${process.env.AUTH0_APPLICATION_URL}/api/v2/`
2447
+ })
2448
+ }).then((_res) => _res.json()));
2449
+ if (OAuthDeviceCode.error) {
2450
+ showError(OAuthDeviceCode.error_description, true);
2451
+ deleteLocalAuthMethod();
2452
+ deleteLocalAccessToken();
2453
+ }
2454
+ await showVerificationCodeAndUri(OAuthDeviceCode);
2455
+ // Poll Auth0 endpoint until you get token or request expires
2456
+ let isSignedIn = false;
2457
+ let isExpired = false;
2458
+ let auth0Token = "";
2459
+ while (!isSignedIn && !isExpired) {
2460
+ // Call Auth0 endpoint to request token
2461
+ const OAuthToken = (await fetch$1(`${process.env.AUTH0_APPLICATION_URL}/oauth/token`, {
2462
+ method: "POST",
2463
+ headers: { "content-type": "application/json" },
2464
+ body: JSON.stringify({
2465
+ client_id: clientId,
2466
+ device_code: OAuthDeviceCode.device_code,
2467
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
2468
+ })
2469
+ }).then((_res) => _res.json()));
2470
+ if (OAuthToken.error) {
2471
+ if (OAuthToken.error === "authorization_pending") {
2472
+ // Wait for the user to sign in
2473
+ await sleep(OAuthDeviceCode.interval * 1000);
2474
+ }
2475
+ else if (OAuthToken.error === "slow_down") {
2476
+ // Wait for the user to sign in
2477
+ await sleep(OAuthDeviceCode.interval * 1000 * 2);
2478
+ }
2479
+ else if (OAuthToken.error === "expired_token") {
2480
+ // The user didn't sign in on time
2481
+ isExpired = true;
2482
+ }
2483
+ }
2484
+ else {
2485
+ // The user signed in
2486
+ isSignedIn = true;
2487
+ auth0Token = OAuthToken.access_token;
2488
+ }
2489
+ }
2490
+ // Send token to cloud function to check nonce, create user and retrieve token
2491
+ const cf = httpsCallable(firebaseFunctions, commonTerms.cloudFunctionsNames.checkNonceOfSIWEAddress);
2492
+ const result = await cf({
2493
+ auth0Token
2494
+ });
2495
+ const { token, valid, message } = result.data;
2496
+ if (!valid) {
2497
+ showError(message, true);
2498
+ deleteLocalAuthMethod();
2499
+ deleteLocalAccessToken();
2500
+ }
2501
+ return token;
2502
+ };
2503
+ /**
2504
+ * Auth command using Sign In With Ethereum
2505
+ * @notice The auth command allows a user to make the association of their Ethereum account with the CLI by leveraging SIWE as an authentication mechanism.
2506
+ * @dev Under the hood, the command handles a manual Device Flow following the guidelines in the SIWE documentation.
2507
+ */
2508
+ const authSIWE = async () => {
2509
+ try {
2510
+ const { firebaseFunctions } = await bootstrapCommandExecutionAndServices();
2511
+ // Console more context for the user.
2512
+ console.log(`${theme.symbols.info} ${theme.text.bold(`You are about to authenticate on this CLI using your Ethereum address (device flow - OAuth 2.0 mechanism).\n${theme.symbols.warning} Please, note that only a Sign-in With Ethereum signature will be required`)}\n`);
2513
+ const spinner = customSpinner(`Checking authentication token...`, `clock`);
2514
+ spinner.start();
2515
+ await sleep(5000);
2516
+ // Manage OAuth Github or SIWE token.
2517
+ const isLocalTokenStored = checkLocalAccessToken();
2518
+ if (!isLocalTokenStored) {
2519
+ spinner.fail(`No local authentication token found\n`);
2520
+ // Generate a new access token using Github Device Flow (OAuth 2.0).
2521
+ const newToken = await executeSIWEDeviceFlow(String(process.env.AUTH_SIWE_CLIENT_ID), firebaseFunctions);
2522
+ // Store the new access token.
2523
+ setLocalAuthMethod("siwe");
2524
+ setLocalAccessToken(newToken);
2525
+ }
2526
+ else
2527
+ spinner.succeed(`Local authentication token found\n`);
2528
+ // Get access token from local store.
2529
+ const token = String(getLocalAccessToken());
2530
+ spinner.text = `Authenticating...`;
2531
+ spinner.start();
2532
+ // Exchange token for credential.
2533
+ const credentials = await signInWithCustomToken(getAuth(), token);
2534
+ spinner.succeed(`Authenticated as ${theme.text.bold(credentials.user.uid)}.`);
2535
+ console.log(`\n${theme.symbols.warning} You can always log out by running the ${theme.text.bold(`phase2cli logout`)} command`);
2536
+ process.exit(0);
2537
+ }
2538
+ catch (error) {
2539
+ // Delete local token.
2540
+ console.log("An error crashed the process. Deleting local token and identity.");
2541
+ console.error(error);
2542
+ deleteLocalAuthMethod();
2543
+ deleteLocalAccessToken();
2544
+ }
2545
+ };
2546
+
2194
2547
  /**
2195
2548
  * Return the verification result for latest contribution.
2196
2549
  * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
@@ -2313,8 +2666,8 @@ const handleDiskSpaceRequirementForNextContribution = async (cloudFunctions, cer
2313
2666
  spinner.fail(`You may not have enough memory to calculate the contribution for the Circuit ${theme.colors.magenta(`${circuitSequencePosition}`)}.\n\n${theme.symbols.info} The required amount of disk space is ${contributionDiskSpaceRequirement < 0.01
2314
2667
  ? theme.text.bold(`< 0.01`)
2315
2668
  : theme.text.bold(contributionDiskSpaceRequirement)} GB but you only have ${participantFreeDiskSpace > 0 ? theme.text.bold(participantFreeDiskSpace.toFixed(2)) : theme.text.bold(0)} GB available memory \nThe estimate ${theme.text.bold("may not be 100% correct")} since is based on the aggregate free memory on your disks but some may not be detected!\n`);
2316
- const { confirmation } = await askForConfirmation(`Please, we kindly ask you to continue with the contribution if you have noticed the estimate is wrong and you have enough memory in your machine`, "Continue", "Exit");
2317
- wannaContributeOrHaveEnoughMemory = !!confirmation;
2669
+ const { confirmationEnoughMemory } = await askForConfirmation(`Please, we kindly ask you to continue with the contribution if you have noticed the estimate is wrong and you have enough memory in your machine`, "Continue", "Exit");
2670
+ wannaContributeOrHaveEnoughMemory = !!confirmationEnoughMemory;
2318
2671
  if (circuitSequencePosition > 1) {
2319
2672
  console.log(`${theme.symbols.info} Please note, you have time until ceremony ends to free up your memory and complete remaining contributions`);
2320
2673
  // Asks the contributor if their wants to terminate contributions for the ceremony.
@@ -2382,8 +2735,12 @@ const handlePublicAttestation = async (firestoreDatabase, circuits, ceremonyId,
2382
2735
  // Write public attestation locally.
2383
2736
  writeFile(getAttestationLocalFilePath(`${ceremonyPrefix}_${commonTerms.foldersAndPathsTerms.attestation}.log`), Buffer.from(publicAttestation));
2384
2737
  await sleep(1000); // workaround for file descriptor unexpected close.
2385
- const gistUrl = await publishGist(participantAccessToken, publicAttestation, ceremonyName, ceremonyPrefix);
2386
- console.log(`\n${theme.symbols.info} Your public attestation has been successfully posted as Github Gist (${theme.text.bold(theme.text.underlined(gistUrl))})`);
2738
+ let gistUrl = "";
2739
+ const isGithub = getLocalAuthMethod() === "github";
2740
+ if (isGithub) {
2741
+ gistUrl = await publishGist(participantAccessToken, publicAttestation, ceremonyName, ceremonyPrefix);
2742
+ console.log(`\n${theme.symbols.info} Your public attestation has been successfully posted as Github Gist (${theme.text.bold(theme.text.underlined(gistUrl))})`);
2743
+ }
2387
2744
  // Prepare a ready-to-share tweet.
2388
2745
  await handleTweetGeneration(ceremonyName, gistUrl);
2389
2746
  };
@@ -2436,6 +2793,7 @@ const listenToCeremonyCircuitDocumentChanges = (firestoreDatabase, ceremonyId, p
2436
2793
  }
2437
2794
  });
2438
2795
  };
2796
+ let contributionInProgress = false;
2439
2797
  /**
2440
2798
  * Listen to current authenticated participant document changes.
2441
2799
  * @dev this is the core business logic related to the execution of the contribute command.
@@ -2563,11 +2921,21 @@ const listenToParticipantDocumentChanges = async (firestoreDatabase, cloudFuncti
2563
2921
  (!noTemporaryContributionData && resumingWithSameTemporaryData);
2564
2922
  // Scenario (3.B).
2565
2923
  if (isCurrentContributor && hasResumableStep && startingOrResumingContribution) {
2924
+ if (contributionInProgress) {
2925
+ console.warn(`\n${theme.symbols.warning} Received instruction to start/resume contribution but contribution is already in progress...[skipping]`);
2926
+ return;
2927
+ }
2566
2928
  // Communicate resume / start of the contribution to participant.
2567
2929
  await simpleLoader(`${changedContributionStep === "DOWNLOADING" /* ParticipantContributionStep.DOWNLOADING */ ? `Starting` : `Resuming`} your contribution...`, `clock`, 3000);
2568
- // Start / Resume the contribution for the participant.
2569
- await handleStartOrResumeContribution(cloudFunctions, firestoreDatabase, ceremony, circuit, participant, entropy, providerUserId, false // not finalizing.
2570
- );
2930
+ try {
2931
+ contributionInProgress = true;
2932
+ // Start / Resume the contribution for the participant.
2933
+ await handleStartOrResumeContribution(cloudFunctions, firestoreDatabase, ceremony, circuit, participant, entropy, providerUserId, false, // not finalizing.
2934
+ circuits.length);
2935
+ }
2936
+ finally {
2937
+ contributionInProgress = false;
2938
+ }
2571
2939
  }
2572
2940
  // Scenario (3.A).
2573
2941
  else if (isWaitingForContribution)
@@ -2616,7 +2984,9 @@ const listenToParticipantDocumentChanges = async (firestoreDatabase, cloudFuncti
2616
2984
  // Get latest contribution verification result.
2617
2985
  await getLatestVerificationResult(firestoreDatabase, ceremony.id, circuit.id, participant.id);
2618
2986
  // Get next circuit for contribution.
2619
- const nextCircuit = timeoutExpired ? getCircuitBySequencePosition(circuits, changedContributionProgress) : getCircuitBySequencePosition(circuits, changedContributionProgress + 1);
2987
+ const nextCircuit = timeoutExpired
2988
+ ? getCircuitBySequencePosition(circuits, changedContributionProgress)
2989
+ : getCircuitBySequencePosition(circuits, changedContributionProgress + 1);
2620
2990
  // Check disk space requirements for participant.
2621
2991
  const wannaGenerateAttestation = await handleDiskSpaceRequirementForNextContribution(cloudFunctions, ceremony.id, nextCircuit.data.sequencePosition, nextCircuit.data.zKeySizeInBytes, timeoutExpired, providerUserId);
2622
2992
  // Check if the participant would like to generate a new attestation.
@@ -2654,11 +3024,12 @@ const listenToParticipantDocumentChanges = async (firestoreDatabase, cloudFuncti
2654
3024
  */
2655
3025
  const contribute = async (opt) => {
2656
3026
  const { firebaseApp, firebaseFunctions, firestoreDatabase } = await bootstrapCommandExecutionAndServices();
2657
- // Check for authentication.
2658
- const { user, providerUserId, token } = await checkAuth(firebaseApp);
2659
3027
  // Get options.
2660
3028
  const ceremonyOpt = opt.ceremony;
2661
3029
  const entropyOpt = opt.entropy;
3030
+ const { auth } = opt;
3031
+ // Check for authentication.
3032
+ const { user, providerUserId, token } = auth ? await authWithToken(firebaseApp, auth) : await checkAuth(firebaseApp);
2662
3033
  // Prepare data.
2663
3034
  let selectedCeremony;
2664
3035
  // Retrieve the opened ceremonies.
@@ -2684,7 +3055,7 @@ const contribute = async (opt) => {
2684
3055
  }
2685
3056
  else {
2686
3057
  // Prompt the user to select a ceremony from the opened ones.
2687
- selectedCeremony = await promptForCeremonySelection(ceremoniesOpenedForContributions, false);
3058
+ selectedCeremony = await promptForCeremonySelection(ceremoniesOpenedForContributions, false, "Which ceremony would you like to contribute to?");
2688
3059
  }
2689
3060
  // Get selected ceremony circuit(s) documents.
2690
3061
  const circuits = await getCeremonyCircuits(firestoreDatabase, selectedCeremony.id);
@@ -2694,7 +3065,7 @@ const contribute = async (opt) => {
2694
3065
  const userDoc = await getDocumentById(firestoreDatabase, commonTerms.collections.users.name, user.uid);
2695
3066
  const userData = userDoc.data();
2696
3067
  if (!userData) {
2697
- spinner.fail(`Unfortunately we could not find a user document with your information. This likely means that you did not pass the GitHub reputation checks and therefore are not elegible to contribute to any ceremony. Please contact the coordinator if you believe this to be an error.`);
3068
+ spinner.fail(`Unfortunately we could not find a user document with your information. This likely means that you did not pass the GitHub reputation checks and therefore are not eligible to contribute to any ceremony. If you believe you pass the requirements, it might be possible that your profile is private and we were not able to fetch your real statistics, in this case please consider making your profile public for the duration of the contribution. Please contact the coordinator if you believe this to be an error.`);
2698
3069
  process.exit(0);
2699
3070
  }
2700
3071
  // Check the user's current participant readiness for contribution status (eligible, already contributed, timed out).
@@ -2845,10 +3216,10 @@ const observe = async () => {
2845
3216
  // Preserve command execution only for coordinators].
2846
3217
  if (!(await isCoordinator(user)))
2847
3218
  showError(COMMAND_ERRORS.COMMAND_NOT_COORDINATOR, true);
2848
- // Get running cerimonies info (if any).
3219
+ // Get running ceremonies info (if any).
2849
3220
  const runningCeremoniesDocs = await getOpenedCeremonies(firestoreDatabase);
2850
3221
  // Ask to select a ceremony.
2851
- const ceremony = await promptForCeremonySelection(runningCeremoniesDocs, false);
3222
+ const ceremony = await promptForCeremonySelection(runningCeremoniesDocs, false, "Which ceremony would you like to observe?");
2852
3223
  console.log(`${logSymbols.info} Refresh rate set to ~3 seconds for waiting queue updates\n`);
2853
3224
  let cursorPos = 0; // Keep track of current cursor position.
2854
3225
  const spinner = customSpinner(`Getting ready...`, "clock");
@@ -2894,7 +3265,7 @@ const handleVerificationKey = async (cloudFunctions, bucketName, finalZkeyLocalF
2894
3265
  spinner.text = "Writing verification key...";
2895
3266
  // Write the verification key locally.
2896
3267
  writeLocalJsonFile(verificationKeyLocalFilePath, vKey);
2897
- await sleep(3000); // workaound for file descriptor.
3268
+ await sleep(3000); // workaround for file descriptor.
2898
3269
  // Upload verification key to storage.
2899
3270
  await multiPartUpload(cloudFunctions, bucketName, verificationKeyStorageFilePath, verificationKeyLocalFilePath, Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB));
2900
3271
  spinner.succeed(`Verification key correctly saved on storage`);
@@ -2914,13 +3285,13 @@ const handleVerifierSmartContract = async (cloudFunctions, bucketName, finalZkey
2914
3285
  const packagePath = `${dirname(fileURLToPath(import.meta.url))}`;
2915
3286
  const verifierPath = packagePath.includes(`src/commands`)
2916
3287
  ? `${dirname(fileURLToPath(import.meta.url))}/../../../../node_modules/snarkjs/templates/verifier_groth16.sol.ejs`
2917
- : `${dirname(fileURLToPath(import.meta.url))}/../../../node_modules/snarkjs/templates/verifier_groth16.sol.ejs`;
3288
+ : `${dirname(fileURLToPath(import.meta.url))}/../node_modules/snarkjs/templates/verifier_groth16.sol.ejs`;
2918
3289
  // Export the Solidity verifier smart contract.
2919
3290
  const verifierCode = await exportVerifierContract(finalZkeyLocalFilePath, verifierPath);
2920
3291
  spinner.text = `Writing verifier smart contract...`;
2921
3292
  // Write the verification key locally.
2922
3293
  writeFile(verifierContractLocalFilePath, verifierCode);
2923
- await sleep(3000); // workaound for file descriptor.
3294
+ await sleep(3000); // workaround for file descriptor.
2924
3295
  // Upload verifier smart contract to storage.
2925
3296
  await multiPartUpload(cloudFunctions, bucketName, verifierContractStorageFilePath, verifierContractLocalFilePath, Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB));
2926
3297
  spinner.succeed(`Verifier smart contract correctly saved on storage`);
@@ -2941,11 +3312,12 @@ const handleVerifierSmartContract = async (cloudFunctions, bucketName, finalZkey
2941
3312
  * @param participant <FirebaseDocumentInfo> - the Firestore document of the participant (coordinator).
2942
3313
  * @param beacon <string> - the value used to compute the final contribution while finalizing the ceremony.
2943
3314
  * @param coordinatorIdentifier <string> - the identifier of the coordinator.
3315
+ * @param circuitsLength <number> - the number of circuits in the ceremony.
2944
3316
  */
2945
- const handleCircuitFinalization = async (cloudFunctions, firestoreDatabase, ceremony, circuit, participant, beacon, coordinatorIdentifier) => {
3317
+ const handleCircuitFinalization = async (cloudFunctions, firestoreDatabase, ceremony, circuit, participant, beacon, coordinatorIdentifier, circuitsLength) => {
2946
3318
  // Step (1).
2947
- await handleStartOrResumeContribution(cloudFunctions, firestoreDatabase, ceremony, circuit, participant, computeSHA256ToHex(beacon), coordinatorIdentifier, true);
2948
- await sleep(2000); // workaound for descriptors.
3319
+ await handleStartOrResumeContribution(cloudFunctions, firestoreDatabase, ceremony, circuit, participant, computeSHA256ToHex(beacon), coordinatorIdentifier, true, circuitsLength);
3320
+ await sleep(2000); // workaround for descriptors.
2949
3321
  // Extract data.
2950
3322
  const { prefix: circuitPrefix } = circuit.data;
2951
3323
  const { prefix: ceremonyPrefix } = ceremony.data;
@@ -2979,10 +3351,11 @@ const handleCircuitFinalization = async (cloudFunctions, firestoreDatabase, cere
2979
3351
  * @dev For proper execution, the command requires the coordinator to be authenticated with a GitHub account (run auth command first) in order to
2980
3352
  * handle sybil-resistance and connect to GitHub APIs to publish the gist containing the final public attestation.
2981
3353
  */
2982
- const finalize = async () => {
3354
+ const finalize = async (opt) => {
2983
3355
  const { firebaseApp, firebaseFunctions, firestoreDatabase } = await bootstrapCommandExecutionAndServices();
2984
3356
  // Check for authentication.
2985
- const { user, providerUserId, token: coordinatorAccessToken } = await checkAuth(firebaseApp);
3357
+ const { auth } = opt;
3358
+ const { user, providerUserId, token: coordinatorAccessToken } = auth ? await authWithToken(firebaseApp, auth) : await checkAuth(firebaseApp);
2986
3359
  // Preserve command execution only for coordinators.
2987
3360
  if (!(await isCoordinator(user)))
2988
3361
  showError(COMMAND_ERRORS.COMMAND_NOT_COORDINATOR, true);
@@ -2993,7 +3366,7 @@ const finalize = async () => {
2993
3366
  showError(COMMAND_ERRORS.COMMAND_FINALIZED_NO_CLOSED_CEREMONIES, true);
2994
3367
  console.log(`${theme.symbols.warning} The computation of the final contribution could take the bulk of your computational resources and memory based on the size of the circuit ${theme.emojis.fire}\n`);
2995
3368
  // Prompt for ceremony selection.
2996
- const selectedCeremony = await promptForCeremonySelection(ceremoniesClosedForFinalization, true);
3369
+ const selectedCeremony = await promptForCeremonySelection(ceremoniesClosedForFinalization, true, "Which ceremony would you like to finalize?");
2997
3370
  // Get coordinator participant document.
2998
3371
  let participant = await getDocumentById(firestoreDatabase, getParticipantsCollectionPath(selectedCeremony.id), user.uid);
2999
3372
  const isCoordinatorReadyForCeremonyFinalization = await checkAndPrepareCoordinatorForFinalization(firebaseFunctions, selectedCeremony.id);
@@ -3017,7 +3390,7 @@ const finalize = async () => {
3017
3390
  const circuits = await getCeremonyCircuits(firestoreDatabase, selectedCeremony.id);
3018
3391
  // Handle finalization for each ceremony circuit.
3019
3392
  for await (const circuit of circuits)
3020
- await handleCircuitFinalization(firebaseFunctions, firestoreDatabase, selectedCeremony, circuit, participant, beacon, providerUserId);
3393
+ await handleCircuitFinalization(firebaseFunctions, firestoreDatabase, selectedCeremony, circuit, participant, beacon, providerUserId, circuits.length);
3021
3394
  process.stdout.write(`\n`);
3022
3395
  const spinner = customSpinner(`Wrapping up the finalization of the ceremony...`, "clock");
3023
3396
  spinner.start();
@@ -3032,7 +3405,7 @@ const finalize = async () => {
3032
3405
  // Generate attestation with final contributions.
3033
3406
  const publicAttestation = await generateValidContributionsAttestation(firestoreDatabase, circuits, selectedCeremony.id, participant.id, contributions, providerUserId, ceremonyName, true);
3034
3407
  // Write public attestation locally.
3035
- writeFile(getAttestationLocalFilePath(`${prefix}_${finalContributionIndex}_${commonTerms.foldersAndPathsTerms.attestation}.log`), Buffer.from(publicAttestation));
3408
+ writeFile(getFinalAttestationLocalFilePath(`${prefix}_${finalContributionIndex}_${commonTerms.foldersAndPathsTerms.attestation}.log`), Buffer.from(publicAttestation));
3036
3409
  await sleep(3000); // workaround for file descriptor unexpected close.
3037
3410
  const gistUrl = await publishGist(coordinatorAccessToken, publicAttestation, ceremonyName, prefix);
3038
3411
  console.log(`\n${theme.symbols.info} Your public final attestation has been successfully posted as Github Gist (${theme.text.bold(theme.text.underlined(gistUrl))})`);
@@ -3093,7 +3466,9 @@ const logout = async () => {
3093
3466
  const auth = getAuth();
3094
3467
  await signOut(auth);
3095
3468
  // Delete local token.
3469
+ deleteLocalAuthMethod();
3096
3470
  deleteLocalAccessToken();
3471
+ deleteLocalBandadaIdentity();
3097
3472
  await sleep(3000); // ~3s.
3098
3473
  spinner.stop();
3099
3474
  console.log(`${theme.symbols.success} Logout successfully completed`);
@@ -3155,6 +3530,55 @@ const listCeremonies = async () => {
3155
3530
  }
3156
3531
  };
3157
3532
 
3533
+ const listParticipants = async () => {
3534
+ try {
3535
+ const { firestoreDatabase } = await bootstrapCommandExecutionAndServices();
3536
+ const allCeremonies = await getAllCeremonies(firestoreDatabase);
3537
+ const selectedCeremony = await promptForCeremonySelection(allCeremonies, true, "Which ceremony would you like to see participants?");
3538
+ const docRef = doc(firestoreDatabase, commonTerms.collections.ceremonies.name, selectedCeremony.id);
3539
+ const participantsRef = collection(docRef, "participants");
3540
+ const participantsSnapshot = await getDocs(participantsRef);
3541
+ const participants = participantsSnapshot.docs.map((participantDoc) => participantDoc.data());
3542
+ const usersRef = collection(firestoreDatabase, "users");
3543
+ const usersSnapshot = await getDocs(usersRef);
3544
+ const users = usersSnapshot.docs.map((userDoc) => {
3545
+ const data = userDoc.data();
3546
+ return { id: userDoc.id, ...data };
3547
+ });
3548
+ const participantDetails = participants
3549
+ .map((participant) => {
3550
+ const user = users.find((_user) => _user.id === participant.userId);
3551
+ if (!user)
3552
+ return null;
3553
+ return {
3554
+ id: user.name,
3555
+ status: participant.status,
3556
+ contributionStep: participant.contributionStep,
3557
+ lastUpdated: new Date(participant.lastUpdated)
3558
+ };
3559
+ })
3560
+ .filter((user) => user !== null);
3561
+ const participantsDone = participantDetails.filter((participant) => participant.status === "DONE");
3562
+ console.log(participantDetails);
3563
+ console.log(`${theme.text.underlined("Total participants:")} ${participantDetails.length}`);
3564
+ console.log(`${theme.text.underlined("Total participants finished:")} ${participantsDone.length}`);
3565
+ }
3566
+ catch (err) {
3567
+ showError(`Something went wrong: ${err.toString()}`, true);
3568
+ }
3569
+ process.exit(0);
3570
+ };
3571
+
3572
+ const setCeremonyCommands = (program) => {
3573
+ const ceremony = program.command("ceremony").description("manage ceremonies");
3574
+ ceremony
3575
+ .command("participants")
3576
+ .description("retrieve participants list of a ceremony")
3577
+ .requiredOption("-c, --ceremony <string>", "the prefix of the ceremony you want to retrieve information about", "")
3578
+ .action(listParticipants);
3579
+ return ceremony;
3580
+ };
3581
+
3158
3582
  // Get pkg info (e.g., name, version).
3159
3583
  const packagePath = `${dirname(fileURLToPath(import.meta.url))}/..`;
3160
3584
  const { description, version, name } = JSON.parse(readFileSync(`${packagePath}/package.json`, "utf8"));
@@ -3163,44 +3587,52 @@ const program = createCommand();
3163
3587
  program.name(name).description(description).version(version);
3164
3588
  // User commands.
3165
3589
  program.command("auth").description("authenticate yourself using your Github account (OAuth 2.0)").action(auth);
3590
+ program
3591
+ .command("auth-bandada")
3592
+ .description("authenticate yourself in a privacy-perserving manner using Bandada")
3593
+ .action(authBandada);
3594
+ program
3595
+ .command("auth-siwe")
3596
+ .description("authenticate yourself using your Ethereum account (Sign In With Ethereum - SIWE)")
3597
+ .action(authSIWE);
3166
3598
  program
3167
3599
  .command("contribute")
3168
3600
  .description("compute contributions for a Phase2 Trusted Setup ceremony circuits")
3169
3601
  .option("-c, --ceremony <string>", "the prefix of the ceremony you want to contribute for", "")
3170
3602
  .option("-e, --entropy <string>", "the entropy (aka toxic waste) of your contribution", "")
3603
+ .option("-a, --auth <string>", "the Github OAuth 2.0 token", "")
3171
3604
  .action(contribute);
3172
3605
  program
3173
3606
  .command("clean")
3174
3607
  .description("clean up output generated by commands from the current working directory")
3175
3608
  .action(clean);
3176
- program
3177
- .command("list")
3178
- .description("List all ceremonies prefixes")
3179
- .action(listCeremonies);
3609
+ program.command("list").description("List all ceremonies prefixes").action(listCeremonies);
3180
3610
  program
3181
3611
  .command("logout")
3182
3612
  .description("sign out from Firebae Auth service and delete Github OAuth 2.0 token from local storage")
3183
3613
  .action(logout);
3184
3614
  program
3185
3615
  .command("validate")
3186
- .description("Validate that a Ceremony Setup file is correct")
3616
+ .description("validate that a Ceremony Setup file is correct")
3187
3617
  .requiredOption("-t, --template <path>", "The path to the ceremony setup template", "")
3188
3618
  .option("-c, --constraints <number>", "The number of constraints to check against")
3189
3619
  .action(validate);
3190
3620
  // Only coordinator commands.
3191
- const ceremony = program.command("coordinate").description("commands for coordinating a ceremony");
3192
- ceremony
3621
+ const coordinate = program.command("coordinate").description("commands for coordinating a ceremony");
3622
+ coordinate
3193
3623
  .command("setup")
3194
3624
  .description("setup a Groth16 Phase 2 Trusted Setup ceremony for zk-SNARK circuits")
3195
- .option('-t, --template <path>', 'The path to the ceremony setup template', '')
3196
- .option('-a, --auth <string>', 'The Github OAuth 2.0 token', '')
3625
+ .option("-t, --template <path>", "The path to the ceremony setup template", "")
3626
+ .option("-a, --auth <string>", "The Github OAuth 2.0 token", "")
3197
3627
  .action(setup);
3198
- ceremony
3628
+ coordinate
3199
3629
  .command("observe")
3200
3630
  .description("observe in real-time the waiting queue of each ceremony circuit")
3201
3631
  .action(observe);
3202
- ceremony
3632
+ coordinate
3203
3633
  .command("finalize")
3204
3634
  .description("finalize a Phase2 Trusted Setup ceremony by applying a beacon, exporting verification key and verifier contract")
3635
+ .option("-a, --auth <string>", "the Github OAuth 2.0 token", "")
3205
3636
  .action(finalize);
3637
+ setCeremonyCommands(program);
3206
3638
  program.parseAsync(process.argv);