@devtion/devcli 1.0.5-alpha.0

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/README.md +118 -0
  2. package/dist/index.js +3206 -0
  3. package/dist/types/commands/auth.d.ts +25 -0
  4. package/dist/types/commands/clean.d.ts +6 -0
  5. package/dist/types/commands/contribute.d.ts +139 -0
  6. package/dist/types/commands/finalize.d.ts +51 -0
  7. package/dist/types/commands/index.d.ts +9 -0
  8. package/dist/types/commands/listCeremonies.d.ts +5 -0
  9. package/dist/types/commands/logout.d.ts +6 -0
  10. package/dist/types/commands/observe.d.ts +22 -0
  11. package/dist/types/commands/setup.d.ts +86 -0
  12. package/dist/types/commands/validate.d.ts +8 -0
  13. package/dist/types/index.d.ts +2 -0
  14. package/dist/types/lib/errors.d.ts +60 -0
  15. package/dist/types/lib/files.d.ts +64 -0
  16. package/dist/types/lib/localConfigs.d.ts +110 -0
  17. package/dist/types/lib/prompts.d.ts +104 -0
  18. package/dist/types/lib/services.d.ts +31 -0
  19. package/dist/types/lib/theme.d.ts +42 -0
  20. package/dist/types/lib/utils.d.ts +158 -0
  21. package/dist/types/types/index.d.ts +65 -0
  22. package/package.json +99 -0
  23. package/src/commands/auth.ts +194 -0
  24. package/src/commands/clean.ts +49 -0
  25. package/src/commands/contribute.ts +1090 -0
  26. package/src/commands/finalize.ts +382 -0
  27. package/src/commands/index.ts +9 -0
  28. package/src/commands/listCeremonies.ts +32 -0
  29. package/src/commands/logout.ts +67 -0
  30. package/src/commands/observe.ts +193 -0
  31. package/src/commands/setup.ts +901 -0
  32. package/src/commands/validate.ts +29 -0
  33. package/src/index.ts +66 -0
  34. package/src/lib/errors.ts +77 -0
  35. package/src/lib/files.ts +102 -0
  36. package/src/lib/localConfigs.ts +186 -0
  37. package/src/lib/prompts.ts +748 -0
  38. package/src/lib/services.ts +191 -0
  39. package/src/lib/theme.ts +45 -0
  40. package/src/lib/utils.ts +778 -0
  41. package/src/types/conf.d.ts +16 -0
  42. package/src/types/index.ts +70 -0
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env node
2
+ import open from "open"
3
+ import {
4
+ isCoordinator,
5
+ getClosedCeremonies,
6
+ getDocumentById,
7
+ getParticipantsCollectionPath,
8
+ checkAndPrepareCoordinatorForFinalization,
9
+ getCeremonyCircuits,
10
+ getVerificationKeyStorageFilePath,
11
+ getBucketName,
12
+ multiPartUpload,
13
+ getVerifierContractStorageFilePath,
14
+ finalizeCeremony,
15
+ generateValidContributionsAttestation,
16
+ commonTerms,
17
+ finalContributionIndex,
18
+ computeSHA256ToHex,
19
+ finalizeCircuit,
20
+ verificationKeyAcronym,
21
+ verifierSmartContractAcronym,
22
+ exportVerifierContract,
23
+ FirebaseDocumentInfo,
24
+ exportVkey
25
+ } from "@p0tion/actions"
26
+ import { Functions } from "firebase/functions"
27
+ import { Firestore } from "firebase/firestore"
28
+ import { dirname } from "path"
29
+ import { fileURLToPath } from "url"
30
+ import { COMMAND_ERRORS, showError } from "../lib/errors.js"
31
+ import {
32
+ customSpinner,
33
+ generateCustomUrlToTweetAboutParticipation,
34
+ handleStartOrResumeContribution,
35
+ publishGist,
36
+ sleep,
37
+ terminate
38
+ } from "../lib/utils.js"
39
+ import { bootstrapCommandExecutionAndServices, checkAuth } from "../lib/services.js"
40
+ import {
41
+ getAttestationLocalFilePath,
42
+ getFinalZkeyLocalFilePath,
43
+ getVerificationKeyLocalFilePath,
44
+ getVerifierContractLocalFilePath,
45
+ localPaths
46
+ } from "../lib/localConfigs.js"
47
+ import theme from "../lib/theme.js"
48
+ import { checkAndMakeNewDirectoryIfNonexistent, writeLocalJsonFile, writeFile } from "../lib/files.js"
49
+ import { promptForCeremonySelection, promptToTypeEntropyOrBeacon } from "../lib/prompts.js"
50
+
51
+ /**
52
+ * Export and store on the ceremony bucket the verification key for the given final contribution.
53
+ * @param cloudFunctions <Functions> - the instance of the Firebase cloud functions for the application.
54
+ * @param bucketName <string> - the name of the ceremony bucket.
55
+ * @param finalZkeyLocalFilePath <string> - the local file path of the final zKey.
56
+ * @param verificationKeyLocalFilePath <string> - the local file path of the verification key.
57
+ * @param verificationKeyStorageFilePath <string> - the storage file path of the verification key.
58
+ */
59
+ export const handleVerificationKey = async (
60
+ cloudFunctions: Functions,
61
+ bucketName: string,
62
+ finalZkeyLocalFilePath: string,
63
+ verificationKeyLocalFilePath: string,
64
+ verificationKeyStorageFilePath: string
65
+ ) => {
66
+ const spinner = customSpinner(`Exporting the verification key...`, "clock")
67
+ spinner.start()
68
+
69
+ // Export the verification key.
70
+ const vKey = await exportVkey(finalZkeyLocalFilePath)
71
+
72
+ spinner.text = "Writing verification key..."
73
+
74
+ // Write the verification key locally.
75
+ writeLocalJsonFile(verificationKeyLocalFilePath, vKey)
76
+
77
+ await sleep(3000) // workaound for file descriptor.
78
+
79
+ // Upload verification key to storage.
80
+ await multiPartUpload(
81
+ cloudFunctions,
82
+ bucketName,
83
+ verificationKeyStorageFilePath,
84
+ verificationKeyLocalFilePath,
85
+ Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB)
86
+ )
87
+
88
+ spinner.succeed(`Verification key correctly saved on storage`)
89
+ }
90
+
91
+ /**
92
+ * Derive and store on the ceremony bucket the Solidity Verifier smart contract for the given final contribution.
93
+ * @param cloudFunctions <Functions> - the instance of the Firebase cloud functions for the application.
94
+ * @param bucketName <string> - the name of the ceremony bucket.
95
+ * @param finalZkeyLocalFilePath <string> - the local file path of the final zKey.
96
+ * @param verifierContractLocalFilePath <string> - the local file path of the verifier smart contract.
97
+ * @param verifierContractStorageFilePath <string> - the storage file path of the verifier smart contract.
98
+ */
99
+ export const handleVerifierSmartContract = async (
100
+ cloudFunctions: Functions,
101
+ bucketName: string,
102
+ finalZkeyLocalFilePath: string,
103
+ verifierContractLocalFilePath: string,
104
+ verifierContractStorageFilePath: string
105
+ ) => {
106
+ const spinner = customSpinner(`Extracting verifier contract...`, `clock`)
107
+ spinner.start()
108
+
109
+ // Verifier path.
110
+ const packagePath = `${dirname(fileURLToPath(import.meta.url))}`
111
+ const verifierPath = packagePath.includes(`src/commands`)
112
+ ? `${dirname(
113
+ fileURLToPath(import.meta.url)
114
+ )}/../../../../node_modules/snarkjs/templates/verifier_groth16.sol.ejs`
115
+ : `${dirname(fileURLToPath(import.meta.url))}/../../../node_modules/snarkjs/templates/verifier_groth16.sol.ejs`
116
+
117
+ // Export the Solidity verifier smart contract.
118
+ const verifierCode = await exportVerifierContract(finalZkeyLocalFilePath, verifierPath)
119
+
120
+ spinner.text = `Writing verifier smart contract...`
121
+
122
+ // Write the verification key locally.
123
+ writeFile(verifierContractLocalFilePath, verifierCode)
124
+
125
+ await sleep(3000) // workaound for file descriptor.
126
+
127
+ // Upload verifier smart contract to storage.
128
+ await multiPartUpload(
129
+ cloudFunctions,
130
+ bucketName,
131
+ verifierContractStorageFilePath,
132
+ verifierContractLocalFilePath,
133
+ Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB)
134
+ )
135
+
136
+ spinner.succeed(`Verifier smart contract correctly saved on storage`)
137
+ }
138
+
139
+ /**
140
+ * Handle the process of finalizing a ceremony circuit.
141
+ * @dev this process results in the extraction of the final ceremony artifacts for the calculation and verification of proofs.
142
+ * @notice this method must enforce the order among these steps:
143
+ * 1) Compute the final contribution (zKey).
144
+ * 2) Extract the verification key (vKey).
145
+ * 3) Extract the Verifier smart contract (.sol).
146
+ * 4) Upload the artifacts in the AWS S3 storage.
147
+ * 5) Complete the final contribution data w/ artifacts references and hashes (cloud function).
148
+ * @param cloudFunctions <Functions> - the instance of the Firebase cloud functions for the application.
149
+ * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
150
+ * @param ceremony <FirebaseDocumentInfo> - the Firestore document of the ceremony.
151
+ * @param circuit <FirebaseDocumentInfo> - the Firestore document of the ceremony circuit.
152
+ * @param participant <FirebaseDocumentInfo> - the Firestore document of the participant (coordinator).
153
+ * @param beacon <string> - the value used to compute the final contribution while finalizing the ceremony.
154
+ * @param coordinatorIdentifier <string> - the identifier of the coordinator.
155
+ */
156
+ export const handleCircuitFinalization = async (
157
+ cloudFunctions: Functions,
158
+ firestoreDatabase: Firestore,
159
+ ceremony: FirebaseDocumentInfo,
160
+ circuit: FirebaseDocumentInfo,
161
+ participant: FirebaseDocumentInfo,
162
+ beacon: string,
163
+ coordinatorIdentifier: string
164
+ ) => {
165
+ // Step (1).
166
+ await handleStartOrResumeContribution(
167
+ cloudFunctions,
168
+ firestoreDatabase,
169
+ ceremony,
170
+ circuit,
171
+ participant,
172
+ computeSHA256ToHex(beacon),
173
+ coordinatorIdentifier,
174
+ true
175
+ )
176
+
177
+ await sleep(2000) // workaound for descriptors.
178
+
179
+ // Extract data.
180
+ const { prefix: circuitPrefix } = circuit.data
181
+ const { prefix: ceremonyPrefix } = ceremony.data
182
+
183
+ // Prepare local paths.
184
+ const finalZkeyLocalFilePath = getFinalZkeyLocalFilePath(`${circuitPrefix}_${finalContributionIndex}.zkey`)
185
+ const verificationKeyLocalFilePath = getVerificationKeyLocalFilePath(
186
+ `${circuitPrefix}_${verificationKeyAcronym}.json`
187
+ )
188
+ const verifierContractLocalFilePath = getVerifierContractLocalFilePath(
189
+ `${circuitPrefix}_${verifierSmartContractAcronym}.sol`
190
+ )
191
+
192
+ // Prepare storage paths.
193
+ const verificationKeyStorageFilePath = getVerificationKeyStorageFilePath(
194
+ circuitPrefix,
195
+ `${circuitPrefix}_${verificationKeyAcronym}.json`
196
+ )
197
+ const verifierContractStorageFilePath = getVerifierContractStorageFilePath(
198
+ circuitPrefix,
199
+ `${circuitPrefix}_${verifierSmartContractAcronym}.sol`
200
+ )
201
+
202
+ // Get ceremony bucket.
203
+ const bucketName = getBucketName(ceremonyPrefix, String(process.env.CONFIG_CEREMONY_BUCKET_POSTFIX))
204
+
205
+ // Step (2 & 4).
206
+ await handleVerificationKey(
207
+ cloudFunctions,
208
+ bucketName,
209
+ finalZkeyLocalFilePath,
210
+ verificationKeyLocalFilePath,
211
+ verificationKeyStorageFilePath
212
+ )
213
+
214
+ // Step (3 & 4).
215
+ await handleVerifierSmartContract(
216
+ cloudFunctions,
217
+ bucketName,
218
+ finalZkeyLocalFilePath,
219
+ verifierContractLocalFilePath,
220
+ verifierContractStorageFilePath
221
+ )
222
+
223
+ // Step (5).
224
+ const spinner = customSpinner(`Wrapping up the finalization of the circuit...`, `clock`)
225
+ spinner.start()
226
+
227
+ // Finalize circuit contribution.
228
+ await finalizeCircuit(cloudFunctions, ceremony.id, circuit.id, bucketName, beacon)
229
+
230
+ await sleep(2000)
231
+
232
+ spinner.succeed(`Circuit has been finalized correctly`)
233
+ }
234
+
235
+ /**
236
+ * Finalize command.
237
+ * @notice The finalize command allows a coordinator to finalize a Trusted Setup Phase 2 ceremony by providing the final beacon,
238
+ * computing the final zKeys and extracting the Verifier Smart Contract + Verification Keys per each ceremony circuit.
239
+ * anyone could use the final zKey to create a proof and everyone else could verify the correctness using the
240
+ * related verification key (off-chain) or Verifier smart contract (on-chain).
241
+ * @dev For proper execution, the command requires the coordinator to be authenticated with a GitHub account (run auth command first) in order to
242
+ * handle sybil-resistance and connect to GitHub APIs to publish the gist containing the final public attestation.
243
+ */
244
+ const finalize = async () => {
245
+ const { firebaseApp, firebaseFunctions, firestoreDatabase } = await bootstrapCommandExecutionAndServices()
246
+
247
+ // Check for authentication.
248
+ const { user, providerUserId, token: coordinatorAccessToken } = await checkAuth(firebaseApp)
249
+
250
+ // Preserve command execution only for coordinators.
251
+ if (!(await isCoordinator(user))) showError(COMMAND_ERRORS.COMMAND_NOT_COORDINATOR, true)
252
+
253
+ // Retrieve the closed ceremonies (ready for finalization).
254
+ const ceremoniesClosedForFinalization = await getClosedCeremonies(firestoreDatabase)
255
+
256
+ // Gracefully exit if no ceremonies are closed and ready for finalization.
257
+ if (!ceremoniesClosedForFinalization.length) showError(COMMAND_ERRORS.COMMAND_FINALIZED_NO_CLOSED_CEREMONIES, true)
258
+
259
+ console.log(
260
+ `${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`
261
+ )
262
+
263
+ // Prompt for ceremony selection.
264
+ const selectedCeremony = await promptForCeremonySelection(ceremoniesClosedForFinalization, true)
265
+
266
+ // Get coordinator participant document.
267
+ let participant = await getDocumentById(
268
+ firestoreDatabase,
269
+ getParticipantsCollectionPath(selectedCeremony.id),
270
+ user.uid
271
+ )
272
+
273
+ const isCoordinatorReadyForCeremonyFinalization = await checkAndPrepareCoordinatorForFinalization(
274
+ firebaseFunctions,
275
+ selectedCeremony.id
276
+ )
277
+
278
+ if (!isCoordinatorReadyForCeremonyFinalization)
279
+ showError(COMMAND_ERRORS.COMMAND_FINALIZED_NOT_READY_FOR_FINALIZATION, true)
280
+
281
+ // Prompt for beacon.
282
+ const beacon = await promptToTypeEntropyOrBeacon(false)
283
+ // Compute hash
284
+ const beaconHash = computeSHA256ToHex(beacon)
285
+ // Display.
286
+ console.log(`${theme.symbols.info} Beacon SHA256 hash ${theme.text.bold(beaconHash)}`)
287
+
288
+ // Clean directories.
289
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.output)
290
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.finalize)
291
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.finalZkeys)
292
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.finalPot)
293
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.finalAttestations)
294
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.verificationKeys)
295
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.verifierContracts)
296
+
297
+ // Get ceremony circuits.
298
+ const circuits = await getCeremonyCircuits(firestoreDatabase, selectedCeremony.id)
299
+
300
+ // Handle finalization for each ceremony circuit.
301
+ for await (const circuit of circuits)
302
+ await handleCircuitFinalization(
303
+ firebaseFunctions,
304
+ firestoreDatabase,
305
+ selectedCeremony,
306
+ circuit,
307
+ participant,
308
+ beacon,
309
+ providerUserId
310
+ )
311
+
312
+ process.stdout.write(`\n`)
313
+
314
+ const spinner = customSpinner(`Wrapping up the finalization of the ceremony...`, "clock")
315
+ spinner.start()
316
+
317
+ // Finalize the ceremony.
318
+ await finalizeCeremony(firebaseFunctions, selectedCeremony.id)
319
+
320
+ spinner.succeed(
321
+ `Great, you have completed the finalization of the ${theme.text.bold(selectedCeremony.data.title)} ceremony ${
322
+ theme.emojis.tada
323
+ }\n`
324
+ )
325
+
326
+ // Get updated coordinator participant document.
327
+ participant = await getDocumentById(firestoreDatabase, getParticipantsCollectionPath(selectedCeremony.id), user.uid)
328
+
329
+ // Extract updated data.
330
+ const { contributions } = participant.data()!
331
+ const { prefix, title: ceremonyName } = selectedCeremony.data
332
+
333
+ // Generate attestation with final contributions.
334
+ const publicAttestation = await generateValidContributionsAttestation(
335
+ firestoreDatabase,
336
+ circuits,
337
+ selectedCeremony.id,
338
+ participant.id,
339
+ contributions,
340
+ providerUserId,
341
+ ceremonyName,
342
+ true
343
+ )
344
+
345
+ // Write public attestation locally.
346
+ writeFile(
347
+ getAttestationLocalFilePath(
348
+ `${prefix}_${finalContributionIndex}_${commonTerms.foldersAndPathsTerms.attestation}.log`
349
+ ),
350
+ Buffer.from(publicAttestation)
351
+ )
352
+
353
+ await sleep(3000) // workaround for file descriptor unexpected close.
354
+
355
+ const gistUrl = await publishGist(coordinatorAccessToken, publicAttestation, ceremonyName, prefix)
356
+
357
+ console.log(
358
+ `\n${
359
+ theme.symbols.info
360
+ } Your public final attestation has been successfully posted as Github Gist (${theme.text.bold(
361
+ theme.text.underlined(gistUrl)
362
+ )})`
363
+ )
364
+
365
+ // Generate a ready to share custom url to tweet about ceremony participation.
366
+ const tweetUrl = generateCustomUrlToTweetAboutParticipation(ceremonyName, gistUrl, true)
367
+
368
+ console.log(
369
+ `${
370
+ theme.symbols.info
371
+ } We encourage you to tweet about the ceremony finalization by clicking the link below\n\n${theme.text.underlined(
372
+ tweetUrl
373
+ )}`
374
+ )
375
+
376
+ // Automatically open a webpage with the tweet.
377
+ await open(tweetUrl)
378
+
379
+ terminate(providerUserId)
380
+ }
381
+
382
+ export default finalize
@@ -0,0 +1,9 @@
1
+ export { default as setup } from "./setup.js"
2
+ export { default as auth } from "./auth.js"
3
+ export { default as contribute } from "./contribute.js"
4
+ export { default as observe } from "./observe.js"
5
+ export { default as finalize } from "./finalize.js"
6
+ export { default as clean } from "./clean.js"
7
+ export { default as logout } from "./logout.js"
8
+ export { default as validate } from "./validate.js"
9
+ export { default as listCeremonies} from "./listCeremonies.js"
@@ -0,0 +1,32 @@
1
+ import { commonTerms, getAllCollectionDocs } from "@p0tion/actions"
2
+ import { showError } from "../lib/errors.js"
3
+ import { bootstrapCommandExecutionAndServices } from "../lib/services.js"
4
+
5
+ /**
6
+ * Validate ceremony setup command.
7
+ */
8
+ const listCeremonies = async () => {
9
+ try {
10
+ // bootstrap command execution and services
11
+ const { firestoreDatabase } = await bootstrapCommandExecutionAndServices()
12
+
13
+ // get all ceremonies
14
+ const ceremonies = await getAllCollectionDocs(firestoreDatabase, commonTerms.collections.ceremonies.name)
15
+ // store all names
16
+ const names: string[] = []
17
+
18
+ // loop through all ceremonies
19
+ for (const ceremony of ceremonies) names.push(ceremony.data().prefix)
20
+
21
+ // print them to the console
22
+ console.log(names.join(", "))
23
+ process.exit(0)
24
+
25
+ } catch (err: any) {
26
+ showError(`${err.toString()}`, false)
27
+ // we want to exit with a non-zero exit code
28
+ process.exit(1)
29
+ }
30
+ }
31
+
32
+ export default listCeremonies
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { getAuth, signOut } from "firebase/auth"
4
+ import { bootstrapCommandExecutionAndServices, checkAuth } from "../lib/services.js"
5
+ import { showError } from "../lib/errors.js"
6
+ import { askForConfirmation } from "../lib/prompts.js"
7
+ import { customSpinner, sleep, terminate } from "../lib/utils.js"
8
+ import theme from "../lib/theme.js"
9
+ import { deleteLocalAccessToken } from "../lib/localConfigs.js"
10
+
11
+ /**
12
+ * Logout command.
13
+ */
14
+ const logout = async () => {
15
+ try {
16
+ // Initialize services.
17
+ const { firebaseApp } = await bootstrapCommandExecutionAndServices()
18
+
19
+ // Check for authentication.
20
+ const { providerUserId } = await checkAuth(firebaseApp)
21
+
22
+ // Inform the user about deassociation in Github and re run auth
23
+ console.log(
24
+ `${
25
+ theme.symbols.warning
26
+ } The logout could sign you out from Firebase and will delete the access token saved locally on this machine. Therefore, you have to run ${theme.text.bold(
27
+ "phase2cli auth"
28
+ )} to authenticate again.\n${
29
+ theme.symbols.info
30
+ } Remember, we cannot revoke the authorization from your Github account from this CLI! You can do this manually as reported in the official Github documentation ${
31
+ theme.emojis.pointDown
32
+ }\n\n${theme.text.bold(
33
+ theme.text.underlined(
34
+ `https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/reviewing-your-authorized-applications-oauth`
35
+ )
36
+ )}\n`
37
+ )
38
+
39
+ // Ask for confirmation.
40
+ const { confirmation } = await askForConfirmation(
41
+ "Are you sure you want to log out from this machine?",
42
+ "Yes",
43
+ "No"
44
+ )
45
+
46
+ if (confirmation) {
47
+ const spinner = customSpinner(`Logging out...`, "clock")
48
+ spinner.start()
49
+
50
+ // Sign out.
51
+ const auth = getAuth()
52
+ await signOut(auth)
53
+
54
+ // Delete local token.
55
+ deleteLocalAccessToken()
56
+
57
+ await sleep(3000) // ~3s.
58
+
59
+ spinner.stop()
60
+ console.log(`${theme.symbols.success} Logout successfully completed`)
61
+ } else terminate(providerUserId)
62
+ } catch (err: any) {
63
+ showError(`Something went wrong: ${err.toString()}`, true)
64
+ }
65
+ }
66
+
67
+ export default logout
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ FirebaseDocumentInfo,
5
+ getCeremonyCircuits,
6
+ getCircuitContributionsFromContributor,
7
+ getOpenedCeremonies,
8
+ isCoordinator,
9
+ convertToDoubleDigits
10
+ } from "@p0tion/actions"
11
+ import { Firestore } from "firebase/firestore"
12
+ import logSymbols from "log-symbols"
13
+ import readline from "readline"
14
+ import { COMMAND_ERRORS, GENERIC_ERRORS, showError } from "../lib/errors.js"
15
+ import { promptForCeremonySelection } from "../lib/prompts.js"
16
+ import { bootstrapCommandExecutionAndServices, checkAuth } from "../lib/services.js"
17
+ import theme from "../lib/theme.js"
18
+ import {customSpinner, getSecondsMinutesHoursFromMillis, sleep } from "../lib/utils.js"
19
+
20
+ /**
21
+ * Clean cursor lines from current position back to root (default: zero).
22
+ * @param currentCursorPos - the current position of the cursor.
23
+ * @returns <number>
24
+ */
25
+ export const cleanCursorPosBackToRoot = (currentCursorPos: number) => {
26
+ while (currentCursorPos < 0) {
27
+ // Get back and clean line by line.
28
+ readline.cursorTo(process.stdout, 0)
29
+ readline.clearLine(process.stdout, 0)
30
+ readline.moveCursor(process.stdout, -1, -1)
31
+
32
+ currentCursorPos += 1
33
+ }
34
+
35
+ return currentCursorPos
36
+ }
37
+
38
+ /**
39
+ * Show the latest updates for the given circuit.
40
+ * @param firestoreDatabase <Firestore> - the Firestore database to query from.
41
+ * @param ceremony <FirebaseDocumentInfo> - the Firebase document containing info about the ceremony.
42
+ * @param circuit <FirebaseDocumentInfo> - the Firebase document containing info about the circuit.
43
+ * @returns Promise<number> return the current position of the cursor (i.e., number of lines displayed).
44
+ */
45
+ export const displayLatestCircuitUpdates = async (
46
+ firestoreDatabase: Firestore,
47
+ ceremony: FirebaseDocumentInfo,
48
+ circuit: FirebaseDocumentInfo
49
+ ): Promise<number> => {
50
+ let observation = theme.text.bold(`- Circuit # ${theme.colors.magenta(circuit.data.sequencePosition)}`) // Observation output.
51
+ let cursorPos = -1 // Current cursor position (nb. decrease every time there's a new line!).
52
+
53
+ const { waitingQueue } = circuit.data
54
+
55
+ // Get info from circuit.
56
+ const { currentContributor } = waitingQueue
57
+ const { completedContributions } = waitingQueue
58
+
59
+ if (!currentContributor) {
60
+ observation += `\n> Nobody's currently waiting to contribute ${theme.emojis.eyes}`
61
+ cursorPos -= 1
62
+ } else {
63
+ // Search for currentContributor' contribution.
64
+ const contributions = await getCircuitContributionsFromContributor(
65
+ firestoreDatabase,
66
+ ceremony.id,
67
+ circuit.id,
68
+ currentContributor
69
+ )
70
+
71
+ if (!contributions.length) {
72
+ // The contributor is currently contributing.
73
+ observation += `\n> Participant ${theme.text.bold(`#${completedContributions + 1}`)} (${theme.text.bold(
74
+ currentContributor
75
+ )}) is currently contributing ${theme.emojis.fire}`
76
+
77
+ cursorPos -= 1
78
+ } else {
79
+ // The contributor has contributed.
80
+ observation += `\n> Participant ${theme.text.bold(`#${completedContributions}`)} (${theme.text.bold(
81
+ currentContributor
82
+ )}) has completed the contribution ${theme.emojis.tada}`
83
+
84
+ cursorPos -= 1
85
+
86
+ // The contributor has finished the contribution.
87
+ const contributionData = contributions.at(0)?.data
88
+
89
+ if (!contributionData) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true)
90
+
91
+ // Convert times to seconds.
92
+ const {
93
+ seconds: contributionTimeSeconds,
94
+ minutes: contributionTimeMinutes,
95
+ hours: contributionTimeHours
96
+ } = getSecondsMinutesHoursFromMillis(contributionData?.contributionComputationTime)
97
+ const {
98
+ seconds: verificationTimeSeconds,
99
+ minutes: verificationTimeMinutes,
100
+ hours: verificationTimeHours
101
+ } = getSecondsMinutesHoursFromMillis(contributionData?.verificationComputationTime)
102
+
103
+ observation += `\n> The ${theme.text.bold("computation")} took ${theme.text.bold(
104
+ `${convertToDoubleDigits(contributionTimeHours)}:${convertToDoubleDigits(
105
+ contributionTimeMinutes
106
+ )}:${convertToDoubleDigits(contributionTimeSeconds)}`
107
+ )}`
108
+ observation += `\n> The ${theme.text.bold("verification")} took ${theme.text.bold(
109
+ `${convertToDoubleDigits(verificationTimeHours)}:${convertToDoubleDigits(
110
+ verificationTimeMinutes
111
+ )}:${convertToDoubleDigits(verificationTimeSeconds)}`
112
+ )}`
113
+ observation += `\n> Contribution ${
114
+ contributionData?.valid
115
+ ? `${theme.text.bold("VALID")} ${theme.symbols.success}`
116
+ : `${theme.text.bold("INVALID")} ${theme.symbols.error}`
117
+ }`
118
+
119
+ cursorPos -= 3
120
+ }
121
+ }
122
+
123
+ // Show observation for circuit.
124
+ process.stdout.write(`${observation}\n\n`)
125
+ cursorPos -= 1
126
+
127
+ return cursorPos
128
+ }
129
+
130
+ /**
131
+ * Observe command.
132
+ */
133
+ const observe = async () => {
134
+ // @todo to be moved as command configuration parameter.
135
+ const observationWaitingTimeInMillis = 3000
136
+ try {
137
+ // Initialize services.
138
+ const { firebaseApp, firestoreDatabase } = await bootstrapCommandExecutionAndServices()
139
+
140
+ // Handle current authenticated user sign in.
141
+ const { user } = await checkAuth(firebaseApp)
142
+
143
+ // Preserve command execution only for coordinators].
144
+ if (!(await isCoordinator(user))) showError(COMMAND_ERRORS.COMMAND_NOT_COORDINATOR, true)
145
+
146
+ // Get running cerimonies info (if any).
147
+ const runningCeremoniesDocs = await getOpenedCeremonies(firestoreDatabase)
148
+
149
+ // Ask to select a ceremony.
150
+ const ceremony = await promptForCeremonySelection(runningCeremoniesDocs, false)
151
+
152
+ console.log(`${logSymbols.info} Refresh rate set to ~3 seconds for waiting queue updates\n`)
153
+
154
+ let cursorPos = 0 // Keep track of current cursor position.
155
+
156
+ const spinner = customSpinner(`Getting ready...`, "clock")
157
+ spinner.start()
158
+
159
+ // Get circuit updates every 3 seconds.
160
+ setInterval(async () => {
161
+ // Clean cursor position back to root.
162
+ cursorPos = cleanCursorPosBackToRoot(cursorPos)
163
+
164
+ spinner.stop()
165
+
166
+ spinner.text = `Updating...`
167
+ spinner.start()
168
+
169
+ // Get updates from circuits.
170
+ const circuits = await getCeremonyCircuits(firestoreDatabase, ceremony.id)
171
+
172
+ await sleep(observationWaitingTimeInMillis / 10) // Just for a smoother UX/UI experience.
173
+
174
+ spinner.stop()
175
+
176
+ // Observe changes for each circuit
177
+ for await (const circuit of circuits)
178
+ cursorPos += await displayLatestCircuitUpdates(firestoreDatabase, ceremony, circuit)
179
+
180
+ process.stdout.write(`Press CTRL+C to exit`)
181
+
182
+ await sleep(1000) // Just for a smoother UX/UI experience.
183
+ }, observationWaitingTimeInMillis)
184
+
185
+ await sleep(observationWaitingTimeInMillis) // Wait until the first update.
186
+
187
+ spinner.stop()
188
+ } catch (err: any) {
189
+ showError(`Something went wrong: ${err.toString()}`, true)
190
+ }
191
+ }
192
+
193
+ export default observe