diff --git a/contract/contracts/event_registry/src/error.rs b/contract/contracts/event_registry/src/error.rs index a060ad58..15081503 100644 --- a/contract/contracts/event_registry/src/error.rs +++ b/contract/contracts/event_registry/src/error.rs @@ -55,4 +55,5 @@ pub enum EventRegistryError { InvalidCategoryId = 71, AlreadyOnWaitlist = 75, TooManyTiers = 80, + TooManyIds = 81, } diff --git a/contract/contracts/event_registry/src/lib.rs b/contract/contracts/event_registry/src/lib.rs index 75d3a94f..ff877b06 100644 --- a/contract/contracts/event_registry/src/lib.rs +++ b/contract/contracts/event_registry/src/lib.rs @@ -533,6 +533,18 @@ impl EventRegistry { storage::get_event(&env, event_id) } + /// Retrieves a batch of events by their IDs (max 50). + pub fn get_events_batch(env: Env, event_ids: Vec) -> Result>, EventRegistryError> { + if event_ids.len() > 50 { + return Err(EventRegistryError::TooManyIds); + } + let mut results = Vec::new(&env); + for id in event_ids.into_iter() { + results.push_back(storage::get_event(&env, id)); + } + Ok(results) + } + /// Returns the total number of tickets sold across all events. pub fn get_global_tickets_sold(env: Env) -> i128 { storage::get_global_tickets_sold(&env) @@ -1708,16 +1720,16 @@ impl EventRegistry { let mut proposal = storage::get_proposal(&env, proposal_id).ok_or(EventRegistryError::MultisigError)?; - // Check if already executed - if proposal.executed { - return Err(EventRegistryError::ProposalAlreadyExecuted); - } - // Check if expired if env.ledger().timestamp() > proposal.expires_at { return Err(EventRegistryError::ProposalExpired); } + // Check if already executed + if proposal.executed { + return Err(EventRegistryError::ProposalAlreadyExecuted); + } + // Check if already approved by this admin if proposal.approvals.contains(&approver) { return Ok(()); // Already approved, no-op diff --git a/contract/contracts/event_registry/src/test.rs b/contract/contracts/event_registry/src/test.rs index a4517203..91efd603 100644 --- a/contract/contracts/event_registry/src/test.rs +++ b/contract/contracts/event_registry/src/test.rs @@ -4471,3 +4471,20 @@ fn test_propose_add_admin_new_address_succeeds() { assert!(!proposal.executed); assert_eq!(proposal.change, crate::types::ParameterChange::AddAdmin(new_admin)); } + +#[test] +fn test_get_events_batch_limit() { + let env = Env::default(); + env.mock_all_auths(); + + let mut event_ids = soroban_sdk::Vec::new(&env); + for i in 0..51 { + event_ids.push_back(soroban_sdk::String::from_str(&env, "event_id")); + } + + let contract_id = env.register_contract(None, EventRegistryContract); + let client = EventRegistryContractClient::new(&env, &contract_id); + + let res = client.try_get_events_batch(&event_ids); + assert_eq!(res, Err(Ok(EventRegistryError::TooManyIds))); +} diff --git a/contract/contracts/ticket_payment/src/contract.rs b/contract/contracts/ticket_payment/src/contract.rs index bd5fdcca..907a71b9 100644 --- a/contract/contracts/ticket_payment/src/contract.rs +++ b/contract/contracts/ticket_payment/src/contract.rs @@ -315,14 +315,14 @@ impl TicketPaymentContract { let mut proposal = get_proposal(&env, proposal_id).ok_or(TicketPaymentError::InvalidProposal)?; - if proposal.status != ProposalStatus::Pending { - return Err(TicketPaymentError::ProposalNotActive); - } - if env.ledger().timestamp() > proposal.expires_at { return Err(TicketPaymentError::ProposalExpired); } + if proposal.status != ProposalStatus::Pending { + return Err(TicketPaymentError::ProposalNotActive); + } + if proposal.voters.contains(&voter) { return Err(TicketPaymentError::AlreadyVoted); } @@ -353,16 +353,16 @@ impl TicketPaymentContract { let mut proposal = get_proposal(&env, proposal_id).ok_or(TicketPaymentError::InvalidProposal)?; - if proposal.status != ProposalStatus::Pending { - return Err(TicketPaymentError::ProposalNotActive); - } - let current_time = env.ledger().timestamp(); if current_time > proposal.expires_at { return Err(TicketPaymentError::ProposalExpired); } + if proposal.status != ProposalStatus::Pending { + return Err(TicketPaymentError::ProposalNotActive); + } + // 48 hours = 48 * 60 * 60 = 172800 seconds if current_time < proposal.created_at + 172800 { return Err(TicketPaymentError::VotingPeriodNotMet); diff --git a/server/migrations/20260629000001_add_is_verified_to_organizers.sql b/server/migrations/20260629000001_add_is_verified_to_organizers.sql new file mode 100644 index 00000000..8c62d2e4 --- /dev/null +++ b/server/migrations/20260629000001_add_is_verified_to_organizers.sql @@ -0,0 +1,2 @@ +-- Add is_verified to organizers +ALTER TABLE organizers ADD COLUMN is_verified BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/server/src/handlers/events.rs b/server/src/handlers/events.rs index 0f901d9c..d6eb476b 100644 --- a/server/src/handlers/events.rs +++ b/server/src/handlers/events.rs @@ -1607,7 +1607,7 @@ pub async fn list_similar_events( let limit = params.limit.unwrap_or(4).clamp(1, 10) as i64; // Fetch the source event to get its location (category via join below). - let source = match sqlx::query_as::<_, Event>("SELECT * FROM events WHERE id = $1") + let source = match sqlx::query_as::<_, Event>("SELECT * FROM events WHERE id = $1 AND is_flagged = FALSE") .bind(event_id) .fetch_optional(&state.pool) .await @@ -1872,7 +1872,7 @@ pub async fn submit_event_rating( let (ticket_status, ticket_event_id) = ticket; let event_exists = - match sqlx::query_scalar::<_, bool>("SELECT EXISTS(SELECT 1 FROM events WHERE id = $1)") + match sqlx::query_scalar::<_, bool>("SELECT EXISTS(SELECT 1 FROM events WHERE id = $1 AND is_flagged = FALSE)") .bind(event_id) .fetch_one(&state.pool) .await @@ -2485,7 +2485,7 @@ pub async fn get_event_social_proof( // Check if event exists let event_exists = - match sqlx::query_scalar::<_, bool>("SELECT EXISTS(SELECT 1 FROM events WHERE id = $1)") + match sqlx::query_scalar::<_, bool>("SELECT EXISTS(SELECT 1 FROM events WHERE id = $1 AND is_flagged = FALSE)") .bind(event_id) .fetch_one(&state.pool) .await @@ -2517,7 +2517,7 @@ pub async fn get_event_social_proof( // Average rating from events table async { sqlx::query_as::<_, (i64, i32)>( - "SELECT sum_of_ratings, count_of_ratings FROM events WHERE id = $1", + "SELECT sum_of_ratings, count_of_ratings FROM events WHERE id = $1 AND is_flagged = FALSE", ) .bind(event_id) .fetch_one(&state.pool) @@ -2536,7 +2536,7 @@ pub async fn get_event_social_proof( // Tickets remaining (total_tickets - minted_tickets) async { sqlx::query_scalar::<_, i64>( - "SELECT total_tickets - minted_tickets FROM events WHERE id = $1", + "SELECT total_tickets - minted_tickets FROM events WHERE id = $1 AND is_flagged = FALSE", ) .bind(event_id) .fetch_one(&state.pool) @@ -2587,7 +2587,7 @@ pub async fn get_attendee_count( Path(event_id): Path, ) -> Response { let row = match sqlx::query_as::<_, (i64, i64)>( - "SELECT minted_tickets, total_tickets FROM events WHERE id = $1", + "SELECT minted_tickets, total_tickets FROM events WHERE id = $1 AND is_flagged = FALSE", ) .bind(event_id) .fetch_optional(&state.pool) @@ -2625,7 +2625,7 @@ pub async fn get_event_revenue( ) -> Response { // 404 if event doesn't exist let exists = - match sqlx::query_scalar::<_, bool>("SELECT EXISTS(SELECT 1 FROM events WHERE id = $1)") + match sqlx::query_scalar::<_, bool>("SELECT EXISTS(SELECT 1 FROM events WHERE id = $1 AND is_flagged = FALSE)") .bind(event_id) .fetch_one(&state.pool) .await @@ -2992,7 +2992,7 @@ pub async fn list_event_ratings( Query(pagination): Query, ) -> Response { let exists = match sqlx::query_scalar::<_, bool>( - "SELECT EXISTS(SELECT 1 FROM events WHERE id = $1)", + "SELECT EXISTS(SELECT 1 FROM events WHERE id = $1 AND is_flagged = FALSE)", ) .bind(event_id) .fetch_one(&state.pool) @@ -3066,7 +3066,7 @@ pub async fn get_ratings_summary( // 404 if event doesn't exist let exists = - match sqlx::query_scalar::<_, bool>("SELECT EXISTS(SELECT 1 FROM events WHERE id = $1)") + match sqlx::query_scalar::<_, bool>("SELECT EXISTS(SELECT 1 FROM events WHERE id = $1 AND is_flagged = FALSE)") .bind(event_id) .fetch_one(&state.pool) .await @@ -3312,7 +3312,7 @@ pub async fn export_attendees_csv( ) -> Response { // Verify the event exists let event_exists = - match sqlx::query_scalar::<_, bool>("SELECT EXISTS(SELECT 1 FROM events WHERE id = $1)") + match sqlx::query_scalar::<_, bool>("SELECT EXISTS(SELECT 1 FROM events WHERE id = $1 AND is_flagged = FALSE)") .bind(event_id) .fetch_one(&state.pool) .await @@ -3417,7 +3417,7 @@ pub async fn list_event_tickets( ) -> Response { // 404 if event does not exist. let event_exists = - match sqlx::query_scalar::<_, bool>("SELECT EXISTS(SELECT 1 FROM events WHERE id = $1)") + match sqlx::query_scalar::<_, bool>("SELECT EXISTS(SELECT 1 FROM events WHERE id = $1 AND is_flagged = FALSE)") .bind(event_id) .fetch_one(&state.pool) .await @@ -3609,7 +3609,7 @@ pub async fn list_ticket_tiers( Path(event_id): Path, ) -> Response { let event_exists = - match sqlx::query_scalar::<_, bool>("SELECT EXISTS(SELECT 1 FROM events WHERE id = $1)") + match sqlx::query_scalar::<_, bool>("SELECT EXISTS(SELECT 1 FROM events WHERE id = $1 AND is_flagged = FALSE)") .bind(event_id) .fetch_one(&state.pool) .await diff --git a/server/src/handlers/soroban_listener.rs b/server/src/handlers/soroban_listener.rs index 81dabce3..d92839ef 100644 --- a/server/src/handlers/soroban_listener.rs +++ b/server/src/handlers/soroban_listener.rs @@ -330,6 +330,14 @@ async fn process_event( "event_status_updated" | "event_cancelled" => { handle_event_status_updated(pool, event).await?; } + // Emitted by EventRegistry::stake_collateral + "collateral_staked" | "CollateralStaked" => { + handle_collateral_staked(pool, event).await?; + } + // Emitted by EventRegistry::unstake_collateral + "collateral_unstaked" | "CollateralUnstaked" => { + handle_collateral_unstaked(pool, event).await?; + } _ => { tracing::debug!("Unhandled event_registry event: {}", event_name); } @@ -472,6 +480,48 @@ async fn handle_event_status_updated(_pool: &PgPool, event: &SorobanEvent) -> Re Ok(()) } +async fn handle_collateral_staked(pool: &PgPool, event: &SorobanEvent) -> Result<(), String> { + let data = &event.value; + let organizer_wallet = data + .get("organizer") + .and_then(|v| v.as_str()) + .unwrap_or_default(); + let is_verified = data + .get("is_verified") + .and_then(|v| v.as_bool()) + .unwrap_or_default(); + + if !organizer_wallet.is_empty() { + sqlx::query("UPDATE organizers SET is_verified = $1, updated_at = NOW() WHERE wallet_address = $2") + .bind(is_verified) + .bind(organizer_wallet) + .execute(pool) + .await + .map_err(|e| format!("Failed to update organizer verified status: {}", e))?; + } + + Ok(()) +} + +async fn handle_collateral_unstaked(pool: &PgPool, event: &SorobanEvent) -> Result<(), String> { + let data = &event.value; + let organizer_wallet = data + .get("organizer") + .and_then(|v| v.as_str()) + .unwrap_or_default(); + + if !organizer_wallet.is_empty() { + sqlx::query("UPDATE organizers SET is_verified = FALSE, updated_at = NOW() WHERE wallet_address = $1") + .bind(organizer_wallet) + .execute(pool) + .await + .map_err(|e| format!("Failed to reset organizer verified status: {}", e))?; + } + + Ok(()) +} + + // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- diff --git a/server/src/models/organizer.rs b/server/src/models/organizer.rs index be8091fb..bcc6acca 100644 --- a/server/src/models/organizer.rs +++ b/server/src/models/organizer.rs @@ -26,6 +26,8 @@ pub struct Organizer { pub wallet_address: Option, /// Timestamp when the organizer account was created. pub created_at: DateTime, + /// Whether the organizer is verified by staking the required collateral. + pub is_verified: bool, /// Timestamp of the last update to this record. Managed by a DB trigger. pub updated_at: DateTime, }