@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.
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/dist/.env +41 -0
- package/dist/index.js +3206 -0
- package/dist/types/commands/auth.d.ts +25 -0
- package/dist/types/commands/clean.d.ts +6 -0
- package/dist/types/commands/contribute.d.ts +139 -0
- package/dist/types/commands/finalize.d.ts +51 -0
- package/dist/types/commands/index.d.ts +9 -0
- package/dist/types/commands/listCeremonies.d.ts +5 -0
- package/dist/types/commands/logout.d.ts +6 -0
- package/dist/types/commands/observe.d.ts +22 -0
- package/dist/types/commands/setup.d.ts +86 -0
- package/dist/types/commands/validate.d.ts +8 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/lib/errors.d.ts +60 -0
- package/dist/types/lib/files.d.ts +64 -0
- package/dist/types/lib/localConfigs.d.ts +110 -0
- package/dist/types/lib/prompts.d.ts +104 -0
- package/dist/types/lib/services.d.ts +31 -0
- package/dist/types/lib/theme.d.ts +42 -0
- package/dist/types/lib/utils.d.ts +158 -0
- package/dist/types/types/index.d.ts +65 -0
- package/package.json +100 -0
- package/src/commands/auth.ts +194 -0
- package/src/commands/clean.ts +49 -0
- package/src/commands/contribute.ts +1090 -0
- package/src/commands/finalize.ts +382 -0
- package/src/commands/index.ts +9 -0
- package/src/commands/listCeremonies.ts +32 -0
- package/src/commands/logout.ts +67 -0
- package/src/commands/observe.ts +193 -0
- package/src/commands/setup.ts +901 -0
- package/src/commands/validate.ts +29 -0
- package/src/index.ts +66 -0
- package/src/lib/errors.ts +77 -0
- package/src/lib/files.ts +102 -0
- package/src/lib/localConfigs.ts +186 -0
- package/src/lib/prompts.ts +748 -0
- package/src/lib/services.ts +191 -0
- package/src/lib/theme.ts +45 -0
- package/src/lib/utils.ts +778 -0
- package/src/types/conf.d.ts +16 -0
- package/src/types/index.ts +70 -0
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
import fetch from "@adobe/node-fetch-retry"
|
|
2
|
+
import { request } from "@octokit/request"
|
|
3
|
+
import {
|
|
4
|
+
commonTerms,
|
|
5
|
+
convertBytesOrKbToGb,
|
|
6
|
+
createCustomLoggerForFile,
|
|
7
|
+
convertToDoubleDigits,
|
|
8
|
+
finalContributionIndex,
|
|
9
|
+
FirebaseDocumentInfo,
|
|
10
|
+
formatZkeyIndex,
|
|
11
|
+
generateGetObjectPreSignedUrl,
|
|
12
|
+
getBucketName,
|
|
13
|
+
getDocumentById,
|
|
14
|
+
getParticipantsCollectionPath,
|
|
15
|
+
getZkeyStorageFilePath,
|
|
16
|
+
multiPartUpload,
|
|
17
|
+
numExpIterations,
|
|
18
|
+
ParticipantContributionStep,
|
|
19
|
+
permanentlyStoreCurrentContributionTimeAndHash,
|
|
20
|
+
progressToNextContributionStep,
|
|
21
|
+
verifyContribution
|
|
22
|
+
} from "@p0tion/actions"
|
|
23
|
+
import { Presets, SingleBar } from "cli-progress"
|
|
24
|
+
import dotenv from "dotenv"
|
|
25
|
+
import { GithubAuthProvider, OAuthCredential } from "firebase/auth"
|
|
26
|
+
import { DocumentData, Firestore } from "firebase/firestore"
|
|
27
|
+
import { Functions } from "firebase/functions"
|
|
28
|
+
import { createWriteStream } from "fs"
|
|
29
|
+
import { getDiskInfoSync } from "node-disk-info"
|
|
30
|
+
import ora, { Ora } from "ora"
|
|
31
|
+
import { zKey } from "snarkjs"
|
|
32
|
+
import { Timer } from "timer-node"
|
|
33
|
+
import { Logger } from "winston"
|
|
34
|
+
import { fileURLToPath } from "url"
|
|
35
|
+
import { dirname } from "path"
|
|
36
|
+
import { GithubGistFile, ProgressBarType, Timing } from "../types/index.js"
|
|
37
|
+
import { COMMAND_ERRORS, CORE_SERVICES_ERRORS, showError, THIRD_PARTY_SERVICES_ERRORS } from "./errors.js"
|
|
38
|
+
import { readFile } from "./files.js"
|
|
39
|
+
import {
|
|
40
|
+
getContributionLocalFilePath,
|
|
41
|
+
getFinalTranscriptLocalFilePath,
|
|
42
|
+
getFinalZkeyLocalFilePath,
|
|
43
|
+
getTranscriptLocalFilePath
|
|
44
|
+
} from "./localConfigs.js"
|
|
45
|
+
import theme from "./theme.js"
|
|
46
|
+
|
|
47
|
+
const packagePath = `${dirname(fileURLToPath(import.meta.url))}`
|
|
48
|
+
dotenv.config({
|
|
49
|
+
path: packagePath.includes(`src/lib`)
|
|
50
|
+
? `${dirname(fileURLToPath(import.meta.url))}/../../.env`
|
|
51
|
+
: `${dirname(fileURLToPath(import.meta.url))}/.env`
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Exchange the Github token for OAuth credential.
|
|
56
|
+
* @param githubToken <string> - the Github token generated through the Device Flow process.
|
|
57
|
+
* @returns <OAuthCredential>
|
|
58
|
+
*/
|
|
59
|
+
export const exchangeGithubTokenForCredentials = (githubToken: string): OAuthCredential =>
|
|
60
|
+
GithubAuthProvider.credential(githubToken)
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get the information associated to the account from which the token has been generated to
|
|
64
|
+
* create a custom unique identifier for the user.
|
|
65
|
+
* @notice the unique identifier has the following form 'handle-identifier'.
|
|
66
|
+
* @param githubToken <string> - the Github token.
|
|
67
|
+
* @returns <Promise<any>> - the Github (provider) unique identifier associated to the user.
|
|
68
|
+
*/
|
|
69
|
+
export const getGithubProviderUserId = async (githubToken: string): Promise<any> => {
|
|
70
|
+
// Ask for user account public information through Github API.
|
|
71
|
+
const response = await request("GET https://api.github.com/user", {
|
|
72
|
+
headers: {
|
|
73
|
+
authorization: `token ${githubToken}`
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
if (response && response.status === 200) return `${response.data.login}-${response.data.id}`
|
|
78
|
+
|
|
79
|
+
showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_GET_GITHUB_ACCOUNT_INFO, true)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get the gists associated to the authenticated user account.
|
|
84
|
+
* @param githubToken <string> - the Github token.
|
|
85
|
+
* @param params <Object<number,number>> - the necessary parameters for the request.
|
|
86
|
+
* @returns <Promise<any>> - the Github gists associated with the authenticated user account.
|
|
87
|
+
*/
|
|
88
|
+
export const getGithubAuthenticatedUserGists = async (
|
|
89
|
+
githubToken: string,
|
|
90
|
+
params: { perPage: number; page: number }
|
|
91
|
+
): Promise<any> => {
|
|
92
|
+
// Ask for user account public information through Github API.
|
|
93
|
+
const response = await request("GET https://api.github.com/gists{?per_page,page}", {
|
|
94
|
+
headers: {
|
|
95
|
+
authorization: `token ${githubToken}`
|
|
96
|
+
},
|
|
97
|
+
per_page: params.perPage, // max items per page = 100.
|
|
98
|
+
page: params.page
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
if (response && response.status === 200) return response.data
|
|
102
|
+
|
|
103
|
+
showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_GET_GITHUB_ACCOUNT_INFO, true)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check whether or not the user has published the gist.
|
|
108
|
+
* @dev gather all the user's gists and check if there is a match with the expected public attestation.
|
|
109
|
+
* @param githubToken <string> - the Github token.
|
|
110
|
+
* @param publicAttestationFilename <string> - the public attestation filename.
|
|
111
|
+
* @returns <Promise<GithubGistFile | undefined>> - return the public attestation gist if and only if has been published.
|
|
112
|
+
*/
|
|
113
|
+
export const getPublicAttestationGist = async (
|
|
114
|
+
githubToken: string,
|
|
115
|
+
publicAttestationFilename: string
|
|
116
|
+
): Promise<GithubGistFile | undefined> => {
|
|
117
|
+
const itemsPerPage = 50 // number of gists to fetch x page.
|
|
118
|
+
let gists: Array<any> = [] // The list of user gists.
|
|
119
|
+
let publishedGist: GithubGistFile | undefined // the published public attestation gist.
|
|
120
|
+
let page = 1 // Page of gists = starts from 1.
|
|
121
|
+
|
|
122
|
+
// Get first batch (page) of gists
|
|
123
|
+
let pageGists = await getGithubAuthenticatedUserGists(githubToken, { perPage: itemsPerPage, page })
|
|
124
|
+
|
|
125
|
+
// State update.
|
|
126
|
+
gists = gists.concat(pageGists)
|
|
127
|
+
|
|
128
|
+
// Keep going until hitting a blank page.
|
|
129
|
+
while (pageGists.length > 0) {
|
|
130
|
+
// Fetch next page.
|
|
131
|
+
page += 1
|
|
132
|
+
pageGists = await getGithubAuthenticatedUserGists(githubToken, { perPage: itemsPerPage, page })
|
|
133
|
+
|
|
134
|
+
// State update.
|
|
135
|
+
gists = gists.concat(pageGists)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Look for public attestation.
|
|
139
|
+
for (const gist of gists) {
|
|
140
|
+
const numberOfFiles = Object.keys(gist.files).length
|
|
141
|
+
const publicAttestationCandidateFile = Object.values(gist.files)[0] as GithubGistFile
|
|
142
|
+
|
|
143
|
+
/// @todo improve check by using expected public attestation content (e.g., hash).
|
|
144
|
+
if (numberOfFiles === 1 && publicAttestationCandidateFile.filename === publicAttestationFilename)
|
|
145
|
+
publishedGist = publicAttestationCandidateFile
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return publishedGist
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Return the Github handle from the provider user id.
|
|
153
|
+
* @notice the provider user identifier must have the following structure 'handle-id'.
|
|
154
|
+
* @param providerUserId <string> - the unique provider user identifier.
|
|
155
|
+
* @returns <string> - the third-party provider handle of the user.
|
|
156
|
+
*/
|
|
157
|
+
export const getUserHandleFromProviderUserId = (providerUserId: string): string => {
|
|
158
|
+
if (providerUserId.indexOf("-") === -1) showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_GET_GITHUB_ACCOUNT_INFO, true)
|
|
159
|
+
|
|
160
|
+
return providerUserId.split("-")[0]
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Return a custom spinner.
|
|
165
|
+
* @param text <string> - the text that should be displayed as spinner status.
|
|
166
|
+
* @param spinnerLogo <any> - the logo.
|
|
167
|
+
* @returns <Ora> - a new Ora custom spinner.
|
|
168
|
+
*/
|
|
169
|
+
export const customSpinner = (text: string, spinnerLogo: any): Ora =>
|
|
170
|
+
ora({
|
|
171
|
+
text,
|
|
172
|
+
spinner: spinnerLogo
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Custom sleeper.
|
|
177
|
+
* @dev to be used in combination with loggers and for workarounds where listeners cannot help.
|
|
178
|
+
* @param ms <number> - sleep amount in milliseconds
|
|
179
|
+
* @returns <Promise<any>>
|
|
180
|
+
*/
|
|
181
|
+
export const sleep = (ms: number): Promise<any> => new Promise((resolve) => setTimeout(resolve, ms))
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Simple loader for task simulation.
|
|
185
|
+
* @param loadingText <string> - spinner text while loading.
|
|
186
|
+
* @param spinnerLogo <any> - spinner logo.
|
|
187
|
+
* @param durationInMs <number> - spinner loading duration in ms.
|
|
188
|
+
* @returns <Promise<void>>.
|
|
189
|
+
*/
|
|
190
|
+
export const simpleLoader = async (loadingText: string, spinnerLogo: any, durationInMs: number): Promise<void> => {
|
|
191
|
+
// Custom spinner (used as loader).
|
|
192
|
+
const loader = customSpinner(loadingText, spinnerLogo)
|
|
193
|
+
|
|
194
|
+
loader.start()
|
|
195
|
+
|
|
196
|
+
// nb. simulate execution for requested duration.
|
|
197
|
+
await sleep(durationInMs)
|
|
198
|
+
|
|
199
|
+
loader.stop()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Check and return the free aggregated disk space (in KB) for participant machine.
|
|
204
|
+
* @dev this method use the node-disk-info method to retrieve the information about
|
|
205
|
+
* disk availability for all visible disks.
|
|
206
|
+
* nb. no other type of data or operation is performed by this methods.
|
|
207
|
+
* @returns <number> - the free aggregated disk space in kB for the participant machine.
|
|
208
|
+
*/
|
|
209
|
+
export const estimateParticipantFreeGlobalDiskSpace = (): number => {
|
|
210
|
+
// Get info about disks.
|
|
211
|
+
const disks = getDiskInfoSync()
|
|
212
|
+
|
|
213
|
+
// Get an estimation of available memory.
|
|
214
|
+
let availableDiskSpace = 0
|
|
215
|
+
|
|
216
|
+
for (const disk of disks) availableDiskSpace += disk.available
|
|
217
|
+
|
|
218
|
+
// Return the disk space available in KB.
|
|
219
|
+
return availableDiskSpace
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get seconds, minutes, hours and days from milliseconds.
|
|
224
|
+
* @param millis <number> - the amount of milliseconds.
|
|
225
|
+
* @returns <Timing> - a custom object containing the amount of seconds, minutes, hours and days in the provided millis.
|
|
226
|
+
*/
|
|
227
|
+
export const getSecondsMinutesHoursFromMillis = (millis: number): Timing => {
|
|
228
|
+
let delta = millis / 1000
|
|
229
|
+
|
|
230
|
+
const days = Math.floor(delta / 86400)
|
|
231
|
+
delta -= days * 86400
|
|
232
|
+
|
|
233
|
+
const hours = Math.floor(delta / 3600) % 24
|
|
234
|
+
delta -= hours * 3600
|
|
235
|
+
|
|
236
|
+
const minutes = Math.floor(delta / 60) % 60
|
|
237
|
+
delta -= minutes * 60
|
|
238
|
+
|
|
239
|
+
const seconds = Math.floor(delta) % 60
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
seconds: seconds >= 60 ? 59 : seconds,
|
|
243
|
+
minutes: minutes >= 60 ? 59 : minutes,
|
|
244
|
+
hours: hours >= 24 ? 23 : hours,
|
|
245
|
+
days
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Convert milliseconds to seconds.
|
|
251
|
+
* @param millis <number>
|
|
252
|
+
* @returns <number>
|
|
253
|
+
*/
|
|
254
|
+
export const convertMillisToSeconds = (millis: number): number => Number((millis / 1000).toFixed(2))
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Gracefully terminate the command execution
|
|
258
|
+
* @params ghUsername <string> - the Github username of the user.
|
|
259
|
+
*/
|
|
260
|
+
export const terminate = async (ghUsername: string) => {
|
|
261
|
+
console.log(`\nSee you, ${theme.text.bold(`@${getUserHandleFromProviderUserId(ghUsername)}`)} ${theme.emojis.wave}`)
|
|
262
|
+
|
|
263
|
+
process.exit(0)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Publish public attestation using Github Gist.
|
|
268
|
+
* @dev the contributor must have agreed to provide 'gist' access during the execution of the 'auth' command.
|
|
269
|
+
* @param accessToken <string> - the contributor access token.
|
|
270
|
+
* @param publicAttestation <string> - the public attestation.
|
|
271
|
+
* @param ceremonyTitle <string> - the ceremony title.
|
|
272
|
+
* @param ceremonyPrefix <string> - the ceremony prefix.
|
|
273
|
+
* @returns <Promise<string>> - the url where the gist has been published.
|
|
274
|
+
*/
|
|
275
|
+
export const publishGist = async (
|
|
276
|
+
token: string,
|
|
277
|
+
content: string,
|
|
278
|
+
ceremonyTitle: string,
|
|
279
|
+
ceremonyPrefix: string
|
|
280
|
+
): Promise<string> => {
|
|
281
|
+
// Make request.
|
|
282
|
+
const response = await request("POST /gists", {
|
|
283
|
+
description: `Attestation for ${ceremonyTitle} MPC Phase 2 Trusted Setup ceremony`,
|
|
284
|
+
public: true,
|
|
285
|
+
files: {
|
|
286
|
+
[`${ceremonyPrefix}_${commonTerms.foldersAndPathsTerms.attestation}.log`]: {
|
|
287
|
+
content
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
headers: {
|
|
291
|
+
authorization: `token ${token}`
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
if (response.status !== 201 || !response.data.html_url)
|
|
296
|
+
showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_GIST_PUBLICATION_FAILED, true)
|
|
297
|
+
|
|
298
|
+
return response.data.html_url!
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Generate a custom url that when clicked allows you to compose a tweet ready to be shared.
|
|
303
|
+
* @param ceremonyName <string> - the name of the ceremony.
|
|
304
|
+
* @param gistUrl <string> - the url of the gist where the public attestation has been shared.
|
|
305
|
+
* @param isFinalizing <boolean> - flag to discriminate between ceremony finalization (true) and contribution (false).
|
|
306
|
+
* @returns <string> - the ready to share tweet url.
|
|
307
|
+
*/
|
|
308
|
+
export const generateCustomUrlToTweetAboutParticipation = (
|
|
309
|
+
ceremonyName: string,
|
|
310
|
+
gistUrl: string,
|
|
311
|
+
isFinalizing: boolean
|
|
312
|
+
) =>
|
|
313
|
+
isFinalizing
|
|
314
|
+
? `https://twitter.com/intent/tweet?text=I%20have%20finalized%20the%20${ceremonyName}%20Phase%202%20Trusted%20Setup%20ceremony!%20You%20can%20view%20my%20final%20attestation%20here:%20${gistUrl}%20#Ethereum%20#ZKP%20#PSE`
|
|
315
|
+
: `https://twitter.com/intent/tweet?text=I%20contributed%20to%20the%20${ceremonyName}%20Phase%202%20Trusted%20Setup%20ceremony!%20You%20can%20contribute%20here:%20https://github.com/privacy-scaling-explorations/p0tion%20You%20can%20view%20my%20attestation%20here:%20${gistUrl}%20#Ethereum%20#ZKP`
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Return a custom progress bar.
|
|
319
|
+
* @param type <ProgressBarType> - the type of the progress bar.
|
|
320
|
+
* @param [message] <string> - additional information to be displayed when downloading/uploading.
|
|
321
|
+
* @returns <SingleBar> - a new custom (single) progress bar.
|
|
322
|
+
*/
|
|
323
|
+
const customProgressBar = (type: ProgressBarType, message?: string): SingleBar => {
|
|
324
|
+
// Formats.
|
|
325
|
+
const uploadFormat = `${theme.emojis.arrowUp} Uploading ${message} [${theme.colors.magenta(
|
|
326
|
+
"{bar}"
|
|
327
|
+
)}] {percentage}% | {value}/{total} Chunks`
|
|
328
|
+
|
|
329
|
+
const downloadFormat = `${theme.emojis.arrowDown} Downloading ${message} [${theme.colors.magenta(
|
|
330
|
+
"{bar}"
|
|
331
|
+
)}] {percentage}% | {value}/{total} GB`
|
|
332
|
+
|
|
333
|
+
// Define a progress bar showing percentage of completion and chunks downloaded/uploaded.
|
|
334
|
+
return new SingleBar(
|
|
335
|
+
{
|
|
336
|
+
format: type === ProgressBarType.DOWNLOAD ? downloadFormat : uploadFormat,
|
|
337
|
+
hideCursor: true,
|
|
338
|
+
clearOnComplete: true
|
|
339
|
+
},
|
|
340
|
+
Presets.legacy
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Download an artifact from the ceremony bucket.
|
|
346
|
+
* @dev this method request a pre-signed url to make a GET request to download the artifact.
|
|
347
|
+
* @param cloudFunctions <Functions> - the instance of the Firebase cloud functions for the application.
|
|
348
|
+
* @param bucketName <string> - the name of the ceremony artifacts bucket (AWS S3).
|
|
349
|
+
* @param storagePath <string> - the storage path that locates the artifact to be downloaded in the bucket.
|
|
350
|
+
* @param localPath <string> - the local path where the artifact will be downloaded.
|
|
351
|
+
*/
|
|
352
|
+
export const downloadCeremonyArtifact = async (
|
|
353
|
+
cloudFunctions: Functions,
|
|
354
|
+
bucketName: string,
|
|
355
|
+
storagePath: string,
|
|
356
|
+
localPath: string
|
|
357
|
+
): Promise<void> => {
|
|
358
|
+
const spinner = customSpinner(`Preparing for downloading the contribution...`, `clock`)
|
|
359
|
+
spinner.start()
|
|
360
|
+
|
|
361
|
+
// Request pre-signed url to make GET download request.
|
|
362
|
+
const getPreSignedUrl = await generateGetObjectPreSignedUrl(cloudFunctions, bucketName, storagePath)
|
|
363
|
+
|
|
364
|
+
// Make fetch to get info about the artifact.
|
|
365
|
+
// @ts-ignore
|
|
366
|
+
const response = await fetch(getPreSignedUrl)
|
|
367
|
+
|
|
368
|
+
if (response.status !== 200 && !response.ok)
|
|
369
|
+
showError(CORE_SERVICES_ERRORS.AWS_CEREMONY_BUCKET_CANNOT_DOWNLOAD_GET_PRESIGNED_URL, true)
|
|
370
|
+
|
|
371
|
+
// Extract and prepare data.
|
|
372
|
+
const content: any = response.body
|
|
373
|
+
const contentLength = Number(response.headers.get("content-length"))
|
|
374
|
+
const contentLengthInGB = convertBytesOrKbToGb(contentLength, true)
|
|
375
|
+
|
|
376
|
+
// Prepare stream.
|
|
377
|
+
const writeStream = createWriteStream(localPath)
|
|
378
|
+
spinner.stop()
|
|
379
|
+
|
|
380
|
+
// Prepare custom progress bar.
|
|
381
|
+
const progressBar = customProgressBar(ProgressBarType.DOWNLOAD, `last contribution`)
|
|
382
|
+
const progressBarStep = contentLengthInGB / 100
|
|
383
|
+
let chunkLengthWritingProgress = 0
|
|
384
|
+
let completedProgress = progressBarStep
|
|
385
|
+
|
|
386
|
+
// Bootstrap the progress bar.
|
|
387
|
+
progressBar.start(contentLengthInGB < 0.01 ? 0.01 : parseFloat(contentLengthInGB.toFixed(2)).valueOf(), 0)
|
|
388
|
+
|
|
389
|
+
// Write chunk by chunk.
|
|
390
|
+
for await (const chunk of content) {
|
|
391
|
+
// Write chunk.
|
|
392
|
+
writeStream.write(chunk)
|
|
393
|
+
// Update current progress.
|
|
394
|
+
chunkLengthWritingProgress += convertBytesOrKbToGb(chunk.length, true)
|
|
395
|
+
|
|
396
|
+
// Display the current progress.
|
|
397
|
+
while (chunkLengthWritingProgress >= completedProgress) {
|
|
398
|
+
// Store new completed progress step by step.
|
|
399
|
+
completedProgress += progressBarStep
|
|
400
|
+
|
|
401
|
+
// Display accordingly in the progress bar.
|
|
402
|
+
progressBar.update(contentLengthInGB < 0.01 ? 0.01 : parseFloat(completedProgress.toFixed(2)).valueOf())
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
await sleep(2000) // workaround to show bar for small artifacts.
|
|
407
|
+
|
|
408
|
+
progressBar.stop()
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
*
|
|
413
|
+
* @param lastZkeyLocalFilePath <string> - the local path of the last contribution.
|
|
414
|
+
* @param nextZkeyLocalFilePath <string> - the local path where the next contribution is going to be stored.
|
|
415
|
+
* @param entropyOrBeacon <string> - the entropy or beacon (only when finalizing) for the contribution.
|
|
416
|
+
* @param contributorOrCoordinatorIdentifier <string> - the identifier of the contributor or coordinator (only when finalizing).
|
|
417
|
+
* @param averageComputingTime <number> - the current average contribution computation time.
|
|
418
|
+
* @param transcriptLogger <Logger> - the custom file logger to generate the contribution transcript.
|
|
419
|
+
* @param isFinalizing <boolean> - flag to discriminate between ceremony finalization (true) and contribution (false).
|
|
420
|
+
* @returns <Promise<number>> - the amount of time spent contributing.
|
|
421
|
+
*/
|
|
422
|
+
export const handleContributionComputation = async (
|
|
423
|
+
lastZkeyLocalFilePath: string,
|
|
424
|
+
nextZkeyLocalFilePath: string,
|
|
425
|
+
entropyOrBeacon: string,
|
|
426
|
+
contributorOrCoordinatorIdentifier: string,
|
|
427
|
+
averageComputingTime: number,
|
|
428
|
+
transcriptLogger: Logger,
|
|
429
|
+
isFinalizing: boolean
|
|
430
|
+
): Promise<number> => {
|
|
431
|
+
// Prepare timer (statistics only).
|
|
432
|
+
const computingTimer = new Timer({ label: ParticipantContributionStep.COMPUTING })
|
|
433
|
+
computingTimer.start()
|
|
434
|
+
|
|
435
|
+
// Time format.
|
|
436
|
+
const { seconds, minutes, hours, days } = getSecondsMinutesHoursFromMillis(averageComputingTime)
|
|
437
|
+
|
|
438
|
+
const spinner = customSpinner(
|
|
439
|
+
`${isFinalizing ? `Applying beacon...` : `Computing contribution...`} ${
|
|
440
|
+
averageComputingTime > 0
|
|
441
|
+
? `${theme.text.bold(
|
|
442
|
+
`(ETA ${theme.text.bold(
|
|
443
|
+
`${convertToDoubleDigits(days)}:${convertToDoubleDigits(hours)}:${convertToDoubleDigits(
|
|
444
|
+
minutes
|
|
445
|
+
)}:${convertToDoubleDigits(seconds)}`
|
|
446
|
+
)}).\n${
|
|
447
|
+
theme.symbols.warning
|
|
448
|
+
} This may take longer or less time on your machine! Everything's fine, just be patient and do not stop the computation to avoid starting over again`
|
|
449
|
+
)}`
|
|
450
|
+
: ``
|
|
451
|
+
}`,
|
|
452
|
+
`clock`
|
|
453
|
+
)
|
|
454
|
+
spinner.start()
|
|
455
|
+
|
|
456
|
+
// Discriminate between contribution finalization or computation.
|
|
457
|
+
if (isFinalizing)
|
|
458
|
+
await zKey.beacon(
|
|
459
|
+
lastZkeyLocalFilePath,
|
|
460
|
+
nextZkeyLocalFilePath,
|
|
461
|
+
contributorOrCoordinatorIdentifier,
|
|
462
|
+
entropyOrBeacon,
|
|
463
|
+
numExpIterations,
|
|
464
|
+
transcriptLogger
|
|
465
|
+
)
|
|
466
|
+
else
|
|
467
|
+
await zKey.contribute(
|
|
468
|
+
lastZkeyLocalFilePath,
|
|
469
|
+
nextZkeyLocalFilePath,
|
|
470
|
+
contributorOrCoordinatorIdentifier,
|
|
471
|
+
entropyOrBeacon,
|
|
472
|
+
transcriptLogger
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
computingTimer.stop()
|
|
476
|
+
|
|
477
|
+
await sleep(3000) // workaround for file descriptor.
|
|
478
|
+
|
|
479
|
+
spinner.stop()
|
|
480
|
+
|
|
481
|
+
return computingTimer.ms()
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Return the most up-to-date data about the participant document for the given ceremony.
|
|
486
|
+
* @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
|
|
487
|
+
* @param ceremonyId <string> - the unique identifier of the ceremony.
|
|
488
|
+
* @param participantId <string> - the unique identifier of the participant.
|
|
489
|
+
* @returns <Promise<DocumentData>> - the most up-to-date participant data.
|
|
490
|
+
*/
|
|
491
|
+
export const getLatestUpdatesFromParticipant = async (
|
|
492
|
+
firestoreDatabase: Firestore,
|
|
493
|
+
ceremonyId: string,
|
|
494
|
+
participantId: string
|
|
495
|
+
): Promise<DocumentData> => {
|
|
496
|
+
// Fetch participant data.
|
|
497
|
+
const participant = await getDocumentById(
|
|
498
|
+
firestoreDatabase,
|
|
499
|
+
getParticipantsCollectionPath(ceremonyId),
|
|
500
|
+
participantId
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
if (!participant.data()) showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_NO_PARTICIPANT_DATA, true)
|
|
504
|
+
|
|
505
|
+
return participant.data()!
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Start or resume a contribution from the last participant contribution step.
|
|
510
|
+
* @notice this method goes through each contribution stage following this order:
|
|
511
|
+
* 1) Downloads the last contribution from previous contributor.
|
|
512
|
+
* 2) Computes the new contribution.
|
|
513
|
+
* 3) Uploads the new contribution.
|
|
514
|
+
* 4) Requests the verification of the new contribution to the coordinator's backend and waits for the result.
|
|
515
|
+
* @param cloudFunctions <Functions> - the instance of the Firebase cloud functions for the application.
|
|
516
|
+
* @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
|
|
517
|
+
* @param ceremony <FirebaseDocumentInfo> - the Firestore document of the ceremony.
|
|
518
|
+
* @param circuit <FirebaseDocumentInfo> - the Firestore document of the ceremony circuit.
|
|
519
|
+
* @param participant <FirebaseDocumentInfo> - the Firestore document of the participant (contributor or coordinator).
|
|
520
|
+
* @param participantContributionStep <ParticipantContributionStep> - the contribution step of the participant (from where to start/resume contribution).
|
|
521
|
+
* @param entropyOrBeaconHash <string> - the entropy or beacon hash (only when finalizing) for the contribution.
|
|
522
|
+
* @param contributorOrCoordinatorIdentifier <string> - the identifier of the contributor or coordinator (only when finalizing).
|
|
523
|
+
* @param isFinalizing <boolean> - flag to discriminate between ceremony finalization (true) and contribution (false).
|
|
524
|
+
*/
|
|
525
|
+
export const handleStartOrResumeContribution = async (
|
|
526
|
+
cloudFunctions: Functions,
|
|
527
|
+
firestoreDatabase: Firestore,
|
|
528
|
+
ceremony: FirebaseDocumentInfo,
|
|
529
|
+
circuit: FirebaseDocumentInfo,
|
|
530
|
+
participant: FirebaseDocumentInfo,
|
|
531
|
+
entropyOrBeaconHash: any,
|
|
532
|
+
contributorOrCoordinatorIdentifier: string,
|
|
533
|
+
isFinalizing: boolean
|
|
534
|
+
): Promise<void> => {
|
|
535
|
+
// Extract data.
|
|
536
|
+
const { prefix: ceremonyPrefix } = ceremony.data
|
|
537
|
+
const { waitingQueue, avgTimings, prefix: circuitPrefix, sequencePosition } = circuit.data
|
|
538
|
+
const { completedContributions } = waitingQueue // = current progress.
|
|
539
|
+
|
|
540
|
+
console.log(
|
|
541
|
+
`${theme.text.bold(`\n- Circuit # ${theme.colors.magenta(`${sequencePosition}`)}`)} (Contribution Steps)`
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
// Get most up-to-date data from the participant document.
|
|
545
|
+
let participantData = await getLatestUpdatesFromParticipant(firestoreDatabase, ceremony.id, participant.id)
|
|
546
|
+
|
|
547
|
+
const spinner = customSpinner(
|
|
548
|
+
`${
|
|
549
|
+
participantData.contributionStep === ParticipantContributionStep.DOWNLOADING
|
|
550
|
+
? `Preparing to begin the contribution. Please note that the contribution can take a long time depending on the size of the circuits and your internet connection.`
|
|
551
|
+
: `Preparing to resume contribution. Please note that the contribution can take a long time depending on the size of the circuits and your internet connection.`
|
|
552
|
+
}`,
|
|
553
|
+
`clock`
|
|
554
|
+
)
|
|
555
|
+
spinner.start()
|
|
556
|
+
|
|
557
|
+
// Compute zkey indexes.
|
|
558
|
+
const lastZkeyIndex = formatZkeyIndex(completedContributions)
|
|
559
|
+
const nextZkeyIndex = formatZkeyIndex(completedContributions + 1)
|
|
560
|
+
|
|
561
|
+
// Prepare zKey filenames.
|
|
562
|
+
const lastZkeyCompleteFilename = `${circuitPrefix}_${lastZkeyIndex}.zkey`
|
|
563
|
+
const nextZkeyCompleteFilename = isFinalizing
|
|
564
|
+
? `${circuitPrefix}_${finalContributionIndex}.zkey`
|
|
565
|
+
: `${circuitPrefix}_${nextZkeyIndex}.zkey`
|
|
566
|
+
// Prepare zKey storage paths.
|
|
567
|
+
const lastZkeyStorageFilePath = getZkeyStorageFilePath(circuitPrefix, lastZkeyCompleteFilename)
|
|
568
|
+
const nextZkeyStorageFilePath = getZkeyStorageFilePath(circuitPrefix, nextZkeyCompleteFilename)
|
|
569
|
+
// Prepare zKey local paths.
|
|
570
|
+
const lastZkeyLocalFilePath = isFinalizing
|
|
571
|
+
? getFinalZkeyLocalFilePath(lastZkeyCompleteFilename)
|
|
572
|
+
: getContributionLocalFilePath(lastZkeyCompleteFilename)
|
|
573
|
+
const nextZkeyLocalFilePath = isFinalizing
|
|
574
|
+
? getFinalZkeyLocalFilePath(nextZkeyCompleteFilename)
|
|
575
|
+
: getContributionLocalFilePath(nextZkeyCompleteFilename)
|
|
576
|
+
|
|
577
|
+
// Generate a custom file logger for contribution transcript.
|
|
578
|
+
const transcriptCompleteFilename = isFinalizing
|
|
579
|
+
? `${circuit.data.prefix}_${contributorOrCoordinatorIdentifier}_${finalContributionIndex}.log`
|
|
580
|
+
: `${circuit.data.prefix}_${nextZkeyIndex}.log`
|
|
581
|
+
const transcriptLocalFilePath = isFinalizing
|
|
582
|
+
? getFinalTranscriptLocalFilePath(transcriptCompleteFilename)
|
|
583
|
+
: getTranscriptLocalFilePath(transcriptCompleteFilename)
|
|
584
|
+
const transcriptLogger = createCustomLoggerForFile(transcriptLocalFilePath)
|
|
585
|
+
|
|
586
|
+
// Populate transcript file w/ header.
|
|
587
|
+
transcriptLogger.info(
|
|
588
|
+
`${isFinalizing ? `Final` : `Contribution`} transcript for ${circuitPrefix} phase 2 contribution.\n${
|
|
589
|
+
isFinalizing
|
|
590
|
+
? `Coordinator: ${contributorOrCoordinatorIdentifier}`
|
|
591
|
+
: `Contributor # ${Number(nextZkeyIndex)}`
|
|
592
|
+
} (${contributorOrCoordinatorIdentifier})\n`
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
// Get ceremony bucket name.
|
|
596
|
+
const bucketName = getBucketName(ceremonyPrefix, String(process.env.CONFIG_CEREMONY_BUCKET_POSTFIX))
|
|
597
|
+
|
|
598
|
+
await sleep(3000) // ~3s.
|
|
599
|
+
spinner.stop()
|
|
600
|
+
|
|
601
|
+
// Contribution step = DOWNLOADING.
|
|
602
|
+
if (isFinalizing || participantData.contributionStep === ParticipantContributionStep.DOWNLOADING) {
|
|
603
|
+
// Download the latest contribution from bucket.
|
|
604
|
+
await downloadCeremonyArtifact(cloudFunctions, bucketName, lastZkeyStorageFilePath, lastZkeyLocalFilePath)
|
|
605
|
+
|
|
606
|
+
console.log(
|
|
607
|
+
`${theme.symbols.success} Contribution ${theme.text.bold(`#${lastZkeyIndex}`)} correctly downloaded`
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
// Advance to next contribution step (COMPUTING) if not finalizing.
|
|
611
|
+
if (!isFinalizing) {
|
|
612
|
+
spinner.text = `Preparing for contribution computation...`
|
|
613
|
+
spinner.start()
|
|
614
|
+
|
|
615
|
+
await progressToNextContributionStep(cloudFunctions, ceremony.id)
|
|
616
|
+
|
|
617
|
+
await sleep(1000)
|
|
618
|
+
// Refresh most up-to-date data from the participant document.
|
|
619
|
+
participantData = await getLatestUpdatesFromParticipant(firestoreDatabase, ceremony.id, participant.id)
|
|
620
|
+
|
|
621
|
+
spinner.stop()
|
|
622
|
+
}
|
|
623
|
+
} else
|
|
624
|
+
console.log(`${theme.symbols.success} Contribution ${theme.text.bold(`#${lastZkeyIndex}`)} already downloaded`)
|
|
625
|
+
|
|
626
|
+
// Contribution step = COMPUTING.
|
|
627
|
+
if (isFinalizing || participantData.contributionStep === ParticipantContributionStep.COMPUTING) {
|
|
628
|
+
// Handle the next contribution computation.
|
|
629
|
+
const computingTime = await handleContributionComputation(
|
|
630
|
+
lastZkeyLocalFilePath,
|
|
631
|
+
nextZkeyLocalFilePath,
|
|
632
|
+
entropyOrBeaconHash,
|
|
633
|
+
contributorOrCoordinatorIdentifier,
|
|
634
|
+
avgTimings.contributionComputation,
|
|
635
|
+
transcriptLogger,
|
|
636
|
+
isFinalizing
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
// Permanently store on db the contribution hash and computing time.
|
|
640
|
+
spinner.text = `Writing contribution metadata...`
|
|
641
|
+
spinner.start()
|
|
642
|
+
|
|
643
|
+
// Read local transcript file info to get the contribution hash.
|
|
644
|
+
const transcriptContents = readFile(transcriptLocalFilePath)
|
|
645
|
+
const matchContributionHash = transcriptContents.match(/Contribution.+Hash.+\n\t\t.+\n\t\t.+\n.+\n\t\t.+\n/)
|
|
646
|
+
|
|
647
|
+
if (!matchContributionHash)
|
|
648
|
+
showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_FINALIZE_NO_TRANSCRIPT_CONTRIBUTION_HASH_MATCH, true)
|
|
649
|
+
|
|
650
|
+
// Format contribution hash.
|
|
651
|
+
const contributionHash = matchContributionHash?.at(0)?.replace("\n\t\t", "")!
|
|
652
|
+
|
|
653
|
+
// Make request to cloud functions to permanently store the information.
|
|
654
|
+
await permanentlyStoreCurrentContributionTimeAndHash(
|
|
655
|
+
cloudFunctions,
|
|
656
|
+
ceremony.id,
|
|
657
|
+
computingTime,
|
|
658
|
+
contributionHash
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
// Format computing time.
|
|
662
|
+
const {
|
|
663
|
+
seconds: computationSeconds,
|
|
664
|
+
minutes: computationMinutes,
|
|
665
|
+
hours: computationHours
|
|
666
|
+
} = getSecondsMinutesHoursFromMillis(computingTime)
|
|
667
|
+
|
|
668
|
+
spinner.succeed(
|
|
669
|
+
`${
|
|
670
|
+
isFinalizing ? "Contribution" : `Contribution ${theme.text.bold(`#${nextZkeyIndex}`)}`
|
|
671
|
+
} computation took ${theme.text.bold(
|
|
672
|
+
`${convertToDoubleDigits(computationHours)}:${convertToDoubleDigits(
|
|
673
|
+
computationMinutes
|
|
674
|
+
)}:${convertToDoubleDigits(computationSeconds)}`
|
|
675
|
+
)}`
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
// Advance to next contribution step (UPLOADING) if not finalizing.
|
|
679
|
+
if (!isFinalizing) {
|
|
680
|
+
spinner.text = `Preparing for uploading the contribution...`
|
|
681
|
+
spinner.start()
|
|
682
|
+
|
|
683
|
+
await progressToNextContributionStep(cloudFunctions, ceremony.id)
|
|
684
|
+
await sleep(1000)
|
|
685
|
+
|
|
686
|
+
// Refresh most up-to-date data from the participant document.
|
|
687
|
+
participantData = await getLatestUpdatesFromParticipant(firestoreDatabase, ceremony.id, participant.id)
|
|
688
|
+
|
|
689
|
+
spinner.stop()
|
|
690
|
+
}
|
|
691
|
+
} else console.log(`${theme.symbols.success} Contribution ${theme.text.bold(`#${nextZkeyIndex}`)} already computed`)
|
|
692
|
+
|
|
693
|
+
// Contribution step = UPLOADING.
|
|
694
|
+
if (isFinalizing || participantData.contributionStep === ParticipantContributionStep.UPLOADING) {
|
|
695
|
+
spinner.text = `Uploading ${isFinalizing ? "final" : "your"} contribution ${
|
|
696
|
+
!isFinalizing ? theme.text.bold(`#${nextZkeyIndex}`) : ""
|
|
697
|
+
} to storage.\n${
|
|
698
|
+
theme.symbols.warning
|
|
699
|
+
} This step may take a while based on circuit size and your contribution speed. Everything's fine, just be patient.`
|
|
700
|
+
spinner.start()
|
|
701
|
+
|
|
702
|
+
if (!isFinalizing)
|
|
703
|
+
await multiPartUpload(
|
|
704
|
+
cloudFunctions,
|
|
705
|
+
bucketName,
|
|
706
|
+
nextZkeyStorageFilePath,
|
|
707
|
+
nextZkeyLocalFilePath,
|
|
708
|
+
Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB),
|
|
709
|
+
ceremony.id,
|
|
710
|
+
participantData.tempContributionData
|
|
711
|
+
)
|
|
712
|
+
else
|
|
713
|
+
await multiPartUpload(
|
|
714
|
+
cloudFunctions,
|
|
715
|
+
bucketName,
|
|
716
|
+
nextZkeyStorageFilePath,
|
|
717
|
+
nextZkeyLocalFilePath,
|
|
718
|
+
Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB)
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
spinner.succeed(
|
|
722
|
+
`${
|
|
723
|
+
isFinalizing ? `Contribution` : `Contribution ${theme.text.bold(`#${nextZkeyIndex}`)}`
|
|
724
|
+
} correctly saved to storage`
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
// Advance to next contribution step (VERIFYING) if not finalizing.
|
|
728
|
+
if (!isFinalizing) {
|
|
729
|
+
spinner.text = `Preparing for requesting contribution verification...`
|
|
730
|
+
spinner.start()
|
|
731
|
+
|
|
732
|
+
await progressToNextContributionStep(cloudFunctions, ceremony.id)
|
|
733
|
+
await sleep(1000)
|
|
734
|
+
|
|
735
|
+
// Refresh most up-to-date data from the participant document.
|
|
736
|
+
participantData = await getLatestUpdatesFromParticipant(firestoreDatabase, ceremony.id, participant.id)
|
|
737
|
+
spinner.stop()
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Contribution step = VERIFYING.
|
|
742
|
+
if (isFinalizing || participantData.contributionStep === ParticipantContributionStep.VERIFYING) {
|
|
743
|
+
// Format verification time.
|
|
744
|
+
const { seconds, minutes, hours } = getSecondsMinutesHoursFromMillis(avgTimings.verifyCloudFunction)
|
|
745
|
+
|
|
746
|
+
process.stdout.write(
|
|
747
|
+
`${theme.symbols.info} Your contribution is under verification ${
|
|
748
|
+
avgTimings.verifyCloudFunction > 0
|
|
749
|
+
? `(~ ${theme.text.bold(
|
|
750
|
+
`${convertToDoubleDigits(hours)}:${convertToDoubleDigits(minutes)}:${convertToDoubleDigits(
|
|
751
|
+
seconds
|
|
752
|
+
)}`
|
|
753
|
+
)})\n${
|
|
754
|
+
theme.symbols.warning
|
|
755
|
+
} This step can take up to one hour based on circuit size. Everything's fine, just be patient.`
|
|
756
|
+
: ``
|
|
757
|
+
}`
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
try {
|
|
761
|
+
// Execute contribution verification.
|
|
762
|
+
await verifyContribution(
|
|
763
|
+
cloudFunctions,
|
|
764
|
+
ceremony.id,
|
|
765
|
+
circuit,
|
|
766
|
+
bucketName,
|
|
767
|
+
contributorOrCoordinatorIdentifier,
|
|
768
|
+
String(process.env.FIREBASE_CF_URL_VERIFY_CONTRIBUTION)
|
|
769
|
+
)
|
|
770
|
+
} catch (error: any) {
|
|
771
|
+
process.stdout.write(
|
|
772
|
+
`\n${theme.symbols.error} ${theme.text.bold(
|
|
773
|
+
"Unfortunately there was an error with the contribution verification. Please restart phase2cli and try again. If the problem persists, please contact the ceremony coordinator."
|
|
774
|
+
)}\n`
|
|
775
|
+
)
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|