diff --git a/soroban-contract/contracts/event_manager/src/lib.rs b/soroban-contract/contracts/event_manager/src/lib.rs index 43d4e7c6..b43c3302 100644 --- a/soroban-contract/contracts/event_manager/src/lib.rs +++ b/soroban-contract/contracts/event_manager/src/lib.rs @@ -4,6 +4,22 @@ use soroban_sdk::{ contract, contractimpl, contracttype, Address, BytesN, Env, IntoVal, String, Symbol, Vec, }; +// Error handling +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Error { + AlreadyInitialized = 1, + EventNotFound = 2, + EventAlreadyCanceled = 3, + CannotSellMoreTickets = 4, + InvalidStartDate = 5, + InvalidEndDate = 6, + NegativeTicketPrice = 7, + InvalidTicketCount = 8, + CounterOverflow = 9, + FactoryNotInitialized = 10, +} + // Storage keys #[contracttype] pub enum DataKey { @@ -36,10 +52,10 @@ pub struct EventManager; #[contractimpl] impl EventManager { /// Initialize the contract with the ticket factory address - pub fn initialize(env: Env, ticket_factory: Address) { + pub fn initialize(env: Env, ticket_factory: Address) -> Result<(), Error> { // Ensure not already initialized if env.storage().instance().has(&DataKey::TicketFactory) { - panic!("Already initialized"); + return Err(Error::AlreadyInitialized); } // Store the ticket factory address @@ -49,6 +65,8 @@ impl EventManager { // Initialize event counter env.storage().instance().set(&DataKey::EventCounter, &0u32); + + Ok(()) } /// Create a new event @@ -61,19 +79,21 @@ impl EventManager { end_date: u64, ticket_price: i128, total_tickets: u128, + ) -> Result { + payment_token: Address, ) -> u32 { // Validate organizer address organizer.require_auth(); // Validate inputs - Self::validate_event_params(&env, start_date, end_date, ticket_price, total_tickets); + Self::validate_event_params(&env, start_date, end_date, ticket_price, total_tickets)?; // Get and increment event counter - let event_id = Self::get_and_increment_counter(&env); + let event_id = Self::get_and_increment_counter(&env)?; // Deploy ticket NFT contract via factory - let ticket_nft_addr = Self::deploy_ticket_nft(&env, event_id, theme.clone(), total_tickets); + let ticket_nft_addr = Self::deploy_ticket_nft(&env, event_id, theme.clone(), total_tickets)?; // Create event struct let event = Event { @@ -109,15 +129,15 @@ impl EventManager { (event_id, organizer, ticket_nft_addr), ); - event_id + Ok(event_id) } /// Get event by ID - pub fn get_event(env: Env, event_id: u32) -> Event { + pub fn get_event(env: Env, event_id: u32) -> Result { env.storage() .persistent() .get(&DataKey::Event(event_id)) - .unwrap_or_else(|| panic!("Event not found")) + .ok_or(Error::EventNotFound) } /// Get total number of events @@ -143,19 +163,19 @@ impl EventManager { } /// Cancel an event - pub fn cancel_event(env: Env, event_id: u32) { + pub fn cancel_event(env: Env, event_id: u32) -> Result<(), Error> { let mut event: Event = env .storage() .persistent() .get(&DataKey::Event(event_id)) - .unwrap_or_else(|| panic!("Event not found")); + .ok_or(Error::EventNotFound)?; // Only organizer can cancel event.organizer.require_auth(); // Check if already canceled if event.is_canceled { - panic!("Event already canceled"); + return Err(Error::EventAlreadyCanceled); } // Mark as canceled @@ -169,15 +189,17 @@ impl EventManager { // Emit cancellation event env.events() .publish((Symbol::new(&env, "event_canceled"),), event_id); + + Ok(()) } /// Update tickets sold (called by ticket purchase logic) - pub fn update_tickets_sold(env: Env, event_id: u32, amount: u128) { + pub fn update_tickets_sold(env: Env, event_id: u32, amount: u128) -> Result<(), Error> { let mut event: Event = env .storage() .persistent() .get(&DataKey::Event(event_id)) - .unwrap_or_else(|| panic!("Event not found")); + .ok_or(Error::EventNotFound)?; // Verify the caller (should be the ticket NFT contract or authorized entity) event.ticket_nft_addr.require_auth(); @@ -186,17 +208,19 @@ impl EventManager { event.tickets_sold = event .tickets_sold .checked_add(amount) - .unwrap_or_else(|| panic!("Overflow in tickets sold")); + .ok_or(Error::CounterOverflow)?; // Ensure we don't oversell if event.tickets_sold > event.total_tickets { - panic!("Cannot sell more tickets than available"); + return Err(Error::CannotSellMoreTickets); } // Update storage env.storage() .persistent() .set(&DataKey::Event(event_id), &event); + + Ok(()) } /// Purchase a ticket for an event @@ -260,30 +284,32 @@ impl EventManager { end_date: u64, ticket_price: i128, total_tickets: u128, - ) { + ) -> Result<(), Error> { let current_time = env.ledger().timestamp(); // Validate dates if start_date < current_time { - panic!("Start date must be in the future"); + return Err(Error::InvalidStartDate); } if end_date <= start_date { - panic!("End date must be after start date"); + return Err(Error::InvalidEndDate); } // Validate ticket price if ticket_price < 0 { - panic!("Ticket price cannot be negative"); + return Err(Error::NegativeTicketPrice); } // Validate total tickets if total_tickets == 0 { - panic!("Total tickets must be greater than 0"); + return Err(Error::InvalidTicketCount); } + + Ok(()) } - fn get_and_increment_counter(env: &Env) -> u32 { + fn get_and_increment_counter(env: &Env) -> Result { let current: u32 = env .storage() .instance() @@ -292,26 +318,152 @@ impl EventManager { let next = current .checked_add(1) - .unwrap_or_else(|| panic!("Event counter overflow")); + .ok_or(Error::CounterOverflow)?; env.storage().instance().set(&DataKey::EventCounter, &next); - current + Ok(current) } - fn deploy_ticket_nft( - env: &Env, - _event_id: u32, - _theme: String, - _total_supply: u128, - ) -> Address { + fn deploy_ticket_nft(env: &Env, event_id: u32, theme: String, total_supply: u128) -> Result { let factory_addr: Address = env .storage() .instance() .get(&DataKey::TicketFactory) + .ok_or(Error::FactoryNotInitialized)?; + + // Call the factory contract to deploy a new NFT contract .unwrap_or_else(|| panic!("Ticket factory not initialized")); // This is a cross-contract call + Ok(nft_addr) + } +} + +#[cfg(test)] +mod test { + use super::*; + use soroban_sdk::{testutils::Address as _, testutils::Ledger, vec, Env, Symbol}; + + #[contract] + pub struct MockFactory; + + #[contractimpl] + impl MockFactory { + pub fn deploy_ticket_nft( + env: Env, + _event_id: u32, + _theme: String, + _total_supply: u128, + ) -> Address { + Address::generate(&env) + } + } + + #[test] + fn test_create_event() { + let env = Env::default(); + let contract_id = env.register_contract(None, EventManager); + let client = EventManagerClient::new(&env, &contract_id); + + let factory_addr = env.register_contract(None, MockFactory); + let organizer = Address::generate(&env); + + // Mock the organizer authorization + env.mock_all_auths(); + + // Initialize + client.initialize(&factory_addr).unwrap(); + + // Create event + let theme = String::from_str(&env, "Rust Conference 2026"); + let event_type = String::from_str(&env, "Conference"); + let start_date = env.ledger().timestamp() + 86400; // 1 day from now + let end_date = start_date + 86400; // 2 days from now + let ticket_price = 1000_0000000; // 100 XLM (7 decimals) + let total_tickets = 500; + + let event_id = client.create_event( + &organizer, + &theme, + &event_type, + &start_date, + &end_date, + &ticket_price, + &total_tickets, + ).unwrap(); + + assert_eq!(event_id, 0); + + // Get event + let event = client.get_event(&event_id).unwrap(); + assert_eq!(event.id, 0); + assert_eq!(event.organizer, organizer); + assert_eq!(event.total_tickets, total_tickets); + assert_eq!(event.tickets_sold, 0); + assert_eq!(event.is_canceled, false); + } + + #[test] + fn test_create_event_past_date() { + let env = Env::default(); + let contract_id = env.register_contract(None, EventManager); + let client = EventManagerClient::new(&env, &contract_id); + + let factory_addr = env.register_contract(None, MockFactory); + let organizer = Address::generate(&env); + + env.mock_all_auths(); + env.ledger().set_timestamp(1000); + client.initialize(&factory_addr).unwrap(); + + let theme = String::from_str(&env, "Past Event"); + let event_type = String::from_str(&env, "Conference"); + let start_date = env.ledger().timestamp().saturating_sub(1); // Past date + let end_date = start_date.saturating_add(86400); + + let result = client.create_event( + &organizer, + &theme, + &event_type, + &start_date, + &end_date, + &1000_0000000, + &100, + ); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), Error::InvalidStartDate); + } + + #[test] + fn test_cancel_event() { + let env = Env::default(); + let contract_id = env.register_contract(None, EventManager); + let client = EventManagerClient::new(&env, &contract_id); + + let factory_addr = env.register_contract(None, MockFactory); + let organizer = Address::generate(&env); + + env.mock_all_auths(); + client.initialize(&factory_addr).unwrap(); + + let event_id = client.create_event( + &organizer, + &String::from_str(&env, "Event"), + &String::from_str(&env, "Type"), + &(env.ledger().timestamp() + 86400), + &(env.ledger().timestamp() + 172800), + &1000_0000000, + &100, + ).unwrap(); + + client.cancel_event(&event_id).unwrap(); + + let event = client.get_event(&event_id).unwrap(); + assert_eq!(event.is_canceled, true); + } +} let salt = BytesN::from_array(&env, &[0u8; 32]); let mut args = Vec::new(&env); args.push_back(env.current_contract_address().to_val()); diff --git a/soroban-contract/contracts/tba_account/src/lib.rs b/soroban-contract/contracts/tba_account/src/lib.rs index 758ede07..039b3ea3 100644 --- a/soroban-contract/contracts/tba_account/src/lib.rs +++ b/soroban-contract/contracts/tba_account/src/lib.rs @@ -4,6 +4,14 @@ use soroban_sdk::{ Val, Vec, }; +// Error handling +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Error { + AlreadyInitialized = 1, + NotInitialized = 2, +} + #[contract] pub struct TbaAccount; @@ -19,11 +27,11 @@ pub enum DataKey { } // Helper functions for storage -fn get_token_contract(env: &Env) -> Address { +fn get_token_contract(env: &Env) -> Result { env.storage() .instance() .get(&DataKey::TokenContract) - .expect("Contract not initialized") + .ok_or(Error::NotInitialized) } fn set_token_contract(env: &Env, token_contract: &Address) { @@ -32,22 +40,22 @@ fn set_token_contract(env: &Env, token_contract: &Address) { .set(&DataKey::TokenContract, token_contract); } -fn get_token_id(env: &Env) -> u128 { +fn get_token_id(env: &Env) -> Result { env.storage() .instance() .get(&DataKey::TokenId) - .expect("Contract not initialized") + .ok_or(Error::NotInitialized) } fn set_token_id(env: &Env, token_id: &u128) { env.storage().instance().set(&DataKey::TokenId, token_id); } -fn _get_implementation_hash(env: &Env) -> BytesN<32> { +fn get_implementation_hash(env: &Env) -> Result, Error> { env.storage() .instance() .get(&DataKey::ImplementationHash) - .expect("Contract not initialized") + .ok_or(Error::NotInitialized) } fn set_implementation_hash(env: &Env, implementation_hash: &BytesN<32>) { @@ -56,11 +64,11 @@ fn set_implementation_hash(env: &Env, implementation_hash: &BytesN<32>) { .set(&DataKey::ImplementationHash, implementation_hash); } -fn _get_salt(env: &Env) -> BytesN<32> { +fn get_salt(env: &Env) -> Result, Error> { env.storage() .instance() .get(&DataKey::Salt) - .expect("Contract not initialized") + .ok_or(Error::NotInitialized) } fn set_salt(env: &Env, salt: &BytesN<32>) { @@ -123,10 +131,10 @@ impl TbaAccount { token_id: u128, implementation_hash: BytesN<32>, salt: BytesN<32>, - ) { + ) -> Result<(), Error> { // Prevent re-initialization if is_initialized(&env) { - panic!("Contract already initialized"); + return Err(Error::AlreadyInitialized); } // Store all parameters @@ -135,6 +143,8 @@ impl TbaAccount { set_implementation_hash(&env, &implementation_hash); set_salt(&env, &salt); set_initialized(&env, &true); + + Ok(()) // Extend instance TTL env.storage() @@ -143,32 +153,32 @@ impl TbaAccount { } /// Get the NFT contract address - pub fn token_contract(env: Env) -> Address { + pub fn token_contract(env: Env) -> Result { get_token_contract(&env) } /// Get the token ID - pub fn token_id(env: Env) -> u128 { + pub fn token_id(env: Env) -> Result { get_token_id(&env) } /// Get the current owner of the NFT (by querying the NFT contract) - pub fn owner(env: Env) -> Address { - let token_contract = get_token_contract(&env); - let token_id = get_token_id(&env); - get_nft_owner(&env, &token_contract, token_id) + pub fn owner(env: Env) -> Result { + let token_contract = get_token_contract(&env)?; + let token_id = get_token_id(&env)?; + Ok(get_nft_owner(&env, &token_contract, token_id)) } /// Get token details as a tuple: (chain_id, token_contract, token_id) /// This matches the ERC-6551 pattern for compatibility /// Note: chain_id is set to 0 as Soroban doesn't expose chain_id in the same way - pub fn token(env: Env) -> (u32, Address, u128) { + pub fn token(env: Env) -> Result<(u32, Address, u128), Error> { // Soroban doesn't have chain_id exposed, using 0 as placeholder // In production, this could be set during initialization let chain_id = 0u32; - let token_contract = get_token_contract(&env); - let token_id = get_token_id(&env); - (chain_id, token_contract, token_id) + let token_contract = get_token_contract(&env)?; + let token_id = get_token_id(&env)?; + Ok((chain_id, token_contract, token_id)) } /// Get the current nonce @@ -179,15 +189,15 @@ impl TbaAccount { /// Execute a transaction to another contract /// Only the current NFT owner can execute transactions /// This function increments the nonce and emits an event - pub fn execute(env: Env, to: Address, func: Symbol, args: Vec) -> Vec { + pub fn execute(env: Env, to: Address, func: Symbol, args: Vec) -> Result, Error> { // Verify contract is initialized if !is_initialized(&env) { - panic!("Contract not initialized"); + return Err(Error::NotInitialized); } // Get the NFT owner and verify authorization - let token_contract = get_token_contract(&env); - let token_id = get_token_id(&env); + let token_contract = get_token_contract(&env)?; + let token_id = get_token_id(&env)?; let owner = get_nft_owner(&env, &token_contract, token_id); // Require authorization from the NFT owner @@ -216,7 +226,7 @@ impl TbaAccount { ); // Invoke the target contract - env.invoke_contract::>(&to, &func, args) + Ok(env.invoke_contract::>(&to, &func, args)) } /// CustomAccountInterface implementation: Check authorization @@ -226,10 +236,10 @@ impl TbaAccount { signature_payload: BytesN<32>, signatures: Vec>, auth_context: Vec, - ) { + ) -> Result<(), Error> { // Get the NFT contract and token ID - let token_contract = get_token_contract(&env); - let token_id = get_token_id(&env); + let token_contract = get_token_contract(&env)?; + let token_id = get_token_id(&env)?; // Get the current owner of the NFT let owner = get_nft_owner(&env, &token_contract, token_id); @@ -242,6 +252,8 @@ impl TbaAccount { Val::from(signatures), Val::from(auth_context), ]); + + Ok(()) } } diff --git a/soroban-contract/contracts/tba_account/src/test.rs b/soroban-contract/contracts/tba_account/src/test.rs index 658cb904..e7361a1c 100644 --- a/soroban-contract/contracts/tba_account/src/test.rs +++ b/soroban-contract/contracts/tba_account/src/test.rs @@ -55,16 +55,15 @@ fn test_initialize() { let salt = BytesN::from_array(&env, &[2u8; 32]); // Initialize should succeed - client.initialize(&nft_contract, &token_id, &impl_hash, &salt); + client.initialize(&nft_contract, &token_id, &impl_hash, &salt).unwrap(); // Verify initialization - assert_eq!(client.token_contract(), nft_contract); - assert_eq!(client.token_id(), token_id); + assert_eq!(client.token_contract().unwrap(), nft_contract); + assert_eq!(client.token_id().unwrap(), token_id); } #[test] -#[should_panic(expected = "Contract already initialized")] -fn test_initialize_twice_panics() { +fn test_initialize_twice_fails() { let (env, client, _) = create_test_env(); let nft_contract = Address::generate(&env); @@ -73,10 +72,12 @@ fn test_initialize_twice_panics() { let salt = BytesN::from_array(&env, &[2u8; 32]); // First initialization - client.initialize(&nft_contract, &token_id, &impl_hash, &salt); + client.initialize(&nft_contract, &token_id, &impl_hash, &salt).unwrap(); - // Second initialization should panic - client.initialize(&nft_contract, &token_id, &impl_hash, &salt); + // Second initialization should fail + let result = client.initialize(&nft_contract, &token_id, &impl_hash, &salt); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), Error::AlreadyInitialized); } #[test] @@ -95,14 +96,14 @@ fn test_execute_success() { let impl_hash = BytesN::from_array(&env, &[1u8; 32]); let salt = BytesN::from_array(&env, &[2u8; 32]); - client.initialize(&nft_contract_id, &token_id, &impl_hash, &salt); + client.initialize(&nft_contract_id, &token_id, &impl_hash, &salt).unwrap(); // Execute through TBA let func = Symbol::new(&env, "test_func"); let args = vec![&env, 42u32.into_val(&env)]; // The account will call owner_of(token_id) on nft_contract_id - let result = client.execute(&target_id, &func, &args); + let result = client.execute(&target_id, &func, &args).unwrap(); // Val doesn't implement PartialEq in some SDK versions, so convert back let val: u32 = result.get(0).unwrap().try_into_val(&env).unwrap(); @@ -127,7 +128,7 @@ fn test_execute_non_owner_fails() { let impl_hash = BytesN::from_array(&env, &[1u8; 32]); let salt = BytesN::from_array(&env, &[2u8; 32]); - client.initialize(&nft_contract_id, &token_id, &impl_hash, &salt); + client.initialize(&nft_contract_id, &token_id, &impl_hash, &salt).unwrap(); let target = Address::generate(&env); let func = Symbol::new(&env, "test"); diff --git a/soroban-contract/contracts/tba_registry/src/lib.rs b/soroban-contract/contracts/tba_registry/src/lib.rs index 10947288..3a1fe9c4 100644 --- a/soroban-contract/contracts/tba_registry/src/lib.rs +++ b/soroban-contract/contracts/tba_registry/src/lib.rs @@ -4,6 +4,13 @@ use soroban_sdk::{ contract, contractimpl, contracttype, Address, BytesN, Env, IntoVal, Symbol, Val, Vec, }; +// Error handling +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Error { + AccountAlreadyDeployed = 1, +} + /// Storage keys for the registry contract #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] @@ -113,15 +120,15 @@ impl TbaRegistry { /// # Returns /// The address of the newly deployed TBA account /// - /// # Panics - /// Panics if the account has already been deployed for these parameters + /// # Errors + /// Returns error if the account has already been deployed for these parameters pub fn create_account( env: Env, implementation_hash: BytesN<32>, token_contract: Address, token_id: u128, salt: BytesN<32>, - ) -> Address { + ) -> Result { // Verify that the caller owns the NFT (Issue #26) // This is a cross-contract call to the NFT contract let owner: Address = env.invoke_contract( @@ -140,7 +147,7 @@ impl TbaRegistry { ); if env.storage().persistent().has(&account_key) { - panic!("Account already deployed for these parameters"); + return Err(Error::AccountAlreadyDeployed); } // Get the WASM hash from storage @@ -196,6 +203,7 @@ impl TbaRegistry { let new_count = current_count + 1; env.storage().persistent().set(&count_key, &new_count); + Ok(deployed_address) // Extend persistent TTL for count env.storage().persistent().extend_ttl( &count_key, diff --git a/soroban-contract/contracts/tba_registry/src/test.rs b/soroban-contract/contracts/tba_registry/src/test.rs index 6f09d84d..d90f40e8 100644 --- a/soroban-contract/contracts/tba_registry/src/test.rs +++ b/soroban-contract/contracts/tba_registry/src/test.rs @@ -85,7 +85,7 @@ fn test_get_account_matches_create_account() { let calculated_address = client.get_account(&impl_hash, &nft_addr, &token_id, &salt); // Deploy the account - let deployed_address = client.create_account(&impl_hash, &nft_addr, &token_id, &salt); + let deployed_address = client.create_account(&impl_hash, &token_contract, &token_id, &salt).unwrap(); // They should match assert_eq!(calculated_address, deployed_address); @@ -100,15 +100,75 @@ fn test_multiple_accounts_same_nft() { let salt1 = BytesN::from_array(&env, &[10u8; 32]); let salt2 = BytesN::from_array(&env, &[20u8; 32]); + let salt3 = BytesN::from_array(&env, &[30u8; 32]); + + // Deploy three accounts for the same NFT with different salts + let addr1 = client.create_account(&impl_hash, &token_contract, &token_id, &salt1).unwrap(); + let addr2 = client.create_account(&impl_hash, &token_contract, &token_id, &salt2).unwrap(); + let addr3 = client.create_account(&impl_hash, &token_contract, &token_id, &salt3).unwrap(); + + // All addresses should be different + assert_ne!(addr1, addr2); + assert_ne!(addr2, addr3); + assert_ne!(addr1, addr3); + + // Account count should be 3 + assert_eq!( + client.total_deployed_accounts(&token_contract, &token_id), + 3 + ); +} client.create_account(&impl_hash, &nft_addr, &token_id, &salt1); client.create_account(&impl_hash, &nft_addr, &token_id, &salt2); + let token_contract = Address::generate(&env); + let token_id = 100u128; + let impl_hash = BytesN::from_array(&env, &[1u8; 32]); + + // Initially zero + assert_eq!( + client.total_deployed_accounts(&token_contract, &token_id), + 0 + ); + + // Deploy accounts and verify count increments + for i in 1u8..=5u8 { + let salt = BytesN::from_array(&env, &[i; 32]); + client.create_account(&impl_hash, &token_contract, &token_id, &salt).unwrap(); + assert_eq!( + client.total_deployed_accounts(&token_contract, &token_id), + i as u32 + ); + } +} + +/// Test: Deployed account is properly initialized +#[test] +fn test_deployed_account_initialized() { + let (env, _registry_addr, client, _wasm_hash) = setup_test(); + + let token_contract = Address::generate(&env); + let token_id = 200u128; + let impl_hash = BytesN::from_array(&env, &[1u8; 32]); + let salt = BytesN::from_array(&env, &[50u8; 32]); + + // Deploy the account + let deployed_address = client.create_account(&impl_hash, &token_contract, &token_id, &salt).unwrap(); + + // Create a client for the deployed TBA account + let tba_client = tba_account_contract::Client::new(&env, &deployed_address); + + // Verify the account is initialized with correct values + assert_eq!(tba_client.token_contract().unwrap(), token_contract); + assert_eq!(tba_client.token_id().unwrap(), token_id); +} + +/// Test: Cannot create account twice with same parameters assert_eq!(client.total_deployed_accounts(&nft_addr, &token_id), 2); } #[test] -#[should_panic(expected = "Account already deployed")] fn test_cannot_create_account_twice() { let (env, _registry_addr, client, _wasm_hash, nft_addr) = setup_test(); @@ -116,6 +176,87 @@ fn test_cannot_create_account_twice() { let impl_hash = BytesN::from_array(&env, &[1u8; 32]); let salt = BytesN::from_array(&env, &[60u8; 32]); - client.create_account(&impl_hash, &nft_addr, &token_id, &salt); - client.create_account(&impl_hash, &nft_addr, &token_id, &salt); + // First deployment should succeed + client.create_account(&impl_hash, &token_contract, &token_id, &salt).unwrap(); + + // Second deployment with same parameters should fail + let result = client.create_account(&impl_hash, &token_contract, &token_id, &salt); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), Error::AccountAlreadyDeployed); +} + +/// Test: get_deployed_address returns correct address +#[test] +fn test_get_deployed_address() { + let (env, _registry_addr, client, _wasm_hash) = setup_test(); + + let token_contract = Address::generate(&env); + let token_id = 400u128; + let impl_hash = BytesN::from_array(&env, &[1u8; 32]); + let salt = BytesN::from_array(&env, &[70u8; 32]); + + // Before deployment, should return None + assert_eq!( + client.get_deployed_address(&impl_hash, &token_contract, &token_id, &salt), + None + ); + + // Deploy the account + let deployed_address = client.create_account(&impl_hash, &token_contract, &token_id, &salt).unwrap(); + + // After deployment, should return the address + assert_eq!( + client.get_deployed_address(&impl_hash, &token_contract, &token_id, &salt), + Some(deployed_address) + ); +} + +/// Test: Different NFTs have separate account counts +#[test] +fn test_different_nfts_separate_counts() { + let (env, _registry_addr, client, _wasm_hash) = setup_test(); + + let token_contract1 = Address::generate(&env); + let token_contract2 = Address::generate(&env); + let impl_hash = BytesN::from_array(&env, &[1u8; 32]); + + // Deploy accounts for NFT 1 + let salt1 = BytesN::from_array(&env, &[80u8; 32]); + client.create_account(&impl_hash, &token_contract1, &1u128, &salt1).unwrap(); + + // Deploy accounts for NFT 2 + let salt2 = BytesN::from_array(&env, &[90u8; 32]); + client.create_account(&impl_hash, &token_contract2, &1u128, &salt2).unwrap(); + + // Each NFT should have count of 1 + assert_eq!(client.total_deployed_accounts(&token_contract1, &1u128), 1); + assert_eq!(client.total_deployed_accounts(&token_contract2, &1u128), 1); + + // Deploy another account for NFT 1 + let salt3 = BytesN::from_array(&env, &[100u8; 32]); + client.create_account(&impl_hash, &token_contract1, &1u128, &salt3).unwrap(); + // Calculate addresses for different parameters + let addr1 = client.get_account( + &impl_hash, + &token_contract, + &1u128, + &BytesN::from_array(&env, &[1u8; 32]), + ); + let addr2 = client.get_account( + &impl_hash, + &token_contract, + &2u128, + &BytesN::from_array(&env, &[1u8; 32]), + ); + let addr3 = client.get_account( + &impl_hash, + &token_contract, + &1u128, + &BytesN::from_array(&env, &[2u8; 32]), + ); + + // All addresses should be different + assert_ne!(addr1, addr2); + assert_ne!(addr1, addr3); + assert_ne!(addr2, addr3); } diff --git a/soroban-contract/contracts/ticket_nft/src/lib.rs b/soroban-contract/contracts/ticket_nft/src/lib.rs index 54089cd5..97cf5cb6 100644 --- a/soroban-contract/contracts/ticket_nft/src/lib.rs +++ b/soroban-contract/contracts/ticket_nft/src/lib.rs @@ -4,7 +4,17 @@ #![no_std] -use soroban_sdk::{contract, contractimpl, contracttype, Address, Env}; +use soroban_sdk::{contract, contracterror, contractimpl, contracttype, Address, Env}; + +// Error handling +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Error { + UserAlreadyHasTicket = 1, + InvalidTokenId = 2, + Unauthorized = 3, + RecipientAlreadyHasTicket = 4, +} /// Storage keys for the NFT contract #[derive(Clone)] @@ -53,10 +63,10 @@ impl TicketNft { /// # Returns /// The token ID of the minted ticket /// - /// # Panics + /// # Errors /// - If caller is not the minter /// - If recipient already has a ticket - pub fn mint_ticket_nft(env: Env, recipient: Address) -> u128 { + pub fn mint_ticket_nft(env: Env, recipient: Address) -> Result { // Authorize: only minter can mint let minter: Address = env.storage().instance().get(&DataKey::Minter).unwrap(); minter.require_auth(); @@ -69,7 +79,7 @@ impl TicketNft { .unwrap_or(0); if current_balance > 0 { - panic!("User already has a ticket"); + return Err(Error::UserAlreadyHasTicket); } // Get next token ID @@ -110,7 +120,7 @@ impl TicketNft { .instance() .extend_ttl(30 * 24 * 60 * 60 / 5, 100 * 24 * 60 * 60 / 5); - token_id + Ok(token_id) } /// Get the owner of a token @@ -118,11 +128,11 @@ impl TicketNft { /// # Arguments /// * `env` - The contract environment /// * `token_id` - The token ID to query - pub fn owner_of(env: Env, token_id: u128) -> Address { + pub fn owner_of(env: Env, token_id: u128) -> Result { env.storage() .persistent() .get(&DataKey::Owner(token_id)) - .expect("Token ID does not exist") + .ok_or(Error::InvalidTokenId) } /// Get the balance of an owner @@ -147,23 +157,23 @@ impl TicketNft { /// * `to` - Recipient address /// * `token_id` - The token ID to transfer /// - /// # Panics + /// # Errors /// - If `from` is not the owner /// - If `to` already has a ticket - pub fn transfer_from(env: Env, from: Address, to: Address, token_id: u128) { + pub fn transfer_from(env: Env, from: Address, to: Address, token_id: u128) -> Result<(), Error> { from.require_auth(); if !Self::is_valid(env.clone(), token_id) { - panic!("Token is not valid"); + return Err(Error::InvalidTokenId); } - let owner = Self::owner_of(env.clone(), token_id); + let owner = Self::owner_of(env.clone(), token_id)?; if owner != from { - panic!("Not the owner"); + return Err(Error::Unauthorized); } if Self::balance_of(env.clone(), to.clone()) > 0 { - panic!("Recipient already has a ticket"); + return Err(Error::RecipientAlreadyHasTicket); } // Update ownership @@ -178,6 +188,8 @@ impl TicketNft { env.storage() .persistent() .set(&DataKey::Balance(to), &1u128); + + Ok(()) } /// Burn a ticket NFT, removing it from existence @@ -189,7 +201,7 @@ impl TicketNft { /// # Panics /// - If caller is not the token owner pub fn burn(env: Env, token_id: u128) { - let owner = Self::owner_of(env.clone(), token_id); + let owner = Self::owner_of(env.clone(), token_id).expect("Invalid token id"); // Authorize: only owner can burn // In a real implementation, we might want to allow minter too, diff --git a/soroban-contract/contracts/ticket_nft/src/test.rs b/soroban-contract/contracts/ticket_nft/src/test.rs index e9e4a5df..4b3d700b 100644 --- a/soroban-contract/contracts/ticket_nft/src/test.rs +++ b/soroban-contract/contracts/ticket_nft/src/test.rs @@ -17,20 +17,19 @@ fn test_minting() { let client = TicketNftClient::new(&env, &contract_id); // Mint first ticket - let token_id1 = client.mint_ticket_nft(&user1); + let token_id1 = client.mint_ticket_nft(&user1).unwrap(); assert_eq!(token_id1, 1); - assert_eq!(client.owner_of(&token_id1), user1); + assert_eq!(client.owner_of(&token_id1).unwrap(), user1); assert_eq!(client.balance_of(&user1), 1); // Mint second ticket - let token_id2 = client.mint_ticket_nft(&user2); + let token_id2 = client.mint_ticket_nft(&user2).unwrap(); assert_eq!(token_id2, 2); - assert_eq!(client.owner_of(&token_id2), user2); + assert_eq!(client.owner_of(&token_id2).unwrap(), user2); assert_eq!(client.balance_of(&user2), 1); } #[test] -#[should_panic(expected = "User already has a ticket")] fn test_cannot_mint_twice_to_same_user() { let env = Env::default(); env.mock_all_auths(); @@ -41,8 +40,10 @@ fn test_cannot_mint_twice_to_same_user() { let contract_id = env.register(TicketNft, (&minter,)); let client = TicketNftClient::new(&env, &contract_id); - client.mint_ticket_nft(&user); - client.mint_ticket_nft(&user); // Should panic + client.mint_ticket_nft(&user).unwrap(); + let result = client.mint_ticket_nft(&user); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), Error::UserAlreadyHasTicket); } #[test] @@ -57,17 +58,16 @@ fn test_transfer() { let contract_id = env.register(TicketNft, (&minter,)); let client = TicketNftClient::new(&env, &contract_id); - let token_id = client.mint_ticket_nft(&user1); + let token_id = client.mint_ticket_nft(&user1).unwrap(); - client.transfer_from(&user1, &user2, &token_id); + client.transfer_from(&user1, &user2, &token_id).unwrap(); - assert_eq!(client.owner_of(&token_id), user2); + assert_eq!(client.owner_of(&token_id).unwrap(), user2); assert_eq!(client.balance_of(&user1), 0); assert_eq!(client.balance_of(&user2), 1); } #[test] -#[should_panic(expected = "Recipient already has a ticket")] fn test_cannot_transfer_to_user_with_ticket() { let env = Env::default(); env.mock_all_auths(); @@ -79,10 +79,12 @@ fn test_cannot_transfer_to_user_with_ticket() { let contract_id = env.register(TicketNft, (&minter,)); let client = TicketNftClient::new(&env, &contract_id); - let token_id1 = client.mint_ticket_nft(&user1); - let _token_id2 = client.mint_ticket_nft(&user2); + let token_id1 = client.mint_ticket_nft(&user1).unwrap(); + let _token_id2 = client.mint_ticket_nft(&user2).unwrap(); - client.transfer_from(&user1, &user2, &token_id1); // Should panic + let result = client.transfer_from(&user1, &user2, &token_id1); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), Error::RecipientAlreadyHasTicket); } #[test] @@ -98,7 +100,7 @@ fn test_only_minter_can_mint() { let client = TicketNftClient::new(&env, &contract_id); // Without mock_all_auths, require_auth() will fail for the minter - client.mint_ticket_nft(&user); + let _ = client.mint_ticket_nft(&user); } #[test] @@ -112,7 +114,7 @@ fn test_burn() { let contract_id = env.register(TicketNft, (&minter,)); let client = TicketNftClient::new(&env, &contract_id); - let token_id = client.mint_ticket_nft(&user); + let token_id = client.mint_ticket_nft(&user).unwrap(); assert!(client.is_valid(&token_id)); client.burn(&token_id); @@ -121,7 +123,6 @@ fn test_burn() { } #[test] -#[should_panic(expected = "Token is not valid")] fn test_cannot_transfer_burned_token() { let env = Env::default(); env.mock_all_auths(); @@ -133,21 +134,9 @@ fn test_cannot_transfer_burned_token() { let contract_id = env.register(TicketNft, (&minter,)); let client = TicketNftClient::new(&env, &contract_id); - let token_id = client.mint_ticket_nft(&user1); + let token_id = client.mint_ticket_nft(&user1).unwrap(); client.burn(&token_id); - client.transfer_from(&user1, &user2, &token_id); // Should panic -} - -#[test] -fn test_get_minter() { - let env = Env::default(); - env.mock_all_auths(); - - let minter = Address::generate(&env); - - let contract_id = env.register(TicketNft, (&minter,)); - let client = TicketNftClient::new(&env, &contract_id); - - assert_eq!(client.get_minter(), minter); -} + let result = client.transfer_from(&user1, &user2, &token_id); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), Error::InvalidTokenId);