@devtion/devcli 0.0.0-92056fa → 0.0.0-9239207

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 +1 -1
  2. package/dist/.env +17 -3
  3. package/dist/index.js +549 -107
  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 +3 -2
  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 +1 -0
  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 +10 -3
  20. package/src/commands/auth.ts +25 -9
  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 +55 -28
  26. package/src/commands/finalize.ts +26 -13
  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 +62 -30
  32. package/src/commands/validate.ts +1 -2
  33. package/src/index.ts +35 -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 +48 -12
  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.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 { 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.
@@ -365,6 +417,12 @@ const getVerificationKeyLocalFilePath = (completeFilename) => `${verificationKey
365
417
  * @returns <string> - the complete final verifier contract path to the file.
366
418
  */
367
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}`;
368
426
  /**
369
427
  * Get the final transcript file path.
370
428
  * @param completeFilename <string> - the complete filename of the file (name.ext).
@@ -414,7 +472,7 @@ const getGithubAuthenticatedUserGists = async (githubToken, params) => {
414
472
  headers: {
415
473
  authorization: `token ${githubToken}`
416
474
  },
417
- per_page: params.perPage,
475
+ per_page: params.perPage, // max items per page = 100.
418
476
  page: params.page
419
477
  });
420
478
  if (response && response.status === 200)
@@ -462,9 +520,10 @@ const getPublicAttestationGist = async (githubToken, publicAttestationFilename)
462
520
  * @returns <string> - the third-party provider handle of the user.
463
521
  */
464
522
  const getUserHandleFromProviderUserId = (providerUserId) => {
465
- if (providerUserId.indexOf("-") === -1)
466
- showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_GET_GITHUB_ACCOUNT_INFO, true);
467
- return providerUserId.split("-")[0];
523
+ if (providerUserId.indexOf("-") === -1) {
524
+ return providerUserId;
525
+ }
526
+ return providerUserId.substring(0, providerUserId.lastIndexOf("-"));
468
527
  };
469
528
  /**
470
529
  * Return a custom spinner.
@@ -579,8 +638,18 @@ const publishGist = async (token, content, ceremonyTitle, ceremonyPrefix) => {
579
638
  * @returns <string> - the ready to share tweet url.
580
639
  */
581
640
  const generateCustomUrlToTweetAboutParticipation = (ceremonyName, gistUrl, isFinalizing) => isFinalizing
582
- ? `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`
583
- : `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`;
641
+ ? `https://twitter.com/intent/tweet?text=I%20have%20finalized%20the%20${ceremonyName}${ceremonyName.toLowerCase().includes("trusted") ||
642
+ ceremonyName.toLowerCase().includes("setup") ||
643
+ ceremonyName.toLowerCase().includes("phase2") ||
644
+ ceremonyName.toLowerCase().includes("ceremony")
645
+ ? "!"
646
+ : "%20Phase%202%20Trusted%20Setup%20ceremony!"}%20You%20can%20view%20my%20final%20attestation%20here:%20${gistUrl}%20#Ethereum%20#ZKP%20#PSE`
647
+ : `https://twitter.com/intent/tweet?text=I%20contributed%20to%20the%20${ceremonyName}${ceremonyName.toLowerCase().includes("trusted") ||
648
+ ceremonyName.toLowerCase().includes("setup") ||
649
+ ceremonyName.toLowerCase().includes("phase2") ||
650
+ ceremonyName.toLowerCase().includes("ceremony")
651
+ ? "!"
652
+ : "%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`;
584
653
  /**
585
654
  * Return a custom progress bar.
586
655
  * @param type <ProgressBarType> - the type of the progress bar.
@@ -708,13 +777,14 @@ const getLatestUpdatesFromParticipant = async (firestoreDatabase, ceremonyId, pa
708
777
  * @param entropyOrBeaconHash <string> - the entropy or beacon hash (only when finalizing) for the contribution.
709
778
  * @param contributorOrCoordinatorIdentifier <string> - the identifier of the contributor or coordinator (only when finalizing).
710
779
  * @param isFinalizing <boolean> - flag to discriminate between ceremony finalization (true) and contribution (false).
780
+ * @param circuitsLength <number> - the total number of circuits in the ceremony.
711
781
  */
