@devtion/devcli 0.0.0-8bb9489 → 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.
package/dist/index.js CHANGED
@@ -2,27 +2,27 @@
2
2
 
3
3
  /**
4
4
  * @module @p0tion/phase2cli
5
- * @version 1.0.9
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.
@@ -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.
@@ -800,7 +853,7 @@ const handleStartOrResumeContribution = async (cloudFunctions, firestoreDatabase
800
853
  spinner.start();
801
854
  // Read local transcript file info to get the contribution hash.
802
855
  const transcriptContents = readFile(transcriptLocalFilePath);
803
- const matchContributionHash = transcriptContents.match(/Contribution.+Hash.+\n\t\t.+\n\t\t.+\n.+\n\t\t.+\n/);
856
+ const matchContributionHash = transcriptContents.match(contribHashRegex);
804
857
  if (!matchContributionHash)
805
858
  showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_FINALIZE_NO_TRANSCRIPT_CONTRIBUTION_HASH_MATCH, true);
806
859
  // Format contribution hash.
@@ -1389,7 +1442,7 @@ const promptPotSelector = async (options) => {
1389
1442
  * @param isFinalizing <boolean> - true when the coordinator must select a ceremony for finalization; otherwise false (participant selects a ceremony for contribution).
1390
1443
  * @returns Promise<FirebaseDocumentInfo> - the Firestore document of the selected ceremony.
1391
1444
  */
