diff --git a/contracts/event/src/storage.rs b/contracts/event/src/storage.rs index 601dd30..b2309e1 100644 --- a/contracts/event/src/storage.rs +++ b/contracts/event/src/storage.rs @@ -6,8 +6,15 @@ use crate::types::{ use soroban_sdk::{contracttype, Address, BytesN, Env, Symbol, Vec}; const CURRENT_VERSION: u32 = 1; -const TTL_THRESHOLD: u32 = 60 * 60 * 24 * 30; -const TTL_BUMP: u32 = 60 * 60 * 24 * 30 * 2; +/// TTL refresh threshold in ledgers (~30 days at 5s/ledger). +const TTL_THRESHOLD: u32 = 518_400; +/// TTL extension target in ledgers (~60 days at 5s/ledger), well within the +/// network maximum of 3,110,400 ledgers. +const TTL_BUMP: u32 = 1_036_800; +/// Reservations expire after 15 minutes; keep their entries on a ~1h/2h +/// ledger-based schedule so they outlive the reservation window. +const RESERVATION_TTL_THRESHOLD: u32 = 720; +const RESERVATION_TTL_BUMP: u32 = 1_440; #[contracttype] pub enum DataKey { @@ -44,17 +51,23 @@ pub fn event_exists(env: &Env, event_id: &Symbol) -> bool { .has(&DataKey::Event(event_id.clone())) } pub fn get_event(env: &Env, event_id: &Symbol) -> Result { + let key = DataKey::Event(event_id.clone()); + let event = env + .storage() + .persistent() + .get(&key) + .ok_or(EventError::EventNotFound)?; env.storage() .persistent() - .get(&DataKey::Event(event_id.clone())) - .ok_or(EventError::EventNotFound) + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + Ok(event) } pub fn save_event(env: &Env, event_id: &Symbol, event: &Event) { let key = DataKey::Event(event_id.clone()); env.storage().persistent().set(&key, event); env.storage() .persistent() - .extend_ttl(&key, 60 * 60 * 24 * 30, 60 * 60 * 24 * 30 * 2); + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); } pub fn update_event(env: &Env, event_id: &Symbol, event: &Event) -> Result<(), EventError> { if !event_exists(env, event_id) { @@ -137,44 +150,66 @@ pub fn get_attendees_paginated( pub fn set_admin(env: &Env, admin: &Address) { env.storage().persistent().set(&DataKey::Admin, admin); - env.storage().persistent().extend_ttl( - &DataKey::Admin, - 60 * 60 * 24 * 30, - 60 * 60 * 24 * 30 * 2, - ); + env.storage() + .persistent() + .extend_ttl(&DataKey::Admin, TTL_THRESHOLD, TTL_BUMP); } pub fn get_admin(env: &Env) -> Result { + let key = DataKey::Admin; + let admin = env + .storage() + .persistent() + .get(&key) + .ok_or(EventError::ContractLinksNotConfigured)?; env.storage() .persistent() - .get(&DataKey::Admin) - .ok_or(EventError::ContractLinksNotConfigured) + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + Ok(admin) } pub fn set_ticket_contract(env: &Env, ticket_contract: &Address) { env.storage() .persistent() .set(&DataKey::TicketContract, ticket_contract); + env.storage() + .persistent() + .extend_ttl(&DataKey::TicketContract, TTL_THRESHOLD, TTL_BUMP); } pub fn set_payments_contract(env: &Env, payments_contract: &Address) { env.storage() .persistent() .set(&DataKey::PaymentsContract, payments_contract); + env.storage() + .persistent() + .extend_ttl(&DataKey::PaymentsContract, TTL_THRESHOLD, TTL_BUMP); } pub fn get_ticket_contract(env: &Env) -> Result { + let key = DataKey::TicketContract; + let address = env + .storage() + .persistent() + .get(&key) + .ok_or(EventError::ContractLinksNotConfigured)?; env.storage() .persistent() - .get(&DataKey::TicketContract) - .ok_or(EventError::ContractLinksNotConfigured) + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + Ok(address) } pub fn get_payments_contract(env: &Env) -> Result { + let key = DataKey::PaymentsContract; + let address = env + .storage() + .persistent() + .get(&key) + .ok_or(EventError::ContractLinksNotConfigured)?; env.storage() .persistent() - .get(&DataKey::PaymentsContract) - .ok_or(EventError::ContractLinksNotConfigured) + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + Ok(address) } pub fn has_linked_contracts(env: &Env) -> bool { @@ -192,7 +227,7 @@ pub fn save_reservation( env.storage().persistent().set(&key, reservation); env.storage() .persistent() - .extend_ttl(&key, 60 * 60, 60 * 60 * 2); + .extend_ttl(&key, RESERVATION_TTL_THRESHOLD, RESERVATION_TTL_BUMP); } pub fn get_reservation( @@ -201,10 +236,15 @@ pub fn get_reservation( attendee: &Address, ) -> Result { let key = DataKey::Reservation(event_id.clone(), attendee.clone()); - env.storage() + let reservation = env + .storage() .persistent() .get(&key) - .ok_or(EventError::ReservationNotFound) + .ok_or(EventError::ReservationNotFound)?; + env.storage() + .persistent() + .extend_ttl(&key, RESERVATION_TTL_THRESHOLD, RESERVATION_TTL_BUMP); + Ok(reservation) } pub fn remove_reservation(env: &Env, event_id: &Symbol, attendee: &Address) { @@ -238,14 +278,20 @@ pub fn set_event_privacy(env: &Env, event_id: &Symbol, level: &PrivacyLevel) { env.storage().persistent().set(&key, level); env.storage() .persistent() - .extend_ttl(&key, 60 * 60 * 24 * 30, 60 * 60 * 24 * 30 * 2); + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); } pub fn get_event_privacy(env: &Env, event_id: &Symbol) -> PrivacyLevel { - env.storage() - .persistent() - .get(&DataKey::EventPrivacy(event_id.clone())) - .unwrap_or(PrivacyLevel::Standard) + let key = DataKey::EventPrivacy(event_id.clone()); + let privacy: Option = env.storage().persistent().get(&key); + // Only extend the TTL when the key already exists: extend_ttl on a missing + // key panics with Error(Storage, MissingValue). + if privacy.is_some() { + env.storage() + .persistent() + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + } + privacy.unwrap_or(PrivacyLevel::Standard) } pub fn has_reservation(env: &Env, event_id: &Symbol, attendee: &Address) -> bool { @@ -260,9 +306,14 @@ pub fn set_postponement(env: &Env, event_id: &Symbol, info: &PostponementInfo) { .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); } pub fn get_postponement(env: &Env, event_id: &Symbol) -> Option { - env.storage() - .persistent() - .get(&DataKey::Postponement(event_id.clone())) + let key = DataKey::Postponement(event_id.clone()); + let info = env.storage().persistent().get(&key); + if info.is_some() { + env.storage() + .persistent() + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + } + info } pub fn remove_postponement(env: &Env, event_id: &Symbol) { env.storage() diff --git a/contracts/factory/src/storage.rs b/contracts/factory/src/storage.rs index d3c6f96..580386f 100644 --- a/contracts/factory/src/storage.rs +++ b/contracts/factory/src/storage.rs @@ -2,8 +2,11 @@ use crate::errors::FactoryError; use crate::types::DeployedEvent; use soroban_sdk::{contracttype, Address, BytesN, Env, Symbol, Vec}; -const TTL_THRESHOLD: u32 = 60 * 60 * 24 * 30; -const TTL_BUMP: u32 = 60 * 60 * 24 * 30 * 2; +/// TTL refresh threshold in ledgers (~30 days at 5s/ledger). +const TTL_THRESHOLD: u32 = 518_400; +/// TTL extension target in ledgers (~60 days at 5s/ledger), well within the +/// network maximum of 3,110,400 ledgers. +const TTL_BUMP: u32 = 1_036_800; const CURRENT_VERSION: u32 = 1; #[contracttype] @@ -27,10 +30,16 @@ pub fn is_initialized(env: &Env) -> bool { } pub fn get_admin(env: &Env) -> Result { + let key = DataKey::Admin; + let admin = env + .storage() + .persistent() + .get(&key) + .ok_or(FactoryError::NotInitialized)?; env.storage() .persistent() - .get(&DataKey::Admin) - .ok_or(FactoryError::NotInitialized) + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + Ok(admin) } pub fn set_admin(env: &Env, admin: &Address) { @@ -41,10 +50,16 @@ pub fn set_admin(env: &Env, admin: &Address) { } pub fn get_event_wasm_hash(env: &Env) -> Result, FactoryError> { + let key = DataKey::EventWasm; + let hash = env + .storage() + .persistent() + .get(&key) + .ok_or(FactoryError::NotInitialized)?; env.storage() .persistent() - .get(&DataKey::EventWasm) - .ok_or(FactoryError::NotInitialized) + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + Ok(hash) } pub fn set_event_wasm_hash(env: &Env, hash: &BytesN<32>) { @@ -55,10 +70,16 @@ pub fn set_event_wasm_hash(env: &Env, hash: &BytesN<32>) { } pub fn get_ticket_contract(env: &Env) -> Result { + let key = DataKey::TicketContract; + let address = env + .storage() + .persistent() + .get(&key) + .ok_or(FactoryError::NotInitialized)?; env.storage() .persistent() - .get(&DataKey::TicketContract) - .ok_or(FactoryError::NotInitialized) + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + Ok(address) } pub fn set_ticket_contract(env: &Env, address: &Address) { @@ -71,10 +92,16 @@ pub fn set_ticket_contract(env: &Env, address: &Address) { } pub fn get_payments_contract(env: &Env) -> Result { + let key = DataKey::PaymentsContract; + let address = env + .storage() + .persistent() + .get(&key) + .ok_or(FactoryError::NotInitialized)?; env.storage() .persistent() - .get(&DataKey::PaymentsContract) - .ok_or(FactoryError::NotInitialized) + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + Ok(address) } pub fn set_payments_contract(env: &Env, address: &Address) { @@ -120,24 +147,46 @@ pub fn save_deployed_event(env: &Env, event: &DeployedEvent) -> Result<(), Facto } pub fn get_deployed_event(env: &Env, event_id: &Symbol) -> Result { + let key = DataKey::DeployedEvent(event_id.clone()); + let event = env + .storage() + .persistent() + .get(&key) + .ok_or(FactoryError::EventNotFoundInRegistry)?; env.storage() .persistent() - .get(&DataKey::DeployedEvent(event_id.clone())) - .ok_or(FactoryError::EventNotFoundInRegistry) + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + Ok(event) } pub fn get_all_event_ids(env: &Env) -> Vec { - env.storage() - .persistent() - .get(&DataKey::AllEvents) - .unwrap_or_else(|| Vec::new(env)) + let key = DataKey::AllEvents; + let events = env + .storage() + .persistent() + .get(&key) + .unwrap_or_else(|| Vec::new(env)); + if !events.is_empty() { + env.storage() + .persistent() + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + } + events } pub fn get_organizer_events(env: &Env, organizer: &Address) -> Vec { - env.storage() - .persistent() - .get(&DataKey::OrganizerEvents(organizer.clone())) - .unwrap_or_else(|| Vec::new(env)) + let key = DataKey::OrganizerEvents(organizer.clone()); + let events = env + .storage() + .persistent() + .get(&key) + .unwrap_or_else(|| Vec::new(env)); + if !events.is_empty() { + env.storage() + .persistent() + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + } + events } pub fn get_contract_version(env: &Env) -> u32 { env.storage() diff --git a/contracts/payments/src/storage.rs b/contracts/payments/src/storage.rs index bdd1516..ac350cb 100644 --- a/contracts/payments/src/storage.rs +++ b/contracts/payments/src/storage.rs @@ -4,9 +4,19 @@ use crate::types::{ }; use soroban_sdk::{contracttype, Address, BytesN, Env, Symbol, Vec}; -const TTL_THRESHOLD: u32 = 60 * 60 * 24 * 30; -const TTL_BUMP: u32 = 60 * 60 * 24 * 30 * 2; +/// TTL refresh threshold in ledgers (~30 days at 5s/ledger). +const TTL_THRESHOLD: u32 = 518_400; +/// TTL extension target in ledgers (~60 days at 5s/ledger), well within the +/// network maximum of 3,110,400 ledgers. +const TTL_BUMP: u32 = 1_036_800; const CURRENT_VERSION: u32 = 1; +/// Processed-nonce replay-protection entries: ~7-day/14-day ledger schedule. +const NONCE_TTL_THRESHOLD: u32 = 120_960; // ~7 days at 5s/ledger +const NONCE_TTL_BUMP: u32 = 241_920; // ~14 days at 5s/ledger +/// Spent-nullifier replay-protection entries: extend to the protocol maximum +/// (3,110,400 ledgers, ~180 days) since a replayed nullifier must never pass. +const NULLIFIER_TTL_THRESHOLD: u32 = 1_036_800; // ~60 days +const NULLIFIER_TTL_BUMP: u32 = 3_110_400; // protocol maximum TTL #[contracttype] #[derive(Clone)] @@ -123,9 +133,14 @@ pub fn set_event_status(env: &Env, event_id: &Symbol, status: &EventStatus) { } pub fn get_event_status(env: &Env, event_id: &Symbol) -> Option { - env.storage() - .persistent() - .get(&DataKey::EventStatus(event_id.clone())) + let key = DataKey::EventStatus(event_id.clone()); + let status = env.storage().persistent().get(&key); + if status.is_some() { + env.storage() + .persistent() + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + } + status } pub fn set_postpone_deadline(env: &Env, event_id: &Symbol, deadline_ledger: u32) { let key = DataKey::PostponeDeadline(event_id.clone()); @@ -148,19 +163,23 @@ pub fn remove_postpone_deadline(env: &Env, event_id: &Symbol) { .remove(&DataKey::PostponeDeadline(event_id.clone())); } pub fn get_admin(env: &Env) -> Result { + let key = DataKey::Admin; + let admin = env + .storage() + .persistent() + .get(&key) + .ok_or(PaymentError::NotInitialized)?; env.storage() .persistent() - .get(&DataKey::Admin) - .ok_or(PaymentError::NotInitialized) + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + Ok(admin) } pub fn set_admin(env: &Env, admin: &soroban_sdk::Address) { env.storage().persistent().set(&DataKey::Admin, admin); - env.storage().persistent().extend_ttl( - &DataKey::Admin, - 60 * 60 * 24 * 30, - 60 * 60 * 24 * 30 * 2, - ); + env.storage() + .persistent() + .extend_ttl(&DataKey::Admin, TTL_THRESHOLD, TTL_BUMP); } pub fn is_paused(env: &Env) -> bool { @@ -177,56 +196,68 @@ pub fn set_paused(env: &Env, paused: bool) { .extend_ttl(&DataKey::Paused, TTL_THRESHOLD, TTL_BUMP); } pub fn get_accepted_token(env: &Env) -> Result { + let key = DataKey::AcceptedToken; + let token = env + .storage() + .persistent() + .get(&key) + .ok_or(PaymentError::NotInitialized)?; env.storage() .persistent() - .get(&DataKey::AcceptedToken) - .ok_or(PaymentError::NotInitialized) + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + Ok(token) } pub fn set_accepted_token(env: &Env, token: &soroban_sdk::Address) { env.storage() .persistent() .set(&DataKey::AcceptedToken, token); - env.storage().persistent().extend_ttl( - &DataKey::AcceptedToken, - 60 * 60 * 24 * 30, - 60 * 60 * 24 * 30 * 2, - ); + env.storage() + .persistent() + .extend_ttl(&DataKey::AcceptedToken, TTL_THRESHOLD, TTL_BUMP); } pub fn get_event_contract(env: &Env) -> Result { + let key = DataKey::EventContract; + let contract = env + .storage() + .persistent() + .get(&key) + .ok_or(PaymentError::NotInitialized)?; env.storage() .persistent() - .get(&DataKey::EventContract) - .ok_or(PaymentError::NotInitialized) + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + Ok(contract) } pub fn set_event_contract(env: &Env, event_contract: &soroban_sdk::Address) { env.storage() .persistent() .set(&DataKey::EventContract, event_contract); - env.storage().persistent().extend_ttl( - &DataKey::EventContract, - 60 * 60 * 24 * 30, - 60 * 60 * 24 * 30 * 2, - ); + env.storage() + .persistent() + .extend_ttl(&DataKey::EventContract, TTL_THRESHOLD, TTL_BUMP); } pub fn get_ticket_contract(env: &Env) -> Result { + let key = DataKey::TicketContract; + let contract = env + .storage() + .persistent() + .get(&key) + .ok_or(PaymentError::NotInitialized)?; env.storage() .persistent() - .get(&DataKey::TicketContract) - .ok_or(PaymentError::NotInitialized) + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + Ok(contract) } pub fn set_ticket_contract(env: &Env, ticket_contract: &soroban_sdk::Address) { env.storage() .persistent() .set(&DataKey::TicketContract, ticket_contract); - env.storage().persistent().extend_ttl( - &DataKey::TicketContract, - 60 * 60 * 24 * 30, - 60 * 60 * 24 * 30 * 2, - ); + env.storage() + .persistent() + .extend_ttl(&DataKey::TicketContract, TTL_THRESHOLD, TTL_BUMP); } pub fn get_event_privacy(env: &Env, event_id: &Symbol) -> EventPrivacyConfig { @@ -248,9 +279,14 @@ pub fn set_event_privacy(env: &Env, event_id: &Symbol, privacy: &EventPrivacyCon } pub fn get_event_config(env: &Env, event_id: &Symbol) -> Option { - env.storage() - .persistent() - .get(&DataKey::EventConfig(event_id.clone())) + let key = DataKey::EventConfig(event_id.clone()); + let config = env.storage().persistent().get(&key); + if config.is_some() { + env.storage() + .persistent() + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + } + config } pub fn set_event_config(env: &Env, event_id: &Symbol, config: &EventConfig) { @@ -287,11 +323,9 @@ pub fn get_next_payment_id(env: &Env) -> u64 { env.storage() .persistent() .set(&DataKey::NextPaymentId, &next_id); - env.storage().persistent().extend_ttl( - &DataKey::NextPaymentId, - 60 * 60 * 24 * 30, - 60 * 60 * 24 * 30 * 2, - ); + env.storage() + .persistent() + .extend_ttl(&DataKey::NextPaymentId, TTL_THRESHOLD, TTL_BUMP); next_id } pub fn get_next_ticket_id(env: &Env) -> u64 { @@ -304,11 +338,9 @@ pub fn get_next_ticket_id(env: &Env) -> u64 { env.storage() .persistent() .set(&DataKey::NextTicketId, &next_id); - env.storage().persistent().extend_ttl( - &DataKey::NextTicketId, - 60 * 60 * 24 * 30, - 60 * 60 * 24 * 30 * 2, - ); + env.storage() + .persistent() + .extend_ttl(&DataKey::NextTicketId, TTL_THRESHOLD, TTL_BUMP); next_id } pub fn save_payment(env: &Env, payment: &PaymentRecord) -> Result<(), PaymentError> { @@ -323,10 +355,16 @@ pub fn save_payment(env: &Env, payment: &PaymentRecord) -> Result<(), PaymentErr Ok(()) } pub fn get_payment(env: &Env, payment_id: u64) -> Result { + let key = DataKey::Payment(payment_id); + let payment = env + .storage() + .persistent() + .get(&key) + .ok_or(PaymentError::PaymentNotFound)?; env.storage() .persistent() - .get(&DataKey::Payment(payment_id)) - .ok_or(PaymentError::PaymentNotFound) + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + Ok(payment) } pub fn save_ticket(env: &Env, ticket: &Ticket) -> Result<(), PaymentError> { let key = DataKey::Ticket(ticket.ticket_id); @@ -340,10 +378,16 @@ pub fn save_ticket(env: &Env, ticket: &Ticket) -> Result<(), PaymentError> { Ok(()) } pub fn get_ticket(env: &Env, ticket_id: u64) -> Result { + let key = DataKey::Ticket(ticket_id); + let ticket = env + .storage() + .persistent() + .get(&key) + .ok_or(PaymentError::TicketNotFound)?; env.storage() .persistent() - .get(&DataKey::Ticket(ticket_id)) - .ok_or(PaymentError::TicketNotFound) + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + Ok(ticket) } /// Map-based: Add an owner-ticket relationship pub fn add_owner_ticket_map(env: &Env, owner: &Address, ticket_id: u64) { @@ -669,27 +713,29 @@ pub fn set_platform_fee_bps(env: &Env, bps: u32) { env.storage() .persistent() .set(&DataKey::PlatformFeeBps, &bps); - env.storage().persistent().extend_ttl( - &DataKey::PlatformFeeBps, - 60 * 60 * 24 * 30, - 60 * 60 * 24 * 30 * 2, - ); + env.storage() + .persistent() + .extend_ttl(&DataKey::PlatformFeeBps, TTL_THRESHOLD, TTL_BUMP); } pub fn get_platform_wallet(env: &Env) -> Result { + let key = DataKey::PlatformWallet; + let wallet = env + .storage() + .persistent() + .get(&key) + .ok_or(PaymentError::NotInitialized)?; env.storage() .persistent() - .get(&DataKey::PlatformWallet) - .ok_or(PaymentError::NotInitialized) + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + Ok(wallet) } pub fn set_platform_wallet(env: &Env, wallet: &Address) { env.storage() .persistent() .set(&DataKey::PlatformWallet, wallet); - env.storage().persistent().extend_ttl( - &DataKey::PlatformWallet, - 60 * 60 * 24 * 30, - 60 * 60 * 24 * 30 * 2, - ); + env.storage() + .persistent() + .extend_ttl(&DataKey::PlatformWallet, TTL_THRESHOLD, TTL_BUMP); } pub fn get_platform_revenue(env: &Env, event_id: &Symbol) -> i128 { env.storage() @@ -752,7 +798,7 @@ pub fn set_nonce(env: &Env, address: &Address, nonce: u64) { env.storage().persistent().set(&key, &true); env.storage() .persistent() - .extend_ttl(&key, 60 * 60 * 24 * 7, 60 * 60 * 24 * 14); + .extend_ttl(&key, NONCE_TTL_THRESHOLD, NONCE_TTL_BUMP); } pub fn has_nonce_hash(env: &Env, hash: &BytesN<32>, nonce: u64) -> bool { @@ -767,7 +813,7 @@ pub fn set_nonce_hash(env: &Env, hash: &BytesN<32>, nonce: u64) { env.storage().persistent().set(&key, &true); env.storage() .persistent() - .extend_ttl(&key, 60 * 60 * 24 * 7, 60 * 60 * 24 * 14); + .extend_ttl(&key, NONCE_TTL_THRESHOLD, NONCE_TTL_BUMP); } pub fn has_nullifier(env: &Env, commitment: &BytesN<32>) -> bool { @@ -782,7 +828,7 @@ pub fn mark_nullifier_spent(env: &Env, commitment: &BytesN<32>) { env.storage().persistent().set(&key, &true); env.storage() .persistent() - .extend_ttl(&key, 60 * 60 * 24 * 365, 60 * 60 * 24 * 365 * 2); + .extend_ttl(&key, NULLIFIER_TTL_THRESHOLD, NULLIFIER_TTL_BUMP); } /// Get the current contract version from storage. diff --git a/contracts/ticket/src/lib.rs b/contracts/ticket/src/lib.rs index db1615f..478d09e 100644 --- a/contracts/ticket/src/lib.rs +++ b/contracts/ticket/src/lib.rs @@ -50,6 +50,11 @@ impl TicketContract { env.storage() .persistent() .set(&DataKey::Ticket(ticket_id), &ticket); + env.storage().persistent().extend_ttl( + &DataKey::Ticket(ticket_id), + storage::TTL_THRESHOLD, + storage::TTL_BUMP, + ); // Use map-based indexing instead of vector storage storage::add_owner_ticket(&env, &owner, ticket_id); @@ -106,6 +111,11 @@ impl TicketContract { env.storage() .persistent() .set(&DataKey::Ticket(ticket_id), &ticket); + env.storage().persistent().extend_ttl( + &DataKey::Ticket(ticket_id), + storage::TTL_THRESHOLD, + storage::TTL_BUMP, + ); // Use map-based indexing: remove from old owner, add to new owner storage::remove_owner_ticket(&env, &from, ticket_id); @@ -153,6 +163,11 @@ impl TicketContract { env.storage() .persistent() .set(&DataKey::Ticket(ticket_id), &ticket); + env.storage().persistent().extend_ttl( + &DataKey::Ticket(ticket_id), + storage::TTL_THRESHOLD, + storage::TTL_BUMP, + ); events::emit_ticket_used( &env, ticket_id, @@ -374,14 +389,22 @@ impl TicketContract { } fn read_next_ticket_id(env: &Env) -> u64 { - env.storage() - .persistent() - .get(&DataKey::NextTicketId) - .unwrap_or(1) + let key = DataKey::NextTicketId; + let id: Option = env.storage().persistent().get(&key); + // Only extend the TTL when the key already exists: extend_ttl on a missing + // key panics with Error(Storage, MissingValue). + if id.is_some() { + env.storage() + .persistent() + .extend_ttl(&key, storage::TTL_THRESHOLD, storage::TTL_BUMP); + } + id.unwrap_or(1) } fn write_next_ticket_id(env: &Env, next_id: u64) { + let key = DataKey::NextTicketId; + env.storage().persistent().set(&key, &next_id); env.storage() .persistent() - .set(&DataKey::NextTicketId, &next_id); + .extend_ttl(&key, storage::TTL_THRESHOLD, storage::TTL_BUMP); } diff --git a/contracts/ticket/src/storage.rs b/contracts/ticket/src/storage.rs index f7f2523..66f9b26 100644 --- a/contracts/ticket/src/storage.rs +++ b/contracts/ticket/src/storage.rs @@ -3,8 +3,11 @@ use soroban_sdk::{contracttype, Address, BytesN, Env, Symbol, Vec}; use crate::errors::TicketError; use crate::types::Ticket; -const TTL_THRESHOLD: u32 = 60 * 60 * 24 * 30; -const TTL_BUMP: u32 = 60 * 60 * 24 * 30 * 2; +/// TTL refresh threshold in ledgers (~30 days at 5s/ledger). +pub const TTL_THRESHOLD: u32 = 518_400; +/// TTL extension target in ledgers (~60 days at 5s/ledger), well within the +/// network maximum of 3,110,400 ledgers. +pub const TTL_BUMP: u32 = 1_036_800; #[allow(dead_code)] const CURRENT_VERSION: u32 = 1; @@ -31,16 +34,24 @@ pub enum DataKey { } pub fn get_ticket(env: &Env, ticket_id: u64) -> Result { + let key = DataKey::Ticket(ticket_id); + let ticket = env + .storage() + .persistent() + .get(&key) + .ok_or(TicketError::TicketNotFound)?; env.storage() .persistent() - .get(&DataKey::Ticket(ticket_id)) - .ok_or(TicketError::TicketNotFound) + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + Ok(ticket) } pub fn update_ticket(env: &Env, ticket: &Ticket) { + let key = DataKey::Ticket(ticket.ticket_id); + env.storage().persistent().set(&key, ticket); env.storage() .persistent() - .set(&DataKey::Ticket(ticket.ticket_id), ticket); + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); } /// Add an owner-ticket relationship (map-based) @@ -93,6 +104,12 @@ pub fn remove_owner_ticket(env: &Env, owner: &Address, ticket_id: u64) { if let Some(idx) = stored_index { let count = get_owner_tickets_count(env, owner); if count == 0 { + // The count entry is missing/archived, so there is no valid index + // bound to reconcile. Defensively remove this slot so a later add + // cannot collide with a stale entry. + env.storage() + .persistent() + .remove(&DataKey::OwnerTicketIndex(owner.clone(), idx)); return; } @@ -171,13 +188,25 @@ pub fn has_event_ticket(env: &Env, event_id: &Symbol, ticket_id: u64) -> bool { /// Get the count of tickets for an event pub fn get_event_tickets_count(env: &Env, event_id: &Symbol) -> u64 { let key = DataKey::EventTicketsCount(event_id.clone()); - env.storage().persistent().get(&key).unwrap_or(0) + let count: Option = env.storage().persistent().get(&key); + if count.is_some() { + env.storage() + .persistent() + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + } + count.unwrap_or(0) } /// Get the count of tickets owned by an address pub fn get_owner_tickets_count(env: &Env, owner: &Address) -> u64 { let key = DataKey::OwnerTicketsCount(owner.clone()); - env.storage().persistent().get(&key).unwrap_or(0) + let count: Option = env.storage().persistent().get(&key); + if count.is_some() { + env.storage() + .persistent() + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + } + count.unwrap_or(0) } /// Get all tickets by owner using indexed storage @@ -188,6 +217,9 @@ pub fn get_tickets_by_owner(env: &Env, owner: Address) -> Vec { for i in 0..count { let idx_key = DataKey::OwnerTicketIndex(owner.clone(), i); if let Some(ticket_id) = env.storage().persistent().get(&idx_key) { + env.storage() + .persistent() + .extend_ttl(&idx_key, TTL_THRESHOLD, TTL_BUMP); tickets.push_back(ticket_id); } } @@ -203,6 +235,9 @@ pub fn get_tickets_by_event(env: &Env, event_id: Symbol) -> Vec { for i in 0..count { let idx_key = DataKey::EventTicketIndex(event_id.clone(), i); if let Some(ticket_id) = env.storage().persistent().get(&idx_key) { + env.storage() + .persistent() + .extend_ttl(&idx_key, TTL_THRESHOLD, TTL_BUMP); tickets.push_back(ticket_id); } } @@ -233,15 +268,22 @@ pub fn verify_version(env: &Env) -> Result<(), TicketError> { } pub fn get_recovery_key(env: &Env, ticket_id: u64) -> Option> { - env.storage() - .persistent() - .get(&DataKey::RecoveryKey(ticket_id)) + let key = DataKey::RecoveryKey(ticket_id); + let value = env.storage().persistent().get(&key); + if value.is_some() { + env.storage() + .persistent() + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + } + value } pub fn set_recovery_key(env: &Env, ticket_id: u64, public_key: &BytesN<32>) { + let key = DataKey::RecoveryKey(ticket_id); + env.storage().persistent().set(&key, public_key); env.storage() .persistent() - .set(&DataKey::RecoveryKey(ticket_id), public_key); + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); } pub fn remove_recovery_key(env: &Env, ticket_id: u64) { @@ -251,10 +293,16 @@ pub fn remove_recovery_key(env: &Env, ticket_id: u64) { } pub fn get_payments_contract(env: &Env) -> Result { + let key = DataKey::PaymentsContract; + let address = env + .storage() + .persistent() + .get(&key) + .ok_or(TicketError::Unauthorized)?; env.storage() .persistent() - .get(&DataKey::PaymentsContract) - .ok_or(TicketError::Unauthorized) // or specific error if available + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + Ok(address) } pub fn set_payments_contract(env: &Env, payments_contract: &Address) { @@ -267,10 +315,16 @@ pub fn set_payments_contract(env: &Env, payments_contract: &Address) { } pub fn get_event_contract(env: &Env) -> Result { + let key = DataKey::EventContract; + let address = env + .storage() + .persistent() + .get(&key) + .ok_or(TicketError::Unauthorized)?; env.storage() .persistent() - .get(&DataKey::EventContract) - .ok_or(TicketError::Unauthorized) + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + Ok(address) } pub fn set_event_contract(env: &Env, event_contract: &Address) { @@ -283,10 +337,16 @@ pub fn set_event_contract(env: &Env, event_contract: &Address) { } pub fn get_admin(env: &Env) -> Result { + let key = DataKey::Admin; + let address = env + .storage() + .persistent() + .get(&key) + .ok_or(TicketError::Unauthorized)?; env.storage() .persistent() - .get(&DataKey::Admin) - .ok_or(TicketError::Unauthorized) + .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); + Ok(address) } pub fn set_admin(env: &Env, admin: &Address) {