@vocdoni/davinci-sdk 0.0.1
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 +661 -0
- package/README.md +635 -0
- package/dist/contracts.d.ts +512 -0
- package/dist/index.d.ts +1388 -0
- package/dist/index.js +2045 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2000 -0
- package/dist/index.mjs.map +1 -0
- package/dist/index.umd.js +2045 -0
- package/dist/sequencer.d.ts +538 -0
- package/package.json +103 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2045 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var axios = require('axios');
|
|
4
|
+
var davinciContracts = require('@vocdoni/davinci-contracts');
|
|
5
|
+
var snarkjs = require('snarkjs');
|
|
6
|
+
var ethers = require('ethers');
|
|
7
|
+
|
|
8
|
+
var ElectionResultsTypeNames = /* @__PURE__ */ ((ElectionResultsTypeNames2) => {
|
|
9
|
+
ElectionResultsTypeNames2["SINGLE_CHOICE_MULTIQUESTION"] = "single-choice-multiquestion";
|
|
10
|
+
ElectionResultsTypeNames2["MULTIPLE_CHOICE"] = "multiple-choice";
|
|
11
|
+
ElectionResultsTypeNames2["BUDGET"] = "budget-based";
|
|
12
|
+
ElectionResultsTypeNames2["APPROVAL"] = "approval";
|
|
13
|
+
ElectionResultsTypeNames2["QUADRATIC"] = "quadratic";
|
|
14
|
+
return ElectionResultsTypeNames2;
|
|
15
|
+
})(ElectionResultsTypeNames || {});
|
|
16
|
+
const ElectionMetadataTemplate = {
|
|
17
|
+
version: "1.2",
|
|
18
|
+
title: {
|
|
19
|
+
default: ""
|
|
20
|
+
},
|
|
21
|
+
description: {
|
|
22
|
+
default: ""
|
|
23
|
+
},
|
|
24
|
+
media: {
|
|
25
|
+
header: "",
|
|
26
|
+
logo: ""
|
|
27
|
+
},
|
|
28
|
+
meta: {},
|
|
29
|
+
questions: [
|
|
30
|
+
{
|
|
31
|
+
title: {
|
|
32
|
+
default: ""
|
|
33
|
+
},
|
|
34
|
+
description: {
|
|
35
|
+
default: ""
|
|
36
|
+
},
|
|
37
|
+
meta: {},
|
|
38
|
+
choices: [
|
|
39
|
+
{
|
|
40
|
+
title: {
|
|
41
|
+
default: "Yes"
|
|
42
|
+
},
|
|
43
|
+
value: 0,
|
|
44
|
+
meta: {}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
title: {
|
|
48
|
+
default: "No"
|
|
49
|
+
},
|
|
50
|
+
value: 1,
|
|
51
|
+
meta: {}
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
type: {
|
|
57
|
+
name: "single-choice-multiquestion" /* SINGLE_CHOICE_MULTIQUESTION */,
|
|
58
|
+
properties: {}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const getElectionMetadataTemplate = () => {
|
|
62
|
+
return JSON.parse(JSON.stringify(ElectionMetadataTemplate));
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
class BaseService {
|
|
66
|
+
constructor(baseURL, config) {
|
|
67
|
+
this.axios = axios.create({ baseURL, ...config });
|
|
68
|
+
}
|
|
69
|
+
async request(config) {
|
|
70
|
+
try {
|
|
71
|
+
const response = await this.axios.request(config);
|
|
72
|
+
return response.data;
|
|
73
|
+
} catch (err) {
|
|
74
|
+
const error = err;
|
|
75
|
+
const message = error.response?.data?.error || error.message;
|
|
76
|
+
const code = error.response?.data?.code || error.code || error.response?.status || 500;
|
|
77
|
+
const e = new Error(message);
|
|
78
|
+
e.code = code;
|
|
79
|
+
throw e;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isUUId(str) {
|
|
85
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(str);
|
|
86
|
+
}
|
|
87
|
+
class VocdoniCensusService extends BaseService {
|
|
88
|
+
constructor(baseURL) {
|
|
89
|
+
super(baseURL);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Constructs the URI for accessing a census by its root
|
|
93
|
+
* @param censusRoot - The census root (hex-prefixed)
|
|
94
|
+
* @returns The constructed URI for the census
|
|
95
|
+
*/
|
|
96
|
+
getCensusUri(censusRoot) {
|
|
97
|
+
return `${this.axios.defaults.baseURL}/censuses/${censusRoot}`;
|
|
98
|
+
}
|
|
99
|
+
createCensus() {
|
|
100
|
+
return this.request({
|
|
101
|
+
method: "POST",
|
|
102
|
+
url: "/censuses"
|
|
103
|
+
}).then((res) => res.census);
|
|
104
|
+
}
|
|
105
|
+
async addParticipants(censusId, participants) {
|
|
106
|
+
if (!isUUId(censusId)) throw new Error("Invalid census ID format");
|
|
107
|
+
await this.request({
|
|
108
|
+
method: "POST",
|
|
109
|
+
url: `/censuses/${censusId}/participants`,
|
|
110
|
+
data: { participants }
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
getParticipants(censusId) {
|
|
114
|
+
if (!isUUId(censusId)) throw new Error("Invalid census ID format");
|
|
115
|
+
return this.request({
|
|
116
|
+
method: "GET",
|
|
117
|
+
url: `/censuses/${censusId}/participants`
|
|
118
|
+
}).then((res) => res.participants);
|
|
119
|
+
}
|
|
120
|
+
getCensusRoot(censusId) {
|
|
121
|
+
if (!isUUId(censusId)) throw new Error("Invalid census ID format");
|
|
122
|
+
return this.request({
|
|
123
|
+
method: "GET",
|
|
124
|
+
url: `/censuses/${censusId}/root`
|
|
125
|
+
}).then((res) => res.root);
|
|
126
|
+
}
|
|
127
|
+
getCensusSizeById(censusId) {
|
|
128
|
+
if (!isUUId(censusId)) throw new Error("Invalid census ID format");
|
|
129
|
+
return this.request({
|
|
130
|
+
method: "GET",
|
|
131
|
+
url: `/censuses/${censusId}/size`
|
|
132
|
+
}).then((res) => res.size);
|
|
133
|
+
}
|
|
134
|
+
getCensusSizeByRoot(censusRoot) {
|
|
135
|
+
return this.request({
|
|
136
|
+
method: "GET",
|
|
137
|
+
url: `/censuses/${censusRoot}/size`
|
|
138
|
+
}).then((res) => res.size);
|
|
139
|
+
}
|
|
140
|
+
getCensusSize(censusIdOrRoot) {
|
|
141
|
+
if (isUUId(censusIdOrRoot)) {
|
|
142
|
+
return this.getCensusSizeById(censusIdOrRoot);
|
|
143
|
+
} else {
|
|
144
|
+
return this.getCensusSizeByRoot(censusIdOrRoot);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async deleteCensus(censusId) {
|
|
148
|
+
if (!isUUId(censusId)) throw new Error("Invalid census ID format");
|
|
149
|
+
await this.request({
|
|
150
|
+
method: "DELETE",
|
|
151
|
+
url: `/censuses/${censusId}`
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
getCensusProof(censusRoot, key) {
|
|
155
|
+
return this.request({
|
|
156
|
+
method: "GET",
|
|
157
|
+
url: `/censuses/${censusRoot}/proof`,
|
|
158
|
+
params: { key }
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
publishCensus(censusId) {
|
|
162
|
+
if (!isUUId(censusId)) throw new Error("Invalid census ID format");
|
|
163
|
+
return this.request({
|
|
164
|
+
method: "POST",
|
|
165
|
+
url: `/censuses/${censusId}/publish`
|
|
166
|
+
}).then((response) => ({
|
|
167
|
+
...response,
|
|
168
|
+
uri: this.getCensusUri(response.root)
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
// BigQuery endpoints
|
|
172
|
+
getSnapshots(params) {
|
|
173
|
+
return this.request({
|
|
174
|
+
method: "GET",
|
|
175
|
+
url: "/snapshots",
|
|
176
|
+
params
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
getLatestSnapshot() {
|
|
180
|
+
return this.request({
|
|
181
|
+
method: "GET",
|
|
182
|
+
url: "/snapshots/latest"
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
getHealth() {
|
|
186
|
+
return this.request({
|
|
187
|
+
method: "GET",
|
|
188
|
+
url: "/health"
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
var CensusOrigin = /* @__PURE__ */ ((CensusOrigin2) => {
|
|
194
|
+
CensusOrigin2[CensusOrigin2["CensusOriginMerkleTree"] = 1] = "CensusOriginMerkleTree";
|
|
195
|
+
CensusOrigin2[CensusOrigin2["CensusOriginCSP"] = 2] = "CensusOriginCSP";
|
|
196
|
+
return CensusOrigin2;
|
|
197
|
+
})(CensusOrigin || {});
|
|
198
|
+
function isBaseCensusProof(proof) {
|
|
199
|
+
return !!proof && typeof proof.root === "string" && typeof proof.address === "string" && typeof proof.weight === "string" && typeof proof.censusOrigin === "number" && Object.values(CensusOrigin).includes(proof.censusOrigin);
|
|
200
|
+
}
|
|
201
|
+
function isMerkleCensusProof(proof) {
|
|
202
|
+
return isBaseCensusProof(proof) && proof.censusOrigin === 1 /* CensusOriginMerkleTree */ && typeof proof.value === "string" && typeof proof.siblings === "string";
|
|
203
|
+
}
|
|
204
|
+
function isCSPCensusProof(proof) {
|
|
205
|
+
return isBaseCensusProof(proof) && proof.censusOrigin === 2 /* CensusOriginCSP */ && typeof proof.processId === "string" && typeof proof.publicKey === "string" && typeof proof.signature === "string";
|
|
206
|
+
}
|
|
207
|
+
function assertMerkleCensusProof(proof) {
|
|
208
|
+
if (!isMerkleCensusProof(proof)) {
|
|
209
|
+
throw new Error("Invalid Merkle census proof payload");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function assertCSPCensusProof(proof) {
|
|
213
|
+
if (!isCSPCensusProof(proof)) {
|
|
214
|
+
throw new Error("Invalid CSP census proof payload");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function createProcessSignatureMessage(processId) {
|
|
219
|
+
const cleanProcessId = processId.replace(/^0x/, "").toLowerCase();
|
|
220
|
+
return `I am creating a new voting process for the davinci.vote protocol identified with id ${cleanProcessId}`;
|
|
221
|
+
}
|
|
222
|
+
async function signProcessCreation(processId, signer) {
|
|
223
|
+
const message = createProcessSignatureMessage(processId);
|
|
224
|
+
return await signer.signMessage(message);
|
|
225
|
+
}
|
|
226
|
+
function validateProcessId(processId) {
|
|
227
|
+
const cleanId = processId.replace(/^0x/, "");
|
|
228
|
+
return /^[0-9a-fA-F]{64}$/.test(cleanId);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function isHexString(str) {
|
|
232
|
+
return /^0x[0-9a-f]{64}$/i.test(str);
|
|
233
|
+
}
|
|
234
|
+
class VocdoniSequencerService extends BaseService {
|
|
235
|
+
constructor(baseURL) {
|
|
236
|
+
super(baseURL);
|
|
237
|
+
}
|
|
238
|
+
async ping() {
|
|
239
|
+
await this.request({ method: "GET", url: "/ping" });
|
|
240
|
+
}
|
|
241
|
+
createProcess(body) {
|
|
242
|
+
if (!validateProcessId(body.processId)) {
|
|
243
|
+
throw new Error("Invalid processId format. Must be a 64-character hex string (32 bytes)");
|
|
244
|
+
}
|
|
245
|
+
return this.request({
|
|
246
|
+
method: "POST",
|
|
247
|
+
url: "/processes",
|
|
248
|
+
data: body
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
getProcess(processId) {
|
|
252
|
+
return this.request({
|
|
253
|
+
method: "GET",
|
|
254
|
+
url: `/processes/${processId}`
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
listProcesses() {
|
|
258
|
+
return this.request({
|
|
259
|
+
method: "GET",
|
|
260
|
+
url: "/processes"
|
|
261
|
+
}).then((res) => res.processes);
|
|
262
|
+
}
|
|
263
|
+
async submitVote(vote) {
|
|
264
|
+
await this.request({
|
|
265
|
+
method: "POST",
|
|
266
|
+
url: "/votes",
|
|
267
|
+
data: vote
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
getVoteStatus(processId, voteId) {
|
|
271
|
+
return this.request({
|
|
272
|
+
method: "GET",
|
|
273
|
+
url: `/votes/${processId}/voteId/${voteId}`
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
async hasAddressVoted(processId, address) {
|
|
277
|
+
try {
|
|
278
|
+
await this.request({
|
|
279
|
+
method: "GET",
|
|
280
|
+
url: `/votes/${processId}/address/${address}`
|
|
281
|
+
});
|
|
282
|
+
return true;
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (error?.code === 40001) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
getInfo() {
|
|
291
|
+
return this.request({
|
|
292
|
+
method: "GET",
|
|
293
|
+
url: "/info"
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
pushMetadata(metadata) {
|
|
297
|
+
return this.request({
|
|
298
|
+
method: "POST",
|
|
299
|
+
url: "/metadata",
|
|
300
|
+
data: metadata
|
|
301
|
+
}).then((res) => res.hash);
|
|
302
|
+
}
|
|
303
|
+
async getMetadata(hashOrUrl) {
|
|
304
|
+
if (hashOrUrl.startsWith("http://") || hashOrUrl.startsWith("https://")) {
|
|
305
|
+
try {
|
|
306
|
+
const response = await fetch(hashOrUrl);
|
|
307
|
+
if (!response.ok) {
|
|
308
|
+
throw new Error(`Failed to fetch metadata from URL: ${response.status} ${response.statusText}`);
|
|
309
|
+
}
|
|
310
|
+
return await response.json();
|
|
311
|
+
} catch (error) {
|
|
312
|
+
throw new Error(`Failed to fetch metadata from URL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (!isHexString(hashOrUrl)) {
|
|
316
|
+
throw new Error("Invalid metadata hash format");
|
|
317
|
+
}
|
|
318
|
+
return this.request({
|
|
319
|
+
method: "GET",
|
|
320
|
+
url: `/metadata/${hashOrUrl}`
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
getMetadataUrl(hash) {
|
|
324
|
+
if (!isHexString(hash)) throw new Error("Invalid metadata hash format");
|
|
325
|
+
return `${this.axios.defaults.baseURL}/metadata/${hash}`;
|
|
326
|
+
}
|
|
327
|
+
getStats() {
|
|
328
|
+
return this.request({
|
|
329
|
+
method: "GET",
|
|
330
|
+
url: "/sequencer/stats"
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
getWorkers() {
|
|
334
|
+
return this.request({
|
|
335
|
+
method: "GET",
|
|
336
|
+
url: "/sequencer/workers"
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
class VocdoniApiService {
|
|
342
|
+
constructor(config) {
|
|
343
|
+
this.sequencer = new VocdoniSequencerService(config.sequencerURL);
|
|
344
|
+
this.census = new VocdoniCensusService(config.censusURL);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const DEFAULT_ENVIRONMENT_URLS = {
|
|
349
|
+
dev: {
|
|
350
|
+
sequencer: "https://sequencer-dev.davinci.vote",
|
|
351
|
+
census: "https://c3-dev.davinci.vote",
|
|
352
|
+
chain: "sepolia"
|
|
353
|
+
},
|
|
354
|
+
stg: {
|
|
355
|
+
sequencer: "https://sequencer1.davinci.vote",
|
|
356
|
+
census: "https://c3.davinci.vote",
|
|
357
|
+
chain: "sepolia"
|
|
358
|
+
},
|
|
359
|
+
prod: {
|
|
360
|
+
// TODO: Add production URLs when available
|
|
361
|
+
sequencer: "",
|
|
362
|
+
census: "",
|
|
363
|
+
chain: "mainnet"
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
function getEnvironmentConfig(environment) {
|
|
367
|
+
return DEFAULT_ENVIRONMENT_URLS[environment];
|
|
368
|
+
}
|
|
369
|
+
function getEnvironmentUrls(environment) {
|
|
370
|
+
const config = DEFAULT_ENVIRONMENT_URLS[environment];
|
|
371
|
+
return {
|
|
372
|
+
sequencer: config.sequencer,
|
|
373
|
+
census: config.census
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function getEnvironmentChain(environment) {
|
|
377
|
+
return DEFAULT_ENVIRONMENT_URLS[environment].chain;
|
|
378
|
+
}
|
|
379
|
+
function resolveConfiguration(options = {}) {
|
|
380
|
+
const environment = options.environment || "prod";
|
|
381
|
+
const defaultConfig = getEnvironmentConfig(environment);
|
|
382
|
+
const resolvedConfig = { ...defaultConfig };
|
|
383
|
+
if (options.customUrls) {
|
|
384
|
+
if (options.customUrls.sequencer !== void 0) {
|
|
385
|
+
resolvedConfig.sequencer = options.customUrls.sequencer;
|
|
386
|
+
}
|
|
387
|
+
if (options.customUrls.census !== void 0) {
|
|
388
|
+
resolvedConfig.census = options.customUrls.census;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (options.customChain) {
|
|
392
|
+
resolvedConfig.chain = options.customChain;
|
|
393
|
+
}
|
|
394
|
+
return resolvedConfig;
|
|
395
|
+
}
|
|
396
|
+
function resolveUrls(options = {}) {
|
|
397
|
+
const config = resolveConfiguration(options);
|
|
398
|
+
return {
|
|
399
|
+
sequencer: config.sequencer,
|
|
400
|
+
census: config.census
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
var processRegistry = {
|
|
405
|
+
sepolia: "0x40939Ec9FD872eb79A1723B559572dfD71a05d11",
|
|
406
|
+
uzh: "0x69B16f67Bd2fB18bD720379E9C1Ef5EaD3872d67",
|
|
407
|
+
mainnet: "0x0"
|
|
408
|
+
};
|
|
409
|
+
var organizationRegistry = {
|
|
410
|
+
sepolia: "0xe7136ED5a7b0e995A8fe35d8B1B815E4160cB491",
|
|
411
|
+
uzh: "0xf7BCE4546805547bE526Ca864d6722Ed193E51Aa",
|
|
412
|
+
mainnet: "0x0"
|
|
413
|
+
};
|
|
414
|
+
var stateTransitionVerifierGroth16 = {
|
|
415
|
+
sepolia: "0xb7A142D24b9220eCBC4f7fcB89Ee952a6C7E332a",
|
|
416
|
+
uzh: "0x5e4673CD378F05cc3Ae25804539c91E711548741",
|
|
417
|
+
mainnet: "0x0"
|
|
418
|
+
};
|
|
419
|
+
var resultsVerifierGroth16 = {
|
|
420
|
+
sepolia: "0x1188cEbB56ecc90e2bAe5c914274C81Fe1a22e67",
|
|
421
|
+
uzh: "0x00c7F87731346F592197E49A90Ad6EC236Ad9985",
|
|
422
|
+
mainnet: "0x0"
|
|
423
|
+
};
|
|
424
|
+
var sequencerRegistry = {
|
|
425
|
+
sepolia: "0x0",
|
|
426
|
+
uzh: "0x0",
|
|
427
|
+
mainnet: "0x0"
|
|
428
|
+
};
|
|
429
|
+
var addressesJson = {
|
|
430
|
+
processRegistry: processRegistry,
|
|
431
|
+
organizationRegistry: organizationRegistry,
|
|
432
|
+
stateTransitionVerifierGroth16: stateTransitionVerifierGroth16,
|
|
433
|
+
resultsVerifierGroth16: resultsVerifierGroth16,
|
|
434
|
+
sequencerRegistry: sequencerRegistry
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const deployedAddresses = addressesJson;
|
|
438
|
+
var TxStatus = /* @__PURE__ */ ((TxStatus2) => {
|
|
439
|
+
TxStatus2["Pending"] = "pending";
|
|
440
|
+
TxStatus2["Completed"] = "completed";
|
|
441
|
+
TxStatus2["Reverted"] = "reverted";
|
|
442
|
+
TxStatus2["Failed"] = "failed";
|
|
443
|
+
return TxStatus2;
|
|
444
|
+
})(TxStatus || {});
|
|
445
|
+
class SmartContractService {
|
|
446
|
+
/**
|
|
447
|
+
* Sends a transaction and yields status events during its lifecycle.
|
|
448
|
+
* This method handles the complete transaction flow from submission to completion,
|
|
449
|
+
* including error handling and status updates.
|
|
450
|
+
*
|
|
451
|
+
* @template T - The type of the successful response data
|
|
452
|
+
* @param txPromise - Promise resolving to the transaction response
|
|
453
|
+
* @param responseHandler - Function to process the successful transaction result
|
|
454
|
+
* @returns AsyncGenerator yielding transaction status events
|
|
455
|
+
*
|
|
456
|
+
* @example
|
|
457
|
+
* ```typescript
|
|
458
|
+
* const txStream = await this.sendTx(
|
|
459
|
+
* contract.someMethod(),
|
|
460
|
+
* async () => await contract.getUpdatedValue()
|
|
461
|
+
* );
|
|
462
|
+
*
|
|
463
|
+
* for await (const event of txStream) {
|
|
464
|
+
* switch (event.status) {
|
|
465
|
+
* case TxStatus.Pending:
|
|
466
|
+
* console.log(`Transaction pending: ${event.hash}`);
|
|
467
|
+
* break;
|
|
468
|
+
* case TxStatus.Completed:
|
|
469
|
+
* console.log(`Transaction completed:`, event.response);
|
|
470
|
+
* break;
|
|
471
|
+
* }
|
|
472
|
+
* }
|
|
473
|
+
* ```
|
|
474
|
+
*/
|
|
475
|
+
async *sendTx(txPromise, responseHandler) {
|
|
476
|
+
try {
|
|
477
|
+
const tx = await txPromise;
|
|
478
|
+
yield { status: "pending" /* Pending */, hash: tx.hash };
|
|
479
|
+
const receipt = await tx.wait();
|
|
480
|
+
if (!receipt) {
|
|
481
|
+
yield { status: "reverted" /* Reverted */, reason: "Transaction was dropped or not mined." };
|
|
482
|
+
} else if (receipt.status === 0) {
|
|
483
|
+
yield { status: "reverted" /* Reverted */, reason: "Transaction reverted." };
|
|
484
|
+
} else {
|
|
485
|
+
const result = await responseHandler();
|
|
486
|
+
yield { status: "completed" /* Completed */, response: result };
|
|
487
|
+
}
|
|
488
|
+
} catch (err) {
|
|
489
|
+
yield {
|
|
490
|
+
status: "failed" /* Failed */,
|
|
491
|
+
error: err instanceof Error ? err : new Error("Unknown transaction error")
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Executes a transaction stream and returns the result or throws an error.
|
|
497
|
+
* This is a convenience method that processes a transaction stream and either
|
|
498
|
+
* returns the successful result or throws an appropriate error.
|
|
499
|
+
*
|
|
500
|
+
* @template T - The type of the successful response data
|
|
501
|
+
* @param stream - AsyncGenerator of transaction status events
|
|
502
|
+
* @returns Promise resolving to the successful response data
|
|
503
|
+
* @throws Error if the transaction fails or reverts
|
|
504
|
+
*
|
|
505
|
+
* @example
|
|
506
|
+
* ```typescript
|
|
507
|
+
* try {
|
|
508
|
+
* const result = await SmartContractService.executeTx(
|
|
509
|
+
* contract.someMethod()
|
|
510
|
+
* );
|
|
511
|
+
* console.log('Transaction successful:', result);
|
|
512
|
+
* } catch (error) {
|
|
513
|
+
* console.error('Transaction failed:', error);
|
|
514
|
+
* }
|
|
515
|
+
* ```
|
|
516
|
+
*/
|
|
517
|
+
static async executeTx(stream) {
|
|
518
|
+
for await (const event of stream) {
|
|
519
|
+
switch (event.status) {
|
|
520
|
+
case "completed" /* Completed */:
|
|
521
|
+
return event.response;
|
|
522
|
+
case "failed" /* Failed */:
|
|
523
|
+
throw event.error;
|
|
524
|
+
case "reverted" /* Reverted */:
|
|
525
|
+
throw new Error(`Transaction reverted: ${event.reason || "unknown reason"}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
throw new Error("Transaction stream ended unexpectedly");
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Normalizes event listener arguments between different ethers.js versions.
|
|
532
|
+
* This helper method ensures consistent event argument handling regardless of
|
|
533
|
+
* whether the event payload follows ethers v5 or v6 format.
|
|
534
|
+
*
|
|
535
|
+
* @template Args - Tuple type representing the expected event arguments
|
|
536
|
+
* @param callback - The event callback function to normalize
|
|
537
|
+
* @returns Normalized event listener function
|
|
538
|
+
*
|
|
539
|
+
* @example
|
|
540
|
+
* ```typescript
|
|
541
|
+
* contract.on('Transfer', this.normalizeListener((from: string, to: string, amount: BigInt) => {
|
|
542
|
+
* console.log(`Transfer from ${from} to ${to}: ${amount}`);
|
|
543
|
+
* }));
|
|
544
|
+
* ```
|
|
545
|
+
*/
|
|
546
|
+
normalizeListener(callback) {
|
|
547
|
+
return (...listenerArgs) => {
|
|
548
|
+
let args;
|
|
549
|
+
if (listenerArgs.length === 1 && listenerArgs[0]?.args) {
|
|
550
|
+
args = listenerArgs[0].args;
|
|
551
|
+
} else {
|
|
552
|
+
args = listenerArgs;
|
|
553
|
+
}
|
|
554
|
+
callback(...args);
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
class ContractServiceError extends Error {
|
|
560
|
+
/**
|
|
561
|
+
* Creates a new ContractServiceError instance.
|
|
562
|
+
*
|
|
563
|
+
* @param message - The error message describing what went wrong
|
|
564
|
+
* @param operation - The operation that was being performed when the error occurred
|
|
565
|
+
*/
|
|
566
|
+
constructor(message, operation) {
|
|
567
|
+
super(message);
|
|
568
|
+
this.operation = operation;
|
|
569
|
+
this.name = this.constructor.name;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
class OrganizationCreateError extends ContractServiceError {
|
|
573
|
+
}
|
|
574
|
+
class OrganizationUpdateError extends ContractServiceError {
|
|
575
|
+
}
|
|
576
|
+
class OrganizationDeleteError extends ContractServiceError {
|
|
577
|
+
}
|
|
578
|
+
class OrganizationAdministratorError extends ContractServiceError {
|
|
579
|
+
}
|
|
580
|
+
class ProcessCreateError extends ContractServiceError {
|
|
581
|
+
}
|
|
582
|
+
class ProcessStatusError extends ContractServiceError {
|
|
583
|
+
}
|
|
584
|
+
class ProcessCensusError extends ContractServiceError {
|
|
585
|
+
}
|
|
586
|
+
class ProcessDurationError extends ContractServiceError {
|
|
587
|
+
}
|
|
588
|
+
class ProcessStateTransitionError extends ContractServiceError {
|
|
589
|
+
}
|
|
590
|
+
class ProcessResultError extends ContractServiceError {
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
var ProcessStatus = /* @__PURE__ */ ((ProcessStatus2) => {
|
|
594
|
+
ProcessStatus2[ProcessStatus2["READY"] = 0] = "READY";
|
|
595
|
+
ProcessStatus2[ProcessStatus2["ENDED"] = 1] = "ENDED";
|
|
596
|
+
ProcessStatus2[ProcessStatus2["CANCELED"] = 2] = "CANCELED";
|
|
597
|
+
ProcessStatus2[ProcessStatus2["PAUSED"] = 3] = "PAUSED";
|
|
598
|
+
ProcessStatus2[ProcessStatus2["RESULTS"] = 4] = "RESULTS";
|
|
599
|
+
return ProcessStatus2;
|
|
600
|
+
})(ProcessStatus || {});
|
|
601
|
+
class ProcessRegistryService extends SmartContractService {
|
|
602
|
+
constructor(contractAddress, runner) {
|
|
603
|
+
super();
|
|
604
|
+
this.contract = davinciContracts.ProcessRegistry__factory.connect(contractAddress, runner);
|
|
605
|
+
}
|
|
606
|
+
// ─── READS ─────────────────────────────────────────────────────────
|
|
607
|
+
async getProcess(processID) {
|
|
608
|
+
return this.contract.getProcess(processID);
|
|
609
|
+
}
|
|
610
|
+
async getProcessCount() {
|
|
611
|
+
const c = await this.contract.processCount();
|
|
612
|
+
return Number(c);
|
|
613
|
+
}
|
|
614
|
+
async getChainID() {
|
|
615
|
+
const chainId = await this.contract.chainID();
|
|
616
|
+
return chainId.toString();
|
|
617
|
+
}
|
|
618
|
+
async getNextProcessId(organizationId) {
|
|
619
|
+
return this.contract.getNextProcessId(organizationId);
|
|
620
|
+
}
|
|
621
|
+
async getProcessEndTime(processID) {
|
|
622
|
+
return this.contract.getProcessEndTime(processID);
|
|
623
|
+
}
|
|
624
|
+
async getRVerifierVKeyHash() {
|
|
625
|
+
return this.contract.getRVerifierVKeyHash();
|
|
626
|
+
}
|
|
627
|
+
async getSTVerifierVKeyHash() {
|
|
628
|
+
return this.contract.getSTVerifierVKeyHash();
|
|
629
|
+
}
|
|
630
|
+
async getMaxCensusOrigin() {
|
|
631
|
+
return this.contract.MAX_CENSUS_ORIGIN();
|
|
632
|
+
}
|
|
633
|
+
async getMaxStatus() {
|
|
634
|
+
return this.contract.MAX_STATUS();
|
|
635
|
+
}
|
|
636
|
+
async getProcessNonce(address) {
|
|
637
|
+
return this.contract.processNonce(address);
|
|
638
|
+
}
|
|
639
|
+
async getProcessDirect(processID) {
|
|
640
|
+
return this.contract.processes(processID);
|
|
641
|
+
}
|
|
642
|
+
async getRVerifier() {
|
|
643
|
+
return this.contract.rVerifier();
|
|
644
|
+
}
|
|
645
|
+
async getSTVerifier() {
|
|
646
|
+
return this.contract.stVerifier();
|
|
647
|
+
}
|
|
648
|
+
// ─── WRITES ────────────────────────────────────────────────────────
|
|
649
|
+
newProcess(status, startTime, duration, ballotMode, census, metadata, encryptionKey, initStateRoot) {
|
|
650
|
+
const contractCensus = {
|
|
651
|
+
censusOrigin: BigInt(census.censusOrigin),
|
|
652
|
+
maxVotes: BigInt(census.maxVotes),
|
|
653
|
+
censusRoot: census.censusRoot,
|
|
654
|
+
censusURI: census.censusURI
|
|
655
|
+
};
|
|
656
|
+
return this.sendTx(
|
|
657
|
+
this.contract.newProcess(
|
|
658
|
+
status,
|
|
659
|
+
startTime,
|
|
660
|
+
duration,
|
|
661
|
+
ballotMode,
|
|
662
|
+
contractCensus,
|
|
663
|
+
metadata,
|
|
664
|
+
encryptionKey,
|
|
665
|
+
initStateRoot
|
|
666
|
+
).catch((e) => {
|
|
667
|
+
throw new ProcessCreateError(e.message, "create");
|
|
668
|
+
}),
|
|
669
|
+
async () => ({ success: true })
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
setProcessStatus(processID, newStatus) {
|
|
673
|
+
return this.sendTx(
|
|
674
|
+
this.contract.setProcessStatus(processID, newStatus).catch((e) => {
|
|
675
|
+
throw new ProcessStatusError(e.message, "setStatus");
|
|
676
|
+
}),
|
|
677
|
+
async () => ({ success: true })
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
setProcessCensus(processID, census) {
|
|
681
|
+
const contractCensus = {
|
|
682
|
+
censusOrigin: BigInt(census.censusOrigin),
|
|
683
|
+
maxVotes: BigInt(census.maxVotes),
|
|
684
|
+
censusRoot: census.censusRoot,
|
|
685
|
+
censusURI: census.censusURI
|
|
686
|
+
};
|
|
687
|
+
return this.sendTx(
|
|
688
|
+
this.contract.setProcessCensus(processID, contractCensus).catch((e) => {
|
|
689
|
+
throw new ProcessCensusError(e.message, "setCensus");
|
|
690
|
+
}),
|
|
691
|
+
async () => ({ success: true })
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
setProcessDuration(processID, duration) {
|
|
695
|
+
return this.sendTx(
|
|
696
|
+
this.contract.setProcessDuration(processID, duration).catch((e) => {
|
|
697
|
+
throw new ProcessDurationError(e.message, "setDuration");
|
|
698
|
+
}),
|
|
699
|
+
async () => ({ success: true })
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Matches the on-chain `submitStateTransition(processId, proof, input)`
|
|
704
|
+
*/
|
|
705
|
+
submitStateTransition(processID, proof, input) {
|
|
706
|
+
return this.sendTx(
|
|
707
|
+
this.contract.submitStateTransition(processID, proof, input).catch((e) => {
|
|
708
|
+
throw new ProcessStateTransitionError(e.message, "submitStateTransition");
|
|
709
|
+
}),
|
|
710
|
+
async () => ({ success: true })
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Sets the results for a voting process.
|
|
715
|
+
*
|
|
716
|
+
* @param processID - The ID of the process to set results for
|
|
717
|
+
* @param proof - Zero-knowledge proof validating the results
|
|
718
|
+
* @param input - Input data for the proof verification
|
|
719
|
+
* @returns A transaction stream that resolves to success status
|
|
720
|
+
*/
|
|
721
|
+
setProcessResults(processID, proof, input) {
|
|
722
|
+
return this.sendTx(
|
|
723
|
+
this.contract.setProcessResults(
|
|
724
|
+
processID,
|
|
725
|
+
proof,
|
|
726
|
+
input
|
|
727
|
+
).catch((e) => {
|
|
728
|
+
throw new ProcessResultError(e.message, "setResults");
|
|
729
|
+
}),
|
|
730
|
+
async () => ({ success: true })
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
// ─── EVENT LISTENERS ───────────────────────────────────────────────────────
|
|
734
|
+
onProcessCreated(cb) {
|
|
735
|
+
this.contract.on(
|
|
736
|
+
this.contract.filters.ProcessCreated(),
|
|
737
|
+
this.normalizeListener(cb)
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
onProcessStatusChanged(cb) {
|
|
741
|
+
this.contract.on(
|
|
742
|
+
this.contract.filters.ProcessStatusChanged(),
|
|
743
|
+
this.normalizeListener(cb)
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
onCensusUpdated(cb) {
|
|
747
|
+
this.contract.on(
|
|
748
|
+
this.contract.filters.CensusUpdated(),
|
|
749
|
+
this.normalizeListener(cb)
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
onProcessDurationChanged(cb) {
|
|
753
|
+
this.contract.on(
|
|
754
|
+
this.contract.filters.ProcessDurationChanged(),
|
|
755
|
+
this.normalizeListener(cb)
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
onStateRootUpdated(cb) {
|
|
759
|
+
this.contract.on(
|
|
760
|
+
this.contract.filters.ProcessStateRootUpdated(),
|
|
761
|
+
this.normalizeListener(cb)
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
onProcessResultsSet(cb) {
|
|
765
|
+
this.contract.on(
|
|
766
|
+
this.contract.filters.ProcessResultsSet(),
|
|
767
|
+
this.normalizeListener(cb)
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
removeAllListeners() {
|
|
771
|
+
this.contract.removeAllListeners();
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
class ProcessOrchestrationService {
|
|
776
|
+
constructor(processRegistry, apiService, organizationRegistry, getCrypto, signer) {
|
|
777
|
+
this.processRegistry = processRegistry;
|
|
778
|
+
this.apiService = apiService;
|
|
779
|
+
this.organizationRegistry = organizationRegistry;
|
|
780
|
+
this.getCrypto = getCrypto;
|
|
781
|
+
this.signer = signer;
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Gets user-friendly process information by transforming raw contract data
|
|
785
|
+
* @param processId - The process ID to fetch
|
|
786
|
+
* @returns Promise resolving to the user-friendly process information
|
|
787
|
+
*/
|
|
788
|
+
async getProcess(processId) {
|
|
789
|
+
const rawProcess = await this.processRegistry.getProcess(processId);
|
|
790
|
+
let metadata = null;
|
|
791
|
+
let title;
|
|
792
|
+
let description;
|
|
793
|
+
let questions = [];
|
|
794
|
+
try {
|
|
795
|
+
if (rawProcess.metadataURI) {
|
|
796
|
+
metadata = await this.apiService.sequencer.getMetadata(rawProcess.metadataURI);
|
|
797
|
+
title = metadata?.title?.default;
|
|
798
|
+
description = metadata?.description?.default;
|
|
799
|
+
if (metadata?.questions) {
|
|
800
|
+
questions = metadata.questions.map((q) => ({
|
|
801
|
+
title: q.title?.default,
|
|
802
|
+
description: q.description?.default,
|
|
803
|
+
choices: q.choices?.map((c) => ({
|
|
804
|
+
title: c.title?.default,
|
|
805
|
+
value: c.value
|
|
806
|
+
})) || []
|
|
807
|
+
}));
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
} catch (error) {
|
|
811
|
+
console.warn(`Failed to fetch metadata for process ${processId}:`, error);
|
|
812
|
+
}
|
|
813
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
814
|
+
const startTime = Number(rawProcess.startTime);
|
|
815
|
+
const duration = Number(rawProcess.duration);
|
|
816
|
+
const endTime = startTime + duration;
|
|
817
|
+
const timeRemaining = now >= endTime ? 0 : now >= startTime ? endTime - now : startTime - now;
|
|
818
|
+
const census = {
|
|
819
|
+
type: Number(rawProcess.census.censusOrigin),
|
|
820
|
+
root: rawProcess.census.censusRoot,
|
|
821
|
+
size: Number(rawProcess.census.maxVotes),
|
|
822
|
+
uri: rawProcess.census.censusURI || ""
|
|
823
|
+
};
|
|
824
|
+
const ballot = {
|
|
825
|
+
numFields: Number(rawProcess.ballotMode.numFields),
|
|
826
|
+
maxValue: rawProcess.ballotMode.maxValue.toString(),
|
|
827
|
+
minValue: rawProcess.ballotMode.minValue.toString(),
|
|
828
|
+
uniqueValues: rawProcess.ballotMode.uniqueValues,
|
|
829
|
+
costFromWeight: rawProcess.ballotMode.costFromWeight,
|
|
830
|
+
costExponent: Number(rawProcess.ballotMode.costExponent),
|
|
831
|
+
maxValueSum: rawProcess.ballotMode.maxValueSum.toString(),
|
|
832
|
+
minValueSum: rawProcess.ballotMode.minValueSum.toString()
|
|
833
|
+
};
|
|
834
|
+
return {
|
|
835
|
+
processId,
|
|
836
|
+
title,
|
|
837
|
+
description,
|
|
838
|
+
census,
|
|
839
|
+
ballot,
|
|
840
|
+
questions,
|
|
841
|
+
status: Number(rawProcess.status),
|
|
842
|
+
creator: rawProcess.organizationId,
|
|
843
|
+
startDate: new Date(startTime * 1e3),
|
|
844
|
+
endDate: new Date(endTime * 1e3),
|
|
845
|
+
duration,
|
|
846
|
+
timeRemaining,
|
|
847
|
+
result: rawProcess.result,
|
|
848
|
+
voteCount: Number(rawProcess.voteCount),
|
|
849
|
+
voteOverwriteCount: Number(rawProcess.voteOverwriteCount),
|
|
850
|
+
metadataURI: rawProcess.metadataURI,
|
|
851
|
+
raw: rawProcess
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Creates a complete voting process with minimal configuration
|
|
856
|
+
* This method handles all the complex orchestration internally
|
|
857
|
+
*/
|
|
858
|
+
async createProcess(config) {
|
|
859
|
+
const { startTime, duration } = this.calculateTiming(config.timing);
|
|
860
|
+
const signerAddress = await this.signer.getAddress();
|
|
861
|
+
const processId = await this.processRegistry.getNextProcessId(signerAddress);
|
|
862
|
+
const censusRoot = config.census.root;
|
|
863
|
+
const ballotMode = config.ballot;
|
|
864
|
+
const metadata = this.createMetadata(config);
|
|
865
|
+
const metadataHash = await this.apiService.sequencer.pushMetadata(metadata);
|
|
866
|
+
const metadataUri = this.apiService.sequencer.getMetadataUrl(metadataHash);
|
|
867
|
+
const signature = await signProcessCreation(processId, this.signer);
|
|
868
|
+
const sequencerResult = await this.apiService.sequencer.createProcess({
|
|
869
|
+
processId,
|
|
870
|
+
censusRoot,
|
|
871
|
+
ballotMode,
|
|
872
|
+
signature,
|
|
873
|
+
censusOrigin: config.census.type
|
|
874
|
+
});
|
|
875
|
+
const census = {
|
|
876
|
+
censusOrigin: config.census.type,
|
|
877
|
+
maxVotes: config.census.size.toString(),
|
|
878
|
+
censusRoot,
|
|
879
|
+
censusURI: config.census.uri
|
|
880
|
+
};
|
|
881
|
+
const encryptionKey = {
|
|
882
|
+
x: sequencerResult.encryptionPubKey[0],
|
|
883
|
+
y: sequencerResult.encryptionPubKey[1]
|
|
884
|
+
};
|
|
885
|
+
const txStream = this.processRegistry.newProcess(
|
|
886
|
+
ProcessStatus.READY,
|
|
887
|
+
startTime,
|
|
888
|
+
duration,
|
|
889
|
+
ballotMode,
|
|
890
|
+
census,
|
|
891
|
+
metadataUri,
|
|
892
|
+
encryptionKey,
|
|
893
|
+
BigInt(sequencerResult.stateRoot)
|
|
894
|
+
);
|
|
895
|
+
let transactionHash = "unknown";
|
|
896
|
+
for await (const event of txStream) {
|
|
897
|
+
if (event.status === "pending") {
|
|
898
|
+
transactionHash = event.hash;
|
|
899
|
+
} else if (event.status === "completed") {
|
|
900
|
+
break;
|
|
901
|
+
} else if (event.status === "failed") {
|
|
902
|
+
throw event.error;
|
|
903
|
+
} else if (event.status === "reverted") {
|
|
904
|
+
throw new Error(`Transaction reverted: ${event.reason || "unknown reason"}`);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return {
|
|
908
|
+
processId,
|
|
909
|
+
transactionHash
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Validates and calculates timing parameters
|
|
914
|
+
*/
|
|
915
|
+
calculateTiming(timing) {
|
|
916
|
+
const { startDate, duration, endDate } = timing;
|
|
917
|
+
if (duration !== void 0 && endDate !== void 0) {
|
|
918
|
+
throw new Error("Cannot specify both 'duration' and 'endDate'. Use one or the other.");
|
|
919
|
+
}
|
|
920
|
+
if (duration === void 0 && endDate === void 0) {
|
|
921
|
+
throw new Error("Must specify either 'duration' (in seconds) or 'endDate'.");
|
|
922
|
+
}
|
|
923
|
+
const startTime = startDate ? this.dateToUnixTimestamp(startDate) : Math.floor(Date.now() / 1e3) + 60;
|
|
924
|
+
let calculatedDuration;
|
|
925
|
+
if (duration !== void 0) {
|
|
926
|
+
calculatedDuration = duration;
|
|
927
|
+
} else {
|
|
928
|
+
const endTime = this.dateToUnixTimestamp(endDate);
|
|
929
|
+
calculatedDuration = endTime - startTime;
|
|
930
|
+
if (calculatedDuration <= 0) {
|
|
931
|
+
throw new Error("End date must be after start date.");
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
935
|
+
if (startTime < now - 30) {
|
|
936
|
+
throw new Error("Start date cannot be in the past.");
|
|
937
|
+
}
|
|
938
|
+
return { startTime, duration: calculatedDuration };
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Converts various date formats to Unix timestamp
|
|
942
|
+
*/
|
|
943
|
+
dateToUnixTimestamp(date) {
|
|
944
|
+
if (typeof date === "number") {
|
|
945
|
+
if (date > 1e10) {
|
|
946
|
+
return Math.floor(date / 1e3);
|
|
947
|
+
}
|
|
948
|
+
return Math.floor(date);
|
|
949
|
+
}
|
|
950
|
+
if (typeof date === "string") {
|
|
951
|
+
const parsed = new Date(date);
|
|
952
|
+
if (isNaN(parsed.getTime())) {
|
|
953
|
+
throw new Error(`Invalid date string: ${date}`);
|
|
954
|
+
}
|
|
955
|
+
return Math.floor(parsed.getTime() / 1e3);
|
|
956
|
+
}
|
|
957
|
+
if (date instanceof Date) {
|
|
958
|
+
if (isNaN(date.getTime())) {
|
|
959
|
+
throw new Error("Invalid Date object provided.");
|
|
960
|
+
}
|
|
961
|
+
return Math.floor(date.getTime() / 1e3);
|
|
962
|
+
}
|
|
963
|
+
throw new Error("Invalid date format. Use Date object, ISO string, or Unix timestamp.");
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Creates metadata from the simplified configuration
|
|
967
|
+
*/
|
|
968
|
+
createMetadata(config) {
|
|
969
|
+
const metadata = getElectionMetadataTemplate();
|
|
970
|
+
metadata.title.default = config.title;
|
|
971
|
+
metadata.description.default = config.description || "";
|
|
972
|
+
if (!config.questions || config.questions.length === 0) {
|
|
973
|
+
throw new Error("Questions are required. Please provide at least one question with choices.");
|
|
974
|
+
}
|
|
975
|
+
metadata.questions = config.questions.map((q) => ({
|
|
976
|
+
title: { default: q.title },
|
|
977
|
+
description: { default: q.description || "" },
|
|
978
|
+
meta: {},
|
|
979
|
+
choices: q.choices.map((c) => ({
|
|
980
|
+
title: { default: c.title },
|
|
981
|
+
value: c.value,
|
|
982
|
+
meta: {}
|
|
983
|
+
}))
|
|
984
|
+
}));
|
|
985
|
+
return metadata;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
class CircomProof {
|
|
990
|
+
constructor(opts = {}) {
|
|
991
|
+
// simple in-memory cache keyed by URL
|
|
992
|
+
this.wasmCache = /* @__PURE__ */ new Map();
|
|
993
|
+
this.zkeyCache = /* @__PURE__ */ new Map();
|
|
994
|
+
this.vkeyCache = /* @__PURE__ */ new Map();
|
|
995
|
+
this.wasmUrl = opts.wasmUrl;
|
|
996
|
+
this.zkeyUrl = opts.zkeyUrl;
|
|
997
|
+
this.vkeyUrl = opts.vkeyUrl;
|
|
998
|
+
this.wasmHash = opts.wasmHash;
|
|
999
|
+
this.zkeyHash = opts.zkeyHash;
|
|
1000
|
+
this.vkeyHash = opts.vkeyHash;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Computes SHA-256 hash of the given data and compares it with the expected hash.
|
|
1004
|
+
* @param data - The data to hash (string or ArrayBuffer or Uint8Array)
|
|
1005
|
+
* @param expectedHash - The expected SHA-256 hash in hexadecimal format
|
|
1006
|
+
* @param filename - The filename for error reporting
|
|
1007
|
+
* @throws Error if the computed hash doesn't match the expected hash
|
|
1008
|
+
*/
|
|
1009
|
+
verifyHash(data, expectedHash, filename) {
|
|
1010
|
+
let bytes;
|
|
1011
|
+
if (typeof data === "string") {
|
|
1012
|
+
bytes = new TextEncoder().encode(data);
|
|
1013
|
+
} else if (data instanceof ArrayBuffer) {
|
|
1014
|
+
bytes = new Uint8Array(data);
|
|
1015
|
+
} else {
|
|
1016
|
+
bytes = data;
|
|
1017
|
+
}
|
|
1018
|
+
const computedHash = ethers.sha256(bytes).slice(2);
|
|
1019
|
+
if (computedHash.toLowerCase() !== expectedHash.toLowerCase()) {
|
|
1020
|
+
throw new Error(
|
|
1021
|
+
`Hash verification failed for ${filename}. Expected: ${expectedHash.toLowerCase()}, Computed: ${computedHash.toLowerCase()}`
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Generate a zk‐SNARK proof.
|
|
1027
|
+
* If you didn't pass wasmUrl/zkeyUrl in the constructor you must supply them here.
|
|
1028
|
+
*/
|
|
1029
|
+
async generate(inputs, urls = {}) {
|
|
1030
|
+
const wasmUrl = urls.wasmUrl ?? this.wasmUrl;
|
|
1031
|
+
const zkeyUrl = urls.zkeyUrl ?? this.zkeyUrl;
|
|
1032
|
+
if (!wasmUrl) throw new Error("`wasmUrl` is required to generate a proof");
|
|
1033
|
+
if (!zkeyUrl) throw new Error("`zkeyUrl` is required to generate a proof");
|
|
1034
|
+
let wasmBin = this.wasmCache.get(wasmUrl);
|
|
1035
|
+
if (!wasmBin) {
|
|
1036
|
+
const r = await fetch(wasmUrl);
|
|
1037
|
+
if (!r.ok) throw new Error(`Failed to fetch wasm at ${wasmUrl}: ${r.status}`);
|
|
1038
|
+
const buf = await r.arrayBuffer();
|
|
1039
|
+
wasmBin = new Uint8Array(buf);
|
|
1040
|
+
if (this.wasmHash) {
|
|
1041
|
+
this.verifyHash(wasmBin, this.wasmHash, "circuit.wasm");
|
|
1042
|
+
}
|
|
1043
|
+
this.wasmCache.set(wasmUrl, wasmBin);
|
|
1044
|
+
}
|
|
1045
|
+
let zkeyBin = this.zkeyCache.get(zkeyUrl);
|
|
1046
|
+
if (!zkeyBin) {
|
|
1047
|
+
const r = await fetch(zkeyUrl);
|
|
1048
|
+
if (!r.ok) throw new Error(`Failed to fetch zkey at ${zkeyUrl}: ${r.status}`);
|
|
1049
|
+
const buf = await r.arrayBuffer();
|
|
1050
|
+
zkeyBin = new Uint8Array(buf);
|
|
1051
|
+
if (this.zkeyHash) {
|
|
1052
|
+
this.verifyHash(zkeyBin, this.zkeyHash, "proving_key.zkey");
|
|
1053
|
+
}
|
|
1054
|
+
this.zkeyCache.set(zkeyUrl, zkeyBin);
|
|
1055
|
+
}
|
|
1056
|
+
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
|
|
1057
|
+
inputs,
|
|
1058
|
+
wasmBin,
|
|
1059
|
+
zkeyBin
|
|
1060
|
+
);
|
|
1061
|
+
return {
|
|
1062
|
+
proof,
|
|
1063
|
+
publicSignals
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
async verify(proof, publicSignals, urlOverride) {
|
|
1067
|
+
const vkeyUrl = urlOverride ?? this.vkeyUrl;
|
|
1068
|
+
if (!vkeyUrl) throw new Error("`vkeyUrl` is required to verify a proof");
|
|
1069
|
+
let vk = this.vkeyCache.get(vkeyUrl);
|
|
1070
|
+
if (!vk) {
|
|
1071
|
+
const r = await fetch(vkeyUrl);
|
|
1072
|
+
if (!r.ok) throw new Error(`Failed to fetch vkey at ${vkeyUrl}: ${r.status}`);
|
|
1073
|
+
const vkeyText = await r.text();
|
|
1074
|
+
if (this.vkeyHash) {
|
|
1075
|
+
this.verifyHash(vkeyText, this.vkeyHash, "verification_key.json");
|
|
1076
|
+
}
|
|
1077
|
+
vk = JSON.parse(vkeyText);
|
|
1078
|
+
this.vkeyCache.set(vkeyUrl, vk);
|
|
1079
|
+
}
|
|
1080
|
+
return snarkjs.groth16.verify(vk, publicSignals, proof);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
var VoteStatus = /* @__PURE__ */ ((VoteStatus2) => {
|
|
1085
|
+
VoteStatus2["Pending"] = "pending";
|
|
1086
|
+
VoteStatus2["Verified"] = "verified";
|
|
1087
|
+
VoteStatus2["Aggregated"] = "aggregated";
|
|
1088
|
+
VoteStatus2["Processed"] = "processed";
|
|
1089
|
+
VoteStatus2["Settled"] = "settled";
|
|
1090
|
+
VoteStatus2["Error"] = "error";
|
|
1091
|
+
return VoteStatus2;
|
|
1092
|
+
})(VoteStatus || {});
|
|
1093
|
+
|
|
1094
|
+
class VoteOrchestrationService {
|
|
1095
|
+
constructor(apiService, getCrypto, signer, censusProviders = {}) {
|
|
1096
|
+
this.apiService = apiService;
|
|
1097
|
+
this.getCrypto = getCrypto;
|
|
1098
|
+
this.signer = signer;
|
|
1099
|
+
this.censusProviders = censusProviders;
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Submit a vote with simplified configuration
|
|
1103
|
+
* This method handles all the complex orchestration internally:
|
|
1104
|
+
* - Fetches process information and encryption keys
|
|
1105
|
+
* - Gets census proof (Merkle or CSP)
|
|
1106
|
+
* - Generates cryptographic proofs
|
|
1107
|
+
* - Signs and submits the vote
|
|
1108
|
+
*
|
|
1109
|
+
* @param config - Simplified vote configuration
|
|
1110
|
+
* @returns Promise resolving to vote submission result
|
|
1111
|
+
*/
|
|
1112
|
+
async submitVote(config) {
|
|
1113
|
+
const process = await this.apiService.sequencer.getProcess(config.processId);
|
|
1114
|
+
if (!process.isAcceptingVotes) {
|
|
1115
|
+
throw new Error("Process is not currently accepting votes");
|
|
1116
|
+
}
|
|
1117
|
+
const voterAddress = await this.signer.getAddress();
|
|
1118
|
+
const censusProof = await this.getCensusProof(
|
|
1119
|
+
process.census.censusOrigin,
|
|
1120
|
+
process.census.censusRoot,
|
|
1121
|
+
voterAddress,
|
|
1122
|
+
config.processId
|
|
1123
|
+
);
|
|
1124
|
+
const { voteId, cryptoOutput, circomInputs } = await this.generateVoteProofInputs(
|
|
1125
|
+
config.processId,
|
|
1126
|
+
voterAddress,
|
|
1127
|
+
process.encryptionKey,
|
|
1128
|
+
process.ballotMode,
|
|
1129
|
+
config.choices,
|
|
1130
|
+
censusProof.weight,
|
|
1131
|
+
config.randomness
|
|
1132
|
+
);
|
|
1133
|
+
const { proof } = await this.generateZkProof(circomInputs);
|
|
1134
|
+
const signature = await this.signVote(voteId);
|
|
1135
|
+
await this.submitVoteRequest({
|
|
1136
|
+
processId: config.processId,
|
|
1137
|
+
censusProof,
|
|
1138
|
+
ballot: cryptoOutput.ballot,
|
|
1139
|
+
ballotProof: proof,
|
|
1140
|
+
ballotInputsHash: cryptoOutput.ballotInputsHash,
|
|
1141
|
+
address: voterAddress,
|
|
1142
|
+
signature,
|
|
1143
|
+
voteId
|
|
1144
|
+
});
|
|
1145
|
+
const status = await this.apiService.sequencer.getVoteStatus(config.processId, voteId);
|
|
1146
|
+
return {
|
|
1147
|
+
voteId,
|
|
1148
|
+
signature,
|
|
1149
|
+
voterAddress,
|
|
1150
|
+
processId: config.processId,
|
|
1151
|
+
status: status.status
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Get the status of a submitted vote
|
|
1156
|
+
*
|
|
1157
|
+
* @param processId - The process ID
|
|
1158
|
+
* @param voteId - The vote ID
|
|
1159
|
+
* @returns Promise resolving to vote status information
|
|
1160
|
+
*/
|
|
1161
|
+
async getVoteStatus(processId, voteId) {
|
|
1162
|
+
const status = await this.apiService.sequencer.getVoteStatus(processId, voteId);
|
|
1163
|
+
return {
|
|
1164
|
+
voteId,
|
|
1165
|
+
status: status.status,
|
|
1166
|
+
processId
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Check if an address has voted in a process
|
|
1171
|
+
*
|
|
1172
|
+
* @param processId - The process ID
|
|
1173
|
+
* @param address - The voter's address
|
|
1174
|
+
* @returns Promise resolving to boolean indicating if the address has voted
|
|
1175
|
+
*/
|
|
1176
|
+
async hasAddressVoted(processId, address) {
|
|
1177
|
+
return this.apiService.sequencer.hasAddressVoted(processId, address);
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Wait for a vote to reach a specific status
|
|
1181
|
+
*
|
|
1182
|
+
* @param processId - The process ID
|
|
1183
|
+
* @param voteId - The vote ID
|
|
1184
|
+
* @param targetStatus - The target status to wait for (default: "settled")
|
|
1185
|
+
* @param timeoutMs - Maximum time to wait in milliseconds (default: 300000 = 5 minutes)
|
|
1186
|
+
* @param pollIntervalMs - Polling interval in milliseconds (default: 5000 = 5 seconds)
|
|
1187
|
+
* @returns Promise resolving to final vote status
|
|
1188
|
+
*/
|
|
1189
|
+
async waitForVoteStatus(processId, voteId, targetStatus = VoteStatus.Settled, timeoutMs = 3e5, pollIntervalMs = 5e3) {
|
|
1190
|
+
const startTime = Date.now();
|
|
1191
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
1192
|
+
const statusInfo = await this.getVoteStatus(processId, voteId);
|
|
1193
|
+
if (statusInfo.status === targetStatus || statusInfo.status === VoteStatus.Error) {
|
|
1194
|
+
return statusInfo;
|
|
1195
|
+
}
|
|
1196
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
1197
|
+
}
|
|
1198
|
+
throw new Error(`Vote did not reach status ${targetStatus} within ${timeoutMs}ms`);
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Get census proof based on census origin type
|
|
1202
|
+
*/
|
|
1203
|
+
async getCensusProof(censusOrigin, censusRoot, voterAddress, processId) {
|
|
1204
|
+
if (censusOrigin === CensusOrigin.CensusOriginMerkleTree) {
|
|
1205
|
+
if (this.censusProviders.merkle) {
|
|
1206
|
+
const proof = await this.censusProviders.merkle({
|
|
1207
|
+
censusRoot,
|
|
1208
|
+
address: voterAddress
|
|
1209
|
+
});
|
|
1210
|
+
assertMerkleCensusProof(proof);
|
|
1211
|
+
return proof;
|
|
1212
|
+
} else {
|
|
1213
|
+
const proof = await this.apiService.census.getCensusProof(censusRoot, voterAddress);
|
|
1214
|
+
assertMerkleCensusProof(proof);
|
|
1215
|
+
return proof;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
if (censusOrigin === CensusOrigin.CensusOriginCSP) {
|
|
1219
|
+
if (!this.censusProviders.csp) {
|
|
1220
|
+
throw new Error(
|
|
1221
|
+
"CSP voting requires a CSP census proof provider. Pass one via VoteOrchestrationService(..., { csp: yourFn })."
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
const proof = await this.censusProviders.csp({
|
|
1225
|
+
processId,
|
|
1226
|
+
address: voterAddress
|
|
1227
|
+
});
|
|
1228
|
+
assertCSPCensusProof(proof);
|
|
1229
|
+
return proof;
|
|
1230
|
+
}
|
|
1231
|
+
throw new Error(`Unsupported census origin: ${censusOrigin}`);
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Generate vote proof inputs using DavinciCrypto
|
|
1235
|
+
*/
|
|
1236
|
+
async generateVoteProofInputs(processId, voterAddress, encryptionKey, ballotMode, choices, weight, customRandomness) {
|
|
1237
|
+
const crypto = await this.getCrypto();
|
|
1238
|
+
this.validateChoices(choices, ballotMode);
|
|
1239
|
+
const fieldValues = choices.map((choice) => choice.toString());
|
|
1240
|
+
const inputs = {
|
|
1241
|
+
address: voterAddress.replace(/^0x/, ""),
|
|
1242
|
+
processID: processId.replace(/^0x/, ""),
|
|
1243
|
+
encryptionKey: [encryptionKey.x, encryptionKey.y],
|
|
1244
|
+
ballotMode,
|
|
1245
|
+
weight,
|
|
1246
|
+
fieldValues
|
|
1247
|
+
};
|
|
1248
|
+
if (customRandomness) {
|
|
1249
|
+
const hexRandomness = customRandomness.startsWith("0x") ? customRandomness : "0x" + customRandomness;
|
|
1250
|
+
const k = BigInt(hexRandomness).toString();
|
|
1251
|
+
inputs.k = k;
|
|
1252
|
+
}
|
|
1253
|
+
const cryptoOutput = await crypto.proofInputs(inputs);
|
|
1254
|
+
return {
|
|
1255
|
+
voteId: cryptoOutput.voteId,
|
|
1256
|
+
cryptoOutput,
|
|
1257
|
+
circomInputs: cryptoOutput.circomInputs
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Validate user choices based on ballot mode
|
|
1262
|
+
*/
|
|
1263
|
+
validateChoices(choices, ballotMode) {
|
|
1264
|
+
const maxValue = parseInt(ballotMode.maxValue);
|
|
1265
|
+
const minValue = parseInt(ballotMode.minValue);
|
|
1266
|
+
for (let i = 0; i < choices.length; i++) {
|
|
1267
|
+
const choice = choices[i];
|
|
1268
|
+
if (choice < minValue || choice > maxValue) {
|
|
1269
|
+
throw new Error(`Choice ${choice} is out of range [${minValue}, ${maxValue}]`);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Generate zk-SNARK proof using CircomProof
|
|
1275
|
+
*/
|
|
1276
|
+
async generateZkProof(circomInputs) {
|
|
1277
|
+
const info = await this.apiService.sequencer.getInfo();
|
|
1278
|
+
const circomProof = new CircomProof({
|
|
1279
|
+
wasmUrl: info.circuitUrl,
|
|
1280
|
+
zkeyUrl: info.provingKeyUrl,
|
|
1281
|
+
vkeyUrl: info.verificationKeyUrl
|
|
1282
|
+
});
|
|
1283
|
+
const { proof, publicSignals } = await circomProof.generate(circomInputs);
|
|
1284
|
+
const isValid = await circomProof.verify(proof, publicSignals);
|
|
1285
|
+
if (!isValid) {
|
|
1286
|
+
throw new Error("Generated proof is invalid");
|
|
1287
|
+
}
|
|
1288
|
+
return { proof, publicSignals };
|
|
1289
|
+
}
|
|
1290
|
+
hexToBytes(hex) {
|
|
1291
|
+
const clean = hex.replace(/^0x/, "");
|
|
1292
|
+
if (clean.length % 2) throw new Error("Invalid hex length");
|
|
1293
|
+
const out = new Uint8Array(clean.length / 2);
|
|
1294
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.substr(i * 2, 2), 16);
|
|
1295
|
+
return out;
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Sign the vote using the signer
|
|
1299
|
+
*/
|
|
1300
|
+
async signVote(voteId) {
|
|
1301
|
+
return this.signer.signMessage(this.hexToBytes(voteId));
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Submit the vote request to the sequencer
|
|
1305
|
+
*/
|
|
1306
|
+
async submitVoteRequest(voteRequest) {
|
|
1307
|
+
const ballotProof = {
|
|
1308
|
+
pi_a: voteRequest.ballotProof.pi_a,
|
|
1309
|
+
pi_b: voteRequest.ballotProof.pi_b,
|
|
1310
|
+
pi_c: voteRequest.ballotProof.pi_c,
|
|
1311
|
+
protocol: voteRequest.ballotProof.protocol
|
|
1312
|
+
};
|
|
1313
|
+
const request = {
|
|
1314
|
+
...voteRequest,
|
|
1315
|
+
ballotProof
|
|
1316
|
+
};
|
|
1317
|
+
await this.apiService.sequencer.submitVote(request);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
class OrganizationRegistryService extends SmartContractService {
|
|
1322
|
+
constructor(contractAddress, runner) {
|
|
1323
|
+
super();
|
|
1324
|
+
this.contract = davinciContracts.OrganizationRegistry__factory.connect(
|
|
1325
|
+
contractAddress,
|
|
1326
|
+
runner
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
// ─── READ OPERATIONS ───────────────────────────────────────────────────────
|
|
1330
|
+
async getOrganization(id) {
|
|
1331
|
+
const { name, metadataURI } = await this.contract.getOrganization(id);
|
|
1332
|
+
return { name, metadataURI };
|
|
1333
|
+
}
|
|
1334
|
+
async existsOrganization(id) {
|
|
1335
|
+
return this.contract.exists(id);
|
|
1336
|
+
}
|
|
1337
|
+
async isAdministrator(id, address) {
|
|
1338
|
+
return this.contract.isAdministrator(id, address);
|
|
1339
|
+
}
|
|
1340
|
+
async getOrganizationCount() {
|
|
1341
|
+
const count = await this.contract.organizationCount();
|
|
1342
|
+
return Number(count);
|
|
1343
|
+
}
|
|
1344
|
+
// ─── WRITE OPERATIONS ──────────────────────────────────────────────────────
|
|
1345
|
+
createOrganization(name, metadataURI, administrators) {
|
|
1346
|
+
return this.sendTx(
|
|
1347
|
+
this.contract.createOrganization(name, metadataURI, administrators).catch((e) => {
|
|
1348
|
+
throw new OrganizationCreateError(e.message, "create");
|
|
1349
|
+
}),
|
|
1350
|
+
async () => ({ success: true })
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
updateOrganization(id, name, metadataURI) {
|
|
1354
|
+
return this.sendTx(
|
|
1355
|
+
this.contract.updateOrganization(id, name, metadataURI).catch((e) => {
|
|
1356
|
+
throw new OrganizationUpdateError(e.message, "update");
|
|
1357
|
+
}),
|
|
1358
|
+
async () => ({ success: true })
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
addAdministrator(id, administrator) {
|
|
1362
|
+
return this.sendTx(
|
|
1363
|
+
this.contract.addAdministrator(id, administrator).catch((e) => {
|
|
1364
|
+
throw new OrganizationAdministratorError(e.message, "addAdministrator");
|
|
1365
|
+
}),
|
|
1366
|
+
async () => ({ success: true })
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
removeAdministrator(id, administrator) {
|
|
1370
|
+
return this.sendTx(
|
|
1371
|
+
this.contract.removeAdministrator(id, administrator).catch((e) => {
|
|
1372
|
+
throw new OrganizationAdministratorError(e.message, "removeAdministrator");
|
|
1373
|
+
}),
|
|
1374
|
+
async () => ({ success: true })
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
deleteOrganization(id) {
|
|
1378
|
+
return this.sendTx(
|
|
1379
|
+
this.contract.deleteOrganization(id).catch((e) => {
|
|
1380
|
+
throw new OrganizationDeleteError(e.message, "delete");
|
|
1381
|
+
}),
|
|
1382
|
+
async () => ({ success: true })
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
// ─── EVENT LISTENERS ───────────────────────────────────────────────────────
|
|
1386
|
+
onOrganizationCreated(cb) {
|
|
1387
|
+
this.contract.on(
|
|
1388
|
+
this.contract.filters.OrganizationCreated(),
|
|
1389
|
+
this.normalizeListener(cb)
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
onOrganizationUpdated(cb) {
|
|
1393
|
+
this.contract.on(
|
|
1394
|
+
this.contract.filters.OrganizationUpdated(),
|
|
1395
|
+
this.normalizeListener(cb)
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
onAdministratorAdded(cb) {
|
|
1399
|
+
this.contract.on(
|
|
1400
|
+
this.contract.filters.AdministratorAdded(),
|
|
1401
|
+
this.normalizeListener(cb)
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
onAdministratorRemoved(cb) {
|
|
1405
|
+
this.contract.on(
|
|
1406
|
+
this.contract.filters.AdministratorRemoved(),
|
|
1407
|
+
this.normalizeListener(cb)
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
removeAllListeners() {
|
|
1411
|
+
this.contract.removeAllListeners();
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
class DavinciCrypto {
|
|
1416
|
+
constructor(opts) {
|
|
1417
|
+
this.initialized = false;
|
|
1418
|
+
const { wasmExecUrl, wasmUrl, initTimeoutMs, wasmExecHash, wasmHash } = opts;
|
|
1419
|
+
if (!wasmExecUrl) throw new Error("`wasmExecUrl` is required");
|
|
1420
|
+
if (!wasmUrl) throw new Error("`wasmUrl` is required");
|
|
1421
|
+
this.wasmExecUrl = wasmExecUrl;
|
|
1422
|
+
this.wasmUrl = wasmUrl;
|
|
1423
|
+
this.initTimeoutMs = initTimeoutMs ?? 5e3;
|
|
1424
|
+
this.wasmExecHash = wasmExecHash;
|
|
1425
|
+
this.wasmHash = wasmHash;
|
|
1426
|
+
}
|
|
1427
|
+
static {
|
|
1428
|
+
// Cache for wasm files
|
|
1429
|
+
this.wasmExecCache = /* @__PURE__ */ new Map();
|
|
1430
|
+
}
|
|
1431
|
+
static {
|
|
1432
|
+
this.wasmBinaryCache = /* @__PURE__ */ new Map();
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Computes SHA-256 hash of the given data and compares it with the expected hash.
|
|
1436
|
+
* @param data - The data to hash (string or ArrayBuffer)
|
|
1437
|
+
* @param expectedHash - The expected SHA-256 hash in hexadecimal format
|
|
1438
|
+
* @param filename - The filename for error reporting
|
|
1439
|
+
* @throws Error if the computed hash doesn't match the expected hash
|
|
1440
|
+
*/
|
|
1441
|
+
verifyHash(data, expectedHash, filename) {
|
|
1442
|
+
let bytes;
|
|
1443
|
+
if (typeof data === "string") {
|
|
1444
|
+
bytes = new TextEncoder().encode(data);
|
|
1445
|
+
} else {
|
|
1446
|
+
bytes = new Uint8Array(data);
|
|
1447
|
+
}
|
|
1448
|
+
const computedHash = ethers.sha256(bytes).slice(2);
|
|
1449
|
+
if (computedHash.toLowerCase() !== expectedHash.toLowerCase()) {
|
|
1450
|
+
throw new Error(
|
|
1451
|
+
`Hash verification failed for ${filename}. Expected: ${expectedHash.toLowerCase()}, Computed: ${computedHash.toLowerCase()}`
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Must be awaited before calling `proofInputs()`.
|
|
1457
|
+
* Safe to call multiple times.
|
|
1458
|
+
*/
|
|
1459
|
+
async init() {
|
|
1460
|
+
if (this.initialized) return;
|
|
1461
|
+
let shimCode = DavinciCrypto.wasmExecCache.get(this.wasmExecUrl);
|
|
1462
|
+
if (!shimCode) {
|
|
1463
|
+
const shim = await fetch(this.wasmExecUrl);
|
|
1464
|
+
if (!shim.ok) {
|
|
1465
|
+
throw new Error(`Failed to fetch wasm_exec.js from ${this.wasmExecUrl}`);
|
|
1466
|
+
}
|
|
1467
|
+
shimCode = await shim.text();
|
|
1468
|
+
if (this.wasmExecHash) {
|
|
1469
|
+
this.verifyHash(shimCode, this.wasmExecHash, "wasm_exec.js");
|
|
1470
|
+
}
|
|
1471
|
+
DavinciCrypto.wasmExecCache.set(this.wasmExecUrl, shimCode);
|
|
1472
|
+
}
|
|
1473
|
+
new Function(shimCode)();
|
|
1474
|
+
if (typeof globalThis.Go !== "function") {
|
|
1475
|
+
throw new Error("Global `Go` constructor not found after loading wasm_exec.js");
|
|
1476
|
+
}
|
|
1477
|
+
this.go = new globalThis.Go();
|
|
1478
|
+
let bytes = DavinciCrypto.wasmBinaryCache.get(this.wasmUrl);
|
|
1479
|
+
if (!bytes) {
|
|
1480
|
+
const resp = await fetch(this.wasmUrl);
|
|
1481
|
+
if (!resp.ok) {
|
|
1482
|
+
throw new Error(`Failed to fetch ballotproof.wasm from ${this.wasmUrl}`);
|
|
1483
|
+
}
|
|
1484
|
+
bytes = await resp.arrayBuffer();
|
|
1485
|
+
if (this.wasmHash) {
|
|
1486
|
+
this.verifyHash(bytes, this.wasmHash, "davinci_crypto.wasm");
|
|
1487
|
+
}
|
|
1488
|
+
DavinciCrypto.wasmBinaryCache.set(this.wasmUrl, bytes);
|
|
1489
|
+
}
|
|
1490
|
+
const { instance } = await WebAssembly.instantiate(bytes, this.go.importObject);
|
|
1491
|
+
this.go.run(instance).catch(() => {
|
|
1492
|
+
});
|
|
1493
|
+
const deadline = Date.now() + this.initTimeoutMs;
|
|
1494
|
+
while (Date.now() < deadline && !globalThis.DavinciCrypto) {
|
|
1495
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1496
|
+
}
|
|
1497
|
+
if (!globalThis.DavinciCrypto) {
|
|
1498
|
+
throw new Error("`DavinciCrypto` not initialized within timeout");
|
|
1499
|
+
}
|
|
1500
|
+
this.initialized = true;
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Convert your inputs into JSON, hand off to Go/WASM, then parse & return.
|
|
1504
|
+
* @throws if called before `await init()`, or if Go returns an error
|
|
1505
|
+
*/
|
|
1506
|
+
async proofInputs(inputs) {
|
|
1507
|
+
if (!this.initialized) {
|
|
1508
|
+
throw new Error("DavinciCrypto not initialized \u2014 call `await init()` first");
|
|
1509
|
+
}
|
|
1510
|
+
const raw = globalThis.DavinciCrypto.proofInputs(JSON.stringify(inputs));
|
|
1511
|
+
if (raw.error) {
|
|
1512
|
+
throw new Error(`Go/WASM proofInputs error: ${raw.error}`);
|
|
1513
|
+
}
|
|
1514
|
+
if (!raw.data) {
|
|
1515
|
+
throw new Error("Go/WASM proofInputs returned no data");
|
|
1516
|
+
}
|
|
1517
|
+
return raw.data;
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Generate a CSP (Credential Service Provider) signature for census proof.
|
|
1521
|
+
* @param censusOrigin - The census origin type (e.g., CensusOrigin.CensusOriginCSP)
|
|
1522
|
+
* @param privKey - The private key in hex format
|
|
1523
|
+
* @param processId - The process ID in hex format
|
|
1524
|
+
* @param address - The address in hex format
|
|
1525
|
+
* @returns The CSP proof as a parsed JSON object
|
|
1526
|
+
* @throws if called before `await init()`, or if Go returns an error
|
|
1527
|
+
*/
|
|
1528
|
+
async cspSign(censusOrigin, privKey, processId, address) {
|
|
1529
|
+
if (!this.initialized) {
|
|
1530
|
+
throw new Error("DavinciCrypto not initialized \u2014 call `await init()` first");
|
|
1531
|
+
}
|
|
1532
|
+
const raw = globalThis.DavinciCrypto.cspSign(censusOrigin, privKey, processId, address);
|
|
1533
|
+
if (raw.error) {
|
|
1534
|
+
throw new Error(`Go/WASM cspSign error: ${raw.error}`);
|
|
1535
|
+
}
|
|
1536
|
+
if (!raw.data) {
|
|
1537
|
+
throw new Error("Go/WASM cspSign returned no data");
|
|
1538
|
+
}
|
|
1539
|
+
return raw.data;
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Verify a CSP (Credential Service Provider) proof.
|
|
1543
|
+
* @param censusOrigin - The census origin type (e.g., CensusOrigin.CensusOriginCSP)
|
|
1544
|
+
* @param root - The census root
|
|
1545
|
+
* @param address - The address
|
|
1546
|
+
* @param processId - The process ID
|
|
1547
|
+
* @param publicKey - The public key
|
|
1548
|
+
* @param signature - The signature
|
|
1549
|
+
* @returns The verification result
|
|
1550
|
+
* @throws if called before `await init()`, or if Go returns an error
|
|
1551
|
+
*/
|
|
1552
|
+
async cspVerify(censusOrigin, root, address, processId, publicKey, signature) {
|
|
1553
|
+
if (!this.initialized) {
|
|
1554
|
+
throw new Error("DavinciCrypto not initialized \u2014 call `await init()` first");
|
|
1555
|
+
}
|
|
1556
|
+
const cspProof = {
|
|
1557
|
+
censusOrigin,
|
|
1558
|
+
root,
|
|
1559
|
+
address,
|
|
1560
|
+
processId,
|
|
1561
|
+
publicKey,
|
|
1562
|
+
signature
|
|
1563
|
+
};
|
|
1564
|
+
const raw = globalThis.DavinciCrypto.cspVerify(JSON.stringify(cspProof));
|
|
1565
|
+
if (raw.error) {
|
|
1566
|
+
throw new Error(`Go/WASM cspVerify error: ${raw.error}`);
|
|
1567
|
+
}
|
|
1568
|
+
if (!raw.data) {
|
|
1569
|
+
throw new Error("Go/WASM cspVerify returned no data");
|
|
1570
|
+
}
|
|
1571
|
+
return raw.data;
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Generate a CSP (Credential Service Provider) census root.
|
|
1575
|
+
* @param censusOrigin - The census origin type (e.g., CensusOrigin.CensusOriginCSP)
|
|
1576
|
+
* @param privKey - The private key in hex format
|
|
1577
|
+
* @returns The census root as a hexadecimal string
|
|
1578
|
+
* @throws if called before `await init()`, or if Go returns an error
|
|
1579
|
+
*/
|
|
1580
|
+
async cspCensusRoot(censusOrigin, privKey) {
|
|
1581
|
+
if (!this.initialized) {
|
|
1582
|
+
throw new Error("DavinciCrypto not initialized \u2014 call `await init()` first");
|
|
1583
|
+
}
|
|
1584
|
+
const raw = globalThis.DavinciCrypto.cspCensusRoot(censusOrigin, privKey);
|
|
1585
|
+
if (raw.error) {
|
|
1586
|
+
throw new Error(`Go/WASM cspCensusRoot error: ${raw.error}`);
|
|
1587
|
+
}
|
|
1588
|
+
if (!raw.data) {
|
|
1589
|
+
throw new Error("Go/WASM cspCensusRoot returned no data");
|
|
1590
|
+
}
|
|
1591
|
+
return raw.data.root;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
class DavinciSDK {
|
|
1596
|
+
constructor(config) {
|
|
1597
|
+
this.initialized = false;
|
|
1598
|
+
const resolvedConfig = resolveConfiguration({
|
|
1599
|
+
environment: config.environment,
|
|
1600
|
+
customUrls: {
|
|
1601
|
+
sequencer: config.sequencerUrl,
|
|
1602
|
+
census: config.censusUrl
|
|
1603
|
+
},
|
|
1604
|
+
customChain: config.chain
|
|
1605
|
+
});
|
|
1606
|
+
this.config = {
|
|
1607
|
+
signer: config.signer,
|
|
1608
|
+
sequencerUrl: config.sequencerUrl ?? resolvedConfig.sequencer,
|
|
1609
|
+
censusUrl: config.censusUrl ?? resolvedConfig.census,
|
|
1610
|
+
chain: config.chain ?? resolvedConfig.chain,
|
|
1611
|
+
contractAddresses: config.contractAddresses || {},
|
|
1612
|
+
useSequencerAddresses: config.useSequencerAddresses || false
|
|
1613
|
+
};
|
|
1614
|
+
this.apiService = new VocdoniApiService({
|
|
1615
|
+
sequencerURL: this.config.sequencerUrl,
|
|
1616
|
+
censusURL: this.config.censusUrl
|
|
1617
|
+
});
|
|
1618
|
+
this.censusProviders = config.censusProviders || {};
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Initialize the SDK and all its components
|
|
1622
|
+
* This must be called before using any SDK functionality
|
|
1623
|
+
*/
|
|
1624
|
+
async init() {
|
|
1625
|
+
if (this.initialized) return;
|
|
1626
|
+
if (this.config.useSequencerAddresses) {
|
|
1627
|
+
await this.updateContractAddressesFromSequencer();
|
|
1628
|
+
}
|
|
1629
|
+
this.initialized = true;
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Get the API service for direct access to sequencer and census APIs
|
|
1633
|
+
*/
|
|
1634
|
+
get api() {
|
|
1635
|
+
return this.apiService;
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Get the process registry service for process management
|
|
1639
|
+
*/
|
|
1640
|
+
get processes() {
|
|
1641
|
+
if (!this._processRegistry) {
|
|
1642
|
+
const processRegistryAddress = this.resolveContractAddress("processRegistry");
|
|
1643
|
+
this._processRegistry = new ProcessRegistryService(processRegistryAddress, this.config.signer);
|
|
1644
|
+
}
|
|
1645
|
+
return this._processRegistry;
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Get the organization registry service for organization management
|
|
1649
|
+
*/
|
|
1650
|
+
get organizations() {
|
|
1651
|
+
if (!this._organizationRegistry) {
|
|
1652
|
+
const organizationRegistryAddress = this.resolveContractAddress("organizationRegistry");
|
|
1653
|
+
this._organizationRegistry = new OrganizationRegistryService(organizationRegistryAddress, this.config.signer);
|
|
1654
|
+
}
|
|
1655
|
+
return this._organizationRegistry;
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* Get or initialize the DavinciCrypto service for cryptographic operations
|
|
1659
|
+
*/
|
|
1660
|
+
async getCrypto() {
|
|
1661
|
+
if (!this.davinciCrypto) {
|
|
1662
|
+
const info = await this.apiService.sequencer.getInfo();
|
|
1663
|
+
this.davinciCrypto = new DavinciCrypto({
|
|
1664
|
+
wasmExecUrl: info.ballotProofWasmHelperExecJsUrl,
|
|
1665
|
+
wasmUrl: info.ballotProofWasmHelperUrl
|
|
1666
|
+
});
|
|
1667
|
+
await this.davinciCrypto.init();
|
|
1668
|
+
}
|
|
1669
|
+
return this.davinciCrypto;
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Get the process orchestration service for simplified process creation
|
|
1673
|
+
*/
|
|
1674
|
+
get processOrchestrator() {
|
|
1675
|
+
if (!this._processOrchestrator) {
|
|
1676
|
+
this._processOrchestrator = new ProcessOrchestrationService(
|
|
1677
|
+
this.processes,
|
|
1678
|
+
this.apiService,
|
|
1679
|
+
this.organizations,
|
|
1680
|
+
() => this.getCrypto(),
|
|
1681
|
+
this.config.signer
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
return this._processOrchestrator;
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Get the vote orchestration service for simplified voting
|
|
1688
|
+
*/
|
|
1689
|
+
get voteOrchestrator() {
|
|
1690
|
+
if (!this._voteOrchestrator) {
|
|
1691
|
+
this._voteOrchestrator = new VoteOrchestrationService(
|
|
1692
|
+
this.apiService,
|
|
1693
|
+
() => this.getCrypto(),
|
|
1694
|
+
this.config.signer,
|
|
1695
|
+
this.censusProviders
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
return this._voteOrchestrator;
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Gets user-friendly process information from the blockchain.
|
|
1702
|
+
* This method fetches raw contract data and transforms it into a user-friendly format
|
|
1703
|
+
* that matches the ProcessConfig interface used for creation, plus additional runtime data.
|
|
1704
|
+
*
|
|
1705
|
+
* @param processId - The process ID to fetch
|
|
1706
|
+
* @returns Promise resolving to user-friendly process information
|
|
1707
|
+
*
|
|
1708
|
+
* @example
|
|
1709
|
+
* ```typescript
|
|
1710
|
+
* const processInfo = await sdk.getProcess("0x1234567890abcdef...");
|
|
1711
|
+
*
|
|
1712
|
+
* // Access the same fields as ProcessConfig
|
|
1713
|
+
* console.log("Title:", processInfo.title);
|
|
1714
|
+
* console.log("Description:", processInfo.description);
|
|
1715
|
+
* console.log("Questions:", processInfo.questions);
|
|
1716
|
+
* console.log("Census size:", processInfo.census.size);
|
|
1717
|
+
* console.log("Ballot config:", processInfo.ballot);
|
|
1718
|
+
*
|
|
1719
|
+
* // Plus additional runtime information
|
|
1720
|
+
* console.log("Status:", processInfo.status);
|
|
1721
|
+
* console.log("Creator:", processInfo.creator);
|
|
1722
|
+
* console.log("Start date:", processInfo.startDate);
|
|
1723
|
+
* console.log("End date:", processInfo.endDate);
|
|
1724
|
+
* console.log("Duration:", processInfo.duration, "seconds");
|
|
1725
|
+
* console.log("Time remaining:", processInfo.timeRemaining, "seconds");
|
|
1726
|
+
*
|
|
1727
|
+
* // Access raw contract data if needed
|
|
1728
|
+
* console.log("Raw data:", processInfo.raw);
|
|
1729
|
+
* ```
|
|
1730
|
+
*/
|
|
1731
|
+
async getProcess(processId) {
|
|
1732
|
+
if (!this.initialized) {
|
|
1733
|
+
throw new Error("SDK must be initialized before getting processes. Call sdk.init() first.");
|
|
1734
|
+
}
|
|
1735
|
+
return this.processOrchestrator.getProcess(processId);
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Creates a complete voting process with minimal configuration.
|
|
1739
|
+
* This is the ultra-easy method for end users that handles all the complex orchestration internally.
|
|
1740
|
+
*
|
|
1741
|
+
* The method automatically:
|
|
1742
|
+
* - Gets encryption keys and initial state root from the sequencer
|
|
1743
|
+
* - Handles process creation signatures
|
|
1744
|
+
* - Coordinates between sequencer API and on-chain contract calls
|
|
1745
|
+
* - Creates and pushes metadata
|
|
1746
|
+
* - Submits the on-chain transaction
|
|
1747
|
+
*
|
|
1748
|
+
* @param config - Simplified process configuration
|
|
1749
|
+
* @returns Promise resolving to the process creation result
|
|
1750
|
+
*
|
|
1751
|
+
* @example
|
|
1752
|
+
* ```typescript
|
|
1753
|
+
* // Option 1: Using duration (traditional approach)
|
|
1754
|
+
* const result1 = await sdk.createProcess({
|
|
1755
|
+
* title: "My Election",
|
|
1756
|
+
* description: "A simple election",
|
|
1757
|
+
* census: {
|
|
1758
|
+
* type: CensusOrigin.CensusOriginMerkleTree,
|
|
1759
|
+
* root: "0x1234...",
|
|
1760
|
+
* size: 100,
|
|
1761
|
+
* uri: "ipfs://your-census-uri"
|
|
1762
|
+
* },
|
|
1763
|
+
* ballot: {
|
|
1764
|
+
* numFields: 2,
|
|
1765
|
+
* maxValue: "3",
|
|
1766
|
+
* minValue: "0",
|
|
1767
|
+
* uniqueValues: false,
|
|
1768
|
+
* costFromWeight: false,
|
|
1769
|
+
* costExponent: 10000,
|
|
1770
|
+
* maxValueSum: "6",
|
|
1771
|
+
* minValueSum: "0"
|
|
1772
|
+
* },
|
|
1773
|
+
* timing: {
|
|
1774
|
+
* startDate: new Date("2024-12-01T10:00:00Z"),
|
|
1775
|
+
* duration: 3600 * 24
|
|
1776
|
+
* },
|
|
1777
|
+
* questions: [
|
|
1778
|
+
* {
|
|
1779
|
+
* title: "What is your favorite color?",
|
|
1780
|
+
* choices: [
|
|
1781
|
+
* { title: "Red", value: 0 },
|
|
1782
|
+
* { title: "Blue", value: 1 }
|
|
1783
|
+
* ]
|
|
1784
|
+
* }
|
|
1785
|
+
* ]
|
|
1786
|
+
* });
|
|
1787
|
+
*
|
|
1788
|
+
* // Option 2: Using start and end dates
|
|
1789
|
+
* const result2 = await sdk.createProcess({
|
|
1790
|
+
* title: "Weekend Vote",
|
|
1791
|
+
* timing: {
|
|
1792
|
+
* startDate: "2024-12-07T09:00:00Z",
|
|
1793
|
+
* endDate: "2024-12-08T18:00:00Z"
|
|
1794
|
+
* }
|
|
1795
|
+
* });
|
|
1796
|
+
* ```
|
|
1797
|
+
*/
|
|
1798
|
+
async createProcess(config) {
|
|
1799
|
+
if (!this.initialized) {
|
|
1800
|
+
throw new Error("SDK must be initialized before creating processes. Call sdk.init() first.");
|
|
1801
|
+
}
|
|
1802
|
+
return this.processOrchestrator.createProcess(config);
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Submit a vote with simplified configuration.
|
|
1806
|
+
* This is the ultra-easy method for end users that handles all the complex voting workflow internally.
|
|
1807
|
+
*
|
|
1808
|
+
* The method automatically:
|
|
1809
|
+
* - Fetches process information and validates voting is allowed
|
|
1810
|
+
* - Gets census proof (Merkle tree based)
|
|
1811
|
+
* - Generates cryptographic proofs and encrypts the vote
|
|
1812
|
+
* - Signs and submits the vote to the sequencer
|
|
1813
|
+
*
|
|
1814
|
+
* @param config - Simplified vote configuration
|
|
1815
|
+
* @returns Promise resolving to vote submission result
|
|
1816
|
+
*
|
|
1817
|
+
* @example
|
|
1818
|
+
* ```typescript
|
|
1819
|
+
* // Submit a vote with voter's private key
|
|
1820
|
+
* const voteResult = await sdk.submitVote({
|
|
1821
|
+
* processId: "0x1234567890abcdef...",
|
|
1822
|
+
* choices: [1, 0], // Vote for option 1 in question 1, option 0 in question 2
|
|
1823
|
+
* voterKey: "0x1234567890abcdef..." // Voter's private key
|
|
1824
|
+
* });
|
|
1825
|
+
*
|
|
1826
|
+
* console.log("Vote ID:", voteResult.voteId);
|
|
1827
|
+
* console.log("Status:", voteResult.status);
|
|
1828
|
+
*
|
|
1829
|
+
* // Submit a vote with a Wallet instance
|
|
1830
|
+
* import { Wallet } from "ethers";
|
|
1831
|
+
* const voterWallet = new Wallet("0x...");
|
|
1832
|
+
*
|
|
1833
|
+
* const voteResult2 = await sdk.submitVote({
|
|
1834
|
+
* processId: "0x1234567890abcdef...",
|
|
1835
|
+
* choices: [2], // Single question vote
|
|
1836
|
+
* voterKey: voterWallet
|
|
1837
|
+
* });
|
|
1838
|
+
* ```
|
|
1839
|
+
*/
|
|
1840
|
+
async submitVote(config) {
|
|
1841
|
+
if (!this.initialized) {
|
|
1842
|
+
throw new Error("SDK must be initialized before submitting votes. Call sdk.init() first.");
|
|
1843
|
+
}
|
|
1844
|
+
return this.voteOrchestrator.submitVote(config);
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Get the status of a submitted vote.
|
|
1848
|
+
*
|
|
1849
|
+
* @param processId - The process ID
|
|
1850
|
+
* @param voteId - The vote ID returned from submitVote()
|
|
1851
|
+
* @returns Promise resolving to vote status information
|
|
1852
|
+
*
|
|
1853
|
+
* @example
|
|
1854
|
+
* ```typescript
|
|
1855
|
+
* const statusInfo = await sdk.getVoteStatus(processId, voteId);
|
|
1856
|
+
* console.log("Vote status:", statusInfo.status);
|
|
1857
|
+
* // Possible statuses: "pending", "verified", "aggregated", "processed", "settled", "error"
|
|
1858
|
+
* ```
|
|
1859
|
+
*/
|
|
1860
|
+
async getVoteStatus(processId, voteId) {
|
|
1861
|
+
if (!this.initialized) {
|
|
1862
|
+
throw new Error("SDK must be initialized before getting vote status. Call sdk.init() first.");
|
|
1863
|
+
}
|
|
1864
|
+
return this.voteOrchestrator.getVoteStatus(processId, voteId);
|
|
1865
|
+
}
|
|
1866
|
+
/**
|
|
1867
|
+
* Check if an address has voted in a process.
|
|
1868
|
+
*
|
|
1869
|
+
* @param processId - The process ID
|
|
1870
|
+
* @param address - The voter's address
|
|
1871
|
+
* @returns Promise resolving to boolean indicating if the address has voted
|
|
1872
|
+
*
|
|
1873
|
+
* @example
|
|
1874
|
+
* ```typescript
|
|
1875
|
+
* const hasVoted = await sdk.hasAddressVoted(processId, "0x1234567890abcdef...");
|
|
1876
|
+
* if (hasVoted) {
|
|
1877
|
+
* console.log("This address has already voted");
|
|
1878
|
+
* }
|
|
1879
|
+
* ```
|
|
1880
|
+
*/
|
|
1881
|
+
async hasAddressVoted(processId, address) {
|
|
1882
|
+
if (!this.initialized) {
|
|
1883
|
+
throw new Error("SDK must be initialized before checking vote status. Call sdk.init() first.");
|
|
1884
|
+
}
|
|
1885
|
+
return this.voteOrchestrator.hasAddressVoted(processId, address);
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* Wait for a vote to reach a specific status.
|
|
1889
|
+
* Useful for waiting for vote confirmation and processing.
|
|
1890
|
+
*
|
|
1891
|
+
* @param processId - The process ID
|
|
1892
|
+
* @param voteId - The vote ID
|
|
1893
|
+
* @param targetStatus - The target status to wait for (default: "settled")
|
|
1894
|
+
* @param timeoutMs - Maximum time to wait in milliseconds (default: 300000 = 5 minutes)
|
|
1895
|
+
* @param pollIntervalMs - Polling interval in milliseconds (default: 5000 = 5 seconds)
|
|
1896
|
+
* @returns Promise resolving to final vote status
|
|
1897
|
+
*
|
|
1898
|
+
* @example
|
|
1899
|
+
* ```typescript
|
|
1900
|
+
* // Submit vote and wait for it to be settled
|
|
1901
|
+
* const voteResult = await sdk.submitVote({
|
|
1902
|
+
* processId: "0x1234567890abcdef...",
|
|
1903
|
+
* choices: [1],
|
|
1904
|
+
* voterKey: "0x..."
|
|
1905
|
+
* });
|
|
1906
|
+
*
|
|
1907
|
+
* // Wait for the vote to be fully processed
|
|
1908
|
+
* const finalStatus = await sdk.waitForVoteStatus(
|
|
1909
|
+
* voteResult.processId,
|
|
1910
|
+
* voteResult.voteId,
|
|
1911
|
+
* "settled", // Wait until vote is settled
|
|
1912
|
+
* 300000, // 5 minute timeout
|
|
1913
|
+
* 5000 // Check every 5 seconds
|
|
1914
|
+
* );
|
|
1915
|
+
*
|
|
1916
|
+
* console.log("Vote final status:", finalStatus.status);
|
|
1917
|
+
* ```
|
|
1918
|
+
*/
|
|
1919
|
+
async waitForVoteStatus(processId, voteId, targetStatus = VoteStatus.Settled, timeoutMs = 3e5, pollIntervalMs = 5e3) {
|
|
1920
|
+
if (!this.initialized) {
|
|
1921
|
+
throw new Error("SDK must be initialized before waiting for vote status. Call sdk.init() first.");
|
|
1922
|
+
}
|
|
1923
|
+
return this.voteOrchestrator.waitForVoteStatus(processId, voteId, targetStatus, timeoutMs, pollIntervalMs);
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Resolve contract address based on configuration priority:
|
|
1927
|
+
* 1. If useSequencerAddresses is true: addresses from sequencer (highest priority)
|
|
1928
|
+
* 2. Custom addresses from config (if provided by user)
|
|
1929
|
+
* 3. Default deployed addresses from npm package
|
|
1930
|
+
*/
|
|
1931
|
+
resolveContractAddress(contractName) {
|
|
1932
|
+
if (this.config.useSequencerAddresses) {
|
|
1933
|
+
return this.getDefaultContractAddress(contractName);
|
|
1934
|
+
}
|
|
1935
|
+
const customAddress = this.config.contractAddresses[contractName];
|
|
1936
|
+
if (customAddress) {
|
|
1937
|
+
return customAddress;
|
|
1938
|
+
}
|
|
1939
|
+
return this.getDefaultContractAddress(contractName);
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Get default contract address from deployed addresses
|
|
1943
|
+
*/
|
|
1944
|
+
getDefaultContractAddress(contractName) {
|
|
1945
|
+
const chain = this.config.chain;
|
|
1946
|
+
switch (contractName) {
|
|
1947
|
+
case "processRegistry":
|
|
1948
|
+
return deployedAddresses.processRegistry[chain];
|
|
1949
|
+
case "organizationRegistry":
|
|
1950
|
+
return deployedAddresses.organizationRegistry[chain];
|
|
1951
|
+
case "stateTransitionVerifier":
|
|
1952
|
+
return deployedAddresses.stateTransitionVerifierGroth16[chain];
|
|
1953
|
+
case "resultsVerifier":
|
|
1954
|
+
return deployedAddresses.resultsVerifierGroth16[chain];
|
|
1955
|
+
case "sequencerRegistry":
|
|
1956
|
+
return deployedAddresses.sequencerRegistry[chain];
|
|
1957
|
+
default:
|
|
1958
|
+
throw new Error(`Unknown contract: ${contractName}`);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Update contract addresses from sequencer info if useSequencerAddresses is enabled
|
|
1963
|
+
* Sequencer addresses have priority over user-provided addresses
|
|
1964
|
+
*/
|
|
1965
|
+
async updateContractAddressesFromSequencer() {
|
|
1966
|
+
try {
|
|
1967
|
+
const info = await this.apiService.sequencer.getInfo();
|
|
1968
|
+
const contracts = info.contracts;
|
|
1969
|
+
if (contracts.process) {
|
|
1970
|
+
this._processRegistry = new ProcessRegistryService(contracts.process, this.config.signer);
|
|
1971
|
+
this.config.contractAddresses.processRegistry = contracts.process;
|
|
1972
|
+
}
|
|
1973
|
+
if (contracts.organization) {
|
|
1974
|
+
this._organizationRegistry = new OrganizationRegistryService(contracts.organization, this.config.signer);
|
|
1975
|
+
this.config.contractAddresses.organizationRegistry = contracts.organization;
|
|
1976
|
+
}
|
|
1977
|
+
if (contracts.stateTransitionVerifier) {
|
|
1978
|
+
this.config.contractAddresses.stateTransitionVerifier = contracts.stateTransitionVerifier;
|
|
1979
|
+
}
|
|
1980
|
+
if (contracts.resultsVerifier) {
|
|
1981
|
+
this.config.contractAddresses.resultsVerifier = contracts.resultsVerifier;
|
|
1982
|
+
}
|
|
1983
|
+
} catch (error) {
|
|
1984
|
+
console.warn("Failed to fetch contract addresses from sequencer, using defaults:", error);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1988
|
+
* Get the current configuration
|
|
1989
|
+
*/
|
|
1990
|
+
getConfig() {
|
|
1991
|
+
return { ...this.config };
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Check if the SDK has been initialized
|
|
1995
|
+
*/
|
|
1996
|
+
isInitialized() {
|
|
1997
|
+
return this.initialized;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
exports.BaseService = BaseService;
|
|
2002
|
+
exports.CensusOrigin = CensusOrigin;
|
|
2003
|
+
exports.CircomProof = CircomProof;
|
|
2004
|
+
exports.ContractServiceError = ContractServiceError;
|
|
2005
|
+
exports.DEFAULT_ENVIRONMENT_URLS = DEFAULT_ENVIRONMENT_URLS;
|
|
2006
|
+
exports.DavinciCrypto = DavinciCrypto;
|
|
2007
|
+
exports.DavinciSDK = DavinciSDK;
|
|
2008
|
+
exports.ElectionMetadataTemplate = ElectionMetadataTemplate;
|
|
2009
|
+
exports.ElectionResultsTypeNames = ElectionResultsTypeNames;
|
|
2010
|
+
exports.OrganizationAdministratorError = OrganizationAdministratorError;
|
|
2011
|
+
exports.OrganizationCreateError = OrganizationCreateError;
|
|
2012
|
+
exports.OrganizationDeleteError = OrganizationDeleteError;
|
|
2013
|
+
exports.OrganizationRegistryService = OrganizationRegistryService;
|
|
2014
|
+
exports.OrganizationUpdateError = OrganizationUpdateError;
|
|
2015
|
+
exports.ProcessCensusError = ProcessCensusError;
|
|
2016
|
+
exports.ProcessCreateError = ProcessCreateError;
|
|
2017
|
+
exports.ProcessDurationError = ProcessDurationError;
|
|
2018
|
+
exports.ProcessOrchestrationService = ProcessOrchestrationService;
|
|
2019
|
+
exports.ProcessRegistryService = ProcessRegistryService;
|
|
2020
|
+
exports.ProcessResultError = ProcessResultError;
|
|
2021
|
+
exports.ProcessStateTransitionError = ProcessStateTransitionError;
|
|
2022
|
+
exports.ProcessStatus = ProcessStatus;
|
|
2023
|
+
exports.ProcessStatusError = ProcessStatusError;
|
|
2024
|
+
exports.SmartContractService = SmartContractService;
|
|
2025
|
+
exports.TxStatus = TxStatus;
|
|
2026
|
+
exports.VocdoniApiService = VocdoniApiService;
|
|
2027
|
+
exports.VocdoniCensusService = VocdoniCensusService;
|
|
2028
|
+
exports.VocdoniSequencerService = VocdoniSequencerService;
|
|
2029
|
+
exports.VoteOrchestrationService = VoteOrchestrationService;
|
|
2030
|
+
exports.VoteStatus = VoteStatus;
|
|
2031
|
+
exports.assertCSPCensusProof = assertCSPCensusProof;
|
|
2032
|
+
exports.assertMerkleCensusProof = assertMerkleCensusProof;
|
|
2033
|
+
exports.createProcessSignatureMessage = createProcessSignatureMessage;
|
|
2034
|
+
exports.deployedAddresses = deployedAddresses;
|
|
2035
|
+
exports.getElectionMetadataTemplate = getElectionMetadataTemplate;
|
|
2036
|
+
exports.getEnvironmentChain = getEnvironmentChain;
|
|
2037
|
+
exports.getEnvironmentConfig = getEnvironmentConfig;
|
|
2038
|
+
exports.getEnvironmentUrls = getEnvironmentUrls;
|
|
2039
|
+
exports.isCSPCensusProof = isCSPCensusProof;
|
|
2040
|
+
exports.isMerkleCensusProof = isMerkleCensusProof;
|
|
2041
|
+
exports.resolveConfiguration = resolveConfiguration;
|
|
2042
|
+
exports.resolveUrls = resolveUrls;
|
|
2043
|
+
exports.signProcessCreation = signProcessCreation;
|
|
2044
|
+
exports.validateProcessId = validateProcessId;
|
|
2045
|
+
//# sourceMappingURL=index.js.map
|