diff --git a/packages/cli/ts/index.ts b/packages/cli/ts/index.ts index ca0a6a3054..5e881e4593 100644 --- a/packages/cli/ts/index.ts +++ b/packages/cli/ts/index.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node import { Command } from "@commander-js/extra-typings"; -import { generateTallyCommitments, getPollParams, verify, getPoll } from "maci-sdk"; +import { generateTallyCommitments, getPollParams, verify, getPoll, isUserRegistered, signup } from "maci-sdk"; import fs from "fs"; import path from "path"; @@ -20,8 +20,6 @@ import { setVerifyingKeys, mergeSignups, timeTravel, - signup, - isRegisteredUser, genProofs, fundWallet, proveOnChain, @@ -31,7 +29,17 @@ import { joinPoll, isJoinedUser, } from "./commands"; -import { TallyData, banner, logError, logGreen, promptSensitiveValue, readContractAddress, success } from "./utils"; +import { + DEFAULT_SG_DATA, + TallyData, + banner, + logError, + logGreen, + logRed, + promptSensitiveValue, + readContractAddress, + success, +} from "./utils"; // set the description version and name of the cli tool const { description, version, name } = JSON.parse( @@ -465,13 +473,17 @@ program const maciAddress = cmdObj.maciAddress || (await readContractAddress("MACI", network?.name)); - await signup({ + const data = await signup({ maciPubKey: cmdObj.pubkey, maciAddress, - sgDataArg: cmdObj.sgData, - quiet: cmdObj.quiet, + sgData: cmdObj.sgData ?? DEFAULT_SG_DATA, signer, }); + + logGreen( + cmdObj.quiet, + success(`State index: ${data.stateIndex.toString()}\n Transaction hash: ${data.transactionHash}`), + ); } catch (error) { program.error((error as Error).message, { exitCode: 1 }); } @@ -489,12 +501,17 @@ program const maciAddress = cmdObj.maciAddress || (await readContractAddress("MACI", network?.name)); - await isRegisteredUser({ + const data = await isUserRegistered({ maciPubKey: cmdObj.pubkey, maciAddress, signer, - quiet: cmdObj.quiet, }); + + if (data.isRegistered) { + logGreen(cmdObj.quiet, success(`State index: ${data.stateIndex?.toString()}`)); + } else { + logRed(cmdObj.quiet, "User is not registered"); + } } catch (error) { program.error((error as Error).message, { exitCode: 1 }); } @@ -516,7 +533,7 @@ program const maciAddress = cmdObj.maciAddress || (await readContractAddress("MACI", network?.name)); - await isJoinedUser({ + const data = await isJoinedUser({ pollPubKey: cmdObj.pubkey, startBlock: cmdObj.startBlock!, maciAddress, @@ -524,6 +541,20 @@ program signer, quiet: cmdObj.quiet, }); + + if (data.isJoined) { + logGreen( + cmdObj.quiet, + success( + [ + `Poll state index: ${data.pollStateIndex?.toString()}, registered: ${data.isJoined}`, + `Voice credits: ${data.voiceCredits?.toString()}`, + ].join("\n"), + ), + ); + } else { + logRed(cmdObj.quiet, "User has not joined the poll"); + } } catch (error) { program.error((error as Error).message, { exitCode: 1 }); } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 0b8e6e6641..d7f0fabb5d 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -30,26 +30,22 @@ "docs": "typedoc --plugin typedoc-plugin-markdown --options ./typedoc.json" }, "dependencies": { - "@commander-js/extra-typings": "^12.1.0", "@nomicfoundation/hardhat-toolbox": "^5.0.0", - "commander": "^12.1.0", - "dotenv": "^16.4.5", "ethers": "^6.13.4", "hardhat": "^2.22.8", "maci-contracts": "^2.5.0", - "maci-crypto": "^2.5.0" + "maci-crypto": "^2.5.0", + "maci-domainobjs": "^2.5.0" }, "devDependencies": { "@types/chai": "^4.3.9", "@types/chai-as-promised": "^7.1.8", "@types/mocha": "^10.0.8", "@types/node": "^22.9.0", - "@types/snarkjs": "^0.7.8", "chai": "^4.3.10", "chai-as-promised": "^7.1.2", "mocha": "^10.7.3", "nyc": "^17.1.0", - "snarkjs": "^0.7.5", "ts-mocha": "^10.0.0", "typescript": "^5.6.3" }, diff --git a/packages/sdk/ts/index.ts b/packages/sdk/ts/index.ts index d1f342a2ed..41e63320b9 100644 --- a/packages/sdk/ts/index.ts +++ b/packages/sdk/ts/index.ts @@ -1,6 +1,7 @@ export { getPoll, getPollParams } from "./poll"; export { verify } from "./verify"; export { generateTallyCommitments } from "./tallyCommitments"; +export { isUserRegistered, isJoinedUser, signup } from "./user"; export { linkPoseidonLibraries, @@ -13,4 +14,4 @@ export { export * from "maci-contracts/typechain-types"; -export type { TallyData, VerifyArgs, IGetPollArgs, IGetPollData } from "./utils"; +export type { TallyData, VerifyArgs, IGetPollArgs, IGetPollData, IIsRegisteredUser, IIsJoinedUser } from "./utils"; diff --git a/packages/sdk/ts/user.ts b/packages/sdk/ts/user.ts new file mode 100644 index 0000000000..c8ca9cda8b --- /dev/null +++ b/packages/sdk/ts/user.ts @@ -0,0 +1,122 @@ +import { ContractTransactionReceipt, isBytesLike } from "ethers"; +import { MACI__factory as MACIFactory, Poll__factory as PollFactory } from "maci-contracts/typechain-types"; +import { PubKey } from "maci-domainobjs"; + +import { contractExists, IRegisteredUserArgs, ISignupArgs, parsePollJoinEvents, parseSignupEvents } from "./utils"; +import { IIsRegisteredUser, IJoinedUserArgs, ISignupData } from "./utils/interfaces"; + +/** + * Checks if user is registered with a given public key + * @param IRegisteredArgs - The arguments for the check register command + * @returns whether the user is registered or not and their state index + */ +export const isUserRegistered = async ({ + maciAddress, + maciPubKey, + signer, + startBlock, +}: IRegisteredUserArgs): Promise => { + const maciContract = MACIFactory.connect(maciAddress, signer); + const publicKey = PubKey.deserialize(maciPubKey); + const startBlockNumber = startBlock || 0; + const currentBlock = await signer.provider!.getBlockNumber(); + + const { stateIndex } = await parseSignupEvents({ + maciContract, + startBlock: startBlockNumber, + currentBlock, + publicKey, + }); + + return { + isRegistered: stateIndex !== undefined, + stateIndex, + }; +}; + +/** + * Signup a user to the MACI contract + * @param {SignupArgs} args - The arguments for the signup command + * @returns {ISignupData} The state index of the user and transaction hash + */ +export const signup = async ({ maciPubKey, maciAddress, sgData, signer }: ISignupArgs): Promise => { + // validate user key + if (!PubKey.isValidSerializedPubKey(maciPubKey)) { + throw new Error("Invalid MACI public key"); + } + + const userMaciPubKey = PubKey.deserialize(maciPubKey); + + const validContract = await contractExists(signer.provider!, maciAddress); + if (!validContract) { + throw new Error("There is no contract deployed at the specified address"); + } + + // we validate that the signup data and voice credit data is valid + if (!isBytesLike(sgData)) { + throw new Error("invalid signup gateway data"); + } + + const maciContract = MACIFactory.connect(maciAddress, signer); + + let stateIndex = ""; + let receipt: ContractTransactionReceipt | null = null; + + try { + // sign up to the MACI contract + const tx = await maciContract.signUp(userMaciPubKey.asContractParam(), sgData); + receipt = await tx.wait(); + + if (receipt?.status !== 1) { + throw new Error("The transaction failed"); + } + + const iface = maciContract.interface; + + // get state index from the event + const [log] = receipt.logs; + const { args } = iface.parseLog(log as unknown as { topics: string[]; data: string }) || { args: [] }; + [stateIndex, ,] = args; + } catch (error) { + throw new Error((error as Error).message); + } + + return { + stateIndex: stateIndex ? stateIndex.toString() : "", + transactionHash: receipt.hash, + }; +}; + +/** + * Checks if user is joined to a poll with the public key + * @param {IJoinedUserArgs} - The arguments for the join check command + * @returns user joined or not and poll state index, voice credit balance + */ +export const isJoinedUser = async ({ + maciAddress, + pollId, + pollPubKey, + signer, + startBlock, +}: IJoinedUserArgs): Promise<{ isJoined: boolean; pollStateIndex?: string; voiceCredits?: string }> => { + const maciContract = MACIFactory.connect(maciAddress, signer); + const pollContracts = await maciContract.getPoll(pollId); + const pollContract = PollFactory.connect(pollContracts.poll, signer); + + const pollPublicKey = PubKey.deserialize(pollPubKey); + const startBlockNumber = startBlock || 0; + const currentBlock = await signer.provider!.getBlockNumber(); + + const { pollStateIndex, voiceCredits } = await parsePollJoinEvents({ + pollContract, + startBlock: startBlockNumber, + currentBlock, + pollPublicKey, + }); + + return { + isJoined: pollStateIndex !== undefined, + pollStateIndex, + voiceCredits, + }; +}; diff --git a/packages/sdk/ts/utils/constants.ts b/packages/sdk/ts/utils/constants.ts new file mode 100644 index 0000000000..636ed550f8 --- /dev/null +++ b/packages/sdk/ts/utils/constants.ts @@ -0,0 +1 @@ +export const BLOCKS_STEP = 1000; diff --git a/packages/sdk/ts/utils/index.ts b/packages/sdk/ts/utils/index.ts index fb750cd782..af4b046562 100644 --- a/packages/sdk/ts/utils/index.ts +++ b/packages/sdk/ts/utils/index.ts @@ -8,5 +8,15 @@ export type { IGetPollParamsArgs, ITallyCommitments, IPollParams, + IRegisteredUserArgs, + IParseSignupEventsArgs, + ISignupData, + ISignupArgs, + IJoinedUserArgs, + IParsePollJoinEventsArgs, + IIsRegisteredUser, + IIsJoinedUser, } from "./interfaces"; export { verifyPerVOSpentVoiceCredits, verifyTallyResults } from "./verifiers"; +export { BLOCKS_STEP } from "./constants"; +export { parsePollJoinEvents, parseSignupEvents } from "./user"; diff --git a/packages/sdk/ts/utils/interfaces.ts b/packages/sdk/ts/utils/interfaces.ts index be043ea593..06bc416762 100644 --- a/packages/sdk/ts/utils/interfaces.ts +++ b/packages/sdk/ts/utils/interfaces.ts @@ -1,3 +1,6 @@ +import { MACI, Poll } from "maci-contracts/typechain-types"; +import { PubKey } from "maci-domainobjs"; + import type { Provider, Signer } from "ethers"; /** @@ -289,3 +292,180 @@ export interface ITallyCommitments { */ newResultsCommitment: bigint; } + +/** + * Interface for the arguments to the register check command + */ +export interface IRegisteredUserArgs { + /** + * A signer object + */ + signer: Signer; + + /** + * The public key of the user + */ + maciPubKey: string; + + /** + * The address of the MACI contract + */ + maciAddress: string; + + /** + * Start block for event parsing + */ + startBlock?: number; +} + +/** + * Interface for the arguments to the parseSignupEvents function + */ +export interface IParseSignupEventsArgs { + /** + * The MACI contract + */ + maciContract: MACI; + + /** + * The start block + */ + startBlock: number; + + /** + * The current block + */ + currentBlock: number; + + /** + * The public key + */ + publicKey: PubKey; +} + +/** + * Interface for the arguments to the signup command + */ +export interface ISignupArgs { + /** + * The public key of the user + */ + maciPubKey: string; + + /** + * A signer object + */ + signer: Signer; + + /** + * The address of the MACI contract + */ + maciAddress: string; + + /** + * The signup gatekeeper data + */ + sgData: string; +} + +/** + * Interface for the return data to the signup command + */ +export interface ISignupData { + /** + * The state index of the user + */ + stateIndex: string; + + /** + * The signup transaction hash + */ + transactionHash: string; +} + +/** + * Interface for the arguments to the parsePollJoinEvents function + */ +export interface IParsePollJoinEventsArgs { + /** + * The MACI contract + */ + pollContract: Poll; + + /** + * The start block + */ + startBlock: number; + + /** + * The current block + */ + currentBlock: number; + + /** + * The public key + */ + pollPublicKey: PubKey; +} + +/** + * Interface for the arguments to the isJoinedUser command + */ +export interface IJoinedUserArgs { + /** + * The address of the MACI contract + */ + maciAddress: string; + + /** + * The id of the poll + */ + pollId: bigint; + + /** + * Poll public key for the poll + */ + pollPubKey: string; + + /** + * A signer object + */ + signer: Signer; + + /** + * The start block number + */ + startBlock: number; +} + +/** + * Interface for the return data to the isRegisteredUser function + */ +export interface IIsRegisteredUser { + /** + * Whether the user is registered + */ + isRegistered: boolean; + /** + * The state index of the user + */ + stateIndex?: string; +} + +/** + * Interface for the return data to the isJoinedUser function + */ +export interface IIsJoinedUser { + /** + * Whether the user joined the poll + */ + isJoined: boolean; + /** + * The state index of the user + */ + pollStateIndex?: string; + /** + * The voice credits of the user + */ + voiceCredits?: string; +} diff --git a/packages/sdk/ts/utils/user.ts b/packages/sdk/ts/utils/user.ts new file mode 100644 index 0000000000..2679c5475c --- /dev/null +++ b/packages/sdk/ts/utils/user.ts @@ -0,0 +1,74 @@ +import { BLOCKS_STEP } from "./constants"; +import { IParsePollJoinEventsArgs, IParseSignupEventsArgs } from "./interfaces"; + +/** + * Parse the poll joining events from the Poll contract + */ + export const parsePollJoinEvents = async ({ + pollContract, + startBlock, + currentBlock, + pollPublicKey, +}: IParsePollJoinEventsArgs): Promise<{ + pollStateIndex?: string; + voiceCredits?: string; +}> => { + // 1000 blocks at a time + for (let block = startBlock; block <= currentBlock; block += BLOCKS_STEP) { + const toBlock = Math.min(block + BLOCKS_STEP - 1, currentBlock); + const pubKey = pollPublicKey.asArray(); + // eslint-disable-next-line no-await-in-loop + const newEvents = await pollContract.queryFilter( + pollContract.filters.PollJoined(pubKey[0], pubKey[1], undefined, undefined, undefined, undefined), + block, + toBlock, + ); + + if (newEvents.length > 0) { + const [event] = newEvents; + + return { + pollStateIndex: event.args[5].toString(), + voiceCredits: event.args[2].toString(), + }; + } + } + + return { + pollStateIndex: undefined, + voiceCredits: undefined, + }; +}; + +/** + * Parse the signup events from the MACI contract + */ +export const parseSignupEvents = async ({ + maciContract, + startBlock, + currentBlock, + publicKey, +}: IParseSignupEventsArgs): Promise<{ stateIndex?: string }> => { + // 1000 blocks at a time + for (let block = startBlock; block <= currentBlock; block += BLOCKS_STEP) { + const toBlock = Math.min(block + 999, currentBlock); + // eslint-disable-next-line no-await-in-loop + const newEvents = await maciContract.queryFilter( + maciContract.filters.SignUp(undefined, undefined, publicKey.rawPubKey[0], publicKey.rawPubKey[1]), + block, + toBlock, + ); + + if (newEvents.length > 0) { + const [event] = newEvents; + + return { + stateIndex: event.args[0].toString(), + }; + } + } + + return { + stateIndex: undefined, + }; +}; diff --git a/packages/sdk/ts/utils/verifiers.ts b/packages/sdk/ts/utils/verifiers.ts index ef47542f73..7aa7998d11 100644 --- a/packages/sdk/ts/utils/verifiers.ts +++ b/packages/sdk/ts/utils/verifiers.ts @@ -39,6 +39,7 @@ export const verifyPerVOSpentVoiceCredits = async ( newSpentVoiceCreditsCommitment, newResultsCommitment, ); + if (!isValid) { failedIndices.push(i); } diff --git a/packages/sdk/ts/verify.ts b/packages/sdk/ts/verify.ts index b15422281c..f8a777dd80 100644 --- a/packages/sdk/ts/verify.ts +++ b/packages/sdk/ts/verify.ts @@ -20,7 +20,8 @@ export const verify = async ({ const useQv = tallyData.isQuadratic; const maciContractAddress = tallyData.maci; - if (!(await contractExists(signer.provider!, maciContractAddress))) { + const validContract = await contractExists(signer.provider!, maciContractAddress); + if (!validContract) { throw new Error(`There is no MACI contract deployed at ${maciContractAddress}.`); } @@ -57,14 +58,14 @@ export const verify = async ({ } // verify total spent voice credits on-chain - if ( - !(await tallyContract.verifySpentVoiceCredits( - tallyData.totalSpentVoiceCredits.spent, - tallyData.totalSpentVoiceCredits.salt, - newResultsCommitment, - newPerVOSpentVoiceCreditsCommitment ?? 0n, - )) - ) { + const verified = await tallyContract.verifySpentVoiceCredits( + tallyData.totalSpentVoiceCredits.spent, + tallyData.totalSpentVoiceCredits.salt, + newResultsCommitment, + newPerVOSpentVoiceCreditsCommitment ?? 0n, + ) + + if (!verified) { throw new Error("The on-chain verification of total spent voice credits failed."); } @@ -85,27 +86,29 @@ export const verify = async ({ ); } - if (useQv) { - if (tallyData.perVOSpentVoiceCredits?.tally.length !== numVoteOptions) { - throw new Error("Wrong number of vote options."); - } - // verify per vote option voice credits on-chain - const failedSpentCredits = await verifyPerVOSpentVoiceCredits( - tallyContract, - tallyData, - voteOptionTreeDepth, - newSpentVoiceCreditsCommitment, - newResultsCommitment, - ); + if (!useQv) { + return true; + } - if (failedSpentCredits.length > 0) { - throw new Error( - `At least one tally result failed the on-chain verification. Please check your Tally data at these indexes: ${failedSpentCredits.join( - ", ", - )}`, - ); - } + if (tallyData.perVOSpentVoiceCredits?.tally.length !== numVoteOptions) { + throw new Error("Wrong number of vote options."); + } + // verify per vote option voice credits on-chain + const failedSpentCredits = await verifyPerVOSpentVoiceCredits( + tallyContract, + tallyData, + voteOptionTreeDepth, + newSpentVoiceCreditsCommitment, + newResultsCommitment, + ); + + if (failedSpentCredits.length > 0) { + throw new Error( + `At least one tally result failed the on-chain verification. Please check your Tally data at these indexes: ${failedSpentCredits.join( + ", ", + )}`, + ); } - return true; + return true; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13b865b682..42420ae59a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -759,18 +759,9 @@ importers: packages/sdk: dependencies: - '@commander-js/extra-typings': - specifier: ^12.1.0 - version: 12.1.0(commander@12.1.0) '@nomicfoundation/hardhat-toolbox': specifier: ^5.0.0 version: 5.0.0(xxl2qhkxzbwobweo6ovit3fcve) - commander: - specifier: ^12.1.0 - version: 12.1.0 - dotenv: - specifier: ^16.4.5 - version: 16.4.7 ethers: specifier: ^6.13.4 version: 6.13.5 @@ -783,6 +774,9 @@ importers: maci-crypto: specifier: ^2.5.0 version: link:../crypto + maci-domainobjs: + specifier: ^2.5.0 + version: link:../domainobjs devDependencies: '@types/chai': specifier: ^4.3.9 @@ -796,9 +790,6 @@ importers: '@types/node': specifier: ^22.9.0 version: 22.10.7 - '@types/snarkjs': - specifier: ^0.7.8 - version: 0.7.9 chai: specifier: ^4.3.10 version: 4.4.1 @@ -811,9 +802,6 @@ importers: nyc: specifier: ^17.1.0 version: 17.1.0 - snarkjs: - specifier: ^0.7.5 - version: 0.7.5 ts-mocha: specifier: ^10.0.0 version: 10.0.0(mocha@10.8.2) @@ -2215,11 +2203,6 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} - '@commander-js/extra-typings@12.1.0': - resolution: {integrity: sha512-wf/lwQvWAA0goIghcb91dQYpkLBcyhOhQNqG/VgWhnKzgt+UOMvra7EX/2fv70arm5RW+PUHoQHHDa6/p77Eqg==} - peerDependencies: - commander: ~12.1.0 - '@commander-js/extra-typings@13.0.0': resolution: {integrity: sha512-4or44L3saI49QRBvdSzfCtzqONlFg0/qy0Cfl+LRynDaxlGs7r2KRTLCELaAoKh4oQguICxRwQfm77/Up+0wTw==} peerDependencies: @@ -16558,10 +16541,6 @@ snapshots: '@colors/colors@1.5.0': {} - '@commander-js/extra-typings@12.1.0(commander@12.1.0)': - dependencies: - commander: 12.1.0 - '@commander-js/extra-typings@13.0.0(commander@13.0.0)': dependencies: commander: 13.0.0