From 073f4bf0dcd23f162996f37a534c3b5a62839021 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Tue, 23 Dec 2025 14:26:01 +0100 Subject: [PATCH 1/5] feat: add support for masking on frontend --- .../src/hooks/enclave/useEnclaveServer.ts | 5 + .../client/src/hooks/voting/useVoteCasting.ts | 173 +++++++++++++----- examples/CRISP/client/src/model/vote.model.ts | 5 + .../pages/Landing/components/DailyPoll.tsx | 17 +- examples/CRISP/client/src/utils/voters.ts | 12 ++ examples/CRISP/server/src/server/indexer.rs | 4 + examples/CRISP/server/src/server/models.rs | 9 + examples/CRISP/server/src/server/repo.rs | 24 +++ .../CRISP/server/src/server/routes/state.rs | 21 +++ .../src/server/token_holders/etherscan.rs | 15 +- .../server/src/server/token_holders/hashes.rs | 2 +- 11 files changed, 222 insertions(+), 65 deletions(-) create mode 100644 examples/CRISP/client/src/utils/voters.ts diff --git a/examples/CRISP/client/src/hooks/enclave/useEnclaveServer.ts b/examples/CRISP/client/src/hooks/enclave/useEnclaveServer.ts index ec6ad335a5..0a38b97e63 100644 --- a/examples/CRISP/client/src/hooks/enclave/useEnclaveServer.ts +++ b/examples/CRISP/client/src/hooks/enclave/useEnclaveServer.ts @@ -9,6 +9,7 @@ import { BroadcastVoteRequest, BroadcastVoteResponse, CurrentRound, + ElegibleVoter, VoteStateLite, VoteStatusRequest, VoteStatusResponse, @@ -27,6 +28,7 @@ const EnclaveEndpoints = { GetWebAllResult: `${ENCLAVE_API}/state/all`, BroadcastVote: `${ENCLAVE_API}/voting/broadcast`, GetVoteStatus: `${ENCLAVE_API}/voting/status`, + GetElegibleVoters: `${ENCLAVE_API}/state/elegible-addresses`, } as const export const useEnclaveServer = () => { @@ -38,6 +40,8 @@ export const useEnclaveServer = () => { const getWebResult = () => fetchData(GetWebAllResult, 'get') const getWebResultByRound = (round_id: number) => fetchData(GetWebResult, 'post', { round_id }) const getVoteStatus = (request: VoteStatusRequest) => fetchData(GetVoteStatus, 'post', request) + const getElegibleVoters = (round_id: number) => + fetchData(EnclaveEndpoints.GetElegibleVoters, 'post', { round_id }) return { isLoading, @@ -47,5 +51,6 @@ export const useEnclaveServer = () => { getRoundStateLite, broadcastVote, getVoteStatus, + getElegibleVoters, } } diff --git a/examples/CRISP/client/src/hooks/voting/useVoteCasting.ts b/examples/CRISP/client/src/hooks/voting/useVoteCasting.ts index 0d90ce0fca..3613160ed3 100644 --- a/examples/CRISP/client/src/hooks/voting/useVoteCasting.ts +++ b/examples/CRISP/client/src/hooks/voting/useVoteCasting.ts @@ -13,6 +13,8 @@ import { useNotificationAlertContext } from '@/context/NotificationAlert/Notific import { Poll } from '@/model/poll.model' import { BroadcastVoteRequest, Vote, VoteStateLite, VotingRound } from '@/model/vote.model' import { hashMessage } from 'viem' +import { useEnclaveServer } from '../enclave/useEnclaveServer' +import { getRandomVoterToMask } from '@/utils/voters' export type VotingStep = 'idle' | 'signing' | 'encrypting' | 'generating_proof' | 'broadcasting' | 'confirming' | 'complete' | 'error' @@ -42,6 +44,15 @@ const extractCleanErrorMessage = (errorMessage: string | undefined): string => { return errorMessage } +interface VoteData { + vote: Vote + slotAddress: string + balance: bigint + signature: string + messageHash: `0x${string}` + error?: string +} + export const useVoteCasting = (customRoundState?: VoteStateLite | null, customVotingRound?: VotingRound | null) => { const { user, @@ -58,15 +69,17 @@ export const useVoteCasting = (customRoundState?: VoteStateLite | null, customVo const votingRound = customVotingRound ?? contextVotingRound const { signMessageAsync } = useSignMessage() + const { getElegibleVoters } = useEnclaveServer() const { showToast } = useNotificationAlertContext() const navigate = useNavigate() - const [isLoading, setIsLoading] = useState(false) + const [isVoting, setIsVoting] = useState(false) + const [isMasking, setIsMasking] = useState(false) const [votingStep, setVotingStep] = useState('idle') const [lastActiveStep, setLastActiveStep] = useState(null) const [stepMessage, setStepMessage] = useState('') const handleProofGeneration = useCallback( - async (vote: Vote, address: string, balance: bigint, signature: string, messageHash: `0x${string}`, isMasking: boolean) => { + async (vote: Vote, address: string, balance: bigint, signature: string, messageHash: `0x${string}`, isAMask: boolean) => { if (!votingRound) throw new Error('No voting round available for proof generation') return generateProof( votingRound.round_id, @@ -76,7 +89,7 @@ export const useVoteCasting = (customRoundState?: VoteStateLite | null, customVo balance, signature, messageHash, - isMasking, + isAMask, ) }, [generateProof, votingRound], @@ -86,12 +99,85 @@ export const useVoteCasting = (customRoundState?: VoteStateLite | null, customVo setVotingStep('idle') setLastActiveStep(null) setStepMessage('') - setIsLoading(false) + setIsVoting(false) }, []) + /** + * Handles masking a vote by selecting a random elegible voter. + */ + const handleMask = useCallback(async (): Promise => { + if (!user || !roundState) { + throw new Error('Cannot mask vote: Missing user or round state.') + } + + const elegibleVoters = await getElegibleVoters(roundState.id) + + if (!elegibleVoters || elegibleVoters.length === 0) { + throw new Error('No elegible voters available for masking') + } + + const randomVoterToMask = getRandomVoterToMask(elegibleVoters) + + return { + vote: { yes: 0n, no: 0n }, + slotAddress: randomVoterToMask.address, + balance: BigInt(randomVoterToMask.balance), + signature: '', + messageHash: '' as `0x${string}`, + } + }, [user, roundState, getElegibleVoters]) + + /** + * Handles the voting process including signing the message. + */ + const handleVote = useCallback( + async (pollSelected: Poll, slotAddress: string): Promise => { + if (!roundState) { + throw new Error('No round state available for voting') + } + + // Step 1: Signing + setVotingStep('signing') + setLastActiveStep('signing') + setStepMessage('Please sign the message in your wallet...') + + const message = `Vote for round ${roundState.id}` + const messageHash = hashMessage(message) + + // vote is either 0 or 1, so we need to encode the vote accordingly. + const balance = 1n + const vote = pollSelected.value === 0 ? { yes: balance, no: 0n } : { yes: 0n, no: balance } + + let signature: string + try { + signature = await signMessageAsync({ message }) + return { + signature, + messageHash, + vote, + slotAddress, + balance, + } + } catch (error) { + console.log('User rejected signature or signing failed', error) + showToast({ type: 'danger', message: 'Signature cancelled or failed.' }) + resetVotingState() + return { + signature: '', + messageHash: '' as `0x${string}`, + vote: { yes: 0n, no: 0n }, + slotAddress: '', + balance: 0n, + error: 'User rejected signature or signing failed', + } + } + }, + [roundState, signMessageAsync, showToast, resetVotingState], + ) + const castVoteWithProof = useCallback( - async (pollSelected: Poll | null, isVoteUpdate: boolean = false, isMasking: boolean = false) => { - if (!pollSelected) { + async (pollSelected: Poll | null, isAMask: boolean = false) => { + if (!isAMask && !pollSelected) { console.log('Cannot cast vote: Poll option not selected.') showToast({ type: 'danger', message: 'Please select a poll option first.' }) return @@ -106,28 +192,19 @@ export const useVoteCasting = (customRoundState?: VoteStateLite | null, customVo return } - setIsLoading(true) - const actionText = isVoteUpdate ? 'Updating vote' : 'Processing vote' - console.log(`${actionText}...`) - try { - // Step 1: Signing - setVotingStep('signing') - setLastActiveStep('signing') - setStepMessage('Please sign the message in your wallet...') - - const message = `Vote for round ${roundState.id}` - const messageHash = hashMessage(message) - - let signature: string - try { - signature = await signMessageAsync({ message }) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (signError) { - console.log('User rejected signature or signing failed') - showToast({ type: 'danger', message: 'Signature cancelled or failed.' }) - resetVotingState() - return + let voteData + + if (isAMask) { + setIsMasking(true) + voteData = await handleMask() + } else { + setIsVoting(true) + voteData = await handleVote(pollSelected!, user.address) + } + + if (voteData.error) { + throw new Error(voteData.error) } // Step 2: Encrypting vote @@ -135,11 +212,14 @@ export const useVoteCasting = (customRoundState?: VoteStateLite | null, customVo setLastActiveStep('encrypting') setStepMessage('') - // vote is either 0 or 1, so we need to encode the vote accordingly. - const balance = 1n - const vote = pollSelected.value === 0 ? { yes: balance, no: 0n } : { yes: 0n, no: balance } - - const encodedProof = await handleProofGeneration(vote, user.address, balance, signature, messageHash, isMasking) + const encodedProof = await handleProofGeneration( + voteData.vote, + voteData.slotAddress, + voteData.balance, + voteData.signature, + voteData.messageHash, + isAMask, + ) if (!encodedProof) { throw new Error('Failed to encrypt vote.') @@ -168,14 +248,18 @@ export const useVoteCasting = (customRoundState?: VoteStateLite | null, customVo switch (broadcastVoteResponse.status) { case 'success': { setVotingStep('complete') - setStepMessage('Vote submitted successfully!') + setStepMessage(`${isAMask ? 'Masking' : 'Vote'} submitted successfully!'`) const url = `https://sepolia.etherscan.io/tx/${broadcastVoteResponse.tx_hash}` setTxUrl(url) - markVotedInRound(roundState.id) + if (!isAMask) markVotedInRound(roundState.id) - const successMessage = broadcastVoteResponse.is_vote_update ? 'Vote updated successfully!' : 'Vote submitted successfully!' + const successMessage = isAMask + ? 'Slot masked successfully' + : broadcastVoteResponse.is_vote_update + ? 'Vote updated successfully!' + : 'Vote submitted successfully!' showToast({ type: 'success', message: successMessage, @@ -213,26 +297,17 @@ export const useVoteCasting = (customRoundState?: VoteStateLite | null, customVo persistent: true, }) } finally { - setIsLoading(false) + setIsVoting(false) + setIsMasking(false) } }, - [ - user, - roundState, - broadcastVote, - setTxUrl, - showToast, - navigate, - handleProofGeneration, - signMessageAsync, - markVotedInRound, - resetVotingState, - ], + [user, roundState, broadcastVote, setTxUrl, showToast, navigate, handleProofGeneration, markVotedInRound, handleMask, handleVote], ) return { castVoteWithProof, - isLoading, + isVoting, + isMasking, votingStep, lastActiveStep, stepMessage, diff --git a/examples/CRISP/client/src/model/vote.model.ts b/examples/CRISP/client/src/model/vote.model.ts index 148824c536..6675935302 100644 --- a/examples/CRISP/client/src/model/vote.model.ts +++ b/examples/CRISP/client/src/model/vote.model.ts @@ -60,3 +60,8 @@ export interface Vote { yes: bigint no: bigint } + +export interface ElegibleVoter { + address: string + balance: number +} diff --git a/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx b/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx index ea4271316c..761697c4b5 100644 --- a/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx +++ b/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx @@ -30,7 +30,7 @@ const DailyPollSection: React.FC = ({ loading, endTime, t const [pollSelected, setPollSelected] = useState(null) const [noPollSelected, setNoPollSelected] = useState(true) const { setOpen } = useModal() - const { castVoteWithProof, isLoading: isCastingVote, votingStep, lastActiveStep, stepMessage } = useVoteCasting() + const { castVoteWithProof, isVoting: isCastingVote, isMasking, votingStep, lastActiveStep, stepMessage } = useVoteCasting() useEffect(() => { ;(async () => { @@ -64,13 +64,13 @@ const DailyPollSection: React.FC = ({ loading, endTime, t } } - const castVote = async () => { + const castVote = async (isMasking: boolean) => { if (!user) { setOpen(true) return } - await castVoteWithProof(pollSelected, hasVotedInCurrentRound) + await castVoteWithProof(pollSelected, isMasking) } return ( @@ -139,11 +139,18 @@ const DailyPollSection: React.FC = ({ loading, endTime, t )} + )} diff --git a/examples/CRISP/client/src/utils/voters.ts b/examples/CRISP/client/src/utils/voters.ts new file mode 100644 index 0000000000..2826eadcee --- /dev/null +++ b/examples/CRISP/client/src/utils/voters.ts @@ -0,0 +1,12 @@ +import { ElegibleVoter } from '@/model/vote.model' + +/** + * Get a random voter details from a list of elegible voters + * @param addresses The list of elegible voters + * @returns The randomly selected voter details + */ +export const getRandomVoterToMask = (voters: ElegibleVoter[]): ElegibleVoter => { + const randomIndex = crypto.getRandomValues(new Uint32Array(1))[0] % voters.length + + return voters[randomIndex] +} diff --git a/examples/CRISP/server/src/server/indexer.rs b/examples/CRISP/server/src/server/indexer.rs index ab1aaaf668..d60ea66607 100644 --- a/examples/CRISP/server/src/server/indexer.rs +++ b/examples/CRISP/server/src/server/indexer.rs @@ -118,6 +118,10 @@ pub async fn register_e3_requested( .into()); } + // Store eligible addresses in the repository. + repo.set_elegible_addresses(token_holders.clone()) + .await?; + // Compute Poseidon hashes for token holder address + balance pairs. let token_holder_hashes = compute_token_holder_hashes(&token_holders) .with_context(|| "Failed to compute token holder hashes")?; diff --git a/examples/CRISP/server/src/server/models.rs b/examples/CRISP/server/src/server/models.rs index e91a839fb9..f648de666d 100644 --- a/examples/CRISP/server/src/server/models.rs +++ b/examples/CRISP/server/src/server/models.rs @@ -223,6 +223,7 @@ pub struct E3Crisp { pub votes_option_1: String, pub votes_option_2: String, pub token_holder_hashes: Vec, + pub elegible_addresses: Vec, pub token_address: String, pub balance_threshold: String, pub ciphertext_inputs: Vec<(Vec, u64)>, @@ -241,3 +242,11 @@ impl From for WebResultRequest { } } } + +/// Represents a token holder with their address and balance. +/// Balance is stored as a string to preserve precision for large numbers. +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct TokenHolder { + pub address: String, + pub balance: String, +} diff --git a/examples/CRISP/server/src/server/repo.rs b/examples/CRISP/server/src/server/repo.rs index d2a44a0125..05281bf465 100644 --- a/examples/CRISP/server/src/server/repo.rs +++ b/examples/CRISP/server/src/server/repo.rs @@ -4,6 +4,8 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. +use crate::server::models::TokenHolder; + use super::{ database::generate_emoji, models::{CurrentRound, E3Crisp, E3StateLite, WebResultRequest}, @@ -141,6 +143,7 @@ impl CrispE3Repository { votes_option_2: "0".to_string(), emojis: generate_emoji(), token_holder_hashes: vec![], + elegible_addresses: vec![], token_address, balance_threshold, ciphertext_inputs: vec![], @@ -310,6 +313,27 @@ impl CrispE3Repository { Ok(e3_crisp.token_holder_hashes) } + pub async fn set_elegible_addresses(&mut self, holders: Vec) -> Result<()> { + let key = self.crisp_key(); + // Placeholder for future implementation + + self.store + .modify(&key, |e3_obj: Option| { + e3_obj.map(|mut e| { + e.elegible_addresses = holders.clone(); + e + }) + }) + .await + .map_err(|_| eyre::eyre!("Could not set elegible_addresses for '{key}'"))?; + Ok(()) + } + + pub async fn get_elegible_addresses(&self) -> Result> { + let e3_crisp = self.get_crisp().await?; + Ok(e3_crisp.elegible_addresses) + } + fn crisp_key(&self) -> String { let e3_id = self.e3_id; format!("_e3:crisp:{e3_id}") diff --git a/examples/CRISP/server/src/server/routes/state.rs b/examples/CRISP/server/src/server/routes/state.rs index a24f770f30..24c5acf9aa 100644 --- a/examples/CRISP/server/src/server/routes/state.rs +++ b/examples/CRISP/server/src/server/routes/state.rs @@ -30,6 +30,7 @@ pub fn setup_routes(config: &mut web::ServiceConfig) { .route("/add-result", web::post().to(handle_program_server_result)) // Get the token holders hashes for a given round .route("/token-holders", web::post().to(get_token_holders_hashes)) + .route("/elegible-addresses", web::post().to(handle_get_elegible_addresses)) .route( "/previous-ciphertext", web::post().to(handle_get_previous_ciphertext), @@ -321,3 +322,23 @@ async fn handle_is_slot_empty( HttpResponse::Ok().json(IsSlotEmptyResponse { is_empty } ) } + +/// Get the elegible addresses for a given round +/// # Arguments +/// * `GetRoundRequest` - The request data containing the round ID +/// # Returns +/// * A JSON response containing the list of elegible addresses and their balances +async fn handle_get_elegible_addresses( + data: web::Json, + store: web::Data, +) -> impl Responder { + let incoming = data.into_inner(); + + match store.e3(incoming.round_id).get_elegible_addresses().await { + Ok(addresses) => HttpResponse::Ok().json(addresses), + Err(e) => { + error!("Error getting elegible addresses: {:?}", e); + HttpResponse::InternalServerError().body("Failed to get elegible addresses") + } + } +} diff --git a/examples/CRISP/server/src/server/token_holders/etherscan.rs b/examples/CRISP/server/src/server/token_holders/etherscan.rs index 1f6c5a3e01..5259fc0698 100644 --- a/examples/CRISP/server/src/server/token_holders/etherscan.rs +++ b/examples/CRISP/server/src/server/token_holders/etherscan.rs @@ -4,15 +4,18 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::server::CONFIG; use alloy::primitives::{Address, U256}; use alloy::providers::ProviderBuilder; use alloy::sol; use eyre::{eyre, Context, Result}; // Add this import use reqwest; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize}; use std::collections::{HashMap, HashSet}; use tokio::time::{sleep, Duration}; +use crate::server::{ + CONFIG, + models::TokenHolder +}; // Define the Votes contract interface for getPastVotes sol! { @@ -28,14 +31,6 @@ sol! { pub const ETHERSCAN_API_URL: &str = "https://api.etherscan.io/v2/api"; const ZERO_ADDRESS: Address = Address::ZERO; -/// Represents a token holder with their address and balance. -/// Balance is stored as a string to preserve precision for large numbers. -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct TokenHolder { - pub address: String, - pub balance: String, -} - // Response types #[derive(Debug, Deserialize)] struct EtherscanResponse { diff --git a/examples/CRISP/server/src/server/token_holders/hashes.rs b/examples/CRISP/server/src/server/token_holders/hashes.rs index a40d8c55fd..c3483fe1d8 100644 --- a/examples/CRISP/server/src/server/token_holders/hashes.rs +++ b/examples/CRISP/server/src/server/token_holders/hashes.rs @@ -11,7 +11,7 @@ use light_poseidon::{Poseidon, PoseidonHasher}; use num_bigint::BigUint; use std::str::FromStr; -use super::etherscan::TokenHolder; +use crate::server::models::TokenHolder; /// Computes Poseidon hashes for token holder address + balance pairs. /// From 0af070a0c2425ee8ede950552cb8476c5cd52370 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Tue, 23 Dec 2025 14:32:15 +0100 Subject: [PATCH 2/5] chore: add license --- examples/CRISP/client/src/utils/voters.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/CRISP/client/src/utils/voters.ts b/examples/CRISP/client/src/utils/voters.ts index 2826eadcee..0f2fc73880 100644 --- a/examples/CRISP/client/src/utils/voters.ts +++ b/examples/CRISP/client/src/utils/voters.ts @@ -1,3 +1,9 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + import { ElegibleVoter } from '@/model/vote.model' /** From 7f65357b58d26b2302d2c93d6a81134af4080bcc Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Tue, 23 Dec 2025 15:42:09 +0100 Subject: [PATCH 3/5] fix: pass slot address and add progress steps to masking --- examples/CRISP/client/src/hooks/voting/useVoteCasting.ts | 2 +- .../CRISP/client/src/pages/Landing/components/DailyPoll.tsx | 4 ++-- examples/CRISP/client/src/utils/voters.ts | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/CRISP/client/src/hooks/voting/useVoteCasting.ts b/examples/CRISP/client/src/hooks/voting/useVoteCasting.ts index 3613160ed3..d1a580f6e9 100644 --- a/examples/CRISP/client/src/hooks/voting/useVoteCasting.ts +++ b/examples/CRISP/client/src/hooks/voting/useVoteCasting.ts @@ -239,7 +239,7 @@ export const useVoteCasting = (customRoundState?: VoteStateLite | null, customVo const voteRequest: BroadcastVoteRequest = { round_id: roundState.id, encoded_proof: encodedProof, - address: user.address, + address: voteData.slotAddress, } const broadcastVoteResponse = await broadcastVote(voteRequest) diff --git a/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx b/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx index 761697c4b5..aeac48d9de 100644 --- a/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx +++ b/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx @@ -119,8 +119,8 @@ const DailyPollSection: React.FC = ({ loading, endTime, t )} - {isCastingVote && } - {loading && !isCastingVote && } + {(isCastingVote || isMasking) && } + {loading && !isCastingVote && !isMasking && }
{pollOptions.map((poll) => (
diff --git a/examples/CRISP/client/src/utils/voters.ts b/examples/CRISP/client/src/utils/voters.ts index 0f2fc73880..1181085e89 100644 --- a/examples/CRISP/client/src/utils/voters.ts +++ b/examples/CRISP/client/src/utils/voters.ts @@ -14,5 +14,6 @@ import { ElegibleVoter } from '@/model/vote.model' export const getRandomVoterToMask = (voters: ElegibleVoter[]): ElegibleVoter => { const randomIndex = crypto.getRandomValues(new Uint32Array(1))[0] % voters.length + console.log(`Selected random voter at index: ${voters[randomIndex].address}`) return voters[randomIndex] } From 3e35fa039927c247db13c34d1c57729ec43d73be Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Tue, 23 Dec 2025 16:38:11 +0100 Subject: [PATCH 4/5] chore: PR comments --- .../src/components/Cards/PollCardResult.tsx | 4 +- .../src/hooks/enclave/useEnclaveServer.ts | 10 ++--- .../client/src/hooks/voting/useVoteCasting.ts | 41 ++++++++++++------- examples/CRISP/client/src/model/vote.model.ts | 2 +- examples/CRISP/client/src/utils/voters.ts | 13 +++--- examples/CRISP/server/src/server/indexer.rs | 2 +- examples/CRISP/server/src/server/models.rs | 2 +- examples/CRISP/server/src/server/repo.rs | 16 ++++---- .../CRISP/server/src/server/routes/state.rs | 35 ++++++++-------- 9 files changed, 72 insertions(+), 53 deletions(-) diff --git a/examples/CRISP/client/src/components/Cards/PollCardResult.tsx b/examples/CRISP/client/src/components/Cards/PollCardResult.tsx index 00b44658c2..2251f55e12 100644 --- a/examples/CRISP/client/src/components/Cards/PollCardResult.tsx +++ b/examples/CRISP/client/src/components/Cards/PollCardResult.tsx @@ -19,8 +19,10 @@ type PollCardResultProps = { isActive?: boolean } const PollCardResult: React.FC = ({ isResult, results, totalVotes, isActive }) => { + const validVotes = results.filter((poll) => poll.votes.toString() !== '0').length + const calculatePercentage = (votes: number) => { - return ((votes / totalVotes) * 100).toFixed(0) + return ((votes / validVotes) * 100).toFixed(0) } return ( diff --git a/examples/CRISP/client/src/hooks/enclave/useEnclaveServer.ts b/examples/CRISP/client/src/hooks/enclave/useEnclaveServer.ts index 0a38b97e63..b410aa534b 100644 --- a/examples/CRISP/client/src/hooks/enclave/useEnclaveServer.ts +++ b/examples/CRISP/client/src/hooks/enclave/useEnclaveServer.ts @@ -9,7 +9,7 @@ import { BroadcastVoteRequest, BroadcastVoteResponse, CurrentRound, - ElegibleVoter, + EligibleVoter, VoteStateLite, VoteStatusRequest, VoteStatusResponse, @@ -28,7 +28,7 @@ const EnclaveEndpoints = { GetWebAllResult: `${ENCLAVE_API}/state/all`, BroadcastVote: `${ENCLAVE_API}/voting/broadcast`, GetVoteStatus: `${ENCLAVE_API}/voting/status`, - GetElegibleVoters: `${ENCLAVE_API}/state/elegible-addresses`, + GetEligibleVoters: `${ENCLAVE_API}/state/eligible-addresses`, } as const export const useEnclaveServer = () => { @@ -40,8 +40,8 @@ export const useEnclaveServer = () => { const getWebResult = () => fetchData(GetWebAllResult, 'get') const getWebResultByRound = (round_id: number) => fetchData(GetWebResult, 'post', { round_id }) const getVoteStatus = (request: VoteStatusRequest) => fetchData(GetVoteStatus, 'post', request) - const getElegibleVoters = (round_id: number) => - fetchData(EnclaveEndpoints.GetElegibleVoters, 'post', { round_id }) + const getEligibleVoters = (round_id: number) => + fetchData(EnclaveEndpoints.GetEligibleVoters, 'post', { round_id }) return { isLoading, @@ -51,6 +51,6 @@ export const useEnclaveServer = () => { getRoundStateLite, broadcastVote, getVoteStatus, - getElegibleVoters, + getEligibleVoters, } } diff --git a/examples/CRISP/client/src/hooks/voting/useVoteCasting.ts b/examples/CRISP/client/src/hooks/voting/useVoteCasting.ts index d1a580f6e9..b2edcc00a8 100644 --- a/examples/CRISP/client/src/hooks/voting/useVoteCasting.ts +++ b/examples/CRISP/client/src/hooks/voting/useVoteCasting.ts @@ -69,7 +69,7 @@ export const useVoteCasting = (customRoundState?: VoteStateLite | null, customVo const votingRound = customVotingRound ?? contextVotingRound const { signMessageAsync } = useSignMessage() - const { getElegibleVoters } = useEnclaveServer() + const { getEligibleVoters } = useEnclaveServer() const { showToast } = useNotificationAlertContext() const navigate = useNavigate() const [isVoting, setIsVoting] = useState(false) @@ -100,32 +100,44 @@ export const useVoteCasting = (customRoundState?: VoteStateLite | null, customVo setLastActiveStep(null) setStepMessage('') setIsVoting(false) + setIsMasking(false) }, []) /** - * Handles masking a vote by selecting a random elegible voter. + * Handles masking a vote by selecting a random eligible voter. */ const handleMask = useCallback(async (): Promise => { if (!user || !roundState) { throw new Error('Cannot mask vote: Missing user or round state.') } - const elegibleVoters = await getElegibleVoters(roundState.id) + const eligibleVoters = await getEligibleVoters(roundState.id) - if (!elegibleVoters || elegibleVoters.length === 0) { - throw new Error('No elegible voters available for masking') + if (!eligibleVoters || eligibleVoters.length === 0) { + throw new Error('No eligible voters available for masking') } - const randomVoterToMask = getRandomVoterToMask(elegibleVoters) + try { + const randomVoterToMask = getRandomVoterToMask(eligibleVoters) - return { - vote: { yes: 0n, no: 0n }, - slotAddress: randomVoterToMask.address, - balance: BigInt(randomVoterToMask.balance), - signature: '', - messageHash: '' as `0x${string}`, + return { + vote: { yes: 0n, no: 0n }, + slotAddress: randomVoterToMask.address, + balance: BigInt(randomVoterToMask.balance), + signature: '', + messageHash: '' as `0x${string}`, + } + } catch (error) { + return { + vote: { yes: 0n, no: 0n }, + slotAddress: '', + balance: 0n, + signature: '', + messageHash: '' as `0x${string}`, + error: (error as Error).message, + } } - }, [user, roundState, getElegibleVoters]) + }, [user, roundState, getEligibleVoters]) /** * Handles the voting process including signing the message. @@ -160,7 +172,6 @@ export const useVoteCasting = (customRoundState?: VoteStateLite | null, customVo } } catch (error) { console.log('User rejected signature or signing failed', error) - showToast({ type: 'danger', message: 'Signature cancelled or failed.' }) resetVotingState() return { signature: '', @@ -172,7 +183,7 @@ export const useVoteCasting = (customRoundState?: VoteStateLite | null, customVo } } }, - [roundState, signMessageAsync, showToast, resetVotingState], + [roundState, signMessageAsync, resetVotingState], ) const castVoteWithProof = useCallback( diff --git a/examples/CRISP/client/src/model/vote.model.ts b/examples/CRISP/client/src/model/vote.model.ts index 6675935302..319e0fc044 100644 --- a/examples/CRISP/client/src/model/vote.model.ts +++ b/examples/CRISP/client/src/model/vote.model.ts @@ -61,7 +61,7 @@ export interface Vote { no: bigint } -export interface ElegibleVoter { +export interface EligibleVoter { address: string balance: number } diff --git a/examples/CRISP/client/src/utils/voters.ts b/examples/CRISP/client/src/utils/voters.ts index 1181085e89..bfe24d070a 100644 --- a/examples/CRISP/client/src/utils/voters.ts +++ b/examples/CRISP/client/src/utils/voters.ts @@ -4,16 +4,19 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import { ElegibleVoter } from '@/model/vote.model' +import { EligibleVoter } from '@/model/vote.model' /** - * Get a random voter details from a list of elegible voters - * @param addresses The list of elegible voters + * Get a random voter details from a list of eligible voters + * @param addresses The list of eligible voters * @returns The randomly selected voter details */ -export const getRandomVoterToMask = (voters: ElegibleVoter[]): ElegibleVoter => { +export const getRandomVoterToMask = (voters: EligibleVoter[]): EligibleVoter => { + if (voters.length === 0) { + throw new Error('No eligible voters available to select from.') + } + const randomIndex = crypto.getRandomValues(new Uint32Array(1))[0] % voters.length - console.log(`Selected random voter at index: ${voters[randomIndex].address}`) return voters[randomIndex] } diff --git a/examples/CRISP/server/src/server/indexer.rs b/examples/CRISP/server/src/server/indexer.rs index d60ea66607..2722558d7f 100644 --- a/examples/CRISP/server/src/server/indexer.rs +++ b/examples/CRISP/server/src/server/indexer.rs @@ -119,7 +119,7 @@ pub async fn register_e3_requested( } // Store eligible addresses in the repository. - repo.set_elegible_addresses(token_holders.clone()) + repo.set_eligible_addresses(token_holders.clone()) .await?; // Compute Poseidon hashes for token holder address + balance pairs. diff --git a/examples/CRISP/server/src/server/models.rs b/examples/CRISP/server/src/server/models.rs index f648de666d..45ef577b4d 100644 --- a/examples/CRISP/server/src/server/models.rs +++ b/examples/CRISP/server/src/server/models.rs @@ -223,7 +223,7 @@ pub struct E3Crisp { pub votes_option_1: String, pub votes_option_2: String, pub token_holder_hashes: Vec, - pub elegible_addresses: Vec, + pub eligible_addresses: Vec, pub token_address: String, pub balance_threshold: String, pub ciphertext_inputs: Vec<(Vec, u64)>, diff --git a/examples/CRISP/server/src/server/repo.rs b/examples/CRISP/server/src/server/repo.rs index 05281bf465..7c86dd6a03 100644 --- a/examples/CRISP/server/src/server/repo.rs +++ b/examples/CRISP/server/src/server/repo.rs @@ -126,7 +126,7 @@ impl CrispE3Repository { return Ok(Some(vote)); } } - + Ok(None) } @@ -143,7 +143,7 @@ impl CrispE3Repository { votes_option_2: "0".to_string(), emojis: generate_emoji(), token_holder_hashes: vec![], - elegible_addresses: vec![], + eligible_addresses: vec![], token_address, balance_threshold, ciphertext_inputs: vec![], @@ -313,25 +313,25 @@ impl CrispE3Repository { Ok(e3_crisp.token_holder_hashes) } - pub async fn set_elegible_addresses(&mut self, holders: Vec) -> Result<()> { + pub async fn set_eligible_addresses(&mut self, holders: Vec) -> Result<()> { let key = self.crisp_key(); // Placeholder for future implementation self.store .modify(&key, |e3_obj: Option| { e3_obj.map(|mut e| { - e.elegible_addresses = holders.clone(); + e.eligible_addresses = holders.clone(); e }) }) - .await - .map_err(|_| eyre::eyre!("Could not set elegible_addresses for '{key}'"))?; + .await + .map_err(|_| eyre::eyre!("Could not set eligible_addresses for '{key}'"))?; Ok(()) } - pub async fn get_elegible_addresses(&self) -> Result> { + pub async fn get_eligible_addresses(&self) -> Result> { let e3_crisp = self.get_crisp().await?; - Ok(e3_crisp.elegible_addresses) + Ok(e3_crisp.eligible_addresses) } fn crisp_key(&self) -> String { diff --git a/examples/CRISP/server/src/server/routes/state.rs b/examples/CRISP/server/src/server/routes/state.rs index 24c5acf9aa..69a27e4f02 100644 --- a/examples/CRISP/server/src/server/routes/state.rs +++ b/examples/CRISP/server/src/server/routes/state.rs @@ -7,9 +7,12 @@ use std::str::FromStr; use crate::server::{ - CONFIG, app_data::AppData, models::{ - GetRoundRequest, IsSlotEmptyRequest, IsSlotEmptyResponse, PreviousCiphertextRequest, PreviousCiphertextResponse, WebhookPayload - } + app_data::AppData, + models::{ + GetRoundRequest, IsSlotEmptyRequest, IsSlotEmptyResponse, PreviousCiphertextRequest, + PreviousCiphertextResponse, WebhookPayload, + }, + CONFIG, }; use actix_web::{web, HttpResponse, Responder}; use alloy::primitives::{Address, Bytes, U256}; @@ -30,7 +33,10 @@ pub fn setup_routes(config: &mut web::ServiceConfig) { .route("/add-result", web::post().to(handle_program_server_result)) // Get the token holders hashes for a given round .route("/token-holders", web::post().to(get_token_holders_hashes)) - .route("/elegible-addresses", web::post().to(handle_get_elegible_addresses)) + .route( + "/eligible-addresses", + web::post().to(handle_get_eligible_addresses), + ) .route( "/previous-ciphertext", web::post().to(handle_get_previous_ciphertext), @@ -284,9 +290,7 @@ async fn get_token_holders_hashes( /// Check if a slot is empty given an address /// # Arguments /// * `IsSlotEmptyRequest` - The request containing round_id and address -async fn handle_is_slot_empty( - data: web::Json, -) -> impl Responder { +async fn handle_is_slot_empty(data: web::Json) -> impl Responder { let incoming = data.into_inner(); let contract = @@ -315,30 +319,29 @@ async fn handle_is_slot_empty( Ok(empty) => empty, Err(e) => { error!("Error checking if slot is empty: {:?}", e); - return HttpResponse::InternalServerError() - .body("Failed to check if slot is empty"); + return HttpResponse::InternalServerError().body("Failed to check if slot is empty"); } }; - HttpResponse::Ok().json(IsSlotEmptyResponse { is_empty } ) + HttpResponse::Ok().json(IsSlotEmptyResponse { is_empty }) } -/// Get the elegible addresses for a given round +/// Get the eligible addresses for a given round /// # Arguments /// * `GetRoundRequest` - The request data containing the round ID /// # Returns -/// * A JSON response containing the list of elegible addresses and their balances -async fn handle_get_elegible_addresses( +/// * A JSON response containing the list of eligible addresses and their balances +async fn handle_get_eligible_addresses( data: web::Json, store: web::Data, ) -> impl Responder { let incoming = data.into_inner(); - match store.e3(incoming.round_id).get_elegible_addresses().await { + match store.e3(incoming.round_id).get_eligible_addresses().await { Ok(addresses) => HttpResponse::Ok().json(addresses), Err(e) => { - error!("Error getting elegible addresses: {:?}", e); - HttpResponse::InternalServerError().body("Failed to get elegible addresses") + error!("Error getting eligible addresses: {:?}", e); + HttpResponse::InternalServerError().body("Failed to get eligible addresses") } } } From fe6c06b6ad8d3a18bad4615ccb855edab06df9d8 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Tue, 23 Dec 2025 16:59:58 +0100 Subject: [PATCH 5/5] fix: display of vote % --- examples/CRISP/client/src/components/Cards/PollCardResult.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/CRISP/client/src/components/Cards/PollCardResult.tsx b/examples/CRISP/client/src/components/Cards/PollCardResult.tsx index 2251f55e12..1acc4dfdad 100644 --- a/examples/CRISP/client/src/components/Cards/PollCardResult.tsx +++ b/examples/CRISP/client/src/components/Cards/PollCardResult.tsx @@ -19,7 +19,7 @@ type PollCardResultProps = { isActive?: boolean } const PollCardResult: React.FC = ({ isResult, results, totalVotes, isActive }) => { - const validVotes = results.filter((poll) => poll.votes.toString() !== '0').length + const validVotes = results.reduce((sum, poll) => sum + Number.parseInt(poll.votes.toString(), 10), 0) const calculatePercentage = (votes: number) => { return ((votes / validVotes) * 100).toFixed(0)