- {noPollSelected && !isEnded &&
Select your favorite
}
+
+ {noPollSelected && !isEnded && (
+
+ {hasVotedInCurrentRound ? 'Select an option to update your vote' : 'Select your favorite'}
+
+ )}
)}
diff --git a/examples/CRISP/client/src/pages/RoundPoll/RoundPoll.tsx b/examples/CRISP/client/src/pages/RoundPoll/RoundPoll.tsx
new file mode 100644
index 0000000000..c4b9d13f54
--- /dev/null
+++ b/examples/CRISP/client/src/pages/RoundPoll/RoundPoll.tsx
@@ -0,0 +1,61 @@
+// 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 React, { Fragment, useEffect, useMemo, useState } from 'react'
+import { useParams, useNavigate } from 'react-router-dom'
+import DailyPollSection from '@/pages/Landing/components/DailyPoll'
+import { useVoteManagementContext } from '@/context/voteManagement'
+import { convertTimestampToDate } from '@/utils/methods'
+import LoadingAnimation from '@/components/LoadingAnimation'
+
+const RoundPoll: React.FC = () => {
+ const { roundId } = useParams<{ roundId: string }>()
+ const navigate = useNavigate()
+ const { roundState, getRoundStateLite, isLoading, currentRoundId } = useVoteManagementContext()
+ const [loading, setLoading] = useState(true)
+
+ const parsedRoundId = roundId ? parseInt(roundId, 10) : null
+ const isValidRoundId = parsedRoundId !== null && !isNaN(parsedRoundId)
+
+ // If this is the current round, redirect to /current
+ useEffect(() => {
+ if (isValidRoundId && currentRoundId !== null && parsedRoundId === currentRoundId) {
+ navigate('/current', { replace: true })
+ }
+ }, [isValidRoundId, parsedRoundId, currentRoundId, navigate])
+
+ // Load the specific round
+ useEffect(() => {
+ const loadRound = async () => {
+ if (isValidRoundId && parsedRoundId !== null) {
+ setLoading(true)
+ await getRoundStateLite(parsedRoundId)
+ setLoading(false)
+ }
+ }
+ loadRound()
+ }, [isValidRoundId, parsedRoundId, getRoundStateLite])
+
+ const endTime = useMemo(() => (roundState ? convertTimestampToDate(roundState.start_time, roundState.duration) : null), [roundState])
+
+ const title = `Round #${roundId}`
+
+ if (loading || isLoading) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+ )
+}
+
+export default RoundPoll
diff --git a/examples/CRISP/client/src/pages/RoundPoll/index.ts b/examples/CRISP/client/src/pages/RoundPoll/index.ts
new file mode 100644
index 0000000000..3e153c5c32
--- /dev/null
+++ b/examples/CRISP/client/src/pages/RoundPoll/index.ts
@@ -0,0 +1,7 @@
+// 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.
+
+export { default } from './RoundPoll'
diff --git a/examples/CRISP/client/src/utils/methods.ts b/examples/CRISP/client/src/utils/methods.ts
index 18d1ef8a1f..a606354a8c 100644
--- a/examples/CRISP/client/src/utils/methods.ts
+++ b/examples/CRISP/client/src/utils/methods.ts
@@ -17,7 +17,7 @@ export const markWinner = (options: PollOption[]) => {
export const convertTimestampToDate = (timestamp: number, secondsToAdd: number = 0): Date => {
const date = new Date(timestamp * 1000)
- date.setSeconds(date.getMinutes() + secondsToAdd)
+ date.setSeconds(date.getSeconds() + secondsToAdd)
return date
}
@@ -48,7 +48,7 @@ export const formatDate = (isoDateString: string): string => {
hour12: true,
})
- return `${dateFormatter.format(date)} - ${timeFormatter.format(date)}`
+ return `${dateFormatter.format(date)} - ${timeFormatter.format(date)}`
}
export const convertPollData = (request: PollRequestResult[]): PollResult[] => {
@@ -113,9 +113,9 @@ export const convertVoteStateLite = (voteState: VoteStateLite): PollResult => {
}
}
-export const debounce = (func: (...args: any[]) => void, wait: number) => {
+export const debounce =
void>(func: T, wait: number) => {
let timeout: ReturnType
- return (...args: any[]) => {
+ return (...args: Parameters) => {
clearTimeout(timeout)
timeout = setTimeout(() => func(...args), wait)
}
diff --git a/examples/CRISP/packages/crisp-sdk/src/types.ts b/examples/CRISP/packages/crisp-sdk/src/types.ts
index 550eb043f6..52a6209619 100644
--- a/examples/CRISP/packages/crisp-sdk/src/types.ts
+++ b/examples/CRISP/packages/crisp-sdk/src/types.ts
@@ -192,4 +192,5 @@ export type VoteProofInputs = {
balance: bigint
vote: Vote
signature: `0x${string}`
+ previousCiphertext?: Uint8Array
}
diff --git a/examples/CRISP/scripts/dev_server.sh b/examples/CRISP/scripts/dev_server.sh
index 928ad6c88b..3e1aa838bf 100755
--- a/examples/CRISP/scripts/dev_server.sh
+++ b/examples/CRISP/scripts/dev_server.sh
@@ -4,4 +4,4 @@ set -e
export CARGO_INCREMENTAL=1
-(cd ./server && cargo run --bin server)
+(cd ./server && rm -rf database && cargo run --bin server)
diff --git a/examples/CRISP/server/src/server/models.rs b/examples/CRISP/server/src/server/models.rs
index d10c083c46..9cc3fc8f0c 100644
--- a/examples/CRISP/server/src/server/models.rs
+++ b/examples/CRISP/server/src/server/models.rs
@@ -54,6 +54,22 @@ pub struct VoteResponse {
pub status: VoteResponseStatus,
pub tx_hash: Option,
pub message: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub is_vote_update: Option,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct VoteStatusRequest {
+ pub round_id: u64,
+ pub address: String,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct VoteStatusResponse {
+ pub round_id: u64,
+ pub address: String,
+ pub has_voted: bool,
+ pub round_status: Option,
}
#[derive(Debug, Deserialize, Serialize)]
diff --git a/examples/CRISP/server/src/server/repo.rs b/examples/CRISP/server/src/server/repo.rs
index 1c417092d3..264b7a296d 100644
--- a/examples/CRISP/server/src/server/repo.rs
+++ b/examples/CRISP/server/src/server/repo.rs
@@ -94,9 +94,19 @@ impl CrispE3Repository {
pub async fn insert_ciphertext_input(&mut self, vote: Vec, index: u64) -> Result<()> {
let key = self.crisp_key();
- self.store.modify(&key, |e3_obj: Option| {
+ self.store
+ .modify(&key, |e3_obj: Option| {
e3_obj.map(|mut e| {
- e.ciphertext_inputs.push((vote.clone(), index));
+ // We check if we already have a vote at this index (re-vote case)
+ // If we do, we update the vote
+ // If we don't, we append the vote
+ if let Some(existing) =
+ e.ciphertext_inputs.iter_mut().find(|(_, i)| *i == index)
+ {
+ existing.0 = vote.clone();
+ } else {
+ e.ciphertext_inputs.push((vote.clone(), index));
+ }
e
})
})
@@ -137,7 +147,7 @@ impl CrispE3Repository {
pub async fn get_vote_count(&self) -> Result {
let e3_crisp = self.get_crisp().await?;
- Ok(u64::try_from(e3_crisp.ciphertext_inputs.len())?)
+ Ok(u64::try_from(e3_crisp.has_voted.len())?)
}
pub async fn update_status(&mut self, value: &str) -> Result<()> {
@@ -205,7 +215,7 @@ impl CrispE3Repository {
status: e3_crisp.status,
chain_id: e3.chain_id,
duration: e3.duration,
- vote_count: u64::try_from(e3_crisp.ciphertext_inputs.len())?,
+ vote_count: u64::try_from(e3_crisp.has_voted.len())?,
start_time: e3_crisp.start_time,
start_block: e3.request_block,
enclave_address: e3.enclave_address,
@@ -219,7 +229,7 @@ impl CrispE3Repository {
let e3_crisp = self.get_crisp().await?;
Ok(e3_crisp.ciphertext_inputs)
}
-
+
pub async fn set_ciphertext_output(&mut self, data: Vec) -> Result<()> {
self.get_e3_repo().set_ciphertext_output(data).await?;
Ok(())
diff --git a/examples/CRISP/server/src/server/routes/voting.rs b/examples/CRISP/server/src/server/routes/voting.rs
index d75ef36629..0484a8097b 100644
--- a/examples/CRISP/server/src/server/routes/voting.rs
+++ b/examples/CRISP/server/src/server/routes/voting.rs
@@ -5,29 +5,78 @@
// or FITNESS FOR A PARTICULAR PURPOSE.
use crate::server::{
- app_data::AppData,
- database::SledDB,
- models::{VoteRequest, VoteResponse, VoteResponseStatus},
- repo::CrispE3Repository,
- CONFIG,
+ CONFIG, app_data::AppData, database::SledDB, models::{
+ VoteRequest, VoteResponse, VoteResponseStatus, VoteStatusRequest, VoteStatusResponse
+ }, repo::CrispE3Repository
};
use actix_web::{web, HttpResponse, Responder};
-use alloy::primitives::{Bytes, U256};
+use alloy::{
+ primitives::{Bytes, U256},
+};
use e3_sdk::evm_helpers::contracts::{EnclaveContract, EnclaveWrite};
use eyre::Error;
use log::{error, info};
pub fn setup_routes(config: &mut web::ServiceConfig) {
config.service(
- web::scope("/voting").route("/broadcast", web::post().to(broadcast_encrypted_vote)),
+ web::scope("/voting")
+ .route("/broadcast", web::post().to(broadcast_encrypted_vote))
+ .route("/status", web::post().to(get_vote_status)),
);
}
+/// Get the vote status for a user in a specific round
+///
+/// # Arguments
+///
+/// * `VoteStatusRequest` - The request containing round_id and address
+///
+/// # Returns
+///
+/// * A JSON response with the vote status
+async fn get_vote_status(
+ data: web::Json,
+ store: web::Data,
+) -> impl Responder {
+ let request = data.into_inner();
+ info!(
+ "[e3_id={}] Checking vote status for address: {}",
+ request.round_id, request.address
+ );
+
+ let has_voted = match store
+ .e3(request.round_id)
+ .has_voted(request.address.clone())
+ .await
+ {
+ Ok(voted) => voted,
+ Err(e) => {
+ error!(
+ "[e3_id={}] Database error checking vote status: {:?}",
+ request.round_id, e
+ );
+ return HttpResponse::InternalServerError().json("Internal server error");
+ }
+ };
+
+ let round_status = match store.e3(request.round_id).get_e3_state_lite().await {
+ Ok(state) => Some(state.status),
+ Err(_) => None,
+ };
+
+ HttpResponse::Ok().json(VoteStatusResponse {
+ round_id: request.round_id,
+ address: request.address,
+ has_voted,
+ round_status,
+ })
+}
+
/// Broadcast an encrypted vote to the blockchain
///
/// # Arguments
///
-/// * `VoteRequest` - The vote data to be broadcast
+/// * `EncryptedVote` - The vote data to be broadcast
///
/// # Returns
///
@@ -36,72 +85,70 @@ async fn broadcast_encrypted_vote(
data: web::Json,
store: web::Data,
) -> impl Responder {
- let vote_request = data.into_inner();
- info!(
- "[e3_id={}] Broadcasting encrypted vote",
- vote_request.round_id
- );
- // Validate and update vote status
+ let vote = data.into_inner();
+ info!("[e3_id={}] Broadcasting encrypted vote", vote.round_id);
+
+ // Check if user has already voted
let has_voted = match store
- .e3(vote_request.round_id)
- .has_voted(vote_request.address.clone())
+ .e3(vote.round_id)
+ .has_voted(vote.address.clone())
.await
{
Ok(voted) => voted,
Err(e) => {
error!(
"[e3_id={}] Database error checking vote status: {:?}",
- vote_request.round_id, e
+ vote.round_id, e
);
return HttpResponse::InternalServerError().json("Internal server error");
}
};
- if has_voted {
- info!("[e3_id={}] User has already voted", vote_request.round_id);
- return HttpResponse::Ok().json(VoteResponse {
- status: VoteResponseStatus::UserAlreadyVoted,
- tx_hash: None,
- message: Some("User Has Already Voted".to_string()),
- });
+ let is_vote_update = has_voted;
+ if is_vote_update {
+ info!("[e3_id={}] User is updating their vote", vote.round_id);
}
- let mut repo = store.e3(vote_request.round_id);
+ let mut repo = store.e3(vote.round_id);
- if let Err(e) = repo
- .insert_voter_address(vote_request.address.clone())
- .await
- {
- error!(
- "[e3_id={}] Database error inserting voter: {:?}",
- vote_request.round_id, e
- );
- return HttpResponse::InternalServerError().json("Internal server error");
+ if !has_voted {
+ if let Err(e) = repo.insert_voter_address(vote.address.clone()).await {
+ error!(
+ "[e3_id={}] Database error inserting voter: {:?}",
+ vote.round_id, e
+ );
+ return HttpResponse::InternalServerError().json("Internal server error");
+ }
}
- let e3_id = U256::from(vote_request.round_id);
+ let e3_id = U256::from(vote.round_id);
// encoded_proof is already encoded in JavaScript, just decode from hex
- let hex_str = vote_request
+ let hex_str = vote
.encoded_proof
.strip_prefix("0x")
- .unwrap_or(&vote_request.encoded_proof);
+ .unwrap_or(&vote.encoded_proof);
let encoded_proof = match hex::decode(hex_str) {
Ok(decoded) => Bytes::from(decoded),
Err(e) => {
error!(
"[e3_id={}] Failed to decode encoded_proof: {:?}",
- vote_request.round_id, e
+ vote.round_id, e
);
// Rollback voter insertion before returning error
- let _ = match repo.remove_voter_address(&vote_request.address).await {
- Ok(_) => (),
- Err(e) => error!("Error rolling back the vote: {e}"),
- };
+
+ if !is_vote_update {
+ let _ = match repo.remove_voter_address(&vote.address).await {
+ Ok(_) => (),
+ Err(e) => error!("Error rolling back the vote: {e}"),
+ };
+ }
+
return HttpResponse::BadRequest().json(VoteResponse {
status: VoteResponseStatus::FailedBroadcast,
tx_hash: None,
message: Some("Invalid hex encoded proof".to_string()),
+ is_vote_update: Some(is_vote_update),
});
}
};
@@ -116,33 +163,57 @@ async fn broadcast_encrypted_vote(
{
Ok(c) => c,
Err(e) => {
- error!(
- "[e3_id={}] Contract creation failed: {:?}",
- vote_request.round_id, e
- );
- // Rollback voter insertion before returning error
- let _ = match repo.remove_voter_address(&vote_request.address).await {
- Ok(_) => (),
- Err(e) => error!("Error rolling back the vote: {e}"),
- };
+ error!("[e3_id={}] Contract creation error: {:?}", vote.round_id, e);
return HttpResponse::InternalServerError().json("Internal server error");
}
};
match contract.publish_input(e3_id, encoded_proof).await {
Ok(hash) => {
+ let message = if is_vote_update {
+ "Vote Updated Successfully"
+ } else {
+ "Vote Successful"
+ };
info!(
- "[e3_id={}] Vote broadcasted successfully",
- vote_request.round_id
+ "[e3_id={}] Vote broadcasted successfully (update: {})",
+ vote.round_id, is_vote_update
);
HttpResponse::Ok().json(VoteResponse {
status: VoteResponseStatus::Success,
tx_hash: Some(hash.transaction_hash.to_string()),
- message: Some("Vote Successful".to_string()),
+ message: Some(message.to_string()),
+ is_vote_update: Some(is_vote_update),
})
}
- Err(e) => handle_vote_error(e, repo, &vote_request.address).await,
+ Err(e) => handle_vote_error(e, repo, &vote.address, has_voted).await,
+ }
+}
+
+/// Extract an error message from an error
+fn extract_error_message(e: &Error) -> String {
+ let error_str = e.to_string();
+
+ if error_str.contains("Internal error") || error_str.contains("-32603") {
+ return "Transaction rejected by the blockchain".to_string();
+ }
+ if error_str.contains("insufficient funds") {
+ return "Insufficient funds to process transaction".to_string();
+ }
+ if error_str.contains("nonce") {
+ return "Transaction conflict, please try again".to_string();
}
+ if error_str.contains("gas") {
+ return "Transaction failed due to gas issues".to_string();
+ }
+ if error_str.contains("reverted") {
+ return "Transaction was reverted by the contract".to_string();
+ }
+ if error_str.contains("timeout") || error_str.contains("Timeout") {
+ return "Transaction timed out, please try again".to_string();
+ }
+
+ "Transaction failed, please try again".to_string()
}
/// Handle the vote error
@@ -150,25 +221,32 @@ async fn broadcast_encrypted_vote(
/// # Arguments
///
/// * `e` - The error that occurred
-/// * `state_data` - The state data to be rolled back
-/// * `key` - The key for the state data
+/// * `repo` - The repository to rollback
/// * `address` - The address for the vote
+/// * `was_update` - Whether this was a vote update (don't rollback if true)
async fn handle_vote_error(
e: Error,
mut repo: CrispE3Repository,
address: &str,
+ was_update: bool,
) -> HttpResponse {
- info!("Error while sending vote transaction: {:?}", e);
+ // Log the full error for debugging
+ error!("Error while sending vote transaction: {:?}", e);
- // Rollback the vote
- match repo.remove_voter_address(address).await {
- Ok(_) => (),
- Err(err) => error!("Error rolling back the vote: {err}"),
- };
+ // Only rollback the vote if this was a new vote, not an update
+ if !was_update {
+ match repo.remove_voter_address(address).await {
+ Ok(_) => (),
+ Err(err) => error!("Error rolling back the vote: {err}"),
+ };
+ }
+
+ let user_message = extract_error_message(&e);
HttpResponse::Ok().json(VoteResponse {
status: VoteResponseStatus::FailedBroadcast,
tx_hash: None,
- message: Some("Failed to broadcast vote".to_string()),
+ message: Some(user_message),
+ is_vote_update: Some(was_update),
})
}