@devtion/backend 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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +151 -0
  3. package/dist/src/functions/index.js +2644 -0
  4. package/dist/src/functions/index.mjs +2596 -0
  5. package/dist/types/functions/ceremony.d.ts +33 -0
  6. package/dist/types/functions/ceremony.d.ts.map +1 -0
  7. package/dist/types/functions/circuit.d.ts +63 -0
  8. package/dist/types/functions/circuit.d.ts.map +1 -0
  9. package/dist/types/functions/index.d.ts +7 -0
  10. package/dist/types/functions/index.d.ts.map +1 -0
  11. package/dist/types/functions/participant.d.ts +58 -0
  12. package/dist/types/functions/participant.d.ts.map +1 -0
  13. package/dist/types/functions/storage.d.ts +37 -0
  14. package/dist/types/functions/storage.d.ts.map +1 -0
  15. package/dist/types/functions/timeout.d.ts +26 -0
  16. package/dist/types/functions/timeout.d.ts.map +1 -0
  17. package/dist/types/functions/user.d.ts +15 -0
  18. package/dist/types/functions/user.d.ts.map +1 -0
  19. package/dist/types/lib/errors.d.ts +75 -0
  20. package/dist/types/lib/errors.d.ts.map +1 -0
  21. package/dist/types/lib/services.d.ts +9 -0
  22. package/dist/types/lib/services.d.ts.map +1 -0
  23. package/dist/types/lib/utils.d.ts +141 -0
  24. package/dist/types/lib/utils.d.ts.map +1 -0
  25. package/dist/types/types/enums.d.ts +13 -0
  26. package/dist/types/types/enums.d.ts.map +1 -0
  27. package/dist/types/types/index.d.ts +130 -0
  28. package/dist/types/types/index.d.ts.map +1 -0
  29. package/package.json +89 -0
  30. package/src/functions/ceremony.ts +333 -0
  31. package/src/functions/circuit.ts +1092 -0
  32. package/src/functions/index.ts +36 -0
  33. package/src/functions/participant.ts +526 -0
  34. package/src/functions/storage.ts +548 -0
  35. package/src/functions/timeout.ts +294 -0
  36. package/src/functions/user.ts +142 -0
  37. package/src/lib/errors.ts +237 -0
  38. package/src/lib/services.ts +28 -0
  39. package/src/lib/utils.ts +472 -0
  40. package/src/types/enums.ts +12 -0
  41. package/src/types/index.ts +140 -0
  42. package/test/index.test.ts +62 -0