712
- const handleStartOrResumeContribution = async (cloudFunctions, firestoreDatabase, ceremony, circuit, participant, entropyOrBeaconHash, contributorOrCoordinatorIdentifier, isFinalizing) => {
782
+ const handleStartOrResumeContribution = async (cloudFunctions, firestoreDatabase, ceremony, circuit, participant, entropyOrBeaconHash, contributorOrCoordinatorIdentifier, isFinalizing, circuitsLength) => {
713
783
  // Extract data.
714
784
  const { prefix: ceremonyPrefix } = ceremony.data;
715
785
  const { waitingQueue, avgTimings, prefix: circuitPrefix, sequencePosition } = circuit.data;
716
786
  const { completedContributions } = waitingQueue; // = current progress.
717
- console.log(`${theme.text.bold(`\n- Circuit # ${theme.colors.magenta(`${sequencePosition}`)}`)} (Contribution Steps)`);
787
+ console.log(`${theme.text.bold(`\n- Circuit # ${theme.colors.magenta(`${sequencePosition}/${circuitsLength}`)}`)} (Contribution Steps)`);
718
788
  // Get most up-to-date data from the participant document.
719
789
  let participantData = await getLatestUpdatesFromParticipant(firestoreDatabase, ceremony.id, participant.id);
720
790
  const spinner = customSpinner(`${participantData.contributionStep === "DOWNLOADING" /* ParticipantContributionStep.DOWNLOADING */
@@ -760,6 +830,7 @@ const handleStartOrResumeContribution = async (cloudFunctions, firestoreDatabase
760
830
  // Download the latest contribution from bucket.
761
831
  await downloadCeremonyArtifact(cloudFunctions, bucketName, lastZkeyStorageFilePath, lastZkeyLocalFilePath);
762
832
  console.log(`${theme.symbols.success} Contribution ${theme.text.bold(`#${lastZkeyIndex}`)} correctly downloaded`);
833
+ await sleep(3000);
763
834
  // Advance to next contribution step (COMPUTING) if not finalizing.
764
835
  if (!isFinalizing) {
765
836
  spinner.text = `Preparing for contribution computation...`;
@@ -782,16 +853,19 @@ const handleStartOrResumeContribution = async (cloudFunctions, firestoreDatabase
782
853
  spinner.start();
783
854
  // Read local transcript file info to get the contribution hash.
784
855
  const transcriptContents = readFile(transcriptLocalFilePath);
785
- const matchContributionHash = transcriptContents.match(/Contribution.+Hash.+\n\t\t.+\n\t\t.+\n.+\n\t\t.+\n/);
856
+ const matchContributionHash = transcriptContents.match(contribHashRegex);
786
857
  if (!matchContributionHash)
787
858
  showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_FINALIZE_NO_TRANSCRIPT_CONTRIBUTION_HASH_MATCH, true);
788
859
  // Format contribution hash.
789
860
  const contributionHash = matchContributionHash?.at(0)?.replace("\n\t\t", "");
861
+ await sleep(500);
790
862
  // Make request to cloud functions to permanently store the information.
791
863
  await permanentlyStoreCurrentContributionTimeAndHash(cloudFunctions, ceremony.id, computingTime, contributionHash);
792
864
  // Format computing time.
793
865
  const { seconds: computationSeconds, minutes: computationMinutes, hours: computationHours } = getSecondsMinutesHoursFromMillis(computingTime);
794
866
  spinner.succeed(`${isFinalizing ? "Contribution" : `Contribution ${theme.text.bold(`#${nextZkeyIndex}`)}`} computation took ${theme.text.bold(`${convertToDoubleDigits(computationHours)}:${convertToDoubleDigits(computationMinutes)}:${convertToDoubleDigits(computationSeconds)}`)}`);
867
+ // ensure the previous step is completed
868
+ await sleep(5000);
795
869
  // Advance to next contribution step (UPLOADING) if not finalizing.
796
870
  if (!isFinalizing) {
797
871
  spinner.text = `Preparing for uploading the contribution...`;
@@ -807,12 +881,17 @@ const handleStartOrResumeContribution = async (cloudFunctions, firestoreDatabase
807
881
  console.log(`${theme.symbols.success} Contribution ${theme.text.bold(`#${nextZkeyIndex}`)} already computed`);
808
882
  // Contribution step = UPLOADING.
809
883
  if (isFinalizing || participantData.contributionStep === "UPLOADING" /* ParticipantContributionStep.UPLOADING */) {
810
- 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.`;
884
+ 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.`;
811
885
  spinner.start();
812
- if (!isFinalizing)
813
- await multiPartUpload(cloudFunctions, bucketName, nextZkeyStorageFilePath, nextZkeyLocalFilePath, Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB), ceremony.id, participantData.tempContributionData);
886
+ const progressBar = customProgressBar(ProgressBarType.UPLOAD, `your contribution`);
887
+ if (!isFinalizing) {
888
+ await multiPartUpload(cloudFunctions, bucketName, nextZkeyStorageFilePath, nextZkeyLocalFilePath, Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB), ceremony.id, participantData.tempContributionData, progressBar);
889
+ progressBar.stop();
890
+ }
814
891
  else
815
892
  await multiPartUpload(cloudFunctions, bucketName, nextZkeyStorageFilePath, nextZkeyLocalFilePath, Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB));
893
+ // small sleep to ensure the previous step is completed
894
+ await sleep(5000);
816
895
  spinner.succeed(`${isFinalizing ? `Contribution` : `Contribution ${theme.text.bold(`#${nextZkeyIndex}`)}`} correctly saved to storage`);
817
896
  // Advance to next contribution step (VERIFYING) if not finalizing.
818
897
  if (!isFinalizing) {
@@ -990,7 +1069,7 @@ const promptCircomCompiler = async () => {
990
1069
  * Shows a list of circuits for a single option selection.
991
1070
  * @dev the circuit names are derived from local R1CS files.
992
1071
  * @param options <Array<string>> - an array of circuits names.
993
- * @returns Promise<string> - the name of the choosen circuit.
1072
+ * @returns Promise<string> - the name of the chosen circuit.
994
1073
  */
995
1074
  const promptCircuitSelector = async (options) => {
996
1075
  const { circuitFilename } = await prompts({
@@ -1008,7 +1087,7 @@ const promptCircuitSelector = async (options) => {
1008
1087
  * Shows a list of standard EC2 VM instance types for a single option selection.
1009
1088
  * @notice the suggested VM configuration type is calculated based on circuit constraint size.
1010
1089
  * @param constraintSize <number> - the amount of circuit constraints
1011
- * @returns Promise<string> - the name of the choosen VM type.
1090
+ * @returns Promise<string> - the name of the chosen VM type.
1012
1091
  */
1013
1092
  const promptVMTypeSelector = async (constraintSize) => {
1014
1093
  let suggestedConfiguration = 0;
@@ -1105,7 +1184,7 @@ const promptVMDiskTypeSelector = async () => {
1105
1184
  /**
1106
1185
  * Show a series of questions about the circuits.
1107
1186
  * @param constraintSize <number> - the amount of circuit constraints.
1108
- * @param timeoutMechanismType <CeremonyTimeoutType> - the choosen timeout mechanism type for the ceremony.
1187
+ * @param timeoutMechanismType <CeremonyTimeoutType> - the chosen timeout mechanism type for the ceremony.
1109
1188
  * @param needPromptCircomCompiler <boolean> - a boolean value indicating if the questions related to the Circom compiler version and commit hash must be asked.
1110
1189
  * @param enforceVM <boolean> - a boolean value indicating if the contribution verification could be supported by VM-only approach or not.
1111
1190
  * @returns Promise<Array<Circuit>> - circuit info prompted by the coordinator.
@@ -1118,7 +1197,7 @@ const promptCircuitInputData = async (constraintSize, timeoutMechanismType, same
1118
1197
  let circomVersion = "";
1119
1198
  let circomCommitHash = "";
1120
1199
  let circuitInputData;
1121
- let useCfOrVm;
1200
+ let cfOrVm;
1122
1201
  let vmDiskType;
1123
1202
  let vmConfigurationType = "";
1124
1203
  const questions = [
@@ -1173,18 +1252,21 @@ const promptCircuitInputData = async (constraintSize, timeoutMechanismType, same
1173
1252
  circomVersion = version;
1174
1253
  circomCommitHash = commitHash;
1175
1254
  }
1176
- // Ask for prefered contribution verification method (CF vs VM).
1255
+ // Ask for preferred contribution verification method (CF vs VM).
1177
1256
  if (!enforceVM) {
1178
1257
  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.
1179
1258
  `VM` // eq. false.
1180
1259
  );
1181
- useCfOrVm = confirmation;
1260
+ cfOrVm = confirmation
1261
+ ? "CF" /* CircuitContributionVerificationMechanism.CF */
1262
+ : "VM" /* CircuitContributionVerificationMechanism.VM */;
1182
1263
  }
1183
- else
1184
- useCfOrVm = "VM" /* CircuitContributionVerificationMechanism.VM */;
1185
- if (useCfOrVm === undefined)
1264
+ else {
1265
+ cfOrVm = "VM" /* CircuitContributionVerificationMechanism.VM */;
1266
+ }
1267
+ if (cfOrVm === undefined)
1186
1268
  showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
1187
- if (!useCfOrVm) {
1269
+ if (cfOrVm === "VM" /* CircuitContributionVerificationMechanism.VM */) {
1188
1270
  // Ask for selecting the specific VM configuration type.
1189
1271
  vmConfigurationType = await promptVMTypeSelector(constraintSize);
1190
1272
  // Ask for selecting the specific VM disk (volume) type.
@@ -1218,9 +1300,7 @@ const promptCircuitInputData = async (constraintSize, timeoutMechanismType, same
1218
1300
  paramsConfiguration: circuitConfigurationValues
1219
1301
  },
1220
1302
  verification: {
1221
- cfOrVm: useCfOrVm
1222
- ? "CF" /* CircuitContributionVerificationMechanism.CF */
1223
- : "VM" /* CircuitContributionVerificationMechanism.VM */,
1303
+ cfOrVm,
1224
1304
  vm: {
1225
1305
  vmConfigurationType,
1226
1306
  vmDiskType
@@ -1256,9 +1336,7 @@ const promptCircuitInputData = async (constraintSize, timeoutMechanismType, same
1256
1336
  paramsConfiguration: circuitConfigurationValues
1257
1337
  },
1258
1338
  verification: {
1259
- cfOrVm: useCfOrVm
1260
- ? "CF" /* CircuitContributionVerificationMechanism.CF */
1261
- : "VM" /* CircuitContributionVerificationMechanism.VM */,
1339
+ cfOrVm,
1262
1340
  vm: {
1263
1341
  vmConfigurationType,
1264
1342
  vmDiskType
@@ -1302,7 +1380,7 @@ const promptCircuitAddition = async () => {
1302
1380
  * Shows a list of pre-computed zKeys for a single option selection.
1303
1381
  * @dev the names are derived from local zKeys files.
1304
1382
  * @param options <Array<string>> - an array of pre-computed zKeys names.
1305
- * @returns Promise<string> - the name of the choosen pre-computed zKey.
1383
+ * @returns Promise<string> - the name of the chosen pre-computed zKey.
1306
1384
  */
1307
1385
  const promptPreComputedZkeySelector = async (options) => {
1308
1386
  const { preComputedZkeyFilename } = await prompts({
@@ -1340,13 +1418,13 @@ const promptNeededPowersForCircuit = async (suggestedSmallestNeededPowers) => {
1340
1418
  * Shows a list of PoT files for a single option selection.
1341
1419
  * @dev the names are derived from local PoT files.
1342
1420
  * @param options <Array<string>> - an array of PoT file names.
1343
- * @returns Promise<string> - the name of the choosen PoT.
1421
+ * @returns Promise<string> - the name of the chosen PoT.
1344
1422
  */
1345
1423
  const promptPotSelector = async (options) => {
1346
1424
  const { potFilename } = await prompts({
1347
1425
  type: "select",
1348
1426
  name: "potFilename",
1349
- message: theme.text.bold("Select the Powers of Tau file choosen for the circuit"),
1427
+ message: theme.text.bold("Select the Powers of Tau file chosen for the circuit"),
1350
1428
  choices: options.map((option) => {
1351
1429
  console.log(option);
1352
1430
  return { title: option, value: option };
@@ -1364,7 +1442,7 @@ const promptPotSelector = async (options) => {
1364
1442
  * @param isFinalizing <boolean> - true when the coordinator must select a ceremony for finalization; otherwise false (participant selects a ceremony for contribution).
1365
1443
  * @returns Promise<FirebaseDocumentInfo> - the Firestore document of the selected ceremony.
1366
1444
  */
1367
- const promptForCeremonySelection = async (ceremoniesDocuments, isFinalizing) => {
1445
+ const promptForCeremonySelection = async (ceremoniesDocuments, isFinalizing, messageToDisplay) => {
1368
1446
  // Prepare state.
1369
1447
  const choices = [];
1370
1448
  // Prepare choices x ceremony.
@@ -1382,9 +1460,7 @@ const promptForCeremonySelection = async (ceremoniesDocuments, isFinalizing) =>
1382
1460
  const { ceremony } = await prompts({
1383
1461
  type: "select",
1384
1462
  name: "ceremony",
1385
- message: theme.text.bold(!isFinalizing
1386
- ? "Which ceremony would you like to contribute to?"
1387
- : "Which ceremony would you like to finalize?"),
1463
+ message: theme.text.bold(messageToDisplay),
1388
1464
  choices,
1389
1465
  initial: 0
1390
1466
  });
@@ -1416,7 +1492,7 @@ const promptToTypeEntropyOrBeacon = async (isEntropy = true) => {
1416
1492
  * @return <Promise<string>> - the entropy.
1417
1493
  */
1418
1494
  const promptForEntropy = async () => {
1419
- // Prompt for entropy generation prefered method.
1495
+ // Prompt for entropy generation preferred method.
1420
1496
  const { confirmation } = await askForConfirmation(`Do you prefer to type your entropy or generate it randomly?`, "Manually", "Randomly");
1421
1497
  if (confirmation === undefined)
1422
1498
  showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
@@ -1535,16 +1611,37 @@ const checkAuth = async (firebaseApp) => {
1535
1611
  showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_NOT_AUTHENTICATED, true);
1536
1612
  // Retrieve local access token.
1537
1613
  const token = String(getLocalAccessToken());
1538
- // Get credentials.
1539
- const credentials = exchangeGithubTokenForCredentials(token);
1540
- // Sign in to Firebase using credentials.
1541
- await signInToFirebase(firebaseApp, credentials);
1614
+ let providerUserId;
1615
+ let username;
1616
+ const authMethod = getLocalAuthMethod();
1617
+ switch (authMethod) {
1618
+ case "github": {
1619
+ // Get credentials.
1620
+ const credentials = exchangeGithubTokenForCredentials(token);
1621
+ // Sign in to Firebase using credentials.
1622
+ await signInToFirebase(firebaseApp, credentials);
1623
+ // Get Github unique identifier (handle-id).
1624
+ providerUserId = await getGithubProviderUserId(String(token));
1625
+ username = getUserHandleFromProviderUserId(providerUserId);
1626
+ break;
1627
+ }
1628
+ case "bandada": {
1629
+ const userCredentials = await signInWithCustomToken(getAuth(), token);
1630
+ providerUserId = userCredentials.user.uid;
1631
+ username = providerUserId;
1632
+ break;
1633
+ }
1634
+ case "siwe": {
1635
+ const userCredentials = await signInWithCustomToken(getAuth(), token);
1636
+ providerUserId = userCredentials.user.uid;
1637
+ username = providerUserId;
1638
+ break;
1639
+ }
1640
+ }
1542
1641
  // Get current authenticated user.
1543
1642
  const user = getCurrentFirebaseAuthUser(firebaseApp);
1544
- // Get Github unique identifier (handle-id).
1545
- const providerUserId = await getGithubProviderUserId(String(token));
1546
1643
  // Greet the user.
1547
- console.log(`Greetings, @${theme.text.bold(getUserHandleFromProviderUserId(providerUserId))} ${theme.emojis.wave}\n`);
1644
+ console.log(`Greetings, @${theme.text.bold(username)} ${theme.emojis.wave}\n`);
1548
1645
  return {
1549
1646
  user,
1550
1647
  token,
@@ -1633,7 +1730,7 @@ const handleAdditionOfCircuitsToCeremony = async (r1csOptions, wasmOptions, cere
1633
1730
  wasmFilename.split(`.${commonTerms.foldersAndPathsTerms.wasm}`)[0]);
1634
1731
  if (matchingWasms.length !== 1)
1635
1732
  showError(COMMAND_ERRORS.COMMAND_SETUP_MISMATCH_R1CS_WASM, true);
1636
- // Get input data for choosen circuit.
1733
+ // Get input data for chosen circuit.
1637
1734
  const circuitInputData = await getInputDataToAddCircuitToCeremony(choosenCircuitFilename, matchingWasms[0], ceremonyTimeoutMechanismType, sameCircomCompiler, circuitSequencePosition, sharedCircomCompilerData);
1638
1735
  // Store circuit data.
1639
1736
  inputDataForCircuits.push(circuitInputData);
@@ -1686,7 +1783,7 @@ const displayCeremonySummary = (ceremonyInputData, circuits) => {
1686
1783
  };
1687
1784
  /**
1688
1785
  * Check if the smallest Powers of Tau has already been downloaded/stored in the correspondent local path
1689
- * @dev we are downloading the Powers of Tau file from Hermez Cryptography Phase 1 Trusted Setup.
1786
+ * @dev we are downloading the Powers of Tau file from Perpetual Powers of Tau Phase 1 Trusted Setup.
1690
1787
  * @param powers <string> - the smallest amount of powers needed for the given circuit (should be in a 'XY' stringified form).
1691
1788
  * @param ptauCompleteFilename <string> - the complete file name of the powers of tau file to be downloaded.
1692
1789
  * @returns <Promise<void>>
@@ -1700,7 +1797,7 @@ const checkAndDownloadSmallestPowersOfTau = async (powers, ptauCompleteFilename)
1700
1797
  .map((dirent) => dirent.name);
1701
1798
  // Check if already downloaded or not.
1702
1799
  if (smallestPtauFileForGivenPowers.length === 0) {
1703
- const spinner = customSpinner(`Downloading the ${theme.text.bold(`#${powers}`)} smallest PoT file needed from the Hermez Cryptography Phase 1 Trusted Setup...`, `clock`);
1800
+ const spinner = customSpinner(`Downloading the ${theme.text.bold(`#${powers}`)} smallest PoT file needed from the Perpetual Powers of Tau Phase 1 Trusted Setup...`, `clock`);
1704
1801
  spinner.start();
1705
1802
  // Download smallest Powers of Tau file from remote server.
1706
1803
  const streamPipeline = promisify(pipeline);
@@ -1723,7 +1820,7 @@ const checkAndDownloadSmallestPowersOfTau = async (powers, ptauCompleteFilename)
1723
1820
  * number of powers greater than or equal to the powers needed by the zKey), the coordinator will be asked
1724
1821
  * to provide a number of powers manually, ranging from the smallest possible to the largest.
1725
1822
  * @param neededPowers <number> - the smallest amount of powers needed by the zKey.
1726
- * @returns Promise<string, string> - the information about the choosen Powers of Tau file for the pre-computed zKey
1823
+ * @returns Promise<string, string> - the information about the chosen Powers of Tau file for the pre-computed zKey
1727
1824
  * along with related powers.
1728
1825
  */
1729
1826
  const handlePreComputedZkeyPowersOfTauSelection = async (neededPowers) => {
@@ -1815,7 +1912,7 @@ const handleCircuitArtifactUploadToStorage = async (firebaseFunctions, bucketNam
1815
1912
  * @notice The setup command allows the coordinator of the ceremony to prepare the next ceremony by interacting with the CLI.
1816
1913
  * @dev For proper execution, the command must be run in a folder containing the R1CS files related to the circuits
1817
1914
  * for which the coordinator wants to create the ceremony. The command will download the necessary Tau powers
1818
- * from Hermez's ceremony Phase 1 Reliable Setup Ceremony.
1915
+ * from PPoT ceremony Phase 1 Setup Ceremony.
1819
1916
  * @param cmd? <any> - the path to the ceremony setup file.
1820
1917
  */
1821
1918
  const setup = async (cmd) => {
@@ -1824,7 +1921,9 @@ const setup = async (cmd) => {
1824
1921
  let ceremonyId = ""; // The unique identifier of the ceremony.
1825
1922
  const { firebaseApp, firebaseFunctions, firestoreDatabase } = await bootstrapCommandExecutionAndServices();
1826
1923
  // Check for authentication.
1827
- const { user, providerUserId } = cmd.auth ? await authWithToken(firebaseApp, cmd.auth) : await checkAuth(firebaseApp);
1924
+ const { user, providerUserId } = cmd.auth
1925
+ ? await authWithToken(firebaseApp, cmd.auth)
1926
+ : await checkAuth(firebaseApp);
1828
1927
  // Preserve command execution only for coordinators.
1829
1928
  if (!(await isCoordinator(user)))
1830
1929
  showError(COMMAND_ERRORS.COMMAND_NOT_COORDINATOR, true);
@@ -1841,7 +1940,7 @@ const setup = async (cmd) => {
1841
1940
  // if there is the file option, then set up the non interactively
1842
1941
  if (cmd.template) {
1843
1942
  // 1. parse the file
1844
- // tmp data - do not cleanup files as we need them
1943
+ // tmp data - do not cleanup files as we need them
1845
1944
  const spinner = customSpinner(`Parsing ${theme.text.bold(cmd.template)} setup configuration file...`, `clock`);
1846
1945
  spinner.start();
1847
1946
  const setupCeremonyData = await parseCeremonyFile(cmd.template);
@@ -1864,12 +1963,20 @@ const setup = async (cmd) => {
1864
1963
  // 3. generate the zKey
1865
1964
  const spinner = customSpinner(`Generating genesis zKey for circuit ${theme.text.bold(circuit.name)}...`, `clock`);
1866
1965
  spinner.start();
1867
- await zKey.newZKey(r1csLocalPathAndFileName, getPotLocalFilePath(circuit.files.potFilename), zkeyLocalPathAndFileName, undefined);
1868
- spinner.succeed(`Generation of the genesis zKey for citcui ${theme.text.bold(circuit.name)} completed successfully`);
1966
+ if (existsSync(zkeyLocalPathAndFileName)) {
1967
+ spinner.succeed(`The genesis zKey for circuit ${theme.text.bold(circuit.name)} is already present on disk`);
1968
+ }
1969
+ else {
1970
+ await zKey.newZKey(r1csLocalPathAndFileName, getPotLocalFilePath(circuit.files.potFilename), zkeyLocalPathAndFileName, undefined);
1971
+ spinner.succeed(`Generation of the genesis zKey for circuit ${theme.text.bold(circuit.name)} completed successfully`);
1972
+ }
1973
+ const hashSpinner = customSpinner(`Calculating hashes for circuit ${theme.text.bold(circuit.name)}...`, `clock`);
1974
+ hashSpinner.start();
1869
1975
  // 4. calculate the hashes
1870
1976
  const wasmBlake2bHash = await blake512FromPath(wasmLocalPathAndFileName);
1871
1977
  const potBlake2bHash = await blake512FromPath(getPotLocalFilePath(circuit.files.potFilename));
1872
1978
  const initialZkeyBlake2bHash = await blake512FromPath(zkeyLocalPathAndFileName);
1979
+ hashSpinner.succeed(`Hashes for circuit ${theme.text.bold(circuit.name)} calculated successfully`);
1873
1980
  // 5. upload the artifacts
1874
1981
  // Upload zKey to Storage.
1875
1982
  await handleCircuitArtifactUploadToStorage(firebaseFunctions, bucketName, circuit.files.initialZkeyStoragePath, zkeyLocalPathAndFileName, circuit.files.initialZkeyFilename);
@@ -1887,9 +1994,9 @@ const setup = async (cmd) => {
1887
1994
  // 6 update the setup data object
1888
1995
  ceremonySetupData.circuits[index].files = {
1889
1996
  ...circuit.files,
1890
- potBlake2bHash: potBlake2bHash,
1891
- wasmBlake2bHash: wasmBlake2bHash,
1892
- initialZkeyBlake2bHash: initialZkeyBlake2bHash
1997
+ potBlake2bHash,
1998
+ wasmBlake2bHash,
1999
+ initialZkeyBlake2bHash
1893
2000
  };
1894
2001
  ceremonySetupData.circuits[index].zKeySizeInBytes = getFileStats(zkeyLocalPathAndFileName).size;
1895
2002
  }
@@ -2097,17 +2204,29 @@ const expirationCountdownForGithubOAuth = (expirationInSeconds) => {
2097
2204
  */
2098
2205
  const onVerification = async (verification) => {
2099
2206
  // Copy code to clipboard.
2100
- clipboard.writeSync(verification.user_code);
2101
- clipboard.readSync();
2207
+ let noClipboard = false;
2208
+ try {
2209
+ clipboard.writeSync(verification.user_code);
2210
+ clipboard.readSync();
2211
+ }
2212
+ catch (error) {
2213
+ noClipboard = true;
2214
+ }
2102
2215
  // Display data.
2103
2216
  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`);
2104
- console.log(theme.colors.magenta(figlet.textSync("Code is Below", { font: "ANSI Shadow" })), '\n');
2105
- 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`);
2217
+ console.log(theme.colors.magenta(figlet.textSync("Code is Below", { font: "ANSI Shadow" })), "\n");
2218
+ const message = !noClipboard ? `has been copied to your clipboard (${theme.emojis.clipboard})` : ``;
2219
+ console.log(`${theme.symbols.info} Your auth code: ${theme.text.bold(verification.user_code)} ${message} ${theme.symbols.success}\n`);
2106
2220
  const spinner = customSpinner(`Redirecting to Github...`, `clock`);
2107
2221
  spinner.start();
2108
2222
  await sleep(10000); // ~10s to make users able to read the CLI.
2109
- // Automatically open the page (# Step 2).
2110
- await open(verification.verification_uri);
2223
+ try {
2224
+ // Automatically open the page (# Step 2).
2225
+ await open(verification.verification_uri);
2226
+ }
2227
+ catch (error) {
2228
+ console.log(`${theme.symbols.info} Please authenticate via GitHub at ${verification.verification_uri}`);
2229
+ }
2111
2230
  spinner.stop();
2112
2231
  // Countdown for time expiration.
2113
2232
  expirationCountdownForGithubOAuth(verification.expires_in);
@@ -2158,6 +2277,7 @@ const auth = async () => {
2158
2277
  // Generate a new access token using Github Device Flow (OAuth 2.0).
2159
2278
  const newToken = await executeGithubDeviceFlow(String(process.env.AUTH_GITHUB_CLIENT_ID));
2160
2279
  // Store the new access token.
2280
+ setLocalAuthMethod("github");
2161
2281
  setLocalAccessToken(newToken);
2162
2282
  }
2163
2283
  else
@@ -2178,6 +2298,249 @@ const auth = async () => {
2178
2298
  terminate(providerUserId);
2179
2299
  };
2180
2300
 
2301
+ const { BANDADA_API_URL } = process.env;
2302
+ const bandadaApi = new ApiSdk(BANDADA_API_URL);
2303
+ const addMemberToGroup = async (groupId, dashboardUrl, identity) => {
2304
+ const commitment = identity.commitment.toString();
2305
+ const group = await bandadaApi.getGroup(groupId);
2306
+ const providerName = group.credentials.id.split("_")[0].toLowerCase();
2307
+ // 6. open a new window with the url:
2308
+ const url = `${dashboardUrl}credentials?group=${groupId}&member=${commitment}&provider=${providerName}`;
2309
+ console.log(`${theme.text.bold(`Verification URL:`)} ${theme.text.underlined(url)}`);
2310
+ open(url);
2311
+ const { confirmation } = await askForConfirmation("Did you join the Bandada group in the browser?");
2312
+ if (!confirmation)
2313
+ showError("You must join the Bandada group to continue the login process", true);
2314
+ };
2315
+ const isGroupMember = async (groupId, identity) => {
2316
+ const commitment = identity.commitment.toString();
2317
+ const isMember = await bandadaApi.isGroupMember(groupId, commitment);
2318
+ return isMember;
2319
+ };
2320
+
2321
+ const { BANDADA_DASHBOARD_URL, BANDADA_GROUP_ID } = process.env;
2322
+ const authBandada = async () => {
2323
+ try {
2324
+ const { firebaseFunctions } = await bootstrapCommandExecutionAndServices();
2325
+ const spinner = customSpinner(`Checking identity string for Semaphore...`, `clock`);
2326
+ spinner.start();
2327
+ // 1. check if _identity string exists in local storage
2328
+ let identityString;
2329
+ const isIdentityStringStored = checkLocalBandadaIdentity();
2330
+ if (isIdentityStringStored) {
2331
+ identityString = getLocalBandadaIdentity();
2332
+ spinner.succeed(`Identity seed found\n`);
2333
+ }
2334
+ else {
2335
+ spinner.warn(`Identity seed not found\n`);
2336
+ // 2. generate a random _identity string and save it in local storage
2337
+ const { seed } = await prompts({
2338
+ type: "text",
2339
+ name: "seed",
2340
+ message: theme.text.bold(`Enter a secret string to use as your identity seed in Semaphore:`),
2341
+ initial: false
2342
+ });
2343
+ identityString = seed;
2344
+ setLocalBandadaIdentity(identityString);
2345
+ }
2346
+ // 3. create a semaphore identity with _identity string as a seed
2347
+ const identity = new Identity(identityString);
2348
+ // 4. check if the user is a member of the group
2349
+ console.log(`Checking Bandada membership...`);
2350
+ const isMember = await isGroupMember(BANDADA_GROUP_ID, identity);
2351
+ if (!isMember) {
2352
+ await addMemberToGroup(BANDADA_GROUP_ID, BANDADA_DASHBOARD_URL, identity);
2353
+ }
2354
+ // 5. generate a proof that the user owns the commitment.
2355
+ spinner.text = `Generating proof of identity...`;
2356
+ spinner.start();
2357
+ // publicSignals = [hash(externalNullifier, identityNullifier), commitment]
2358
+ const initDirectoryName = getLocalDirname();
2359
+ const directoryName = initDirectoryName.includes("/src") ? "." : initDirectoryName;
2360
+ const { proof, publicSignals } = await groth16.fullProve({
2361
+ identityTrapdoor: identity.trapdoor,
2362
+ identityNullifier: identity.nullifier,
2363
+ externalNullifier: BANDADA_GROUP_ID
2364
+ }, `${directoryName}/public/mini-semaphore.wasm`, `${directoryName}/public/mini-semaphore.zkey`);
2365
+ spinner.succeed(`Proof generated.\n`);
2366
+ spinner.text = `Sending proof to verification...`;
2367
+ spinner.start();
2368
+ // 6. send proof to a cloud function that verifies it and checks membership
2369
+ const cf = httpsCallable(firebaseFunctions, commonTerms.cloudFunctionsNames.bandadaValidateProof);
2370
+ const result = await cf({
2371
+ proof,
2372
+ publicSignals
2373
+ });
2374
+ const { valid, token, message } = result.data;
2375
+ if (!valid) {
2376
+ showError(message, true);
2377
+ deleteLocalAuthMethod();
2378
+ deleteLocalAccessToken();
2379
+ deleteLocalBandadaIdentity();
2380
+ }
2381
+ spinner.succeed(`Proof verified.\n`);
2382
+ spinner.text = `Authenticating...`;
2383
+ spinner.start();
2384
+ // 7. Auth to p0tion firebase
2385
+ const credentials = await signInWithCustomToken(getAuth(), token);
2386
+ setLocalAuthMethod("bandada");
2387
+ setLocalAccessToken(token);
2388
+ spinner.succeed(`Authenticated as ${theme.text.bold(credentials.user.uid)}.`);
2389
+ console.log(`\n${theme.symbols.warning} You can always log out by running the ${theme.text.bold(`phase2cli logout`)} command`);
2390
+ }
2391
+ catch (error) {
2392
+ // Delete local token.
2393
+ console.log("An error crashed the process. Deleting local token and identity.");
2394
+ console.error(error);
2395
+ deleteLocalAuthMethod();
2396
+ deleteLocalAccessToken();
2397
+ deleteLocalBandadaIdentity();
2398
+ }
2399
+ process.exit(0);
2400
+ };
2401
+
2402
+ const showVerificationCodeAndUri = async (OAuthDeviceCode) => {
2403
+ // Copy code to clipboard.
2404
+ let noClipboard = false;
2405
+ try {
2406
+ clipboard.writeSync(OAuthDeviceCode.user_code);
2407
+ clipboard.readSync();
2408
+ }
2409
+ catch (error) {
2410
+ noClipboard = true;
2411
+ }
2412
+ // Display data.
2413
+ 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`);
2414
+ console.log(theme.colors.magenta(figlet.textSync("Code is Below", { font: "ANSI Shadow" })), "\n");
2415
+ const message = !noClipboard ? `has been copied to your clipboard (${theme.emojis.clipboard})` : ``;
2416
+ console.log(`${theme.symbols.info} Your auth code: ${theme.text.bold(OAuthDeviceCode.user_code)} ${message} ${theme.symbols.success}\n`);
2417
+ const spinner = customSpinner(`Redirecting to Sign In With Ethereum...`, `clock`);
2418
+ spinner.start();
2419
+ await sleep(10000); // ~10s to make users able to read the CLI.
2420
+ try {
2421
+ // Automatically open the page (# Step 2).
2422
+ await open(OAuthDeviceCode.verification_uri_complete);
2423
+ }
2424
+ catch (error) {
2425
+ console.log(`${theme.symbols.info} Please authenticate via SIWE at ${OAuthDeviceCode.verification_uri_complete}`);
2426
+ }
2427
+ spinner.stop();
2428
+ };
2429
+ /**
2430
+ * Return the token to sign in to Firebase after passing the SIWE Device Flow
2431
+ * @param clientId <string> - The client id of the Auth0 application.
2432
+ * @param firebaseFunctions <any> - The Firebase functions instance to call the cloud function
2433
+ * @returns <string> - The token to sign in to Firebase
2434
+ */
2435
+ const executeSIWEDeviceFlow = async (clientId, firebaseFunctions) => {
2436
+ // Call Auth0 endpoint to request device code uri
2437
+ const OAuthDeviceCode = (await fetch$1(`${process.env.AUTH0_APPLICATION_URL}/oauth/device/code`, {
2438
+ method: "POST",
2439
+ headers: { "content-type": "application/json" },
2440
+ body: JSON.stringify({
2441
+ client_id: clientId,
2442
+ scope: "openid",
2443
+ audience: `${process.env.AUTH0_APPLICATION_URL}/api/v2/`
2444
+ })
2445
+ }).then((_res) => _res.json()));
2446
+ if (OAuthDeviceCode.error) {
2447
+ showError(OAuthDeviceCode.error_description, true);
2448
+ deleteLocalAuthMethod();
2449
+ deleteLocalAccessToken();
2450
+ }
2451
+ await showVerificationCodeAndUri(OAuthDeviceCode);
2452
+ // Poll Auth0 endpoint until you get token or request expires
2453
+ let isSignedIn = false;
2454
+ let isExpired = false;
2455
+ let auth0Token = "";
2456
+ while (!isSignedIn && !isExpired) {
2457
+ // Call Auth0 endpoint to request token
2458
+ const OAuthToken = (await fetch$1(`${process.env.AUTH0_APPLICATION_URL}/oauth/token`, {
2459
+ method: "POST",
2460
+ headers: { "content-type": "application/json" },
2461
+ body: JSON.stringify({
2462
+ client_id: clientId,
2463
+ device_code: OAuthDeviceCode.device_code,
2464
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
2465
+ })
2466
+ }).then((_res) => _res.json()));
2467
+ if (OAuthToken.error) {
2468
+ if (OAuthToken.error === "authorization_pending") {
2469
+ // Wait for the user to sign in
2470
+ await sleep(OAuthDeviceCode.interval * 1000);
2471
+ }
2472
+ else if (OAuthToken.error === "slow_down") {
2473
+ // Wait for the user to sign in
2474
+ await sleep(OAuthDeviceCode.interval * 1000 * 2);
2475
+ }
2476
+ else if (OAuthToken.error === "expired_token") {
2477
+ // The user didn't sign in on time
2478
+ isExpired = true;
2479
+ }
2480
+ }
2481
+ else {
2482
+ // The user signed in
2483
+ isSignedIn = true;
2484
+ auth0Token = OAuthToken.access_token;
2485
+ }
2486
+ }
2487
+ // Send token to cloud function to check nonce, create user and retrieve token
2488
+ const cf = httpsCallable(firebaseFunctions, commonTerms.cloudFunctionsNames.checkNonceOfSIWEAddress);
2489
+ const result = await cf({
2490
+ auth0Token
2491
+ });
2492
+ const { token, valid, message } = result.data;
2493
+ if (!valid) {
2494
+ showError(message, true);
2495
+ deleteLocalAuthMethod();
2496
+ deleteLocalAccessToken();
2497
+ }
2498
+ return token;
2499
+ };
2500
+ /**
2501
+ * Auth command using Sign In With Ethereum
2502
+ * @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.
2503
+ * @dev Under the hood, the command handles a manual Device Flow following the guidelines in the SIWE documentation.
2504
+ */
2505
+ const authSIWE = async () => {
2506
+ try {
2507
+ const { firebaseFunctions } = await bootstrapCommandExecutionAndServices();
2508
+ // Console more context for the user.
2509
+ 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`);
2510
+ const spinner = customSpinner(`Checking authentication token...`, `clock`);
2511
+ spinner.start();
2512
+ await sleep(5000);
2513
+ // Manage OAuth Github or SIWE token.
2514
+ const isLocalTokenStored = checkLocalAccessToken();
2515
+ if (!isLocalTokenStored) {
2516
+ spinner.fail(`No local authentication token found\n`);
2517
+ // Generate a new access token using Github Device Flow (OAuth 2.0).
2518
+ const newToken = await executeSIWEDeviceFlow(String(process.env.AUTH_SIWE_CLIENT_ID), firebaseFunctions);
2519
+ // Store the new access token.
2520
+ setLocalAuthMethod("siwe");
2521
+ setLocalAccessToken(newToken);
2522
+ }
2523
+ else
2524
+ spinner.succeed(`Local authentication token found\n`);
2525
+ // Get access token from local store.
2526
+ const token = String(getLocalAccessToken());
2527
+ spinner.text = `Authenticating...`;
2528
+ spinner.start();
2529
+ // Exchange token for credential.
2530
+ const credentials = await signInWithCustomToken(getAuth(), token);
2531
+ spinner.succeed(`Authenticated as ${theme.text.bold(credentials.user.uid)}.`);
2532
+ console.log(`\n${theme.symbols.warning} You can always log out by running the ${theme.text.bold(`phase2cli logout`)} command`);
2533
+ process.exit(0);
2534
+ }
2535
+ catch (error) {
2536
+ // Delete local token.
2537
+ console.log("An error crashed the process. Deleting local token and identity.");
2538
+ console.error(error);
2539
+ deleteLocalAuthMethod();
2540
+ deleteLocalAccessToken();
2541
+ }
2542
+ };
2543
+
2181
2544
  /**
2182
2545
  * Return the verification result for latest contribution.
2183
2546
  * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
@@ -2300,8 +2663,8 @@ const handleDiskSpaceRequirementForNextContribution = async (cloudFunctions, cer
2300
2663
  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
2301
2664
  ? theme.text.bold(`< 0.01`)
2302
2665
  : 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`);
2303
- 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");
2304
- wannaContributeOrHaveEnoughMemory = !!confirmation;
2666
+ 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");
2667
+ wannaContributeOrHaveEnoughMemory = !!confirmationEnoughMemory;
2305
2668
  if (circuitSequencePosition > 1) {
2306
2669
  console.log(`${theme.symbols.info} Please note, you have time until ceremony ends to free up your memory and complete remaining contributions`);
2307
2670
  // Asks the contributor if their wants to terminate contributions for the ceremony.
@@ -2369,8 +2732,12 @@ const handlePublicAttestation = async (firestoreDatabase, circuits, ceremonyId,
2369
2732
  // Write public attestation locally.
2370
2733
  writeFile(getAttestationLocalFilePath(`${ceremonyPrefix}_${commonTerms.foldersAndPathsTerms.attestation}.log`), Buffer.from(publicAttestation));
2371
2734
  await sleep(1000); // workaround for file descriptor unexpected close.
2372
- const gistUrl = await publishGist(participantAccessToken, publicAttestation, ceremonyName, ceremonyPrefix);
2373
- console.log(`\n${theme.symbols.info} Your public attestation has been successfully posted as Github Gist (${theme.text.bold(theme.text.underlined(gistUrl))})`);
2735
+ let gistUrl = "";
2736
+ const isGithub = getLocalAuthMethod() === "github";
2737
+ if (isGithub) {
2738
+ gistUrl = await publishGist(participantAccessToken, publicAttestation, ceremonyName, ceremonyPrefix);
2739
+ console.log(`\n${theme.symbols.info} Your public attestation has been successfully posted as Github Gist (${theme.text.bold(theme.text.underlined(gistUrl))})`);
2740
+ }
2374
2741
  // Prepare a ready-to-share tweet.
2375
2742
  await handleTweetGeneration(ceremonyName, gistUrl);
2376
2743
  };
@@ -2423,6 +2790,7 @@ const listenToCeremonyCircuitDocumentChanges = (firestoreDatabase, ceremonyId, p
2423
2790
  }
2424
2791
  });
2425
2792
  };
2793
+ let contributionInProgress = false;
2426
2794
  /**
2427
2795
  * Listen to current authenticated participant document changes.
2428
2796
  * @dev this is the core business logic related to the execution of the contribute command.
@@ -2550,11 +2918,21 @@ const listenToParticipantDocumentChanges = async (firestoreDatabase, cloudFuncti
2550
2918
  (!noTemporaryContributionData && resumingWithSameTemporaryData);
2551
2919
  // Scenario (3.B).
2552
2920
  if (isCurrentContributor && hasResumableStep && startingOrResumingContribution) {
2921
+ if (contributionInProgress) {
2922
+ console.warn(`\n${theme.symbols.warning} Received instruction to start/resume contribution but contribution is already in progress...[skipping]`);
2923
+ return;
2924
+ }
2553
2925
  // Communicate resume / start of the contribution to participant.
2554
2926
  await simpleLoader(`${changedContributionStep === "DOWNLOADING" /* ParticipantContributionStep.DOWNLOADING */ ? `Starting` : `Resuming`} your contribution...`, `clock`, 3000);
2555
- // Start / Resume the contribution for the participant.
2556
- await handleStartOrResumeContribution(cloudFunctions, firestoreDatabase, ceremony, circuit, participant, entropy, providerUserId, false // not finalizing.
2557
- );
2927
+ try {
2928
+ contributionInProgress = true;
2929
+ // Start / Resume the contribution for the participant.
2930
+ await handleStartOrResumeContribution(cloudFunctions, firestoreDatabase, ceremony, circuit, participant, entropy, providerUserId, false, // not finalizing.
2931
+ circuits.length);
2932
+ }
2933
+ finally {
2934
+ contributionInProgress = false;
2935
+ }
2558
2936
  }
2559
2937
  // Scenario (3.A).
2560
2938
  else if (isWaitingForContribution)
@@ -2603,7 +2981,9 @@ const listenToParticipantDocumentChanges = async (firestoreDatabase, cloudFuncti
2603
2981
  // Get latest contribution verification result.
2604
2982
  await getLatestVerificationResult(firestoreDatabase, ceremony.id, circuit.id, participant.id);
2605
2983
  // Get next circuit for contribution.
2606
- const nextCircuit = timeoutExpired ? getCircuitBySequencePosition(circuits, changedContributionProgress) : getCircuitBySequencePosition(circuits, changedContributionProgress + 1);
2984
+ const nextCircuit = timeoutExpired
2985
+ ? getCircuitBySequencePosition(circuits, changedContributionProgress)
2986
+ : getCircuitBySequencePosition(circuits, changedContributionProgress + 1);
2607
2987
  // Check disk space requirements for participant.
2608
2988
  const wannaGenerateAttestation = await handleDiskSpaceRequirementForNextContribution(cloudFunctions, ceremony.id, nextCircuit.data.sequencePosition, nextCircuit.data.zKeySizeInBytes, timeoutExpired, providerUserId);
2609
2989
  // Check if the participant would like to generate a new attestation.
@@ -2641,11 +3021,12 @@ const listenToParticipantDocumentChanges = async (firestoreDatabase, cloudFuncti
2641
3021
  */
2642
3022
  const contribute = async (opt) => {
2643
3023
  const { firebaseApp, firebaseFunctions, firestoreDatabase } = await bootstrapCommandExecutionAndServices();
2644
- // Check for authentication.
2645
- const { user, providerUserId, token } = await checkAuth(firebaseApp);
2646
3024
  // Get options.
2647
3025
  const ceremonyOpt = opt.ceremony;
2648
3026
  const entropyOpt = opt.entropy;
3027
+ const { auth } = opt;
3028
+ // Check for authentication.
3029
+ const { user, providerUserId, token } = auth ? await authWithToken(firebaseApp, auth) : await checkAuth(firebaseApp);
2649
3030
  // Prepare data.
2650
3031
  let selectedCeremony;
2651
3032
  // Retrieve the opened ceremonies.
@@ -2671,7 +3052,7 @@ const contribute = async (opt) => {
2671
3052
  }
2672
3053
  else {
2673
3054
  // Prompt the user to select a ceremony from the opened ones.
2674
- selectedCeremony = await promptForCeremonySelection(ceremoniesOpenedForContributions, false);
3055
+ selectedCeremony = await promptForCeremonySelection(ceremoniesOpenedForContributions, false, "Which ceremony would you like to contribute to?");
2675
3056
  }
2676
3057
  // Get selected ceremony circuit(s) documents.
2677
3058
  const circuits = await getCeremonyCircuits(firestoreDatabase, selectedCeremony.id);
@@ -2681,7 +3062,7 @@ const contribute = async (opt) => {
2681
3062
  const userDoc = await getDocumentById(firestoreDatabase, commonTerms.collections.users.name, user.uid);
2682
3063
  const userData = userDoc.data();
2683
3064
  if (!userData) {
2684
- 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.`);
3065
+ 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.`);
2685
3066
  process.exit(0);
2686
3067
  }
2687
3068
  // Check the user's current participant readiness for contribution status (eligible, already contributed, timed out).
@@ -2832,10 +3213,10 @@ const observe = async () => {
2832
3213
  // Preserve command execution only for coordinators].
2833
3214
  if (!(await isCoordinator(user)))
2834
3215
  showError(COMMAND_ERRORS.COMMAND_NOT_COORDINATOR, true);
2835
- // Get running cerimonies info (if any).
3216
+ // Get running ceremonies info (if any).
2836
3217
  const runningCeremoniesDocs = await getOpenedCeremonies(firestoreDatabase);
2837
3218
  // Ask to select a ceremony.
2838
- const ceremony = await promptForCeremonySelection(runningCeremoniesDocs, false);
3219
+ const ceremony = await promptForCeremonySelection(runningCeremoniesDocs, false, "Which ceremony would you like to observe?");
2839
3220
  console.log(`${logSymbols.info} Refresh rate set to ~3 seconds for waiting queue updates\n`);
2840
3221
  let cursorPos = 0; // Keep track of current cursor position.
2841
3222
  const spinner = customSpinner(`Getting ready...`, "clock");
@@ -2881,7 +3262,7 @@ const handleVerificationKey = async (cloudFunctions, bucketName, finalZkeyLocalF
2881
3262
  spinner.text = "Writing verification key...";
2882
3263
  // Write the verification key locally.
2883
3264
  writeLocalJsonFile(verificationKeyLocalFilePath, vKey);
2884
- await sleep(3000); // workaound for file descriptor.
3265
+ await sleep(3000); // workaround for file descriptor.
2885
3266
  // Upload verification key to storage.
2886
3267
  await multiPartUpload(cloudFunctions, bucketName, verificationKeyStorageFilePath, verificationKeyLocalFilePath, Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB));
2887
3268
  spinner.succeed(`Verification key correctly saved on storage`);
@@ -2901,13 +3282,13 @@ const handleVerifierSmartContract = async (cloudFunctions, bucketName, finalZkey
2901
3282
  const packagePath = `${dirname(fileURLToPath(import.meta.url))}`;
2902
3283
  const verifierPath = packagePath.includes(`src/commands`)
2903
3284
  ? `${dirname(fileURLToPath(import.meta.url))}/../../../../node_modules/snarkjs/templates/verifier_groth16.sol.ejs`
2904
- : `${dirname(fileURLToPath(import.meta.url))}/../../../node_modules/snarkjs/templates/verifier_groth16.sol.ejs`;
3285
+ : `${dirname(fileURLToPath(import.meta.url))}/../node_modules/snarkjs/templates/verifier_groth16.sol.ejs`;
2905
3286
  // Export the Solidity verifier smart contract.
2906
3287
  const verifierCode = await exportVerifierContract(finalZkeyLocalFilePath, verifierPath);
2907
3288
  spinner.text = `Writing verifier smart contract...`;
2908
3289
  // Write the verification key locally.
2909
3290
  writeFile(verifierContractLocalFilePath, verifierCode);
2910
- await sleep(3000); // workaound for file descriptor.
3291
+ await sleep(3000); // workaround for file descriptor.
2911
3292
  // Upload verifier smart contract to storage.
2912
3293
  await multiPartUpload(cloudFunctions, bucketName, verifierContractStorageFilePath, verifierContractLocalFilePath, Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB));
2913
3294
  spinner.succeed(`Verifier smart contract correctly saved on storage`);
@@ -2928,11 +3309,12 @@ const handleVerifierSmartContract = async (cloudFunctions, bucketName, finalZkey
2928
3309
  * @param participant <FirebaseDocumentInfo> - the Firestore document of the participant (coordinator).
2929
3310
  * @param beacon <string> - the value used to compute the final contribution while finalizing the ceremony.
2930
3311
  * @param coordinatorIdentifier <string> - the identifier of the coordinator.
3312
+ * @param circuitsLength <number> - the number of circuits in the ceremony.
2931
3313
  */
2932
- const handleCircuitFinalization = async (cloudFunctions, firestoreDatabase, ceremony, circuit, participant, beacon, coordinatorIdentifier) => {
3314
+ const handleCircuitFinalization = async (cloudFunctions, firestoreDatabase, ceremony, circuit, participant, beacon, coordinatorIdentifier, circuitsLength) => {
2933
3315
  // Step (1).
2934
- await handleStartOrResumeContribution(cloudFunctions, firestoreDatabase, ceremony, circuit, participant, computeSHA256ToHex(beacon), coordinatorIdentifier, true);
2935
- await sleep(2000); // workaound for descriptors.
3316
+ await handleStartOrResumeContribution(cloudFunctions, firestoreDatabase, ceremony, circuit, participant, computeSHA256ToHex(beacon), coordinatorIdentifier, true, circuitsLength);
3317
+ await sleep(2000); // workaround for descriptors.
2936
3318
  // Extract data.
2937
3319
  const { prefix: circuitPrefix } = circuit.data;
2938
3320
  const { prefix: ceremonyPrefix } = ceremony.data;
@@ -2966,10 +3348,11 @@ const handleCircuitFinalization = async (cloudFunctions, firestoreDatabase, cere
2966
3348
  * @dev For proper execution, the command requires the coordinator to be authenticated with a GitHub account (run auth command first) in order to
2967
3349
  * handle sybil-resistance and connect to GitHub APIs to publish the gist containing the final public attestation.
2968
3350
  */
2969
- const finalize = async () => {
3351
+ const finalize = async (opt) => {
2970
3352
  const { firebaseApp, firebaseFunctions, firestoreDatabase } = await bootstrapCommandExecutionAndServices();
2971
3353
  // Check for authentication.
2972
- const { user, providerUserId, token: coordinatorAccessToken } = await checkAuth(firebaseApp);
3354
+ const { auth } = opt;
3355
+ const { user, providerUserId, token: coordinatorAccessToken } = auth ? await authWithToken(firebaseApp, auth) : await checkAuth(firebaseApp);
2973
3356
  // Preserve command execution only for coordinators.
2974
3357
  if (!(await isCoordinator(user)))
2975
3358
  showError(COMMAND_ERRORS.COMMAND_NOT_COORDINATOR, true);
@@ -2980,7 +3363,7 @@ const finalize = async () => {
2980
3363
  showError(COMMAND_ERRORS.COMMAND_FINALIZED_NO_CLOSED_CEREMONIES, true);
2981
3364
  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`);
2982
3365
  // Prompt for ceremony selection.
2983
- const selectedCeremony = await promptForCeremonySelection(ceremoniesClosedForFinalization, true);
3366
+ const selectedCeremony = await promptForCeremonySelection(ceremoniesClosedForFinalization, true, "Which ceremony would you like to finalize?");
2984
3367
  // Get coordinator participant document.
2985
3368
  let participant = await getDocumentById(firestoreDatabase, getParticipantsCollectionPath(selectedCeremony.id), user.uid);
2986
3369
  const isCoordinatorReadyForCeremonyFinalization = await checkAndPrepareCoordinatorForFinalization(firebaseFunctions, selectedCeremony.id);
@@ -3004,7 +3387,7 @@ const finalize = async () => {
3004
3387
  const circuits = await getCeremonyCircuits(firestoreDatabase, selectedCeremony.id);
3005
3388
  // Handle finalization for each ceremony circuit.
3006
3389
  for await (const circuit of circuits)
3007
- await handleCircuitFinalization(firebaseFunctions, firestoreDatabase, selectedCeremony, circuit, participant, beacon, providerUserId);
3390
+ await handleCircuitFinalization(firebaseFunctions, firestoreDatabase, selectedCeremony, circuit, participant, beacon, providerUserId, circuits.length);
3008
3391
  process.stdout.write(`\n`);
3009
3392
  const spinner = customSpinner(`Wrapping up the finalization of the ceremony...`, "clock");
3010
3393
  spinner.start();
@@ -3019,7 +3402,7 @@ const finalize = async () => {
3019
3402
  // Generate attestation with final contributions.
3020
3403
  const publicAttestation = await generateValidContributionsAttestation(firestoreDatabase, circuits, selectedCeremony.id, participant.id, contributions, providerUserId, ceremonyName, true);
3021
3404
  // Write public attestation locally.
3022
- writeFile(getAttestationLocalFilePath(`${prefix}_${finalContributionIndex}_${commonTerms.foldersAndPathsTerms.attestation}.log`), Buffer.from(publicAttestation));
3405
+ writeFile(getFinalAttestationLocalFilePath(`${prefix}_${finalContributionIndex}_${commonTerms.foldersAndPathsTerms.attestation}.log`), Buffer.from(publicAttestation));
3023
3406
  await sleep(3000); // workaround for file descriptor unexpected close.
3024
3407
  const gistUrl = await publishGist(coordinatorAccessToken, publicAttestation, ceremonyName, prefix);
3025
3408
  console.log(`\n${theme.symbols.info} Your public final attestation has been successfully posted as Github Gist (${theme.text.bold(theme.text.underlined(gistUrl))})`);
@@ -3080,7 +3463,9 @@ const logout = async () => {
3080
3463
  const auth = getAuth();
3081
3464
  await signOut(auth);
3082
3465
  // Delete local token.
3466
+ deleteLocalAuthMethod();
3083
3467
  deleteLocalAccessToken();
3468
+ deleteLocalBandadaIdentity();
3084
3469
  await sleep(3000); // ~3s.
3085
3470
  spinner.stop();
3086
3471
  console.log(`${theme.symbols.success} Logout successfully completed`);
@@ -3142,6 +3527,55 @@ const listCeremonies = async () => {
3142
3527
  }
3143
3528
  };
3144
3529
 
3530
+ const listParticipants = async () => {
3531
+ try {
3532
+ const { firestoreDatabase } = await bootstrapCommandExecutionAndServices();
3533
+ const allCeremonies = await getAllCeremonies(firestoreDatabase);
3534
+ const selectedCeremony = await promptForCeremonySelection(allCeremonies, true, "Which ceremony would you like to see participants?");
3535
+ const docRef = doc(firestoreDatabase, commonTerms.collections.ceremonies.name, selectedCeremony.id);
3536
+ const participantsRef = collection(docRef, "participants");
3537
+ const participantsSnapshot = await getDocs(participantsRef);
3538
+ const participants = participantsSnapshot.docs.map((participantDoc) => participantDoc.data());
3539
+ const usersRef = collection(firestoreDatabase, "users");
3540
+ const usersSnapshot = await getDocs(usersRef);
3541
+ const users = usersSnapshot.docs.map((userDoc) => {
3542
+ const data = userDoc.data();
3543
+ return { id: userDoc.id, ...data };
3544
+ });
3545
+ const participantDetails = participants
3546
+ .map((participant) => {
3547
+ const user = users.find((_user) => _user.id === participant.userId);
3548
+ if (!user)
3549
+ return null;
3550
+ return {
3551
+ id: user.name,
3552
+ status: participant.status,
3553
+ contributionStep: participant.contributionStep,
3554
+ lastUpdated: new Date(participant.lastUpdated)
3555
+ };
3556
+ })
3557
+ .filter((user) => user !== null);
3558
+ const participantsDone = participantDetails.filter((participant) => participant.status === "DONE");
3559
+ console.log(participantDetails);
3560
+ console.log(`${theme.text.underlined("Total participants:")} ${participantDetails.length}`);
3561
+ console.log(`${theme.text.underlined("Total participants finished:")} ${participantsDone.length}`);
3562
+ }
3563
+ catch (err) {
3564
+ showError(`Something went wrong: ${err.toString()}`, true);
3565
+ }
3566
+ process.exit(0);
3567
+ };
3568
+
3569
+ const setCeremonyCommands = (program) => {
3570
+ const ceremony = program.command("ceremony").description("manage ceremonies");
3571
+ ceremony
3572
+ .command("participants")
3573
+ .description("retrieve participants list of a ceremony")
3574
+ .requiredOption("-c, --ceremony <string>", "the prefix of the ceremony you want to retrieve information about", "")
3575
+ .action(listParticipants);
3576
+ return ceremony;
3577
+ };
3578
+
3145
3579
  // Get pkg info (e.g., name, version).
3146
3580
  const packagePath = `${dirname(fileURLToPath(import.meta.url))}/..`;
3147
3581
  const { description, version, name } = JSON.parse(readFileSync(`${packagePath}/package.json`, "utf8"));
@@ -3150,44 +3584,52 @@ const program = createCommand();
3150
3584
  program.name(name).description(description).version(version);
3151
3585
  // User commands.
3152
3586
  program.command("auth").description("authenticate yourself using your Github account (OAuth 2.0)").action(auth);
3587
+ program
3588
+ .command("auth-bandada")
3589
+ .description("authenticate yourself in a privacy-perserving manner using Bandada")
3590
+ .action(authBandada);
3591
+ program
3592
+ .command("auth-siwe")
3593
+ .description("authenticate yourself using your Ethereum account (Sign In With Ethereum - SIWE)")
3594
+ .action(authSIWE);
3153
3595
  program
3154
3596
  .command("contribute")
3155
3597
  .description("compute contributions for a Phase2 Trusted Setup ceremony circuits")
3156
3598
  .option("-c, --ceremony <string>", "the prefix of the ceremony you want to contribute for", "")
3157
3599
  .option("-e, --entropy <string>", "the entropy (aka toxic waste) of your contribution", "")
3600
+ .option("-a, --auth <string>", "the Github OAuth 2.0 token", "")
3158
3601
  .action(contribute);
3159
3602
  program
3160
3603
  .command("clean")
3161
3604
  .description("clean up output generated by commands from the current working directory")
3162
3605
  .action(clean);
3163
- program
3164
- .command("list")
3165
- .description("List all ceremonies prefixes")
3166
- .action(listCeremonies);
3606
+ program.command("list").description("List all ceremonies prefixes").action(listCeremonies);
3167
3607
  program
3168
3608
  .command("logout")
3169
3609
  .description("sign out from Firebae Auth service and delete Github OAuth 2.0 token from local storage")
3170
3610
  .action(logout);
3171
3611
  program
3172
3612
  .command("validate")
3173
- .description("Validate that a Ceremony Setup file is correct")
3613
+ .description("validate that a Ceremony Setup file is correct")
3174
3614
  .requiredOption("-t, --template <path>", "The path to the ceremony setup template", "")
3175
3615
  .option("-c, --constraints <number>", "The number of constraints to check against")
3176
3616
  .action(validate);
3177
3617
  // Only coordinator commands.
3178
- const ceremony = program.command("coordinate").description("commands for coordinating a ceremony");
3179
- ceremony
3618
+ const coordinate = program.command("coordinate").description("commands for coordinating a ceremony");
3619
+ coordinate
3180
3620
  .command("setup")
3181
3621
  .description("setup a Groth16 Phase 2 Trusted Setup ceremony for zk-SNARK circuits")
3182
- .option('-t, --template <path>', 'The path to the ceremony setup template', '')
3183
- .option('-a, --auth <string>', 'The Github OAuth 2.0 token', '')
3622
+ .option("-t, --template <path>", "The path to the ceremony setup template", "")
3623
+ .option("-a, --auth <string>", "The Github OAuth 2.0 token", "")
3184
3624
  .action(setup);
3185
- ceremony
3625
+ coordinate
3186
3626
  .command("observe")
3187
3627
  .description("observe in real-time the waiting queue of each ceremony circuit")
3188
3628
  .action(observe);
3189
- ceremony
3629
+ coordinate
3190
3630
  .command("finalize")
3191
3631
  .description("finalize a Phase2 Trusted Setup ceremony by applying a beacon, exporting verification key and verifier contract")
3632
+ .option("-a, --auth <string>", "the Github OAuth 2.0 token", "")
3192
3633
  .action(finalize);
3634
+ setCeremonyCommands(program);
3193
3635
  program.parseAsync(process.argv);