1392
- const promptForCeremonySelection = async (ceremoniesDocuments, isFinalizing) => {
1445
+ const promptForCeremonySelection = async (ceremoniesDocuments, isFinalizing, messageToDisplay) => {
1393
1446
  // Prepare state.
1394
1447
  const choices = [];
1395
1448
  // Prepare choices x ceremony.
@@ -1407,9 +1460,7 @@ const promptForCeremonySelection = async (ceremoniesDocuments, isFinalizing) =>
1407
1460
  const { ceremony } = await prompts({
1408
1461
  type: "select",
1409
1462
  name: "ceremony",
1410
- message: theme.text.bold(!isFinalizing
1411
- ? "Which ceremony would you like to contribute to?"
1412
- : "Which ceremony would you like to finalize?"),
1463
+ message: theme.text.bold(messageToDisplay),
1413
1464
  choices,
1414
1465
  initial: 0
1415
1466
  });
@@ -1560,16 +1611,37 @@ const checkAuth = async (firebaseApp) => {
1560
1611
  showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_NOT_AUTHENTICATED, true);
1561
1612
  // Retrieve local access token.
1562
1613
  const token = String(getLocalAccessToken());
1563
- // Get credentials.
1564
- const credentials = exchangeGithubTokenForCredentials(token);
1565
- // Sign in to Firebase using credentials.
1566
- 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
+ }
1567
1641
  // Get current authenticated user.
1568
1642
  const user = getCurrentFirebaseAuthUser(firebaseApp);
1569
- // Get Github unique identifier (handle-id).
1570
- const providerUserId = await getGithubProviderUserId(String(token));
1571
1643
  // Greet the user.
1572
- console.log(`Greetings, @${theme.text.bold(getUserHandleFromProviderUserId(providerUserId))} ${theme.emojis.wave}\n`);
1644
+ console.log(`Greetings, @${theme.text.bold(username)} ${theme.emojis.wave}\n`);
1573
1645
  return {
1574
1646
  user,
1575
1647
  token,
@@ -1711,7 +1783,7 @@ const displayCeremonySummary = (ceremonyInputData, circuits) => {
1711
1783
  };
1712
1784
  /**
1713
1785
  * Check if the smallest Powers of Tau has already been downloaded/stored in the correspondent local path
1714
- * @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.
1715
1787
  * @param powers <string> - the smallest amount of powers needed for the given circuit (should be in a 'XY' stringified form).
1716
1788
  * @param ptauCompleteFilename <string> - the complete file name of the powers of tau file to be downloaded.
1717
1789
  * @returns <Promise<void>>
@@ -1725,7 +1797,7 @@ const checkAndDownloadSmallestPowersOfTau = async (powers, ptauCompleteFilename)
1725
1797
  .map((dirent) => dirent.name);
1726
1798
  // Check if already downloaded or not.
1727
1799
  if (smallestPtauFileForGivenPowers.length === 0) {
1728
- 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`);
1729
1801
  spinner.start();
1730
1802
  // Download smallest Powers of Tau file from remote server.
1731
1803
  const streamPipeline = promisify(pipeline);
@@ -1840,7 +1912,7 @@ const handleCircuitArtifactUploadToStorage = async (firebaseFunctions, bucketNam
1840
1912
  * @notice The setup command allows the coordinator of the ceremony to prepare the next ceremony by interacting with the CLI.
1841
1913
  * @dev For proper execution, the command must be run in a folder containing the R1CS files related to the circuits
1842
1914
  * for which the coordinator wants to create the ceremony. The command will download the necessary Tau powers
1843
- * from Hermez's ceremony Phase 1 Reliable Setup Ceremony.
1915
+ * from PPoT ceremony Phase 1 Setup Ceremony.
1844
1916
  * @param cmd? <any> - the path to the ceremony setup file.
1845
1917
  */
1846
1918
  const setup = async (cmd) => {
@@ -1891,12 +1963,20 @@ const setup = async (cmd) => {
1891
1963
  // 3. generate the zKey
1892
1964
  const spinner = customSpinner(`Generating genesis zKey for circuit ${theme.text.bold(circuit.name)}...`, `clock`);
1893
1965
  spinner.start();
1894
- await zKey.newZKey(r1csLocalPathAndFileName, getPotLocalFilePath(circuit.files.potFilename), zkeyLocalPathAndFileName, undefined);
1895
- 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();
1896
1975
  // 4. calculate the hashes
1897
1976
  const wasmBlake2bHash = await blake512FromPath(wasmLocalPathAndFileName);
1898
1977
  const potBlake2bHash = await blake512FromPath(getPotLocalFilePath(circuit.files.potFilename));
1899
1978
  const initialZkeyBlake2bHash = await blake512FromPath(zkeyLocalPathAndFileName);
1979
+ hashSpinner.succeed(`Hashes for circuit ${theme.text.bold(circuit.name)} calculated successfully`);
1900
1980
  // 5. upload the artifacts
1901
1981
  // Upload zKey to Storage.
1902
1982
  await handleCircuitArtifactUploadToStorage(firebaseFunctions, bucketName, circuit.files.initialZkeyStoragePath, zkeyLocalPathAndFileName, circuit.files.initialZkeyFilename);
@@ -1914,9 +1994,9 @@ const setup = async (cmd) => {
1914
1994
  // 6 update the setup data object
1915
1995
  ceremonySetupData.circuits[index].files = {
1916
1996
  ...circuit.files,
1917
- potBlake2bHash: potBlake2bHash,
1918
- wasmBlake2bHash: wasmBlake2bHash,
1919
- initialZkeyBlake2bHash: initialZkeyBlake2bHash
1997
+ potBlake2bHash,
1998
+ wasmBlake2bHash,
1999
+ initialZkeyBlake2bHash
1920
2000
  };
1921
2001
  ceremonySetupData.circuits[index].zKeySizeInBytes = getFileStats(zkeyLocalPathAndFileName).size;
1922
2002
  }
@@ -2197,6 +2277,7 @@ const auth = async () => {
2197
2277
  // Generate a new access token using Github Device Flow (OAuth 2.0).
2198
2278
  const newToken = await executeGithubDeviceFlow(String(process.env.AUTH_GITHUB_CLIENT_ID));
2199
2279
  // Store the new access token.
2280
+ setLocalAuthMethod("github");
2200
2281
  setLocalAccessToken(newToken);
2201
2282
  }
2202
2283
  else
@@ -2217,6 +2298,249 @@ const auth = async () => {
2217
2298
  terminate(providerUserId);
2218
2299
  };
2219
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
+
2220
2544
  /**
2221
2545
  * Return the verification result for latest contribution.
2222
2546
  * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
@@ -2339,8 +2663,8 @@ const handleDiskSpaceRequirementForNextContribution = async (cloudFunctions, cer
2339
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
2340
2664
  ? theme.text.bold(`< 0.01`)
2341
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`);
2342
- 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");
2343
- 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;
2344
2668
  if (circuitSequencePosition > 1) {
2345
2669
  console.log(`${theme.symbols.info} Please note, you have time until ceremony ends to free up your memory and complete remaining contributions`);
2346
2670
  // Asks the contributor if their wants to terminate contributions for the ceremony.
@@ -2408,8 +2732,12 @@ const handlePublicAttestation = async (firestoreDatabase, circuits, ceremonyId,
2408
2732
  // Write public attestation locally.
2409
2733
  writeFile(getAttestationLocalFilePath(`${ceremonyPrefix}_${commonTerms.foldersAndPathsTerms.attestation}.log`), Buffer.from(publicAttestation));
2410
2734
  await sleep(1000); // workaround for file descriptor unexpected close.
2411
- const gistUrl = await publishGist(participantAccessToken, publicAttestation, ceremonyName, ceremonyPrefix);
2412
- 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
+ }
2413
2741
  // Prepare a ready-to-share tweet.
2414
2742
  await handleTweetGeneration(ceremonyName, gistUrl);
2415
2743
  };
@@ -2462,6 +2790,7 @@ const listenToCeremonyCircuitDocumentChanges = (firestoreDatabase, ceremonyId, p
2462
2790
  }
2463
2791
  });
2464
2792
  };
2793
+ let contributionInProgress = false;
2465
2794
  /**
2466
2795
  * Listen to current authenticated participant document changes.
2467
2796
  * @dev this is the core business logic related to the execution of the contribute command.
@@ -2589,11 +2918,21 @@ const listenToParticipantDocumentChanges = async (firestoreDatabase, cloudFuncti
2589
2918
  (!noTemporaryContributionData && resumingWithSameTemporaryData);
2590
2919
  // Scenario (3.B).
2591
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
+ }
2592
2925
  // Communicate resume / start of the contribution to participant.
2593
2926
  await simpleLoader(`${changedContributionStep === "DOWNLOADING" /* ParticipantContributionStep.DOWNLOADING */ ? `Starting` : `Resuming`} your contribution...`, `clock`, 3000);
2594
- // Start / Resume the contribution for the participant.
2595
- await handleStartOrResumeContribution(cloudFunctions, firestoreDatabase, ceremony, circuit, participant, entropy, providerUserId, false, // not finalizing.
2596
- circuits.length);
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
+ }
2597
2936
  }
2598
2937
  // Scenario (3.A).
2599
2938
  else if (isWaitingForContribution)
@@ -2685,7 +3024,7 @@ const contribute = async (opt) => {
2685
3024
  // Get options.
2686
3025
  const ceremonyOpt = opt.ceremony;
2687
3026
  const entropyOpt = opt.entropy;
2688
- const auth = opt.auth;
3027
+ const { auth } = opt;
2689
3028
  // Check for authentication.
2690
3029
  const { user, providerUserId, token } = auth ? await authWithToken(firebaseApp, auth) : await checkAuth(firebaseApp);
2691
3030
  // Prepare data.
@@ -2713,7 +3052,7 @@ const contribute = async (opt) => {
2713
3052
  }
2714
3053
  else {
2715
3054
  // Prompt the user to select a ceremony from the opened ones.
2716
- selectedCeremony = await promptForCeremonySelection(ceremoniesOpenedForContributions, false);
3055
+ selectedCeremony = await promptForCeremonySelection(ceremoniesOpenedForContributions, false, "Which ceremony would you like to contribute to?");
2717
3056
  }
2718
3057
  // Get selected ceremony circuit(s) documents.
2719
3058
  const circuits = await getCeremonyCircuits(firestoreDatabase, selectedCeremony.id);
@@ -2723,7 +3062,7 @@ const contribute = async (opt) => {
2723
3062
  const userDoc = await getDocumentById(firestoreDatabase, commonTerms.collections.users.name, user.uid);
2724
3063
  const userData = userDoc.data();
2725
3064
  if (!userData) {
2726
- 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. 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.`);
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.`);
2727
3066
  process.exit(0);
2728
3067
  }
2729
3068
  // Check the user's current participant readiness for contribution status (eligible, already contributed, timed out).
@@ -2877,7 +3216,7 @@ const observe = async () => {
2877
3216
  // Get running ceremonies info (if any).
2878
3217
  const runningCeremoniesDocs = await getOpenedCeremonies(firestoreDatabase);
2879
3218
  // Ask to select a ceremony.
2880
- const ceremony = await promptForCeremonySelection(runningCeremoniesDocs, false);
3219
+ const ceremony = await promptForCeremonySelection(runningCeremoniesDocs, false, "Which ceremony would you like to observe?");
2881
3220
  console.log(`${logSymbols.info} Refresh rate set to ~3 seconds for waiting queue updates\n`);
2882
3221
  let cursorPos = 0; // Keep track of current cursor position.
2883
3222
  const spinner = customSpinner(`Getting ready...`, "clock");
@@ -2923,7 +3262,7 @@ const handleVerificationKey = async (cloudFunctions, bucketName, finalZkeyLocalF
2923
3262
  spinner.text = "Writing verification key...";
2924
3263
  // Write the verification key locally.
2925
3264
  writeLocalJsonFile(verificationKeyLocalFilePath, vKey);
2926
- await sleep(3000); // workaound for file descriptor.
3265
+ await sleep(3000); // workaround for file descriptor.
2927
3266
  // Upload verification key to storage.
2928
3267
  await multiPartUpload(cloudFunctions, bucketName, verificationKeyStorageFilePath, verificationKeyLocalFilePath, Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB));
2929
3268
  spinner.succeed(`Verification key correctly saved on storage`);
@@ -2949,7 +3288,7 @@ const handleVerifierSmartContract = async (cloudFunctions, bucketName, finalZkey
2949
3288
  spinner.text = `Writing verifier smart contract...`;
2950
3289
  // Write the verification key locally.
2951
3290
  writeFile(verifierContractLocalFilePath, verifierCode);
2952
- await sleep(3000); // workaound for file descriptor.
3291
+ await sleep(3000); // workaround for file descriptor.
2953
3292
  // Upload verifier smart contract to storage.
2954
3293
  await multiPartUpload(cloudFunctions, bucketName, verifierContractStorageFilePath, verifierContractLocalFilePath, Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB));
2955
3294
  spinner.succeed(`Verifier smart contract correctly saved on storage`);
@@ -2975,7 +3314,7 @@ const handleVerifierSmartContract = async (cloudFunctions, bucketName, finalZkey
2975
3314
  const handleCircuitFinalization = async (cloudFunctions, firestoreDatabase, ceremony, circuit, participant, beacon, coordinatorIdentifier, circuitsLength) => {
2976
3315
  // Step (1).
2977
3316
  await handleStartOrResumeContribution(cloudFunctions, firestoreDatabase, ceremony, circuit, participant, computeSHA256ToHex(beacon), coordinatorIdentifier, true, circuitsLength);
2978
- await sleep(2000); // workaound for descriptors.
3317
+ await sleep(2000); // workaround for descriptors.
2979
3318
  // Extract data.
2980
3319
  const { prefix: circuitPrefix } = circuit.data;
2981
3320
  const { prefix: ceremonyPrefix } = ceremony.data;
@@ -3012,7 +3351,7 @@ const handleCircuitFinalization = async (cloudFunctions, firestoreDatabase, cere
3012
3351
  const finalize = async (opt) => {
3013
3352
  const { firebaseApp, firebaseFunctions, firestoreDatabase } = await bootstrapCommandExecutionAndServices();
3014
3353
  // Check for authentication.
3015
- const auth = opt.auth;
3354
+ const { auth } = opt;
3016
3355
  const { user, providerUserId, token: coordinatorAccessToken } = auth ? await authWithToken(firebaseApp, auth) : await checkAuth(firebaseApp);
3017
3356
  // Preserve command execution only for coordinators.
3018
3357
  if (!(await isCoordinator(user)))
@@ -3024,7 +3363,7 @@ const finalize = async (opt) => {
3024
3363
  showError(COMMAND_ERRORS.COMMAND_FINALIZED_NO_CLOSED_CEREMONIES, true);
3025
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`);
3026
3365
  // Prompt for ceremony selection.
3027
- const selectedCeremony = await promptForCeremonySelection(ceremoniesClosedForFinalization, true);
3366
+ const selectedCeremony = await promptForCeremonySelection(ceremoniesClosedForFinalization, true, "Which ceremony would you like to finalize?");
3028
3367
  // Get coordinator participant document.
3029
3368
  let participant = await getDocumentById(firestoreDatabase, getParticipantsCollectionPath(selectedCeremony.id), user.uid);
3030
3369
  const isCoordinatorReadyForCeremonyFinalization = await checkAndPrepareCoordinatorForFinalization(firebaseFunctions, selectedCeremony.id);
@@ -3124,7 +3463,9 @@ const logout = async () => {
3124
3463
  const auth = getAuth();
3125
3464
  await signOut(auth);
3126
3465
  // Delete local token.
3466
+ deleteLocalAuthMethod();
3127
3467
  deleteLocalAccessToken();
3468
+ deleteLocalBandadaIdentity();
3128
3469
  await sleep(3000); // ~3s.
3129
3470
  spinner.stop();
3130
3471
  console.log(`${theme.symbols.success} Logout successfully completed`);
@@ -3186,6 +3527,55 @@ const listCeremonies = async () => {
3186
3527
  }
3187
3528
  };
3188
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
+
3189
3579
  // Get pkg info (e.g., name, version).
3190
3580
  const packagePath = `${dirname(fileURLToPath(import.meta.url))}/..`;
3191
3581
  const { description, version, name } = JSON.parse(readFileSync(`${packagePath}/package.json`, "utf8"));
@@ -3194,6 +3584,14 @@ const program = createCommand();
3194
3584
  program.name(name).description(description).version(version);
3195
3585
  // User commands.
3196
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);
3197
3595
  program
