@devtion/devcli 0.0.0-4088679 → 0.0.0-477457c

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 (40) hide show
  1. package/README.md +2 -2
  2. package/dist/.env +17 -3
  3. package/dist/index.js +544 -106
  4. package/dist/public/mini-semaphore.wasm +0 -0
  5. package/dist/public/mini-semaphore.zkey +0 -0
  6. package/dist/types/commands/authBandada.d.ts +2 -0
  7. package/dist/types/commands/authSIWE.d.ts +7 -0
  8. package/dist/types/commands/ceremony/index.d.ts +3 -0
  9. package/dist/types/commands/ceremony/listParticipants.d.ts +2 -0
  10. package/dist/types/commands/finalize.d.ts +2 -1
  11. package/dist/types/commands/index.d.ts +2 -0
  12. package/dist/types/commands/setup.d.ts +3 -3
  13. package/dist/types/lib/bandada.d.ts +6 -0
  14. package/dist/types/lib/files.d.ts +0 -1
  15. package/dist/types/lib/localConfigs.d.ts +38 -0
  16. package/dist/types/lib/prompts.d.ts +6 -6
  17. package/dist/types/lib/utils.d.ts +2 -1
  18. package/dist/types/types/index.d.ts +69 -0
  19. package/package.json +12 -5
  20. package/src/commands/auth.ts +32 -11
  21. package/src/commands/authBandada.ts +120 -0
  22. package/src/commands/authSIWE.ts +185 -0
  23. package/src/commands/ceremony/index.ts +20 -0
  24. package/src/commands/ceremony/listParticipants.ts +56 -0
  25. package/src/commands/contribute.ts +51 -25
  26. package/src/commands/finalize.ts +21 -9
  27. package/src/commands/index.ts +3 -1
  28. package/src/commands/listCeremonies.ts +1 -2
  29. package/src/commands/logout.ts +3 -1
  30. package/src/commands/observe.ts +7 -3
  31. package/src/commands/setup.ts +63 -28
  32. package/src/commands/validate.ts +1 -2
  33. package/src/index.ts +33 -13
  34. package/src/lib/bandada.ts +51 -0
  35. package/src/lib/errors.ts +2 -2
  36. package/src/lib/localConfigs.ts +54 -0
  37. package/src/lib/prompts.ts +22 -25
  38. package/src/lib/services.ts +38 -15
  39. package/src/lib/utils.ts +51 -13
  40. package/src/types/index.ts +75 -0
package/dist/index.js CHANGED
@@ -1,28 +1,28 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * @module @devtion/devcli
5
- * @version 1.0.6
4
+ * @module @p0tion/phase2cli
5
+ * @version 1.2.8
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 { 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 '@devtion/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';
21
21
  import fetch from '@adobe/node-fetch-retry';
22
22
  import { request } from '@octokit/request';
23
23
  import { SingleBar, Presets } from 'cli-progress';
24
24
  import dotenv from 'dotenv';
25
- import { GithubAuthProvider, getAuth, signOut } from 'firebase/auth';
25
+ import { GithubAuthProvider, signInWithCustomToken, getAuth, signOut } from 'firebase/auth';
26
26
  import { getDiskInfoSync } from 'node-disk-info';
27
27
  import ora from 'ora';
28
28
  import { Timer } from 'timer-node';
@@ -36,7 +36,10 @@ import figlet from 'figlet';
36
36
  import { createOAuthDeviceAuth } from '@octokit/auth-oauth-device';
37
37
  import clipboard from 'clipboardy';
38
38
  import open from 'open';
39
- 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';
40
43
  import readline from 'readline';
41
44
 
42
45
  /**
@@ -97,7 +100,7 @@ const CORE_SERVICES_ERRORS = {
97
100
  FIREBASE_TOKEN_EXPIRED_REMOVED_PERMISSIONS: `The Github authorization has failed due to lack of association between your account and the CLI`,
98
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.`,
99
102
  FIREBASE_FAILED_CREDENTIALS_VERIFICATION: `Firebase cannot verify your Github credentials due to network errors. Please, try once again later.`,
100
- 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.`,
101
104
  FIREBASE_CEREMONY_NOT_OPENED: `There are no ceremonies opened to contributions`,
102
105
  FIREBASE_CEREMONY_NOT_CLOSED: `There are no ceremonies ready to finalization`,
103
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.`,
@@ -119,7 +122,7 @@ const COMMAND_ERRORS = {
119
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.`,
120
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.`,
121
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.`,
122
- 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.`,
123
126
  COMMAND_SETUP_ABORT: `You chose to abort the setup process.`,
124
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.`,
125
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.`,
@@ -235,6 +238,14 @@ const checkAndMakeNewDirectoryIfNonexistent = (directoryLocalPath) => {
235
238
  const writeLocalJsonFile = (filePath, data) => {
236
239
  fs.writeFileSync(filePath, JSON.stringify(data), "utf-8");
237
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
+ };
238
249
 
239
250
  // Get npm package name.
240
251
  const packagePath$4 = `${dirname(fileURLToPath(import.meta.url))}/..`;
@@ -250,6 +261,14 @@ const config = new Conf({
250
261
  accessToken: {
251
262
  type: "string",
252
263
  default: ""
264
+ },
265
+ bandadaIdentity: {
266
+ type: "string",
267
+ default: ""
268
+ },
269
+ authMethod: {
270
+ type: "string",
271
+ default: ""
253
272
  }
254
273
  }
255
274
  });
@@ -310,6 +329,39 @@ const setLocalAccessToken = (token) => config.set("accessToken", token);
310
329
  * Delete the stored access token.
311
330
  */
312
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");
313
365
  /**
314
366
  * Get the complete local file path.
315
367
  * @param cwd <string> - the current working directory path.
@@ -420,7 +472,7 @@ const getGithubAuthenticatedUserGists = async (githubToken, params) => {
420
472
  headers: {
421
473
  authorization: `token ${githubToken}`
422
474
  },
423
- per_page: params.perPage,
475
+ per_page: params.perPage, // max items per page = 100.
424
476
  page: params.page
425
477
  });
426
478
  if (response && response.status === 200)
@@ -468,9 +520,10 @@ const getPublicAttestationGist = async (githubToken, publicAttestationFilename)
468
520
  * @returns <string> - the third-party provider handle of the user.
469
521
  */
470
522
  const getUserHandleFromProviderUserId = (providerUserId) => {
471
- if (providerUserId.indexOf("-") === -1)
472
- showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_GET_GITHUB_ACCOUNT_INFO, true);
473
- return providerUserId.split("-")[0];
523
+ if (providerUserId.indexOf("-") === -1) {
524
+ return providerUserId;
525
+ }
526
+ return providerUserId.substring(0, providerUserId.lastIndexOf("-"));
474
527
  };
475
528
  /**
476
529
  * Return a custom spinner.
@@ -584,9 +637,22 @@ const publishGist = async (token, content, ceremonyTitle, ceremonyPrefix) => {
584
637
  * @param isFinalizing <boolean> - flag to discriminate between ceremony finalization (true) and contribution (false).
585
638
  * @returns <string> - the ready to share tweet url.
586
639
  */
587
- const generateCustomUrlToTweetAboutParticipation = (ceremonyName, gistUrl, isFinalizing) => isFinalizing
588
- ? `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`
589
- : `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
+ };
590
656
  /**
591
657
  * Return a custom progress bar.
592
658
  * @param type <ProgressBarType> - the type of the progress bar.
@@ -714,13 +780,14 @@ const getLatestUpdatesFromParticipant = async (firestoreDatabase, ceremonyId, pa
714
780
  * @param entropyOrBeaconHash <string> - the entropy or beacon hash (only when finalizing) for the contribution.
715
781
  * @param contributorOrCoordinatorIdentifier <string> - the identifier of the contributor or coordinator (only when finalizing).
716
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.
717
784
  */
718
- const handleStartOrResumeContribution = async (cloudFunctions, firestoreDatabase, ceremony, circuit, participant, entropyOrBeaconHash, contributorOrCoordinatorIdentifier, isFinalizing) => {
785
+ const handleStartOrResumeContribution = async (cloudFunctions, firestoreDatabase, ceremony, circuit, participant, entropyOrBeaconHash, contributorOrCoordinatorIdentifier, isFinalizing, circuitsLength) => {
719
786
  // Extract data.
720
787
  const { prefix: ceremonyPrefix } = ceremony.data;
721
788
  const { waitingQueue, avgTimings, prefix: circuitPrefix, sequencePosition } = circuit.data;
722
789
  const { completedContributions } = waitingQueue; // = current progress.
723
- 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)`);
724
791
  // Get most up-to-date data from the participant document.
