diff --git a/packages/test_starknet/src/token.cairo b/packages/test_starknet/src/token.cairo index 1a8eeb30..68fd3f43 100644 --- a/packages/test_starknet/src/token.cairo +++ b/packages/test_starknet/src/token.cairo @@ -21,4 +21,6 @@ pub mod test_context_coverage; pub mod test_examples_coverage; #[cfg(test)] pub mod test_core_token_coverage; +#[cfg(test)] +pub mod test_token_uri_settings_address; diff --git a/packages/test_starknet/src/token/test_token_uri_settings_address.cairo b/packages/test_starknet/src/token/test_token_uri_settings_address.cairo new file mode 100644 index 00000000..c8842b3d --- /dev/null +++ b/packages/test_starknet/src/token/test_token_uri_settings_address.cairo @@ -0,0 +1,90 @@ +use game_components_test_starknet::minigame::mocks::minigame_starknet_mock::{ + IMinigameStarknetMockDispatcherTrait, IMinigameStarknetMockInitDispatcherTrait, +}; +use game_components_token::interface::IMinigameTokenMixinDispatcherTrait; +use openzeppelin_token::erc721::interface::{ + IERC721MetadataDispatcher, IERC721MetadataDispatcherTrait, +}; + +// setup helpers +use super::setup::{ + ALICE, OWNER, ZERO_ADDRESS, deploy_full_token_contract, deploy_minigame_registry_contract, + deploy_mock_game, +}; + +// ================================================================================================ +// TOKEN_URI TESTS - Settings Address Zero Check Fix +// ================================================================================================ + +/// Test that token_uri works correctly when settings_address is zero (the fix) +/// This test verifies the bug fix where we check if settings_address is zero +/// before attempting a contract call, preventing panics +#[test] +fn test_token_uri_with_zero_settings_address() { + let registry = deploy_minigame_registry_contract(); + + // Deploy mock game and initialize it with ZERO_ADDRESS for settings_address + let (minigame_dispatcher, minigame_init_dispatcher, minigame_mock_dispatcher) = + deploy_mock_game(); + + // Deploy token contract with registry first (needed for game initialization) + let (token_dispatcher, _erc721_dispatcher, _, token_address) = deploy_full_token_contract( + Option::Some("TestToken"), + Option::Some("TT"), + Option::Some("https://test.com/"), + Option::None, // royalty_receiver + Option::Some(0), // royalty_fraction + Option::Some(registry.contract_address), + Option::None // event_relayer_address + ); + + // Initialize the mock game with zero settings_address to simulate the bug scenario + // game will auto-register during initialization + minigame_init_dispatcher + .initializer( + OWNER(), // game_creator + "TestGame", // game_name + "A test game", // game_description + "Dev", // game_developer + "Pub", // game_publisher + "Action", // game_genre + "", // game_image + Option::None, // game_color + Option::None, // client_url + Option::None, // renderer_address (not needed for this test) + Option::Some(ZERO_ADDRESS()), // settings_address - zero address + Option::None, // objectives_address + token_address // minigame_token_address + ); + + // Mint a token with the game + let token_id = token_dispatcher + .mint( + Option::Some(minigame_dispatcher.contract_address), + Option::None, // player_name + Option::None, // settings_id + Option::None, // start + Option::None, // end + Option::None, // objective_ids + Option::None, // context + Option::None, // client_url + Option::None, // renderer_address + ALICE(), + false // soulbound + ); + + // Set score for the token using end_game + minigame_mock_dispatcher.end_game(token_id, 100); + + let erc721_metadata = IERC721MetadataDispatcher { + contract_address: token_dispatcher.contract_address, + }; + + // Before the fix, this would attempt to call a contract at zero address and panic + // After the fix, it should return default settings_details without panicking + let token_uri = erc721_metadata.token_uri(token_id.into()); + + // Verify that token_uri was generated successfully (non-empty) + // The exact content depends on the implementation, but it should not be empty + assert!(token_uri.len() > 0, "Token URI should not be empty"); +} diff --git a/packages/token/src/examples/full_token_contract.cairo b/packages/token/src/examples/full_token_contract.cairo index e692ea2d..3e334a81 100644 --- a/packages/token/src/examples/full_token_contract.cairo +++ b/packages/token/src/examples/full_token_contract.cairo @@ -2,34 +2,32 @@ // This demonstrates how to configure and use the modular components use core::num::traits::Zero; -use starknet::{ContractAddress, syscalls::call_contract_syscall}; -use starknet::storage::{StoragePointerReadAccess}; +use game_components_metagame::extensions::context::structs::GameContextDetails; +use game_components_minigame::extensions::settings::structs::GameSettingDetails; +use game_components_minigame::interface::{IMinigameDispatcher, IMinigameDispatcherTrait}; +use game_components_minigame::structs::GameDetail; +use game_components_utils::renderer::{create_custom_metadata, create_default_svg}; +use openzeppelin_introspection::src5::SRC5Component; +use openzeppelin_token::common::erc2981::erc2981::{DefaultConfig, ERC2981Component}; // Core imports use openzeppelin_token::erc721::{ERC721Component, interface::IERC721Metadata}; -use openzeppelin_introspection::src5::SRC5Component; -use openzeppelin_token::common::erc2981::erc2981::{DefaultConfig, ERC2981Component}; +use starknet::ContractAddress; +use starknet::storage::StoragePointerReadAccess; +use starknet::syscalls::call_contract_syscall; // Game components imports use crate::core::core_token::CoreTokenComponent; -use crate::structs::TokenMetadata; +use crate::examples::minigame_registry_contract::{ + IMinigameRegistryDispatcher, IMinigameRegistryDispatcherTrait, +}; +use crate::extensions::context::context::ContextComponent; use crate::extensions::minter::minter::MinterComponent; use crate::extensions::objectives::objectives::ObjectivesComponent; -use crate::extensions::context::context::ContextComponent; use crate::extensions::renderer::renderer::RendererComponent; use crate::extensions::settings::settings::SettingsComponent; - -use crate::examples::minigame_registry_contract::{ - IMinigameRegistryDispatcher, IMinigameRegistryDispatcherTrait, -}; - use crate::interface::{ITokenEventRelayerDispatcher, ITokenEventRelayerDispatcherTrait}; - -use game_components_metagame::extensions::context::structs::GameContextDetails; -use game_components_minigame::extensions::settings::structs::GameSettingDetails; -use game_components_minigame::interface::{IMinigameDispatcher, IMinigameDispatcherTrait}; -use game_components_minigame::structs::GameDetail; -use game_components_utils::renderer::{create_default_svg, create_custom_metadata}; +use crate::structs::TokenMetadata; #[starknet::contract] @@ -290,26 +288,30 @@ pub mod FullTokenContract { Result::Err(_) => array![].span(), }; - let mut settings_calldata = array![]; - settings_calldata.append(token_metadata.settings_id.into()); + // Check if settings_address is zero before attempting contract call + let mut default_settings = GameSettingDetails { + name: "", description: "", settings: array![].span(), + }; + + let settings_details = if settings_address.is_zero() { + default_settings + } else { + let mut settings_calldata = array![]; + settings_calldata.append(token_metadata.settings_id.into()); - let settings_details = match call_contract_syscall( settings_address, settings_details_selector, settings_calldata.span(), ) { - Result::Ok(result) => { - // Try to deserialize the result as GameSettingDetails - let mut result_span = result; - match Serde::::deserialize(ref result_span) { - Option::Some(settings_details) => settings_details, - Option::None => GameSettingDetails { - name: "", description: "", settings: array![].span(), - }, - } - }, - Result::Err(_) => GameSettingDetails { - name: "", description: "", settings: array![].span(), - }, + Result::Ok(result) => { + // Try to deserialize the result as GameSettingDetails + let mut result_span = result; + match Serde::::deserialize(ref result_span) { + Option::Some(settings_details) => settings_details, + Option::None => default_settings, + } + }, + Result::Err(_) => default_settings, + } }; let minted_by_address = self.minter.get_minter_address(token_metadata.minted_by);