3198
3596
  .command("contribute")
3199
3597
  .description("compute contributions for a Phase2 Trusted Setup ceremony circuits")
@@ -3212,25 +3610,26 @@ program
3212
3610
  .action(logout);
3213
3611
  program
3214
3612
  .command("validate")
3215
- .description("Validate that a Ceremony Setup file is correct")
3613
+ .description("validate that a Ceremony Setup file is correct")
3216
3614
  .requiredOption("-t, --template <path>", "The path to the ceremony setup template", "")
3217
3615
  .option("-c, --constraints <number>", "The number of constraints to check against")
3218
3616
  .action(validate);
3219
3617
  // Only coordinator commands.
3220
- const ceremony = program.command("coordinate").description("commands for coordinating a ceremony");
3221
- ceremony
3618
+ const coordinate = program.command("coordinate").description("commands for coordinating a ceremony");
3619
+ coordinate
3222
3620
  .command("setup")
3223
3621
  .description("setup a Groth16 Phase 2 Trusted Setup ceremony for zk-SNARK circuits")
3224
3622
  .option("-t, --template <path>", "The path to the ceremony setup template", "")
3225
3623
  .option("-a, --auth <string>", "The Github OAuth 2.0 token", "")
3226
3624
  .action(setup);
3227
- ceremony
3625
+ coordinate
3228
3626
  .command("observe")
3229
3627
  .description("observe in real-time the waiting queue of each ceremony circuit")
3230
3628
  .action(observe);
3231
- ceremony
3629
+ coordinate
3232
3630
  .command("finalize")
3233
3631
  .description("finalize a Phase2 Trusted Setup ceremony by applying a beacon, exporting verification key and verifier contract")
3234
3632
  .option("-a, --auth <string>", "the Github OAuth 2.0 token", "")
3235
3633
  .action(finalize);
3634
+ setCeremonyCommands(program);
3236
3635
  program.parseAsync(process.argv);