725
792
  let participantData = await getLatestUpdatesFromParticipant(firestoreDatabase, ceremony.id, participant.id);
726
793
  const spinner = customSpinner(`${participantData.contributionStep === "DOWNLOADING" /* ParticipantContributionStep.DOWNLOADING */
@@ -766,6 +833,7 @@ const handleStartOrResumeContribution = async (cloudFunctions, firestoreDatabase
766
833
  // Download the latest contribution from bucket.
767
834
  await downloadCeremonyArtifact(cloudFunctions, bucketName, lastZkeyStorageFilePath, lastZkeyLocalFilePath);
768
835
  console.log(`${theme.symbols.success} Contribution ${theme.text.bold(`#${lastZkeyIndex}`)} correctly downloaded`);
836
+ await sleep(3000);
769
837
  // Advance to next contribution step (COMPUTING) if not finalizing.
770
838
  if (!isFinalizing) {
771
839
  spinner.text = `Preparing for contribution computation...`;
@@ -788,16 +856,19 @@ const handleStartOrResumeContribution = async (cloudFunctions, firestoreDatabase
788
856
  spinner.start();
789
857
  // Read local transcript file info to get the contribution hash.
790
858
  const transcriptContents = readFile(transcriptLocalFilePath);
791
- const matchContributionHash = transcriptContents.match(/Contribution.+Hash.+\n\t\t.+\n\t\t.+\n.+\n\t\t.+\n/);
859
+ const matchContributionHash = transcriptContents.match(contribHashRegex);
792
860
  if (!matchContributionHash)
793
861
  showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_FINALIZE_NO_TRANSCRIPT_CONTRIBUTION_HASH_MATCH, true);
794
862
  // Format contribution hash.
795
863
  const contributionHash = matchContributionHash?.at(0)?.replace("\n\t\t", "");
864
+ await sleep(500);
796
865
  // Make request to cloud functions to permanently store the information.
797
866
  await permanentlyStoreCurrentContributionTimeAndHash(cloudFunctions, ceremony.id, computingTime, contributionHash);
798
867
  // Format computing time.
799
868
  const { seconds: computationSeconds, minutes: computationMinutes, hours: computationHours } = getSecondsMinutesHoursFromMillis(computingTime);
800
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);
801
872
  // Advance to next contribution step (UPLOADING) if not finalizing.
802
873
  if (!isFinalizing) {
803
874
  spinner.text = `Preparing for uploading the contribution...`;
@@ -815,10 +886,15 @@ const handleStartOrResumeContribution = async (cloudFunctions, firestoreDatabase
815
886
  if (isFinalizing || participantData.contributionStep === "UPLOADING" /* ParticipantContributionStep.UPLOADING */) {
816
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.`;
817
888
  spinner.start();
818
- if (!isFinalizing)
819
- 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
+ }
820
894
  else
821
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);
822
898
  spinner.succeed(`${isFinalizing ? `Contribution` : `Contribution ${theme.text.bold(`#${nextZkeyIndex}`)}`} correctly saved to storage`);
823
899
  // Advance to next contribution step (VERIFYING) if not finalizing.
