Skip to content

Commit

Permalink
Added import/export functionality for censuses
Browse files Browse the repository at this point in the history
  • Loading branch information
marcvelmer committed Oct 26, 2023
1 parent 8d4a2b7 commit 7cca0a8
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 16 deletions.
78 changes: 78 additions & 0 deletions src/api/census.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ enum CensusAPIMethods {
SIZE = '/censuses/{id}/size',
WEIGHT = '/censuses/{id}/weight',
TYPE = '/censuses/{id}/type',
EXPORT = '/censuses/{id}/export',
IMPORT = '/censuses/{id}/import',
}

interface ICensusCreateResponse {
Expand All @@ -34,6 +36,30 @@ export interface ICensusPublishResponse {
uri: string;
}

export interface ICensusExportResponse {
/**
* The type of the census
*/
type: number;

/**
* The root hash of the census
*/
rootHash: string;

/**
* The data of the census
*/
data: string;

/**
* The max levels of the census
*/
maxLevels: number;
}

export interface ICensusImportResponse {}

export interface ICensusProofResponse {
/**
* The type of the census
Expand Down Expand Up @@ -185,6 +211,58 @@ export abstract class CensusAPI extends API {
.catch(this.isApiError);
}

/**
* Exports the given census identifier
*
* @param {string} url API endpoint URL
* @param {string} authToken Authentication token
* @param {string} censusId The census ID we want to export
* @returns {Promise<ICensusExportResponse>} on success
*/
public static export(url: string, authToken: string, censusId: string): Promise<ICensusExportResponse> {
return axios
.get<ICensusExportResponse>(url + CensusAPIMethods.EXPORT.replace('{id}', censusId), {
headers: {
Authorization: 'Bearer ' + authToken,
},
})
.then((response) => response.data)
.catch(this.isApiError);
}

/**
* Imports data into the given census identifier
*
* @param {string} url API endpoint URL
* @param {string} authToken Authentication token
* @param {string} censusId The census ID we want to export
* @param {number} type The type of the census
* @param {string} rootHash The root hash of the census
* @param {string} data The census data to be imported
* @returns {Promise<void>} on success
*/
public static import(
url: string,
authToken: string,
censusId: string,
type: number,
rootHash: string,
data: string
): Promise<ICensusImportResponse> {
return axios
.post<ICensusImportResponse>(
url + CensusAPIMethods.IMPORT.replace('{id}', censusId),
{ type, rootHash, data },
{
headers: {
Authorization: 'Bearer ' + authToken,
},
}
)
.then((response) => response.data)
.catch(this.isApiError);
}

/**
* Returns the size of a given census
*
Expand Down
4 changes: 2 additions & 2 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ export class VocdoniSDKClient {
if (!election.census.isPublished) {
await this.censusService.createCensus(election.census as PlainCensus | WeightedCensus);
} else if (!election.maxCensusSize && !election.census.size) {
await this.censusService.fetchCensusInfo(election.census.censusId).then((censusInfo) => {
await this.censusService.get(election.census.censusId).then((censusInfo) => {
election.census.size = censusInfo.size;
election.census.weight = censusInfo.weight;
});
Expand Down Expand Up @@ -820,7 +820,7 @@ export class VocdoniSDKClient {
* @returns {Promise<{size: number, weight: bigint}>}
*/
fetchCensusInfo(censusId: string): Promise<{ size: number; weight: bigint; type: CensusType }> {
return this.censusService.fetchCensusInfo(censusId);
return this.censusService.get(censusId);
}

/**
Expand Down
59 changes: 49 additions & 10 deletions src/services/census.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Service, ServiceProperties } from './service';
import { CensusType, ICensusParticipant, PlainCensus, WeightedCensus } from '../types';
import { CensusAPI, ICensusPublishResponse, WalletAPI } from '../api';
import { CensusAPI, ICensusImportResponse, ICensusPublishResponse, WalletAPI } from '../api';
import { Wallet } from '@ethersproject/wallet';
import invariant from 'tiny-invariant';

Expand All @@ -13,7 +13,7 @@ type CensusServiceParameters = ServiceProperties & CensusServiceProperties;

type CensusAuth = {
identifier: string;
wallet: Wallet;
wallet?: Wallet;
};

/**
Expand All @@ -31,6 +31,20 @@ export type CensusProof = {
siblings?: Array<string>;
};

/**
* @typedef CensusImportExport
* @property {number} type
* @property {string} rootHash
* @property {string} data
* @property {number} maxLevels
*/
export type CensusImportExport = {
type: number;
rootHash: string;
data: string;
maxLevels: number;
};

/**
* @typedef CspCensusProof
* @property {string} type
Expand Down Expand Up @@ -63,9 +77,9 @@ export class CensusService extends Service implements CensusServiceProperties {
* Fetches the information of a given census.
*
* @param censusId
* @returns {Promise<{size: number, weight: bigint}>}
* @returns {Promise<{size: number, weight: bigint, type: CensusType}>}
*/
fetchCensusInfo(censusId: string): Promise<{ size: number; weight: bigint; type: CensusType }> {
get(censusId: string): Promise<{ size: number; weight: bigint; type: CensusType }> {
invariant(this.url, 'No URL set');

return Promise.all([
Expand Down Expand Up @@ -105,12 +119,12 @@ export class CensusService extends Service implements CensusServiceProperties {
}));
}

create(censusType: CensusType): Promise<string> {
create(censusType: CensusType): Promise<{ id: string; auth: string }> {
invariant(this.url, 'No URL set');

return this.fetchAccountToken()
.then(() => CensusAPI.create(this.url, this.auth.identifier, censusType))
.then((response) => response.censusID);
.then((response) => ({ id: response.censusID, auth: this.auth.identifier }));
}

async add(censusId: string, participants: ICensusParticipant[]) {
Expand Down Expand Up @@ -169,6 +183,31 @@ export class CensusService extends Service implements CensusServiceProperties {
return CensusAPI.publish(this.url, this.auth.identifier, censusId);
}

/**
* Exports the given census identifier.
*
* @param {string} censusId The census identifier
*/
export(censusId: string): Promise<CensusImportExport> {
invariant(this.url, 'No URL set');
invariant(this.auth, 'No census auth set');

return CensusAPI.export(this.url, this.auth.identifier, censusId);
}

/**
* Imports data into the given census identifier.
*
* @param {string} censusId The census identifier
* @param {CensusImportExport} data The census data
*/
import(censusId: string, data: CensusImportExport): Promise<ICensusImportResponse> {
invariant(this.url, 'No URL set');
invariant(this.auth, 'No census auth set');

return CensusAPI.import(this.url, this.auth.identifier, censusId, data.type, data.rootHash, data.data);
}

/**
* Publishes the given census.
*
Expand All @@ -177,7 +216,7 @@ export class CensusService extends Service implements CensusServiceProperties {
*/
createCensus(census: PlainCensus | WeightedCensus): Promise<void> {
return this.create(census.type)
.then((censusId) => this.add(censusId, census.participants))
.then((censusInfo) => this.add(censusInfo.id, census.participants))
.then((censusId) => this.publish(censusId))
.then((censusPublish) => {
census.censusId = censusPublish.censusID;
Expand All @@ -198,9 +237,9 @@ export class CensusService extends Service implements CensusServiceProperties {
*/
// @ts-ignore
private createCensusParallel(census: PlainCensus | WeightedCensus): Promise<void> {
return this.create(census.type).then((censusId) =>
Promise.all(this.addParallel(censusId, census.participants))
.then(() => this.publish(censusId))
return this.create(census.type).then((censusInfo) =>
Promise.all(this.addParallel(censusInfo.id, census.participants))
.then(() => this.publish(censusInfo.id))
.then((censusPublish) => {
census.censusId = censusPublish.censusID;
census.censusURI = censusPublish.uri;
Expand Down
2 changes: 1 addition & 1 deletion src/services/election.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export class ElectionService extends Service implements ElectionServicePropertie
const electionInfo = await ElectionAPI.info(this.url, electionId);

return this.censusService
.fetchCensusInfo(electionInfo.census.censusRoot)
.get(electionInfo.census.censusRoot)
.then((censusInfo) =>
PublishedElection.build({
id: electionInfo.electionId,
Expand Down
49 changes: 46 additions & 3 deletions test/services/census.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const pad = (num, size) => {
return num;
};

let census, censusPublish;

describe('Census Service tests', () => {
it('should have the correct type and properties', () => {
const service = new CensusService({});
Expand Down Expand Up @@ -54,7 +56,7 @@ describe('Census Service tests', () => {
expect(census.size).toEqual(numVotes);
expect(census.weight).toEqual(BigInt(55));

const censusInfo = await service.fetchCensusInfo(census.censusId);
const censusInfo = await service.get(census.censusId);
expect(censusInfo.type).toEqual(CensusType.WEIGHTED);
expect(censusInfo.size).toEqual(10);
expect(censusInfo.weight).toEqual(BigInt(55));
Expand All @@ -78,7 +80,7 @@ describe('Census Service tests', () => {
expect(census.size).toEqual(numVotes);
expect(census.weight).toEqual(BigInt(numVotes));

const censusInfo = await service.fetchCensusInfo(census.censusId);
const censusInfo = await service.get(census.censusId);
expect(censusInfo.type).toEqual(CensusType.WEIGHTED);
expect(censusInfo.size).toEqual(numVotes);
expect(censusInfo.weight).toEqual(BigInt(numVotes));
Expand All @@ -102,9 +104,50 @@ describe('Census Service tests', () => {
expect(census.size).toEqual(numVotes);
expect(census.weight).toEqual(BigInt(numVotes));

const censusInfo = await service.fetchCensusInfo(census.censusId);
const censusInfo = await service.get(census.censusId);
expect(censusInfo.type).toEqual(CensusType.WEIGHTED);
expect(censusInfo.size).toEqual(numVotes);
expect(censusInfo.weight).toEqual(BigInt(numVotes));
}, 40000);
it('should create a census and export/import it correctly', async () => {
const numVotes = 10;
const service = new CensusService({ url: URL, chunk_size: CENSUS_CHUNK_SIZE });
const participants: Wallet[] = [...new Array(numVotes)].map(() => Wallet.createRandom());

census = await service.create(CensusType.WEIGHTED);
const newCensus = await service.create(CensusType.WEIGHTED);

await service.add(
census.id,
participants.map((p) => ({ key: p.address, weight: BigInt(1) }))
);

const exportedCensus = await service.export(census.id);
await service.import(newCensus.id, exportedCensus);
censusPublish = await service.publish(census.id);

const censusInfo = await service.get(census.id);
const newCensusInfo = await service.get(newCensus.id);

expect(censusInfo.type).toEqual(newCensusInfo.type);
expect(censusInfo.size).toEqual(newCensusInfo.size);
expect(censusInfo.weight).toEqual(newCensusInfo.weight);
}, 30000);
it('should reuse a census, modify it and publish it again', async () => {
if (!census) {
return;
}
const service = new CensusService({ url: URL, chunk_size: CENSUS_CHUNK_SIZE, auth: { identifier: census.auth } });
const oldCensusInfo = await service.get(census.id);

await service.add(census.id, [{ key: Wallet.createRandom().address, weight: BigInt(1) }]);

const newCensusPublish = await service.publish(census.id);
const newCensusInfo = await service.get(census.id);

expect(newCensusInfo.type).toEqual(oldCensusInfo.type);
expect(newCensusInfo.size).toEqual(oldCensusInfo.size + 1);
expect(newCensusInfo.weight).toEqual(oldCensusInfo.weight + BigInt(1));
expect(newCensusPublish.uri).not.toEqual(censusPublish.uri);
}, 30000);
});

0 comments on commit 7cca0a8

Please sign in to comment.