@@ -0,0 +1,36 @@
1
+ import admin from "firebase-admin"
2
+
3
+ export { registerAuthUser, processSignUpWithCustomClaims } from "./user"
4
+ export {
5
+ startCeremony,
6
+ stopCeremony,
7
+ setupCeremony,
8
+ initEmptyWaitingQueueForCircuit,
9
+ finalizeCeremony
10
+ } from "./ceremony"
11
+ export {
12
+ checkParticipantForCeremony,
13
+ progressToNextContributionStep,
14
+ permanentlyStoreCurrentContributionTimeAndHash,
15
+ temporaryStoreCurrentContributionMultiPartUploadId,
16
+ temporaryStoreCurrentContributionUploadedChunkData,
17
+ progressToNextCircuitForContribution,
18
+ checkAndPrepareCoordinatorForFinalization
19
+ } from "./participant"
20
+ export {
21
+ coordinateCeremonyParticipant,
22
+ verifycontribution,
23
+ refreshParticipantAfterContributionVerification,
24
+ finalizeCircuit
25
+ } from "./circuit"
26
+ export {
27
+ createBucket,
28
+ checkIfObjectExist,
29
+ generateGetObjectPreSignedUrl,
30
+ startMultiPartUpload,
31
+ generatePreSignedUrlsParts,
32
+ completeMultiPartUpload
33
+ } from "./storage"
34
+ export { checkAndRemoveBlockingContributor, resumeContributionAfterTimeoutExpiration } from "./timeout"
35
+
36
+ admin.initializeApp()
@@ -0,0 +1,526 @@
1
+ import * as functions from "firebase-functions"
2
+ import admin from "firebase-admin"
3
+ import dotenv from "dotenv"
4
+ import {
5
+ ParticipantDocument,
6
+ CeremonyState,
7
+ ParticipantStatus,
8
+ ParticipantContributionStep,
9
+ getParticipantsCollectionPath,
10
+ commonTerms
11
+ } from "@p0tion/actions"
12
+ import { FieldValue } from "firebase-admin/firestore"
13
+ import {
14
+ PermanentlyStoreCurrentContributionTimeAndHash,
15
+ TemporaryStoreCurrentContributionMultiPartUploadId,
16
+ TemporaryStoreCurrentContributionUploadedChunkData
17
+ } from "../types/index"
18
+ import {
19
+ getCeremonyCircuits,
20
+ getCurrentServerTimestampInMillis,
21
+ getDocumentById,
22
+ queryNotExpiredTimeouts
23
+ } from "../lib/utils"
24
+ import { COMMON_ERRORS, logAndThrowError, printLog, SPECIFIC_ERRORS } from "../lib/errors"
25
+ import { LogLevel } from "../types/enums"
26
+
27
+ dotenv.config()
28
+
29
+ /**
30
+ * Check the user's current participant status for the ceremony.
31
+ * @notice this cloud function has several tasks:
32
+ * 1) Check if the authenticated user is a participant
33
+ * 1.A) If not, register it has new participant for the ceremony.
34
+ * 1.B) Otherwise:
35
+ * 2.A) Check if already contributed to all circuits or,
36
+ * 3.A) If already contributed, return false
37
+ * 2.B) Check if it has a timeout in progress
38
+ * 3.B) If timeout expired, allows the participant to resume the contribution and remove stale/outdated
39
+ * temporary data.
40
+ * 3.C) Otherwise, return false.
41
+ * 2.C) Check if there are temporary stale contribution data if the contributor has interrupted the contribution
42
+ * while completing the `COMPUTING` step and, if any, delete them.
43
+ * 1.D) If no timeout / participant already exist, just return true.
44
+ * @dev true when the participant can participate (1.A, 3.B, 1.D); otherwise false.
45
+ */
46
+ export const checkParticipantForCeremony = functions
47
+ .region('europe-west1')
48
+ .runWith({
49
+ memory: "512MB"
50
+ })
51
+ .https.onCall(async (data: { ceremonyId: string }, context: functions.https.CallableContext) => {
52
+ if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator))
53
+ logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER)
54
+
55
+ if (!data.ceremonyId) logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA)
56
+
57
+ // Prepare Firestore DB.
58
+ const firestore = admin.firestore()
59
+
60
+ // Get data.
61
+ const { ceremonyId } = data
62
+ const userId = context.auth?.uid
63
+
64
+ // Look for the ceremony document.
65
+ const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId)
66
+
67
+ // Extract data.
68
+ const ceremonyData = ceremonyDoc.data()
69
+ const { state } = ceremonyData!
70
+
71
+ if (!ceremonyData) logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA)
72
+
73
+ // Check pre-condition (ceremony state opened).
74
+ if (state !== CeremonyState.OPENED) logAndThrowError(SPECIFIC_ERRORS.SE_PARTICIPANT_CEREMONY_NOT_OPENED)
75
+
76
+ // Check (1).
77
+ // nb. do not use `getDocumentById()` here as we need the falsy condition.
78
+ const participantDoc = await firestore.collection(getParticipantsCollectionPath(ceremonyId)).doc(userId!).get()
79
+
80
+ if (!participantDoc.exists) {
81
+ // Action (1.A).
82
+ const participantData: ParticipantDocument = {
83
+ userId: participantDoc.id,
84
+ status: ParticipantStatus.WAITING,
85
+ contributionProgress: 0,
86
+ contributionStartedAt: 0,
87
+ contributions: [],
88
+ lastUpdated: getCurrentServerTimestampInMillis()
89
+ }
90
+
91
+ // Register user as participant.
92
+ await participantDoc.ref.set(participantData)
93
+
94
+ printLog(
95
+ `The user ${userId} has been registered as participant for ceremony ${ceremonyDoc.id}`,
96
+ LogLevel.DEBUG
97
+ )
98
+
99
+ return true
100
+ }
101
+ // Check (1.B).
102
+
103
+ // Extract data.
104
+ const participantData = participantDoc.data()
105
+ const { contributionProgress, contributionStep, contributions, status, tempContributionData } = participantData!
106
+
107
+ if (!participantData) logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA)
108
+
109
+ // Get ceremony' circuits.
110
+ const circuits = await getCeremonyCircuits(ceremonyDoc.id)
111
+
112
+ // Check (2.A).
113
+ if (contributionProgress === circuits.length && status === ParticipantStatus.DONE) {
114
+ // Action (3.A).
115
+ printLog(`Contributor ${participantDoc.id} has already contributed to all circuits`, LogLevel.DEBUG)
116
+
117
+ return false
118
+ }
119
+
120
+ // Pre-conditions.
121
+ const staleContributionData = contributionProgress >= 1 && contributions.length === contributionProgress
122
+ const wasComputing = !!contributionStep && contributionStep === ParticipantContributionStep.COMPUTING
123
+
124
+ // Check (2.B).
125
+ if (status === ParticipantStatus.TIMEDOUT) {
126
+ // Query for not expired timeouts.
127
+ const notExpiredTimeouts = await queryNotExpiredTimeouts(ceremonyDoc.id, participantDoc.id)
128
+
129
+ if (notExpiredTimeouts.empty) {
130
+ // nb. stale contribution data is always the latest contribution.
131
+ if (staleContributionData) contributions.pop()
132
+
133
+ // Action (3.B).
134
+ participantDoc.ref.update({
135
+ status: ParticipantStatus.EXHUMED,
136
+ contributions,
137
+ tempContributionData: tempContributionData ? tempContributionData : FieldValue.delete(),
138
+ contributionStep: ParticipantContributionStep.DOWNLOADING,
139
+ contributionStartedAt: 0,
140
+ verificationStartedAt: FieldValue.delete(),
141
+ lastUpdated: getCurrentServerTimestampInMillis()
142
+ })
143
+
144
+ printLog(`Timeout expired for participant ${participantDoc.id}`, LogLevel.DEBUG)
145
+
146
+ return true
147
+ }
148
+ // Action (3.C).
149
+ printLog(`Timeout still in effect for the participant ${participantDoc.id}`, LogLevel.DEBUG)
150
+
151
+ return false
152
+ }
153
+
154
+ // Check (2.C).
155
+ if (staleContributionData && wasComputing) {
156
+ // nb. stale contribution data is always the latest contribution.
157
+ contributions.pop()
158
+
159
+ participantDoc.ref.update({
160
+ contributions,
161
+ lastUpdated: getCurrentServerTimestampInMillis()
162
+ })
163
+
164
+ printLog(`Removed stale contribution data for ${participantDoc.id}`, LogLevel.DEBUG)
165
+ }
166
+
167
+ // Action (1.D).
168
+ return true
169
+ })
170
+
171
+ /**
172
+ * Progress the participant to the next circuit preparing for the next contribution.
173
+ * @dev The participant can progress if and only if:
174
+ * 1) the participant has just been registered and is waiting to be queued for the first contribution (contributionProgress = 0 && status = WAITING).
175
+ * 2) the participant has just finished the contribution for a circuit (contributionProgress != 0 && status = CONTRIBUTED && contributionStep = COMPLETED).
176
+ */
177
+ export const progressToNextCircuitForContribution = functions
178
+ .region('europe-west1')
179
+ .runWith({
180
+ memory: "512MB"
181
+ })
182
+ .https.onCall(async (data: { ceremonyId: string }, context: functions.https.CallableContext): Promise<void> => {
183
+ if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator))
184
+ logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER)
185
+
186
+ if (!data.ceremonyId) logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA)
187
+
188
+ // Get data.
189
+ const { ceremonyId } = data
190
+ const userId = context.auth?.uid
191
+
192
+ // Look for the ceremony document.
193
+ const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId)
194
+ const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyId), userId!)
195
+
196
+ // Prepare documents data.
197
+ const participantData = participantDoc.data()
198
+
199
+ if (!ceremonyDoc.data() || !participantData) logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA)
200
+
201
+ // Extract data.
202
+ const { contributionProgress, contributionStep, status } = participantData!
203
+
204
+ // Define pre-conditions.
205
+ const waitingToBeQueuedForFirstContribution = status === ParticipantStatus.WAITING && contributionProgress === 0
206
+ const completedContribution =
207
+ status === ParticipantStatus.CONTRIBUTED &&
208
+ contributionStep === ParticipantContributionStep.COMPLETED &&
209
+ contributionProgress !== 0
210
+
211
+ // Check pre-conditions (1) or (2).
212
+ if (completedContribution || waitingToBeQueuedForFirstContribution)
213
+ await participantDoc.ref.update({
214
+ contributionProgress: contributionProgress + 1,
215
+ status: ParticipantStatus.READY,
216
+ lastUpdated: getCurrentServerTimestampInMillis()
217
+ })
218
+ else logAndThrowError(SPECIFIC_ERRORS.SE_CONTRIBUTE_CANNOT_PROGRESS_TO_NEXT_CIRCUIT)
219
+
220
+ printLog(
221
+ `Participant/Contributor ${userId} progress to the circuit in position ${contributionProgress + 1}`,
222
+ LogLevel.DEBUG
223
+ )
224
+ })
225
+
226
+ /**
227
+ * Progress the participant to the next contribution step while contributing to a circuit.
228
+ * @dev this cloud function must enforce the order among the contribution steps:
229
+ * 1) Downloading the last contribution.
230
+ * 2) Computing the next contribution.
231
+ * 3) Uploading the next contribution.
232
+ * 4) Requesting the verification to the cloud function `verifycontribution`.
233
+ * 5) Completed contribution computation and verification.
234
+ */
235
+ export const progressToNextContributionStep = functions
236
+ .region('europe-west1')
237
+ .runWith({
238
+ memory: "512MB"
239
+ })
240
+ .https.onCall(async (data: { ceremonyId: string }, context: functions.https.CallableContext) => {
241
+ if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator))
242
+ logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER)
243
+
244
+ if (!data.ceremonyId) logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA)
245
+
246
+ // Get data.
247
+ const { ceremonyId } = data
248
+ const userId = context.auth?.uid
249
+
250
+ // Look for the ceremony document.
251
+ const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId)
252
+ const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyDoc.id), userId!)
253
+
254
+ if (!ceremonyDoc.data() || !participantDoc.data()) logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA)
255
+
256
+ // Extract data.
257
+ const { state } = ceremonyDoc.data()!
258
+ const { status, contributionStep } = participantDoc.data()!
259
+
260
+ // Pre-condition: ceremony must be opened.
261
+ if (state !== CeremonyState.OPENED) logAndThrowError(SPECIFIC_ERRORS.SE_PARTICIPANT_CEREMONY_NOT_OPENED)
262
+
263
+ // Pre-condition: participant has contributing status.
264
+ if (status !== ParticipantStatus.CONTRIBUTING) logAndThrowError(SPECIFIC_ERRORS.SE_PARTICIPANT_NOT_CONTRIBUTING)
265
+
266
+ // Prepare the next contribution step.
267
+ let nextContributionStep = contributionStep
268
+
269
+ if (contributionStep === ParticipantContributionStep.DOWNLOADING)
270
+ nextContributionStep = ParticipantContributionStep.COMPUTING
271
+ else if (contributionStep === ParticipantContributionStep.COMPUTING)
272
+ nextContributionStep = ParticipantContributionStep.UPLOADING
273
+ else if (contributionStep === ParticipantContributionStep.UPLOADING)
274
+ nextContributionStep = ParticipantContributionStep.VERIFYING
275
+ else if (contributionStep === ParticipantContributionStep.VERIFYING)
276
+ nextContributionStep = ParticipantContributionStep.COMPLETED
277
+
278
+ // Send tx.
279
+ await participantDoc.ref.update({
280
+ contributionStep: nextContributionStep,
281
+ verificationStartedAt:
282
+ nextContributionStep === ParticipantContributionStep.VERIFYING
283
+ ? getCurrentServerTimestampInMillis()
284
+ : 0,
285
+ lastUpdated: getCurrentServerTimestampInMillis()
286
+ })
287
+
288
+ printLog(
289
+ `Participant ${participantDoc.id} advanced to ${nextContributionStep} contribution step`,
290
+ LogLevel.DEBUG
291
+ )
292
+ })
293
+
294
+ /**
295
+ * Write the information about current contribution hash and computation time for the current contributor.
296
+ * @dev enable the current contributor to resume a contribution from where it had left off.
297
+ */
298
+ export const permanentlyStoreCurrentContributionTimeAndHash = functions
299
+ .region('europe-west1')
300
+ .runWith({
301
+ memory: "512MB"
302
+ })
303
+ .https.onCall(
304
+ async (data: PermanentlyStoreCurrentContributionTimeAndHash, context: functions.https.CallableContext) => {
305
+ if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator))
306
+ logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER)
307
+
308
+ if (!data.ceremonyId || !data.contributionHash || data.contributionComputationTime <= 0)
309
+ logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA)
310
+
311
+ // Get data.
312
+ const { ceremonyId } = data
313
+ const userId = context.auth?.uid
314
+ const isCoordinator = context?.auth?.token.coordinator
315
+
316
+ // Look for the ceremony document.
317
+ const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId)
318
+ const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyDoc.id), userId!)
319
+
320
+ if (!ceremonyDoc.data() || !participantDoc.data())
321
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA)
322
+
323
+ // Extract data.
324
+ const { status, contributionStep, contributions: currentContributions } = participantDoc.data()!
325
+
326
+ // Pre-condition: computing contribution step or finalizing (only for coordinator when finalizing ceremony).
327
+ if (
328
+ contributionStep === ParticipantContributionStep.COMPUTING ||
329
+ (isCoordinator && status === ParticipantStatus.FINALIZING)
330
+ )
331
+ // Send tx.
332
+ await participantDoc.ref.set(
333
+ {
334
+ contributions: [
335
+ ...currentContributions,
336
+ {
337
+ hash: data.contributionHash,
338
+ computationTime: data.contributionComputationTime
339
+ }
340
+ ]
341
+ },
342
+ { merge: true }
343
+ )
344
+ else logAndThrowError(SPECIFIC_ERRORS.SE_PARTICIPANT_CANNOT_STORE_PERMANENT_DATA)
345
+
346
+ printLog(
347
+ `Participant ${participantDoc.id} has successfully stored the contribution hash ${data.contributionHash} and computation time ${data.contributionComputationTime}`,
348
+ LogLevel.DEBUG
349
+ )
350
+ }
351
+ )
352
+
353
+ /**
354
+ * Write temporary information about the unique identifier about the opened multi-part upload to eventually resume the contribution.
355
+ * @dev enable the current contributor to resume a multi-part upload from where it had left off.
356
+ */
357
+ export const temporaryStoreCurrentContributionMultiPartUploadId = functions
358
+ .region('europe-west1')
359
+ .runWith({
360
+ memory: "512MB"
361
+ })
362
+ .https.onCall(
363
+ async (data: TemporaryStoreCurrentContributionMultiPartUploadId, context: functions.https.CallableContext) => {
364
+ if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator))
365
+ logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER)
366
+
367
+ if (!data.ceremonyId || !data.uploadId) logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA)
368
+
369
+ // Get data.
370
+ const { ceremonyId, uploadId } = data
371
+ const userId = context.auth?.uid
372
+
373
+ // Look for the ceremony document.
374
+ const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId)
375
+ const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyDoc.id), userId!)
376
+
377
+ if (!ceremonyDoc.data() || !participantDoc.data())
378
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA)
379
+
380
+ // Extract data.
381
+ const { contributionStep, tempContributionData: currentTempContributionData } = participantDoc.data()!
382
+
383
+ // Pre-condition: check if the current contributor has uploading contribution step.
384
+ if (contributionStep !== ParticipantContributionStep.UPLOADING)
385
+ logAndThrowError(SPECIFIC_ERRORS.SE_PARTICIPANT_CANNOT_STORE_TEMPORARY_DATA)
386
+
387
+ // Send tx.
388
+ await participantDoc.ref.set(
389
+ {
390
+ tempContributionData: {
391
+ ...currentTempContributionData,
392
+ uploadId,
393
+ chunks: []
394
+ },
395
+ lastUpdated: getCurrentServerTimestampInMillis()
396
+ },
397
+ { merge: true }
398
+ )
399
+
400
+ printLog(
401
+ `Participant ${participantDoc.id} has successfully stored the temporary data for ${uploadId} multi-part upload`,
402
+ LogLevel.DEBUG
403
+ )
404
+ }
405
+ )
406
+
407
+ /**
408
+ * Write temporary information about the etags and part numbers for each uploaded chunk in order to make the upload resumable from last chunk.
409
+ * @dev enable the current contributor to resume a multi-part upload from where it had left off.
410
+ */
411
+ export const temporaryStoreCurrentContributionUploadedChunkData = functions
412
+ .region('europe-west1')
413
+ .runWith({
414
+ memory: "512MB"
415
+ })
416
+ .https.onCall(
417
+ async (data: TemporaryStoreCurrentContributionUploadedChunkData, context: functions.https.CallableContext) => {
418
+ if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator))
419
+ logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER)
420
+
421
+ if (!data.ceremonyId || !data.chunk) logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA)
422
+
423
+ // Get data.
424
+ const { ceremonyId, chunk } = data
425
+ const userId = context.auth?.uid
426
+
427
+ // Look for the ceremony document.
428
+ const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId)
429
+ const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyDoc.id), userId!)
430
+
431
+ if (!ceremonyDoc.data() || !participantDoc.data())
432
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA)
433
+
434
+ // Extract data.
435
+ const { contributionStep, tempContributionData: currentTempContributionData } = participantDoc.data()!
436
+
437
+ // Pre-condition: check if the current contributor has uploading contribution step.
438
+ if (contributionStep !== ParticipantContributionStep.UPLOADING)
439
+ logAndThrowError(SPECIFIC_ERRORS.SE_PARTICIPANT_CANNOT_STORE_TEMPORARY_DATA)
440
+
441
+ // Get already uploaded chunks.
442
+ const chunks = currentTempContributionData.chunks ? currentTempContributionData.chunks : []
443
+
444
+ // Push last chunk.
445
+ chunks.push(chunk)
446
+
447
+ // Update.
448
+ await participantDoc.ref.set(
449
+ {
450
+ tempContributionData: {
451
+ ...currentTempContributionData,
452
+ chunks
453
+ },
454
+ lastUpdated: getCurrentServerTimestampInMillis()
455
+ },
456
+ { merge: true }
457
+ )
458
+
459
+ printLog(
460
+ `Participant ${participantDoc.id} has successfully stored the temporary uploaded chunk data: ETag ${chunk.ETag} and PartNumber ${chunk.PartNumber}`,
461
+ LogLevel.DEBUG
462
+ )
463
+ }
464
+ )
465
+
466
+ /**
467
+ * Prepare the coordinator for the finalization of the ceremony.
468
+ * @dev checks that the ceremony is closed (= CLOSED) and that the coordinator has already +
469
+ * contributed to every selected ceremony circuits (= DONE).
470
+ */
471
+ export const checkAndPrepareCoordinatorForFinalization = functions
472
+ .region('europe-west1')
473
+ .runWith({
474
+ memory: "512MB"
475
+ })
476
+ .https.onCall(async (data: { ceremonyId: string }, context: functions.https.CallableContext): Promise<boolean> => {
477
+ if (!context.auth || !context.auth.token.coordinator) logAndThrowError(COMMON_ERRORS.CM_NOT_COORDINATOR_ROLE)
478
+
479
+ if (!data.ceremonyId) logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA)
480
+
481
+ // Get data.
482
+ const { ceremonyId } = data
483
+ const userId = context.auth?.uid
484
+
485
+ // Look for the ceremony document.
486
+ const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId)
487
+ const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyId), userId!)
488
+
489
+ if (!ceremonyDoc.data() || !participantDoc.data()) logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA)
490
+
491
+ // Get ceremony circuits.
492
+ const circuits = await getCeremonyCircuits(ceremonyId)
493
+
494
+ // Extract data.
495
+ const { state } = ceremonyDoc.data()!
496
+ const { contributionProgress, status } = participantDoc.data()!
497
+
498
+ // Check pre-conditions.
499
+ if (
500
+ state === CeremonyState.CLOSED &&
501
+ status === ParticipantStatus.DONE &&
502
+ contributionProgress === circuits.length
503
+ ) {
504
+ // Make coordinator ready for finalization.
505
+ await participantDoc.ref.set(
506
+ {
507
+ status: ParticipantStatus.FINALIZING,
508
+ lastUpdated: getCurrentServerTimestampInMillis()
509
+ },
510
+ { merge: true }
511
+ )
512
+
513
+ printLog(
514
+ `The coordinator ${participantDoc.id} is now ready to finalize the ceremony ${ceremonyId}.`,
515
+ LogLevel.DEBUG
516
+ )
517
+
518
+ return true
519
+ }
520
+ printLog(
521
+ `The coordinator ${participantDoc.id} is not ready to finalize the ceremony ${ceremonyId}.`,
522
+ LogLevel.DEBUG
523
+ )
524
+
525
+ return false
526
+ })