824
900
  if (!isFinalizing) {
@@ -996,7 +1072,7 @@ const promptCircomCompiler = async () => {
996
1072
  * Shows a list of circuits for a single option selection.
997
1073
  * @dev the circuit names are derived from local R1CS files.
998
1074
  * @param options <Array<string>> - an array of circuits names.
999
- * @returns Promise<string> - the name of the choosen circuit.
1075
+ * @returns Promise<string> - the name of the chosen circuit.
1000
1076
  */
1001
1077
  const promptCircuitSelector = async (options) => {
1002
1078
  const { circuitFilename } = await prompts({
@@ -1014,7 +1090,7 @@ const promptCircuitSelector = async (options) => {
1014
1090
  * Shows a list of standard EC2 VM instance types for a single option selection.
1015
1091
  * @notice the suggested VM configuration type is calculated based on circuit constraint size.
1016
1092
  * @param constraintSize <number> - the amount of circuit constraints
1017
- * @returns Promise<string> - the name of the choosen VM type.
1093
+ * @returns Promise<string> - the name of the chosen VM type.
1018
1094
  */
1019
1095
  const promptVMTypeSelector = async (constraintSize) => {
1020
1096
  let suggestedConfiguration = 0;
@@ -1111,7 +1187,7 @@ const promptVMDiskTypeSelector = async () => {
1111
1187
  /**
1112
1188
  * Show a series of questions about the circuits.
1113
1189
  * @param constraintSize <number> - the amount of circuit constraints.
1114
- * @param timeoutMechanismType <CeremonyTimeoutType> - the choosen timeout mechanism type for the ceremony.
1190
+ * @param timeoutMechanismType <CeremonyTimeoutType> - the chosen timeout mechanism type for the ceremony.
1115
1191
  * @param needPromptCircomCompiler <boolean> - a boolean value indicating if the questions related to the Circom compiler version and commit hash must be asked.
1116
1192
  * @param enforceVM <boolean> - a boolean value indicating if the contribution verification could be supported by VM-only approach or not.
1117
1193
  * @returns Promise<Array<Circuit>> - circuit info prompted by the coordinator.
@@ -1124,7 +1200,7 @@ const promptCircuitInputData = async (constraintSize, timeoutMechanismType, same
1124
1200
  let circomVersion = "";
1125
1201
  let circomCommitHash = "";
1126
1202
  let circuitInputData;
1127
- let useCfOrVm;
1203
+ let cfOrVm;
1128
1204
  let vmDiskType;
1129
1205
  let vmConfigurationType = "";
1130
1206
  const questions = [
@@ -1179,18 +1255,21 @@ const promptCircuitInputData = async (constraintSize, timeoutMechanismType, same
1179
1255
  circomVersion = version;
1180
1256
  circomCommitHash = commitHash;
1181
1257
  }
1182
- // Ask for prefered contribution verification method (CF vs VM).
1258
+ // Ask for preferred contribution verification method (CF vs VM).
1183
1259
  if (!enforceVM) {
1184
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.
1185
1261
  `VM` // eq. false.
1186
1262
  );
1187
- useCfOrVm = confirmation;
1263
+ cfOrVm = confirmation
1264
+ ? "CF" /* CircuitContributionVerificationMechanism.CF */
1265
+ : "VM" /* CircuitContributionVerificationMechanism.VM */;
1188
1266
  }
1189
- else
1190
- useCfOrVm = "VM" /* CircuitContributionVerificationMechanism.VM */;
1191
- if (useCfOrVm === undefined)
1267
+ else {
1268
+ cfOrVm = "VM" /* CircuitContributionVerificationMechanism.VM */;
1269
+ }
1270
+ if (cfOrVm === undefined)
1192
1271
  showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
1193
- if (!useCfOrVm) {
1272
+ if (cfOrVm === "VM" /* CircuitContributionVerificationMechanism.VM */) {
1194
1273
  // Ask for selecting the specific VM configuration type.
1195
1274
  vmConfigurationType = await promptVMTypeSelector(constraintSize);
1196
1275
  // Ask for selecting the specific VM disk (volume) type.
@@ -1224,9 +1303,7 @@ const promptCircuitInputData = async (constraintSize, timeoutMechanismType, same
1224
1303
  paramsConfiguration: circuitConfigurationValues
1225
1304
  },
1226
1305
  verification: {
1227
- cfOrVm: useCfOrVm
1228
- ? "CF" /* CircuitContributionVerificationMechanism.CF */
1229
- : "VM" /* CircuitContributionVerificationMechanism.VM */,
1306
+ cfOrVm,
1230
1307
  vm: {
1231
1308
  vmConfigurationType,
1232
1309
  vmDiskType
@@ -1262,9 +1339,7 @@ const promptCircuitInputData = async (constraintSize, timeoutMechanismType, same
1262
1339
  paramsConfiguration: circuitConfigurationValues
1263
1340
  },
1264
1341
  verification: {
1265
- cfOrVm: useCfOrVm
1266
- ? "CF" /* CircuitContributionVerificationMechanism.CF */
1267
- : "VM" /* CircuitContributionVerificationMechanism.VM */,
1342
+ cfOrVm,
1268
1343
  vm: {
1269
1344
  vmConfigurationType,
1270
1345
  vmDiskType
@@ -1308,7 +1383,7 @@ const promptCircuitAddition = async () => {
1308
1383
  * Shows a list of pre-computed zKeys for a single option selection.
1309
1384
  * @dev the names are derived from local zKeys files.
1310
1385
  * @param options <Array<string>> - an array of pre-computed zKeys names.
1311
- * @returns Promise<string> - the name of the choosen pre-computed zKey.
1386
+ * @returns Promise<string> - the name of the chosen pre-computed zKey.
1312
1387
  */
1313
1388
  const promptPreComputedZkeySelector = async (options) => {
1314
1389
  const { preComputedZkeyFilename } = await prompts({
@@ -1346,13 +1421,13 @@ const promptNeededPowersForCircuit = async (suggestedSmallestNeededPowers) => {
1346
1421
  * Shows a list of PoT files for a single option selection.
1347
1422
  * @dev the names are derived from local PoT files.
1348
1423
  * @param options <Array<string>> - an array of PoT file names.
1349
- * @returns Promise<string> - the name of the choosen PoT.
1424
+ * @returns Promise<string> - the name of the chosen PoT.
1350
1425
  */
1351
1426
  const promptPotSelector = async (options) => {
1352
1427
  const { potFilename } = await prompts({
1353
1428
  type: "select",
1354
1429
  name: "potFilename",
1355
- 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"),
1356
1431
  choices: options.map((option) => {
1357
1432
  console.log(option);
1358
1433
  return { title: option, value: option };
@@ -1370,7 +1445,7 @@ const promptPotSelector = async (options) => {
1370
1445
  * @param isFinalizing <boolean> - true when the coordinator must select a ceremony for finalization; otherwise false (participant selects a ceremony for contribution).
1371
1446
  * @returns Promise<FirebaseDocumentInfo> - the Firestore document of the selected ceremony.
1372
1447
  */
1373
- const promptForCeremonySelection = async (ceremoniesDocuments, isFinalizing) => {
1448
+ const promptForCeremonySelection = async (ceremoniesDocuments, isFinalizing, messageToDisplay) => {
1374
1449
  // Prepare state.
1375
1450
  const choices = [];
1376
1451
  // Prepare choices x ceremony.
@@ -1388,9 +1463,7 @@ const promptForCeremonySelection = async (ceremoniesDocuments, isFinalizing) =>
1388
1463
  const { ceremony } = await prompts({
1389
1464
  type: "select",
1390
1465
  name: "ceremony",
1391
- message: theme.text.bold(!isFinalizing
1392
- ? "Which ceremony would you like to contribute to?"
1393
- : "Which ceremony would you like to finalize?"),
1466
+ message: theme.text.bold(messageToDisplay),
1394
1467
  choices,
1395
1468
  initial: 0
1396
1469
  });
@@ -1422,7 +1495,7 @@ const promptToTypeEntropyOrBeacon = async (isEntropy = true) => {
1422
1495
  * @return <Promise<string>> - the entropy.
1423
1496
  */
1424
1497
  const promptForEntropy = async () => {
1425
- // Prompt for entropy generation prefered method.
1498
+ // Prompt for entropy generation preferred method.
1426
1499
  const { confirmation } = await askForConfirmation(`Do you prefer to type your entropy or generate it randomly?`, "Manually", "Randomly");
1427
1500
  if (confirmation === undefined)
1428
1501
  showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
@@ -1541,16 +1614,37 @@ const checkAuth = async (firebaseApp) => {
1541
1614
  showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_NOT_AUTHENTICATED, true);
1542
1615
  // Retrieve local access token.
1543
1616
  const token = String(getLocalAccessToken());
1544
- // Get credentials.
1545
- const credentials = exchangeGithubTokenForCredentials(token);
1546
- // Sign in to Firebase using credentials.
1547
- 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
+ }
1548
1644
  // Get current authenticated user.
1549
1645
  const user = getCurrentFirebaseAuthUser(firebaseApp);
1550
- // Get Github unique identifier (handle-id).
1551
- const providerUserId = await getGithubProviderUserId(String(token));
1552
1646
  // Greet the user.
1553
- console.log(`Greetings, @${theme.text.bold(getUserHandleFromProviderUserId(providerUserId))} ${theme.emojis.wave}\n`);
1647
+ console.log(`Greetings, @${theme.text.bold(username)} ${theme.emojis.wave}\n`);
1554
1648
  return {
1555
1649
  user,
1556
1650
  token,
@@ -1639,7 +1733,7 @@ const handleAdditionOfCircuitsToCeremony = async (r1csOptions, wasmOptions, cere
1639
1733
  wasmFilename.split(`.${commonTerms.foldersAndPathsTerms.wasm}`)[0]);
1640
1734
  if (matchingWasms.length !== 1)
1641
1735
  showError(COMMAND_ERRORS.COMMAND_SETUP_MISMATCH_R1CS_WASM, true);
1642
- // Get input data for choosen circuit.
1736
+ // Get input data for chosen circuit.
1643
1737
  const circuitInputData = await getInputDataToAddCircuitToCeremony(choosenCircuitFilename, matchingWasms[0], ceremonyTimeoutMechanismType, sameCircomCompiler, circuitSequencePosition, sharedCircomCompilerData);
1644
1738
  // Store circuit data.
1645
1739
  inputDataForCircuits.push(circuitInputData);
@@ -1692,7 +1786,7 @@ const displayCeremonySummary = (ceremonyInputData, circuits) => {
1692
1786
  };
1693
1787
  /**
1694
1788
  * Check if the smallest Powers of Tau has already been downloaded/stored in the correspondent local path
1695
- * @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.
1696
1790
  * @param powers <string> - the smallest amount of powers needed for the given circuit (should be in a 'XY' stringified form).
1697
1791
  * @param ptauCompleteFilename <string> - the complete file name of the powers of tau file to be downloaded.
1698
1792
  * @returns <Promise<void>>
@@ -1706,7 +1800,7 @@ const checkAndDownloadSmallestPowersOfTau = async (powers, ptauCompleteFilename)
1706
1800
  .map((dirent) => dirent.name);
1707
1801
  // Check if already downloaded or not.
1708
1802
  if (smallestPtauFileForGivenPowers.length === 0) {
1709
- 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`);
1710
1804
  spinner.start();
1711
1805
  // Download smallest Powers of Tau file from remote server.
1712
1806
  const streamPipeline = promisify(pipeline);
@@ -1729,7 +1823,7 @@ const checkAndDownloadSmallestPowersOfTau = async (powers, ptauCompleteFilename)
1729
1823
  * number of powers greater than or equal to the powers needed by the zKey), the coordinator will be asked
1730
1824
  * to provide a number of powers manually, ranging from the smallest possible to the largest.
1731
1825
  * @param neededPowers <number> - the smallest amount of powers needed by the zKey.
1732
- * @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
1733
1827
  * along with related powers.
1734
1828
  */
1735
1829
  const handlePreComputedZkeyPowersOfTauSelection = async (neededPowers) => {
@@ -1792,6 +1886,7 @@ const handleCeremonyBucketCreation = async (firebaseFunctions, ceremonyPrefix) =
1792
1886
  spinner.start();
1793
1887
  try {
1794
1888
  // Make the call to create the bucket.
1889
+ spinner.info(`Creating bucket ${bucketName}`);
1795
1890
  await createS3Bucket(firebaseFunctions, bucketName);
1796
1891
  }
1797
1892
  catch (error) {
@@ -1821,7 +1916,7 @@ const handleCircuitArtifactUploadToStorage = async (firebaseFunctions, bucketNam
1821
1916
  * @notice The setup command allows the coordinator of the ceremony to prepare the next ceremony by interacting with the CLI.
1822
1917
  * @dev For proper execution, the command must be run in a folder containing the R1CS files related to the circuits
1823
1918
  * for which the coordinator wants to create the ceremony. The command will download the necessary Tau powers
1824
- * from Hermez's ceremony Phase 1 Reliable Setup Ceremony.
1919
+ * from PPoT ceremony Phase 1 Setup Ceremony.
1825
1920
  * @param cmd? <any> - the path to the ceremony setup file.
1826
1921
  */
1827
1922
  const setup = async (cmd) => {
@@ -1830,7 +1925,9 @@ const setup = async (cmd) => {
1830
1925
  let ceremonyId = ""; // The unique identifier of the ceremony.
1831
1926
  const { firebaseApp, firebaseFunctions, firestoreDatabase } = await bootstrapCommandExecutionAndServices();
1832
1927
  // Check for authentication.
1833
- const { user, providerUserId } = cmd.auth ? await authWithToken(firebaseApp, cmd.auth) : await checkAuth(firebaseApp);
1928
+ const { user, providerUserId } = cmd.auth
1929
+ ? await authWithToken(firebaseApp, cmd.auth)
1930
+ : await checkAuth(firebaseApp);
1834
1931
  // Preserve command execution only for coordinators.
1835
1932
  if (!(await isCoordinator(user)))
1836
1933
  showError(COMMAND_ERRORS.COMMAND_NOT_COORDINATOR, true);
@@ -1847,7 +1944,7 @@ const setup = async (cmd) => {
1847
1944
  // if there is the file option, then set up the non interactively
1848
1945
  if (cmd.template) {
1849
1946
  // 1. parse the file
1850
- // tmp data - do not cleanup files as we need them
1947
+ // tmp data - do not cleanup files as we need them
1851
1948
  const spinner = customSpinner(`Parsing ${theme.text.bold(cmd.template)} setup configuration file...`, `clock`);
1852
1949
  spinner.start();
1853
1950
  const setupCeremonyData = await parseCeremonyFile(cmd.template);
@@ -1870,12 +1967,20 @@ const setup = async (cmd) => {
1870
1967
  // 3. generate the zKey
1871
1968
  const spinner = customSpinner(`Generating genesis zKey for circuit ${theme.text.bold(circuit.name)}...`, `clock`);
1872
1969
  spinner.start();
1873
- await zKey.newZKey(r1csLocalPathAndFileName, getPotLocalFilePath(circuit.files.potFilename), zkeyLocalPathAndFileName, undefined);
1874
- spinner.succeed(`Generation of the genesis zKey for citcui ${theme.text.bold(circuit.name)} completed successfully`);
1970
+ if (existsSync(zkeyLocalPathAndFileName)) {
1971
+ spinner.succeed(`The genesis zKey for circuit ${theme.text.bold(circuit.name)} is already present on disk`);
1972
+ }
1973
+ else {
1974
+ await zKey.newZKey(r1csLocalPathAndFileName, getPotLocalFilePath(circuit.files.potFilename), zkeyLocalPathAndFileName, undefined);
1975
+ spinner.succeed(`Generation of the genesis zKey for circuit ${theme.text.bold(circuit.name)} completed successfully`);
1976
+ }
1977
+ const hashSpinner = customSpinner(`Calculating hashes for circuit ${theme.text.bold(circuit.name)}...`, `clock`);
1978
+ hashSpinner.start();
1875
1979
  // 4. calculate the hashes
1876
1980
  const wasmBlake2bHash = await blake512FromPath(wasmLocalPathAndFileName);
1877
1981
  const potBlake2bHash = await blake512FromPath(getPotLocalFilePath(circuit.files.potFilename));
1878
1982
  const initialZkeyBlake2bHash = await blake512FromPath(zkeyLocalPathAndFileName);
1983
+ hashSpinner.succeed(`Hashes for circuit ${theme.text.bold(circuit.name)} calculated successfully`);
1879
1984
  // 5. upload the artifacts
1880
1985
  // Upload zKey to Storage.
1881
1986
  await handleCircuitArtifactUploadToStorage(firebaseFunctions, bucketName, circuit.files.initialZkeyStoragePath, zkeyLocalPathAndFileName, circuit.files.initialZkeyFilename);
@@ -1893,9 +1998,9 @@ const setup = async (cmd) => {
1893
1998
  // 6 update the setup data object
1894
1999
  ceremonySetupData.circuits[index].files = {
1895
2000
  ...circuit.files,
1896
- potBlake2bHash: potBlake2bHash,
1897
- wasmBlake2bHash: wasmBlake2bHash,
1898
- initialZkeyBlake2bHash: initialZkeyBlake2bHash
2001
+ potBlake2bHash,
2002
+ wasmBlake2bHash,
2003
+ initialZkeyBlake2bHash
1899
2004
  };
1900
2005
  ceremonySetupData.circuits[index].zKeySizeInBytes = getFileStats(zkeyLocalPathAndFileName).size;
1901
2006
  }
@@ -2086,10 +2191,11 @@ const expirationCountdownForGithubOAuth = (expirationInSeconds) => {
2086
2191
  // Update time and seconds counter.
2087
2192
  expirationInSeconds -= interval;
2088
2193
  secondsCounter -= interval;
2089
- if (secondsCounter % 60 === 0)
2090
- secondsCounter = 0;
2194
+ if (secondsCounter === 0) {
2195
+ secondsCounter = 59;
2196
+ }
2091
2197
  // Notify user.
2092
- process.stdout.write(`${theme.symbols.warning} Expires in ${theme.text.bold(theme.colors.magenta(`00:${Math.floor(expirationInSeconds / 60)}:${secondsCounter}`))}\r`);
2198
+ process.stdout.write(`${theme.symbols.warning} Expires in ${theme.text.bold(theme.colors.magenta(`00:${Math.floor(expirationInSeconds / 60)}:${secondsCounter.toString().padStart(2, "0")}`))}\r`);
2093
2199
  }
2094
2200
  else {
2095
2201
  process.stdout.write(`\n\n`); // workaround to \r.
@@ -2102,18 +2208,31 @@ const expirationCountdownForGithubOAuth = (expirationInSeconds) => {
2102
2208
  * @param verification <Verification> - the data from Github OAuth2.0 device flow.
2103
2209
  */
2104
2210
  const onVerification = async (verification) => {
2211
+ verification.interval = 7; // overwrite Github interval with 7 seconds
2105
2212
  // Copy code to clipboard.
2106
- clipboard.writeSync(verification.user_code);
2107
- clipboard.readSync();
2213
+ let noClipboard = false;
2214
+ try {
2215
+ clipboard.writeSync(verification.user_code);
2216
+ clipboard.readSync();
2217
+ }
2218
+ catch (error) {
2219
+ noClipboard = true;
2220
+ }
2108
2221
  // Display data.
2109
2222
  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`);
2110
- console.log(theme.colors.magenta(figlet.textSync("Code is Below", { font: "ANSI Shadow" })), '\n');
2111
- 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`);
2223
+ console.log(theme.colors.magenta(figlet.textSync("Code is Below", { font: "ANSI Shadow" })), "\n");
2224
+ const message = !noClipboard ? `has been copied to your clipboard (${theme.emojis.clipboard})` : ``;
2225
+ console.log(`${theme.symbols.info} Your auth code: ${theme.text.bold(verification.user_code)} ${message} ${theme.symbols.success}\n`);
2112
2226
  const spinner = customSpinner(`Redirecting to Github...`, `clock`);
2113
2227
  spinner.start();
2114
2228
  await sleep(10000); // ~10s to make users able to read the CLI.
2115
- // Automatically open the page (# Step 2).
2116
- await open(verification.verification_uri);
2229
+ try {
2230
+ // Automatically open the page (# Step 2).
2231
+ await open(verification.verification_uri);
2232
+ }
2233
+ catch (error) {
2234
+ console.log(`${theme.symbols.info} Please authenticate via GitHub at ${verification.verification_uri}`);
2235
+ }
2117
2236
  spinner.stop();
2118
2237
  // Countdown for time expiration.
2119
2238
  expirationCountdownForGithubOAuth(verification.expires_in);
@@ -2164,6 +2283,7 @@ const auth = async () => {
2164
2283
  // Generate a new access token using Github Device Flow (OAuth 2.0).
2165
2284
  const newToken = await executeGithubDeviceFlow(String(process.env.AUTH_GITHUB_CLIENT_ID));
2166
2285
  // Store the new access token.
2286
+ setLocalAuthMethod("github");
2167
2287
  setLocalAccessToken(newToken);
2168
2288
  }
2169
2289
  else
@@ -2184,6 +2304,249 @@ const auth = async () => {
2184
2304
  terminate(providerUserId);
2185
2305
  };
2186
2306
 
2307
+ const { BANDADA_API_URL } = process.env;
2308
+ const bandadaApi = new ApiSdk(BANDADA_API_URL);
2309
+ const addMemberToGroup = async (groupId, dashboardUrl, identity) => {
2310
+ const commitment = identity.commitment.toString();
2311
+ const group = await bandadaApi.getGroup(groupId);
2312
+ const providerName = group.credentials.id.split("_")[0].toLowerCase();
2313
+ // 6. open a new window with the url:
2314
+ const url = `${dashboardUrl}credentials?group=${groupId}&member=${commitment}&provider=${providerName}`;
2315
+ console.log(`${theme.text.bold(`Verification URL:`)} ${theme.text.underlined(url)}`);
2316
+ open(url);
2317
+ const { confirmation } = await askForConfirmation("Did you join the Bandada group in the browser?");
2318
+ if (!confirmation)
2319
+ showError("You must join the Bandada group to continue the login process", true);
2320
+ };
2321
+ const isGroupMember = async (groupId, identity) => {
2322
+ const commitment = identity.commitment.toString();
2323
+ const isMember = await bandadaApi.isGroupMember(groupId, commitment);
2324
+ return isMember;
2325
+ };
2326
+
2327
+ const { BANDADA_DASHBOARD_URL, BANDADA_GROUP_ID } = process.env;
2328
+ const authBandada = async () => {
2329
+ try {
2330
+ const { firebaseFunctions } = await bootstrapCommandExecutionAndServices();
2331
+ const spinner = customSpinner(`Checking identity string for Semaphore...`, `clock`);
2332
+ spinner.start();
2333
+ // 1. check if _identity string exists in local storage
2334
+ let identityString;
2335
+ const isIdentityStringStored = checkLocalBandadaIdentity();
2336
+ if (isIdentityStringStored) {
2337
+ identityString = getLocalBandadaIdentity();
2338
+ spinner.succeed(`Identity seed found\n`);
2339
+ }
2340
+ else {
2341
+ spinner.warn(`Identity seed not found\n`);
2342
+ // 2. generate a random _identity string and save it in local storage
2343
+ const { seed } = await prompts({
2344
+ type: "text",
2345
+ name: "seed",
2346
+ message: theme.text.bold(`Enter a secret string to use as your identity seed in Semaphore:`),
2347
+ initial: false
2348
+ });
2349
+ identityString = seed;
2350
+ setLocalBandadaIdentity(identityString);
2351
+ }
2352
+ // 3. create a semaphore identity with _identity string as a seed
2353
+ const identity = new Identity(identityString);
2354
+ // 4. check if the user is a member of the group
2355
+ console.log(`Checking Bandada membership...`);
2356
+ const isMember = await isGroupMember(BANDADA_GROUP_ID, identity);
2357
+ if (!isMember) {
2358
+ await addMemberToGroup(BANDADA_GROUP_ID, BANDADA_DASHBOARD_URL, identity);
2359
+ }
2360
+ // 5. generate a proof that the user owns the commitment.
2361
+ spinner.text = `Generating proof of identity...`;
2362
+ spinner.start();
2363
+ // publicSignals = [hash(externalNullifier, identityNullifier), commitment]
2364
+ const initDirectoryName = getLocalDirname();
2365
+ const directoryName = initDirectoryName.includes("/src") ? "." : initDirectoryName;
2366
+ const { proof, publicSignals } = await groth16.fullProve({
2367
+ identityTrapdoor: identity.trapdoor,
2368
+ identityNullifier: identity.nullifier,
2369
+ externalNullifier: BANDADA_GROUP_ID
2370
+ }, `${directoryName}/public/mini-semaphore.wasm`, `${directoryName}/public/mini-semaphore.zkey`);
2371
+ spinner.succeed(`Proof generated.\n`);
2372
+ spinner.text = `Sending proof to verification...`;
2373
+ spinner.start();
2374
+ // 6. send proof to a cloud function that verifies it and checks membership
2375
+ const cf = httpsCallable(firebaseFunctions, commonTerms.cloudFunctionsNames.bandadaValidateProof);
2376
+ const result = await cf({
2377
+ proof,
2378
+ publicSignals
2379
+ });
2380
+ const { valid, token, message } = result.data;
2381
+ if (!valid) {
2382
+ showError(message, true);
2383
+ deleteLocalAuthMethod();
2384
+ deleteLocalAccessToken();
2385
+ deleteLocalBandadaIdentity();
2386
+ }
2387
+ spinner.succeed(`Proof verified.\n`);
2388
+ spinner.text = `Authenticating...`;
2389
+ spinner.start();
2390
+ // 7. Auth to p0tion firebase
2391
+ const credentials = await signInWithCustomToken(getAuth(), token);
2392
+ setLocalAuthMethod("bandada");
2393
+ setLocalAccessToken(token);
2394
+ spinner.succeed(`Authenticated as ${theme.text.bold(credentials.user.uid)}.`);
2395
+ console.log(`\n${theme.symbols.warning} You can always log out by running the ${theme.text.bold(`phase2cli logout`)} command`);
2396
+ }
2397
+ catch (error) {
2398
+ // Delete local token.
2399
+ console.log("An error crashed the process. Deleting local token and identity.");
2400
+ console.error(error);
2401
+ deleteLocalAuthMethod();
2402
+ deleteLocalAccessToken();
2403
+ deleteLocalBandadaIdentity();
2404
+ }
2405
+ process.exit(0);
2406
+ };
2407
+
2408
+ const showVerificationCodeAndUri = async (OAuthDeviceCode) => {
2409
+ // Copy code to clipboard.
2410
+ let noClipboard = false;
2411
+ try {
2412
+ clipboard.writeSync(OAuthDeviceCode.user_code);
2413
+ clipboard.readSync();
2414
+ }
2415
+ catch (error) {
2416
+ noClipboard = true;
2417
+ }
2418
+ // Display data.
2419
+ 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`);
2420
+ console.log(theme.colors.magenta(figlet.textSync("Code is Below", { font: "ANSI Shadow" })), "\n");
2421
+ const message = !noClipboard ? `has been copied to your clipboard (${theme.emojis.clipboard})` : ``;
2422
+ console.log(`${theme.symbols.info} Your auth code: ${theme.text.bold(OAuthDeviceCode.user_code)} ${message} ${theme.symbols.success}\n`);
2423
+ const spinner = customSpinner(`Redirecting to Sign In With Ethereum...`, `clock`);
2424
+ spinner.start();
2425
+ await sleep(10000); // ~10s to make users able to read the CLI.
2426
+ try {
2427
+ // Automatically open the page (# Step 2).
2428
+ await open(OAuthDeviceCode.verification_uri_complete);
2429
+ }
2430
+ catch (error) {
2431
+ console.log(`${theme.symbols.info} Please authenticate via SIWE at ${OAuthDeviceCode.verification_uri_complete}`);
2432
+ }
2433
+ spinner.stop();
2434
+ };
2435
+ /**
2436
+ * Return the token to sign in to Firebase after passing the SIWE Device Flow
2437
+ * @param clientId <string> - The client id of the Auth0 application.
2438
+ * @param firebaseFunctions <any> - The Firebase functions instance to call the cloud function
2439
+ * @returns <string> - The token to sign in to Firebase
2440
+ */
2441
+ const executeSIWEDeviceFlow = async (clientId, firebaseFunctions) => {
2442
+ // Call Auth0 endpoint to request device code uri
2443
+ const OAuthDeviceCode = (await fetch$1(`${process.env.AUTH0_APPLICATION_URL}/oauth/device/code`, {
2444
+ method: "POST",
2445
+ headers: { "content-type": "application/json" },
2446
+ body: JSON.stringify({
2447
+ client_id: clientId,
2448
+ scope: "openid",
2449
+ audience: `${process.env.AUTH0_APPLICATION_URL}/api/v2/`
2450
+ })
2451
+ }).then((_res) => _res.json()));
2452
+ if (OAuthDeviceCode.error) {
2453
+ showError(OAuthDeviceCode.error_description, true);
2454
+ deleteLocalAuthMethod();
2455
+ deleteLocalAccessToken();
2456
+ }
2457
+ await showVerificationCodeAndUri(OAuthDeviceCode);
2458
+ // Poll Auth0 endpoint until you get token or request expires
2459
+ let isSignedIn = false;
2460
+ let isExpired = false;
2461
+ let auth0Token = "";
2462
+ while (!isSignedIn && !isExpired) {
2463
+ // Call Auth0 endpoint to request token
2464
+ const OAuthToken = (await fetch$1(`${process.env.AUTH0_APPLICATION_URL}/oauth/token`, {
2465
+ method: "POST",
2466
+ headers: { "content-type": "application/json" },
2467
+ body: JSON.stringify({
2468
+ client_id: clientId,
2469
+ device_code: OAuthDeviceCode.device_code,
2470
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
2471
+ })
2472
+ }).then((_res) => _res.json()));
2473
+ if (OAuthToken.error) {
2474
+ if (OAuthToken.error === "authorization_pending") {
2475
+ // Wait for the user to sign in
2476
+ await sleep(OAuthDeviceCode.interval * 1000);
2477
+ }
2478
+ else if (OAuthToken.error === "slow_down") {
2479
+ // Wait for the user to sign in
2480
+ await sleep(OAuthDeviceCode.interval * 1000 * 2);
2481
+ }
2482
+ else if (OAuthToken.error === "expired_token") {
2483
+ // The user didn't sign in on time
2484
+ isExpired = true;
2485
+ }
2486
+ }
2487
+ else {
2488
+ // The user signed in
2489
+ isSignedIn = true;
2490
+ auth0Token = OAuthToken.access_token;
2491
+ }
2492
+ }
2493
+ // Send token to cloud function to check nonce, create user and retrieve token
2494
+ const cf = httpsCallable(firebaseFunctions, commonTerms.cloudFunctionsNames.checkNonceOfSIWEAddress);
2495
+ const result = await cf({
2496
+ auth0Token
2497
+ });
2498
+ const { token, valid, message } = result.data;
2499
+ if (!valid) {
2500
+ showError(message, true);
2501
+ deleteLocalAuthMethod();
2502
+ deleteLocalAccessToken();
2503
+ }
2504
+ return token;
2505
+ };
2506
+ /**
2507
+ * Auth command using Sign In With Ethereum
2508
+ * @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.
2509
+ * @dev Under the hood, the command handles a manual Device Flow following the guidelines in the SIWE documentation.
2510
+ */
2511
+ const authSIWE = async () => {
2512
+ try {
2513
+ const { firebaseFunctions } = await bootstrapCommandExecutionAndServices();
2514
+ // Console more context for the user.
2515
+ 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`);
2516
+ const spinner = customSpinner(`Checking authentication token...`, `clock`);
2517
+ spinner.start();
2518
+ await sleep(5000);
2519
+ // Manage OAuth Github or SIWE token.
2520
+ const isLocalTokenStored = checkLocalAccessToken();
2521
+ if (!isLocalTokenStored) {
2522
+ spinner.fail(`No local authentication token found\n`);
2523
+ // Generate a new access token using Github Device Flow (OAuth 2.0).
2524
+ const newToken = await executeSIWEDeviceFlow(String(process.env.AUTH_SIWE_CLIENT_ID), firebaseFunctions);
2525
+ // Store the new access token.
2526
+ setLocalAuthMethod("siwe");
2527
+ setLocalAccessToken(newToken);
2528
+ }
2529
+ else
2530
+ spinner.succeed(`Local authentication token found\n`);
2531
+ // Get access token from local store.
2532
+ const token = String(getLocalAccessToken());
2533
+ spinner.text = `Authenticating...`;
2534
+ spinner.start();
2535
+ // Exchange token for credential.
2536
+ const credentials = await signInWithCustomToken(getAuth(), token);
2537
+ spinner.succeed(`Authenticated as ${theme.text.bold(credentials.user.uid)}.`);
2538
+ console.log(`\n${theme.symbols.warning} You can always log out by running the ${theme.text.bold(`phase2cli logout`)} command`);
2539
+ process.exit(0);
2540
+ }
2541
+ catch (error) {
2542
+ // Delete local token.
2543
+ console.log("An error crashed the process. Deleting local token and identity.");
2544
+ console.error(error);
2545
+ deleteLocalAuthMethod();
2546
+ deleteLocalAccessToken();
2547
+ }
2548
+ };
2549
+
2187
2550
  /**
2188
2551
  * Return the verification result for latest contribution.
2189
2552
  * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
@@ -2306,8 +2669,8 @@ const handleDiskSpaceRequirementForNextContribution = async (cloudFunctions, cer
2306
2669
  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
2307
2670
  ? theme.text.bold(`< 0.01`)
2308
2671
  : 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`);
2309
- 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");
2310
- wannaContributeOrHaveEnoughMemory = !!confirmation;
2672
+ 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");
2673
+ wannaContributeOrHaveEnoughMemory = !!confirmationEnoughMemory;
2311
2674
  if (circuitSequencePosition > 1) {
2312
2675
  console.log(`${theme.symbols.info} Please note, you have time until ceremony ends to free up your memory and complete remaining contributions`);
2313
2676
  // Asks the contributor if their wants to terminate contributions for the ceremony.
@@ -2375,8 +2738,12 @@ const handlePublicAttestation = async (firestoreDatabase, circuits, ceremonyId,
2375
2738
  // Write public attestation locally.
2376
2739
  writeFile(getAttestationLocalFilePath(`${ceremonyPrefix}_${commonTerms.foldersAndPathsTerms.attestation}.log`), Buffer.from(publicAttestation));
2377
2740
  await sleep(1000); // workaround for file descriptor unexpected close.
2378
- const gistUrl = await publishGist(participantAccessToken, publicAttestation, ceremonyName, ceremonyPrefix);
2379
- console.log(`\n${theme.symbols.info} Your public attestation has been successfully posted as Github Gist (${theme.text.bold(theme.text.underlined(gistUrl))})`);
2741
+ let gistUrl = "";
2742
+ const isGithub = getLocalAuthMethod() === "github";
2743
+ if (isGithub) {
2744
+ gistUrl = await publishGist(participantAccessToken, publicAttestation, ceremonyName, ceremonyPrefix);
2745
+ console.log(`\n${theme.symbols.info} Your public attestation has been successfully posted as Github Gist (${theme.text.bold(theme.text.underlined(gistUrl))})`);
2746
+ }
2380
2747
  // Prepare a ready-to-share tweet.
2381
2748
  await handleTweetGeneration(ceremonyName, gistUrl);
2382
2749
  };
@@ -2429,6 +2796,7 @@ const listenToCeremonyCircuitDocumentChanges = (firestoreDatabase, ceremonyId, p
2429
2796
  }
2430
2797
  });
2431
2798
  };
2799
+ let contributionInProgress = false;
2432
2800
  /**
2433
2801
  * Listen to current authenticated participant document changes.
2434
2802
  * @dev this is the core business logic related to the execution of the contribute command.
@@ -2556,11 +2924,21 @@ const listenToParticipantDocumentChanges = async (firestoreDatabase, cloudFuncti
2556
2924
  (!noTemporaryContributionData && resumingWithSameTemporaryData);
2557
2925
  // Scenario (3.B).
2558
2926
  if (isCurrentContributor && hasResumableStep && startingOrResumingContribution) {
2927
+ if (contributionInProgress) {
2928
+ console.warn(`\n${theme.symbols.warning} Received instruction to start/resume contribution but contribution is already in progress...[skipping]`);
2929
+ return;
2930
+ }
2559
2931
  // Communicate resume / start of the contribution to participant.
2560
2932
  await simpleLoader(`${changedContributionStep === "DOWNLOADING" /* ParticipantContributionStep.DOWNLOADING */ ? `Starting` : `Resuming`} your contribution...`, `clock`, 3000);
2561
- // Start / Resume the contribution for the participant.
2562
- await handleStartOrResumeContribution(cloudFunctions, firestoreDatabase, ceremony, circuit, participant, entropy, providerUserId, false // not finalizing.
2563
- );
2933
+ try {
2934
+ contributionInProgress = true;
2935
+ // Start / Resume the contribution for the participant.
2936
+ await handleStartOrResumeContribution(cloudFunctions, firestoreDatabase, ceremony, circuit, participant, entropy, providerUserId, false, // not finalizing.
2937
+ circuits.length);
2938
+ }
2939
+ finally {
2940
+ contributionInProgress = false;
2941
+ }
2564
2942
  }
2565
2943
  // Scenario (3.A).
2566
2944
  else if (isWaitingForContribution)
@@ -2609,7 +2987,9 @@ const listenToParticipantDocumentChanges = async (firestoreDatabase, cloudFuncti
2609
2987
  // Get latest contribution verification result.
2610
2988
  await getLatestVerificationResult(firestoreDatabase, ceremony.id, circuit.id, participant.id);
2611
2989
  // Get next circuit for contribution.
2612
- const nextCircuit = timeoutExpired ? getCircuitBySequencePosition(circuits, changedContributionProgress) : getCircuitBySequencePosition(circuits, changedContributionProgress + 1);
2990
+ const nextCircuit = timeoutExpired
2991
+ ? getCircuitBySequencePosition(circuits, changedContributionProgress)
2992
+ : getCircuitBySequencePosition(circuits, changedContributionProgress + 1);
2613
2993
  // Check disk space requirements for participant.
2614
2994
  const wannaGenerateAttestation = await handleDiskSpaceRequirementForNextContribution(cloudFunctions, ceremony.id, nextCircuit.data.sequencePosition, nextCircuit.data.zKeySizeInBytes, timeoutExpired, providerUserId);
2615
2995
  // Check if the participant would like to generate a new attestation.
@@ -2650,7 +3030,7 @@ const contribute = async (opt) => {
2650
3030
  // Get options.
2651
3031
  const ceremonyOpt = opt.ceremony;
2652
3032
  const entropyOpt = opt.entropy;
2653
- const auth = opt.auth;
3033
+ const { auth } = opt;
2654
3034
  // Check for authentication.
2655
3035
  const { user, providerUserId, token } = auth ? await authWithToken(firebaseApp, auth) : await checkAuth(firebaseApp);
2656
3036
  // Prepare data.
@@ -2678,7 +3058,7 @@ const contribute = async (opt) => {
2678
3058
  }
2679
3059
  else {
2680
3060
  // Prompt the user to select a ceremony from the opened ones.
2681
- selectedCeremony = await promptForCeremonySelection(ceremoniesOpenedForContributions, false);
3061
+ selectedCeremony = await promptForCeremonySelection(ceremoniesOpenedForContributions, false, "Which ceremony would you like to contribute to?");
2682
3062
  }
2683
3063
  // Get selected ceremony circuit(s) documents.
2684
3064
  const circuits = await getCeremonyCircuits(firestoreDatabase, selectedCeremony.id);
@@ -2688,7 +3068,7 @@ const contribute = async (opt) => {
2688
3068
  const userDoc = await getDocumentById(firestoreDatabase, commonTerms.collections.users.name, user.uid);
2689
3069
  const userData = userDoc.data();
2690
3070
  if (!userData) {
2691
- 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.`);
3071
+ 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.`);
2692
3072
  process.exit(0);
2693
3073
  }
2694
3074
  // Check the user's current participant readiness for contribution status (eligible, already contributed, timed out).
@@ -2839,10 +3219,10 @@ const observe = async () => {
2839
3219
  // Preserve command execution only for coordinators].
2840
3220
  if (!(await isCoordinator(user)))
2841
3221
  showError(COMMAND_ERRORS.COMMAND_NOT_COORDINATOR, true);
2842
- // Get running cerimonies info (if any).
3222
+ // Get running ceremonies info (if any).
2843
3223
  const runningCeremoniesDocs = await getOpenedCeremonies(firestoreDatabase);
2844
3224
  // Ask to select a ceremony.
2845
- const ceremony = await promptForCeremonySelection(runningCeremoniesDocs, false);
3225
+ const ceremony = await promptForCeremonySelection(runningCeremoniesDocs, false, "Which ceremony would you like to observe?");
2846
3226
  console.log(`${logSymbols.info} Refresh rate set to ~3 seconds for waiting queue updates\n`);
2847
3227
  let cursorPos = 0; // Keep track of current cursor position.
2848
3228
  const spinner = customSpinner(`Getting ready...`, "clock");
@@ -2888,7 +3268,7 @@ const handleVerificationKey = async (cloudFunctions, bucketName, finalZkeyLocalF
2888
3268
  spinner.text = "Writing verification key...";
2889
3269
  // Write the verification key locally.
2890
3270
  writeLocalJsonFile(verificationKeyLocalFilePath, vKey);
2891
- await sleep(3000); // workaound for file descriptor.
3271
+ await sleep(3000); // workaround for file descriptor.
2892
3272
  // Upload verification key to storage.
2893
3273
  await multiPartUpload(cloudFunctions, bucketName, verificationKeyStorageFilePath, verificationKeyLocalFilePath, Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB));
2894
3274
  spinner.succeed(`Verification key correctly saved on storage`);
@@ -2914,7 +3294,7 @@ const handleVerifierSmartContract = async (cloudFunctions, bucketName, finalZkey
2914
3294
  spinner.text = `Writing verifier smart contract...`;
2915
3295
  // Write the verification key locally.
2916
3296
  writeFile(verifierContractLocalFilePath, verifierCode);
2917
- await sleep(3000); // workaound for file descriptor.
3297
+ await sleep(3000); // workaround for file descriptor.
2918
3298
  // Upload verifier smart contract to storage.
2919
3299
  await multiPartUpload(cloudFunctions, bucketName, verifierContractStorageFilePath, verifierContractLocalFilePath, Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB));
2920
3300
  spinner.succeed(`Verifier smart contract correctly saved on storage`);
@@ -2935,11 +3315,12 @@ const handleVerifierSmartContract = async (cloudFunctions, bucketName, finalZkey
2935
3315
  * @param participant <FirebaseDocumentInfo> - the Firestore document of the participant (coordinator).
2936
3316
  * @param beacon <string> - the value used to compute the final contribution while finalizing the ceremony.
2937
3317
  * @param coordinatorIdentifier <string> - the identifier of the coordinator.
3318
+ * @param circuitsLength <number> - the number of circuits in the ceremony.
2938
3319
  */
2939
- const handleCircuitFinalization = async (cloudFunctions, firestoreDatabase, ceremony, circuit, participant, beacon, coordinatorIdentifier) => {
3320
+ const handleCircuitFinalization = async (cloudFunctions, firestoreDatabase, ceremony, circuit, participant, beacon, coordinatorIdentifier, circuitsLength) => {
2940
3321
  // Step (1).
2941
- await handleStartOrResumeContribution(cloudFunctions, firestoreDatabase, ceremony, circuit, participant, computeSHA256ToHex(beacon), coordinatorIdentifier, true);
2942
- await sleep(2000); // workaound for descriptors.
3322
+ await handleStartOrResumeContribution(cloudFunctions, firestoreDatabase, ceremony, circuit, participant, computeSHA256ToHex(beacon), coordinatorIdentifier, true, circuitsLength);
3323
+ await sleep(2000); // workaround for descriptors.
2943
3324
  // Extract data.
2944
3325
  const { prefix: circuitPrefix } = circuit.data;
2945
3326
  const { prefix: ceremonyPrefix } = ceremony.data;
@@ -2976,7 +3357,7 @@ const handleCircuitFinalization = async (cloudFunctions, firestoreDatabase, cere
2976
3357
  const finalize = async (opt) => {
2977
3358
  const { firebaseApp, firebaseFunctions, firestoreDatabase } = await bootstrapCommandExecutionAndServices();
2978
3359
  // Check for authentication.
2979
- const auth = opt.auth;
3360
+ const { auth } = opt;
2980
3361
  const { user, providerUserId, token: coordinatorAccessToken } = auth ? await authWithToken(firebaseApp, auth) : await checkAuth(firebaseApp);
2981
3362
  // Preserve command execution only for coordinators.
2982
3363
  if (!(await isCoordinator(user)))
@@ -2988,7 +3369,7 @@ const finalize = async (opt) => {
2988
3369
  showError(COMMAND_ERRORS.COMMAND_FINALIZED_NO_CLOSED_CEREMONIES, true);
2989
3370
  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`);
2990
3371
  // Prompt for ceremony selection.
2991
- const selectedCeremony = await promptForCeremonySelection(ceremoniesClosedForFinalization, true);
3372
+ const selectedCeremony = await promptForCeremonySelection(ceremoniesClosedForFinalization, true, "Which ceremony would you like to finalize?");
2992
3373
  // Get coordinator participant document.
2993
3374
  let participant = await getDocumentById(firestoreDatabase, getParticipantsCollectionPath(selectedCeremony.id), user.uid);
2994
3375
  const isCoordinatorReadyForCeremonyFinalization = await checkAndPrepareCoordinatorForFinalization(firebaseFunctions, selectedCeremony.id);
@@ -3012,7 +3393,7 @@ const finalize = async (opt) => {
3012
3393
  const circuits = await getCeremonyCircuits(firestoreDatabase, selectedCeremony.id);
3013
3394
  // Handle finalization for each ceremony circuit.
3014
3395
  for await (const circuit of circuits)
3015
- await handleCircuitFinalization(firebaseFunctions, firestoreDatabase, selectedCeremony, circuit, participant, beacon, providerUserId);
3396
+ await handleCircuitFinalization(firebaseFunctions, firestoreDatabase, selectedCeremony, circuit, participant, beacon, providerUserId, circuits.length);
3016
3397
  process.stdout.write(`\n`);
3017
3398
  const spinner = customSpinner(`Wrapping up the finalization of the ceremony...`, "clock");
3018
3399
  spinner.start();
@@ -3088,7 +3469,9 @@ const logout = async () => {
3088
3469
  const auth = getAuth();
3089
3470
  await signOut(auth);
3090
3471
  // Delete local token.
3472
+ deleteLocalAuthMethod();
3091
3473
  deleteLocalAccessToken();
3474
+ deleteLocalBandadaIdentity();
3092
3475
  await sleep(3000); // ~3s.
3093
3476
  spinner.stop();
3094
3477
  console.log(`${theme.symbols.success} Logout successfully completed`);
@@ -3150,6 +3533,55 @@ const listCeremonies = async () => {
3150
3533
  }
3151
3534
  };
3152
3535
 
3536
+ const listParticipants = async () => {
3537
+ try {
3538
+ const { firestoreDatabase } = await bootstrapCommandExecutionAndServices();
3539
+ const allCeremonies = await getAllCeremonies(firestoreDatabase);
3540
+ const selectedCeremony = await promptForCeremonySelection(allCeremonies, true, "Which ceremony would you like to see participants?");
3541
+ const docRef = doc(firestoreDatabase, commonTerms.collections.ceremonies.name, selectedCeremony.id);
3542
+ const participantsRef = collection(docRef, "participants");
3543
+ const participantsSnapshot = await getDocs(participantsRef);
3544
+ const participants = participantsSnapshot.docs.map((participantDoc) => participantDoc.data());
3545
+ const usersRef = collection(firestoreDatabase, "users");
3546
+ const usersSnapshot = await getDocs(usersRef);
3547
+ const users = usersSnapshot.docs.map((userDoc) => {
3548
+ const data = userDoc.data();
3549
+ return { id: userDoc.id, ...data };
3550
+ });
3551
+ const participantDetails = participants
3552
+ .map((participant) => {
3553
+ const user = users.find((_user) => _user.id === participant.userId);
3554
+ if (!user)
3555
+ return null;
3556
+ return {
3557
+ id: user.name,
3558
+ status: participant.status,
3559
+ contributionStep: participant.contributionStep,
3560
+ lastUpdated: new Date(participant.lastUpdated)
3561
+ };
3562
+ })
3563
+ .filter((user) => user !== null);
3564
+ const participantsDone = participantDetails.filter((participant) => participant.status === "DONE");
3565
+ console.log(participantDetails);
3566
+ console.log(`${theme.text.underlined("Total participants:")} ${participantDetails.length}`);
3567
+ console.log(`${theme.text.underlined("Total participants finished:")} ${participantsDone.length}`);
3568
+ }
3569
+ catch (err) {
3570
+ showError(`Something went wrong: ${err.toString()}`, true);
3571
+ }
3572
+ process.exit(0);
3573
+ };
3574
+
3575
+ const setCeremonyCommands = (program) => {
3576
+ const ceremony = program.command("ceremony").description("manage ceremonies");
3577
+ ceremony
3578
+ .command("participants")
3579
+ .description("retrieve participants list of a ceremony")
3580
+ .requiredOption("-c, --ceremony <string>", "the prefix of the ceremony you want to retrieve information about", "")
3581
+ .action(listParticipants);
3582
+ return ceremony;
3583
+ };
3584
+
3153
3585
  // Get pkg info (e.g., name, version).
3154
3586
  const packagePath = `${dirname(fileURLToPath(import.meta.url))}/..`;
3155
3587
  const { description, version, name } = JSON.parse(readFileSync(`${packagePath}/package.json`, "utf8"));
@@ -3158,6 +3590,14 @@ const program = createCommand();
3158
3590
  program.name(name).description(description).version(version);
3159
3591
  // User commands.
3160
3592
  program.command("auth").description("authenticate yourself using your Github account (OAuth 2.0)").action(auth);
3593
+ program
3594
+ .command("auth-bandada")
3595
+ .description("authenticate yourself in a privacy-perserving manner using Bandada")
3596
+ .action(authBandada);
3597
+ program
3598
+ .command("auth-siwe")
3599
+ .description("authenticate yourself using your Ethereum account (Sign In With Ethereum - SIWE)")
3600
+ .action(authSIWE);
3161
3601
  program
3162
3602
  .command("contribute")
3163
3603
  .description("compute contributions for a Phase2 Trusted Setup ceremony circuits")
@@ -3169,35 +3609,33 @@ program
3169
3609
  .command("clean")
3170
3610
  .description("clean up output generated by commands from the current working directory")
3171
3611
  .action(clean);
3172
- program
3173
- .command("list")
3174
- .description("List all ceremonies prefixes")
3175
- .action(listCeremonies);
3612
+ program.command("list").description("List all ceremonies prefixes").action(listCeremonies);
3176
3613
  program
3177
3614
  .command("logout")
3178
3615
  .description("sign out from Firebae Auth service and delete Github OAuth 2.0 token from local storage")
3179
3616
  .action(logout);
3180
3617
  program
3181
3618
  .command("validate")
3182
- .description("Validate that a Ceremony Setup file is correct")
3619
+ .description("validate that a Ceremony Setup file is correct")
3183
3620
  .requiredOption("-t, --template <path>", "The path to the ceremony setup template", "")
3184
3621
  .option("-c, --constraints <number>", "The number of constraints to check against")
3185
3622
  .action(validate);
3186
3623
  // Only coordinator commands.
3187
- const ceremony = program.command("coordinate").description("commands for coordinating a ceremony");
3188
- ceremony
3624
+ const coordinate = program.command("coordinate").description("commands for coordinating a ceremony");
3625
+ coordinate
3189
3626
  .command("setup")
3190
3627
  .description("setup a Groth16 Phase 2 Trusted Setup ceremony for zk-SNARK circuits")
3191
- .option('-t, --template <path>', 'The path to the ceremony setup template', '')
3192
- .option('-a, --auth <string>', 'The Github OAuth 2.0 token', '')
3628
+ .option("-t, --template <path>", "The path to the ceremony setup template", "")
3629
+ .option("-a, --auth <string>", "The Github OAuth 2.0 token", "")
3193
3630
  .action(setup);
3194
- ceremony
3631
+ coordinate
3195
3632
  .command("observe")
3196
3633
  .description("observe in real-time the waiting queue of each ceremony circuit")
3197
3634
  .action(observe);
3198
- ceremony
3635
+ coordinate
3199
3636
  .command("finalize")
3200
3637
  .description("finalize a Phase2 Trusted Setup ceremony by applying a beacon, exporting verification key and verifier contract")
3201
3638
  .option("-a, --auth <string>", "the Github OAuth 2.0 token", "")
3202
3639
  .action(finalize);
3640
+ setCeremonyCommands(program);
3203
3641
  program.parseAsync(process.argv);