@devtion/devcli 0.0.0-7e983e3

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 (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +118 -0
  3. package/dist/.env +41 -0
  4. package/dist/index.js +3206 -0
  5. package/dist/types/commands/auth.d.ts +25 -0
  6. package/dist/types/commands/clean.d.ts +6 -0
  7. package/dist/types/commands/contribute.d.ts +139 -0
  8. package/dist/types/commands/finalize.d.ts +51 -0
  9. package/dist/types/commands/index.d.ts +9 -0
  10. package/dist/types/commands/listCeremonies.d.ts +5 -0
  11. package/dist/types/commands/logout.d.ts +6 -0
  12. package/dist/types/commands/observe.d.ts +22 -0
  13. package/dist/types/commands/setup.d.ts +86 -0
  14. package/dist/types/commands/validate.d.ts +8 -0
  15. package/dist/types/index.d.ts +2 -0
  16. package/dist/types/lib/errors.d.ts +60 -0
  17. package/dist/types/lib/files.d.ts +64 -0
  18. package/dist/types/lib/localConfigs.d.ts +110 -0
  19. package/dist/types/lib/prompts.d.ts +104 -0
  20. package/dist/types/lib/services.d.ts +31 -0
  21. package/dist/types/lib/theme.d.ts +42 -0
  22. package/dist/types/lib/utils.d.ts +158 -0
  23. package/dist/types/types/index.d.ts +65 -0
  24. package/package.json +100 -0
  25. package/src/commands/auth.ts +194 -0
  26. package/src/commands/clean.ts +49 -0
  27. package/src/commands/contribute.ts +1090 -0
  28. package/src/commands/finalize.ts +382 -0
  29. package/src/commands/index.ts +9 -0
  30. package/src/commands/listCeremonies.ts +32 -0
  31. package/src/commands/logout.ts +67 -0
  32. package/src/commands/observe.ts +193 -0
  33. package/src/commands/setup.ts +901 -0
  34. package/src/commands/validate.ts +29 -0
  35. package/src/index.ts +66 -0
  36. package/src/lib/errors.ts +77 -0
  37. package/src/lib/files.ts +102 -0
  38. package/src/lib/localConfigs.ts +186 -0
  39. package/src/lib/prompts.ts +748 -0
  40. package/src/lib/services.ts +191 -0
  41. package/src/lib/theme.ts +45 -0
  42. package/src/lib/utils.ts +778 -0
  43. package/src/types/conf.d.ts +16 -0
  44. package/src/types/index.ts +70 -0
@@ -0,0 +1,1090 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ getOpenedCeremonies,
5
+ getCeremonyCircuits,
6
+ checkParticipantForCeremony,
7
+ getDocumentById,
8
+ getParticipantsCollectionPath,
9
+ getContributionsValidityForContributor,
10
+ formatZkeyIndex,
11
+ getCurrentActiveParticipantTimeout,
12
+ getCircuitBySequencePosition,
13
+ convertBytesOrKbToGb,
14
+ resumeContributionAfterTimeoutExpiration,
15
+ progressToNextCircuitForContribution,
16
+ getCircuitContributionsFromContributor,
17
+ ParticipantStatus,
18
+ ParticipantContributionStep,
19
+ Contribution,
20
+ ContributionValidity,
21
+ FirebaseDocumentInfo,
22
+ generateValidContributionsAttestation,
23
+ commonTerms,
24
+ convertToDoubleDigits
25
+ } from "@p0tion/actions"
26
+ import { DocumentSnapshot, DocumentData, Firestore, onSnapshot, Timestamp } from "firebase/firestore"
27
+ import { Functions } from "firebase/functions"
28
+ import open from "open"
29
+ import { askForConfirmation, promptForCeremonySelection, promptForEntropy } from "../lib/prompts.js"
30
+ import {
31
+ terminate,
32
+ customSpinner,
33
+ simpleLoader,
34
+ getSecondsMinutesHoursFromMillis,
35
+ sleep,
36
+ publishGist,
37
+ generateCustomUrlToTweetAboutParticipation,
38
+ handleStartOrResumeContribution,
39
+ getPublicAttestationGist,
40
+ estimateParticipantFreeGlobalDiskSpace
41
+ } from "../lib/utils.js"
42
+ import { COMMAND_ERRORS, showError } from "../lib/errors.js"
43
+ import { bootstrapCommandExecutionAndServices, checkAuth } from "../lib/services.js"
44
+ import { getAttestationLocalFilePath, localPaths } from "../lib/localConfigs.js"
45
+ import theme from "../lib/theme.js"
46
+ import { checkAndMakeNewDirectoryIfNonexistent, writeFile } from "../lib/files.js"
47
+
48
+ /**
49
+ * Return the verification result for latest contribution.
50
+ * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
51
+ * @param ceremonyId <string> - the unique identifier of the ceremony.
52
+ * @param circuitId <string> - the unique identifier of the circuit.
53
+ * @param participantId <string> - the unique identifier of the contributor.
54
+ */
55
+ export const getLatestVerificationResult = async (
56
+ firestoreDatabase: Firestore,
57
+ ceremonyId: string,
58
+ circuitId: string,
59
+ participantId: string
60
+ ) => {
61
+ // Clean cursor.
62
+ process.stdout.clearLine(0)
63
+ process.stdout.cursorTo(0)
64
+
65
+ const spinner = customSpinner(`Getting info about the verification of your contribution...`, `clock`)
66
+ spinner.start()
67
+
68
+ // Get circuit contribution from contributor.
69
+ const circuitContributionsFromContributor = await getCircuitContributionsFromContributor(
70
+ firestoreDatabase,
71
+ ceremonyId,
72
+ circuitId,
73
+ participantId
74
+ )
75
+
76
+ const contribution = circuitContributionsFromContributor.at(0)
77
+
78
+ spinner.stop()
79
+
80
+ console.log(
81
+ `${contribution?.data.valid ? theme.symbols.success : theme.symbols.error} Your contribution is ${
82
+ contribution?.data.valid ? `correct` : `wrong`
83
+ }`
84
+ )
85
+ }
86
+
87
+ /**
88
+ * Generate a ready-to-share tweet on public attestation.
89
+ * @param ceremonyTitle <string> - the title of the ceremony.
90
+ * @param gistUrl <string> - the Github public attestation gist url.
91
+ */
92
+ export const handleTweetGeneration = async (ceremonyTitle: string, gistUrl: string): Promise<void> => {
93
+ // Generate a ready to share custom url to tweet about ceremony participation.
94
+ const tweetUrl = generateCustomUrlToTweetAboutParticipation(ceremonyTitle, gistUrl, false)
95
+
96
+ console.log(
97
+ `${
98
+ theme.symbols.info
99
+ } We encourage you to tweet to spread the word about your participation to the ceremony by clicking the link below\n\n${theme.text.underlined(
100
+ tweetUrl
101
+ )}`
102
+ )
103
+
104
+ // Automatically open a webpage with the tweet.
105
+ await open(tweetUrl)
106
+ }
107
+
108
+ /**
109
+ * Display if a set of contributions computed for a circuit is valid/invalid.
110
+ * @param contributionsWithValidity <Array<ContributionValidity>> - list of contributor contributions together with contribution validity.
111
+ */
112
+ export const displayContributionValidity = (contributionsWithValidity: Array<ContributionValidity>) => {
113
+ // Circuit index position.
114
+ let circuitSequencePosition = 1 // nb. incremental value is enough because the contributions are already sorted x circuit sequence position.
115
+
116
+ for (const contributionWithValidity of contributionsWithValidity) {
117
+ // Display.
118
+ console.log(
119
+ `${contributionWithValidity.valid ? theme.symbols.success : theme.symbols.error} ${theme.text.bold(
120
+ `Circuit`
121
+ )} ${theme.text.bold(theme.colors.magenta(circuitSequencePosition))}`
122
+ )
123
+
124
+ // Increment circuit position.
125
+ circuitSequencePosition += 1
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Display and manage data necessary when participant has already made the contribution for all circuits of a ceremony.
131
+ * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
132
+ * @param circuits <Array<FirebaseDocumentInfo>> - the array of ceremony circuits documents.
133
+ * @param ceremonyId <string> - the unique identifier of the ceremony.
134
+ * @param participantId <string> - the unique identifier of the contributor.
135
+ */
136
+ export const handleContributionValidity = async (
137
+ firestoreDatabase: Firestore,
138
+ circuits: Array<FirebaseDocumentInfo>,
139
+ ceremonyId: string,
140
+ participantId: string
141
+ ) => {
142
+ // Get contributors' contributions validity.
143
+ const contributionsWithValidity = await getContributionsValidityForContributor(
144
+ firestoreDatabase,
145
+ circuits,
146
+ ceremonyId,
147
+ participantId,
148
+ false
149
+ )
150
+
151
+ // Filter only valid contributions.
152
+ const validContributions = contributionsWithValidity.filter(
153
+ (contributionWithValidity: ContributionValidity) => contributionWithValidity.valid
154
+ )
155
+
156
+ if (!validContributions.length)
157
+ console.log(
158
+ `\n${theme.symbols.error} You have provided ${theme.text.bold(
159
+ theme.colors.magenta(circuits.length)
160
+ )} out of ${theme.text.bold(theme.colors.magenta(circuits.length))} invalid contributions ${
161
+ theme.emojis.upsideDown
162
+ }`
163
+ )
164
+ else {
165
+ console.log(
166
+ `\nYou have provided ${theme.colors.magenta(
167
+ theme.text.bold(validContributions.length)
168
+ )} out of ${theme.colors.magenta(theme.text.bold(circuits.length))} valid contributions ${
169
+ theme.emojis.tada
170
+ }`
171
+ )
172
+
173
+ // Display (in)valid contributions per circuit.
174
+ displayContributionValidity(contributionsWithValidity)
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Display and manage data necessary when participant would like to contribute but there is still an on-going timeout.
180
+ * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
181
+ * @param ceremonyId <string> - the unique identifier of the ceremony.
182
+ * @param participantId <string> - the unique identifier of the contributor.
183
+ * @param participantContributionProgress <number> - the progress in the contribution of the various circuits of the ceremony.
184
+ * @param wasContributing <boolean> - flag to discriminate between participant currently contributing (true) or not (false).
185
+ */
186
+ export const handleTimedoutMessageForContributor = async (
187
+ firestoreDatabase: Firestore,
188
+ participantId: string,
189
+ ceremonyId: string,
190
+ participantContributionProgress: number,
191
+ wasContributing: boolean
192
+ ) => {
193
+ // Check if the participant was contributing when timeout happened.
194
+ if (!wasContributing)
195
+ console.log(theme.text.bold(`\n- Circuit # ${theme.colors.magenta(participantContributionProgress)}`))
196
+
197
+ // Display timeout message.
198
+ console.log(
199
+ `\n${theme.symbols.error} ${
200
+ wasContributing
201
+ ? `Your contribution took longer than the estimated time and you were removed as current contributor. You should wait for a timeout to expire before you can rejoin for contribution.`
202
+ : `The waiting time (timeout) to retry the contribution has not yet expired.`
203
+ }\n\n${
204
+ theme.symbols.warning
205
+ } Note that the timeout could be triggered due to network latency, disk availability issues, un/intentional crashes, limited hardware capabilities.`
206
+ )
207
+
208
+ // nb. workaround to attend timeout to be written on the database.
209
+ /// @todo use listeners instead (when possible).
210
+ await simpleLoader(`Getting timeout expiration...`, `clock`, 5000)
211
+
212
+ // Retrieve latest updated active timeouts for contributor.
213
+ const activeTimeouts = await getCurrentActiveParticipantTimeout(firestoreDatabase, ceremonyId, participantId)
214
+
215
+ if (activeTimeouts.length !== 1) showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_NO_UNIQUE_ACTIVE_TIMEOUTS, true)
216
+
217
+ // Get active timeout.
218
+ const activeTimeout = activeTimeouts.at(0)!
219
+
220
+ if (!activeTimeout.data) showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_NO_ACTIVE_TIMEOUT_DATA, true)
221
+
222
+ // Extract data.
223
+ const { endDate } = activeTimeout.data!
224
+
225
+ const { seconds, minutes, hours, days } = getSecondsMinutesHoursFromMillis(
226
+ Number(endDate) - Timestamp.now().toMillis()
227
+ )
228
+
229
+ console.log(
230
+ `${theme.symbols.info} Your timeout will end in ${theme.text.bold(
231
+ `${convertToDoubleDigits(days)}:${convertToDoubleDigits(hours)}:${convertToDoubleDigits(
232
+ minutes
233
+ )}:${convertToDoubleDigits(seconds)}`
234
+ )} (dd/hh/mm/ss)`
235
+ )
236
+ }
237
+
238
+ /**
239
+ * Check if the participant has enough disk space available before joining the waiting queue
240
+ * for the computing the next circuit contribution.
241
+ * @param cloudFunctions <Functions> - the instance of the Firebase cloud functions for the application.
242
+ * @param ceremonyId <string> - the unique identifier of the ceremony.
243
+ * @param circuitSequencePosition <number> - the position of the circuit in the sequence for contribution.
244
+ * @param circuitZkeySizeInBytes <number> - the size in bytes of the circuit zKey.
245
+ * @param isResumingAfterTimeout <boolean> - flag to discriminate between resuming after a timeout expiration (true) or progressing to next contribution (false).
246
+ * @param providerUserId <string> - the external third-party provider user identifier.
247
+ * @return <Promise<boolean>> - true when the contributor would like to generate the attestation and do not provide any further contribution to the ceremony; otherwise false.
248
+ */
249
+ export const handleDiskSpaceRequirementForNextContribution = async (
250
+ cloudFunctions: Functions,
251
+ ceremonyId: string,
252
+ circuitSequencePosition: number,
253
+ circuitZkeySizeInBytes: number,
254
+ isResumingAfterTimeout: boolean,
255
+ providerUserId: string
256
+ ): Promise<boolean> => {
257
+ let wannaContributeOrHaveEnoughMemory: boolean = false // true when the contributor has enough memory or wants to contribute in any case; otherwise false.
258
+
259
+ // Custom spinner.
260
+ const spinner = customSpinner(`Checking disk space requirement for next contribution...`, `clock`)
261
+ spinner.start()
262
+
263
+ // Compute disk space requirement to support circuit contribution (zKey size * 2).
264
+ const contributionDiskSpaceRequirement = convertBytesOrKbToGb(circuitZkeySizeInBytes * 2, true)
265
+ // Get participant available disk space.
266
+ const participantFreeDiskSpace = convertBytesOrKbToGb(estimateParticipantFreeGlobalDiskSpace(), false)
267
+
268
+ // Check.
269
+ if (participantFreeDiskSpace < contributionDiskSpaceRequirement) {
270
+ spinner.fail(
271
+ `You may not have enough memory to calculate the contribution for the Circuit ${theme.colors.magenta(
272
+ `${circuitSequencePosition}`
273
+ )}.\n\n${theme.symbols.info} The required amount of disk space is ${
274
+ contributionDiskSpaceRequirement < 0.01
275
+ ? theme.text.bold(`< 0.01`)
276
+ : theme.text.bold(contributionDiskSpaceRequirement)
277
+ } GB but you only have ${
278
+ participantFreeDiskSpace > 0 ? theme.text.bold(participantFreeDiskSpace.toFixed(2)) : theme.text.bold(0)
279
+ } GB available memory \nThe estimate ${theme.text.bold(
280
+ "may not be 100% correct"
281
+ )} since is based on the aggregate free memory on your disks but some may not be detected!\n`
282
+ )
283
+
284
+ const { confirmation } = await askForConfirmation(
285
+ `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`,
286
+ "Continue",
287
+ "Exit"
288
+ )
289
+ wannaContributeOrHaveEnoughMemory = !!confirmation
290
+
291
+ if (circuitSequencePosition > 1) {
292
+ console.log(
293
+ `${theme.symbols.info} Please note, you have time until ceremony ends to free up your memory and complete remaining contributions`
294
+ )
295
+
296
+ // Asks the contributor if their wants to terminate contributions for the ceremony.
297
+ const { confirmation } = await askForConfirmation(
298
+ `Please note, this action is irreversible! Do you want to end your contributions for the ceremony?`
299
+ )
300
+
301
+ return !!confirmation
302
+ }
303
+ } else wannaContributeOrHaveEnoughMemory = true
304
+
305
+ if (wannaContributeOrHaveEnoughMemory) {
306
+ spinner.succeed(
307
+ `Memory requirement to contribute to ${theme.text.bold(
308
+ `Circuit ${theme.colors.magenta(`${circuitSequencePosition}`)}`
309
+ )} satisfied`
310
+ )
311
+
312
+ // Memory requirement for next contribution met.
313
+ if (!isResumingAfterTimeout) {
314
+ spinner.text = "Progressing to next circuit for contribution..."
315
+ spinner.start()
316
+
317
+ // Progress the participant to the next circuit making it ready for contribution.
318
+ await progressToNextCircuitForContribution(cloudFunctions, ceremonyId)
319
+ } else {
320
+ spinner.text = "Resuming your contribution after timeout expiration..."
321
+ spinner.start()
322
+
323
+ // Resume contribution after timeout expiration (same circuit).
324
+ await resumeContributionAfterTimeoutExpiration(cloudFunctions, ceremonyId)
325
+ }
326
+
327
+ spinner.info(
328
+ `Joining the ${theme.text.bold(
329
+ `Circuit ${theme.colors.magenta(`${circuitSequencePosition}`)}`
330
+ )} waiting queue for contribution (this may take a while)`
331
+ )
332
+
333
+ return false
334
+ }
335
+ terminate(providerUserId)
336
+
337
+ return false
338
+ }
339
+
340
+ /**
341
+ * Generate the public attestation for the contributor.
342
+ * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
343
+ * @param circuits <Array<FirebaseDocumentInfo>> - the array of ceremony circuits documents.
344
+ * @param ceremonyId <string> - the unique identifier of the ceremony.
345
+ * @param participantId <string> - the unique identifier of the contributor.
346
+ * @param participantContributions <Array<Co> - the document data of the participant.
347
+ * @param contributorIdentifier <string> - the identifier of the contributor (handle, name, uid).
348
+ * @param ceremonyName <string> - the name of the ceremony.
349
+ * @returns <Promise<string>> - the public attestation.
350
+ */
351
+ export const generatePublicAttestation = async (
352
+ firestoreDatabase: Firestore,
353
+ circuits: Array<FirebaseDocumentInfo>,
354
+ ceremonyId: string,
355
+ participantId: string,
356
+ participantContributions: Array<Contribution>,
357
+ contributorIdentifier: string,
358
+ ceremonyName: string
359
+ ): Promise<string> => {
360
+ // Display contribution validity.
361
+ await handleContributionValidity(firestoreDatabase, circuits, ceremonyId, participantId)
362
+
363
+ await sleep(3000)
364
+
365
+ // Get only valid contribution hashes.
366
+ return generateValidContributionsAttestation(
367
+ firestoreDatabase,
368
+ circuits,
369
+ ceremonyId,
370
+ participantId,
371
+ participantContributions,
372
+ contributorIdentifier,
373
+ ceremonyName,
374
+ false
375
+ )
376
+ }
377
+
378
+ /**
379
+ * Generate a public attestation for a contributor, publish the attestation as gist, and prepare a new ready-to-share tweet about ceremony participation.
380
+ * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
381
+ * @param circuits <Array<FirebaseDocumentInfo>> - the array of ceremony circuits documents.
382
+ * @param ceremonyId <string> - the unique identifier of the ceremony.
383
+ * @param participantId <string> - the unique identifier of the contributor.
384
+ * @param participantContributions <Array<Co> - the document data of the participant.
385
+ * @param contributorIdentifier <string> - the identifier of the contributor (handle, name, uid).
386
+ * @param ceremonyName <string> - the name of the ceremony.
387
+ * @param ceremonyPrefix <string> - the prefix of the ceremony.
388
+ * @param participantAccessToken <string> - the access token of the participant.
389
+ */
390
+ export const handlePublicAttestation = async (
391
+ firestoreDatabase: Firestore,
392
+ circuits: Array<FirebaseDocumentInfo>,
393
+ ceremonyId: string,
394
+ participantId: string,
395
+ participantContributions: Array<Contribution>,
396
+ contributorIdentifier: string,
397
+ ceremonyName: string,
398
+ ceremonyPrefix: string,
399
+ participantAccessToken: string
400
+ ) => {
401
+ await simpleLoader(`Generating your public attestation...`, `clock`, 3000)
402
+
403
+ // Generate attestation with valid contributions.
404
+ const publicAttestation = await generatePublicAttestation(
405
+ firestoreDatabase,
406
+ circuits,
407
+ ceremonyId,
408
+ participantId,
409
+ participantContributions,
410
+ contributorIdentifier,
411
+ ceremonyName
412
+ )
413
+
414
+ // Write public attestation locally.
415
+ writeFile(
416
+ getAttestationLocalFilePath(`${ceremonyPrefix}_${commonTerms.foldersAndPathsTerms.attestation}.log`),
417
+ Buffer.from(publicAttestation)
418
+ )
419
+
420
+ await sleep(1000) // workaround for file descriptor unexpected close.
421
+
422
+ const gistUrl = await publishGist(participantAccessToken, publicAttestation, ceremonyName, ceremonyPrefix)
423
+
424
+ console.log(
425
+ `\n${theme.symbols.info} Your public attestation has been successfully posted as Github Gist (${theme.text.bold(
426
+ theme.text.underlined(gistUrl)
427
+ )})`
428
+ )
429
+
430
+ // Prepare a ready-to-share tweet.
431
+ await handleTweetGeneration(ceremonyName, gistUrl)
432
+ }
433
+
434
+ /**
435
+ * Listen to circuit document changes.
436
+ * @notice the circuit is the one for which the participant wants to contribute.
437
+ * @dev display custom messages in order to make the participant able to follow what's going while waiting in the queue.
438
+ * Also, this listener use another listener for the current circuit contributor in order to inform the waiting participant about the current contributor's progress.
439
+ * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
440
+ * @param ceremonyId <string> - the unique identifier of the ceremony.
441
+ * @param participantId <string> - the unique identifier of the participant.
442
+ * @param circuit <FirebaseDocumentInfo> - the Firestore document info about the circuit.
443
+ */
444
+ export const listenToCeremonyCircuitDocumentChanges = (
445
+ firestoreDatabase: Firestore,
446
+ ceremonyId: string,
447
+ participantId: string,
448
+ circuit: FirebaseDocumentInfo
449
+ ) => {
450
+ console.log(
451
+ `${theme.text.bold(
452
+ `\n- Circuit # ${theme.colors.magenta(`${circuit.data.sequencePosition}`)}`
453
+ )} (Waiting Queue)`
454
+ )
455
+
456
+ let cachedLatestPosition = 0
457
+
458
+ const unsubscribeToCeremonyCircuitListener = onSnapshot(circuit.ref, async (changedCircuit: DocumentSnapshot) => {
459
+ // Check data.
460
+ if (!circuit.data || !changedCircuit.data()) showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_NO_CIRCUIT_DATA, true)
461
+
462
+ // Extract data.
463
+ const { avgTimings, waitingQueue } = changedCircuit.data()!
464
+ const { fullContribution, verifyCloudFunction } = avgTimings
465
+ const { currentContributor } = waitingQueue
466
+
467
+ // Get circuit current contributor participant document.
468
+ const circuitCurrentContributor = await getDocumentById(
469
+ firestoreDatabase,
470
+ getParticipantsCollectionPath(ceremonyId),
471
+ currentContributor
472
+ )
473
+
474
+ // Check data.
475
+ if (!circuitCurrentContributor.data())
476
+ showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_NO_CURRENT_CONTRIBUTOR_DATA, true)
477
+
478
+ // Get participant position in the waiting queue of the circuit.
479
+ const latestParticipantPositionInQueue = waitingQueue.contributors.indexOf(participantId) + 1
480
+
481
+ // Compute time estimation based on latest participant position in the waiting queue.
482
+ const newEstimatedWaitingTime =
483
+ fullContribution <= 0 && verifyCloudFunction <= 0
484
+ ? 0
485
+ : (fullContribution + verifyCloudFunction) * (latestParticipantPositionInQueue - 1)
486
+
487
+ // Extract time.
488
+ const { seconds, minutes, hours, days } = getSecondsMinutesHoursFromMillis(newEstimatedWaitingTime)
489
+
490
+ // Check if the participant is now the new current contributor for the circuit.
491
+ if (latestParticipantPositionInQueue === 1) {
492
+ console.log(`\n${theme.symbols.info} Your contribution will begin shortly ${theme.emojis.tada}`)
493
+
494
+ // Unsubscribe from updates.
495
+ unsubscribeToCeremonyCircuitListener()
496
+ // eslint-disable no-unused-vars
497
+ } else if (latestParticipantPositionInQueue !== cachedLatestPosition) {
498
+ // Display updated position and waiting time.
499
+ console.log(
500
+ `${theme.symbols.info} ${`You will have to wait for ${theme.text.bold(
501
+ theme.colors.magenta(latestParticipantPositionInQueue - 1)
502
+ )} contributors`} (~${
503
+ newEstimatedWaitingTime > 0
504
+ ? `${theme.text.bold(
505
+ `${convertToDoubleDigits(days)}:${convertToDoubleDigits(hours)}:${convertToDoubleDigits(
506
+ minutes
507
+ )}:${convertToDoubleDigits(seconds)}`
508
+ )}`
509
+ : `no time`
510
+ } (dd/hh/mm/ss))`
511
+ )
512
+
513
+ cachedLatestPosition = latestParticipantPositionInQueue
514
+ }
515
+ })
516
+ }
517
+
518
+ /**
519
+ * Listen to current authenticated participant document changes.
520
+ * @dev this is the core business logic related to the execution of the contribute command.
521
+ * Basically, the command follows the updates of circuit waiting queue, participant status and contribution steps,
522
+ * while covering aspects regarding memory requirements, contribution completion or resumability, interaction w/ cloud functions, and so on.
523
+ * @notice in order to compute a contribute for each circuit, this method follows several steps:
524
+ * 1) Checking participant memory availability on root disk before joining for the first contribution (circuit having circuitPosition = 1).
525
+ * 2) Check if the participant has not completed the contributions for every circuit or has just finished contributing.
526
+ * 3) If (2) is true:
527
+ * 3.A) Check if the participant switched to `WAITING` as contribution status.
528
+ * 3.A.1) if true; display circuit waiting queue updates to the participant (listener to circuit document changes).
529
+ * 3.A.2) otherwise; do nothing and continue with other checks.
530
+ * 3.B) Check if the participant switched to `CONTRIBUTING` status. The participant must be the current contributor for the circuit w/ a resumable contribution step.
531
+ * 3.B.1) if true; start or resume the contribution from last contribution step.
532
+ * 3.B.2) otherwise; do nothing and continue with other checks.
533
+ * 3.C) Check if the current contributor is resuming from the "VERIFYING" contribution step.
534
+ * 3.C.1) if true; display previous completed steps and wait for verification results.
535
+ * 3.C.2) otherwise; do nothing and continue with other checks.
536
+ * 3.D) Check if the 'verifycontribution' cloud function has successfully completed the execution.
537
+ * 3.D.1) if true; get and display contribution verification results.
538
+ * 3.D.2) otherwise; do nothing and continue with other checks.
539
+ * 3.E) Check if the participant experiences a timeout while contributing.
540
+ * 3.E.1) if true; display timeout message and gracefully terminate.
541
+ * 3.E.2) otherwise; do nothing and continue with other checks.
542
+ * 3.F) Check if the participant has completed the contribution or is trying to resume the contribution after timeout expiration.
543
+ * 3.F.1) if true; check the memory requirement for next/current (completed/resuming) contribution while
544
+ * handling early interruption of contributions resulting in a final public attestation generation.
545
+ * (this allows a user to stop their contributions to a certain circuit X if their cannot provide/do not own
546
+ * an adequate amount of memory for satisfying the memory requirements of the next/current contribution).
547
+ * 3.F.2) otherwise; do nothing and continue with other checks.
548
+ * 3.G) Check if the participant has already contributed to every circuit when running the command.
549
+ * 3.G.1) if true; generate public final attestation and gracefully exit.
550
+ * 3.G.2) otherwise; do nothing
551
+ * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
552
+ * @param cloudFunctions <Functions> - the instance of the Firebase cloud functions for the application.
553
+ * @param participant <DocumentSnapshot<DocumentData>> - the Firestore document of the participant.
554
+ * @param ceremony <FirebaseDocumentInfo> - the Firestore document info about the selected ceremony.
555
+ * @param entropy <string> - the random value (aka toxic waste) entered by the participant for the contribution.
556
+ * @param providerUserId <string> - the unique provider user identifier associated to the authenticated account.
557
+ * @param accessToken <string> - the Github token generated through the Device Flow process.
558
+ */
559
+ export const listenToParticipantDocumentChanges = async (
560
+ firestoreDatabase: Firestore,
561
+ cloudFunctions: Functions,
562
+ participant: DocumentSnapshot<DocumentData>,
563
+ ceremony: FirebaseDocumentInfo,
564
+ entropy: string,
565
+ providerUserId: string,
566
+ accessToken: string
567
+ ) => {
568
+ // Listen to participant document changes.
569
+ // nb. this listener encapsulates the core business logic of the contribute command.
570
+ // the `changedParticipant` is the updated version (w/ newest changes) of the participant's document.
571
+ const unsubscribe = onSnapshot(participant.ref, async (changedParticipant: DocumentSnapshot) => {
572
+ // Check data.
573
+ if (!participant.data() || !changedParticipant.data())
574
+ showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_NO_PARTICIPANT_DATA, true)
575
+
576
+ // Extract data.
577
+ const {
578
+ contributionProgress: prevContributionProgress,
579
+ status: prevStatus,
580
+ contributions: prevContributions,
581
+ contributionStep: prevContributionStep,
582
+ tempContributionData: prevTempContributionData
583
+ } = participant.data()!
584
+
585
+ const {
586
+ contributionProgress: changedContributionProgress,
587
+ status: changedStatus,
588
+ contributionStep: changedContributionStep,
589
+ contributions: changedContributions,
590
+ tempContributionData: changedTempContributionData,
591
+ verificationStartedAt: changedVerificationStartedAt
592
+ } = changedParticipant.data()!
593
+
594
+ // Get latest updates from ceremony circuits.
595
+ const circuits = await getCeremonyCircuits(firestoreDatabase, ceremony.id)
596
+
597
+ // Step (1).
598
+ // Handle disk space requirement check for first contribution.
599
+ if (
600
+ changedStatus === ParticipantStatus.WAITING &&
601
+ !changedContributionStep &&
602
+ !changedContributions.length &&
603
+ !changedContributionProgress
604
+ ) {
605
+ // Get circuit by sequence position among ceremony circuits.
606
+ const circuit = getCircuitBySequencePosition(circuits, changedContributionProgress + 1)
607
+
608
+ // Extract data.
609
+ const { sequencePosition, zKeySizeInBytes } = circuit.data
610
+
611
+ // Check participant disk space availability for next contribution.
612
+ await handleDiskSpaceRequirementForNextContribution(
613
+ cloudFunctions,
614
+ ceremony.id,
615
+ sequencePosition,
616
+ zKeySizeInBytes,
617
+ false,
618
+ providerUserId
619
+ )
620
+ }
621
+
622
+ // Step (2).
623
+ if (changedContributionProgress > 0 && changedContributionProgress <= circuits.length) {
624
+ // Step (3).
625
+ // Get circuit for which the participant wants to contribute.
626
+ const circuit = circuits[changedContributionProgress - 1]
627
+
628
+ // Check data.
629
+ if (!circuit.data) showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_NO_CIRCUIT_DATA, true)
630
+
631
+ // Extract circuit data.
632
+ const { waitingQueue } = circuit.data
633
+
634
+ // Define pre-conditions for different scenarios.
635
+ const isWaitingForContribution = changedStatus === ParticipantStatus.WAITING
636
+
637
+ const isCurrentContributor =
638
+ changedStatus === ParticipantStatus.CONTRIBUTING && waitingQueue.currentContributor === participant.id
639
+
640
+ const isResumingContribution =
641
+ changedContributionStep === prevContributionStep &&
642
+ changedContributionProgress === prevContributionProgress
643
+
644
+ const noStatusChanges = changedStatus === prevStatus
645
+
646
+ const progressToNextContribution = changedContributionStep === ParticipantContributionStep.COMPLETED
647
+
648
+ const completedContribution = progressToNextContribution && changedStatus === ParticipantStatus.CONTRIBUTED
649
+
650
+ const timeoutTriggeredWhileContributing =
651
+ changedStatus === ParticipantStatus.TIMEDOUT &&
652
+ changedContributionStep !== ParticipantContributionStep.COMPLETED
653
+
654
+ const timeoutExpired = changedStatus === ParticipantStatus.EXHUMED
655
+
656
+ const alreadyContributedToEveryCeremonyCircuit =
657
+ changedStatus === ParticipantStatus.DONE &&
658
+ changedContributionStep === ParticipantContributionStep.COMPLETED &&
659
+ changedContributionProgress === circuits.length &&
660
+ changedContributions.length === circuits.length
661
+
662
+ const noTemporaryContributionData = !prevTempContributionData && !changedTempContributionData
663
+
664
+ const samePermanentContributionData =
665
+ (!prevContributions && !changedContributions) ||
666
+ prevContributions.length === changedContributions.length
667
+
668
+ const downloadingStep = changedContributionStep === ParticipantContributionStep.DOWNLOADING
669
+ const computingStep = changedContributionStep === ParticipantContributionStep.COMPUTING
670
+ const uploadingStep = changedContributionStep === ParticipantContributionStep.UPLOADING
671
+
672
+ const hasResumableStep = downloadingStep || computingStep || uploadingStep
673
+
674
+ const resumingContribution =
675
+ prevContributionStep === changedContributionStep &&
676
+ prevStatus === changedStatus &&
677
+ prevContributionProgress === changedContributionProgress
678
+
679
+ const resumingContributionButAdvancedToAnotherStep = prevContributionStep !== changedContributionStep
680
+
681
+ const resumingAfterTimeoutExpiration = prevStatus === ParticipantStatus.EXHUMED
682
+
683
+ const neverResumedContribution = !prevContributionStep
684
+
685
+ const resumingWithSameTemporaryData =
686
+ !!prevTempContributionData &&
687
+ !!changedTempContributionData &&
688
+ JSON.stringify(Object.keys(prevTempContributionData).sort()) ===
689
+ JSON.stringify(Object.keys(changedTempContributionData).sort()) &&
690
+ JSON.stringify(Object.values(prevTempContributionData).sort()) ===
691
+ JSON.stringify(Object.values(changedTempContributionData).sort())
692
+
693
+ const startingOrResumingContribution =
694
+ // Pre-condition W => contribute / resume when contribution step = DOWNLOADING.
695
+ (isCurrentContributor &&
696
+ downloadingStep &&
697
+ (resumingContribution ||
698
+ resumingContributionButAdvancedToAnotherStep ||
699
+ resumingAfterTimeoutExpiration ||
700
+ neverResumedContribution)) ||
701
+ // Pre-condition X => contribute / resume when contribution step = COMPUTING.
702
+ (computingStep && resumingContribution && samePermanentContributionData) ||
703
+ // Pre-condition Y => contribute / resume when contribution step = UPLOADING without any pre-uploaded chunk.
704
+ (uploadingStep && resumingContribution && noTemporaryContributionData) ||
705
+ // Pre-condition Z => contribute / resume when contribution step = UPLOADING w/ some pre-uploaded chunk.
706
+ (!noTemporaryContributionData && resumingWithSameTemporaryData)
707
+
708
+ // Scenario (3.B).
709
+ if (isCurrentContributor && hasResumableStep && startingOrResumingContribution) {
710
+ // Communicate resume / start of the contribution to participant.
711
+ await simpleLoader(
712
+ `${
713
+ changedContributionStep === ParticipantContributionStep.DOWNLOADING ? `Starting` : `Resuming`
714
+ } your contribution...`,
715
+ `clock`,
716
+ 3000
717
+ )
718
+
719
+ // Start / Resume the contribution for the participant.
720
+ await handleStartOrResumeContribution(
721
+ cloudFunctions,
722
+ firestoreDatabase,
723
+ ceremony,
724
+ circuit,
725
+ participant,
726
+ entropy,
727
+ providerUserId,
728
+ false // not finalizing.
729
+ )
730
+ }
731
+ // Scenario (3.A).
732
+ else if (isWaitingForContribution)
733
+ listenToCeremonyCircuitDocumentChanges(firestoreDatabase, ceremony.id, participant.id, circuit)
734
+
735
+ // Scenario (3.C).
736
+ // Pre-condition: current contributor + resuming from verification step.
737
+ if (
738
+ isCurrentContributor &&
739
+ isResumingContribution &&
740
+ changedContributionStep === ParticipantContributionStep.VERIFYING
741
+ ) {
742
+ const spinner = customSpinner(`Getting info about your current contribution...`, `clock`)
743
+ spinner.start()
744
+
745
+ // Get current and next index.
746
+ const currentZkeyIndex = formatZkeyIndex(changedContributionProgress)
747
+ const nextZkeyIndex = formatZkeyIndex(changedContributionProgress + 1)
748
+
749
+ // Get average verification time (Cloud Function).
750
+ const avgVerifyCloudFunctionTime = circuit.data.avgTimings.verifyCloudFunction
751
+ // Compute estimated time left for this contribution verification.
752
+ const estimatedTimeLeftForVerification =
753
+ Date.now() - changedVerificationStartedAt - avgVerifyCloudFunctionTime
754
+ // Format time.
755
+ const { seconds, minutes, hours } = getSecondsMinutesHoursFromMillis(estimatedTimeLeftForVerification)
756
+
757
+ spinner.stop()
758
+
759
+ console.log(
760
+ `${theme.text.bold(
761
+ `\n- Circuit # ${theme.colors.magenta(`${circuit.data.sequencePosition}`)}`
762
+ )} (Contribution Steps)`
763
+ )
764
+ console.log(
765
+ `${theme.symbols.success} Contribution ${theme.text.bold(`#${currentZkeyIndex}`)} downloaded`
766
+ )
767
+ console.log(`${theme.symbols.success} Contribution ${theme.text.bold(`#${nextZkeyIndex}`)} computed`)
768
+ console.log(
769
+ `${theme.symbols.success} Contribution ${theme.text.bold(`#${nextZkeyIndex}`)} saved on storage`
770
+ )
771
+
772
+ /// @todo resuming a contribution verification could potentially lead to no verification at all #18.
773
+ console.log(
774
+ `${theme.symbols.info} Contribution verification in progress (~ ${theme.text.bold(
775
+ `${convertToDoubleDigits(hours)}:${convertToDoubleDigits(minutes)}:${convertToDoubleDigits(
776
+ seconds
777
+ )}`
778
+ )})`
779
+ )
780
+ }
781
+
782
+ // Scenario (3.D).
783
+ // Pre-condition: contribution has been verified and,
784
+ // contributor status: DONE if completed all contributions or CONTRIBUTED if just completed the last one (not all).
785
+ if (
786
+ progressToNextContribution &&
787
+ noStatusChanges &&
788
+ (changedStatus === ParticipantStatus.DONE || changedStatus === ParticipantStatus.CONTRIBUTED)
789
+ )
790
+ // Get latest contribution verification result.
791
+ await getLatestVerificationResult(firestoreDatabase, ceremony.id, circuit.id, participant.id)
792
+
793
+ // Scenario (3.E).
794
+ if (timeoutTriggeredWhileContributing) {
795
+ await handleTimedoutMessageForContributor(
796
+ firestoreDatabase,
797
+ participant.id,
798
+ ceremony.id,
799
+ changedContributionProgress,
800
+ true
801
+ )
802
+
803
+ terminate(providerUserId)
804
+ }
805
+
806
+ // Scenario (3.F).
807
+ if (completedContribution || timeoutExpired) {
808
+ // Show data about latest contribution verification
809
+ if (completedContribution)
810
+ // Get latest contribution verification result.
811
+ await getLatestVerificationResult(firestoreDatabase, ceremony.id, circuit.id, participant.id)
812
+
813
+ // Get next circuit for contribution.
814
+ const nextCircuit = getCircuitBySequencePosition(circuits, changedContributionProgress + 1)
815
+
816
+ // Check disk space requirements for participant.
817
+ const wannaGenerateAttestation = await handleDiskSpaceRequirementForNextContribution(
818
+ cloudFunctions,
819
+ ceremony.id,
820
+ nextCircuit.data.sequencePosition,
821
+ nextCircuit.data.zKeySizeInBytes,
822
+ timeoutExpired,
823
+ providerUserId
824
+ )
825
+
826
+ // Check if the participant would like to generate a new attestation.
827
+ if (wannaGenerateAttestation) {
828
+ // Handle public attestation generation and operations.
829
+ await handlePublicAttestation(
830
+ firestoreDatabase,
831
+ circuits,
832
+ ceremony.id,
833
+ participant.id,
834
+ changedContributions,
835
+ providerUserId,
836
+ ceremony.data.title,
837
+ ceremony.data.prefix,
838
+ accessToken
839
+ )
840
+
841
+ console.log(
842
+ `\nThank you for participating and securing the ${ceremony.data.title} ceremony ${theme.emojis.pray}`
843
+ )
844
+
845
+ // Unsubscribe from listener.
846
+ unsubscribe()
847
+
848
+ // Gracefully exit.
849
+ terminate(providerUserId)
850
+ }
851
+ }
852
+
853
+ // Scenario (3.G).
854
+ if (alreadyContributedToEveryCeremonyCircuit) {
855
+ // Get latest contribution verification result.
856
+ await getLatestVerificationResult(firestoreDatabase, ceremony.id, circuit.id, participant.id)
857
+
858
+ // Handle public attestation generation and operations.
859
+ await handlePublicAttestation(
860
+ firestoreDatabase,
861
+ circuits,
862
+ ceremony.id,
863
+ participant.id,
864
+ changedContributions,
865
+ providerUserId,
866
+ ceremony.data.title,
867
+ ceremony.data.prefix,
868
+ accessToken
869
+ )
870
+
871
+ console.log(
872
+ `\nThank you for participating and securing the ${ceremony.data.title} ceremony ${theme.emojis.pray}`
873
+ )
874
+
875
+ // Unsubscribe from listener.
876
+ unsubscribe()
877
+
878
+ // Gracefully exit.
879
+ terminate(providerUserId)
880
+ }
881
+ }
882
+ })
883
+ }
884
+
885
+ /**
886
+ * Contribute command.
887
+ * @notice The contribute command allows an authenticated user to become a participant (contributor) to the selected ceremony by providing the
888
+ * entropy (toxic waste) for the contribution.
889
+ * @dev For proper execution, the command requires the user to be authenticated with Github account (run auth command first) in order to
890
+ * handle sybil-resistance and connect to Github APIs to publish the gist containing the public attestation.
891
+ */
892
+ const contribute = async (opt: any) => {
893
+ const { firebaseApp, firebaseFunctions, firestoreDatabase } = await bootstrapCommandExecutionAndServices()
894
+
895
+ // Check for authentication.
896
+ const { user, providerUserId, token } = await checkAuth(firebaseApp)
897
+
898
+ // Get options.
899
+ const ceremonyOpt = opt.ceremony
900
+ const entropyOpt = opt.entropy
901
+
902
+ // Prepare data.
903
+ let selectedCeremony: FirebaseDocumentInfo
904
+
905
+ // Retrieve the opened ceremonies.
906
+ const ceremoniesOpenedForContributions = await getOpenedCeremonies(firestoreDatabase)
907
+
908
+ // Gracefully exit if no ceremonies are opened for contribution.
909
+ if (!ceremoniesOpenedForContributions.length)
910
+ showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_NO_OPENED_CEREMONIES, true)
911
+
912
+ console.log(
913
+ `${theme.symbols.warning} ${theme.text.bold(
914
+ `The contribution process is based on a parallel waiting queue mechanism allowing one contributor at a time per circuit with a maximum time upper-bound. Each contribution may require the bulk of your computing resources and memory based on the size of the circuit (ETAs could vary!). If you stop your contribution at any step, you have to restart the step from scratch (except for uploading).`
915
+ )}\n`
916
+ )
917
+
918
+ if (ceremonyOpt) {
919
+ // Check if the input ceremony title match with an opened ceremony.
920
+ const selectedCeremonyDocument = ceremoniesOpenedForContributions.filter(
921
+ (openedCeremony: FirebaseDocumentInfo) => openedCeremony.data.prefix === ceremonyOpt
922
+ )
923
+
924
+ if (selectedCeremonyDocument.length !== 1) {
925
+ // Notify user about error.
926
+ console.log(`${theme.symbols.error} ${COMMAND_ERRORS.COMMAND_CONTRIBUTE_WRONG_OPTION_CEREMONY}`)
927
+
928
+ // Show potential ceremonies
929
+ console.log(`${theme.symbols.info} Currently, you can contribute to the following ceremonies: `)
930
+
931
+ for (const openedCeremony of ceremoniesOpenedForContributions)
932
+ console.log(`- ${theme.text.bold(openedCeremony.data.prefix)}\n`)
933
+
934
+ terminate(providerUserId)
935
+ } else selectedCeremony = selectedCeremonyDocument.at(0)
936
+ } else {
937
+ // Prompt the user to select a ceremony from the opened ones.
938
+ selectedCeremony = await promptForCeremonySelection(ceremoniesOpenedForContributions, false)
939
+ }
940
+
941
+ // Get selected ceremony circuit(s) documents.
942
+ const circuits = await getCeremonyCircuits(firestoreDatabase, selectedCeremony.id)
943
+
944
+ const spinner = customSpinner(`Verifying your participant status...`, `clock`)
945
+ spinner.start()
946
+
947
+ // Check that the user's document is created
948
+ const userDoc = await getDocumentById(firestoreDatabase, commonTerms.collections.users.name, user.uid)
949
+ const userData = userDoc.data()
950
+ if (!userData) {
951
+ spinner.fail(
952
+ `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.`
953
+ )
954
+ process.exit(0)
955
+ }
956
+
957
+ // Check the user's current participant readiness for contribution status (eligible, already contributed, timed out).
958
+ const canParticipantContributeToCeremony = await checkParticipantForCeremony(firebaseFunctions, selectedCeremony.id)
959
+
960
+ await sleep(2000) // wait for CF execution.
961
+
962
+ // Get updated participant data.
963
+ const participant = await getDocumentById(
964
+ firestoreDatabase,
965
+ getParticipantsCollectionPath(selectedCeremony.id),
966
+ user.uid
967
+ )
968
+
969
+ const participantData = participant.data()
970
+
971
+ if (!participantData) showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_NO_PARTICIPANT_DATA, true)
972
+
973
+ if (canParticipantContributeToCeremony) {
974
+ spinner.succeed(`Great, you are qualified to contribute to the ceremony`)
975
+
976
+ let entropy = "" // toxic waste.
977
+
978
+ // Prepare local directories.
979
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.output)
980
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.contribute)
981
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.contributions)
982
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.attestations)
983
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.transcripts)
984
+
985
+ // Extract participant data.
986
+ const { contributionProgress, contributionStep } = participantData!
987
+
988
+ // Check if the participant can input the entropy
989
+ if (
990
+ contributionProgress < circuits.length ||
991
+ (contributionProgress === circuits.length && contributionStep < ParticipantContributionStep.UPLOADING)
992
+ ) {
993
+ if (entropyOpt) entropy = entropyOpt
994
+ /// @todo should we preserve entropy between different re-run of the command? (e.g., resume after timeout).
995
+ // Prompt for entropy generation.
996
+ else entropy = await promptForEntropy()
997
+ }
998
+
999
+ // Listener to following the core contribution workflow.
1000
+ await listenToParticipantDocumentChanges(
1001
+ firestoreDatabase,
1002
+ firebaseFunctions,
1003
+ participant,
1004
+ selectedCeremony,
1005
+ entropy,
1006
+ providerUserId,
1007
+ token
1008
+ )
1009
+ } else {
1010
+ // Extract participant data.
1011
+ const { status, contributionStep, contributionProgress } = participantData!
1012
+
1013
+ // Check whether the participant has already contributed to all circuits.
1014
+ if (
1015
+ (!canParticipantContributeToCeremony && status === ParticipantStatus.DONE) ||
1016
+ status === ParticipantStatus.FINALIZED
1017
+ ) {
1018
+ spinner.info(`You have already made the contributions for the circuits in the ceremony`)
1019
+
1020
+ // await handleContributionValidity(firestoreDatabase, circuits, selectedCeremony.id, participant.id)
1021
+
1022
+ spinner.text = "Checking your public attestation gist..."
1023
+ spinner.start()
1024
+
1025
+ // Check whether the user has published the Github Gist about the public attestation.
1026
+ const publishedPublicAttestationGist = await getPublicAttestationGist(
1027
+ token,
1028
+ `${selectedCeremony.data.prefix}_${commonTerms.foldersAndPathsTerms.attestation}.log`
1029
+ )
1030
+
1031
+ if (!publishedPublicAttestationGist) {
1032
+ spinner.stop()
1033
+
1034
+ await handlePublicAttestation(
1035
+ firestoreDatabase,
1036
+ circuits,
1037
+ selectedCeremony.id,
1038
+ participant.id,
1039
+ participantData?.contributions!,
1040
+ providerUserId,
1041
+ selectedCeremony.data.title,
1042
+ selectedCeremony.data.prefix,
1043
+ token
1044
+ )
1045
+ } else {
1046
+ // Extract url from raw.
1047
+ const gistUrl = publishedPublicAttestationGist.raw_url.substring(
1048
+ 0,
1049
+ publishedPublicAttestationGist.raw_url.indexOf("/raw/")
1050
+ )
1051
+
1052
+ spinner.stop()
1053
+
1054
+ process.stdout.write(`\n`)
1055
+ console.log(
1056
+ `${
1057
+ theme.symbols.success
1058
+ } Your public attestation has been successfully posted as Github Gist (${theme.text.bold(
1059
+ theme.text.underlined(gistUrl)
1060
+ )})`
1061
+ )
1062
+
1063
+ // Prepare a ready-to-share tweet.
1064
+ await handleTweetGeneration(selectedCeremony.data.title, gistUrl)
1065
+ }
1066
+
1067
+ console.log(
1068
+ `\nThank you for participating and securing the ${selectedCeremony.data.title} ceremony ${theme.emojis.pray}`
1069
+ )
1070
+ }
1071
+
1072
+ // Check if there's a timeout still in effect for the participant.
1073
+ if (status === ParticipantStatus.TIMEDOUT && contributionStep !== ParticipantContributionStep.COMPLETED) {
1074
+ spinner.warn(`Oops, you are not allowed to continue your contribution due to current timeout`)
1075
+
1076
+ await handleTimedoutMessageForContributor(
1077
+ firestoreDatabase,
1078
+ participant.id,
1079
+ selectedCeremony.id,
1080
+ contributionProgress,
1081
+ false
1082
+ )
1083
+ }
1084
+
1085
+ // Exit gracefully.
1086
+ terminate(providerUserId)
1087
+ }
1088
+ }
1089
+
1090
+ export default contribute