From e835f6e718b7ff0e198db824c0ea98d6ddbe76e4 Mon Sep 17 00:00:00 2001 From: Bal7hazar Date: Thu, 21 Nov 2024 15:55:55 +0100 Subject: [PATCH 1/4] Slot package (#8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🚀 Kick start slot package * ✨ Migrate few schemas * ✨ Migrate more models * ✨ Create missing models and according basic impls * ✨ Introduce service and socials * 🐛 deployement service type * ♻️ Refactor everything into 3 packages * ♻️ Finish refactoring * ✨ Add new systems leveraging new components --- Scarb.lock | 20 +- Scarb.toml | 7 +- contracts/Scarb.lock | 32 +- contracts/Scarb.toml | 18 +- contracts/src/constants.cairo | 3 + contracts/src/lib.cairo | 6 +- contracts/src/systems/actions.cairo | 293 ----------------- contracts/src/systems/controller.cairo | 59 ++++ contracts/src/systems/registry.cairo | 287 +++++++++++++++++ contracts/src/systems/slot.cairo | 74 +++++ packages/controller/README.md | 0 packages/controller/Scarb.toml | 9 + .../src/components/controllable.cairo | 31 ++ packages/controller/src/constants.cairo | 1 + packages/controller/src/lib.cairo | 20 ++ packages/controller/src/models/account.cairo | 92 ++++++ .../controller/src/models/controller.cairo | 157 ++++++++++ packages/controller/src/models/index.cairo | 56 ++++ packages/controller/src/models/member.cairo | 95 ++++++ packages/controller/src/models/signer.cairo | 118 +++++++ packages/controller/src/models/team.cairo | 80 +++++ packages/controller/src/store.cairo | 81 +++++ packages/controller/src/types/method.cairo | 31 ++ packages/controller/src/types/role.cairo | 34 ++ packages/provider/README.md | 0 packages/provider/Scarb.toml | 9 + .../provider/src/components/deployable.cairo | 96 ++++++ packages/provider/src/constants.cairo | 1 + .../src/elements/services/interface.cairo | 3 + .../src/elements/services/katana.cairo | 7 + .../provider/src/elements/services/saya.cairo | 7 + .../src/elements/services/torii.cairo | 7 + packages/provider/src/lib.cairo | 27 ++ packages/provider/src/models/deployment.cairo | 141 +++++++++ packages/provider/src/models/factory.cairo | 84 +++++ packages/provider/src/models/index.cairo | 23 ++ packages/provider/src/store.cairo | 57 ++++ packages/provider/src/types/service.cairo | 54 ++++ packages/provider/src/types/status.cairo | 31 ++ packages/provider/src/types/tier.cairo | 46 +++ packages/registry/Scarb.toml | 2 +- .../src/components/controllable.cairo | 52 ---- .../src/components/initializable.cairo | 35 +++ .../src/components/registerable.cairo | 216 +++++++++++++ .../{registrable.cairo => trackable.cairo} | 217 +++---------- packages/registry/src/helpers/json.cairo | 175 +++++++++++ packages/registry/src/lib.cairo | 26 +- packages/registry/src/models/access.cairo | 124 ++++++++ .../registry/src/models/achievement.cairo | 4 +- packages/registry/src/models/game.cairo | 294 ++++++++++-------- packages/registry/src/models/index.cairo | 19 +- packages/registry/src/store.cairo | 37 ++- .../registry/src/tests/mocks/controller.cairo | 52 ---- .../registry/src/tests/mocks/register.cairo | 183 +++++++++++ .../registry/src/tests/mocks/registrer.cairo | 222 ------------- .../registry/src/tests/mocks/tracker.cairo | 134 ++++++++ packages/registry/src/tests/setup.cairo | 39 +-- .../src/tests/test_controllable.cairo | 54 ---- .../src/tests/test_registerable.cairo | 131 ++++++++ .../registry/src/tests/test_registrable.cairo | 212 ------------- .../registry/src/tests/test_trackable.cairo | 125 ++++++++ packages/registry/src/types/metadata.cairo | 98 ++++++ packages/registry/src/types/role.cairo | 34 ++ packages/registry/src/types/socials.cairo | 94 ++++++ 64 files changed, 3539 insertions(+), 1237 deletions(-) create mode 100644 contracts/src/constants.cairo delete mode 100644 contracts/src/systems/actions.cairo create mode 100644 contracts/src/systems/controller.cairo create mode 100644 contracts/src/systems/registry.cairo create mode 100644 contracts/src/systems/slot.cairo create mode 100644 packages/controller/README.md create mode 100644 packages/controller/Scarb.toml create mode 100644 packages/controller/src/components/controllable.cairo create mode 100644 packages/controller/src/constants.cairo create mode 100644 packages/controller/src/lib.cairo create mode 100644 packages/controller/src/models/account.cairo create mode 100644 packages/controller/src/models/controller.cairo create mode 100644 packages/controller/src/models/index.cairo create mode 100644 packages/controller/src/models/member.cairo create mode 100644 packages/controller/src/models/signer.cairo create mode 100644 packages/controller/src/models/team.cairo create mode 100644 packages/controller/src/store.cairo create mode 100644 packages/controller/src/types/method.cairo create mode 100644 packages/controller/src/types/role.cairo create mode 100644 packages/provider/README.md create mode 100644 packages/provider/Scarb.toml create mode 100644 packages/provider/src/components/deployable.cairo create mode 100644 packages/provider/src/constants.cairo create mode 100644 packages/provider/src/elements/services/interface.cairo create mode 100644 packages/provider/src/elements/services/katana.cairo create mode 100644 packages/provider/src/elements/services/saya.cairo create mode 100644 packages/provider/src/elements/services/torii.cairo create mode 100644 packages/provider/src/lib.cairo create mode 100644 packages/provider/src/models/deployment.cairo create mode 100644 packages/provider/src/models/factory.cairo create mode 100644 packages/provider/src/models/index.cairo create mode 100644 packages/provider/src/store.cairo create mode 100644 packages/provider/src/types/service.cairo create mode 100644 packages/provider/src/types/status.cairo create mode 100644 packages/provider/src/types/tier.cairo delete mode 100644 packages/registry/src/components/controllable.cairo create mode 100644 packages/registry/src/components/initializable.cairo create mode 100644 packages/registry/src/components/registerable.cairo rename packages/registry/src/components/{registrable.cairo => trackable.cairo} (52%) create mode 100644 packages/registry/src/helpers/json.cairo create mode 100644 packages/registry/src/models/access.cairo delete mode 100644 packages/registry/src/tests/mocks/controller.cairo create mode 100644 packages/registry/src/tests/mocks/register.cairo delete mode 100644 packages/registry/src/tests/mocks/registrer.cairo create mode 100644 packages/registry/src/tests/mocks/tracker.cairo delete mode 100644 packages/registry/src/tests/test_controllable.cairo create mode 100644 packages/registry/src/tests/test_registerable.cairo delete mode 100644 packages/registry/src/tests/test_registrable.cairo create mode 100644 packages/registry/src/tests/test_trackable.cairo create mode 100644 packages/registry/src/types/metadata.cairo create mode 100644 packages/registry/src/types/role.cairo create mode 100644 packages/registry/src/types/socials.cairo diff --git a/Scarb.lock b/Scarb.lock index 5e0bf28..08088cd 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -2,7 +2,7 @@ version = 1 [[package]] -name = "arcade_registry" +name = "arcade_trophy" version = "0.0.0" dependencies = [ "dojo", @@ -10,7 +10,7 @@ dependencies = [ ] [[package]] -name = "arcade_trophy" +name = "controller" version = "0.0.0" dependencies = [ "dojo", @@ -37,3 +37,19 @@ dependencies = [ name = "dojo_plugin" version = "2.8.4" source = "git+https://github.com/dojoengine/dojo?tag=v1.0.1#d7335e6f5c76a1dda887ec00c594c2c019b4a05f" + +[[package]] +name = "provider" +version = "0.0.0" +dependencies = [ + "dojo", + "dojo_cairo_test", +] + +[[package]] +name = "registry" +version = "0.0.0" +dependencies = [ + "dojo", + "dojo_cairo_test", +] diff --git a/Scarb.toml b/Scarb.toml index 5214c64..808b9a2 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -1,5 +1,10 @@ [workspace] -members = ["packages/trophy", "packages/registry"] +members = [ + "packages/trophy", + "packages/registry", + "packages/provider", + "packages/controller", +] description = "Dojo achievement library" homepage = "https://github.com/cartridge-gg/arcade/" cairo-version = "2.8.4" diff --git a/contracts/Scarb.lock b/contracts/Scarb.lock index 6288b45..66547a8 100644 --- a/contracts/Scarb.lock +++ b/contracts/Scarb.lock @@ -2,7 +2,18 @@ version = 1 [[package]] -name = "arcade_registry" +name = "arcade" +version = "0.0.0" +dependencies = [ + "controller", + "dojo", + "dojo_cairo_test", + "provider", + "registry", +] + +[[package]] +name = "controller" version = "0.0.0" dependencies = [ "dojo", @@ -10,8 +21,8 @@ dependencies = [ [[package]] name = "dojo" -version = "1.0.0" -source = "git+https://github.com/dojoengine/dojo?tag=v1.0.0#74280d48fa2828095331487dede59f9b2e378cd3" +version = "1.0.1" +source = "git+https://github.com/dojoengine/dojo?tag=v1.0.1#d7335e6f5c76a1dda887ec00c594c2c019b4a05f" dependencies = [ "dojo_plugin", ] @@ -19,7 +30,7 @@ dependencies = [ [[package]] name = "dojo_cairo_test" version = "1.0.0-rc.0" -source = "git+https://github.com/dojoengine/dojo?tag=v1.0.0#74280d48fa2828095331487dede59f9b2e378cd3" +source = "git+https://github.com/dojoengine/dojo?tag=v1.0.1#d7335e6f5c76a1dda887ec00c594c2c019b4a05f" dependencies = [ "dojo", ] @@ -27,13 +38,18 @@ dependencies = [ [[package]] name = "dojo_plugin" version = "2.8.4" -source = "git+https://github.com/dojoengine/dojo?tag=v1.0.0#74280d48fa2828095331487dede59f9b2e378cd3" +source = "git+https://github.com/dojoengine/dojo?tag=v1.0.1#d7335e6f5c76a1dda887ec00c594c2c019b4a05f" [[package]] -name = "game_center" +name = "provider" +version = "0.0.0" +dependencies = [ + "dojo", +] + +[[package]] +name = "registry" version = "0.0.0" dependencies = [ - "arcade_registry", "dojo", - "dojo_cairo_test", ] diff --git a/contracts/Scarb.toml b/contracts/Scarb.toml index fbccbec..740ef08 100644 --- a/contracts/Scarb.toml +++ b/contracts/Scarb.toml @@ -1,6 +1,6 @@ [package] cairo-version = "2.8.4" -name = "game_center" +name = "arcade" version = "0.0.0" [cairo] @@ -11,7 +11,9 @@ dev = "sozo clean && sozo build --typescript && sozo migrate plan && sozo migrat [dependencies] dojo = { git = "https://github.com/dojoengine/dojo", tag = "v1.0.1" } -arcade_registry = { path = ".." } +controller = { path = ".." } +provider = { path = ".." } +registry = { path = ".." } starknet = "2.8.4" cairo_test = "2.8.4" @@ -21,8 +23,16 @@ dojo_cairo_test = { git = "https://github.com/dojoengine/dojo", tag = "v1.0.1" } [[target.starknet-contract]] build-external-contracts = [ "dojo::world::world_contract::world", - "arcade_registry::models::index::m_Game", - "arcade_registry::models::index::m_Achievement", + "controller::models::index::m_Account", + "controller::models::index::m_Controller", + "controller::models::index::m_Member", + "controller::models::index::m_Signer", + "controller::models::index::m_Team", + "provider::models::index::m_Deployment", + "provider::models::index::m_Factory", + "registry::models::index::m_Access", + "registry::models::index::m_Achievement", + "registry::models::index::m_Game", ] [profile.slot] diff --git a/contracts/src/constants.cairo b/contracts/src/constants.cairo new file mode 100644 index 0000000..17451ba --- /dev/null +++ b/contracts/src/constants.cairo @@ -0,0 +1,3 @@ +pub fn NAMESPACE() -> ByteArray { + "ARCADE" +} diff --git a/contracts/src/lib.cairo b/contracts/src/lib.cairo index e90e396..b490fef 100644 --- a/contracts/src/lib.cairo +++ b/contracts/src/lib.cairo @@ -1,3 +1,7 @@ +mod constants; + mod systems { - mod actions; + mod controller; + mod registry; + mod slot; } diff --git a/contracts/src/systems/actions.cairo b/contracts/src/systems/actions.cairo deleted file mode 100644 index 87203b8..0000000 --- a/contracts/src/systems/actions.cairo +++ /dev/null @@ -1,293 +0,0 @@ -// Interfaces - -#[starknet::interface] -trait IActions { - fn register_game( - self: @TContractState, - world_address: felt252, - namespace: felt252, - name: ByteArray, - description: ByteArray, - torii_url: ByteArray, - image_uri: ByteArray - ); - fn update_game( - self: @TContractState, - world_address: felt252, - namespace: felt252, - name: ByteArray, - description: ByteArray, - torii_url: ByteArray, - image_uri: ByteArray - ); - fn publish_game(self: @TContractState, world_address: felt252, namespace: felt252); - fn hide_game(self: @TContractState, world_address: felt252, namespace: felt252); - fn whitelist_game(self: @TContractState, world_address: felt252, namespace: felt252); - fn blacklist_game(self: @TContractState, world_address: felt252, namespace: felt252); - fn remove_game(self: @TContractState, world_address: felt252, namespace: felt252); - fn register_achievement( - self: @TContractState, - world_address: felt252, - namespace: felt252, - identifier: felt252, - karma: u16 - ); - fn update_achievement( - self: @TContractState, - world_address: felt252, - namespace: felt252, - identifier: felt252, - karma: u16 - ); - fn publish_achievement( - self: @TContractState, world_address: felt252, namespace: felt252, identifier: felt252 - ); - fn hide_achievement( - self: @TContractState, world_address: felt252, namespace: felt252, identifier: felt252 - ); - fn whitelist_achievement( - self: @TContractState, world_address: felt252, namespace: felt252, identifier: felt252 - ); - fn blacklist_achievement( - self: @TContractState, world_address: felt252, namespace: felt252, identifier: felt252 - ); - fn remove_achievement( - self: @TContractState, world_address: felt252, namespace: felt252, identifier: felt252 - ); -} - -// Contracts - -#[dojo::contract] -mod Actions { - // Core imports - - use core::poseidon::poseidon_hash_span; - - // Starknet imports - - use starknet::get_caller_address; - - // Dojo imports - - use dojo::world::WorldStorage; - - // Component imports - - use arcade_registry::components::controllable::ControllableComponent; - use arcade_registry::components::registrable::RegistrableComponent; - - // Local imports - - use super::IActions; - - // Components - - component!(path: ControllableComponent, storage: controllable, event: ControllableEvent); - impl ControllableInternalImpl = ControllableComponent::InternalImpl; - component!(path: RegistrableComponent, storage: registrable, event: RegistrableEvent); - impl RegistrableInternalImpl = RegistrableComponent::InternalImpl; - - // Storage - - #[storage] - struct Storage { - #[substorage(v0)] - controllable: ControllableComponent::Storage, - #[substorage(v0)] - registrable: RegistrableComponent::Storage, - } - - // Events - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ControllableEvent: ControllableComponent::Event, - #[flat] - RegistrableEvent: RegistrableComponent::Event, - } - - // Errors - - mod errors { - const ACTIONS_CALLER_NOT_OWNER: felt252 = 'Actions: caller not owner'; - } - - // Implementations - - #[abi(embed_v0)] - impl ActionsImpl of IActions { - fn register_game( - self: @ContractState, - world_address: felt252, - namespace: felt252, - name: ByteArray, - description: ByteArray, - torii_url: ByteArray, - image_uri: ByteArray, - ) { - let owner: felt252 = get_caller_address().into(); - self - .registrable - .register_game( - self.world_storage(), - world_address, - namespace, - name, - description, - torii_url, - image_uri, - owner - ) - } - - fn update_game( - self: @ContractState, - world_address: felt252, - namespace: felt252, - name: ByteArray, - description: ByteArray, - torii_url: ByteArray, - image_uri: ByteArray, - ) { - // [Check] Caller is the game owner - let world = self.world_storage(); - self.controllable.assert_is_owner(world, world_address, namespace); - // [Effect] Update game - self - .registrable - .update_game( - world, world_address, namespace, name, description, torii_url, image_uri - ) - } - - fn publish_game(self: @ContractState, world_address: felt252, namespace: felt252) { - // [Check] Caller is the game owner - let world = self.world_storage(); - self.controllable.assert_is_owner(world, world_address, namespace); - // [Effect] Publish game - self.registrable.publish_game(world, world_address, namespace); - } - - fn hide_game(self: @ContractState, world_address: felt252, namespace: felt252) { - // [Check] Caller is the game owner - let world = self.world_storage(); - self.controllable.assert_is_owner(world, world_address, namespace); - // [Effect] Hide game - self.registrable.hide_game(world, world_address, namespace); - } - - fn whitelist_game(self: @ContractState, world_address: felt252, namespace: felt252) { - // [Check] Caller is a resource owner or writer - let world = self.world_storage(); - // self.controllable.assert_is_authorized(world); - // [Effect] Whitelist game - self.registrable.whitelist_game(world, world_address, namespace); - } - - fn blacklist_game(self: @ContractState, world_address: felt252, namespace: felt252) { - // [Check] Caller is a resource owner or writer - let world = self.world_storage(); - // self.controllable.assert_is_authorized(world); - // [Effect] Blacklist game - self.registrable.blacklist_game(world, world_address, namespace); - } - - fn remove_game(self: @ContractState, world_address: felt252, namespace: felt252) { - // [Check] Caller is the game owner - let world = self.world_storage(); - self.controllable.assert_is_owner(world, world_address, namespace); - // [Effect] Remove game - self.registrable.remove_game(world, world_address, namespace) - } - - fn register_achievement( - self: @ContractState, - world_address: felt252, - namespace: felt252, - identifier: felt252, - karma: u16, - ) { - // [Check] Caller is the game owner - let world = self.world_storage(); - self.controllable.assert_is_owner(world, world_address, namespace); - // [Effect] Register achievement - self - .registrable - .register_achievement(world, world_address, namespace, identifier, karma) - } - - fn update_achievement( - self: @ContractState, - world_address: felt252, - namespace: felt252, - identifier: felt252, - karma: u16, - ) { - // [Check] Caller is the game owner - let world = self.world_storage(); - self.controllable.assert_is_owner(world, world_address, namespace); - // [Effect] Update achievement - self.registrable.update_achievement(world, world_address, namespace, identifier, karma) - } - - fn publish_achievement( - self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 - ) { - // [Check] Caller is the game owner - let world = self.world_storage(); - self.controllable.assert_is_owner(world, world_address, namespace); - // [Effect] Publish achievement - self.registrable.publish_achievement(world, world_address, namespace, identifier); - } - - fn hide_achievement( - self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 - ) { - // [Check] Caller is the game owner - let world = self.world_storage(); - self.controllable.assert_is_owner(world, world_address, namespace); - // [Effect] Whitelist achievement - self.registrable.whitelist_achievement(world, world_address, namespace, identifier); - } - - fn whitelist_achievement( - self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 - ) { - // [Check] Caller is a resource owner or writer - let world = self.world_storage(); - // self.controllable.assert_is_authorized(world); - // [Effect] Whitelist achievement - self.registrable.whitelist_achievement(world, world_address, namespace, identifier); - } - - fn blacklist_achievement( - self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 - ) { - // [Check] Caller is a resource owner or writer - let world = self.world_storage(); - // self.controllable.assert_is_authorized(world); - // [Effect] Blacklist achievement - self.registrable.blacklist_achievement(world, world_address, namespace, identifier); - } - - fn remove_achievement( - self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 - ) { - // [Check] Caller is the game owner - let world = self.world_storage(); - self.controllable.assert_is_owner(world, world_address, namespace); - // [Effect] Remove achievement - self.registrable.remove_achievement(world, world_address, namespace, identifier); - } - } - - #[generate_trait] - impl Private of PrivateTrait { - fn world_storage(self: @ContractState) -> WorldStorage { - self.world(@"game_center") - } - } -} diff --git a/contracts/src/systems/controller.cairo b/contracts/src/systems/controller.cairo new file mode 100644 index 0000000..116aa67 --- /dev/null +++ b/contracts/src/systems/controller.cairo @@ -0,0 +1,59 @@ +// Interfaces + +#[starknet::interface] +trait IController {} + +// Contracts + +#[dojo::contract] +mod Controller { + // Dojo imports + + use dojo::world::WorldStorage; + + // Component imports + + use controller::components::controllable::ControllableComponent; + + // Internal imports + + use arcade::constants::NAMESPACE; + + // Local imports + + use super::IController; + + // Components + + component!(path: ControllableComponent, storage: controllable, event: ControllableEvent); + impl ControllableInternalImpl = ControllableComponent::InternalImpl; + + // Storage + + #[storage] + struct Storage { + #[substorage(v0)] + controllable: ControllableComponent::Storage, + } + + // Events + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ControllableEvent: ControllableComponent::Event, + } + + // Implementations + + #[abi(embed_v0)] + impl ControllerImpl of IController {} + + #[generate_trait] + impl Private of PrivateTrait { + fn world_storage(self: @ContractState) -> WorldStorage { + self.world(@NAMESPACE()) + } + } +} diff --git a/contracts/src/systems/registry.cairo b/contracts/src/systems/registry.cairo new file mode 100644 index 0000000..ae091ea --- /dev/null +++ b/contracts/src/systems/registry.cairo @@ -0,0 +1,287 @@ +// Interfaces + +#[starknet::interface] +trait IRegistry { + fn register_game( + self: @TContractState, + world_address: felt252, + namespace: felt252, + project: felt252, + color: Option, + name: Option, + description: Option, + image: Option, + banner: Option, + discord: Option, + telegram: Option, + twitter: Option, + youtube: Option, + website: Option, + ); + fn update_game( + self: @TContractState, + world_address: felt252, + namespace: felt252, + color: Option, + name: Option, + description: Option, + image: Option, + banner: Option, + discord: Option, + telegram: Option, + twitter: Option, + youtube: Option, + website: Option, + ); + fn publish_game(self: @TContractState, world_address: felt252, namespace: felt252); + fn hide_game(self: @TContractState, world_address: felt252, namespace: felt252); + fn whitelist_game(self: @TContractState, world_address: felt252, namespace: felt252); + fn blacklist_game(self: @TContractState, world_address: felt252, namespace: felt252); + fn remove_game(self: @TContractState, world_address: felt252, namespace: felt252); + fn register_achievement( + self: @TContractState, + world_address: felt252, + namespace: felt252, + identifier: felt252, + karma: u16, + ); + fn update_achievement( + self: @TContractState, + world_address: felt252, + namespace: felt252, + identifier: felt252, + karma: u16, + ); + fn publish_achievement( + self: @TContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ); + fn hide_achievement( + self: @TContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ); + fn whitelist_achievement( + self: @TContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ); + fn blacklist_achievement( + self: @TContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ); + fn remove_achievement( + self: @TContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ); +} + +// Contracts + +#[dojo::contract] +mod Registry { + // Dojo imports + + use dojo::world::WorldStorage; + + // Component imports + + use registry::components::initializable::InitializableComponent; + use registry::components::registerable::RegisterableComponent; + use registry::components::trackable::TrackableComponent; + + // Internal imports + + use arcade::constants::NAMESPACE; + + // Local imports + + use super::IRegistry; + + // Components + + component!(path: InitializableComponent, storage: initializable, event: InitializableEvent); + impl InitializableImpl = InitializableComponent::InternalImpl; + component!(path: RegisterableComponent, storage: registerable, event: RegisterableEvent); + impl RegisterableImpl = RegisterableComponent::InternalImpl; + component!(path: TrackableComponent, storage: trackable, event: TrackableEvent); + impl TrackableImpl = TrackableComponent::InternalImpl; + + // Storage + + #[storage] + struct Storage { + #[substorage(v0)] + initializable: InitializableComponent::Storage, + #[substorage(v0)] + registerable: RegisterableComponent::Storage, + #[substorage(v0)] + trackable: TrackableComponent::Storage, + } + + // Events + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + InitializableEvent: InitializableComponent::Event, + #[flat] + RegisterableEvent: RegisterableComponent::Event, + #[flat] + TrackableEvent: TrackableComponent::Event, + } + + // Constructor + + fn dojo_init(self: @ContractState, owner: felt252) { + self.initializable.initialize(self.world_storage(), owner); + } + + // Implementations + + #[abi(embed_v0)] + impl RegistryImpl of IRegistry { + fn register_game( + self: @ContractState, + world_address: felt252, + namespace: felt252, + project: felt252, + color: Option, + name: Option, + description: Option, + image: Option, + banner: Option, + discord: Option, + telegram: Option, + twitter: Option, + youtube: Option, + website: Option, + ) { + self + .registerable + .register( + self.world_storage(), + world_address, + namespace, + project, + color, + name, + description, + image, + banner, + discord, + telegram, + twitter, + youtube, + website, + ) + } + + fn update_game( + self: @ContractState, + world_address: felt252, + namespace: felt252, + color: Option, + name: Option, + description: Option, + image: Option, + banner: Option, + discord: Option, + telegram: Option, + twitter: Option, + youtube: Option, + website: Option, + ) { + self + .registerable + .update( + self.world_storage(), + world_address, + namespace, + color, + name, + description, + image, + banner, + discord, + telegram, + twitter, + youtube, + website, + ) + } + + fn publish_game(self: @ContractState, world_address: felt252, namespace: felt252) { + self.registerable.publish(self.world_storage(), world_address, namespace); + } + + fn hide_game(self: @ContractState, world_address: felt252, namespace: felt252) { + self.registerable.hide(self.world_storage(), world_address, namespace); + } + + fn whitelist_game(self: @ContractState, world_address: felt252, namespace: felt252) { + self.registerable.whitelist(self.world_storage(), world_address, namespace); + } + + fn blacklist_game(self: @ContractState, world_address: felt252, namespace: felt252) { + self.registerable.blacklist(self.world_storage(), world_address, namespace); + } + + fn remove_game(self: @ContractState, world_address: felt252, namespace: felt252) { + self.registerable.remove(self.world_storage(), world_address, namespace); + } + + fn register_achievement( + self: @ContractState, + world_address: felt252, + namespace: felt252, + identifier: felt252, + karma: u16, + ) { + self + .trackable + .register(self.world_storage(), world_address, namespace, identifier, karma) + } + + fn update_achievement( + self: @ContractState, + world_address: felt252, + namespace: felt252, + identifier: felt252, + karma: u16, + ) { + self.trackable.update(self.world_storage(), world_address, namespace, identifier, karma) + } + + fn publish_achievement( + self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ) { + self.trackable.publish(self.world_storage(), world_address, namespace, identifier); + } + + fn hide_achievement( + self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ) { + self.trackable.hide(self.world_storage(), world_address, namespace, identifier); + } + + fn whitelist_achievement( + self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ) { + self.trackable.whitelist(self.world_storage(), world_address, namespace, identifier); + } + + fn blacklist_achievement( + self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ) { + self.trackable.blacklist(self.world_storage(), world_address, namespace, identifier); + } + + fn remove_achievement( + self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ) { + self.trackable.remove(self.world_storage(), world_address, namespace, identifier); + } + } + + #[generate_trait] + impl Private of PrivateTrait { + fn world_storage(self: @ContractState) -> WorldStorage { + self.world(@NAMESPACE()) + } + } +} diff --git a/contracts/src/systems/slot.cairo b/contracts/src/systems/slot.cairo new file mode 100644 index 0000000..7a28021 --- /dev/null +++ b/contracts/src/systems/slot.cairo @@ -0,0 +1,74 @@ +// Interfaces + +#[starknet::interface] +trait ISlot { + fn deploy(self: @TContractState, service: u8, project: felt252, tier: u8); + fn remove(self: @TContractState, service: u8, project: felt252); +} + +// Contracts + +#[dojo::contract] +mod Slot { + // Dojo imports + + use dojo::world::WorldStorage; + + // External imports + + use provider::components::deployable::DeployableComponent; + use provider::types::service::Service; + use provider::types::tier::Tier; + + // Internal imports + + use arcade::constants::NAMESPACE; + + // Local imports + + use super::ISlot; + + // Components + + component!(path: DeployableComponent, storage: deployable, event: DeployableEvent); + impl DeployableImpl = DeployableComponent::InternalImpl; + + // Storage + + #[storage] + struct Storage { + #[substorage(v0)] + deployable: DeployableComponent::Storage, + } + + // Events + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + DeployableEvent: DeployableComponent::Event, + } + + // Implementations + + #[abi(embed_v0)] + impl SlotImpl of ISlot { + fn deploy(self: @ContractState, service: u8, project: felt252, tier: u8,) { + let world = self.world_storage(); + self.deployable.deploy(world, service.into(), project, tier.into()) + } + + fn remove(self: @ContractState, service: u8, project: felt252,) { + let world = self.world_storage(); + self.deployable.remove(world, service.into(), project); + } + } + + #[generate_trait] + impl Private of PrivateTrait { + fn world_storage(self: @ContractState) -> WorldStorage { + self.world(@NAMESPACE()) + } + } +} diff --git a/packages/controller/README.md b/packages/controller/README.md new file mode 100644 index 0000000..e69de29 diff --git a/packages/controller/Scarb.toml b/packages/controller/Scarb.toml new file mode 100644 index 0000000..2823fb9 --- /dev/null +++ b/packages/controller/Scarb.toml @@ -0,0 +1,9 @@ +[package] +name = "controller" +version.workspace = true + +[dependencies] +dojo.workspace = true + +[dev-dependencies] +dojo_cairo_test.workspace = true diff --git a/packages/controller/src/components/controllable.cairo b/packages/controller/src/components/controllable.cairo new file mode 100644 index 0000000..5854165 --- /dev/null +++ b/packages/controller/src/components/controllable.cairo @@ -0,0 +1,31 @@ +#[starknet::component] +mod ControllableComponent { + // Dojo imports + + use dojo::world::WorldStorage; + + // Internal imports + + use controller::store::{Store, StoreTrait}; + use controller::models::account::{Account, AccountTrait, AccountAssert}; + use controller::models::controller::{Controller, ControllerTrait, ControllerAssert}; + use controller::models::member::{Member, MemberTrait, MemberAssert}; + use controller::models::signer::{Signer, SignerTrait, SignerAssert}; + use controller::models::team::{Team, TeamTrait, TeamAssert}; + + // Storage + + #[storage] + struct Storage {} + + // Events + + #[event] + #[derive(Drop, starknet::Event)] + enum Event {} + + #[generate_trait] + impl InternalImpl< + TContractState, +HasComponent + > of InternalTrait {} +} diff --git a/packages/controller/src/constants.cairo b/packages/controller/src/constants.cairo new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/controller/src/constants.cairo @@ -0,0 +1 @@ + diff --git a/packages/controller/src/lib.cairo b/packages/controller/src/lib.cairo new file mode 100644 index 0000000..c62d605 --- /dev/null +++ b/packages/controller/src/lib.cairo @@ -0,0 +1,20 @@ +mod constants; +mod store; + +mod types { + mod method; + mod role; +} + +mod models { + mod index; + mod account; + mod controller; + mod member; + mod signer; + mod team; +} + +mod components { + mod controllable; +} diff --git a/packages/controller/src/models/account.cairo b/packages/controller/src/models/account.cairo new file mode 100644 index 0000000..0eac43e --- /dev/null +++ b/packages/controller/src/models/account.cairo @@ -0,0 +1,92 @@ +// Intenral imports + +use controller::models::index::Account; + +// Errors + +pub mod errors { + pub const ACCOUNT_ALREADY_EXISTS: felt252 = 'Account: already exists'; + pub const ACCOUNT_NOT_EXIST: felt252 = 'Account: does not exist'; + pub const ACCOUNT_INVALID_IDENTIFIER: felt252 = 'Account: invalid identifier'; + pub const ACCOUNT_INVALID_USERNAME: felt252 = 'Account: invalid username'; +} + +#[generate_trait] +impl AccountImpl of AccountTrait { + #[inline] + fn new( + id: felt252, controllers: u32, name: felt252, username: felt252, socials: ByteArray, + ) -> Account { + // [Check] Inputs + AccountAssert::assert_valid_identifier(id); + AccountAssert::assert_valid_username(username); + // [Return] Account + Account { + id: id, + controllers: controllers, + name: name, + username: username, + socials: socials, + credits: 0, + } + } +} + +#[generate_trait] +impl AccountAssert of AssertTrait { + #[inline] + fn assert_does_not_exist(self: @Account) { + assert(self.name == @0, errors::ACCOUNT_ALREADY_EXISTS); + } + + #[inline] + fn assert_does_exist(self: @Account) { + assert(self.name != @0, errors::ACCOUNT_NOT_EXIST); + } + + #[inline] + fn assert_valid_identifier(identifier: felt252) { + assert(identifier != 0, errors::ACCOUNT_INVALID_IDENTIFIER); + } + + #[inline] + fn assert_valid_username(username: felt252) { + assert(username != 0, errors::ACCOUNT_INVALID_USERNAME); + } +} + +#[cfg(test)] +mod tests { + // Local imports + + use super::{Account, AccountTrait, AccountAssert}; + + // Constants + + const IDENTIFIER: felt252 = 'ID'; + const USERNAME: felt252 = 'USERNAME'; + + #[test] + fn test_deployment_new() { + let account = AccountTrait::new(IDENTIFIER, 0, 'NAME', USERNAME, "{}"); + assert_eq!(account.id, IDENTIFIER); + assert_eq!(account.controllers, 0); + assert_eq!(account.name, 'NAME'); + assert_eq!(account.username, USERNAME); + assert_eq!(account.socials, "{}"); + assert_eq!(account.credits, 0); + } + + #[test] + fn test_deployment_assert_does_exist() { + let account = AccountTrait::new(IDENTIFIER, 0, 'NAME', USERNAME, "{}"); + account.assert_does_exist(); + } + + #[test] + #[should_panic(expected: 'Account: already exists')] + fn test_deployment_revert_already_exists() { + let account = AccountTrait::new(IDENTIFIER, 0, 'NAME', USERNAME, "{}"); + account.assert_does_not_exist(); + } +} diff --git a/packages/controller/src/models/controller.cairo b/packages/controller/src/models/controller.cairo new file mode 100644 index 0000000..cf43b40 --- /dev/null +++ b/packages/controller/src/models/controller.cairo @@ -0,0 +1,157 @@ +// Intenral imports + +use controller::models::index::Controller; + +// Errors + +pub mod errors { + pub const CONTROLLER_ALREADY_EXISTS: felt252 = 'Controller: already exists'; + pub const CONTROLLER_NOT_EXIST: felt252 = 'Controller: does not exist'; + pub const CONTROLLER_INVALID_ACCOUNT_ID: felt252 = 'Controller: invalid account id'; + pub const CONTROLLER_INVALID_IDENTIFIER: felt252 = 'Controller: invalid identifier'; + pub const CONTROLLER_INVALID_SIGNERS: felt252 = 'Controller: invalid signers'; + pub const CONTROLLER_INVALID_ADDRESS: felt252 = 'Controller: invalid address'; + pub const CONTROLLER_INVALID_NETWORK: felt252 = 'Controller: invalid network'; +} + +#[generate_trait] +impl ControllerImpl of ControllerTrait { + #[inline] + fn new( + account_id: felt252, + id: felt252, + signers: u32, + address: felt252, + network: felt252, + constructor_calldata: ByteArray, + ) -> Controller { + // [Check] Inputs + ControllerAssert::assert_valid_account_id(account_id); + ControllerAssert::assert_valid_identifier(id); + ControllerAssert::assert_valid_signers(signers); + ControllerAssert::assert_valid_address(address); + ControllerAssert::assert_valid_network(network); + // [Return] Controller + Controller { + account_id: account_id, + id: id, + signers: signers, + address: address, + network: network, + constructor_calldata: constructor_calldata + } + } +} + +#[generate_trait] +impl ControllerAssert of AssertTrait { + #[inline] + fn assert_does_not_exist(self: @Controller) { + assert(self.account_id == @0, errors::CONTROLLER_ALREADY_EXISTS); + } + + #[inline] + fn assert_does_exist(self: @Controller) { + assert(self.account_id != @0, errors::CONTROLLER_NOT_EXIST); + } + + #[inline] + fn assert_valid_identifier(id: felt252) { + assert(id != 0, errors::CONTROLLER_INVALID_IDENTIFIER); + } + + #[inline] + fn assert_valid_account_id(account_id: felt252) { + assert(account_id != 0, errors::CONTROLLER_INVALID_ACCOUNT_ID); + } + + #[inline] + fn assert_valid_signers(signers: u32) { + assert(signers != 0, errors::CONTROLLER_INVALID_SIGNERS); + } + + #[inline] + fn assert_valid_address(address: felt252) { + assert(address != 0, errors::CONTROLLER_INVALID_ADDRESS); + } + + #[inline] + fn assert_valid_network(network: felt252) { + assert(network != 0, errors::CONTROLLER_INVALID_NETWORK); + } +} + +#[cfg(test)] +mod tests { + // Local imports + + use super::{Controller, ControllerTrait, ControllerAssert}; + + // Constants + + const IDENTIFIER: felt252 = 'IDENTIFIER'; + const ACCOUNT_ID: felt252 = 'ACCOUNT_ID'; + const SIGNERS: u32 = 1; + const ADDRESS: felt252 = 'ADDRESS'; + const NETWORK: felt252 = 'NETWORK'; + + #[test] + fn test_deployment_new() { + let controller = ControllerTrait::new( + ACCOUNT_ID, IDENTIFIER, SIGNERS, ADDRESS, NETWORK, "" + ); + assert_eq!(controller.id, IDENTIFIER); + assert_eq!(controller.account_id, ACCOUNT_ID); + assert_eq!(controller.signers, SIGNERS); + assert_eq!(controller.address, ADDRESS); + assert_eq!(controller.network, NETWORK); + assert_eq!(controller.constructor_calldata, ""); + } + + #[test] + fn test_deployment_assert_does_exist() { + let controller = ControllerTrait::new( + ACCOUNT_ID, IDENTIFIER, SIGNERS, ADDRESS, NETWORK, "" + ); + controller.assert_does_exist(); + } + + #[test] + #[should_panic(expected: 'Controller: already exists')] + fn test_deployment_revert_already_exists() { + let controller = ControllerTrait::new( + ACCOUNT_ID, IDENTIFIER, SIGNERS, ADDRESS, NETWORK, "" + ); + controller.assert_does_not_exist(); + } + + #[test] + #[should_panic(expected: 'Controller: invalid account id')] + fn test_deployment_revert_invalid_account_id() { + ControllerTrait::new(0, IDENTIFIER, SIGNERS, ADDRESS, NETWORK, ""); + } + + #[test] + #[should_panic(expected: 'Controller: invalid identifier')] + fn test_deployment_revert_invalid_identifier() { + ControllerTrait::new(ACCOUNT_ID, 0, SIGNERS, ADDRESS, NETWORK, ""); + } + + #[test] + #[should_panic(expected: 'Controller: invalid signers')] + fn test_deployment_revert_invalid_signers() { + ControllerTrait::new(ACCOUNT_ID, IDENTIFIER, 0, ADDRESS, NETWORK, ""); + } + + #[test] + #[should_panic(expected: 'Controller: invalid address')] + fn test_deployment_revert_invalid_address() { + ControllerTrait::new(ACCOUNT_ID, IDENTIFIER, SIGNERS, 0, NETWORK, ""); + } + + #[test] + #[should_panic(expected: 'Controller: invalid network')] + fn test_deployment_revert_invalid_network() { + ControllerTrait::new(ACCOUNT_ID, IDENTIFIER, SIGNERS, ADDRESS, 0, ""); + } +} diff --git a/packages/controller/src/models/index.cairo b/packages/controller/src/models/index.cairo new file mode 100644 index 0000000..19673c0 --- /dev/null +++ b/packages/controller/src/models/index.cairo @@ -0,0 +1,56 @@ +//! Models + +#[derive(Clone, Drop, Serde)] +#[dojo::model] +pub struct Account { + #[key] + id: felt252, + controllers: u32, + name: felt252, + username: felt252, + socials: ByteArray, + credits: felt252, +} + +#[derive(Clone, Drop, Serde)] +#[dojo::model] +pub struct Controller { + #[key] + account_id: felt252, + #[key] + id: felt252, + signers: u32, + address: felt252, + network: felt252, + constructor_calldata: ByteArray, +} + +#[derive(Copy, Drop, Serde)] +#[dojo::model] +pub struct Member { + #[key] + account_id: felt252, + #[key] + team_id: felt252, + role: u8, +} + +#[derive(Clone, Drop, Serde)] +#[dojo::model] +pub struct Signer { + #[key] + account_id: felt252, + #[key] + controller_id: felt252, + method: u8, + metadata: ByteArray, +} + +#[derive(Clone, Drop, Serde)] +#[dojo::model] +pub struct Team { + #[key] + id: felt252, + name: felt252, + description: ByteArray, +} diff --git a/packages/controller/src/models/member.cairo b/packages/controller/src/models/member.cairo new file mode 100644 index 0000000..77c3010 --- /dev/null +++ b/packages/controller/src/models/member.cairo @@ -0,0 +1,95 @@ +// Intenral imports + +use controller::models::index::Member; +use controller::types::role::Role; + +// Errors + +pub mod errors { + pub const MEMBER_ALREADY_EXISTS: felt252 = 'Member: already exists'; + pub const MEMBER_NOT_EXIST: felt252 = 'Member: does not exist'; + pub const MEMBER_INVALID_ACCOUNT_ID: felt252 = 'Member: invalid account id'; + pub const MEMBER_INVALID_TEAM_ID: felt252 = 'Member: invalid team id'; + pub const MEMBER_INVALID_ROLE: felt252 = 'Member: invalid role'; +} + +#[generate_trait] +impl MemberImpl of MemberTrait { + #[inline] + fn new(account_id: felt252, team_id: felt252, role: Role) -> Member { + // [Check] Inputs + MemberAssert::assert_valid_account_id(account_id); + MemberAssert::assert_valid_team_id(team_id); + MemberAssert::assert_valid_role(role); + // [Return] Member + Member { account_id: account_id, team_id: team_id, role: role.into() } + } +} + +#[generate_trait] +impl MemberAssert of AssertTrait { + #[inline] + fn assert_does_not_exist(self: @Member) { + assert(self.role == @Role::None.into(), errors::MEMBER_ALREADY_EXISTS); + } + + #[inline] + fn assert_does_exist(self: @Member) { + assert(self.role != @Role::None.into(), errors::MEMBER_NOT_EXIST); + } + + #[inline] + fn assert_valid_account_id(account_id: felt252) { + assert(account_id != 0, errors::MEMBER_INVALID_ACCOUNT_ID); + } + + #[inline] + fn assert_valid_team_id(team_id: felt252) { + assert(team_id != 0, errors::MEMBER_INVALID_TEAM_ID); + } + + #[inline] + fn assert_valid_role(role: Role) { + assert(role != Role::None, errors::MEMBER_INVALID_ROLE); + } +} + +#[cfg(test)] +mod tests { + // Local imports + + use super::{Member, MemberTrait, MemberAssert, Role}; + + // Constants + + const ACCOUNT_ID: felt252 = 'ACCOUNT_ID'; + const TEAM_ID: felt252 = 'TEAM_ID'; + const ROLE: Role = Role::Admin; + + #[test] + fn test_deployment_new() { + let member = MemberTrait::new(ACCOUNT_ID, TEAM_ID, ROLE); + assert_eq!(member.account_id, ACCOUNT_ID); + assert_eq!(member.team_id, TEAM_ID); + assert_eq!(member.role, ROLE.into()); + } + + #[test] + fn test_deployment_assert_does_exist() { + let member = MemberTrait::new(ACCOUNT_ID, TEAM_ID, ROLE); + member.assert_does_exist(); + } + + #[test] + #[should_panic(expected: 'Member: already exists')] + fn test_deployment_revert_already_exists() { + let member = MemberTrait::new(ACCOUNT_ID, TEAM_ID, ROLE); + member.assert_does_not_exist(); + } + + #[test] + #[should_panic(expected: 'Member: invalid role')] + fn test_deployment_revert_invalid_role() { + MemberTrait::new(ACCOUNT_ID, TEAM_ID, Role::None); + } +} diff --git a/packages/controller/src/models/signer.cairo b/packages/controller/src/models/signer.cairo new file mode 100644 index 0000000..bb76e27 --- /dev/null +++ b/packages/controller/src/models/signer.cairo @@ -0,0 +1,118 @@ +// Intenral imports + +use controller::models::index::Signer; +use controller::types::method::Method; + +// Errors + +pub mod errors { + pub const SIGNER_ALREADY_EXISTS: felt252 = 'Signer: already exists'; + pub const SIGNER_NOT_EXIST: felt252 = 'Signer: does not exist'; + pub const SIGNER_INVALID_ACCOUNT_ID: felt252 = 'Signer: invalid account id'; + pub const SIGNER_INVALID_CONTROLLER_ID: felt252 = 'Signer: invalid controller id'; + pub const SIGNER_INVALID_IDENTIFIER: felt252 = 'Signer: invalid identifier'; + pub const SIGNER_INVALID_METHOD: felt252 = 'Signer: invalid method'; + pub const SIGNER_INVALID_METADATA: felt252 = 'Signer: invalid metadata'; +} + +#[generate_trait] +impl SignerImpl of SignerTrait { + #[inline] + fn new( + account_id: felt252, controller_id: felt252, method: Method, metadata: ByteArray, + ) -> Signer { + // [Check] Inputs + SignerAssert::assert_valid_account_id(account_id); + SignerAssert::assert_valid_controller_id(controller_id); + SignerAssert::assert_valid_method(method); + + // [Return] Signer + Signer { + account_id: account_id, + controller_id: controller_id, + method: method.into(), + metadata: metadata, + } + } +} + +#[generate_trait] +impl SignerAssert of AssertTrait { + #[inline] + fn assert_does_not_exist(self: @Signer) { + assert(self.account_id == @0, errors::SIGNER_ALREADY_EXISTS); + } + + #[inline] + fn assert_does_exist(self: @Signer) { + assert(self.account_id != @0, errors::SIGNER_NOT_EXIST); + } + + #[inline] + fn assert_valid_account_id(account_id: felt252) { + assert(account_id != 0, errors::SIGNER_INVALID_ACCOUNT_ID); + } + + #[inline] + fn assert_valid_controller_id(controller_id: felt252) { + assert(controller_id != 0, errors::SIGNER_INVALID_CONTROLLER_ID); + } + + #[inline] + fn assert_valid_method(method: Method) { + assert(method != Method::None, errors::SIGNER_INVALID_METHOD); + } +} + +#[cfg(test)] +mod tests { + // Local imports + + use super::{Signer, SignerTrait, SignerAssert, Method}; + + // Constants + + const ACCOUNT_ID: felt252 = 'ACCOUNT_ID'; + const CONTROLLER_ID: felt252 = 'CONTROLLER_ID'; + const METHOD: Method = Method::StarknetAccount; + + #[test] + fn test_deployment_new() { + let signer = SignerTrait::new(ACCOUNT_ID, CONTROLLER_ID, METHOD, ""); + assert_eq!(signer.account_id, ACCOUNT_ID); + assert_eq!(signer.controller_id, CONTROLLER_ID); + assert_eq!(signer.method, METHOD.into()); + assert_eq!(signer.metadata, ""); + } + + #[test] + fn test_deployment_assert_does_exist() { + let signer = SignerTrait::new(ACCOUNT_ID, CONTROLLER_ID, METHOD, ""); + signer.assert_does_exist(); + } + + #[test] + #[should_panic(expected: 'Signer: already exists')] + fn test_deployment_revert_already_exists() { + let signer = SignerTrait::new(ACCOUNT_ID, CONTROLLER_ID, METHOD, ""); + signer.assert_does_not_exist(); + } + + #[test] + #[should_panic(expected: 'Signer: invalid account id')] + fn test_deployment_revert_invalid_account_id() { + SignerTrait::new(0, CONTROLLER_ID, METHOD, ""); + } + + #[test] + #[should_panic(expected: 'Signer: invalid controller id')] + fn test_deployment_revert_invalid_controller_id() { + SignerTrait::new(ACCOUNT_ID, 0, METHOD, ""); + } + + #[test] + #[should_panic(expected: 'Signer: invalid method')] + fn test_deployment_revert_invalid_method() { + SignerTrait::new(ACCOUNT_ID, CONTROLLER_ID, Method::None, ""); + } +} diff --git a/packages/controller/src/models/team.cairo b/packages/controller/src/models/team.cairo new file mode 100644 index 0000000..68586ef --- /dev/null +++ b/packages/controller/src/models/team.cairo @@ -0,0 +1,80 @@ +// Intenral imports + +use controller::models::index::Team; + +// Errors + +pub mod errors { + pub const TEAM_ALREADY_EXISTS: felt252 = 'Team: already exists'; + pub const TEAM_NOT_EXIST: felt252 = 'Team: does not exist'; + pub const TEAM_INVALID_IDENTIFIER: felt252 = 'Team: invalid identifier'; + pub const TEAM_INVALID_NAME: felt252 = 'Team: invalid name'; +} + +#[generate_trait] +impl TeamImpl of TeamTrait { + #[inline] + fn new(id: felt252, name: felt252, description: ByteArray) -> Team { + // [Check] Inputs + TeamAssert::assert_valid_identifier(id); + TeamAssert::assert_valid_name(name); + // [Return] Team + Team { id: id, name: name, description: description, } + } +} + +#[generate_trait] +impl TeamAssert of AssertTrait { + #[inline] + fn assert_does_not_exist(self: @Team) { + assert(self.name == @0, errors::TEAM_ALREADY_EXISTS); + } + + #[inline] + fn assert_does_exist(self: @Team) { + assert(self.name != @0, errors::TEAM_NOT_EXIST); + } + + #[inline] + fn assert_valid_identifier(identifier: felt252) { + assert(identifier != 0, errors::TEAM_INVALID_IDENTIFIER); + } + + #[inline] + fn assert_valid_name(name: felt252) { + assert(name != 0, errors::TEAM_INVALID_NAME); + } +} + +#[cfg(test)] +mod tests { + // Local imports + + use super::{Team, TeamTrait, TeamAssert}; + + // Constants + + const IDENTIFIER: felt252 = 'ID'; + const NAME: felt252 = 'NAME'; + + #[test] + fn test_deployment_new() { + let team = TeamTrait::new(IDENTIFIER, 'NAME', ""); + assert_eq!(team.id, IDENTIFIER); + assert_eq!(team.name, 'NAME'); + assert_eq!(team.description, ""); + } + + #[test] + fn test_deployment_assert_does_exist() { + let team = TeamTrait::new(IDENTIFIER, 'NAME', ""); + team.assert_does_exist(); + } + + #[test] + #[should_panic(expected: 'Team: already exists')] + fn test_deployment_revert_already_exists() { + let team = TeamTrait::new(IDENTIFIER, 'NAME', ""); + team.assert_does_not_exist(); + } +} diff --git a/packages/controller/src/store.cairo b/packages/controller/src/store.cairo new file mode 100644 index 0000000..96473e7 --- /dev/null +++ b/packages/controller/src/store.cairo @@ -0,0 +1,81 @@ +//! Store struct and component management methods. + +// Starknet imports + +use starknet::SyscallResultTrait; + +// Dojo imports + +use dojo::world::WorldStorage; +use dojo::model::ModelStorage; + +// Models imports + +use controller::models::account::Account; +use controller::models::controller::Controller; +use controller::models::member::Member; +use controller::models::signer::Signer; +use controller::models::team::Team; + + +// Structs + +#[derive(Copy, Drop)] +struct Store { + world: WorldStorage, +} + +// Implementations + +#[generate_trait] +impl StoreImpl of StoreTrait { + #[inline] + fn new(world: WorldStorage) -> Store { + Store { world: world } + } + + #[inline] + fn get_account(self: Store, account_id: felt252) -> Account { + self.world.read_model(account_id) + } + + #[inline] + fn get_controller(self: Store, controller_id: felt252) -> Controller { + self.world.read_model(controller_id) + } + + #[inline] + fn get_member(self: Store, account_id: felt252, team_id: felt252) -> Member { + self.world.read_model((account_id, team_id)) + } + + #[inline] + fn get_signer(self: Store, signer_id: felt252) -> Signer { + self.world.read_model(signer_id) + } + + #[inline] + fn get_team(self: Store, team_id: felt252) -> Team { + self.world.read_model(team_id) + } + + #[inline] + fn set_account(ref self: Store, account: @Account) { + self.world.write_model(account); + } + + #[inline] + fn set_controller(ref self: Store, controller: @Controller) { + self.world.write_model(controller); + } + + #[inline] + fn set_member(ref self: Store, member: @Member) { + self.world.write_model(member); + } + + #[inline] + fn set_team(ref self: Store, team: @Team) { + self.world.write_model(team); + } +} diff --git a/packages/controller/src/types/method.cairo b/packages/controller/src/types/method.cairo new file mode 100644 index 0000000..3f1462f --- /dev/null +++ b/packages/controller/src/types/method.cairo @@ -0,0 +1,31 @@ +#[derive(Copy, Drop, PartialEq)] +pub enum Method { + None, + WebAuthn, + StarknetAccount, +} + +// Implementations + +impl IntoMethodU8 of core::Into { + #[inline] + fn into(self: Method) -> u8 { + match self { + Method::None => 0, + Method::WebAuthn => 1, + Method::StarknetAccount => 2, + } + } +} + +impl IntoU8Method of core::Into { + #[inline] + fn into(self: u8) -> Method { + match self { + 0 => Method::None, + 1 => Method::WebAuthn, + 2 => Method::StarknetAccount, + _ => Method::None, + } + } +} diff --git a/packages/controller/src/types/role.cairo b/packages/controller/src/types/role.cairo new file mode 100644 index 0000000..ae3b9c1 --- /dev/null +++ b/packages/controller/src/types/role.cairo @@ -0,0 +1,34 @@ +#[derive(Copy, Drop, PartialEq)] +pub enum Role { + None, + Owner, + Admin, + Member, +} + +// Implementations + +impl IntoRoleU8 of core::Into { + #[inline] + fn into(self: Role) -> u8 { + match self { + Role::None => 0, + Role::Owner => 1, + Role::Admin => 2, + Role::Member => 3, + } + } +} + +impl IntoU8Role of core::Into { + #[inline] + fn into(self: u8) -> Role { + match self { + 0 => Role::None, + 1 => Role::Owner, + 2 => Role::Admin, + 3 => Role::Member, + _ => Role::None, + } + } +} diff --git a/packages/provider/README.md b/packages/provider/README.md new file mode 100644 index 0000000..e69de29 diff --git a/packages/provider/Scarb.toml b/packages/provider/Scarb.toml new file mode 100644 index 0000000..41a1d64 --- /dev/null +++ b/packages/provider/Scarb.toml @@ -0,0 +1,9 @@ +[package] +name = "provider" +version.workspace = true + +[dependencies] +dojo.workspace = true + +[dev-dependencies] +dojo_cairo_test.workspace = true diff --git a/packages/provider/src/components/deployable.cairo b/packages/provider/src/components/deployable.cairo new file mode 100644 index 0000000..e2668fa --- /dev/null +++ b/packages/provider/src/components/deployable.cairo @@ -0,0 +1,96 @@ +#[starknet::component] +mod DeployableComponent { + // Dojo imports + + use dojo::world::WorldStorage; + + // Internal imports + + use provider::store::{Store, StoreTrait}; + use provider::models::deployment::{Deployment, DeploymentTrait, DeploymentAssert}; + use provider::models::factory::{Factory, FactoryTrait, FactoryAssert}; + use provider::types::service::{Service, ServiceTrait, SERVICE_COUNT}; + use provider::types::status::Status; + use provider::types::tier::Tier; + + // Storage + + #[storage] + struct Storage {} + + // Events + + #[event] + #[derive(Drop, starknet::Event)] + enum Event {} + + #[generate_trait] + impl InternalImpl< + TContractState, +HasComponent + > of InternalTrait { + fn initialize(self: @ComponentState, world: WorldStorage,) { + // [Setup] Datastore + let mut store: Store = StoreTrait::new(world); + // [Effect] Create every service factories + let mut index = SERVICE_COUNT; + while index > 0 { + let service: Service = index.into(); + let version = service.version(); + let factory = FactoryTrait::new(service, version, version); + store.set_factory(@factory); + index -= 1; + } + } + + fn deploy( + self: @ComponentState, + world: WorldStorage, + service: Service, + project: felt252, + tier: Tier, + ) { + // [Setup] Datastore + let mut store: Store = StoreTrait::new(world); + + // [Check] Factory exists + let factory = store.get_factory(service.into()); + factory.assert_does_exist(); + + // [Check] Deployment does not exist + let deployment = store.get_deployment(service.into(), project); + deployment.assert_does_not_exist(); + + // [Effect] Create deployment + let owner = starknet::get_caller_address().into(); + let deployment = DeploymentTrait::new( + service: service, project: project, owner: owner, tier: tier, config: "", + ); + store.set_deployment(@deployment); + } + + fn remove( + self: @ComponentState, + world: WorldStorage, + service: Service, + project: felt252, + ) { + // [Setup] Datastore + let mut store: Store = StoreTrait::new(world); + + // [Check] Factory exists + let factory = store.get_factory(service.into()); + factory.assert_does_exist(); + + // [Check] Deployment exists + let mut deployment = store.get_deployment(service.into(), project); + deployment.assert_does_exist(); + + // [Check] Caller is owner + deployment.assert_is_owner(starknet::get_caller_address().into()); + + // [Effect] Delete deployment + deployment.nullify(); + store.delete_deployment(@deployment); + } + } +} diff --git a/packages/provider/src/constants.cairo b/packages/provider/src/constants.cairo new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/provider/src/constants.cairo @@ -0,0 +1 @@ + diff --git a/packages/provider/src/elements/services/interface.cairo b/packages/provider/src/elements/services/interface.cairo new file mode 100644 index 0000000..00bd093 --- /dev/null +++ b/packages/provider/src/elements/services/interface.cairo @@ -0,0 +1,3 @@ +trait ServiceTrait { + fn version() -> felt252; +} diff --git a/packages/provider/src/elements/services/katana.cairo b/packages/provider/src/elements/services/katana.cairo new file mode 100644 index 0000000..9ba0892 --- /dev/null +++ b/packages/provider/src/elements/services/katana.cairo @@ -0,0 +1,7 @@ +use provider::elements::services::interface::ServiceTrait; + +impl Katana of ServiceTrait { + fn version() -> felt252 { + '1.0.1' + } +} diff --git a/packages/provider/src/elements/services/saya.cairo b/packages/provider/src/elements/services/saya.cairo new file mode 100644 index 0000000..4173f97 --- /dev/null +++ b/packages/provider/src/elements/services/saya.cairo @@ -0,0 +1,7 @@ +use provider::elements::services::interface::ServiceTrait; + +impl Saya of ServiceTrait { + fn version() -> felt252 { + '1.0.1' + } +} diff --git a/packages/provider/src/elements/services/torii.cairo b/packages/provider/src/elements/services/torii.cairo new file mode 100644 index 0000000..3f7cdd7 --- /dev/null +++ b/packages/provider/src/elements/services/torii.cairo @@ -0,0 +1,7 @@ +use provider::elements::services::interface::ServiceTrait; + +impl Torii of ServiceTrait { + fn version() -> felt252 { + '1.0.1' + } +} diff --git a/packages/provider/src/lib.cairo b/packages/provider/src/lib.cairo new file mode 100644 index 0000000..8277531 --- /dev/null +++ b/packages/provider/src/lib.cairo @@ -0,0 +1,27 @@ +mod constants; +mod store; + +mod elements { + mod services { + mod interface; + mod katana; + mod torii; + mod saya; + } +} + +mod types { + mod tier; + mod service; + mod status; +} + +mod models { + mod index; + mod deployment; + mod factory; +} + +mod components { + mod deployable; +} diff --git a/packages/provider/src/models/deployment.cairo b/packages/provider/src/models/deployment.cairo new file mode 100644 index 0000000..d36653c --- /dev/null +++ b/packages/provider/src/models/deployment.cairo @@ -0,0 +1,141 @@ +// Intenral imports + +use provider::models::index::Deployment; +use provider::types::service::Service; +use provider::types::status::Status; +use provider::types::tier::Tier; + +// Errors + +pub mod errors { + pub const DEPLOYMENT_ALREADY_EXISTS: felt252 = 'Deployment: already exists'; + pub const DEPLOYMENT_NOT_EXIST: felt252 = 'Deployment: does not exist'; + pub const DEPLOYMENT_INVALID_SERVICE: felt252 = 'Deployment: invalid service'; + pub const DEPLOYMENT_INVALID_PROJECT: felt252 = 'Deployment: invalid project'; + pub const DEPLOYMENT_INVALID_OWNER: felt252 = 'Deployment: invalid owner'; + pub const DEPLOYMENT_INVALID_STATUS: felt252 = 'Deployment: invalid status'; + pub const DEPLOYMENT_INVALID_TIER: felt252 = 'Deployment: invalid tier'; + pub const DEPLOYMENT_NOT_OWNER: felt252 = 'Deployment: caller is not owner'; +} + +#[generate_trait] +impl DeploymentImpl of DeploymentTrait { + #[inline] + fn new( + service: Service, project: felt252, owner: felt252, tier: Tier, config: ByteArray, + ) -> Deployment { + // [Check] Inputs + DeploymentAssert::assert_valid_service(service); + DeploymentAssert::assert_valid_project(project); + DeploymentAssert::assert_valid_owner(owner); + DeploymentAssert::assert_valid_tier(tier); + // [Return] Deployment + Deployment { + service: service.into(), + project: project, + owner: owner, + status: Status::Disabled.into(), + tier: tier.into(), + config: config, + } + } + + #[inline] + fn nullify(ref self: Deployment) { + self.owner = 0; + } +} + +#[generate_trait] +impl DeploymentAssert of AssertTrait { + #[inline] + fn assert_does_not_exist(self: @Deployment) { + assert(self.owner == @0, errors::DEPLOYMENT_ALREADY_EXISTS); + } + + #[inline] + fn assert_does_exist(self: @Deployment) { + assert(self.owner != @0, errors::DEPLOYMENT_NOT_EXIST); + } + + #[inline] + fn assert_valid_service(service: Service) { + assert(service != Service::None, errors::DEPLOYMENT_INVALID_SERVICE); + } + + #[inline] + fn assert_valid_project(project: felt252) { + assert(project != 0, errors::DEPLOYMENT_INVALID_PROJECT); + } + + #[inline] + fn assert_valid_owner(owner: felt252) { + assert(owner != 0, errors::DEPLOYMENT_INVALID_OWNER); + } + + #[inline] + fn assert_valid_status(status: Status) { + assert(status != Status::None, errors::DEPLOYMENT_INVALID_STATUS); + } + + #[inline] + fn assert_valid_tier(tier: Tier) { + assert(tier != Tier::None, errors::DEPLOYMENT_INVALID_TIER); + } + + #[inline] + fn assert_is_owner(self: @Deployment, caller: felt252) { + assert(@caller == self.owner, errors::DEPLOYMENT_NOT_OWNER); + } +} + +#[cfg(test)] +mod tests { + // Local imports + + use super::{Deployment, DeploymentTrait, DeploymentAssert, Service, Status, Tier}; + + // Constants + + const SERVICE: Service = Service::Katana; + const PROJECT: felt252 = 'PROJECT'; + const OWNER: felt252 = 'OWNER'; + const TIER: Tier = Tier::Basic; + const NOT_OWNER: felt252 = 'NOT_OWNER'; + + #[test] + fn test_deployment_new() { + let deployment = DeploymentTrait::new(SERVICE, PROJECT, OWNER, TIER, ""); + assert_eq!(deployment.service, SERVICE.into()); + assert_eq!(deployment.project, PROJECT); + assert_eq!(deployment.owner, OWNER); + assert_eq!(deployment.tier, TIER.into()); + assert_eq!(deployment.config, ""); + } + + #[test] + fn test_deployment_assert_does_exist() { + let deployment = DeploymentTrait::new(SERVICE, PROJECT, OWNER, TIER, ""); + deployment.assert_does_exist(); + } + + #[test] + #[should_panic(expected: 'Deployment: already exists')] + fn test_deployment_revert_already_exists() { + let mut deployment = DeploymentTrait::new(SERVICE, PROJECT, OWNER, TIER, ""); + deployment.assert_does_not_exist(); + } + + #[test] + fn test_deployment_assert_is_owner() { + let deployment = DeploymentTrait::new(SERVICE, PROJECT, OWNER, TIER, ""); + deployment.assert_is_owner(OWNER); + } + + #[test] + #[should_panic(expected: 'Deployment: caller is not owner')] + fn test_deployment_revert_not_owner() { + let deployment = DeploymentTrait::new(SERVICE, PROJECT, OWNER, TIER, ""); + deployment.assert_is_owner(NOT_OWNER); + } +} diff --git a/packages/provider/src/models/factory.cairo b/packages/provider/src/models/factory.cairo new file mode 100644 index 0000000..985e54f --- /dev/null +++ b/packages/provider/src/models/factory.cairo @@ -0,0 +1,84 @@ +// Intenral imports + +use provider::models::index::Factory; +use provider::types::service::Service; + +// Errors + +pub mod errors { + pub const SERVICE_ALREADY_EXISTS: felt252 = 'Factory: already exists'; + pub const SERVICE_NOT_EXIST: felt252 = 'Factory: does not exist'; + pub const SERVICE_INVALID_IDENTIFIER: felt252 = 'Factory: invalid identifier'; + pub const SERVICE_INVALID_VERSION: felt252 = 'Factory: invalid version'; +} + +#[generate_trait] +impl FactoryImpl of FactoryTrait { + #[inline] + fn new(service: Service, version: felt252, default_version: felt252,) -> Factory { + // [Check] Inputs + let factory_id: u8 = service.into(); + FactoryAssert::assert_valid_identifier(factory_id); + FactoryAssert::assert_valid_version(version); + FactoryAssert::assert_valid_version(default_version); + // [Return] Factory + Factory { id: factory_id, version: version, default_version: default_version, } + } +} + +#[generate_trait] +impl FactoryAssert of AssertTrait { + #[inline] + fn assert_does_not_exist(self: @Factory) { + assert(self.version == @0, errors::SERVICE_ALREADY_EXISTS); + } + + #[inline] + fn assert_does_exist(self: @Factory) { + assert(self.version != @0, errors::SERVICE_NOT_EXIST); + } + + #[inline] + fn assert_valid_identifier(identifier: u8) { + assert(identifier != 0, errors::SERVICE_INVALID_IDENTIFIER); + } + + #[inline] + fn assert_valid_version(version: felt252) { + assert(version != 0, errors::SERVICE_INVALID_VERSION); + } +} + +#[cfg(test)] +mod tests { + // Local imports + + use super::{Factory, FactoryTrait, FactoryAssert, Service}; + + // Constants + + const SERVICE: Service = Service::Katana; + const VERSION: felt252 = 'VERSION'; + const DEFAULT_VERSION: felt252 = 'DEFAULT'; + + #[test] + fn test_service_new() { + let service = FactoryTrait::new(SERVICE, VERSION, DEFAULT_VERSION); + assert_eq!(service.id, SERVICE.into()); + assert_eq!(service.version, VERSION); + assert_eq!(service.default_version, DEFAULT_VERSION); + } + + #[test] + fn test_service_assert_does_exist() { + let service = FactoryTrait::new(SERVICE, VERSION, DEFAULT_VERSION); + service.assert_does_exist(); + } + + #[test] + #[should_panic(expected: 'Factory: already exists')] + fn test_service_revert_already_exists() { + let service = FactoryTrait::new(SERVICE, VERSION, DEFAULT_VERSION); + service.assert_does_not_exist(); + } +} diff --git a/packages/provider/src/models/index.cairo b/packages/provider/src/models/index.cairo new file mode 100644 index 0000000..f44d853 --- /dev/null +++ b/packages/provider/src/models/index.cairo @@ -0,0 +1,23 @@ +//! Models + +#[derive(Clone, Drop, Serde)] +#[dojo::model] +pub struct Deployment { + #[key] + service: u8, + #[key] + project: felt252, + owner: felt252, + status: u8, + tier: u8, + config: ByteArray, +} + +#[derive(Copy, Drop, Serde)] +#[dojo::model] +pub struct Factory { + #[key] + id: u8, + version: felt252, + default_version: felt252, +} diff --git a/packages/provider/src/store.cairo b/packages/provider/src/store.cairo new file mode 100644 index 0000000..0d53989 --- /dev/null +++ b/packages/provider/src/store.cairo @@ -0,0 +1,57 @@ +//! Store struct and component management methods. + +// Starknet imports + +use starknet::SyscallResultTrait; + +// Dojo imports + +use dojo::world::WorldStorage; +use dojo::model::ModelStorage; + +// Models imports + +use provider::models::deployment::Deployment; +use provider::models::factory::Factory; + +// Structs + +#[derive(Copy, Drop)] +struct Store { + world: WorldStorage, +} + +// Implementations + +#[generate_trait] +impl StoreImpl of StoreTrait { + #[inline] + fn new(world: WorldStorage) -> Store { + Store { world: world } + } + + #[inline] + fn get_deployment(self: Store, service: u8, project: felt252) -> Deployment { + self.world.read_model((service, project)) + } + + #[inline] + fn get_factory(self: Store, factory_id: u8) -> Factory { + self.world.read_model(factory_id) + } + + #[inline] + fn set_deployment(ref self: Store, deployment: @Deployment) { + self.world.write_model(deployment); + } + + #[inline] + fn set_factory(ref self: Store, factory: @Factory) { + self.world.write_model(factory); + } + + #[inline] + fn delete_deployment(ref self: Store, deployment: @Deployment) { + self.world.erase_model(deployment); + } +} diff --git a/packages/provider/src/types/service.cairo b/packages/provider/src/types/service.cairo new file mode 100644 index 0000000..4840d1d --- /dev/null +++ b/packages/provider/src/types/service.cairo @@ -0,0 +1,54 @@ +// Internal imports + +use provider::elements::services; + +// Constants + +pub const SERVICE_COUNT: u8 = 3; + +#[derive(Copy, Drop, PartialEq)] +pub enum Service { + None, + Katana, + Torii, + Saya, +} + +// Implementations + +#[generate_trait] +impl ServiceImpl of ServiceTrait { + fn version(self: Service) -> felt252 { + match self { + Service::None => 0, + Service::Katana => services::katana::Katana::version(), + Service::Torii => services::torii::Torii::version(), + Service::Saya => services::saya::Saya::version(), + } + } +} + +impl IntoServiceU8 of core::Into { + #[inline] + fn into(self: Service) -> u8 { + match self { + Service::None => 0, + Service::Katana => 1, + Service::Torii => 2, + Service::Saya => 3, + } + } +} + +impl IntoU8Service of core::Into { + #[inline] + fn into(self: u8) -> Service { + match self { + 0 => Service::None, + 1 => Service::Katana, + 2 => Service::Torii, + 3 => Service::Saya, + _ => Service::None, + } + } +} diff --git a/packages/provider/src/types/status.cairo b/packages/provider/src/types/status.cairo new file mode 100644 index 0000000..10bc447 --- /dev/null +++ b/packages/provider/src/types/status.cairo @@ -0,0 +1,31 @@ +#[derive(Copy, Drop, PartialEq)] +pub enum Status { + None, + Active, + Disabled, +} + +// Implementations + +impl IntoStatusU8 of core::Into { + #[inline] + fn into(self: Status) -> u8 { + match self { + Status::None => 0, + Status::Active => 1, + Status::Disabled => 2, + } + } +} + +impl IntoU8Status of core::Into { + #[inline] + fn into(self: u8) -> Status { + match self { + 0 => Status::None, + 1 => Status::Active, + 2 => Status::Disabled, + _ => Status::None, + } + } +} diff --git a/packages/provider/src/types/tier.cairo b/packages/provider/src/types/tier.cairo new file mode 100644 index 0000000..6233602 --- /dev/null +++ b/packages/provider/src/types/tier.cairo @@ -0,0 +1,46 @@ +#[derive(Copy, Drop, PartialEq)] +pub enum Tier { + None, + Basic, + Common, + Uncommon, + Rare, + Epic, + Legendary, + Insane, +} + +// Implementations + +impl IntoTierU8 of core::Into { + #[inline] + fn into(self: Tier) -> u8 { + match self { + Tier::None => 0, + Tier::Basic => 1, + Tier::Common => 2, + Tier::Uncommon => 3, + Tier::Rare => 4, + Tier::Epic => 5, + Tier::Legendary => 6, + Tier::Insane => 7, + } + } +} + +impl IntoU8Tier of core::Into { + #[inline] + fn into(self: u8) -> Tier { + match self { + 0 => Tier::None, + 1 => Tier::Basic, + 2 => Tier::Common, + 3 => Tier::Uncommon, + 4 => Tier::Rare, + 5 => Tier::Epic, + 6 => Tier::Legendary, + 7 => Tier::Insane, + _ => Tier::None, + } + } +} diff --git a/packages/registry/Scarb.toml b/packages/registry/Scarb.toml index 5817c43..6a51cc9 100644 --- a/packages/registry/Scarb.toml +++ b/packages/registry/Scarb.toml @@ -1,5 +1,5 @@ [package] -name = "arcade_registry" +name = "registry" version.workspace = true [dependencies] diff --git a/packages/registry/src/components/controllable.cairo b/packages/registry/src/components/controllable.cairo deleted file mode 100644 index 5f5b639..0000000 --- a/packages/registry/src/components/controllable.cairo +++ /dev/null @@ -1,52 +0,0 @@ -#[starknet::component] -mod ControllableComponent { - // Starknet imports - - use starknet::info::get_caller_address; - - // Dojo imports - - use dojo::world::WorldStorage; - - // Internal imports - - use arcade_registry::store::{Store, StoreTrait}; - use arcade_registry::models::game::Game; - - // Storage - - #[storage] - struct Storage {} - - // Events - - #[event] - #[derive(Drop, starknet::Event)] - enum Event {} - - // Errors - - mod errors { - const CONTROLLABLE_UNAUTHORIZED_CALLER: felt252 = 'Controllable: unauthorized call'; - } - - #[generate_trait] - impl InternalImpl< - TContractState, +HasComponent - > of InternalTrait { - fn assert_is_owner( - self: @ComponentState, - world: WorldStorage, - world_address: felt252, - namespace: felt252 - ) { - // [Setup] Datastore - let store: Store = StoreTrait::new(world); - - // [Return] Game owner - let game = store.get_game(world_address, namespace); - let caller = get_caller_address(); - assert(game.owner == caller.into(), errors::CONTROLLABLE_UNAUTHORIZED_CALLER); - } - } -} diff --git a/packages/registry/src/components/initializable.cairo b/packages/registry/src/components/initializable.cairo new file mode 100644 index 0000000..f12f123 --- /dev/null +++ b/packages/registry/src/components/initializable.cairo @@ -0,0 +1,35 @@ +#[starknet::component] +mod InitializableComponent { + // Dojo imports + + use dojo::world::WorldStorage; + + // Internal imports + + use registry::store::{Store, StoreTrait}; + use registry::models::access::AccessTrait; + use registry::types::role::Role; + + // Storage + + #[storage] + struct Storage {} + + // Events + + #[event] + #[derive(Drop, starknet::Event)] + enum Event {} + + #[generate_trait] + impl InternalImpl< + TContractState, +HasComponent + > of InternalTrait { + fn initialize(self: @ComponentState, world: WorldStorage, owner: felt252) { + // [Effect] Initialize component + let mut store = StoreTrait::new(world); + let access = AccessTrait::new(owner, Role::Owner); + store.set_access(@access); + } + } +} diff --git a/packages/registry/src/components/registerable.cairo b/packages/registry/src/components/registerable.cairo new file mode 100644 index 0000000..56ed296 --- /dev/null +++ b/packages/registry/src/components/registerable.cairo @@ -0,0 +1,216 @@ +#[starknet::component] +mod RegisterableComponent { + // Dojo imports + + use dojo::world::WorldStorage; + + // Internal imports + + use registry::store::{Store, StoreTrait}; + use registry::models::access::{Access, AccessTrait, AccessAssert}; + use registry::models::game::{Game, GameTrait, GameAssert}; + use registry::types::metadata::{Metadata, MetadataTrait}; + use registry::types::socials::{Socials, SocialsTrait}; + use registry::types::role::Role; + + // Storage + + #[storage] + struct Storage {} + + // Events + + #[event] + #[derive(Drop, starknet::Event)] + enum Event {} + + #[generate_trait] + impl InternalImpl< + TContractState, +HasComponent + > of InternalTrait { + fn register( + self: @ComponentState, + world: WorldStorage, + world_address: felt252, + namespace: felt252, + project: felt252, + color: Option, + name: Option, + description: Option, + image: Option, + banner: Option, + discord: Option, + telegram: Option, + twitter: Option, + youtube: Option, + website: Option, + ) { + // [Setup] Datastore + let mut store: Store = StoreTrait::new(world); + + // [Check] Game does not exist + let game = store.get_game(world_address, namespace); + game.assert_does_not_exist(); + + // [Effect] Create game + let metadata = MetadataTrait::new(color, name, description, image, banner); + let socials = SocialsTrait::new(discord, telegram, twitter, youtube, website); + let owner: felt252 = starknet::get_caller_address().into(); + let game = GameTrait::new(world_address, namespace, project, metadata, socials, owner); + + // [Effect] Store game + store.set_game(@game); + } + + fn update( + self: @ComponentState, + world: WorldStorage, + world_address: felt252, + namespace: felt252, + color: Option, + name: Option, + description: Option, + image: Option, + banner: Option, + discord: Option, + telegram: Option, + twitter: Option, + youtube: Option, + website: Option, + ) { + // [Setup] Datastore + let mut store: Store = StoreTrait::new(world); + + // [Check] Game exists + let mut game = store.get_game(world_address, namespace); + game.assert_does_exist(); + + // [Check] Caller is owner + game.assert_is_owner(starknet::get_caller_address().into()); + + // [Effect] Update game + let metadata = MetadataTrait::new(color, name, description, image, banner); + let socials = SocialsTrait::new(discord, telegram, twitter, youtube, website); + game.update(metadata, socials); + + // [Effect] Update game + store.set_game(@game); + } + + fn publish( + self: @ComponentState, + world: WorldStorage, + world_address: felt252, + namespace: felt252, + ) { + // [Setup] Datastore + let mut store: Store = StoreTrait::new(world); + + // [Check] Game exists + let mut game = store.get_game(world_address, namespace); + game.assert_does_exist(); + + // [Check] Caller is owner + game.assert_is_owner(starknet::get_caller_address().into()); + + // [Effect] Publish game + game.publish(); + + // [Effect] Store game + store.set_game(@game); + } + + fn hide( + self: @ComponentState, + world: WorldStorage, + world_address: felt252, + namespace: felt252, + ) { + // [Setup] Datastore + let mut store: Store = StoreTrait::new(world); + + // [Check] Game exists + let mut game = store.get_game(world_address, namespace); + game.assert_does_exist(); + + // [Check] Caller is owner + game.assert_is_owner(starknet::get_caller_address().into()); + + // [Effect] Hide game + game.hide(); + + // [Effect] Store game + store.set_game(@game); + } + + fn whitelist( + self: @ComponentState, + world: WorldStorage, + world_address: felt252, + namespace: felt252, + ) { + // [Setup] Datastore + let mut store: Store = StoreTrait::new(world); + + // [Check] Caller is allowed + let caller = starknet::get_caller_address().into(); + let access = store.get_access(caller); + access.assert_is_allowed(Role::Admin); + + // [Check] Game exists + let mut game = store.get_game(world_address, namespace); + game.assert_does_exist(); + + // [Effect] Whitelist game + game.whitelist(); + + // [Effect] Store game + store.set_game(@game); + } + + fn blacklist( + self: @ComponentState, + world: WorldStorage, + world_address: felt252, + namespace: felt252, + ) { + // [Setup] Datastore + let mut store: Store = StoreTrait::new(world); + + // [Check] Caller is allowed + let caller = starknet::get_caller_address().into(); + let access = store.get_access(caller); + access.assert_is_allowed(Role::Admin); + + // [Check] Game exists + let mut game = store.get_game(world_address, namespace); + game.assert_does_exist(); + + // [Effect] Blacklist game + game.blacklist(); + + // [Effect] Store game + store.set_game(@game); + } + + fn remove( + self: @ComponentState, + world: WorldStorage, + world_address: felt252, + namespace: felt252, + ) { + // [Setup] Datastore + let mut store: Store = StoreTrait::new(world); + + // [Check] Game exists + let mut game = store.get_game(world_address, namespace); + game.assert_does_exist(); + + // [Effect] Remove game + game.nullify(); + + // [Effect] Store game + store.delete_game(@game); + } + } +} diff --git a/packages/registry/src/components/registrable.cairo b/packages/registry/src/components/trackable.cairo similarity index 52% rename from packages/registry/src/components/registrable.cairo rename to packages/registry/src/components/trackable.cairo index c8fe29e..3bb1ddd 100644 --- a/packages/registry/src/components/registrable.cairo +++ b/packages/registry/src/components/trackable.cairo @@ -1,14 +1,16 @@ #[starknet::component] -mod RegistrableComponent { +mod TrackableComponent { // Dojo imports use dojo::world::WorldStorage; // Internal imports - use arcade_registry::store::{Store, StoreTrait}; - use arcade_registry::models::game::{Game, GameTrait, GameAssert}; - use arcade_registry::models::achievement::{Achievement, AchievementTrait, AchievementAssert}; + use registry::store::{Store, StoreTrait}; + use registry::models::access::{Access, AccessTrait, AccessAssert}; + use registry::models::achievement::{Achievement, AchievementTrait, AchievementAssert}; + use registry::models::game::{Game, GameTrait, GameAssert}; + use registry::types::role::Role; // Storage @@ -25,158 +27,7 @@ mod RegistrableComponent { impl InternalImpl< TContractState, +HasComponent > of InternalTrait { - fn register_game( - self: @ComponentState, - world: WorldStorage, - world_address: felt252, - namespace: felt252, - name: ByteArray, - description: ByteArray, - torii_url: ByteArray, - image_uri: ByteArray, - owner: felt252, - ) { - // [Setup] Datastore - let mut store: Store = StoreTrait::new(world); - - // [Check] Game does not exist - let game = store.get_game(world_address, namespace); - game.assert_does_not_exist(); - - // [Effect] Create game - let game = GameTrait::new( - world_address, namespace, name, description, torii_url, image_uri, owner - ); - - // [Effect] Store game - store.set_game(game); - } - - fn update_game( - self: @ComponentState, - world: WorldStorage, - world_address: felt252, - namespace: felt252, - name: ByteArray, - description: ByteArray, - torii_url: ByteArray, - image_uri: ByteArray, - ) { - // [Setup] Datastore - let mut store: Store = StoreTrait::new(world); - - // [Check] Game exists - let mut game = store.get_game(world_address, namespace); - game.assert_does_exist(); - - // [Effect] Update game - game.update(name, description, torii_url, image_uri); - - // [Effect] Update game - store.set_game(game); - } - - fn publish_game( - self: @ComponentState, - world: WorldStorage, - world_address: felt252, - namespace: felt252, - ) { - // [Setup] Datastore - let mut store: Store = StoreTrait::new(world); - - // [Check] Game exists - let mut game = store.get_game(world_address, namespace); - game.assert_does_exist(); - - // [Effect] Publish game - game.publish(); - - // [Effect] Store game - store.set_game(game); - } - - fn hide_game( - self: @ComponentState, - world: WorldStorage, - world_address: felt252, - namespace: felt252, - ) { - // [Setup] Datastore - let mut store: Store = StoreTrait::new(world); - - // [Check] Game exists - let mut game = store.get_game(world_address, namespace); - game.assert_does_exist(); - - // [Effect] Hide game - game.hide(); - - // [Effect] Store game - store.set_game(game); - } - - fn whitelist_game( - self: @ComponentState, - world: WorldStorage, - world_address: felt252, - namespace: felt252, - ) { - // [Setup] Datastore - let mut store: Store = StoreTrait::new(world); - - // [Check] Game exists - let mut game = store.get_game(world_address, namespace); - game.assert_does_exist(); - - // [Effect] Whitelist game - game.whitelist(); - - // [Effect] Store game - store.set_game(game); - } - - fn blacklist_game( - self: @ComponentState, - world: WorldStorage, - world_address: felt252, - namespace: felt252, - ) { - // [Setup] Datastore - let mut store: Store = StoreTrait::new(world); - - // [Check] Game exists - let mut game = store.get_game(world_address, namespace); - game.assert_does_exist(); - - // [Effect] Blacklist game - game.blacklist(); - - // [Effect] Store game - store.set_game(game); - } - - fn remove_game( - self: @ComponentState, - world: WorldStorage, - world_address: felt252, - namespace: felt252, - ) { - // [Setup] Datastore - let mut store: Store = StoreTrait::new(world); - - // [Check] Game exists - let mut game = store.get_game(world_address, namespace); - game.assert_does_exist(); - - // [Effect] Remove game - game.nullify(); - - // [Effect] Store game - store.set_game(game); - } - - fn register_achievement( + fn register( self: @ComponentState, world: WorldStorage, world_address: felt252, @@ -191,6 +42,9 @@ mod RegistrableComponent { let mut game = store.get_game(world_address, namespace); game.assert_does_exist(); + // [Check] Caller is owner + game.assert_is_owner(starknet::get_caller_address().into()); + // [Check] Achievement does not exist let achievement = store.get_achievement(world_address, namespace, identifier); achievement.assert_does_not_exist(); @@ -202,11 +56,11 @@ mod RegistrableComponent { game.add(achievement.karma); // [Effect] Store entities - store.set_achievement(achievement); - store.set_game(game); + store.set_achievement(@achievement); + store.set_game(@game); } - fn update_achievement( + fn update( self: @ComponentState, world: WorldStorage, world_address: felt252, @@ -221,6 +75,9 @@ mod RegistrableComponent { let mut game = store.get_game(world_address, namespace); game.assert_does_exist(); + // [Check] Caller is owner + game.assert_is_owner(starknet::get_caller_address().into()); + // [Check] Achievement exists let mut achievement = store.get_achievement(world_address, namespace, identifier); achievement.assert_does_exist(); @@ -231,11 +88,11 @@ mod RegistrableComponent { game.add(achievement.karma); // [Effect] Update entities - store.set_achievement(achievement); - store.set_game(game); + store.set_achievement(@achievement); + store.set_game(@game); } - fn publish_achievement( + fn publish( self: @ComponentState, world: WorldStorage, world_address: felt252, @@ -249,6 +106,9 @@ mod RegistrableComponent { let mut game = store.get_game(world_address, namespace); game.assert_does_exist(); + // [Check] Caller is owner + game.assert_is_owner(starknet::get_caller_address().into()); + // [Check] Achievement exists let mut achievement = store.get_achievement(world_address, namespace, identifier); achievement.assert_does_exist(); @@ -257,10 +117,10 @@ mod RegistrableComponent { achievement.publish(); // [Effect] Store achievement - store.set_achievement(achievement); + store.set_achievement(@achievement); } - fn hide_achievement( + fn hide( self: @ComponentState, world: WorldStorage, world_address: felt252, @@ -274,6 +134,9 @@ mod RegistrableComponent { let mut game = store.get_game(world_address, namespace); game.assert_does_exist(); + // [Check] Caller is owner + game.assert_is_owner(starknet::get_caller_address().into()); + // [Check] Achievement exists let mut achievement = store.get_achievement(world_address, namespace, identifier); achievement.assert_does_exist(); @@ -282,10 +145,10 @@ mod RegistrableComponent { achievement.hide(); // [Effect] Store achievement - store.set_achievement(achievement); + store.set_achievement(@achievement); } - fn whitelist_achievement( + fn whitelist( self: @ComponentState, world: WorldStorage, world_address: felt252, @@ -295,6 +158,11 @@ mod RegistrableComponent { // [Setup] Datastore let mut store: Store = StoreTrait::new(world); + // [Check] Caller is allowed + let caller = starknet::get_caller_address().into(); + let access = store.get_access(caller); + access.assert_is_allowed(Role::Admin); + // [Check] Game exists let mut game = store.get_game(world_address, namespace); game.assert_does_exist(); @@ -307,10 +175,10 @@ mod RegistrableComponent { achievement.whitelist(); // [Effect] Store achievement - store.set_achievement(achievement); + store.set_achievement(@achievement); } - fn blacklist_achievement( + fn blacklist( self: @ComponentState, world: WorldStorage, world_address: felt252, @@ -320,6 +188,11 @@ mod RegistrableComponent { // [Setup] Datastore let mut store: Store = StoreTrait::new(world); + // [Check] Caller is allowed + let caller = starknet::get_caller_address().into(); + let access = store.get_access(caller); + access.assert_is_allowed(Role::Admin); + // [Check] Game exists let mut game = store.get_game(world_address, namespace); game.assert_does_exist(); @@ -332,10 +205,10 @@ mod RegistrableComponent { achievement.blacklist(); // [Effect] Store achievement - store.set_achievement(achievement); + store.set_achievement(@achievement); } - fn remove_achievement( + fn remove( self: @ComponentState, world: WorldStorage, world_address: felt252, @@ -358,8 +231,8 @@ mod RegistrableComponent { achievement.nullify(); // [Effect] Store entities - store.set_achievement(achievement); - store.set_game(game); + store.delete_achievement(@achievement); + store.set_game(@game); } } } diff --git a/packages/registry/src/helpers/json.cairo b/packages/registry/src/helpers/json.cairo new file mode 100644 index 0000000..3d4c10c --- /dev/null +++ b/packages/registry/src/helpers/json.cairo @@ -0,0 +1,175 @@ +//! JSON helper functions + +pub trait JsonifiableTrait { + fn jsonify(self: T) -> ByteArray; +} + +pub impl Jsonifiable, +core::fmt::Display> of JsonifiableTrait { + fn jsonify(self: T) -> ByteArray { + format!("{}", self) + } +} + +#[generate_trait] +pub impl JsonifiableSimple of JsonifiableSimpleTrait { + fn jsonify(name: ByteArray, value: ByteArray) -> ByteArray { + format!("\"{}\":{}", name, value) + } +} + +#[generate_trait] +pub impl JsonifiableString of JsonifiableStringTrait { + fn jsonify(name: ByteArray, value: ByteArray) -> ByteArray { + format!("\"{}\":\"{}\"", name, value) + } +} + +#[generate_trait] +pub impl JsonifiableArray, +Drop> of JsonifiableArrayTrait { + fn jsonify(name: ByteArray, mut value: Array) -> ByteArray { + let mut string = "["; + let mut index: u32 = 0; + while let Option::Some(item) = value.pop_front() { + if index > 0 { + string += ","; + } + string += item.jsonify(); + index += 1; + }; + JsonifiableSimple::jsonify(name, string + "]") + } +} + +#[cfg(test)] +mod tests { + // Local imports + + use super::{ + Jsonifiable, JsonifiableSimple, JsonifiableString, JsonifiableArray, JsonifiableTrait + }; + + #[derive(Drop)] + struct BooleanObject { + value: bool, + } + + #[derive(Drop)] + struct IntegerObject { + value: u8, + } + + #[derive(Drop)] + struct FeltObject { + value: felt252, + } + + #[derive(Drop)] + struct ByteArrayObject { + value: ByteArray, + } + + #[derive(Drop)] + struct Complex { + boolean: bool, + integer: u8, + felt: felt252, + byte_array: ByteArray, + array: Array, + object_array: Array, + object: IntegerObject, + } + + pub impl IntegerObjectJsonifiable of JsonifiableTrait { + fn jsonify(self: IntegerObject) -> ByteArray { + let mut string = "{"; + string += JsonifiableSimple::jsonify("value", format!("{}", self.value)); + string + "}" + } + } + + pub impl BooleanObjectJsonifiable of JsonifiableTrait { + fn jsonify(self: BooleanObject) -> ByteArray { + let mut string = "{"; + string += JsonifiableSimple::jsonify("value", format!("{}", self.value)); + string + "}" + } + } + + pub impl FeltObjectJsonifiable of JsonifiableTrait { + fn jsonify(self: FeltObject) -> ByteArray { + let mut string = "{"; + string += JsonifiableSimple::jsonify("value", format!("{}", self.value)); + string + "}" + } + } + + pub impl ByteArrayObjectJsonifiable of JsonifiableTrait { + fn jsonify(self: ByteArrayObject) -> ByteArray { + let mut string = "{"; + string += JsonifiableString::jsonify("value", format!("{}", self.value)); + string + "}" + } + } + + pub impl ComplexJsonifiable of JsonifiableTrait { + fn jsonify(self: Complex) -> ByteArray { + let mut string = "{"; + string += JsonifiableSimple::jsonify("boolean", format!("{}", self.boolean)); + string += "," + JsonifiableSimple::jsonify("integer", format!("{}", self.integer)); + string += "," + JsonifiableSimple::jsonify("felt", format!("{}", self.felt)); + string += "," + + JsonifiableString::jsonify("byte_array", format!("{}", self.byte_array)); + string += "," + JsonifiableArray::jsonify("array", self.array); + string += "," + JsonifiableArray::jsonify("object_array", self.object_array); + string += "," + JsonifiableSimple::jsonify("object", self.object.jsonify()); + string + "}" + } + } + + #[test] + fn test_jsonify_integer_object() { + let integer_object = IntegerObject { value: 1 }; + let json = integer_object.jsonify(); + assert_eq!(json, "{\"value\":1}"); + } + + #[test] + fn test_jsonify_boolean_object() { + let boolean_object = BooleanObject { value: true }; + let json = boolean_object.jsonify(); + assert_eq!(json, "{\"value\":true}"); + } + + #[test] + fn test_jsonify_felt_object() { + let felt_object = FeltObject { value: '1' }; + let json = felt_object.jsonify(); + assert_eq!(json, "{\"value\":49}"); + } + + #[test] + fn test_jsonify_byte_array_object() { + let byte_array_object = ByteArrayObject { value: "test" }; + let json = byte_array_object.jsonify(); + assert_eq!(json, "{\"value\":\"test\"}"); + } + + #[test] + fn test_jsonify_complex() { + let complex = Complex { + boolean: true, + integer: 1, + felt: '1', + byte_array: "test", + array: array![1, 2, 3], + object_array: array![IntegerObject { value: 1 }, IntegerObject { value: 2 }], + object: IntegerObject { value: 1 }, + }; + let json = complex.jsonify(); + assert_eq!( + json, + "{\"boolean\":true,\"integer\":1,\"felt\":49,\"byte_array\":\"test\",\"array\":[1,2,3],\"object_array\":[{\"value\":1},{\"value\":2}],\"object\":{\"value\":1}}" + ); + } +} + diff --git a/packages/registry/src/lib.cairo b/packages/registry/src/lib.cairo index c79fc9f..81df536 100644 --- a/packages/registry/src/lib.cairo +++ b/packages/registry/src/lib.cairo @@ -1,25 +1,37 @@ mod constants; mod store; +mod helpers { + mod json; +} + +mod types { + mod role; + mod metadata; + mod socials; +} + mod models { mod index; - mod game; + mod access; mod achievement; + mod game; } mod components { - mod controllable; - mod registrable; + mod initializable; + mod registerable; + mod trackable; } #[cfg(test)] mod tests { mod setup; - mod test_controllable; - mod test_registrable; + mod test_registerable; + mod test_trackable; mod mocks { - mod controller; - mod registrer; + mod register; + mod tracker; } } diff --git a/packages/registry/src/models/access.cairo b/packages/registry/src/models/access.cairo new file mode 100644 index 0000000..fb4c5ac --- /dev/null +++ b/packages/registry/src/models/access.cairo @@ -0,0 +1,124 @@ +// Intenral imports + +use registry::constants; +use registry::models::index::Access; +use registry::types::role::Role; + +// Errors + +pub mod errors { + pub const ACCESS_INVALID_ADDRESS: felt252 = 'Access: invalid address'; + pub const ACCESS_INVALID_ROLE: felt252 = 'Access: invalid role'; + pub const ACCESS_NOT_ALLOWED: felt252 = 'Access: not allowed'; + pub const ACCESS_NOT_GRANTABLE: felt252 = 'Access: not grantable'; + pub const ACCESS_NOT_REVOKABLE: felt252 = 'Access: not revokable'; +} + +#[generate_trait] +impl AccessImpl of AccessTrait { + #[inline] + fn new(address: felt252, role: Role) -> Access { + // [Check] Inputs + AccessAssert::assert_valid_address(address); + AccessAssert::assert_valid_role(role); + // [Return] Access + Access { address, role: role.into() } + } + + #[inline] + fn grant(ref self: Access, role: Role) { + // [Check] Address + AccessAssert::assert_valid_address(self.address); + // [Check] Role + AccessAssert::assert_valid_role(role); + // [Check] Grantability + self.assert_is_grantable(role); + // [Update] Role + self.role = role.into(); + } + + #[inline] + fn revoke(ref self: Access) { + // [Check] Address + AccessAssert::assert_valid_address(self.address); + // [Check] Revokability + let role: Role = Role::None; + self.assert_is_revokable(role); + // [Update] Role + self.role = role.into(); + } +} + +#[generate_trait] +impl AccessAssert of AssertTrait { + #[inline] + fn assert_valid_address(address: felt252) { + assert(address != 0, errors::ACCESS_INVALID_ADDRESS); + } + + #[inline] + fn assert_valid_role(role: Role) { + assert(role != Role::None, errors::ACCESS_INVALID_ROLE); + } + + #[inline] + fn assert_is_grantable(self: @Access, role: Role) { + assert(self.role < @role.into(), errors::ACCESS_NOT_GRANTABLE); + } + + #[inline] + fn assert_is_revokable(self: @Access, role: Role) { + assert(self.role > @role.into(), errors::ACCESS_NOT_REVOKABLE); + } + + #[inline] + fn assert_is_allowed(self: @Access, role: Role) { + assert(self.role >= @role.into(), errors::ACCESS_NOT_ALLOWED); + } +} + +#[cfg(test)] +mod tests { + // Local imports + + use super::{Access, AccessTrait, AccessAssert, Role}; + + // Constants + + const CALLER: felt252 = 'CALLER'; + + #[test] + fn test_access_grant() { + let mut access = Access { address: CALLER, role: Role::None.into() }; + access.grant(Role::Owner); + assert_eq!(access.role, Role::Owner.into()); + } + + #[test] + fn test_access_revoke() { + let mut access = Access { address: CALLER, role: Role::Owner.into() }; + access.revoke(); + assert_eq!(access.role, Role::None.into()); + } + + #[test] + #[should_panic(expected: ('Access: not allowed',))] + fn test_access_revert_not_allowed() { + let access = Access { address: CALLER, role: Role::None.into() }; + access.assert_is_allowed(Role::Owner); + } + + #[test] + #[should_panic(expected: ('Access: not grantable',))] + fn test_access_grant_revert_not_grantable() { + let mut access = Access { address: CALLER, role: Role::Owner.into() }; + access.assert_is_grantable(Role::Admin); + } + + #[test] + #[should_panic(expected: ('Access: not revokable',))] + fn test_access_revoke_revert_not_revokable() { + let mut access = Access { address: CALLER, role: Role::None.into() }; + access.assert_is_revokable(Role::None); + } +} diff --git a/packages/registry/src/models/achievement.cairo b/packages/registry/src/models/achievement.cairo index c413689..902c84d 100644 --- a/packages/registry/src/models/achievement.cairo +++ b/packages/registry/src/models/achievement.cairo @@ -1,7 +1,7 @@ // Intenral imports -use arcade_registry::models::index::Achievement; -use arcade_registry::constants; +use registry::models::index::Achievement; +use registry::constants; // Errors diff --git a/packages/registry/src/models/game.cairo b/packages/registry/src/models/game.cairo index fd00ac1..6e87282 100644 --- a/packages/registry/src/models/game.cairo +++ b/packages/registry/src/models/game.cairo @@ -1,20 +1,25 @@ // Intenral imports -use arcade_registry::models::index::Game; -use arcade_registry::constants; +use registry::constants; +use registry::models::index::Game; +use registry::types::metadata::Metadata; +use registry::types::socials::Socials; +use registry::helpers::json::JsonifiableTrait; // Errors pub mod errors { + pub const GAME_ALREADY_EXISTS: felt252 = 'Game: already exists'; + pub const GAME_NOT_EXIST: felt252 = 'Game: does not exist'; + pub const GAME_INVALID_PROJECT: felt252 = 'Game: invalid project'; + pub const GAME_INVALID_OWNER: felt252 = 'Game: invalid owner'; pub const GAME_INVALID_WORLD: felt252 = 'Game: invalid world'; pub const GAME_INVALID_NAMESPACE: felt252 = 'Game: invalid namespace'; pub const GAME_INVALID_NAME: felt252 = 'Game: invalid name'; - pub const GAME_INVALID_DESCRIPTION: felt252 = 'Game: invalid description'; - pub const GAME_INVALID_TORII_URL: felt252 = 'Game: invalid torii url'; + pub const GAME_INVALID_PRIORITY: felt252 = 'Game: invalid priority'; pub const GAME_INVALID_KARMA: felt252 = 'Game: cannot exceed 1000'; - pub const GAME_NOT_EXIST: felt252 = 'Game: does not exist'; - pub const GAME_ALREADY_EXISTS: felt252 = 'Game: already exists'; pub const GAME_NOT_WHITELISTABLE: felt252 = 'Game: not whitelistable'; + pub const GAME_NOT_OWNER: felt252 = 'Game: caller is not owner'; } #[generate_trait] @@ -23,40 +28,39 @@ impl GameImpl of GameTrait { fn new( world_address: felt252, namespace: felt252, - name: ByteArray, - description: ByteArray, - torii_url: ByteArray, - image_uri: ByteArray, + project: felt252, + metadata: Metadata, + socials: Socials, owner: felt252, ) -> Game { // [Check] Inputs + GameAssert::assert_valid_project(project); + GameAssert::assert_valid_owner(owner); GameAssert::assert_valid_world(world_address); GameAssert::assert_valid_namespace(namespace); - GameAssert::assert_valid_name(@name); - GameAssert::assert_valid_description(@description); - GameAssert::assert_valid_torii_url(@torii_url); // [Return] Game Game { - world_address, - namespace, + world_address: world_address, + namespace: namespace, + project: project, + active: true, published: false, whitelisted: false, - total_karma: 0, - name, - description, - torii_url, - image_uri, - owner, + karma: 0, + priority: 0, + socials: socials.jsonify(), + metadata: metadata.jsonify(), + owner: owner, } } #[inline] fn add(ref self: Game, karma: u16) { // [Check] Inputs - let total_karma = self.total_karma + karma; + let total_karma = self.karma + karma; GameAssert::assert_valid_karma(total_karma); // [Update] Points - self.total_karma = total_karma; + self.karma = total_karma; // [Effect] Reset visibility status self.published = false; self.whitelisted = false; @@ -64,29 +68,17 @@ impl GameImpl of GameTrait { #[inline] fn remove(ref self: Game, karma: u16) { - self.total_karma -= karma; + self.karma -= karma; // [Effect] Reset visibility status self.published = false; self.whitelisted = false; } #[inline] - fn update( - ref self: Game, - name: ByteArray, - description: ByteArray, - torii_url: ByteArray, - image_uri: ByteArray - ) { - // [Check] Inputs - GameAssert::assert_valid_name(@name); - GameAssert::assert_valid_description(@description); - GameAssert::assert_valid_torii_url(@torii_url); + fn update(ref self: Game, metadata: Metadata, socials: Socials) { // [Effect] Update Game - self.name = name; - self.description = description; - self.torii_url = torii_url; - self.image_uri = image_uri; + self.metadata = metadata.jsonify(); + self.socials = socials.jsonify(); // [Effect] Reset visibility status self.published = false; self.whitelisted = false; @@ -103,7 +95,6 @@ impl GameImpl of GameTrait { fn hide(ref self: Game) { // [Effect] Reset visibility status self.published = false; - self.whitelisted = false; } #[inline] @@ -117,7 +108,6 @@ impl GameImpl of GameTrait { #[inline] fn blacklist(ref self: Game) { // [Effect] Reset visibility status - self.published = false; self.whitelisted = false; } @@ -125,11 +115,7 @@ impl GameImpl of GameTrait { fn nullify(ref self: Game) { self.published = false; self.whitelisted = false; - self.total_karma = 0; - self.name = ""; - self.description = ""; - self.torii_url = ""; - self.image_uri = ""; + self.project = 0; } } @@ -137,37 +123,32 @@ impl GameImpl of GameTrait { impl GameAssert of AssertTrait { #[inline] fn assert_does_not_exist(self: @Game) { - assert(self.name == @"", errors::GAME_ALREADY_EXISTS); + assert(self.project == @0, errors::GAME_ALREADY_EXISTS); } #[inline] fn assert_does_exist(self: @Game) { - assert(self.name != @"", errors::GAME_NOT_EXIST); + assert(self.project != @0, errors::GAME_NOT_EXIST); } #[inline] - fn assert_valid_world(world: felt252) { - assert(world != 0, errors::GAME_INVALID_WORLD); + fn assert_valid_project(project: felt252) { + assert(project != 0, errors::GAME_INVALID_PROJECT); } #[inline] - fn assert_valid_namespace(namespace: felt252) { - assert(namespace != 0, errors::GAME_INVALID_NAMESPACE); + fn assert_valid_owner(owner: felt252) { + assert(owner != 0, errors::GAME_INVALID_OWNER); } #[inline] - fn assert_valid_name(name: @ByteArray) { - assert(name.len() > 0, errors::GAME_INVALID_NAME); - } - - #[inline] - fn assert_valid_description(description: @ByteArray) { - assert(description.len() > 0, errors::GAME_INVALID_DESCRIPTION); + fn assert_valid_world(world: felt252) { + assert(world != 0, errors::GAME_INVALID_WORLD); } #[inline] - fn assert_valid_torii_url(torii_url: @ByteArray) { - assert(torii_url.len() > 0, errors::GAME_INVALID_TORII_URL); + fn assert_valid_namespace(namespace: felt252) { + assert(namespace != 0, errors::GAME_INVALID_NAMESPACE); } #[inline] @@ -179,6 +160,11 @@ impl GameAssert of AssertTrait { fn assert_is_whitelistable(self: @Game) { assert(*self.published, errors::GAME_NOT_WHITELISTABLE); } + + #[inline] + fn assert_is_owner(self: @Game, caller: felt252) { + assert(@caller == self.owner, errors::GAME_NOT_OWNER); + } } #[cfg(test)] @@ -187,6 +173,11 @@ mod tests { use core::byte_array::{ByteArray, ByteArrayTrait}; + // Internal imports + + use registry::types::metadata::{Metadata, MetadataTrait, MetadataJsonifiable}; + use registry::types::socials::{Socials, SocialsTrait, SocialsJsonifiable}; + // Local imports use super::{Game, GameTrait, GameAssert}; @@ -195,77 +186,94 @@ mod tests { const WORLD_ADDRESS: felt252 = 'WORLD'; const NAMESPACE: felt252 = 'NAMESPACE'; + const PROJECT: felt252 = 'PROJECT'; const OWNER: felt252 = 'OWNER'; + #[test] fn test_game_new() { - let name = "NAME"; - let description = "DESCRIPTION"; - let torii_url = "TORII_URL"; - let image_uri = "IMAGE_URI"; + let metadata = core::Default::default(); + let socials = core::Default::default(); let game = GameTrait::new( - WORLD_ADDRESS, - NAMESPACE, - name.clone(), - description.clone(), - torii_url.clone(), - image_uri.clone(), - OWNER, + world_address: WORLD_ADDRESS, + namespace: NAMESPACE, + project: PROJECT, + metadata: metadata.clone(), + socials: socials.clone(), + owner: OWNER, ); assert_eq!(game.world_address, WORLD_ADDRESS); assert_eq!(game.namespace, NAMESPACE); - assert_eq!(game.name, name); - assert_eq!(game.description, description); - assert_eq!(game.torii_url, torii_url); - assert_eq!(game.image_uri, image_uri); + assert_eq!(game.project, PROJECT); + assert_eq!(game.active, true); + assert_eq!(game.published, false); + assert_eq!(game.whitelisted, false); + assert_eq!(game.karma, 0); + assert_eq!(game.priority, 0); + assert_eq!(game.socials, socials.clone().jsonify()); + assert_eq!(game.metadata, metadata.clone().jsonify()); assert_eq!(game.owner, OWNER); } #[test] fn test_game_add() { let mut game = GameTrait::new( - WORLD_ADDRESS, NAMESPACE, "NAME", "DESCRIPTION", "TORII_URL", "IMAGE_URI", OWNER, + world_address: WORLD_ADDRESS, + namespace: NAMESPACE, + project: PROJECT, + metadata: core::Default::default(), + socials: core::Default::default(), + owner: OWNER, ); game.add(100); - assert_eq!(game.total_karma, 100); + assert_eq!(game.karma, 100); } #[test] fn test_game_remove() { let mut game = GameTrait::new( - WORLD_ADDRESS, NAMESPACE, "NAME", "DESCRIPTION", "TORII_URL", "IMAGE_URI", OWNER, + world_address: WORLD_ADDRESS, + namespace: NAMESPACE, + project: PROJECT, + metadata: core::Default::default(), + socials: core::Default::default(), + owner: OWNER, ); game.add(100); - assert_eq!(game.total_karma, 100); + assert_eq!(game.karma, 100); game.remove(50); - assert_eq!(game.total_karma, 50); + assert_eq!(game.karma, 50); } #[test] fn test_game_update() { let mut game = GameTrait::new( - WORLD_ADDRESS, NAMESPACE, "NAME", "DESCRIPTION", "TORII_URL", "IMAGE_URI", OWNER, + world_address: WORLD_ADDRESS, + namespace: NAMESPACE, + project: PROJECT, + metadata: core::Default::default(), + socials: core::Default::default(), + owner: OWNER, + ); + let metadata = MetadataTrait::new( + Option::Some('123456'), Option::None, Option::None, Option::None, Option::None + ); + let socials = SocialsTrait::new( + Option::Some("discord"), Option::None, Option::None, Option::None, Option::None ); - let new_name = "NEW_NAME"; - let new_description = "NEW_DESCRIPTION"; - let new_torii_url = "NEW_TORII_URL"; - let new_image_uri = "NEW_IMAGE_URI"; - game - .update( - new_name.clone(), - new_description.clone(), - new_torii_url.clone(), - new_image_uri.clone() - ); - assert_eq!(game.name, new_name); - assert_eq!(game.description, new_description); - assert_eq!(game.torii_url, new_torii_url); - assert_eq!(game.image_uri, new_image_uri); + game.update(metadata.clone(), socials.clone()); + assert_eq!(game.metadata, metadata.clone().jsonify()); + assert_eq!(game.socials, socials.clone().jsonify()); } #[test] fn test_game_publish() { let mut game = GameTrait::new( - WORLD_ADDRESS, NAMESPACE, "NAME", "DESCRIPTION", "TORII_URL", "IMAGE_URI", OWNER, + world_address: WORLD_ADDRESS, + namespace: NAMESPACE, + project: PROJECT, + metadata: core::Default::default(), + socials: core::Default::default(), + owner: OWNER, ); game.publish(); assert_eq!(game.published, true); @@ -274,7 +282,12 @@ mod tests { #[test] fn test_game_hide() { let mut game = GameTrait::new( - WORLD_ADDRESS, NAMESPACE, "NAME", "DESCRIPTION", "TORII_URL", "IMAGE_URI", OWNER, + world_address: WORLD_ADDRESS, + namespace: NAMESPACE, + project: PROJECT, + metadata: core::Default::default(), + socials: core::Default::default(), + owner: OWNER, ); game.publish(); game.hide(); @@ -284,7 +297,12 @@ mod tests { #[test] fn test_game_whitelist() { let mut game = GameTrait::new( - WORLD_ADDRESS, NAMESPACE, "NAME", "DESCRIPTION", "TORII_URL", "IMAGE_URI", OWNER, + world_address: WORLD_ADDRESS, + namespace: NAMESPACE, + project: PROJECT, + metadata: core::Default::default(), + socials: core::Default::default(), + owner: OWNER, ); game.publish(); game.whitelist(); @@ -294,7 +312,12 @@ mod tests { #[test] fn test_game_blacklist() { let mut game = GameTrait::new( - WORLD_ADDRESS, NAMESPACE, "NAME", "DESCRIPTION", "TORII_URL", "IMAGE_URI", OWNER, + world_address: WORLD_ADDRESS, + namespace: NAMESPACE, + project: PROJECT, + metadata: core::Default::default(), + socials: core::Default::default(), + owner: OWNER, ); game.publish(); game.whitelist(); @@ -305,22 +328,29 @@ mod tests { #[test] fn test_game_nullify() { let mut game = GameTrait::new( - WORLD_ADDRESS, NAMESPACE, "NAME", "DESCRIPTION", "TORII_URL", "IMAGE_URI", OWNER, + world_address: WORLD_ADDRESS, + namespace: NAMESPACE, + project: PROJECT, + metadata: core::Default::default(), + socials: core::Default::default(), + owner: OWNER, ); game.nullify(); - assert_eq!(game.name, ""); - assert_eq!(game.description, ""); - assert_eq!(game.torii_url, ""); - assert_eq!(game.image_uri, ""); - assert_eq!(game.total_karma, 0); + assert_eq!(game.project, 0); assert_eq!(game.whitelisted, false); + assert_eq!(game.published, false); } #[test] #[should_panic(expected: 'Game: already exists')] fn test_game_assert_does_not_exist() { let mut game = GameTrait::new( - WORLD_ADDRESS, NAMESPACE, "NAME", "DESCRIPTION", "TORII_URL", "IMAGE_URI", OWNER, + world_address: WORLD_ADDRESS, + namespace: NAMESPACE, + project: PROJECT, + metadata: core::Default::default(), + socials: core::Default::default(), + owner: OWNER, ); game.assert_does_not_exist(); } @@ -329,9 +359,14 @@ mod tests { #[should_panic(expected: 'Game: does not exist')] fn test_game_assert_does_exist() { let mut game = GameTrait::new( - WORLD_ADDRESS, NAMESPACE, "NAME", "DESCRIPTION", "TORII_URL", "IMAGE_URI", OWNER, + world_address: WORLD_ADDRESS, + namespace: NAMESPACE, + project: PROJECT, + metadata: core::Default::default(), + socials: core::Default::default(), + owner: OWNER, ); - game.name = ""; + game.project = 0; game.assert_does_exist(); } @@ -348,21 +383,15 @@ mod tests { } #[test] - #[should_panic(expected: 'Game: invalid name')] - fn test_game_assert_valid_name_empty() { - GameAssert::assert_valid_name(@""); + #[should_panic(expected: 'Game: invalid project')] + fn test_game_assert_valid_project_zero() { + GameAssert::assert_valid_project(0); } #[test] - #[should_panic(expected: 'Game: invalid description')] - fn test_game_assert_valid_description_empty() { - GameAssert::assert_valid_description(@""); - } - - #[test] - #[should_panic(expected: 'Game: invalid torii url')] - fn test_game_assert_valid_torii_url_empty() { - GameAssert::assert_valid_torii_url(@""); + #[should_panic(expected: 'Game: invalid owner')] + fn test_game_assert_valid_owner_zero() { + GameAssert::assert_valid_owner(0); } #[test] @@ -375,10 +404,29 @@ mod tests { #[should_panic(expected: 'Game: not whitelistable')] fn test_game_assert_is_whitelistable_not_published() { let mut game = GameTrait::new( - WORLD_ADDRESS, NAMESPACE, "NAME", "DESCRIPTION", "TORII_URL", "IMAGE_URI", OWNER, + world_address: WORLD_ADDRESS, + namespace: NAMESPACE, + project: PROJECT, + metadata: core::Default::default(), + socials: core::Default::default(), + owner: OWNER, ); game.publish(); game.hide(); game.whitelist(); } + + #[test] + #[should_panic(expected: 'Game: caller is not owner')] + fn test_game_assert_is_owner() { + let game = GameTrait::new( + world_address: WORLD_ADDRESS, + namespace: NAMESPACE, + project: PROJECT, + metadata: core::Default::default(), + socials: core::Default::default(), + owner: OWNER, + ); + game.assert_is_owner('CALLER'); + } } diff --git a/packages/registry/src/models/index.cairo b/packages/registry/src/models/index.cairo index 94bf8d6..49b886c 100644 --- a/packages/registry/src/models/index.cairo +++ b/packages/registry/src/models/index.cairo @@ -1,5 +1,13 @@ /// Models +#[derive(Clone, Drop, Serde)] +#[dojo::model] +pub struct Access { + #[key] + address: felt252, + role: u8, +} + #[derive(Clone, Drop, Serde)] #[dojo::model] pub struct Game { @@ -7,13 +15,14 @@ pub struct Game { world_address: felt252, #[key] namespace: felt252, + project: felt252, + active: bool, published: bool, whitelisted: bool, - total_karma: u16, - name: ByteArray, - description: ByteArray, - torii_url: ByteArray, - image_uri: ByteArray, + priority: u8, + karma: u16, + metadata: ByteArray, + socials: ByteArray, owner: felt252, } diff --git a/packages/registry/src/store.cairo b/packages/registry/src/store.cairo index 1edf3d2..469cb09 100644 --- a/packages/registry/src/store.cairo +++ b/packages/registry/src/store.cairo @@ -10,8 +10,9 @@ use dojo::world::WorldStorage; use dojo::model::ModelStorage; // Models imports -use arcade_registry::models::game::Game; -use arcade_registry::models::achievement::Achievement; +use registry::models::access::Access; +use registry::models::achievement::Achievement; +use registry::models::game::Game; // Structs @@ -30,8 +31,8 @@ impl StoreImpl of StoreTrait { } #[inline] - fn get_game(self: Store, world_address: felt252, namespace: felt252) -> Game { - self.world.read_model((world_address, namespace)) + fn get_access(self: Store, address: felt252) -> Access { + self.world.read_model(address) } #[inline] @@ -42,12 +43,32 @@ impl StoreImpl of StoreTrait { } #[inline] - fn set_game(ref self: Store, game: Game) { - self.world.write_model(@game); + fn get_game(self: Store, world_address: felt252, namespace: felt252) -> Game { + self.world.read_model((world_address, namespace)) + } + + #[inline] + fn set_access(ref self: Store, access: @Access) { + self.world.write_model(access); + } + + #[inline] + fn set_achievement(ref self: Store, achievement: @Achievement) { + self.world.write_model(achievement); + } + + #[inline] + fn set_game(ref self: Store, game: @Game) { + self.world.write_model(game); + } + + #[inline] + fn delete_achievement(ref self: Store, achievement: @Achievement) { + self.world.erase_model(achievement); } #[inline] - fn set_achievement(ref self: Store, achievement: Achievement) { - self.world.write_model(@achievement); + fn delete_game(ref self: Store, game: @Game) { + self.world.erase_model(game); } } diff --git a/packages/registry/src/tests/mocks/controller.cairo b/packages/registry/src/tests/mocks/controller.cairo deleted file mode 100644 index 8777186..0000000 --- a/packages/registry/src/tests/mocks/controller.cairo +++ /dev/null @@ -1,52 +0,0 @@ -#[starknet::interface] -trait IController { - fn assert_is_authorized(self: @TContractState) {} - fn assert_is_owner(self: @TContractState, world_address: felt252, namespace: felt252); -} - -#[dojo::contract] -pub mod Controller { - // Dojo imports - - use dojo::world::WorldStorage; - - // Internal imports - - use arcade_registry::components::controllable::ControllableComponent; - - // Local imports - - use super::IController; - - // Components - - component!(path: ControllableComponent, storage: controllable, event: ControllableEvent); - impl InternalImpl = ControllableComponent::InternalImpl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub controllable: ControllableComponent::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - ControllableEvent: ControllableComponent::Event - } - - #[abi(embed_v0)] - impl ControllerImpl of IController { - fn assert_is_owner(self: @ContractState, world_address: felt252, namespace: felt252) { - self.controllable.assert_is_owner(self.world_storage(), world_address, namespace); - } - } - - #[generate_trait] - impl Private of PrivateTrait { - fn world_storage(self: @ContractState) -> WorldStorage { - self.world(@"namespace") - } - } -} diff --git a/packages/registry/src/tests/mocks/register.cairo b/packages/registry/src/tests/mocks/register.cairo new file mode 100644 index 0000000..32722fa --- /dev/null +++ b/packages/registry/src/tests/mocks/register.cairo @@ -0,0 +1,183 @@ +#[starknet::interface] +trait IRegister { + fn register( + self: @TContractState, + world_address: felt252, + namespace: felt252, + project: felt252, + color: Option, + name: Option, + description: Option, + image: Option, + banner: Option, + discord: Option, + telegram: Option, + twitter: Option, + youtube: Option, + website: Option, + ); + fn update( + self: @TContractState, + world_address: felt252, + namespace: felt252, + color: Option, + name: Option, + description: Option, + image: Option, + banner: Option, + discord: Option, + telegram: Option, + twitter: Option, + youtube: Option, + website: Option, + ); + fn publish(self: @TContractState, world_address: felt252, namespace: felt252); + fn hide(self: @TContractState, world_address: felt252, namespace: felt252); + fn whitelist(self: @TContractState, world_address: felt252, namespace: felt252); + fn blacklist(self: @TContractState, world_address: felt252, namespace: felt252); +} + +#[dojo::contract] +pub mod Register { + // Starknet imports + + use starknet::{ContractAddress, get_block_timestamp, get_contract_address}; + + // Dojo imports + + use dojo::world::WorldStorage; + use dojo::contract::{IContractDispatcher, IContractDispatcherTrait}; + + // Internal imports + + use registry::components::initializable::InitializableComponent; + use registry::components::registerable::RegisterableComponent; + + // Local imports + + use super::IRegister; + + // Components + + component!(path: InitializableComponent, storage: initializable, event: InitializableEvent); + impl InitializableImpl = InitializableComponent::InternalImpl; + component!(path: RegisterableComponent, storage: registerable, event: RegisterableEvent); + impl RegisterableImpl = RegisterableComponent::InternalImpl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub initializable: InitializableComponent::Storage, + #[substorage(v0)] + pub registerable: RegisterableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + InitializableEvent: InitializableComponent::Event, + #[flat] + RegisterableEvent: RegisterableComponent::Event + } + + fn dojo_init(self: @ContractState, owner: felt252) { + self.initializable.initialize(self.world_storage(), owner); + } + + #[abi(embed_v0)] + impl RegisterImpl of IRegister { + fn register( + self: @ContractState, + world_address: felt252, + namespace: felt252, + project: felt252, + color: Option, + name: Option, + description: Option, + image: Option, + banner: Option, + discord: Option, + telegram: Option, + twitter: Option, + youtube: Option, + website: Option, + ) { + self + .registerable + .register( + self.world_storage(), + world_address, + namespace, + project, + color, + name, + description, + image, + banner, + discord, + telegram, + twitter, + youtube, + website, + ); + } + + fn update( + self: @ContractState, + world_address: felt252, + namespace: felt252, + color: Option, + name: Option, + description: Option, + image: Option, + banner: Option, + discord: Option, + telegram: Option, + twitter: Option, + youtube: Option, + website: Option, + ) { + self + .registerable + .update( + self.world_storage(), + world_address, + namespace, + color, + name, + description, + image, + banner, + discord, + telegram, + twitter, + youtube, + website, + ); + } + + fn publish(self: @ContractState, world_address: felt252, namespace: felt252) { + self.registerable.publish(self.world_storage(), world_address, namespace); + } + + fn hide(self: @ContractState, world_address: felt252, namespace: felt252) { + self.registerable.hide(self.world_storage(), world_address, namespace); + } + + fn whitelist(self: @ContractState, world_address: felt252, namespace: felt252) { + self.registerable.whitelist(self.world_storage(), world_address, namespace); + } + + fn blacklist(self: @ContractState, world_address: felt252, namespace: felt252) { + self.registerable.blacklist(self.world_storage(), world_address, namespace); + } + } + + #[generate_trait] + impl Private of PrivateTrait { + fn world_storage(self: @ContractState) -> WorldStorage { + self.world(@"namespace") + } + } +} diff --git a/packages/registry/src/tests/mocks/registrer.cairo b/packages/registry/src/tests/mocks/registrer.cairo deleted file mode 100644 index 9ffc7c5..0000000 --- a/packages/registry/src/tests/mocks/registrer.cairo +++ /dev/null @@ -1,222 +0,0 @@ -#[starknet::interface] -trait IRegistrer { - fn register_game( - self: @TContractState, - world_address: felt252, - namespace: felt252, - name: ByteArray, - description: ByteArray, - torii_url: ByteArray, - image_uri: ByteArray, - owner: felt252, - ); - fn update_game( - self: @TContractState, - world_address: felt252, - namespace: felt252, - name: ByteArray, - description: ByteArray, - torii_url: ByteArray, - image_uri: ByteArray, - ); - fn publish_game(self: @TContractState, world_address: felt252, namespace: felt252); - fn hide_game(self: @TContractState, world_address: felt252, namespace: felt252); - fn whitelist_game(self: @TContractState, world_address: felt252, namespace: felt252); - fn blacklist_game(self: @TContractState, world_address: felt252, namespace: felt252); - fn register_achievement( - self: @TContractState, - world_address: felt252, - namespace: felt252, - identifier: felt252, - karma: u16, - ); - fn update_achievement( - self: @TContractState, - world_address: felt252, - namespace: felt252, - identifier: felt252, - karma: u16, - ); - fn publish_achievement( - self: @TContractState, world_address: felt252, namespace: felt252, identifier: felt252 - ); - fn hide_achievement( - self: @TContractState, world_address: felt252, namespace: felt252, identifier: felt252 - ); - fn whitelist_achievement( - self: @TContractState, world_address: felt252, namespace: felt252, identifier: felt252 - ); - fn blacklist_achievement( - self: @TContractState, world_address: felt252, namespace: felt252, identifier: felt252 - ); -} - -#[dojo::contract] -pub mod Registrer { - // Starknet imports - - use starknet::{ContractAddress, get_block_timestamp, get_contract_address}; - - // Dojo imports - - use dojo::world::WorldStorage; - use dojo::contract::{IContractDispatcher, IContractDispatcherTrait}; - - // Internal imports - - use arcade_registry::components::registrable::RegistrableComponent; - - // Local imports - - use super::IRegistrer; - - // Components - - component!(path: RegistrableComponent, storage: registrable, event: RegistrableEvent); - impl InternalImpl = RegistrableComponent::InternalImpl; - - #[storage] - pub struct Storage { - #[substorage(v0)] - pub registrable: RegistrableComponent::Storage - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - RegistrableEvent: RegistrableComponent::Event - } - - #[abi(embed_v0)] - impl RegistrerImpl of IRegistrer { - fn register_game( - self: @ContractState, - world_address: felt252, - namespace: felt252, - name: ByteArray, - description: ByteArray, - torii_url: ByteArray, - image_uri: ByteArray, - owner: felt252, - ) { - self - .registrable - .register_game( - self.world_storage(), - world_address, - namespace, - name, - description, - torii_url, - image_uri, - owner - ); - } - - fn update_game( - self: @ContractState, - world_address: felt252, - namespace: felt252, - name: ByteArray, - description: ByteArray, - torii_url: ByteArray, - image_uri: ByteArray, - ) { - self - .registrable - .update_game( - self.world_storage(), - world_address, - namespace, - name, - description, - torii_url, - image_uri - ); - } - - fn publish_game(self: @ContractState, world_address: felt252, namespace: felt252) { - self.registrable.publish_game(self.world_storage(), world_address, namespace); - } - - fn hide_game(self: @ContractState, world_address: felt252, namespace: felt252) { - self.registrable.hide_game(self.world_storage(), world_address, namespace); - } - - fn whitelist_game(self: @ContractState, world_address: felt252, namespace: felt252) { - self.registrable.whitelist_game(self.world_storage(), world_address, namespace); - } - - fn blacklist_game(self: @ContractState, world_address: felt252, namespace: felt252) { - self.registrable.blacklist_game(self.world_storage(), world_address, namespace); - } - - fn register_achievement( - self: @ContractState, - world_address: felt252, - namespace: felt252, - identifier: felt252, - karma: u16, - ) { - self - .registrable - .register_achievement( - self.world_storage(), world_address, namespace, identifier, karma - ); - } - - fn update_achievement( - self: @ContractState, - world_address: felt252, - namespace: felt252, - identifier: felt252, - karma: u16, - ) { - self - .registrable - .update_achievement( - self.world_storage(), world_address, namespace, identifier, karma - ); - } - - fn publish_achievement( - self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 - ) { - self - .registrable - .publish_achievement(self.world_storage(), world_address, namespace, identifier); - } - - fn hide_achievement( - self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 - ) { - self - .registrable - .hide_achievement(self.world_storage(), world_address, namespace, identifier); - } - - fn whitelist_achievement( - self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 - ) { - self - .registrable - .whitelist_achievement(self.world_storage(), world_address, namespace, identifier); - } - - fn blacklist_achievement( - self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 - ) { - self - .registrable - .blacklist_achievement(self.world_storage(), world_address, namespace, identifier); - } - } - - #[generate_trait] - impl Private of PrivateTrait { - fn world_storage(self: @ContractState) -> WorldStorage { - self.world(@"namespace") - } - } -} diff --git a/packages/registry/src/tests/mocks/tracker.cairo b/packages/registry/src/tests/mocks/tracker.cairo new file mode 100644 index 0000000..cc6a57c --- /dev/null +++ b/packages/registry/src/tests/mocks/tracker.cairo @@ -0,0 +1,134 @@ +#[starknet::interface] +trait ITracker { + fn register( + self: @TContractState, + world_address: felt252, + namespace: felt252, + identifier: felt252, + karma: u16, + ); + fn update( + self: @TContractState, + world_address: felt252, + namespace: felt252, + identifier: felt252, + karma: u16, + ); + fn publish( + self: @TContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ); + fn hide(self: @TContractState, world_address: felt252, namespace: felt252, identifier: felt252); + fn whitelist( + self: @TContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ); + fn blacklist( + self: @TContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ); +} + +#[dojo::contract] +pub mod Tracker { + // Starknet imports + + use starknet::{ContractAddress, get_block_timestamp, get_contract_address}; + + // Dojo imports + + use dojo::world::WorldStorage; + use dojo::contract::{IContractDispatcher, IContractDispatcherTrait}; + + // Internal imports + + use registry::components::initializable::InitializableComponent; + use registry::components::trackable::TrackableComponent; + + // Local imports + + use super::ITracker; + + // Components + + component!(path: InitializableComponent, storage: initializable, event: InitializableEvent); + impl InitializableImpl = InitializableComponent::InternalImpl; + component!(path: TrackableComponent, storage: trackable, event: TrackableEvent); + impl TrackableImpl = TrackableComponent::InternalImpl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub initializable: InitializableComponent::Storage, + #[substorage(v0)] + pub trackable: TrackableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + InitializableEvent: InitializableComponent::Event, + #[flat] + TrackableEvent: TrackableComponent::Event + } + + fn dojo_init(self: @ContractState, owner: felt252) { + self.initializable.initialize(self.world_storage(), owner); + } + + #[abi(embed_v0)] + impl TrackerImpl of ITracker { + fn register( + self: @ContractState, + world_address: felt252, + namespace: felt252, + identifier: felt252, + karma: u16, + ) { + self + .trackable + .register(self.world_storage(), world_address, namespace, identifier, karma); + } + + fn update( + self: @ContractState, + world_address: felt252, + namespace: felt252, + identifier: felt252, + karma: u16, + ) { + self + .trackable + .update(self.world_storage(), world_address, namespace, identifier, karma); + } + + fn publish( + self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ) { + self.trackable.publish(self.world_storage(), world_address, namespace, identifier); + } + + fn hide( + self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ) { + self.trackable.hide(self.world_storage(), world_address, namespace, identifier); + } + + fn whitelist( + self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ) { + self.trackable.whitelist(self.world_storage(), world_address, namespace, identifier); + } + + fn blacklist( + self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ) { + self.trackable.blacklist(self.world_storage(), world_address, namespace, identifier); + } + } + + #[generate_trait] + impl Private of PrivateTrait { + fn world_storage(self: @ContractState) -> WorldStorage { + self.world(@"namespace") + } + } +} diff --git a/packages/registry/src/tests/setup.cairo b/packages/registry/src/tests/setup.cairo index 869be43..e48a861 100644 --- a/packages/registry/src/tests/setup.cairo +++ b/packages/registry/src/tests/setup.cairo @@ -7,7 +7,7 @@ mod setup { use starknet::ContractAddress; use starknet::testing; - use starknet::testing::{set_contract_address, set_block_timestamp}; + use starknet::testing::{set_caller_address, set_contract_address, set_block_timestamp}; // Dojo imports @@ -19,9 +19,9 @@ mod setup { // Internal imports - use arcade_registry::models::{index as models}; - use arcade_registry::tests::mocks::controller::{Controller, IController, IControllerDispatcher}; - use arcade_registry::tests::mocks::registrer::{Registrer, IRegistrer, IRegistrerDispatcher}; + use registry::models::{index as models}; + use registry::tests::mocks::register::{Register, IRegister, IRegisterDispatcher}; + use registry::tests::mocks::tracker::{Tracker, ITracker, ITrackerDispatcher}; // Constant @@ -35,8 +35,8 @@ mod setup { #[derive(Copy, Drop)] struct Systems { - controller: IControllerDispatcher, - registrer: IRegistrerDispatcher, + register: IRegisterDispatcher, + tracker: ITrackerDispatcher, } #[derive(Copy, Drop)] @@ -58,36 +58,39 @@ mod setup { fn setup_namespace() -> NamespaceDef { NamespaceDef { namespace: "namespace", resources: [ - TestResource::Model(models::m_Game::TEST_CLASS_HASH), + TestResource::Model(models::m_Access::TEST_CLASS_HASH), TestResource::Model(models::m_Achievement::TEST_CLASS_HASH), - TestResource::Contract(Controller::TEST_CLASS_HASH), - TestResource::Contract(Registrer::TEST_CLASS_HASH), + TestResource::Model(models::m_Game::TEST_CLASS_HASH), + TestResource::Contract(Register::TEST_CLASS_HASH), + TestResource::Contract(Tracker::TEST_CLASS_HASH), ].span() } } fn setup_contracts() -> Span { [ - ContractDefTrait::new(@"namespace", @"Controller") - .with_writer_of([dojo::utils::bytearray_hash(@"namespace")].span()), - ContractDefTrait::new(@"namespace", @"Registrer") - .with_writer_of([dojo::utils::bytearray_hash(@"namespace")].span()), + ContractDefTrait::new(@"namespace", @"Register") + .with_writer_of([dojo::utils::bytearray_hash(@"namespace")].span()) + .with_init_calldata(array![OWNER().into()].span()), + ContractDefTrait::new(@"namespace", @"Tracker") + .with_writer_of([dojo::utils::bytearray_hash(@"namespace")].span()) + .with_init_calldata(array![OWNER().into()].span()), ].span() } #[inline] - fn spawn_game() -> (WorldStorage, Systems, Context) { + fn spawn() -> (WorldStorage, Systems, Context) { // [Setup] World set_contract_address(OWNER()); let namespace_def = setup_namespace(); let world = spawn_test_world([namespace_def].span()); world.sync_perms_and_inits(setup_contracts()); // [Setup] Systems - let (controller_address, _) = world.dns(@"Controller").unwrap(); - let (registrer_address, _) = world.dns(@"Registrer").unwrap(); + let (register_address, _) = world.dns(@"Register").unwrap(); + let (tracker_address, _) = world.dns(@"Tracker").unwrap(); let systems = Systems { - controller: IControllerDispatcher { contract_address: controller_address }, - registrer: IRegistrerDispatcher { contract_address: registrer_address }, + register: IRegisterDispatcher { contract_address: register_address }, + tracker: ITrackerDispatcher { contract_address: tracker_address }, }; // [Setup] Context diff --git a/packages/registry/src/tests/test_controllable.cairo b/packages/registry/src/tests/test_controllable.cairo deleted file mode 100644 index bd33966..0000000 --- a/packages/registry/src/tests/test_controllable.cairo +++ /dev/null @@ -1,54 +0,0 @@ -// Core imports - -use core::num::traits::Zero; - -// Starknet imports - -use starknet::ContractAddress; -use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; -use starknet::testing; - -// Internal imports - -use arcade_registry::store::{Store, StoreTrait}; -use arcade_registry::models::game::{Game, GameTrait}; -use arcade_registry::models::achievement::{Achievement, AchievementTrait}; -use arcade_registry::tests::mocks::controller::{ - Controller, IControllerDispatcher, IControllerDispatcherTrait -}; -use arcade_registry::tests::setup::setup::{spawn_game, Systems, Context, OWNER, PLAYER}; - -// Constants - -const WORLD_ADDRESS: felt252 = 'WORLD'; -const NAMEPSACE: felt252 = 'NAMESPACE'; - -// Tests - -#[test] -fn test_controllable_assert_is_owner() { - // [Setup] - let (world, systems, _context) = spawn_game(); - let mut store = StoreTrait::new(world); - let game = GameTrait::new( - WORLD_ADDRESS, NAMEPSACE, "NAME", "DESCRIPTION", "TORII_URL", "IMAGE_URI", OWNER().into() - ); - store.set_game(game); - // [Assert] - systems.controller.assert_is_owner(WORLD_ADDRESS, NAMEPSACE); -} - -#[test] -#[should_panic(expected: ('Controllable: unauthorized call', 'ENTRYPOINT_FAILED'))] -fn test_controllable_assert_is_owner_revert() { - // [Setup] - let (world, systems, _context) = spawn_game(); - let mut store = StoreTrait::new(world); - let game = GameTrait::new( - WORLD_ADDRESS, NAMEPSACE, "NAME", "DESCRIPTION", "TORII_URL", "IMAGE_URI", OWNER().into() - ); - store.set_game(game); - // [Assert] - testing::set_contract_address(PLAYER()); - systems.controller.assert_is_owner(WORLD_ADDRESS, NAMEPSACE); -} diff --git a/packages/registry/src/tests/test_registerable.cairo b/packages/registry/src/tests/test_registerable.cairo new file mode 100644 index 0000000..cf62fc8 --- /dev/null +++ b/packages/registry/src/tests/test_registerable.cairo @@ -0,0 +1,131 @@ +// Core imports + +use core::num::traits::Zero; + +// Starknet imports + +use starknet::ContractAddress; +use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; +use starknet::testing; + +// Internal imports + +use registry::store::{Store, StoreTrait}; +use registry::models::game::{Game, GameTrait}; +use registry::models::achievement::{Achievement, AchievementTrait}; +use registry::tests::mocks::register::{Register, IRegisterDispatcher, IRegisterDispatcherTrait}; +use registry::tests::setup::setup::{spawn, Systems, Context, PLAYER, OWNER}; + +// Constants + +const WORLD_ADDRESS: felt252 = 'WORLD'; +const NAMEPSACE: felt252 = 'NAMESPACE'; +const PROJECT: felt252 = 'PROJECT'; + +// Helpers + +fn register(systems: @Systems) { + testing::set_contract_address(PLAYER()); + (*systems) + .register + .register( + WORLD_ADDRESS, + NAMEPSACE, + PROJECT, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + ); +} + +// Tests + +#[test] +fn test_registrable_register() { + // [Setup] + let (world, systems, _context) = spawn(); + // [Register] Game + register(@systems); + // [Assert] Game + let store = StoreTrait::new(world); + let game: Game = store.get_game(WORLD_ADDRESS, NAMEPSACE); + assert_eq!(game.world_address, WORLD_ADDRESS); + assert_eq!(game.namespace, NAMEPSACE); + assert_eq!(game.published, false); + assert_eq!(game.whitelisted, false); + assert_eq!(game.karma, 0); + assert_eq!( + game.metadata, + "{\"color\":\"\",\"name\":\"\",\"description\":\"\",\"image\":\"\",\"banner\":\"\"}" + ); + assert_eq!( + game.socials, + "{\"discord\":\"\",\"telegram\":\"\",\"twitter\":\"\",\"youtube\":\"\",\"website\":\"\"}" + ); + assert_eq!(game.owner, PLAYER().into()); +} + +#[test] +fn test_registrable_update() { + // [Setup] + let (world, systems, _context) = spawn(); + register(@systems); + // [Update] Game + let color = Option::Some('#123456'); + systems + .register + .update( + WORLD_ADDRESS, + NAMEPSACE, + color, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + ); + // [Assert] Game + let store = StoreTrait::new(world); + let game = store.get_game(WORLD_ADDRESS, NAMEPSACE); + assert_eq!( + game.metadata, + "{\"color\":\"#123456\",\"name\":\"\",\"description\":\"\",\"image\":\"\",\"banner\":\"\"}" + ); +} + +#[test] +fn test_registrable_publish() { + // [Setup] + let (world, systems, _context) = spawn(); + register(@systems); + systems.register.publish(WORLD_ADDRESS, NAMEPSACE); + // [Assert] Game + let store = StoreTrait::new(world); + let game = store.get_game(WORLD_ADDRESS, NAMEPSACE); + assert_eq!(game.published, true); +} + +#[test] +fn test_registrable_whitelist() { + // [Setup] + let (world, systems, _context) = spawn(); + register(@systems); + systems.register.publish(WORLD_ADDRESS, NAMEPSACE); + testing::set_contract_address(OWNER()); + systems.register.whitelist(WORLD_ADDRESS, NAMEPSACE); + // [Assert] Game + let store = StoreTrait::new(world); + let game = store.get_game(WORLD_ADDRESS, NAMEPSACE); + assert_eq!(game.whitelisted, true); +} diff --git a/packages/registry/src/tests/test_registrable.cairo b/packages/registry/src/tests/test_registrable.cairo deleted file mode 100644 index a1481a3..0000000 --- a/packages/registry/src/tests/test_registrable.cairo +++ /dev/null @@ -1,212 +0,0 @@ -// Core imports - -use core::num::traits::Zero; - -// Starknet imports - -use starknet::ContractAddress; -use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; -use starknet::testing; - -// Internal imports - -use arcade_registry::store::{Store, StoreTrait}; -use arcade_registry::models::game::{Game, GameTrait}; -use arcade_registry::models::achievement::{Achievement, AchievementTrait}; -use arcade_registry::tests::mocks::registrer::{ - Registrer, IRegistrerDispatcher, IRegistrerDispatcherTrait -}; -use arcade_registry::tests::setup::setup::{spawn_game, Systems, Context, PLAYER}; - -// Constants - -const WORLD_ADDRESS: felt252 = 'WORLD'; -const NAMEPSACE: felt252 = 'NAMESPACE'; - -// Helpers - -fn register_game(systems: @Systems, context: @Context) { - let name: ByteArray = "NAME"; - let description: ByteArray = "DESCRIPTION"; - let torii_url: ByteArray = "TORII_URL"; - let image_uri: ByteArray = "IMAGE_URI"; - let owner: felt252 = *context.player_id; - (*systems) - .registrer - .register_game( - WORLD_ADDRESS, - NAMEPSACE, - name.clone(), - description.clone(), - torii_url.clone(), - image_uri.clone(), - owner - ); -} - -// Tests - -#[test] -fn test_registrable_register_game() { - // [Setup] - let (world, systems, context) = spawn_game(); - let name: ByteArray = "NAME"; - let description: ByteArray = "DESCRIPTION"; - let torii_url: ByteArray = "TORII_URL"; - let image_uri: ByteArray = "IMAGE_URI"; - let owner: felt252 = context.player_id; - // [Register] Game - systems - .registrer - .register_game( - WORLD_ADDRESS, - NAMEPSACE, - name.clone(), - description.clone(), - torii_url.clone(), - image_uri.clone(), - owner - ); - // [Assert] Game - let store = StoreTrait::new(world); - let game = store.get_game(WORLD_ADDRESS, NAMEPSACE); - assert_eq!(game.world_address, WORLD_ADDRESS); - assert_eq!(game.namespace, NAMEPSACE); - assert_eq!(game.published, false); - assert_eq!(game.whitelisted, false); - assert_eq!(game.total_karma, 0); - assert_eq!(game.name, name); - assert_eq!(game.description, description); - assert_eq!(game.torii_url, torii_url); - assert_eq!(game.image_uri, image_uri); - assert_eq!(game.owner, owner); -} - -#[test] -fn test_registrable_update_game() { - // [Setup] - let (world, systems, context) = spawn_game(); - register_game(@systems, @context); - // [Update] Game - let new_name: ByteArray = "NEW_NAME"; - let new_description: ByteArray = "NEW_DESCRIPTION"; - let new_torii_url: ByteArray = "NEW_TORII_URL"; - let new_image_uri: ByteArray = "NEW_IMAGE_URI"; - systems - .registrer - .update_game( - WORLD_ADDRESS, - NAMEPSACE, - new_name.clone(), - new_description.clone(), - new_torii_url.clone(), - new_image_uri.clone() - ); - // [Assert] Game - let store = StoreTrait::new(world); - let game = store.get_game(WORLD_ADDRESS, NAMEPSACE); - assert_eq!(game.name, new_name); - assert_eq!(game.description, new_description); - assert_eq!(game.torii_url, new_torii_url); - assert_eq!(game.image_uri, new_image_uri); -} - -#[test] -fn test_registrable_publish_game() { - // [Setup] - let (world, systems, context) = spawn_game(); - register_game(@systems, @context); - systems.registrer.publish_game(WORLD_ADDRESS, NAMEPSACE); - // [Assert] Game - let store = StoreTrait::new(world); - let game = store.get_game(WORLD_ADDRESS, NAMEPSACE); - assert_eq!(game.published, true); -} - -#[test] -fn test_registrable_whitelist_game() { - // [Setup] - let (world, systems, context) = spawn_game(); - register_game(@systems, @context); - systems.registrer.publish_game(WORLD_ADDRESS, NAMEPSACE); - systems.registrer.whitelist_game(WORLD_ADDRESS, NAMEPSACE); - // [Assert] Game - let store = StoreTrait::new(world); - let game = store.get_game(WORLD_ADDRESS, NAMEPSACE); - assert_eq!(game.whitelisted, true); -} - -#[test] -fn test_registrable_register_achievement() { - // [Setup] - let (world, systems, context) = spawn_game(); - register_game(@systems, @context); - // [Register] Achievement - let identifier: felt252 = 'IDENTIFIER'; - let karma: u16 = 10; - systems.registrer.register_achievement(WORLD_ADDRESS, NAMEPSACE, identifier, karma); - // [Assert] Achievement - let store = StoreTrait::new(world); - let achievement = store.get_achievement(WORLD_ADDRESS, NAMEPSACE, identifier); - assert_eq!(achievement.id, identifier); - assert_eq!(achievement.karma, karma); - // [Assert] Game - let game = store.get_game(WORLD_ADDRESS, NAMEPSACE); - assert_eq!(game.total_karma, karma); -} - -#[test] -fn test_registrable_update_achievement() { - // [Setup] - let (world, systems, context) = spawn_game(); - register_game(@systems, @context); - // [Register] Achievement - let identifier: felt252 = 'IDENTIFIER'; - let karma: u16 = 10; - systems.registrer.register_achievement(WORLD_ADDRESS, NAMEPSACE, identifier, karma); - // [Update] Achievement - let new_karma: u16 = 20; - systems.registrer.update_achievement(WORLD_ADDRESS, NAMEPSACE, identifier, new_karma); - // [Assert] Achievement - let store = StoreTrait::new(world); - let achievement = store.get_achievement(WORLD_ADDRESS, NAMEPSACE, identifier); - assert_eq!(achievement.karma, new_karma); - // [Assert] Game - let game = store.get_game(WORLD_ADDRESS, NAMEPSACE); - assert_eq!(game.total_karma, new_karma); -} - -#[test] -fn test_registrable_publish_achievement() { - // [Setup] - let (world, systems, context) = spawn_game(); - register_game(@systems, @context); - // [Register] Achievement - let identifier: felt252 = 'IDENTIFIER'; - let karma: u16 = 10; - systems.registrer.register_achievement(WORLD_ADDRESS, NAMEPSACE, identifier, karma); - // [Publish] Achievement - systems.registrer.publish_achievement(WORLD_ADDRESS, NAMEPSACE, identifier); - // [Assert] Achievement - let store = StoreTrait::new(world); - let achievement = store.get_achievement(WORLD_ADDRESS, NAMEPSACE, identifier); - assert_eq!(achievement.published, true); -} - -#[test] -fn test_registrable_whitelist_achievement() { - // [Setup] - let (world, systems, context) = spawn_game(); - register_game(@systems, @context); - // [Register] Achievement - let identifier: felt252 = 'IDENTIFIER'; - let karma: u16 = 10; - systems.registrer.register_achievement(WORLD_ADDRESS, NAMEPSACE, identifier, karma); - // [Whitelist] Achievement - systems.registrer.publish_achievement(WORLD_ADDRESS, NAMEPSACE, identifier); - systems.registrer.whitelist_achievement(WORLD_ADDRESS, NAMEPSACE, identifier); - // [Assert] Achievement - let store = StoreTrait::new(world); - let achievement = store.get_achievement(WORLD_ADDRESS, NAMEPSACE, identifier); - assert_eq!(achievement.whitelisted, true); -} diff --git a/packages/registry/src/tests/test_trackable.cairo b/packages/registry/src/tests/test_trackable.cairo new file mode 100644 index 0000000..858bdad --- /dev/null +++ b/packages/registry/src/tests/test_trackable.cairo @@ -0,0 +1,125 @@ +// Core imports + +use core::num::traits::Zero; + +// Starknet imports + +use starknet::ContractAddress; +use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; +use starknet::testing; + +// Internal imports + +use registry::store::{Store, StoreTrait}; +use registry::models::game::{Game, GameTrait}; +use registry::models::achievement::{Achievement, AchievementTrait}; +use registry::tests::mocks::register::{Register, IRegisterDispatcher, IRegisterDispatcherTrait}; +use registry::tests::mocks::tracker::{Tracker, ITrackerDispatcher, ITrackerDispatcherTrait}; +use registry::tests::setup::setup::{spawn, Systems, Context, PLAYER, OWNER}; + +// Constants + +const WORLD_ADDRESS: felt252 = 'WORLD'; +const NAMEPSACE: felt252 = 'NAMESPACE'; +const PROJECT: felt252 = 'PROJECT'; + +// Helpers + +fn register(systems: @Systems) { + testing::set_contract_address(PLAYER()); + (*systems) + .register + .register( + WORLD_ADDRESS, + NAMEPSACE, + PROJECT, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + ); +} + +// Tests + +#[test] +fn test_trackable_register_achievement() { + // [Setup] + let (world, systems, _context) = spawn(); + register(@systems); + // [Register] Achievement + let identifier: felt252 = 'IDENTIFIER'; + let karma: u16 = 10; + systems.tracker.register(WORLD_ADDRESS, NAMEPSACE, identifier, karma); + // [Assert] Achievement + let store = StoreTrait::new(world); + let achievement = store.get_achievement(WORLD_ADDRESS, NAMEPSACE, identifier); + assert_eq!(achievement.id, identifier); + assert_eq!(achievement.karma, karma); + // [Assert] Game + let game = store.get_game(WORLD_ADDRESS, NAMEPSACE); + assert_eq!(game.karma, karma); +} + +#[test] +fn test_trackable_update_achievement() { + // [Setup] + let (world, systems, _context) = spawn(); + register(@systems); + // [Register] Achievement + let identifier: felt252 = 'IDENTIFIER'; + let karma: u16 = 10; + systems.tracker.register(WORLD_ADDRESS, NAMEPSACE, identifier, karma); + // [Update] Achievement + let new_karma: u16 = 20; + systems.tracker.update(WORLD_ADDRESS, NAMEPSACE, identifier, new_karma); + // [Assert] Achievement + let store = StoreTrait::new(world); + let achievement = store.get_achievement(WORLD_ADDRESS, NAMEPSACE, identifier); + assert_eq!(achievement.karma, new_karma); + // [Assert] Game + let game = store.get_game(WORLD_ADDRESS, NAMEPSACE); + assert_eq!(game.karma, new_karma); +} + +#[test] +fn test_trackable_publish_achievement() { + // [Setup] + let (world, systems, _context) = spawn(); + register(@systems); + // [Register] Achievement + let identifier: felt252 = 'IDENTIFIER'; + let karma: u16 = 10; + systems.tracker.register(WORLD_ADDRESS, NAMEPSACE, identifier, karma); + // [Publish] Achievement + systems.tracker.publish(WORLD_ADDRESS, NAMEPSACE, identifier); + // [Assert] Achievement + let store = StoreTrait::new(world); + let achievement = store.get_achievement(WORLD_ADDRESS, NAMEPSACE, identifier); + assert_eq!(achievement.published, true); +} + +#[test] +fn test_trackable_whitelist_achievement() { + // [Setup] + let (world, systems, _context) = spawn(); + register(@systems); + // [Register] Achievement + let identifier: felt252 = 'IDENTIFIER'; + let karma: u16 = 10; + systems.tracker.register(WORLD_ADDRESS, NAMEPSACE, identifier, karma); + // [Whitelist] Achievement + systems.tracker.publish(WORLD_ADDRESS, NAMEPSACE, identifier); + testing::set_contract_address(OWNER()); + systems.tracker.whitelist(WORLD_ADDRESS, NAMEPSACE, identifier); + // [Assert] Achievement + let store = StoreTrait::new(world); + let achievement = store.get_achievement(WORLD_ADDRESS, NAMEPSACE, identifier); + assert_eq!(achievement.whitelisted, true); +} diff --git a/packages/registry/src/types/metadata.cairo b/packages/registry/src/types/metadata.cairo new file mode 100644 index 0000000..bf005da --- /dev/null +++ b/packages/registry/src/types/metadata.cairo @@ -0,0 +1,98 @@ +// Internal imports + +use registry::helpers::json::{JsonifiableString, JsonifiableTrait}; + +// Constants + +const COLOR_LENGTH: usize = 7; + +#[derive(Clone, Drop)] +pub struct Metadata { + color: felt252, + name: ByteArray, + description: ByteArray, + image: ByteArray, + banner: ByteArray, +} + +// Implementations + +#[generate_trait] +pub impl MetadataImpl of MetadataTrait { + fn new( + color: Option, + name: Option, + description: Option, + image: Option, + banner: Option + ) -> Metadata { + let color = match color { + Option::Some(color) => color, + Option::None => 0, + }; + let name = match name { + Option::Some(name) => name, + Option::None => "", + }; + let description = match description { + Option::Some(description) => description, + Option::None => "", + }; + let image = match image { + Option::Some(image) => image, + Option::None => "", + }; + let banner = match banner { + Option::Some(banner) => banner, + Option::None => "", + }; + Metadata { + color: color, name: name, description: description, image: image, banner: banner + } + } +} + +pub impl MetadataJsonifiable of JsonifiableTrait { + fn jsonify(self: Metadata) -> ByteArray { + let mut color = ""; + if self.color != 0 { + color.append_word(self.color, COLOR_LENGTH); + } + let mut string = "{"; + string += JsonifiableString::jsonify("color", format!("{}", color)); + string += "," + JsonifiableString::jsonify("name", format!("{}", self.name)); + string += "," + JsonifiableString::jsonify("description", format!("{}", self.description)); + string += "," + JsonifiableString::jsonify("image", format!("{}", self.image)); + string += "," + JsonifiableString::jsonify("banner", format!("{}", self.banner)); + string + "}" + } +} + +pub impl MetadataDefault of core::Default { + fn default() -> Metadata { + MetadataTrait::new(Option::None, Option::None, Option::None, Option::None, Option::None) + } +} + +#[cfg(test)] +mod tests { + // Local imports + + use super::{Metadata, JsonifiableTrait}; + + #[test] + fn test_metadata_jsonify() { + let metadata = Metadata { + color: '#123456', + name: "name", + description: "description", + image: "image", + banner: "banner", + }; + let json = metadata.jsonify(); + assert_eq!( + json, + "{\"color\":\"#123456\",\"name\":\"name\",\"description\":\"description\",\"image\":\"image\",\"banner\":\"banner\"}" + ); + } +} diff --git a/packages/registry/src/types/role.cairo b/packages/registry/src/types/role.cairo new file mode 100644 index 0000000..abf5492 --- /dev/null +++ b/packages/registry/src/types/role.cairo @@ -0,0 +1,34 @@ +#[derive(Copy, Drop, PartialEq)] +pub enum Role { + None, + Member, + Admin, + Owner, +} + +// Implementations + +impl IntoRoleU8 of core::Into { + #[inline] + fn into(self: Role) -> u8 { + match self { + Role::None => 0, + Role::Member => 1, + Role::Admin => 2, + Role::Owner => 3, + } + } +} + +impl IntoU8Role of core::Into { + #[inline] + fn into(self: u8) -> Role { + match self { + 0 => Role::None, + 1 => Role::Member, + 2 => Role::Admin, + 3 => Role::Owner, + _ => Role::None, + } + } +} diff --git a/packages/registry/src/types/socials.cairo b/packages/registry/src/types/socials.cairo new file mode 100644 index 0000000..5fc9d37 --- /dev/null +++ b/packages/registry/src/types/socials.cairo @@ -0,0 +1,94 @@ +// Internal imports + +use registry::helpers::json::{JsonifiableString, JsonifiableTrait}; + +#[derive(Clone, Drop)] +pub struct Socials { + discord: ByteArray, + telegram: ByteArray, + twitter: ByteArray, + youtube: ByteArray, + website: ByteArray, +} + +// Implementations + +#[generate_trait] +pub impl SocialsImpl of SocialsTrait { + fn new( + discord: Option, + telegram: Option, + twitter: Option, + youtube: Option, + website: Option + ) -> Socials { + let discord = match discord { + Option::Some(discord) => discord, + Option::None => "", + }; + let telegram = match telegram { + Option::Some(telegram) => telegram, + Option::None => "", + }; + let twitter = match twitter { + Option::Some(twitter) => twitter, + Option::None => "", + }; + let youtube = match youtube { + Option::Some(youtube) => youtube, + Option::None => "", + }; + let website = match website { + Option::Some(website) => website, + Option::None => "", + }; + Socials { + discord: discord, + telegram: telegram, + twitter: twitter, + youtube: youtube, + website: website + } + } +} + +pub impl SocialsJsonifiable of JsonifiableTrait { + fn jsonify(self: Socials) -> ByteArray { + let mut string = "{"; + string += JsonifiableString::jsonify("discord", format!("{}", self.discord)); + string += "," + JsonifiableString::jsonify("telegram", format!("{}", self.telegram)); + string += "," + JsonifiableString::jsonify("twitter", format!("{}", self.twitter)); + string += "," + JsonifiableString::jsonify("youtube", format!("{}", self.youtube)); + string += "," + JsonifiableString::jsonify("website", format!("{}", self.website)); + string + "}" + } +} + +pub impl SocialsDefault of core::Default { + fn default() -> Socials { + SocialsTrait::new(Option::None, Option::None, Option::None, Option::None, Option::None) + } +} + +#[cfg(test)] +mod tests { + // Local imports + + use super::{Socials, JsonifiableTrait}; + + #[test] + fn test_socials_jsonify() { + let socials = Socials { + discord: "discord", + telegram: "telegram", + twitter: "twitter", + youtube: "youtube", + website: "website", + }; + let json = socials.jsonify(); + assert_eq!( + json, + "{\"discord\":\"discord\",\"telegram\":\"telegram\",\"twitter\":\"twitter\",\"youtube\":\"youtube\",\"website\":\"website\"}" + ); + } +} From 121c2da7795df43d2649a7deb9800ffe6019c359 Mon Sep 17 00:00:00 2001 From: Bal7hazar Date: Mon, 25 Nov 2024 14:44:18 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=91=A5=20Society=20feature=20(#10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🎉 Begin society package * ✨ Create member, guild and alliance features * ✨ Add follow feature * ♻️ Refactor and add pin feature * ✨ Create missing contracts * 🚀 Delpoy on slot --- Scarb.lock | 11 +- Scarb.toml | 7 +- contracts/Scarb.lock | 17 + contracts/Scarb.toml | 11 +- contracts/dojo_dev.toml | 17 +- contracts/dojo_slot.toml | 21 +- contracts/katana_dev.toml | 9 + contracts/katana_slot.toml | 7 + contracts/manifest_slot.json | 3207 +++++++++++++++++ contracts/src/lib.cairo | 10 +- contracts/src/systems/pinner.cairo | 74 + contracts/src/systems/registry.cairo | 60 +- contracts/src/systems/slot.cairo | 34 +- contracts/src/systems/society.cairo | 310 ++ .../{controller.cairo => wallet.cairo} | 8 +- contracts/src/tests/mocks/register.cairo | 197 + contracts/src/tests/mocks/tracker.cairo | 142 + contracts/src/tests/setup.cairo | 134 + contracts/src/tests/test_setup.cairo | 11 + contracts/src/tests/test_trackable.cairo | 125 + contracts/torii_dev.toml | 0 packages/{trophy => achievement}/README.md | 0 packages/{trophy => achievement}/Scarb.toml | 2 +- .../src/components/achievable.cairo | 10 +- .../achievement/src/components/pinnable.cairo | 58 + .../src/events/creation.cairo} | 91 +- .../src/events/index.cairo | 18 +- packages/achievement/src/events/pinning.cairo | 57 + .../src/events/progress.cairo | 6 +- .../{trophy => achievement}/src/lib.cairo | 4 +- .../{trophy => achievement}/src/store.cairo | 22 +- .../src/tests/mocks/achiever.cairo | 6 +- .../src/tests/setup.cairo | 5 +- .../src/tests/test_achievable.cairo | 12 +- .../src/types/index.cairo | 0 .../src/types/task.cairo | 24 +- .../src/components/controllable.cairo | 2 - packages/controller/src/lib.cairo | 3 - packages/controller/src/models/account.cairo | 8 +- .../controller/src/models/controller.cairo | 18 +- packages/controller/src/models/index.cairo | 19 - packages/controller/src/models/member.cairo | 95 - packages/controller/src/models/signer.cairo | 14 +- packages/controller/src/store.cairo | 22 - .../provider/src/components/deployable.cairo | 48 +- .../provider/src/components/groupable.cairo | 87 + packages/provider/src/lib.cairo | 4 + packages/provider/src/models/deployment.cairo | 48 +- packages/provider/src/models/factory.cairo | 2 +- packages/provider/src/models/index.cairo | 24 +- .../src/models/team.cairo | 57 +- packages/provider/src/models/teammate.cairo | 120 + packages/provider/src/store.cairo | 32 + .../src/types/role.cairo | 12 +- .../src/components/registerable.cairo | 27 +- .../registry/src/components/trackable.cairo | 24 +- packages/registry/src/models/access.cairo | 2 +- .../registry/src/models/achievement.cairo | 2 +- packages/registry/src/models/game.cairo | 2 +- .../registry/src/tests/mocks/register.cairo | 28 +- .../registry/src/tests/mocks/tracker.cairo | 28 +- packages/society/README.md | 0 packages/society/Scarb.toml | 10 + .../society/src/components/allianceable.cairo | 291 ++ .../society/src/components/followable.cairo | 53 + .../society/src/components/guildable.cairo | 302 ++ packages/society/src/constants.cairo | 4 + packages/society/src/events/follow.cairo | 72 + packages/society/src/events/index.cairo | 10 + packages/society/src/lib.cairo | 24 + packages/society/src/models/alliance.cairo | 132 + packages/society/src/models/guild.cairo | 244 ++ packages/society/src/models/index.cairo | 38 + packages/society/src/models/member.cairo | 199 + packages/society/src/store.cairo | 77 + packages/society/src/types/role.cairo | 34 + 76 files changed, 6570 insertions(+), 375 deletions(-) create mode 100644 contracts/katana_dev.toml create mode 100644 contracts/katana_slot.toml create mode 100644 contracts/manifest_slot.json create mode 100644 contracts/src/systems/pinner.cairo create mode 100644 contracts/src/systems/society.cairo rename contracts/src/systems/{controller.cairo => wallet.cairo} (88%) create mode 100644 contracts/src/tests/mocks/register.cairo create mode 100644 contracts/src/tests/mocks/tracker.cairo create mode 100644 contracts/src/tests/setup.cairo create mode 100644 contracts/src/tests/test_setup.cairo create mode 100644 contracts/src/tests/test_trackable.cairo create mode 100644 contracts/torii_dev.toml rename packages/{trophy => achievement}/README.md (100%) rename packages/{trophy => achievement}/Scarb.toml (84%) rename packages/{trophy => achievement}/src/components/achievable.cairo (92%) create mode 100644 packages/achievement/src/components/pinnable.cairo rename packages/{trophy/src/events/trophy.cairo => achievement/src/events/creation.cairo} (60%) rename packages/{trophy => achievement}/src/events/index.cairo (66%) create mode 100644 packages/achievement/src/events/pinning.cairo rename packages/{trophy => achievement}/src/events/progress.cairo (89%) rename packages/{trophy => achievement}/src/lib.cairo (82%) rename packages/{trophy => achievement}/src/store.cairo (61%) rename packages/{trophy => achievement}/src/tests/mocks/achiever.cairo (94%) rename packages/{trophy => achievement}/src/tests/setup.cairo (92%) rename packages/{trophy => achievement}/src/tests/test_achievable.cairo (88%) rename packages/{trophy => achievement}/src/types/index.cairo (100%) rename packages/{trophy => achievement}/src/types/task.cairo (67%) delete mode 100644 packages/controller/src/models/member.cairo create mode 100644 packages/provider/src/components/groupable.cairo rename packages/{controller => provider}/src/models/team.cairo (50%) create mode 100644 packages/provider/src/models/teammate.cairo rename packages/{controller => provider}/src/types/role.cairo (80%) create mode 100644 packages/society/README.md create mode 100644 packages/society/Scarb.toml create mode 100644 packages/society/src/components/allianceable.cairo create mode 100644 packages/society/src/components/followable.cairo create mode 100644 packages/society/src/components/guildable.cairo create mode 100644 packages/society/src/constants.cairo create mode 100644 packages/society/src/events/follow.cairo create mode 100644 packages/society/src/events/index.cairo create mode 100644 packages/society/src/lib.cairo create mode 100644 packages/society/src/models/alliance.cairo create mode 100644 packages/society/src/models/guild.cairo create mode 100644 packages/society/src/models/index.cairo create mode 100644 packages/society/src/models/member.cairo create mode 100644 packages/society/src/store.cairo create mode 100644 packages/society/src/types/role.cairo diff --git a/Scarb.lock b/Scarb.lock index 08088cd..4115ce9 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -2,7 +2,7 @@ version = 1 [[package]] -name = "arcade_trophy" +name = "achievement" version = "0.0.0" dependencies = [ "dojo", @@ -53,3 +53,12 @@ dependencies = [ "dojo", "dojo_cairo_test", ] + +[[package]] +name = "society" +version = "0.0.0" +dependencies = [ + "dojo", + "dojo_cairo_test", + "registry", +] diff --git a/Scarb.toml b/Scarb.toml index 808b9a2..de99f9f 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -1,9 +1,10 @@ [workspace] members = [ - "packages/trophy", - "packages/registry", - "packages/provider", "packages/controller", + "packages/provider", + "packages/registry", + "packages/society", + "packages/achievement", ] description = "Dojo achievement library" homepage = "https://github.com/cartridge-gg/arcade/" diff --git a/contracts/Scarb.lock b/contracts/Scarb.lock index 66547a8..6d36761 100644 --- a/contracts/Scarb.lock +++ b/contracts/Scarb.lock @@ -1,15 +1,24 @@ # Code generated by scarb DO NOT EDIT. version = 1 +[[package]] +name = "achievement" +version = "0.0.0" +dependencies = [ + "dojo", +] + [[package]] name = "arcade" version = "0.0.0" dependencies = [ + "achievement", "controller", "dojo", "dojo_cairo_test", "provider", "registry", + "society", ] [[package]] @@ -53,3 +62,11 @@ version = "0.0.0" dependencies = [ "dojo", ] + +[[package]] +name = "society" +version = "0.0.0" +dependencies = [ + "dojo", + "registry", +] diff --git a/contracts/Scarb.toml b/contracts/Scarb.toml index 740ef08..a842750 100644 --- a/contracts/Scarb.toml +++ b/contracts/Scarb.toml @@ -11,9 +11,11 @@ dev = "sozo clean && sozo build --typescript && sozo migrate plan && sozo migrat [dependencies] dojo = { git = "https://github.com/dojoengine/dojo", tag = "v1.0.1" } +achievement = { path = ".." } controller = { path = ".." } provider = { path = ".." } registry = { path = ".." } +society = { path = ".." } starknet = "2.8.4" cairo_test = "2.8.4" @@ -23,16 +25,21 @@ dojo_cairo_test = { git = "https://github.com/dojoengine/dojo", tag = "v1.0.1" } [[target.starknet-contract]] build-external-contracts = [ "dojo::world::world_contract::world", + "achievement::events::index::e_TrophyPinning", "controller::models::index::m_Account", "controller::models::index::m_Controller", - "controller::models::index::m_Member", "controller::models::index::m_Signer", - "controller::models::index::m_Team", "provider::models::index::m_Deployment", "provider::models::index::m_Factory", + "provider::models::index::m_Team", + "provider::models::index::m_Teammate", "registry::models::index::m_Access", "registry::models::index::m_Achievement", "registry::models::index::m_Game", + "society::models::index::m_Alliance", + "society::models::index::m_Guild", + "society::models::index::m_Member", + "society::events::index::e_Follow", ] [profile.slot] diff --git a/contracts/dojo_dev.toml b/contracts/dojo_dev.toml index d15138a..b08cce6 100644 --- a/contracts/dojo_dev.toml +++ b/contracts/dojo_dev.toml @@ -1,6 +1,6 @@ [world] name = "Cartridge World" -description = "Cartridge World Achievements" +description = "Cartridge World" website = "https://github.com/dojoengine/dojo-starter" seed = "arcade" @@ -11,10 +11,21 @@ github = "https://github.com/dojoengine/dojo-starter" telegram = "https://t.me/dojoengine" [namespace] -default = "game_center" +default = "ARCADE" + +[init_call_args] +"ARCADE-Registry" = [ + "0x6677fe62ee39c7b07401f754138502bab7fac99d2d3c5d37df7d1c6fab10819", +] [writers] -"game_center" = ["game_center-Actions"] +"ARCADE" = [ + "ARCADE-Registry", + "ARCADE-Slot", + "ARCADE-Society", + "ARCADE-Pinner", + "ARCADE-Wallet", +] [env] rpc_url = "http://localhost:5050/" diff --git a/contracts/dojo_slot.toml b/contracts/dojo_slot.toml index 01a15a4..1c8ba9c 100644 --- a/contracts/dojo_slot.toml +++ b/contracts/dojo_slot.toml @@ -1,6 +1,6 @@ [world] name = "Cartridge World" -description = "Cartridge World Achievements" +description = "Cartridge World" website = "https://github.com/dojoengine/dojo-starter" seed = "arcade" @@ -11,12 +11,23 @@ github = "https://github.com/dojoengine/dojo-starter" telegram = "https://t.me/dojoengine" [namespace] -default = "game_center" +default = "ARCADE" + +[init_call_args] +"ARCADE-Registry" = [ + "0x6677fe62ee39c7b07401f754138502bab7fac99d2d3c5d37df7d1c6fab10819", +] [writers] -"game_center" = ["game_center-Actions"] +"ARCADE" = [ + "ARCADE-Registry", + "ARCADE-Slot", + "ARCADE-Society", + "ARCADE-Pinner", + "ARCADE-Wallet", +] [env] rpc_url = "https://api.cartridge.gg/x/arcade/katana" -account_address = "0x79e1341bc8e27b2ae2544fe902f0c9e723cfba8e37c47371d90385cc17265cc" -private_key = "0x506a3b48830c89357283236d8724f68d014d681499b2256e7589c950a4198c5" +account_address = "0x6677fe62ee39c7b07401f754138502bab7fac99d2d3c5d37df7d1c6fab10819" +private_key = "0x3e3979c1ed728490308054fe357a9f49cf67f80f9721f44cc57235129e090f4" diff --git a/contracts/katana_dev.toml b/contracts/katana_dev.toml new file mode 100644 index 0000000..91b3325 --- /dev/null +++ b/contracts/katana_dev.toml @@ -0,0 +1,9 @@ +[server] +http_addr = "0.0.0.0" +http_port = 5050 +http_cors_origins = ["*"] + +[dev] +seed = "0" +dev = true +no_fee = true diff --git a/contracts/katana_slot.toml b/contracts/katana_slot.toml new file mode 100644 index 0000000..c7b5966 --- /dev/null +++ b/contracts/katana_slot.toml @@ -0,0 +1,7 @@ +[server] +http_cors_origins = ["*"] + +[dev] +seed = "0" +dev = true +no_fee = true diff --git a/contracts/manifest_slot.json b/contracts/manifest_slot.json new file mode 100644 index 0000000..9acf9bc --- /dev/null +++ b/contracts/manifest_slot.json @@ -0,0 +1,3207 @@ +{ + "world": { + "class_hash": "0x79d9ce84b97bcc2a631996c3100d57966fc2f5b061fb1ec4dfd0040976bcac6", + "address": "0x7f2e83a348505cdffc89cc89518ffc26277bffa4bd9fb225633fa2311308afa", + "seed": "arcade", + "name": "Cartridge World", + "entrypoints": [ + "uuid", + "set_metadata", + "register_namespace", + "register_event", + "register_model", + "register_contract", + "init_contract", + "upgrade_event", + "upgrade_model", + "upgrade_contract", + "emit_event", + "emit_events", + "set_entity", + "set_entities", + "delete_entity", + "delete_entities", + "grant_owner", + "revoke_owner", + "grant_writer", + "revoke_writer", + "upgrade" + ], + "abi": [ + { + "type": "impl", + "name": "World", + "interface_name": "dojo::world::iworld::IWorld" + }, + { + "type": "struct", + "name": "core::byte_array::ByteArray", + "members": [ + { + "name": "data", + "type": "core::array::Array::" + }, + { + "name": "pending_word", + "type": "core::felt252" + }, + { + "name": "pending_word_len", + "type": "core::integer::u32" + } + ] + }, + { + "type": "enum", + "name": "dojo::world::resource::Resource", + "variants": [ + { + "name": "Model", + "type": "(core::starknet::contract_address::ContractAddress, core::felt252)" + }, + { + "name": "Event", + "type": "(core::starknet::contract_address::ContractAddress, core::felt252)" + }, + { + "name": "Contract", + "type": "(core::starknet::contract_address::ContractAddress, core::felt252)" + }, + { + "name": "Namespace", + "type": "core::byte_array::ByteArray" + }, + { + "name": "World", + "type": "()" + }, + { + "name": "Unregistered", + "type": "()" + } + ] + }, + { + "type": "struct", + "name": "dojo::model::metadata::ResourceMetadata", + "members": [ + { + "name": "resource_id", + "type": "core::felt252" + }, + { + "name": "metadata_uri", + "type": "core::byte_array::ByteArray" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::>", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::>" + } + ] + }, + { + "type": "enum", + "name": "dojo::model::definition::ModelIndex", + "variants": [ + { + "name": "Keys", + "type": "core::array::Span::" + }, + { + "name": "Id", + "type": "core::felt252" + }, + { + "name": "MemberId", + "type": "(core::felt252, core::felt252)" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "dojo::meta::layout::FieldLayout", + "members": [ + { + "name": "selector", + "type": "core::felt252" + }, + { + "name": "layout", + "type": "dojo::meta::layout::Layout" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "enum", + "name": "dojo::meta::layout::Layout", + "variants": [ + { + "name": "Fixed", + "type": "core::array::Span::" + }, + { + "name": "Struct", + "type": "core::array::Span::" + }, + { + "name": "Tuple", + "type": "core::array::Span::" + }, + { + "name": "Array", + "type": "core::array::Span::" + }, + { + "name": "ByteArray", + "type": "()" + }, + { + "name": "Enum", + "type": "core::array::Span::" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "enum", + "name": "core::bool", + "variants": [ + { + "name": "False", + "type": "()" + }, + { + "name": "True", + "type": "()" + } + ] + }, + { + "type": "interface", + "name": "dojo::world::iworld::IWorld", + "items": [ + { + "type": "function", + "name": "resource", + "inputs": [ + { + "name": "selector", + "type": "core::felt252" + } + ], + "outputs": [ + { + "type": "dojo::world::resource::Resource" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "uuid", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u32" + } + ], + "state_mutability": "external" + }, + { + "type": "function", + "name": "metadata", + "inputs": [ + { + "name": "resource_selector", + "type": "core::felt252" + } + ], + "outputs": [ + { + "type": "dojo::model::metadata::ResourceMetadata" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "set_metadata", + "inputs": [ + { + "name": "metadata", + "type": "dojo::model::metadata::ResourceMetadata" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "register_namespace", + "inputs": [ + { + "name": "namespace", + "type": "core::byte_array::ByteArray" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "register_event", + "inputs": [ + { + "name": "namespace", + "type": "core::byte_array::ByteArray" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "register_model", + "inputs": [ + { + "name": "namespace", + "type": "core::byte_array::ByteArray" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "register_contract", + "inputs": [ + { + "name": "salt", + "type": "core::felt252" + }, + { + "name": "namespace", + "type": "core::byte_array::ByteArray" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [ + { + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "state_mutability": "external" + }, + { + "type": "function", + "name": "init_contract", + "inputs": [ + { + "name": "selector", + "type": "core::felt252" + }, + { + "name": "init_calldata", + "type": "core::array::Span::" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "upgrade_event", + "inputs": [ + { + "name": "namespace", + "type": "core::byte_array::ByteArray" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "upgrade_model", + "inputs": [ + { + "name": "namespace", + "type": "core::byte_array::ByteArray" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "upgrade_contract", + "inputs": [ + { + "name": "namespace", + "type": "core::byte_array::ByteArray" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [ + { + "type": "core::starknet::class_hash::ClassHash" + } + ], + "state_mutability": "external" + }, + { + "type": "function", + "name": "emit_event", + "inputs": [ + { + "name": "event_selector", + "type": "core::felt252" + }, + { + "name": "keys", + "type": "core::array::Span::" + }, + { + "name": "values", + "type": "core::array::Span::" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "emit_events", + "inputs": [ + { + "name": "event_selector", + "type": "core::felt252" + }, + { + "name": "keys", + "type": "core::array::Span::>" + }, + { + "name": "values", + "type": "core::array::Span::>" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "entity", + "inputs": [ + { + "name": "model_selector", + "type": "core::felt252" + }, + { + "name": "index", + "type": "dojo::model::definition::ModelIndex" + }, + { + "name": "layout", + "type": "dojo::meta::layout::Layout" + } + ], + "outputs": [ + { + "type": "core::array::Span::" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "entities", + "inputs": [ + { + "name": "model_selector", + "type": "core::felt252" + }, + { + "name": "indexes", + "type": "core::array::Span::" + }, + { + "name": "layout", + "type": "dojo::meta::layout::Layout" + } + ], + "outputs": [ + { + "type": "core::array::Span::>" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "set_entity", + "inputs": [ + { + "name": "model_selector", + "type": "core::felt252" + }, + { + "name": "index", + "type": "dojo::model::definition::ModelIndex" + }, + { + "name": "values", + "type": "core::array::Span::" + }, + { + "name": "layout", + "type": "dojo::meta::layout::Layout" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "set_entities", + "inputs": [ + { + "name": "model_selector", + "type": "core::felt252" + }, + { + "name": "indexes", + "type": "core::array::Span::" + }, + { + "name": "values", + "type": "core::array::Span::>" + }, + { + "name": "layout", + "type": "dojo::meta::layout::Layout" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "delete_entity", + "inputs": [ + { + "name": "model_selector", + "type": "core::felt252" + }, + { + "name": "index", + "type": "dojo::model::definition::ModelIndex" + }, + { + "name": "layout", + "type": "dojo::meta::layout::Layout" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "delete_entities", + "inputs": [ + { + "name": "model_selector", + "type": "core::felt252" + }, + { + "name": "indexes", + "type": "core::array::Span::" + }, + { + "name": "layout", + "type": "dojo::meta::layout::Layout" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "is_owner", + "inputs": [ + { + "name": "resource", + "type": "core::felt252" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "grant_owner", + "inputs": [ + { + "name": "resource", + "type": "core::felt252" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "revoke_owner", + "inputs": [ + { + "name": "resource", + "type": "core::felt252" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "is_writer", + "inputs": [ + { + "name": "resource", + "type": "core::felt252" + }, + { + "name": "contract", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "grant_writer", + "inputs": [ + { + "name": "resource", + "type": "core::felt252" + }, + { + "name": "contract", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "revoke_writer", + "inputs": [ + { + "name": "resource", + "type": "core::felt252" + }, + { + "name": "contract", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "impl", + "name": "UpgradeableWorld", + "interface_name": "dojo::world::iworld::IUpgradeableWorld" + }, + { + "type": "interface", + "name": "dojo::world::iworld::IUpgradeableWorld", + "items": [ + { + "type": "function", + "name": "upgrade", + "inputs": [ + { + "name": "new_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "constructor", + "name": "constructor", + "inputs": [ + { + "name": "world_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::WorldSpawned", + "kind": "struct", + "members": [ + { + "name": "creator", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::WorldUpgraded", + "kind": "struct", + "members": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::NamespaceRegistered", + "kind": "struct", + "members": [ + { + "name": "namespace", + "type": "core::byte_array::ByteArray", + "kind": "key" + }, + { + "name": "hash", + "type": "core::felt252", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::ModelRegistered", + "kind": "struct", + "members": [ + { + "name": "name", + "type": "core::byte_array::ByteArray", + "kind": "key" + }, + { + "name": "namespace", + "type": "core::byte_array::ByteArray", + "kind": "key" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::EventRegistered", + "kind": "struct", + "members": [ + { + "name": "name", + "type": "core::byte_array::ByteArray", + "kind": "key" + }, + { + "name": "namespace", + "type": "core::byte_array::ByteArray", + "kind": "key" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::ContractRegistered", + "kind": "struct", + "members": [ + { + "name": "name", + "type": "core::byte_array::ByteArray", + "kind": "key" + }, + { + "name": "namespace", + "type": "core::byte_array::ByteArray", + "kind": "key" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + }, + { + "name": "salt", + "type": "core::felt252", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::ModelUpgraded", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + }, + { + "name": "prev_address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::EventUpgraded", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + }, + { + "name": "prev_address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::ContractUpgraded", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::ContractInitialized", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "init_calldata", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::EventEmitted", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "system_address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + }, + { + "name": "keys", + "type": "core::array::Span::", + "kind": "data" + }, + { + "name": "values", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::MetadataUpdate", + "kind": "struct", + "members": [ + { + "name": "resource", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "uri", + "type": "core::byte_array::ByteArray", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::StoreSetRecord", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "entity_id", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "keys", + "type": "core::array::Span::", + "kind": "data" + }, + { + "name": "values", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::StoreUpdateRecord", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "entity_id", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "values", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::StoreUpdateMember", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "entity_id", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "member_selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "values", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::StoreDelRecord", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "entity_id", + "type": "core::felt252", + "kind": "key" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::WriterUpdated", + "kind": "struct", + "members": [ + { + "name": "resource", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "contract", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + }, + { + "name": "value", + "type": "core::bool", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::OwnerUpdated", + "kind": "struct", + "members": [ + { + "name": "resource", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "contract", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + }, + { + "name": "value", + "type": "core::bool", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::Event", + "kind": "enum", + "variants": [ + { + "name": "WorldSpawned", + "type": "dojo::world::world_contract::world::WorldSpawned", + "kind": "nested" + }, + { + "name": "WorldUpgraded", + "type": "dojo::world::world_contract::world::WorldUpgraded", + "kind": "nested" + }, + { + "name": "NamespaceRegistered", + "type": "dojo::world::world_contract::world::NamespaceRegistered", + "kind": "nested" + }, + { + "name": "ModelRegistered", + "type": "dojo::world::world_contract::world::ModelRegistered", + "kind": "nested" + }, + { + "name": "EventRegistered", + "type": "dojo::world::world_contract::world::EventRegistered", + "kind": "nested" + }, + { + "name": "ContractRegistered", + "type": "dojo::world::world_contract::world::ContractRegistered", + "kind": "nested" + }, + { + "name": "ModelUpgraded", + "type": "dojo::world::world_contract::world::ModelUpgraded", + "kind": "nested" + }, + { + "name": "EventUpgraded", + "type": "dojo::world::world_contract::world::EventUpgraded", + "kind": "nested" + }, + { + "name": "ContractUpgraded", + "type": "dojo::world::world_contract::world::ContractUpgraded", + "kind": "nested" + }, + { + "name": "ContractInitialized", + "type": "dojo::world::world_contract::world::ContractInitialized", + "kind": "nested" + }, + { + "name": "EventEmitted", + "type": "dojo::world::world_contract::world::EventEmitted", + "kind": "nested" + }, + { + "name": "MetadataUpdate", + "type": "dojo::world::world_contract::world::MetadataUpdate", + "kind": "nested" + }, + { + "name": "StoreSetRecord", + "type": "dojo::world::world_contract::world::StoreSetRecord", + "kind": "nested" + }, + { + "name": "StoreUpdateRecord", + "type": "dojo::world::world_contract::world::StoreUpdateRecord", + "kind": "nested" + }, + { + "name": "StoreUpdateMember", + "type": "dojo::world::world_contract::world::StoreUpdateMember", + "kind": "nested" + }, + { + "name": "StoreDelRecord", + "type": "dojo::world::world_contract::world::StoreDelRecord", + "kind": "nested" + }, + { + "name": "WriterUpdated", + "type": "dojo::world::world_contract::world::WriterUpdated", + "kind": "nested" + }, + { + "name": "OwnerUpdated", + "type": "dojo::world::world_contract::world::OwnerUpdated", + "kind": "nested" + } + ] + } + ] + }, + "contracts": [ + { + "address": "0x490e67f5aed3498e7c5b3ff1614f90e6ea33b95b9924878278f8a168bedc06a", + "class_hash": "0x1411c001996f2ba10ce3953cd56ac90ace486dafd746cde530888514ee88825", + "abi": [ + { + "type": "impl", + "name": "Pinner__ContractImpl", + "interface_name": "dojo::contract::interface::IContract" + }, + { + "type": "interface", + "name": "dojo::contract::interface::IContract", + "items": [] + }, + { + "type": "impl", + "name": "Pinner__DeployedContractImpl", + "interface_name": "dojo::meta::interface::IDeployedResource" + }, + { + "type": "struct", + "name": "core::byte_array::ByteArray", + "members": [ + { + "name": "data", + "type": "core::array::Array::" + }, + { + "name": "pending_word", + "type": "core::felt252" + }, + { + "name": "pending_word_len", + "type": "core::integer::u32" + } + ] + }, + { + "type": "interface", + "name": "dojo::meta::interface::IDeployedResource", + "items": [ + { + "type": "function", + "name": "dojo_name", + "inputs": [], + "outputs": [ + { + "type": "core::byte_array::ByteArray" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "PinnerImpl", + "interface_name": "arcade::systems::pinner::IPinner" + }, + { + "type": "interface", + "name": "arcade::systems::pinner::IPinner", + "items": [ + { + "type": "function", + "name": "pin", + "inputs": [ + { + "name": "achievement_id", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "unpin", + "inputs": [ + { + "name": "achievement_id", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + } + ] + }, + { + "type": "function", + "name": "dojo_init", + "inputs": [], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "impl", + "name": "WorldProviderImpl", + "interface_name": "dojo::contract::components::world_provider::IWorldProvider" + }, + { + "type": "struct", + "name": "dojo::world::iworld::IWorldDispatcher", + "members": [ + { + "name": "contract_address", + "type": "core::starknet::contract_address::ContractAddress" + } + ] + }, + { + "type": "interface", + "name": "dojo::contract::components::world_provider::IWorldProvider", + "items": [ + { + "type": "function", + "name": "world_dispatcher", + "inputs": [], + "outputs": [ + { + "type": "dojo::world::iworld::IWorldDispatcher" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "UpgradeableImpl", + "interface_name": "dojo::contract::components::upgradeable::IUpgradeable" + }, + { + "type": "interface", + "name": "dojo::contract::components::upgradeable::IUpgradeable", + "items": [ + { + "type": "function", + "name": "upgrade", + "inputs": [ + { + "name": "new_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "constructor", + "name": "constructor", + "inputs": [] + }, + { + "type": "event", + "name": "dojo::contract::components::upgradeable::upgradeable_cpt::Upgraded", + "kind": "struct", + "members": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::contract::components::upgradeable::upgradeable_cpt::Event", + "kind": "enum", + "variants": [ + { + "name": "Upgraded", + "type": "dojo::contract::components::upgradeable::upgradeable_cpt::Upgraded", + "kind": "nested" + } + ] + }, + { + "type": "event", + "name": "dojo::contract::components::world_provider::world_provider_cpt::Event", + "kind": "enum", + "variants": [] + }, + { + "type": "event", + "name": "achievement::components::pinnable::PinnableComponent::Event", + "kind": "enum", + "variants": [] + }, + { + "type": "event", + "name": "arcade::systems::pinner::Pinner::Event", + "kind": "enum", + "variants": [ + { + "name": "UpgradeableEvent", + "type": "dojo::contract::components::upgradeable::upgradeable_cpt::Event", + "kind": "nested" + }, + { + "name": "WorldProviderEvent", + "type": "dojo::contract::components::world_provider::world_provider_cpt::Event", + "kind": "nested" + }, + { + "name": "PinnableEvent", + "type": "achievement::components::pinnable::PinnableComponent::Event", + "kind": "flat" + } + ] + } + ], + "init_calldata": [], + "tag": "ARCADE-Pinner", + "selector": "0x31f65cff5a1a8ec1430aaf76958f8f3e621846763aa047828df543ee3feec93", + "systems": [ + "upgrade" + ] + }, + { + "address": "0x10eaf49256a4a7a43f84e9f3b04a18ba7776d0b9ba3c24a643ed1ebdc16b20", + "class_hash": "0x13e69ce8dcc6f595e55cff97d8845598310799b11eae9b8e82e6bbf90375afd", + "abi": [ + { + "type": "impl", + "name": "Registry__ContractImpl", + "interface_name": "dojo::contract::interface::IContract" + }, + { + "type": "interface", + "name": "dojo::contract::interface::IContract", + "items": [] + }, + { + "type": "impl", + "name": "Registry__DeployedContractImpl", + "interface_name": "dojo::meta::interface::IDeployedResource" + }, + { + "type": "struct", + "name": "core::byte_array::ByteArray", + "members": [ + { + "name": "data", + "type": "core::array::Array::" + }, + { + "name": "pending_word", + "type": "core::felt252" + }, + { + "name": "pending_word_len", + "type": "core::integer::u32" + } + ] + }, + { + "type": "interface", + "name": "dojo::meta::interface::IDeployedResource", + "items": [ + { + "type": "function", + "name": "dojo_name", + "inputs": [], + "outputs": [ + { + "type": "core::byte_array::ByteArray" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "function", + "name": "dojo_init", + "inputs": [ + { + "name": "owner", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "impl", + "name": "RegistryImpl", + "interface_name": "arcade::systems::registry::IRegistry" + }, + { + "type": "enum", + "name": "core::option::Option::", + "variants": [ + { + "name": "Some", + "type": "core::felt252" + }, + { + "name": "None", + "type": "()" + } + ] + }, + { + "type": "enum", + "name": "core::option::Option::", + "variants": [ + { + "name": "Some", + "type": "core::byte_array::ByteArray" + }, + { + "name": "None", + "type": "()" + } + ] + }, + { + "type": "interface", + "name": "arcade::systems::registry::IRegistry", + "items": [ + { + "type": "function", + "name": "register_game", + "inputs": [ + { + "name": "world_address", + "type": "core::felt252" + }, + { + "name": "namespace", + "type": "core::felt252" + }, + { + "name": "project", + "type": "core::felt252" + }, + { + "name": "color", + "type": "core::option::Option::" + }, + { + "name": "name", + "type": "core::option::Option::" + }, + { + "name": "description", + "type": "core::option::Option::" + }, + { + "name": "image", + "type": "core::option::Option::" + }, + { + "name": "banner", + "type": "core::option::Option::" + }, + { + "name": "discord", + "type": "core::option::Option::" + }, + { + "name": "telegram", + "type": "core::option::Option::" + }, + { + "name": "twitter", + "type": "core::option::Option::" + }, + { + "name": "youtube", + "type": "core::option::Option::" + }, + { + "name": "website", + "type": "core::option::Option::" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "update_game", + "inputs": [ + { + "name": "world_address", + "type": "core::felt252" + }, + { + "name": "namespace", + "type": "core::felt252" + }, + { + "name": "color", + "type": "core::option::Option::" + }, + { + "name": "name", + "type": "core::option::Option::" + }, + { + "name": "description", + "type": "core::option::Option::" + }, + { + "name": "image", + "type": "core::option::Option::" + }, + { + "name": "banner", + "type": "core::option::Option::" + }, + { + "name": "discord", + "type": "core::option::Option::" + }, + { + "name": "telegram", + "type": "core::option::Option::" + }, + { + "name": "twitter", + "type": "core::option::Option::" + }, + { + "name": "youtube", + "type": "core::option::Option::" + }, + { + "name": "website", + "type": "core::option::Option::" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "publish_game", + "inputs": [ + { + "name": "world_address", + "type": "core::felt252" + }, + { + "name": "namespace", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "hide_game", + "inputs": [ + { + "name": "world_address", + "type": "core::felt252" + }, + { + "name": "namespace", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "whitelist_game", + "inputs": [ + { + "name": "world_address", + "type": "core::felt252" + }, + { + "name": "namespace", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "blacklist_game", + "inputs": [ + { + "name": "world_address", + "type": "core::felt252" + }, + { + "name": "namespace", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "remove_game", + "inputs": [ + { + "name": "world_address", + "type": "core::felt252" + }, + { + "name": "namespace", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "register_achievement", + "inputs": [ + { + "name": "world_address", + "type": "core::felt252" + }, + { + "name": "namespace", + "type": "core::felt252" + }, + { + "name": "identifier", + "type": "core::felt252" + }, + { + "name": "karma", + "type": "core::integer::u16" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "update_achievement", + "inputs": [ + { + "name": "world_address", + "type": "core::felt252" + }, + { + "name": "namespace", + "type": "core::felt252" + }, + { + "name": "identifier", + "type": "core::felt252" + }, + { + "name": "karma", + "type": "core::integer::u16" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "publish_achievement", + "inputs": [ + { + "name": "world_address", + "type": "core::felt252" + }, + { + "name": "namespace", + "type": "core::felt252" + }, + { + "name": "identifier", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "hide_achievement", + "inputs": [ + { + "name": "world_address", + "type": "core::felt252" + }, + { + "name": "namespace", + "type": "core::felt252" + }, + { + "name": "identifier", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "whitelist_achievement", + "inputs": [ + { + "name": "world_address", + "type": "core::felt252" + }, + { + "name": "namespace", + "type": "core::felt252" + }, + { + "name": "identifier", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "blacklist_achievement", + "inputs": [ + { + "name": "world_address", + "type": "core::felt252" + }, + { + "name": "namespace", + "type": "core::felt252" + }, + { + "name": "identifier", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "remove_achievement", + "inputs": [ + { + "name": "world_address", + "type": "core::felt252" + }, + { + "name": "namespace", + "type": "core::felt252" + }, + { + "name": "identifier", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "WorldProviderImpl", + "interface_name": "dojo::contract::components::world_provider::IWorldProvider" + }, + { + "type": "struct", + "name": "dojo::world::iworld::IWorldDispatcher", + "members": [ + { + "name": "contract_address", + "type": "core::starknet::contract_address::ContractAddress" + } + ] + }, + { + "type": "interface", + "name": "dojo::contract::components::world_provider::IWorldProvider", + "items": [ + { + "type": "function", + "name": "world_dispatcher", + "inputs": [], + "outputs": [ + { + "type": "dojo::world::iworld::IWorldDispatcher" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "UpgradeableImpl", + "interface_name": "dojo::contract::components::upgradeable::IUpgradeable" + }, + { + "type": "interface", + "name": "dojo::contract::components::upgradeable::IUpgradeable", + "items": [ + { + "type": "function", + "name": "upgrade", + "inputs": [ + { + "name": "new_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "constructor", + "name": "constructor", + "inputs": [] + }, + { + "type": "event", + "name": "dojo::contract::components::upgradeable::upgradeable_cpt::Upgraded", + "kind": "struct", + "members": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::contract::components::upgradeable::upgradeable_cpt::Event", + "kind": "enum", + "variants": [ + { + "name": "Upgraded", + "type": "dojo::contract::components::upgradeable::upgradeable_cpt::Upgraded", + "kind": "nested" + } + ] + }, + { + "type": "event", + "name": "dojo::contract::components::world_provider::world_provider_cpt::Event", + "kind": "enum", + "variants": [] + }, + { + "type": "event", + "name": "registry::components::initializable::InitializableComponent::Event", + "kind": "enum", + "variants": [] + }, + { + "type": "event", + "name": "registry::components::registerable::RegisterableComponent::Event", + "kind": "enum", + "variants": [] + }, + { + "type": "event", + "name": "registry::components::trackable::TrackableComponent::Event", + "kind": "enum", + "variants": [] + }, + { + "type": "event", + "name": "arcade::systems::registry::Registry::Event", + "kind": "enum", + "variants": [ + { + "name": "UpgradeableEvent", + "type": "dojo::contract::components::upgradeable::upgradeable_cpt::Event", + "kind": "nested" + }, + { + "name": "WorldProviderEvent", + "type": "dojo::contract::components::world_provider::world_provider_cpt::Event", + "kind": "nested" + }, + { + "name": "InitializableEvent", + "type": "registry::components::initializable::InitializableComponent::Event", + "kind": "flat" + }, + { + "name": "RegisterableEvent", + "type": "registry::components::registerable::RegisterableComponent::Event", + "kind": "flat" + }, + { + "name": "TrackableEvent", + "type": "registry::components::trackable::TrackableComponent::Event", + "kind": "flat" + } + ] + } + ], + "init_calldata": [ + "0x6677fe62ee39c7b07401f754138502bab7fac99d2d3c5d37df7d1c6fab10819" + ], + "tag": "ARCADE-Registry", + "selector": "0x54d3bcd441104e039ceaec4a413e72de393b65f79d1df74dc3346dc7f861173", + "systems": [ + "upgrade" + ] + }, + { + "address": "0x5658b002b30df4f33715936e72ab95bf881399d3fe1e0e24656fc1aa26991c1", + "class_hash": "0xeff274bcfc8782f81351d52a6c0809135c315159815019ffff5122410cf19f", + "abi": [ + { + "type": "impl", + "name": "Slot__ContractImpl", + "interface_name": "dojo::contract::interface::IContract" + }, + { + "type": "interface", + "name": "dojo::contract::interface::IContract", + "items": [] + }, + { + "type": "impl", + "name": "Slot__DeployedContractImpl", + "interface_name": "dojo::meta::interface::IDeployedResource" + }, + { + "type": "struct", + "name": "core::byte_array::ByteArray", + "members": [ + { + "name": "data", + "type": "core::array::Array::" + }, + { + "name": "pending_word", + "type": "core::felt252" + }, + { + "name": "pending_word_len", + "type": "core::integer::u32" + } + ] + }, + { + "type": "interface", + "name": "dojo::meta::interface::IDeployedResource", + "items": [ + { + "type": "function", + "name": "dojo_name", + "inputs": [], + "outputs": [ + { + "type": "core::byte_array::ByteArray" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "function", + "name": "dojo_init", + "inputs": [], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "impl", + "name": "SlotImpl", + "interface_name": "arcade::systems::slot::ISlot" + }, + { + "type": "interface", + "name": "arcade::systems::slot::ISlot", + "items": [ + { + "type": "function", + "name": "deploy", + "inputs": [ + { + "name": "service", + "type": "core::integer::u8" + }, + { + "name": "project", + "type": "core::felt252" + }, + { + "name": "tier", + "type": "core::integer::u8" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "remove", + "inputs": [ + { + "name": "service", + "type": "core::integer::u8" + }, + { + "name": "project", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "hire", + "inputs": [ + { + "name": "project", + "type": "core::felt252" + }, + { + "name": "account_id", + "type": "core::felt252" + }, + { + "name": "role", + "type": "core::integer::u8" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "fire", + "inputs": [ + { + "name": "project", + "type": "core::felt252" + }, + { + "name": "account_id", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "WorldProviderImpl", + "interface_name": "dojo::contract::components::world_provider::IWorldProvider" + }, + { + "type": "struct", + "name": "dojo::world::iworld::IWorldDispatcher", + "members": [ + { + "name": "contract_address", + "type": "core::starknet::contract_address::ContractAddress" + } + ] + }, + { + "type": "interface", + "name": "dojo::contract::components::world_provider::IWorldProvider", + "items": [ + { + "type": "function", + "name": "world_dispatcher", + "inputs": [], + "outputs": [ + { + "type": "dojo::world::iworld::IWorldDispatcher" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "UpgradeableImpl", + "interface_name": "dojo::contract::components::upgradeable::IUpgradeable" + }, + { + "type": "interface", + "name": "dojo::contract::components::upgradeable::IUpgradeable", + "items": [ + { + "type": "function", + "name": "upgrade", + "inputs": [ + { + "name": "new_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "constructor", + "name": "constructor", + "inputs": [] + }, + { + "type": "event", + "name": "dojo::contract::components::upgradeable::upgradeable_cpt::Upgraded", + "kind": "struct", + "members": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::contract::components::upgradeable::upgradeable_cpt::Event", + "kind": "enum", + "variants": [ + { + "name": "Upgraded", + "type": "dojo::contract::components::upgradeable::upgradeable_cpt::Upgraded", + "kind": "nested" + } + ] + }, + { + "type": "event", + "name": "dojo::contract::components::world_provider::world_provider_cpt::Event", + "kind": "enum", + "variants": [] + }, + { + "type": "event", + "name": "provider::components::deployable::DeployableComponent::Event", + "kind": "enum", + "variants": [] + }, + { + "type": "event", + "name": "provider::components::groupable::GroupableComponent::Event", + "kind": "enum", + "variants": [] + }, + { + "type": "event", + "name": "arcade::systems::slot::Slot::Event", + "kind": "enum", + "variants": [ + { + "name": "UpgradeableEvent", + "type": "dojo::contract::components::upgradeable::upgradeable_cpt::Event", + "kind": "nested" + }, + { + "name": "WorldProviderEvent", + "type": "dojo::contract::components::world_provider::world_provider_cpt::Event", + "kind": "nested" + }, + { + "name": "DeployableEvent", + "type": "provider::components::deployable::DeployableComponent::Event", + "kind": "flat" + }, + { + "name": "GroupableEvent", + "type": "provider::components::groupable::GroupableComponent::Event", + "kind": "flat" + } + ] + } + ], + "init_calldata": [], + "tag": "ARCADE-Slot", + "selector": "0x16361cb59732e8b56d69550297b7d8f86c6d1be2fc71b5dde135aeb1d16f3f7", + "systems": [ + "upgrade" + ] + }, + { + "address": "0x7e02509b8033a924f2fd06434a0fb08337b847ce035c987dfd2abb77856f438", + "class_hash": "0x352e56826f28843dde3c5814efe4ac5098e3514d59a28a14315e18280db6afe", + "abi": [ + { + "type": "impl", + "name": "Society__ContractImpl", + "interface_name": "dojo::contract::interface::IContract" + }, + { + "type": "interface", + "name": "dojo::contract::interface::IContract", + "items": [] + }, + { + "type": "impl", + "name": "Society__DeployedContractImpl", + "interface_name": "dojo::meta::interface::IDeployedResource" + }, + { + "type": "struct", + "name": "core::byte_array::ByteArray", + "members": [ + { + "name": "data", + "type": "core::array::Array::" + }, + { + "name": "pending_word", + "type": "core::felt252" + }, + { + "name": "pending_word_len", + "type": "core::integer::u32" + } + ] + }, + { + "type": "interface", + "name": "dojo::meta::interface::IDeployedResource", + "items": [ + { + "type": "function", + "name": "dojo_name", + "inputs": [], + "outputs": [ + { + "type": "core::byte_array::ByteArray" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "SocietyImpl", + "interface_name": "arcade::systems::society::ISociety" + }, + { + "type": "enum", + "name": "core::option::Option::", + "variants": [ + { + "name": "Some", + "type": "core::felt252" + }, + { + "name": "None", + "type": "()" + } + ] + }, + { + "type": "enum", + "name": "core::option::Option::", + "variants": [ + { + "name": "Some", + "type": "core::byte_array::ByteArray" + }, + { + "name": "None", + "type": "()" + } + ] + }, + { + "type": "enum", + "name": "core::bool", + "variants": [ + { + "name": "False", + "type": "()" + }, + { + "name": "True", + "type": "()" + } + ] + }, + { + "type": "interface", + "name": "arcade::systems::society::ISociety", + "items": [ + { + "type": "function", + "name": "follow", + "inputs": [ + { + "name": "target", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "unfollow", + "inputs": [ + { + "name": "target", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "create_alliance", + "inputs": [ + { + "name": "color", + "type": "core::option::Option::" + }, + { + "name": "name", + "type": "core::option::Option::" + }, + { + "name": "description", + "type": "core::option::Option::" + }, + { + "name": "image", + "type": "core::option::Option::" + }, + { + "name": "banner", + "type": "core::option::Option::" + }, + { + "name": "discord", + "type": "core::option::Option::" + }, + { + "name": "telegram", + "type": "core::option::Option::" + }, + { + "name": "twitter", + "type": "core::option::Option::" + }, + { + "name": "youtube", + "type": "core::option::Option::" + }, + { + "name": "website", + "type": "core::option::Option::" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "open_alliance", + "inputs": [ + { + "name": "free", + "type": "core::bool" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "close_alliance", + "inputs": [], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "crown_guild", + "inputs": [ + { + "name": "guild_id", + "type": "core::integer::u32" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "hire_guild", + "inputs": [ + { + "name": "guild_id", + "type": "core::integer::u32" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "fire_guild", + "inputs": [ + { + "name": "guild_id", + "type": "core::integer::u32" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "request_alliance", + "inputs": [ + { + "name": "alliance_id", + "type": "core::integer::u32" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "cancel_alliance", + "inputs": [], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "leave_alliance", + "inputs": [], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "create_guild", + "inputs": [ + { + "name": "color", + "type": "core::option::Option::" + }, + { + "name": "name", + "type": "core::option::Option::" + }, + { + "name": "description", + "type": "core::option::Option::" + }, + { + "name": "image", + "type": "core::option::Option::" + }, + { + "name": "banner", + "type": "core::option::Option::" + }, + { + "name": "discord", + "type": "core::option::Option::" + }, + { + "name": "telegram", + "type": "core::option::Option::" + }, + { + "name": "twitter", + "type": "core::option::Option::" + }, + { + "name": "youtube", + "type": "core::option::Option::" + }, + { + "name": "website", + "type": "core::option::Option::" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "open_guild", + "inputs": [ + { + "name": "free", + "type": "core::bool" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "close_guild", + "inputs": [], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "crown_member", + "inputs": [ + { + "name": "member_id", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "promote_member", + "inputs": [ + { + "name": "member_id", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "demote_member", + "inputs": [ + { + "name": "member_id", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "hire_member", + "inputs": [ + { + "name": "member_id", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "fire_member", + "inputs": [ + { + "name": "member_id", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "request_guild", + "inputs": [ + { + "name": "guild_id", + "type": "core::integer::u32" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "cancel_guild", + "inputs": [], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "leave_guild", + "inputs": [], + "outputs": [], + "state_mutability": "view" + } + ] + }, + { + "type": "function", + "name": "dojo_init", + "inputs": [], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "impl", + "name": "WorldProviderImpl", + "interface_name": "dojo::contract::components::world_provider::IWorldProvider" + }, + { + "type": "struct", + "name": "dojo::world::iworld::IWorldDispatcher", + "members": [ + { + "name": "contract_address", + "type": "core::starknet::contract_address::ContractAddress" + } + ] + }, + { + "type": "interface", + "name": "dojo::contract::components::world_provider::IWorldProvider", + "items": [ + { + "type": "function", + "name": "world_dispatcher", + "inputs": [], + "outputs": [ + { + "type": "dojo::world::iworld::IWorldDispatcher" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "UpgradeableImpl", + "interface_name": "dojo::contract::components::upgradeable::IUpgradeable" + }, + { + "type": "interface", + "name": "dojo::contract::components::upgradeable::IUpgradeable", + "items": [ + { + "type": "function", + "name": "upgrade", + "inputs": [ + { + "name": "new_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "constructor", + "name": "constructor", + "inputs": [] + }, + { + "type": "event", + "name": "dojo::contract::components::upgradeable::upgradeable_cpt::Upgraded", + "kind": "struct", + "members": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::contract::components::upgradeable::upgradeable_cpt::Event", + "kind": "enum", + "variants": [ + { + "name": "Upgraded", + "type": "dojo::contract::components::upgradeable::upgradeable_cpt::Upgraded", + "kind": "nested" + } + ] + }, + { + "type": "event", + "name": "dojo::contract::components::world_provider::world_provider_cpt::Event", + "kind": "enum", + "variants": [] + }, + { + "type": "event", + "name": "society::components::allianceable::AllianceableComponent::Event", + "kind": "enum", + "variants": [] + }, + { + "type": "event", + "name": "society::components::followable::FollowableComponent::Event", + "kind": "enum", + "variants": [] + }, + { + "type": "event", + "name": "society::components::guildable::GuildableComponent::Event", + "kind": "enum", + "variants": [] + }, + { + "type": "event", + "name": "arcade::systems::society::Society::Event", + "kind": "enum", + "variants": [ + { + "name": "UpgradeableEvent", + "type": "dojo::contract::components::upgradeable::upgradeable_cpt::Event", + "kind": "nested" + }, + { + "name": "WorldProviderEvent", + "type": "dojo::contract::components::world_provider::world_provider_cpt::Event", + "kind": "nested" + }, + { + "name": "AllianceableEvent", + "type": "society::components::allianceable::AllianceableComponent::Event", + "kind": "flat" + }, + { + "name": "FollowableEvent", + "type": "society::components::followable::FollowableComponent::Event", + "kind": "flat" + }, + { + "name": "GuildableEvent", + "type": "society::components::guildable::GuildableComponent::Event", + "kind": "flat" + } + ] + } + ], + "init_calldata": [], + "tag": "ARCADE-Society", + "selector": "0x11d3443ddda56afc016a818156b817be5668888312fafd0e31705f1120eddc1", + "systems": [ + "upgrade" + ] + }, + { + "address": "0x73d2533f3cc0d79a0096cfa127c76fa5592bd0416bef89841a1dbb6816db026", + "class_hash": "0x580ba5d9a580a061630f436f0565dc5bb048f5daee84d79741fda5ce765c2f", + "abi": [ + { + "type": "impl", + "name": "Wallet__ContractImpl", + "interface_name": "dojo::contract::interface::IContract" + }, + { + "type": "interface", + "name": "dojo::contract::interface::IContract", + "items": [] + }, + { + "type": "impl", + "name": "Wallet__DeployedContractImpl", + "interface_name": "dojo::meta::interface::IDeployedResource" + }, + { + "type": "struct", + "name": "core::byte_array::ByteArray", + "members": [ + { + "name": "data", + "type": "core::array::Array::" + }, + { + "name": "pending_word", + "type": "core::felt252" + }, + { + "name": "pending_word_len", + "type": "core::integer::u32" + } + ] + }, + { + "type": "interface", + "name": "dojo::meta::interface::IDeployedResource", + "items": [ + { + "type": "function", + "name": "dojo_name", + "inputs": [], + "outputs": [ + { + "type": "core::byte_array::ByteArray" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "WalletImpl", + "interface_name": "arcade::systems::wallet::IWallet" + }, + { + "type": "interface", + "name": "arcade::systems::wallet::IWallet", + "items": [] + }, + { + "type": "function", + "name": "dojo_init", + "inputs": [], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "impl", + "name": "WorldProviderImpl", + "interface_name": "dojo::contract::components::world_provider::IWorldProvider" + }, + { + "type": "struct", + "name": "dojo::world::iworld::IWorldDispatcher", + "members": [ + { + "name": "contract_address", + "type": "core::starknet::contract_address::ContractAddress" + } + ] + }, + { + "type": "interface", + "name": "dojo::contract::components::world_provider::IWorldProvider", + "items": [ + { + "type": "function", + "name": "world_dispatcher", + "inputs": [], + "outputs": [ + { + "type": "dojo::world::iworld::IWorldDispatcher" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "UpgradeableImpl", + "interface_name": "dojo::contract::components::upgradeable::IUpgradeable" + }, + { + "type": "interface", + "name": "dojo::contract::components::upgradeable::IUpgradeable", + "items": [ + { + "type": "function", + "name": "upgrade", + "inputs": [ + { + "name": "new_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "constructor", + "name": "constructor", + "inputs": [] + }, + { + "type": "event", + "name": "dojo::contract::components::upgradeable::upgradeable_cpt::Upgraded", + "kind": "struct", + "members": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::contract::components::upgradeable::upgradeable_cpt::Event", + "kind": "enum", + "variants": [ + { + "name": "Upgraded", + "type": "dojo::contract::components::upgradeable::upgradeable_cpt::Upgraded", + "kind": "nested" + } + ] + }, + { + "type": "event", + "name": "dojo::contract::components::world_provider::world_provider_cpt::Event", + "kind": "enum", + "variants": [] + }, + { + "type": "event", + "name": "controller::components::controllable::ControllableComponent::Event", + "kind": "enum", + "variants": [] + }, + { + "type": "event", + "name": "arcade::systems::wallet::Wallet::Event", + "kind": "enum", + "variants": [ + { + "name": "UpgradeableEvent", + "type": "dojo::contract::components::upgradeable::upgradeable_cpt::Event", + "kind": "nested" + }, + { + "name": "WorldProviderEvent", + "type": "dojo::contract::components::world_provider::world_provider_cpt::Event", + "kind": "nested" + }, + { + "name": "ControllableEvent", + "type": "controller::components::controllable::ControllableComponent::Event", + "kind": "flat" + } + ] + } + ], + "init_calldata": [], + "tag": "ARCADE-Wallet", + "selector": "0x69ba9525fdd6458dfaea7f06bcfd0cf5fef14d66c5516cf8548c2e2b845c6a5", + "systems": [ + "upgrade" + ] + } + ], + "models": [ + { + "members": [], + "class_hash": "0x4e2a4a65e9597fae6a3db15dbf8360cbb90ac4b00803eb24715b0d3c6b62867", + "tag": "ARCADE-Access", + "selector": "0x4de83c9f22c74953e76afcef63ce27a77e04d2304133da1ec46fa2e46a0c40f" + }, + { + "members": [], + "class_hash": "0x4e923487edecb8caba2730b7eb7ada85e31c9a3ab2bc6b865cb7a7723d7d4eb", + "tag": "ARCADE-Account", + "selector": "0x7e96bc903044d45737e5ec7e23b596e8e7f110a1b1f80d413b53725c7bbb2f6" + }, + { + "members": [], + "class_hash": "0x509b4c7c85a5ad6ede5f70f838fe3b039e09fe9e1537e522e562a8c2fa887b4", + "tag": "ARCADE-Achievement", + "selector": "0x4909446396e41e99e20f04d9f5d9e83ab83beea6089c76d0fef29b034de9736" + }, + { + "members": [], + "class_hash": "0x465212d5a289fbf8bf5eb122866b7a41dcfe1e369bca5a90bc9657cfb849676", + "tag": "ARCADE-Alliance", + "selector": "0x74a88ab0bed983c65d7e57761329312c125ef0be4ef7889f560153000132866" + }, + { + "members": [], + "class_hash": "0x8d4d1e78893b9b0e541eb5e20913057e7f70acd6e0302d9a8357c594db1015", + "tag": "ARCADE-Controller", + "selector": "0x7b2fac00792560d241723d9852f29e952bb0ecc88219dd3fb86b61796dc5952" + }, + { + "members": [], + "class_hash": "0x707deea7afe8c277a3de09d5ccb124bf1f727ea61f0bcb618c5e7f2de4c2d5f", + "tag": "ARCADE-Deployment", + "selector": "0x5354f17394d652912bae10be363d24d155edbdb936fa275f855491253cb63a4" + }, + { + "members": [], + "class_hash": "0x46ee9af02075375a761b271a5fb5896bf34f7040f35d3f4d2793006f2db5e37", + "tag": "ARCADE-Factory", + "selector": "0x59995d7c14b165cb6738a67e319c6ad553a58d9c05f87f0c35190b13e1c223" + }, + { + "members": [], + "class_hash": "0x2fd5d2cccf18fcf8c974292188bd6fef67c7c0ea20029e3c408e78d786b0a2e", + "tag": "ARCADE-Game", + "selector": "0x6143bc86ed1a08df992c568392c454a92ef7e7b5ba08e9bf75643cf5cfc8b14" + }, + { + "members": [], + "class_hash": "0x70ef31c81bda95eea90a15e2156deea23537ad9d13fb29cfbafae65ea9bd5b8", + "tag": "ARCADE-Guild", + "selector": "0x95501f151f575b4fab06c5ceb7237739dd0608982727cbc226273aecf006aa" + }, + { + "members": [], + "class_hash": "0x7b293a8785e51acba3c0eb8ed8356bbd40700d7f5b55e8fe8d3b8de613b512a", + "tag": "ARCADE-Member", + "selector": "0x7b9b4b91d2d7823ac5c041c32867f856e6393905bedb2c4b7f58d56bf13ec43" + }, + { + "members": [], + "class_hash": "0x693b5887e2b62bea0163daae7ecfc98e02aa1c32469ccb4d831de4bc19ab719", + "tag": "ARCADE-Signer", + "selector": "0x79493096b3a4188aade984eaf8272a97748ee48111c1f7e6683a89f64406c1a" + }, + { + "members": [], + "class_hash": "0x398bdccbc7f8450bb139af04a99a0fddd8367b3bd21202095ec1df96108df98", + "tag": "ARCADE-Team", + "selector": "0x56407a8963e9ebbb56d8c167c40bc0bd8ce7e38ac48c632421d5cf3dc865a01" + }, + { + "members": [], + "class_hash": "0x6fd8d97850b3e9d127a5b457bfa76d3048a74f25074ce40f929e9e6b4d356fd", + "tag": "ARCADE-Teammate", + "selector": "0x56a4d220830ecdcb5e816e49c743a4e0f784b7bdea24737be188d1f1059308e" + } + ], + "events": [ + { + "members": [], + "class_hash": "0x53ac068c49f133ab7ddd4c0fef626a8fa94eb46d113b37abd8c2f3f9738e31e", + "tag": "ARCADE-Follow", + "selector": "0x38866790c8a50b1c2d43786d8d06856b7ab65ce7a59e136bc47fbae18b147f1" + }, + { + "members": [], + "class_hash": "0x40ce2ebeff98431ff013e5b8deeff73fbb562a38950c8eb391998f022ac18a5", + "tag": "ARCADE-TrophyPinning", + "selector": "0x7b9d51ffd54b6bfa69d849bf8f35fb7bb08820e792d3eeca9dd990f4905aacb" + } + ] +} \ No newline at end of file diff --git a/contracts/src/lib.cairo b/contracts/src/lib.cairo index b490fef..7821122 100644 --- a/contracts/src/lib.cairo +++ b/contracts/src/lib.cairo @@ -1,7 +1,15 @@ mod constants; mod systems { - mod controller; + mod pinner; mod registry; mod slot; + mod society; + mod wallet; +} + +#[cfg(test)] +mod tests { + mod setup; + mod test_setup; } diff --git a/contracts/src/systems/pinner.cairo b/contracts/src/systems/pinner.cairo new file mode 100644 index 0000000..ed537ec --- /dev/null +++ b/contracts/src/systems/pinner.cairo @@ -0,0 +1,74 @@ +// Interfaces + +#[starknet::interface] +trait IPinner { + fn pin(self: @TContractState, achievement_id: felt252); + fn unpin(self: @TContractState, achievement_id: felt252); +} + +// Contracts + +#[dojo::contract] +mod Pinner { + // Dojo imports + + use dojo::world::WorldStorage; + + // Component imports + + use achievement::components::pinnable::PinnableComponent; + + // Internal imports + + use arcade::constants::NAMESPACE; + + // Local imports + + use super::IPinner; + + // Components + + component!(path: PinnableComponent, storage: pinnable, event: PinnableEvent); + impl PinnableInternalImpl = PinnableComponent::InternalImpl; + + // Storage + + #[storage] + struct Storage { + #[substorage(v0)] + pinnable: PinnableComponent::Storage, + } + + // Events + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + PinnableEvent: PinnableComponent::Event, + } + + // Implementations + + #[abi(embed_v0)] + impl PinnerImpl of IPinner { + fn pin(self: @ContractState, achievement_id: felt252) { + let world: WorldStorage = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.pinnable.pin(world, caller, achievement_id); + } + + fn unpin(self: @ContractState, achievement_id: felt252) { + let world: WorldStorage = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.pinnable.unpin(world, caller, achievement_id); + } + } + + #[generate_trait] + impl Private of PrivateTrait { + fn world_storage(self: @ContractState) -> WorldStorage { + self.world(@NAMESPACE()) + } + } +} diff --git a/contracts/src/systems/registry.cairo b/contracts/src/systems/registry.cairo index ae091ea..51b7dcb 100644 --- a/contracts/src/systems/registry.cairo +++ b/contracts/src/systems/registry.cairo @@ -151,10 +151,13 @@ mod Registry { youtube: Option, website: Option, ) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); self .registerable .register( - self.world_storage(), + world, + caller, world_address, namespace, project, @@ -186,10 +189,13 @@ mod Registry { youtube: Option, website: Option, ) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); self .registerable .update( - self.world_storage(), + world, + caller, world_address, namespace, color, @@ -206,23 +212,33 @@ mod Registry { } fn publish_game(self: @ContractState, world_address: felt252, namespace: felt252) { - self.registerable.publish(self.world_storage(), world_address, namespace); + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.registerable.publish(world, caller, world_address, namespace); } fn hide_game(self: @ContractState, world_address: felt252, namespace: felt252) { - self.registerable.hide(self.world_storage(), world_address, namespace); + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.registerable.hide(world, caller, world_address, namespace); } fn whitelist_game(self: @ContractState, world_address: felt252, namespace: felt252) { - self.registerable.whitelist(self.world_storage(), world_address, namespace); + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.registerable.whitelist(world, caller, world_address, namespace); } fn blacklist_game(self: @ContractState, world_address: felt252, namespace: felt252) { - self.registerable.blacklist(self.world_storage(), world_address, namespace); + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.registerable.blacklist(world, caller, world_address, namespace); } fn remove_game(self: @ContractState, world_address: felt252, namespace: felt252) { - self.registerable.remove(self.world_storage(), world_address, namespace); + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.registerable.remove(world, caller, world_address, namespace); } fn register_achievement( @@ -232,9 +248,9 @@ mod Registry { identifier: felt252, karma: u16, ) { - self - .trackable - .register(self.world_storage(), world_address, namespace, identifier, karma) + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.trackable.register(world, caller, world_address, namespace, identifier, karma) } fn update_achievement( @@ -244,37 +260,49 @@ mod Registry { identifier: felt252, karma: u16, ) { - self.trackable.update(self.world_storage(), world_address, namespace, identifier, karma) + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.trackable.update(world, caller, world_address, namespace, identifier, karma) } fn publish_achievement( self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 ) { - self.trackable.publish(self.world_storage(), world_address, namespace, identifier); + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.trackable.publish(world, caller, world_address, namespace, identifier); } fn hide_achievement( self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 ) { - self.trackable.hide(self.world_storage(), world_address, namespace, identifier); + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.trackable.hide(world, caller, world_address, namespace, identifier); } fn whitelist_achievement( self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 ) { - self.trackable.whitelist(self.world_storage(), world_address, namespace, identifier); + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.trackable.whitelist(world, caller, world_address, namespace, identifier); } fn blacklist_achievement( self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 ) { - self.trackable.blacklist(self.world_storage(), world_address, namespace, identifier); + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.trackable.blacklist(world, caller, world_address, namespace, identifier); } fn remove_achievement( self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 ) { - self.trackable.remove(self.world_storage(), world_address, namespace, identifier); + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.trackable.remove(world, caller, world_address, namespace, identifier); } } diff --git a/contracts/src/systems/slot.cairo b/contracts/src/systems/slot.cairo index 7a28021..ea72dc4 100644 --- a/contracts/src/systems/slot.cairo +++ b/contracts/src/systems/slot.cairo @@ -4,6 +4,8 @@ trait ISlot { fn deploy(self: @TContractState, service: u8, project: felt252, tier: u8); fn remove(self: @TContractState, service: u8, project: felt252); + fn hire(self: @TContractState, project: felt252, account_id: felt252, role: u8); + fn fire(self: @TContractState, project: felt252, account_id: felt252); } // Contracts @@ -17,8 +19,10 @@ mod Slot { // External imports use provider::components::deployable::DeployableComponent; + use provider::components::groupable::GroupableComponent; use provider::types::service::Service; use provider::types::tier::Tier; + use provider::types::role::Role; // Internal imports @@ -32,6 +36,8 @@ mod Slot { component!(path: DeployableComponent, storage: deployable, event: DeployableEvent); impl DeployableImpl = DeployableComponent::InternalImpl; + component!(path: GroupableComponent, storage: groupable, event: GroupableEvent); + impl GroupableImpl = GroupableComponent::InternalImpl; // Storage @@ -39,6 +45,8 @@ mod Slot { struct Storage { #[substorage(v0)] deployable: DeployableComponent::Storage, + #[substorage(v0)] + groupable: GroupableComponent::Storage, } // Events @@ -48,6 +56,14 @@ mod Slot { enum Event { #[flat] DeployableEvent: DeployableComponent::Event, + #[flat] + GroupableEvent: GroupableComponent::Event, + } + + // Constructor + + fn dojo_init(self: @ContractState) { + self.deployable.initialize(self.world_storage()); } // Implementations @@ -56,12 +72,26 @@ mod Slot { impl SlotImpl of ISlot { fn deploy(self: @ContractState, service: u8, project: felt252, tier: u8,) { let world = self.world_storage(); - self.deployable.deploy(world, service.into(), project, tier.into()) + let caller: felt252 = starknet::get_caller_address().into(); + self.deployable.deploy(world, caller, service.into(), project, tier.into()) } fn remove(self: @ContractState, service: u8, project: felt252,) { let world = self.world_storage(); - self.deployable.remove(world, service.into(), project); + let caller: felt252 = starknet::get_caller_address().into(); + self.deployable.remove(world, caller, service.into(), project); + } + + fn hire(self: @ContractState, project: felt252, account_id: felt252, role: u8) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.groupable.add(world, caller, project, account_id, role.into()); + } + + fn fire(self: @ContractState, project: felt252, account_id: felt252) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.groupable.remove(world, caller, project, account_id); } } diff --git a/contracts/src/systems/society.cairo b/contracts/src/systems/society.cairo new file mode 100644 index 0000000..f665e2d --- /dev/null +++ b/contracts/src/systems/society.cairo @@ -0,0 +1,310 @@ +// Interfaces + +#[starknet::interface] +trait ISociety { + fn follow(self: @TContractState, target: felt252,); + fn unfollow(self: @TContractState, target: felt252,); + fn create_alliance( + self: @TContractState, + color: Option, + name: Option, + description: Option, + image: Option, + banner: Option, + discord: Option, + telegram: Option, + twitter: Option, + youtube: Option, + website: Option + ); + fn open_alliance(self: @TContractState, free: bool); + fn close_alliance(self: @TContractState); + fn crown_guild(self: @TContractState, guild_id: u32); + fn hire_guild(self: @TContractState, guild_id: u32); + fn fire_guild(self: @TContractState, guild_id: u32); + fn request_alliance(self: @TContractState, alliance_id: u32); + fn cancel_alliance(self: @TContractState); + fn leave_alliance(self: @TContractState); + fn create_guild( + self: @TContractState, + color: Option, + name: Option, + description: Option, + image: Option, + banner: Option, + discord: Option, + telegram: Option, + twitter: Option, + youtube: Option, + website: Option + ); + fn open_guild(self: @TContractState, free: bool); + fn close_guild(self: @TContractState); + fn crown_member(self: @TContractState, member_id: felt252); + fn promote_member(self: @TContractState, member_id: felt252); + fn demote_member(self: @TContractState, member_id: felt252); + fn hire_member(self: @TContractState, member_id: felt252); + fn fire_member(self: @TContractState, member_id: felt252); + fn request_guild(self: @TContractState, guild_id: u32); + fn cancel_guild(self: @TContractState); + fn leave_guild(self: @TContractState); +} + +// Contracts + +#[dojo::contract] +mod Society { + // Dojo imports + + use dojo::world::WorldStorage; + + // Component imports + + use society::components::allianceable::AllianceableComponent; + use society::components::followable::FollowableComponent; + use society::components::guildable::GuildableComponent; + + // Internal imports + + use arcade::constants::NAMESPACE; + + // Local imports + + use super::ISociety; + + // Components + + component!(path: AllianceableComponent, storage: allianceable, event: AllianceableEvent); + impl AllianceableImpl = AllianceableComponent::InternalImpl; + component!(path: FollowableComponent, storage: followable, event: FollowableEvent); + impl FollowableImpl = FollowableComponent::InternalImpl; + component!(path: GuildableComponent, storage: guildable, event: GuildableEvent); + impl GuildableImpl = GuildableComponent::InternalImpl; + + // Storage + + #[storage] + struct Storage { + #[substorage(v0)] + allianceable: AllianceableComponent::Storage, + #[substorage(v0)] + followable: FollowableComponent::Storage, + #[substorage(v0)] + guildable: GuildableComponent::Storage, + } + + // Events + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + AllianceableEvent: AllianceableComponent::Event, + #[flat] + FollowableEvent: FollowableComponent::Event, + #[flat] + GuildableEvent: GuildableComponent::Event, + } + + // Implementations + + #[abi(embed_v0)] + impl SocietyImpl of ISociety { + fn follow(self: @ContractState, target: felt252) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.followable.follow(world, caller, target); + } + + fn unfollow(self: @ContractState, target: felt252) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.followable.unfollow(world, caller, target); + } + + // Alliance + + fn create_alliance( + self: @ContractState, + color: Option, + name: Option, + description: Option, + image: Option, + banner: Option, + discord: Option, + telegram: Option, + twitter: Option, + youtube: Option, + website: Option, + ) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self + .allianceable + .create( + world, + caller, + color, + name, + description, + image, + banner, + discord, + telegram, + twitter, + youtube, + website + ); + } + + fn open_alliance(self: @ContractState, free: bool) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.allianceable.open(world, caller, free); + } + + fn close_alliance(self: @ContractState) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.allianceable.close(world, caller); + } + + fn crown_guild(self: @ContractState, guild_id: u32) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.allianceable.crown(world, caller, guild_id); + } + + fn hire_guild(self: @ContractState, guild_id: u32) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.allianceable.hire(world, caller, guild_id); + } + + fn fire_guild(self: @ContractState, guild_id: u32) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.allianceable.fire(world, caller, guild_id); + } + + fn request_alliance(self: @ContractState, alliance_id: u32) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.allianceable.request(world, caller, alliance_id); + } + + fn cancel_alliance(self: @ContractState) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.allianceable.cancel(world, caller); + } + + fn leave_alliance(self: @ContractState) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.allianceable.leave(world, caller); + } + + // Guild + + fn create_guild( + self: @ContractState, + color: Option, + name: Option, + description: Option, + image: Option, + banner: Option, + discord: Option, + telegram: Option, + twitter: Option, + youtube: Option, + website: Option, + ) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self + .guildable + .create( + world, + caller, + color, + name, + description, + image, + banner, + discord, + telegram, + twitter, + youtube, + website + ); + } + + fn open_guild(self: @ContractState, free: bool) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.guildable.open(world, caller, free); + } + + fn close_guild(self: @ContractState) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.guildable.close(world, caller); + } + + fn crown_member(self: @ContractState, member_id: felt252) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.guildable.crown(world, caller, member_id); + } + + fn promote_member(self: @ContractState, member_id: felt252) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.guildable.promote(world, caller, member_id); + } + + fn demote_member(self: @ContractState, member_id: felt252) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.guildable.demote(world, caller, member_id); + } + + fn hire_member(self: @ContractState, member_id: felt252) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.guildable.hire(world, caller, member_id); + } + + fn fire_member(self: @ContractState, member_id: felt252) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.guildable.fire(world, caller, member_id); + } + + fn request_guild(self: @ContractState, guild_id: u32) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.guildable.request(world, caller, guild_id); + } + + fn cancel_guild(self: @ContractState) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.guildable.cancel(world, caller); + } + + fn leave_guild(self: @ContractState) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.guildable.leave(world, caller); + } + } + + #[generate_trait] + impl Private of PrivateTrait { + fn world_storage(self: @ContractState) -> WorldStorage { + self.world(@NAMESPACE()) + } + } +} diff --git a/contracts/src/systems/controller.cairo b/contracts/src/systems/wallet.cairo similarity index 88% rename from contracts/src/systems/controller.cairo rename to contracts/src/systems/wallet.cairo index 116aa67..7d2269c 100644 --- a/contracts/src/systems/controller.cairo +++ b/contracts/src/systems/wallet.cairo @@ -1,12 +1,12 @@ // Interfaces #[starknet::interface] -trait IController {} +trait IWallet {} // Contracts #[dojo::contract] -mod Controller { +mod Wallet { // Dojo imports use dojo::world::WorldStorage; @@ -21,7 +21,7 @@ mod Controller { // Local imports - use super::IController; + use super::IWallet; // Components @@ -48,7 +48,7 @@ mod Controller { // Implementations #[abi(embed_v0)] - impl ControllerImpl of IController {} + impl WalletImpl of IWallet {} #[generate_trait] impl Private of PrivateTrait { diff --git a/contracts/src/tests/mocks/register.cairo b/contracts/src/tests/mocks/register.cairo new file mode 100644 index 0000000..2dd246c --- /dev/null +++ b/contracts/src/tests/mocks/register.cairo @@ -0,0 +1,197 @@ +#[starknet::interface] +trait IRegister { + fn register( + self: @TContractState, + world_address: felt252, + namespace: felt252, + project: felt252, + color: Option, + name: Option, + description: Option, + image: Option, + banner: Option, + discord: Option, + telegram: Option, + twitter: Option, + youtube: Option, + website: Option, + ); + fn update( + self: @TContractState, + world_address: felt252, + namespace: felt252, + color: Option, + name: Option, + description: Option, + image: Option, + banner: Option, + discord: Option, + telegram: Option, + twitter: Option, + youtube: Option, + website: Option, + ); + fn publish(self: @TContractState, world_address: felt252, namespace: felt252); + fn hide(self: @TContractState, world_address: felt252, namespace: felt252); + fn whitelist(self: @TContractState, world_address: felt252, namespace: felt252); + fn blacklist(self: @TContractState, world_address: felt252, namespace: felt252); +} + +#[dojo::contract] +pub mod Register { + // Starknet imports + + use starknet::{ContractAddress, get_block_timestamp, get_contract_address}; + + // Dojo imports + + use dojo::world::WorldStorage; + use dojo::contract::{IContractDispatcher, IContractDispatcherTrait}; + + // Internal imports + + use registry::components::initializable::InitializableComponent; + use registry::components::registerable::RegisterableComponent; + + // Local imports + + use super::IRegister; + + // Components + + component!(path: InitializableComponent, storage: initializable, event: InitializableEvent); + impl InitializableImpl = InitializableComponent::InternalImpl; + component!(path: RegisterableComponent, storage: registerable, event: RegisterableEvent); + impl RegisterableImpl = RegisterableComponent::InternalImpl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub initializable: InitializableComponent::Storage, + #[substorage(v0)] + pub registerable: RegisterableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + InitializableEvent: InitializableComponent::Event, + #[flat] + RegisterableEvent: RegisterableComponent::Event + } + + fn dojo_init(self: @ContractState, owner: felt252) { + self.initializable.initialize(self.world_storage(), owner); + } + + #[abi(embed_v0)] + impl RegisterImpl of IRegister { + fn register( + self: @ContractState, + world_address: felt252, + namespace: felt252, + project: felt252, + color: Option, + name: Option, + description: Option, + image: Option, + banner: Option, + discord: Option, + telegram: Option, + twitter: Option, + youtube: Option, + website: Option, + ) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self + .registerable + .register( + world, + caller, + world_address, + namespace, + project, + color, + name, + description, + image, + banner, + discord, + telegram, + twitter, + youtube, + website + ); + } + + fn update( + self: @ContractState, + world_address: felt252, + namespace: felt252, + color: Option, + name: Option, + description: Option, + image: Option, + banner: Option, + discord: Option, + telegram: Option, + twitter: Option, + youtube: Option, + website: Option, + ) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self + .registerable + .update( + world, + caller, + world_address, + namespace, + color, + name, + description, + image, + banner, + discord, + telegram, + twitter, + youtube, + website, + ); + } + + fn publish(self: @ContractState, world_address: felt252, namespace: felt252) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.registerable.publish(world, caller, world_address, namespace); + } + + fn hide(self: @ContractState, world_address: felt252, namespace: felt252) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.registerable.hide(world, caller, world_address, namespace); + } + + fn whitelist(self: @ContractState, world_address: felt252, namespace: felt252) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.registerable.whitelist(world, caller, world_address, namespace); + } + + fn blacklist(self: @ContractState, world_address: felt252, namespace: felt252) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.registerable.blacklist(world, caller, world_address, namespace); + } + } + + #[generate_trait] + impl Private of PrivateTrait { + fn world_storage(self: @ContractState) -> WorldStorage { + self.world(@"namespace") + } + } +} diff --git a/contracts/src/tests/mocks/tracker.cairo b/contracts/src/tests/mocks/tracker.cairo new file mode 100644 index 0000000..cc6c875 --- /dev/null +++ b/contracts/src/tests/mocks/tracker.cairo @@ -0,0 +1,142 @@ +#[starknet::interface] +trait ITracker { + fn register( + self: @TContractState, + world_address: felt252, + namespace: felt252, + identifier: felt252, + karma: u16, + ); + fn update( + self: @TContractState, + world_address: felt252, + namespace: felt252, + identifier: felt252, + karma: u16, + ); + fn publish( + self: @TContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ); + fn hide(self: @TContractState, world_address: felt252, namespace: felt252, identifier: felt252); + fn whitelist( + self: @TContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ); + fn blacklist( + self: @TContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ); +} + +#[dojo::contract] +pub mod Tracker { + // Starknet imports + + use starknet::{ContractAddress, get_block_timestamp, get_contract_address}; + + // Dojo imports + + use dojo::world::WorldStorage; + use dojo::contract::{IContractDispatcher, IContractDispatcherTrait}; + + // Internal imports + + use registry::components::initializable::InitializableComponent; + use registry::components::trackable::TrackableComponent; + + // Local imports + + use super::ITracker; + + // Components + + component!(path: InitializableComponent, storage: initializable, event: InitializableEvent); + impl InitializableImpl = InitializableComponent::InternalImpl; + component!(path: TrackableComponent, storage: trackable, event: TrackableEvent); + impl TrackableImpl = TrackableComponent::InternalImpl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub initializable: InitializableComponent::Storage, + #[substorage(v0)] + pub trackable: TrackableComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + InitializableEvent: InitializableComponent::Event, + #[flat] + TrackableEvent: TrackableComponent::Event + } + + fn dojo_init(self: @ContractState, owner: felt252) { + self.initializable.initialize(self.world_storage(), owner); + } + + #[abi(embed_v0)] + impl TrackerImpl of ITracker { + fn register( + self: @ContractState, + world_address: felt252, + namespace: felt252, + identifier: felt252, + karma: u16, + ) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.trackable.register(world, caller, world_address, namespace, identifier, karma); + } + + fn update( + self: @ContractState, + world_address: felt252, + namespace: felt252, + identifier: felt252, + karma: u16, + ) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.trackable.update(world, caller, world_address, namespace, identifier, karma); + } + + fn publish( + self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.trackable.publish(world, caller, world_address, namespace, identifier); + } + + fn hide( + self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.trackable.hide(world, caller, world_address, namespace, identifier); + } + + fn whitelist( + self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.trackable.whitelist(world, caller, world_address, namespace, identifier); + } + + fn blacklist( + self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 + ) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.trackable.blacklist(world, caller, world_address, namespace, identifier); + } + } + + #[generate_trait] + impl Private of PrivateTrait { + fn world_storage(self: @ContractState) -> WorldStorage { + self.world(@"namespace") + } + } +} diff --git a/contracts/src/tests/setup.cairo b/contracts/src/tests/setup.cairo new file mode 100644 index 0000000..d86763c --- /dev/null +++ b/contracts/src/tests/setup.cairo @@ -0,0 +1,134 @@ +mod setup { + // Core imports + + use core::debug::PrintTrait; + + // Starknet imports + + use starknet::ContractAddress; + use starknet::testing; + use starknet::testing::{set_caller_address, set_contract_address, set_block_timestamp}; + + // Dojo imports + + use dojo::world::{WorldStorage, WorldStorageTrait}; + use dojo_cairo_test::{ + spawn_test_world, NamespaceDef, ContractDef, TestResource, ContractDefTrait, + WorldStorageTestTrait + }; + + // External imports + + use controller::models::{index as controller_models}; + use provider::models::{index as provider_models}; + use registry::models::{index as registry_models}; + use society::models::{index as society_models}; + use society::events::{index as society_events}; + use achievement::events::{index as achievement_events}; + + // Internal imports + + use arcade::constants::NAMESPACE; + use arcade::systems::pinner::{Pinner, IPinnerDispatcher}; + use arcade::systems::registry::{Registry, IRegistryDispatcher}; + use arcade::systems::slot::{Slot, ISlotDispatcher}; + use arcade::systems::society::{Society, ISocietyDispatcher}; + use arcade::systems::wallet::{Wallet, IWalletDispatcher}; + + // Constant + + fn OWNER() -> ContractAddress { + starknet::contract_address_const::<'OWNER'>() + } + + fn PLAYER() -> ContractAddress { + starknet::contract_address_const::<'PLAYER'>() + } + + #[derive(Copy, Drop)] + struct Systems { + pinner: IPinnerDispatcher, + registry: IRegistryDispatcher, + slot: ISlotDispatcher, + society: ISocietyDispatcher, + wallet: IWalletDispatcher, + } + + #[derive(Copy, Drop)] + struct Context { + player_id: felt252, + } + + #[inline] + fn setup_namespace() -> NamespaceDef { + NamespaceDef { + namespace: NAMESPACE(), resources: [ + TestResource::Model(controller_models::m_Account::TEST_CLASS_HASH), + TestResource::Model(controller_models::m_Controller::TEST_CLASS_HASH), + TestResource::Model(controller_models::m_Signer::TEST_CLASS_HASH), + TestResource::Model(provider_models::m_Deployment::TEST_CLASS_HASH), + TestResource::Model(provider_models::m_Factory::TEST_CLASS_HASH), + TestResource::Model(provider_models::m_Team::TEST_CLASS_HASH), + TestResource::Model(provider_models::m_Teammate::TEST_CLASS_HASH), + TestResource::Model(registry_models::m_Access::TEST_CLASS_HASH), + TestResource::Model(registry_models::m_Achievement::TEST_CLASS_HASH), + TestResource::Model(registry_models::m_Game::TEST_CLASS_HASH), + TestResource::Model(society_models::m_Alliance::TEST_CLASS_HASH), + TestResource::Model(society_models::m_Guild::TEST_CLASS_HASH), + TestResource::Model(society_models::m_Member::TEST_CLASS_HASH), + TestResource::Event(society_events::e_Follow::TEST_CLASS_HASH), + TestResource::Event(achievement_events::e_TrophyPinning::TEST_CLASS_HASH), + TestResource::Contract(Pinner::TEST_CLASS_HASH), + TestResource::Contract(Registry::TEST_CLASS_HASH), + TestResource::Contract(Slot::TEST_CLASS_HASH), + TestResource::Contract(Society::TEST_CLASS_HASH), + TestResource::Contract(Wallet::TEST_CLASS_HASH), + ].span() + } + } + + fn setup_contracts() -> Span { + [ + ContractDefTrait::new(@NAMESPACE(), @"Pinner") + .with_writer_of([dojo::utils::bytearray_hash(@NAMESPACE())].span()), + ContractDefTrait::new(@NAMESPACE(), @"Registry") + .with_writer_of([dojo::utils::bytearray_hash(@NAMESPACE())].span()) + .with_init_calldata(array![OWNER().into()].span()), + ContractDefTrait::new(@NAMESPACE(), @"Slot") + .with_writer_of([dojo::utils::bytearray_hash(@NAMESPACE())].span()) + .with_init_calldata(array![].span()), + ContractDefTrait::new(@NAMESPACE(), @"Society") + .with_writer_of([dojo::utils::bytearray_hash(@NAMESPACE())].span()), + ContractDefTrait::new(@NAMESPACE(), @"Wallet") + .with_writer_of([dojo::utils::bytearray_hash(@NAMESPACE())].span()), + ].span() + } + + #[inline] + fn spawn() -> (WorldStorage, Systems, Context) { + // [Setup] World + set_contract_address(OWNER()); + let namespace_def = setup_namespace(); + let world = spawn_test_world([namespace_def].span()); + world.sync_perms_and_inits(setup_contracts()); + // [Setup] Systems + let (pinner_address, _) = world.dns(@"Pinner").unwrap(); + let (registry_address, _) = world.dns(@"Registry").unwrap(); + let (slot_address, _) = world.dns(@"Slot").unwrap(); + let (society_address, _) = world.dns(@"Society").unwrap(); + let (wallet_address, _) = world.dns(@"Wallet").unwrap(); + let systems = Systems { + pinner: IPinnerDispatcher { contract_address: pinner_address }, + registry: IRegistryDispatcher { contract_address: registry_address }, + slot: ISlotDispatcher { contract_address: slot_address }, + society: ISocietyDispatcher { contract_address: society_address }, + wallet: IWalletDispatcher { contract_address: wallet_address }, + }; + + // [Setup] Context + let context = Context { player_id: PLAYER().into() }; + + // [Return] + (world, systems, context) + } +} diff --git a/contracts/src/tests/test_setup.cairo b/contracts/src/tests/test_setup.cairo new file mode 100644 index 0000000..dbaf80e --- /dev/null +++ b/contracts/src/tests/test_setup.cairo @@ -0,0 +1,11 @@ +// Internal imports + +use arcade::tests::setup::setup::spawn; + +// Tests + +#[test] +fn test_setup() { + // [Setup] + let (_, _, _) = spawn(); +} diff --git a/contracts/src/tests/test_trackable.cairo b/contracts/src/tests/test_trackable.cairo new file mode 100644 index 0000000..858bdad --- /dev/null +++ b/contracts/src/tests/test_trackable.cairo @@ -0,0 +1,125 @@ +// Core imports + +use core::num::traits::Zero; + +// Starknet imports + +use starknet::ContractAddress; +use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; +use starknet::testing; + +// Internal imports + +use registry::store::{Store, StoreTrait}; +use registry::models::game::{Game, GameTrait}; +use registry::models::achievement::{Achievement, AchievementTrait}; +use registry::tests::mocks::register::{Register, IRegisterDispatcher, IRegisterDispatcherTrait}; +use registry::tests::mocks::tracker::{Tracker, ITrackerDispatcher, ITrackerDispatcherTrait}; +use registry::tests::setup::setup::{spawn, Systems, Context, PLAYER, OWNER}; + +// Constants + +const WORLD_ADDRESS: felt252 = 'WORLD'; +const NAMEPSACE: felt252 = 'NAMESPACE'; +const PROJECT: felt252 = 'PROJECT'; + +// Helpers + +fn register(systems: @Systems) { + testing::set_contract_address(PLAYER()); + (*systems) + .register + .register( + WORLD_ADDRESS, + NAMEPSACE, + PROJECT, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + Option::None, + ); +} + +// Tests + +#[test] +fn test_trackable_register_achievement() { + // [Setup] + let (world, systems, _context) = spawn(); + register(@systems); + // [Register] Achievement + let identifier: felt252 = 'IDENTIFIER'; + let karma: u16 = 10; + systems.tracker.register(WORLD_ADDRESS, NAMEPSACE, identifier, karma); + // [Assert] Achievement + let store = StoreTrait::new(world); + let achievement = store.get_achievement(WORLD_ADDRESS, NAMEPSACE, identifier); + assert_eq!(achievement.id, identifier); + assert_eq!(achievement.karma, karma); + // [Assert] Game + let game = store.get_game(WORLD_ADDRESS, NAMEPSACE); + assert_eq!(game.karma, karma); +} + +#[test] +fn test_trackable_update_achievement() { + // [Setup] + let (world, systems, _context) = spawn(); + register(@systems); + // [Register] Achievement + let identifier: felt252 = 'IDENTIFIER'; + let karma: u16 = 10; + systems.tracker.register(WORLD_ADDRESS, NAMEPSACE, identifier, karma); + // [Update] Achievement + let new_karma: u16 = 20; + systems.tracker.update(WORLD_ADDRESS, NAMEPSACE, identifier, new_karma); + // [Assert] Achievement + let store = StoreTrait::new(world); + let achievement = store.get_achievement(WORLD_ADDRESS, NAMEPSACE, identifier); + assert_eq!(achievement.karma, new_karma); + // [Assert] Game + let game = store.get_game(WORLD_ADDRESS, NAMEPSACE); + assert_eq!(game.karma, new_karma); +} + +#[test] +fn test_trackable_publish_achievement() { + // [Setup] + let (world, systems, _context) = spawn(); + register(@systems); + // [Register] Achievement + let identifier: felt252 = 'IDENTIFIER'; + let karma: u16 = 10; + systems.tracker.register(WORLD_ADDRESS, NAMEPSACE, identifier, karma); + // [Publish] Achievement + systems.tracker.publish(WORLD_ADDRESS, NAMEPSACE, identifier); + // [Assert] Achievement + let store = StoreTrait::new(world); + let achievement = store.get_achievement(WORLD_ADDRESS, NAMEPSACE, identifier); + assert_eq!(achievement.published, true); +} + +#[test] +fn test_trackable_whitelist_achievement() { + // [Setup] + let (world, systems, _context) = spawn(); + register(@systems); + // [Register] Achievement + let identifier: felt252 = 'IDENTIFIER'; + let karma: u16 = 10; + systems.tracker.register(WORLD_ADDRESS, NAMEPSACE, identifier, karma); + // [Whitelist] Achievement + systems.tracker.publish(WORLD_ADDRESS, NAMEPSACE, identifier); + testing::set_contract_address(OWNER()); + systems.tracker.whitelist(WORLD_ADDRESS, NAMEPSACE, identifier); + // [Assert] Achievement + let store = StoreTrait::new(world); + let achievement = store.get_achievement(WORLD_ADDRESS, NAMEPSACE, identifier); + assert_eq!(achievement.whitelisted, true); +} diff --git a/contracts/torii_dev.toml b/contracts/torii_dev.toml new file mode 100644 index 0000000..e69de29 diff --git a/packages/trophy/README.md b/packages/achievement/README.md similarity index 100% rename from packages/trophy/README.md rename to packages/achievement/README.md diff --git a/packages/trophy/Scarb.toml b/packages/achievement/Scarb.toml similarity index 84% rename from packages/trophy/Scarb.toml rename to packages/achievement/Scarb.toml index 378ec2f..9fcfb4a 100644 --- a/packages/trophy/Scarb.toml +++ b/packages/achievement/Scarb.toml @@ -1,5 +1,5 @@ [package] -name = "arcade_trophy" +name = "achievement" version.workspace = true [dependencies] diff --git a/packages/trophy/src/components/achievable.cairo b/packages/achievement/src/components/achievable.cairo similarity index 92% rename from packages/trophy/src/components/achievable.cairo rename to packages/achievement/src/components/achievable.cairo index ab3c011..f05fa26 100644 --- a/packages/trophy/src/components/achievable.cairo +++ b/packages/achievement/src/components/achievable.cairo @@ -4,18 +4,14 @@ mod AchievableComponent { use core::debug::PrintTrait; - // Starknet imports - - use starknet::info::{get_caller_address, get_block_timestamp, get_contract_address}; - // Dojo imports use dojo::world::WorldStorage; // Internal imports - use arcade_trophy::types::task::Task; - use arcade_trophy::store::{Store, StoreTrait}; + use achievement::types::task::Task; + use achievement::store::{Store, StoreTrait}; // Errors @@ -111,7 +107,7 @@ mod AchievableComponent { let store: Store = StoreTrait::new(world); // [Event] Emit achievement completion - let time: u64 = get_block_timestamp(); + let time: u64 = starknet::get_block_timestamp(); store.progress(player_id, task_id, count, time); } } diff --git a/packages/achievement/src/components/pinnable.cairo b/packages/achievement/src/components/pinnable.cairo new file mode 100644 index 0000000..d33c493 --- /dev/null +++ b/packages/achievement/src/components/pinnable.cairo @@ -0,0 +1,58 @@ +#[starknet::component] +mod PinnableComponent { + // Core imports + + use core::debug::PrintTrait; + + // Dojo imports + + use dojo::world::WorldStorage; + + // Internal imports + + use achievement::store::{Store, StoreTrait}; + + // Errors + + mod errors {} + + // Storage + + #[storage] + struct Storage {} + + // Events + + #[event] + #[derive(Drop, starknet::Event)] + enum Event {} + + #[generate_trait] + impl InternalImpl< + TContractState, +HasComponent + > of InternalTrait { + fn pin( + self: @ComponentState, + world: WorldStorage, + player_id: felt252, + achievement_id: felt252, + ) { + // [Setup] Store + let store: Store = StoreTrait::new(world); + + // [Event] Emit achievement creation + let time = starknet::get_block_timestamp(); + store.pin(player_id, achievement_id, time); + } + + fn unpin( + self: @ComponentState, + world: WorldStorage, + player_id: felt252, + achievement_id: felt252, + ) { + let store: Store = StoreTrait::new(world); + store.unpin(player_id, achievement_id); + } + } +} diff --git a/packages/trophy/src/events/trophy.cairo b/packages/achievement/src/events/creation.cairo similarity index 60% rename from packages/trophy/src/events/trophy.cairo rename to packages/achievement/src/events/creation.cairo index b231b7a..9971bcd 100644 --- a/packages/trophy/src/events/trophy.cairo +++ b/packages/achievement/src/events/creation.cairo @@ -1,22 +1,27 @@ // Internal imports -use arcade_trophy::events::index::TrophyCreation; -use arcade_trophy::types::task::{Task, TaskTrait}; +use achievement::events::index::TrophyCreation; +use achievement::types::task::{Task, TaskTrait}; + +// Constants + +const MAX_POINTS: u16 = 100; // Errors pub mod errors { - pub const TROPHY_INVALID_ID: felt252 = 'Trophy: invalid id'; - pub const TROPHY_INVALID_TITLE: felt252 = 'Trophy: invalid title'; - pub const TROPHY_INVALID_DESCRIPTION: felt252 = 'Trophy: invalid desc.'; - pub const TROPHY_INVALID_TASKS: felt252 = 'Trophy: invalid tasks.'; - pub const TROPHY_INVALID_DURATION: felt252 = 'Trophy: invalid duration.'; + pub const CREATION_INVALID_ID: felt252 = 'Creation: invalid id'; + pub const CREATION_INVALID_TITLE: felt252 = 'Creation: invalid title'; + pub const CREATION_INVALID_DESCRIPTION: felt252 = 'Creation: invalid desc.'; + pub const CREATION_INVALID_TASKS: felt252 = 'Creation: invalid tasks'; + pub const CREATION_INVALID_DURATION: felt252 = 'Creation: invalid duration'; + pub const CREATION_INVALID_POINTS: felt252 = 'Creation: too much points'; } // Implementations #[generate_trait] -impl TrophyImpl of TrophyTrait { +impl CreationImpl of CreationTrait { #[inline] fn new( id: felt252, @@ -34,10 +39,11 @@ impl TrophyImpl of TrophyTrait { ) -> TrophyCreation { // [Check] Inputs // [Info] We don't check points here, leave free the game to decide - TrophyAssert::assert_valid_id(id); - TrophyAssert::assert_valid_title(title); - TrophyAssert::assert_valid_description(@description); - TrophyAssert::assert_valid_duration(start, end); + CreationAssert::assert_valid_id(id); + CreationAssert::assert_valid_title(title); + CreationAssert::assert_valid_description(@description); + CreationAssert::assert_valid_duration(start, end); + CreationAssert::assert_valid_points(points); // [Return] TrophyCreation TrophyCreation { id, hidden, index, points, start, end, group, icon, title, description, tasks, data @@ -46,30 +52,35 @@ impl TrophyImpl of TrophyTrait { } #[generate_trait] -impl TrophyAssert of AssertTrait { +impl CreationAssert of AssertTrait { #[inline] fn assert_valid_id(id: felt252) { - assert(id != 0, errors::TROPHY_INVALID_ID); + assert(id != 0, errors::CREATION_INVALID_ID); } #[inline] fn assert_valid_title(title: felt252) { - assert(title != 0, errors::TROPHY_INVALID_TITLE); + assert(title != 0, errors::CREATION_INVALID_TITLE); } #[inline] fn assert_valid_description(description: @ByteArray) { - assert(description.len() > 0, errors::TROPHY_INVALID_DESCRIPTION); + assert(description.len() > 0, errors::CREATION_INVALID_DESCRIPTION); } #[inline] fn assert_valid_tasks(tasks: Span) { - assert(tasks.len() > 0, errors::TROPHY_INVALID_TASKS); + assert(tasks.len() > 0, errors::CREATION_INVALID_TASKS); } #[inline] fn assert_valid_duration(start: u64, end: u64) { - assert(end >= start, errors::TROPHY_INVALID_DURATION); + assert(end >= start, errors::CREATION_INVALID_DURATION); + } + + #[inline] + fn assert_valid_points(points: u16) { + assert(points <= MAX_POINTS, errors::CREATION_INVALID_POINTS); } } @@ -77,12 +88,12 @@ impl TrophyAssert of AssertTrait { mod tests { // Local imports - use super::TrophyTrait; - use super::{Task, TaskTrait}; + use super::CreationTrait; + use super::{Task, TaskTrait, MAX_POINTS}; // Constants - const ID: felt252 = 'TROPHY'; + const ID: felt252 = 'CREATION'; const INDEX: u8 = 0; const GROUP: felt252 = 'GROUP'; const HIDDEN: bool = false; @@ -97,7 +108,7 @@ mod tests { #[test] fn test_achievement_creation_new() { let tasks: Array = array![TaskTrait::new(TASK_ID, TOTAL, "TASK DESCRIPTION"),]; - let achievement = TrophyTrait::new( + let achievement = CreationTrait::new( ID, HIDDEN, INDEX, @@ -126,10 +137,10 @@ mod tests { } #[test] - #[should_panic(expected: ('Trophy: invalid id',))] + #[should_panic(expected: ('Creation: invalid id',))] fn test_achievement_creation_new_invalid_id() { let tasks: Array = array![TaskTrait::new(TASK_ID, TOTAL, "TASK DESCRIPTION"),]; - TrophyTrait::new( + CreationTrait::new( 0, HIDDEN, INDEX, @@ -146,30 +157,50 @@ mod tests { } #[test] - #[should_panic(expected: ('Trophy: invalid title',))] + #[should_panic(expected: ('Creation: invalid title',))] fn test_achievement_creation_new_invalid_title() { let tasks: Array = array![TaskTrait::new(TASK_ID, TOTAL, "TASK DESCRIPTION"),]; - TrophyTrait::new( + CreationTrait::new( ID, HIDDEN, INDEX, POINTS, START, END, GROUP, ICON, 0, "DESCRIPTION", tasks.span(), "" ); } #[test] - #[should_panic(expected: ('Trophy: invalid desc.',))] + #[should_panic(expected: ('Creation: invalid desc.',))] fn test_achievement_creation_new_invalid_description() { let tasks: Array = array![TaskTrait::new(TASK_ID, TOTAL, "TASK DESCRIPTION"),]; - TrophyTrait::new( + CreationTrait::new( ID, HIDDEN, INDEX, POINTS, START, END, GROUP, ICON, TITLE, "", tasks.span(), "" ); } #[test] - #[should_panic(expected: ('Trophy: invalid duration.',))] + #[should_panic(expected: ('Creation: invalid duration',))] fn test_achievement_creation_new_invalid_duration() { let tasks: Array = array![TaskTrait::new(TASK_ID, TOTAL, "TASK DESCRIPTION"),]; - TrophyTrait::new( + CreationTrait::new( ID, HIDDEN, INDEX, POINTS, START, 0, GROUP, ICON, TITLE, "DESCRIPTION", tasks.span(), "" ); } + + #[test] + #[should_panic(expected: ('Creation: too much points',))] + fn test_achievement_creation_new_invalid_points() { + let tasks: Array = array![TaskTrait::new(TASK_ID, TOTAL, "TASK DESCRIPTION"),]; + CreationTrait::new( + ID, + HIDDEN, + INDEX, + MAX_POINTS + 1, + START, + END, + GROUP, + ICON, + TITLE, + "DESCRIPTION", + tasks.span(), + "" + ); + } } diff --git a/packages/trophy/src/events/index.cairo b/packages/achievement/src/events/index.cairo similarity index 66% rename from packages/trophy/src/events/index.cairo rename to packages/achievement/src/events/index.cairo index 17d6825..0b3b3fc 100644 --- a/packages/trophy/src/events/index.cairo +++ b/packages/achievement/src/events/index.cairo @@ -1,11 +1,11 @@ -/// Events +//! Events // Internal imports -use arcade_trophy::types::task::Task; +use achievement::types::task::Task; #[derive(Clone, Drop, Serde)] -#[dojo::event(historical: true)] +#[dojo::event] pub struct TrophyCreation { #[key] id: felt252, @@ -23,7 +23,7 @@ pub struct TrophyCreation { } #[derive(Copy, Drop, Serde)] -#[dojo::event(historical: true)] +#[dojo::event] pub struct TrophyProgression { #[key] player_id: felt252, @@ -32,3 +32,13 @@ pub struct TrophyProgression { count: u32, time: u64, } + +#[derive(Copy, Drop, Serde)] +#[dojo::event] +pub struct TrophyPinning { + #[key] + player_id: felt252, + #[key] + achievement_id: felt252, + time: u64, +} diff --git a/packages/achievement/src/events/pinning.cairo b/packages/achievement/src/events/pinning.cairo new file mode 100644 index 0000000..ff9f851 --- /dev/null +++ b/packages/achievement/src/events/pinning.cairo @@ -0,0 +1,57 @@ +// Internal imports + +use achievement::events::index::TrophyPinning; + +// Errors + +pub mod errors { + pub const PINNING_INVALID_ID: felt252 = 'Pinning: invalid id'; +} + +// Implementations + +#[generate_trait] +impl PinningImpl of PinningTrait { + #[inline] + fn new(player_id: felt252, achievement_id: felt252, time: u64) -> TrophyPinning { + // [Check] Inputs + PinningAssert::assert_valid_id(achievement_id); + // [Return] Pinning + TrophyPinning { player_id, achievement_id, time } + } +} + +#[generate_trait] +impl PinningAssert of AssertTrait { + #[inline] + fn assert_valid_id(achievement_id: felt252) { + assert(achievement_id != 0, errors::PINNING_INVALID_ID); + } +} + +#[cfg(test)] +mod tests { + // Local imports + + use super::PinningTrait; + + // Constants + + const PLAYER_ID: felt252 = 'PLAYER'; + const ACHIEVEMENT_ID: felt252 = 'ACHIEVEMENT'; + const TIME: u64 = 1000000000; + + #[test] + fn test_pinning_new() { + let pinning = PinningTrait::new(PLAYER_ID, ACHIEVEMENT_ID, TIME,); + assert_eq!(pinning.player_id, PLAYER_ID); + assert_eq!(pinning.achievement_id, ACHIEVEMENT_ID); + assert_eq!(pinning.time, TIME); + } + + #[test] + #[should_panic(expected: ('Pinning: invalid id',))] + fn test_pinning_new_invalid_id() { + PinningTrait::new(PLAYER_ID, 0, TIME); + } +} diff --git a/packages/trophy/src/events/progress.cairo b/packages/achievement/src/events/progress.cairo similarity index 89% rename from packages/trophy/src/events/progress.cairo rename to packages/achievement/src/events/progress.cairo index 114ebc9..4769743 100644 --- a/packages/trophy/src/events/progress.cairo +++ b/packages/achievement/src/events/progress.cairo @@ -1,6 +1,6 @@ // Internal imports -use arcade_trophy::events::index::TrophyProgression; +use achievement::events::index::TrophyProgression; // Errors @@ -15,7 +15,7 @@ impl ProgressImpl of ProgressTrait { #[inline] fn new(player_id: felt252, task_id: felt252, count: u32, time: u64,) -> TrophyProgression { // [Check] Inputs - ProgressAssert::assert_valid_id(task_id); + ProgressAssert::assert_valid_task(task_id); // [Return] Progress TrophyProgression { player_id, task_id, count, time } } @@ -24,7 +24,7 @@ impl ProgressImpl of ProgressTrait { #[generate_trait] impl ProgressAssert of AssertTrait { #[inline] - fn assert_valid_id(task_id: felt252) { + fn assert_valid_task(task_id: felt252) { assert(task_id != 0, errors::PROGRESS_INVALID_TASK); } } diff --git a/packages/trophy/src/lib.cairo b/packages/achievement/src/lib.cairo similarity index 82% rename from packages/trophy/src/lib.cairo rename to packages/achievement/src/lib.cairo index f7b1414..2fa09f0 100644 --- a/packages/trophy/src/lib.cairo +++ b/packages/achievement/src/lib.cairo @@ -7,12 +7,14 @@ mod types { mod events { mod index; - mod trophy; + mod creation; mod progress; + mod pinning; } mod components { mod achievable; + mod pinnable; } #[cfg(test)] diff --git a/packages/trophy/src/store.cairo b/packages/achievement/src/store.cairo similarity index 61% rename from packages/trophy/src/store.cairo rename to packages/achievement/src/store.cairo index 8e44e3d..d0a8944 100644 --- a/packages/trophy/src/store.cairo +++ b/packages/achievement/src/store.cairo @@ -11,12 +11,12 @@ use dojo::event::EventStorage; // Events imports -use arcade_trophy::events::trophy::{TrophyCreation, TrophyTrait}; -use arcade_trophy::events::progress::{TrophyProgression, ProgressTrait}; - +use achievement::events::creation::{TrophyCreation, CreationTrait}; +use achievement::events::progress::{TrophyProgression, ProgressTrait}; +use achievement::events::pinning::{TrophyPinning, PinningTrait}; // Internal imports -use arcade_trophy::types::task::{Task, TaskTrait}; +use achievement::types::task::{Task, TaskTrait}; // Structs @@ -50,7 +50,7 @@ impl StoreImpl of StoreTrait { tasks: Span, data: ByteArray, ) { - let event: TrophyCreation = TrophyTrait::new( + let event: TrophyCreation = CreationTrait::new( id, hidden, index, points, start, end, group, icon, title, description, tasks, data ); self.world.emit_event(@event); @@ -61,4 +61,16 @@ impl StoreImpl of StoreTrait { let event: TrophyProgression = ProgressTrait::new(player_id, task_id, count, time); self.world.emit_event(@event); } + + #[inline] + fn pin(mut self: Store, player_id: felt252, achievement_id: felt252, time: u64,) { + let event: TrophyPinning = PinningTrait::new(player_id, achievement_id, time); + self.world.emit_event(@event); + } + + #[inline] + fn unpin(mut self: Store, player_id: felt252, achievement_id: felt252,) { + let event: TrophyPinning = PinningTrait::new(player_id, achievement_id, 0); + self.world.emit_event(@event); + } } diff --git a/packages/trophy/src/tests/mocks/achiever.cairo b/packages/achievement/src/tests/mocks/achiever.cairo similarity index 94% rename from packages/trophy/src/tests/mocks/achiever.cairo rename to packages/achievement/src/tests/mocks/achiever.cairo index 442a27a..75ed4b7 100644 --- a/packages/trophy/src/tests/mocks/achiever.cairo +++ b/packages/achievement/src/tests/mocks/achiever.cairo @@ -1,6 +1,6 @@ // Internal imports -use arcade_trophy::types::task::Task; +use achievement::types::task::Task; #[starknet::interface] trait IAchiever { @@ -35,8 +35,8 @@ pub mod Achiever { // Internal imports - use arcade_trophy::types::task::Task; - use arcade_trophy::components::achievable::AchievableComponent; + use achievement::types::task::Task; + use achievement::components::achievable::AchievableComponent; // Local imports diff --git a/packages/trophy/src/tests/setup.cairo b/packages/achievement/src/tests/setup.cairo similarity index 92% rename from packages/trophy/src/tests/setup.cairo rename to packages/achievement/src/tests/setup.cairo index ac95c98..be25b14 100644 --- a/packages/trophy/src/tests/setup.cairo +++ b/packages/achievement/src/tests/setup.cairo @@ -19,8 +19,8 @@ mod setup { // Internal imports - use arcade_trophy::events::{index as events}; - use arcade_trophy::tests::mocks::achiever::{Achiever, IAchiever, IAchieverDispatcher}; + use achievement::events::{index as events}; + use achievement::tests::mocks::achiever::{Achiever, IAchiever, IAchieverDispatcher}; // Constant @@ -58,6 +58,7 @@ mod setup { namespace: "namespace", resources: [ TestResource::Event(events::e_TrophyCreation::TEST_CLASS_HASH), TestResource::Event(events::e_TrophyProgression::TEST_CLASS_HASH), + TestResource::Event(events::e_TrophyPinning::TEST_CLASS_HASH), TestResource::Contract(Achiever::TEST_CLASS_HASH), ].span() } diff --git a/packages/trophy/src/tests/test_achievable.cairo b/packages/achievement/src/tests/test_achievable.cairo similarity index 88% rename from packages/trophy/src/tests/test_achievable.cairo rename to packages/achievement/src/tests/test_achievable.cairo index 6c24921..dd89629 100644 --- a/packages/trophy/src/tests/test_achievable.cairo +++ b/packages/achievement/src/tests/test_achievable.cairo @@ -14,13 +14,11 @@ use dojo::world::world::Event; // Internal imports -use arcade_trophy::types::task::{Task, TaskTrait}; -use arcade_trophy::events::trophy::{TrophyCreation, TrophyTrait}; -use arcade_trophy::events::progress::{TrophyProgression, ProgressTrait}; -use arcade_trophy::tests::mocks::achiever::{ - Achiever, IAchieverDispatcher, IAchieverDispatcherTrait -}; -use arcade_trophy::tests::setup::setup::{spawn_game, clear_events, Systems, PLAYER}; +use achievement::types::task::{Task, TaskTrait}; +use achievement::events::creation::{TrophyCreation, CreationTrait}; +use achievement::events::progress::{TrophyProgression, ProgressTrait}; +use achievement::tests::mocks::achiever::{Achiever, IAchieverDispatcher, IAchieverDispatcherTrait}; +use achievement::tests::setup::setup::{spawn_game, clear_events, Systems, PLAYER}; // Constants diff --git a/packages/trophy/src/types/index.cairo b/packages/achievement/src/types/index.cairo similarity index 100% rename from packages/trophy/src/types/index.cairo rename to packages/achievement/src/types/index.cairo diff --git a/packages/trophy/src/types/task.cairo b/packages/achievement/src/types/task.cairo similarity index 67% rename from packages/trophy/src/types/task.cairo rename to packages/achievement/src/types/task.cairo index c13d283..d41ebe2 100644 --- a/packages/trophy/src/types/task.cairo +++ b/packages/achievement/src/types/task.cairo @@ -1,13 +1,13 @@ // Internal imports -use arcade_trophy::types::index::Task; +use achievement::types::index::Task; // Errors pub mod errors { - pub const TROPHY_INVALID_ID: felt252 = 'Task: invalid id'; - pub const TROPHY_INVALID_DESCRIPTION: felt252 = 'Task: invalid description'; - pub const TROPHY_INVALID_TOTAL: felt252 = 'Task: invalid total'; + pub const TASK_INVALID_ID: felt252 = 'Task: invalid id'; + pub const TASK_INVALID_DESCRIPTION: felt252 = 'Task: invalid description'; + pub const TASK_INVALID_TOTAL: felt252 = 'Task: invalid total'; } // Implementations @@ -29,17 +29,17 @@ impl TaskImpl of TaskTrait { impl TaskAssert of AssertTrait { #[inline] fn assert_valid_id(id: felt252) { - assert(id != 0, errors::TROPHY_INVALID_ID); + assert(id != 0, errors::TASK_INVALID_ID); } #[inline] fn assert_valid_total(total: u32) { - assert(total > 0, errors::TROPHY_INVALID_TOTAL); + assert(total > 0, errors::TASK_INVALID_TOTAL); } #[inline] fn assert_valid_description(description: @ByteArray) { - assert(description.len() > 0, errors::TROPHY_INVALID_DESCRIPTION); + assert(description.len() > 0, errors::TASK_INVALID_DESCRIPTION); } } @@ -51,11 +51,11 @@ mod tests { // Constants - const ID: felt252 = 'TROPHY'; + const ID: felt252 = 'TASK'; const TOTAL: u32 = 100; #[test] - fn test_achievement_creation_new() { + fn test_task_creation_new() { let achievement = TaskTrait::new(ID, TOTAL, "DESCRIPTION"); assert_eq!(achievement.id, ID); assert_eq!(achievement.total, TOTAL); @@ -64,20 +64,20 @@ mod tests { #[test] #[should_panic(expected: ('Task: invalid id',))] - fn test_achievement_creation_new_invalid_id() { + fn test_task_creation_new_invalid_id() { TaskTrait::new(0, TOTAL, "DESCRIPTION"); } #[test] #[should_panic(expected: ('Task: invalid total',))] - fn test_achievement_creation_new_invalid_total() { + fn test_task_creation_new_invalid_total() { TaskTrait::new(ID, 0, "DESCRIPTION"); } #[test] #[should_panic(expected: ('Task: invalid description',))] - fn test_achievement_creation_new_invalid_description() { + fn test_task_creation_new_invalid_description() { TaskTrait::new(ID, TOTAL, ""); } } diff --git a/packages/controller/src/components/controllable.cairo b/packages/controller/src/components/controllable.cairo index 5854165..aaedd7e 100644 --- a/packages/controller/src/components/controllable.cairo +++ b/packages/controller/src/components/controllable.cairo @@ -9,9 +9,7 @@ mod ControllableComponent { use controller::store::{Store, StoreTrait}; use controller::models::account::{Account, AccountTrait, AccountAssert}; use controller::models::controller::{Controller, ControllerTrait, ControllerAssert}; - use controller::models::member::{Member, MemberTrait, MemberAssert}; use controller::models::signer::{Signer, SignerTrait, SignerAssert}; - use controller::models::team::{Team, TeamTrait, TeamAssert}; // Storage diff --git a/packages/controller/src/lib.cairo b/packages/controller/src/lib.cairo index c62d605..580f933 100644 --- a/packages/controller/src/lib.cairo +++ b/packages/controller/src/lib.cairo @@ -3,16 +3,13 @@ mod store; mod types { mod method; - mod role; } mod models { mod index; mod account; mod controller; - mod member; mod signer; - mod team; } mod components { diff --git a/packages/controller/src/models/account.cairo b/packages/controller/src/models/account.cairo index 0eac43e..683fc54 100644 --- a/packages/controller/src/models/account.cairo +++ b/packages/controller/src/models/account.cairo @@ -1,4 +1,4 @@ -// Intenral imports +// Internal imports use controller::models::index::Account; @@ -67,7 +67,7 @@ mod tests { const USERNAME: felt252 = 'USERNAME'; #[test] - fn test_deployment_new() { + fn test_account_new() { let account = AccountTrait::new(IDENTIFIER, 0, 'NAME', USERNAME, "{}"); assert_eq!(account.id, IDENTIFIER); assert_eq!(account.controllers, 0); @@ -78,14 +78,14 @@ mod tests { } #[test] - fn test_deployment_assert_does_exist() { + fn test_account_assert_does_exist() { let account = AccountTrait::new(IDENTIFIER, 0, 'NAME', USERNAME, "{}"); account.assert_does_exist(); } #[test] #[should_panic(expected: 'Account: already exists')] - fn test_deployment_revert_already_exists() { + fn test_account_revert_already_exists() { let account = AccountTrait::new(IDENTIFIER, 0, 'NAME', USERNAME, "{}"); account.assert_does_not_exist(); } diff --git a/packages/controller/src/models/controller.cairo b/packages/controller/src/models/controller.cairo index cf43b40..590759c 100644 --- a/packages/controller/src/models/controller.cairo +++ b/packages/controller/src/models/controller.cairo @@ -1,4 +1,4 @@ -// Intenral imports +// Internal imports use controller::models::index::Controller; @@ -96,7 +96,7 @@ mod tests { const NETWORK: felt252 = 'NETWORK'; #[test] - fn test_deployment_new() { + fn test_controller_new() { let controller = ControllerTrait::new( ACCOUNT_ID, IDENTIFIER, SIGNERS, ADDRESS, NETWORK, "" ); @@ -109,7 +109,7 @@ mod tests { } #[test] - fn test_deployment_assert_does_exist() { + fn test_controller_assert_does_exist() { let controller = ControllerTrait::new( ACCOUNT_ID, IDENTIFIER, SIGNERS, ADDRESS, NETWORK, "" ); @@ -118,7 +118,7 @@ mod tests { #[test] #[should_panic(expected: 'Controller: already exists')] - fn test_deployment_revert_already_exists() { + fn test_controller_revert_already_exists() { let controller = ControllerTrait::new( ACCOUNT_ID, IDENTIFIER, SIGNERS, ADDRESS, NETWORK, "" ); @@ -127,31 +127,31 @@ mod tests { #[test] #[should_panic(expected: 'Controller: invalid account id')] - fn test_deployment_revert_invalid_account_id() { + fn test_controller_revert_invalid_account_id() { ControllerTrait::new(0, IDENTIFIER, SIGNERS, ADDRESS, NETWORK, ""); } #[test] #[should_panic(expected: 'Controller: invalid identifier')] - fn test_deployment_revert_invalid_identifier() { + fn test_controller_revert_invalid_identifier() { ControllerTrait::new(ACCOUNT_ID, 0, SIGNERS, ADDRESS, NETWORK, ""); } #[test] #[should_panic(expected: 'Controller: invalid signers')] - fn test_deployment_revert_invalid_signers() { + fn test_controller_revert_invalid_signers() { ControllerTrait::new(ACCOUNT_ID, IDENTIFIER, 0, ADDRESS, NETWORK, ""); } #[test] #[should_panic(expected: 'Controller: invalid address')] - fn test_deployment_revert_invalid_address() { + fn test_controller_revert_invalid_address() { ControllerTrait::new(ACCOUNT_ID, IDENTIFIER, SIGNERS, 0, NETWORK, ""); } #[test] #[should_panic(expected: 'Controller: invalid network')] - fn test_deployment_revert_invalid_network() { + fn test_controller_revert_invalid_network() { ControllerTrait::new(ACCOUNT_ID, IDENTIFIER, SIGNERS, ADDRESS, 0, ""); } } diff --git a/packages/controller/src/models/index.cairo b/packages/controller/src/models/index.cairo index 19673c0..ac38ceb 100644 --- a/packages/controller/src/models/index.cairo +++ b/packages/controller/src/models/index.cairo @@ -25,16 +25,6 @@ pub struct Controller { constructor_calldata: ByteArray, } -#[derive(Copy, Drop, Serde)] -#[dojo::model] -pub struct Member { - #[key] - account_id: felt252, - #[key] - team_id: felt252, - role: u8, -} - #[derive(Clone, Drop, Serde)] #[dojo::model] pub struct Signer { @@ -45,12 +35,3 @@ pub struct Signer { method: u8, metadata: ByteArray, } - -#[derive(Clone, Drop, Serde)] -#[dojo::model] -pub struct Team { - #[key] - id: felt252, - name: felt252, - description: ByteArray, -} diff --git a/packages/controller/src/models/member.cairo b/packages/controller/src/models/member.cairo deleted file mode 100644 index 77c3010..0000000 --- a/packages/controller/src/models/member.cairo +++ /dev/null @@ -1,95 +0,0 @@ -// Intenral imports - -use controller::models::index::Member; -use controller::types::role::Role; - -// Errors - -pub mod errors { - pub const MEMBER_ALREADY_EXISTS: felt252 = 'Member: already exists'; - pub const MEMBER_NOT_EXIST: felt252 = 'Member: does not exist'; - pub const MEMBER_INVALID_ACCOUNT_ID: felt252 = 'Member: invalid account id'; - pub const MEMBER_INVALID_TEAM_ID: felt252 = 'Member: invalid team id'; - pub const MEMBER_INVALID_ROLE: felt252 = 'Member: invalid role'; -} - -#[generate_trait] -impl MemberImpl of MemberTrait { - #[inline] - fn new(account_id: felt252, team_id: felt252, role: Role) -> Member { - // [Check] Inputs - MemberAssert::assert_valid_account_id(account_id); - MemberAssert::assert_valid_team_id(team_id); - MemberAssert::assert_valid_role(role); - // [Return] Member - Member { account_id: account_id, team_id: team_id, role: role.into() } - } -} - -#[generate_trait] -impl MemberAssert of AssertTrait { - #[inline] - fn assert_does_not_exist(self: @Member) { - assert(self.role == @Role::None.into(), errors::MEMBER_ALREADY_EXISTS); - } - - #[inline] - fn assert_does_exist(self: @Member) { - assert(self.role != @Role::None.into(), errors::MEMBER_NOT_EXIST); - } - - #[inline] - fn assert_valid_account_id(account_id: felt252) { - assert(account_id != 0, errors::MEMBER_INVALID_ACCOUNT_ID); - } - - #[inline] - fn assert_valid_team_id(team_id: felt252) { - assert(team_id != 0, errors::MEMBER_INVALID_TEAM_ID); - } - - #[inline] - fn assert_valid_role(role: Role) { - assert(role != Role::None, errors::MEMBER_INVALID_ROLE); - } -} - -#[cfg(test)] -mod tests { - // Local imports - - use super::{Member, MemberTrait, MemberAssert, Role}; - - // Constants - - const ACCOUNT_ID: felt252 = 'ACCOUNT_ID'; - const TEAM_ID: felt252 = 'TEAM_ID'; - const ROLE: Role = Role::Admin; - - #[test] - fn test_deployment_new() { - let member = MemberTrait::new(ACCOUNT_ID, TEAM_ID, ROLE); - assert_eq!(member.account_id, ACCOUNT_ID); - assert_eq!(member.team_id, TEAM_ID); - assert_eq!(member.role, ROLE.into()); - } - - #[test] - fn test_deployment_assert_does_exist() { - let member = MemberTrait::new(ACCOUNT_ID, TEAM_ID, ROLE); - member.assert_does_exist(); - } - - #[test] - #[should_panic(expected: 'Member: already exists')] - fn test_deployment_revert_already_exists() { - let member = MemberTrait::new(ACCOUNT_ID, TEAM_ID, ROLE); - member.assert_does_not_exist(); - } - - #[test] - #[should_panic(expected: 'Member: invalid role')] - fn test_deployment_revert_invalid_role() { - MemberTrait::new(ACCOUNT_ID, TEAM_ID, Role::None); - } -} diff --git a/packages/controller/src/models/signer.cairo b/packages/controller/src/models/signer.cairo index bb76e27..a6c213c 100644 --- a/packages/controller/src/models/signer.cairo +++ b/packages/controller/src/models/signer.cairo @@ -1,4 +1,4 @@ -// Intenral imports +// Internal imports use controller::models::index::Signer; use controller::types::method::Method; @@ -77,7 +77,7 @@ mod tests { const METHOD: Method = Method::StarknetAccount; #[test] - fn test_deployment_new() { + fn test_signer_new() { let signer = SignerTrait::new(ACCOUNT_ID, CONTROLLER_ID, METHOD, ""); assert_eq!(signer.account_id, ACCOUNT_ID); assert_eq!(signer.controller_id, CONTROLLER_ID); @@ -86,33 +86,33 @@ mod tests { } #[test] - fn test_deployment_assert_does_exist() { + fn test_signer_assert_does_exist() { let signer = SignerTrait::new(ACCOUNT_ID, CONTROLLER_ID, METHOD, ""); signer.assert_does_exist(); } #[test] #[should_panic(expected: 'Signer: already exists')] - fn test_deployment_revert_already_exists() { + fn test_signer_revert_already_exists() { let signer = SignerTrait::new(ACCOUNT_ID, CONTROLLER_ID, METHOD, ""); signer.assert_does_not_exist(); } #[test] #[should_panic(expected: 'Signer: invalid account id')] - fn test_deployment_revert_invalid_account_id() { + fn test_signer_revert_invalid_account_id() { SignerTrait::new(0, CONTROLLER_ID, METHOD, ""); } #[test] #[should_panic(expected: 'Signer: invalid controller id')] - fn test_deployment_revert_invalid_controller_id() { + fn test_signer_revert_invalid_controller_id() { SignerTrait::new(ACCOUNT_ID, 0, METHOD, ""); } #[test] #[should_panic(expected: 'Signer: invalid method')] - fn test_deployment_revert_invalid_method() { + fn test_signer_revert_invalid_method() { SignerTrait::new(ACCOUNT_ID, CONTROLLER_ID, Method::None, ""); } } diff --git a/packages/controller/src/store.cairo b/packages/controller/src/store.cairo index 96473e7..d652149 100644 --- a/packages/controller/src/store.cairo +++ b/packages/controller/src/store.cairo @@ -13,9 +13,7 @@ use dojo::model::ModelStorage; use controller::models::account::Account; use controller::models::controller::Controller; -use controller::models::member::Member; use controller::models::signer::Signer; -use controller::models::team::Team; // Structs @@ -44,21 +42,11 @@ impl StoreImpl of StoreTrait { self.world.read_model(controller_id) } - #[inline] - fn get_member(self: Store, account_id: felt252, team_id: felt252) -> Member { - self.world.read_model((account_id, team_id)) - } - #[inline] fn get_signer(self: Store, signer_id: felt252) -> Signer { self.world.read_model(signer_id) } - #[inline] - fn get_team(self: Store, team_id: felt252) -> Team { - self.world.read_model(team_id) - } - #[inline] fn set_account(ref self: Store, account: @Account) { self.world.write_model(account); @@ -68,14 +56,4 @@ impl StoreImpl of StoreTrait { fn set_controller(ref self: Store, controller: @Controller) { self.world.write_model(controller); } - - #[inline] - fn set_member(ref self: Store, member: @Member) { - self.world.write_model(member); - } - - #[inline] - fn set_team(ref self: Store, team: @Team) { - self.world.write_model(team); - } } diff --git a/packages/provider/src/components/deployable.cairo b/packages/provider/src/components/deployable.cairo index e2668fa..4cd0d47 100644 --- a/packages/provider/src/components/deployable.cairo +++ b/packages/provider/src/components/deployable.cairo @@ -10,8 +10,11 @@ mod DeployableComponent { use provider::models::deployment::{Deployment, DeploymentTrait, DeploymentAssert}; use provider::models::factory::{Factory, FactoryTrait, FactoryAssert}; use provider::types::service::{Service, ServiceTrait, SERVICE_COUNT}; + use provider::models::team::{Team, TeamTrait, TeamAssert}; + use provider::models::teammate::{Teammate, TeammateTrait, TeammateAssert}; use provider::types::status::Status; use provider::types::tier::Tier; + use provider::types::role::Role; // Storage @@ -45,6 +48,7 @@ mod DeployableComponent { fn deploy( self: @ComponentState, world: WorldStorage, + caller_id: felt252, service: Service, project: felt252, tier: Tier, @@ -60,10 +64,29 @@ mod DeployableComponent { let deployment = store.get_deployment(service.into(), project); deployment.assert_does_not_exist(); + // [Check] Caller permission + let mut team = store.get_team(project); + if team.exists() { + // [Check] Caller is at least an admin + let teammate = store.get_teammate(project, team.time, caller_id); + teammate.assert_is_allowed(Role::Admin); + // [Effect] Increment deployment count + team.deploy(); + store.set_team(@team); + } else { + // [Effect] Create team + let time = starknet::get_block_timestamp(); + let mut team = TeamTrait::new(project, time, project, ""); + team.deploy(); + store.set_team(@team); + // [Effect] Create teammate + let teammate = TeammateTrait::new(project, time, caller_id, Role::Owner); + store.set_teammate(@teammate); + } + // [Effect] Create deployment - let owner = starknet::get_caller_address().into(); let deployment = DeploymentTrait::new( - service: service, project: project, owner: owner, tier: tier, config: "", + service: service, project: project, tier: tier, config: "", ); store.set_deployment(@deployment); } @@ -71,6 +94,7 @@ mod DeployableComponent { fn remove( self: @ComponentState, world: WorldStorage, + caller_id: felt252, service: Service, project: felt252, ) { @@ -85,12 +109,28 @@ mod DeployableComponent { let mut deployment = store.get_deployment(service.into(), project); deployment.assert_does_exist(); - // [Check] Caller is owner - deployment.assert_is_owner(starknet::get_caller_address().into()); + // [Check] Team exists + let mut team = store.get_team(project); + team.assert_does_exist(); + + // [Check] Caller is at least admin + let teammate = store.get_teammate(project, team.time, caller_id); + teammate.assert_is_allowed(Role::Admin); // [Effect] Delete deployment deployment.nullify(); store.delete_deployment(@deployment); + + // [Effect] Decrement deployment count + team.remove(); + + // [Effect] Delete team if no deployments left + if team.deployment_count == 0 { + team.nullify(); + store.delete_team(@team); + } else { + store.set_team(@team); + } } } } diff --git a/packages/provider/src/components/groupable.cairo b/packages/provider/src/components/groupable.cairo new file mode 100644 index 0000000..d7af41b --- /dev/null +++ b/packages/provider/src/components/groupable.cairo @@ -0,0 +1,87 @@ +#[starknet::component] +mod GroupableComponent { + // Dojo imports + + use dojo::world::WorldStorage; + + // Internal imports + + use provider::store::{Store, StoreTrait}; + use provider::models::team::{Team, TeamTrait, TeamAssert}; + use provider::models::teammate::{Teammate, TeammateTrait, TeammateAssert}; + use provider::types::role::Role; + + // Storage + + #[storage] + struct Storage {} + + // Events + + #[event] + #[derive(Drop, starknet::Event)] + enum Event {} + + #[generate_trait] + impl InternalImpl< + TContractState, +HasComponent + > of InternalTrait { + fn add( + self: @ComponentState, + world: WorldStorage, + caller_id: felt252, + name: felt252, + account_id: felt252, + role: Role, + ) { + // [Setup] Datastore + let mut store: Store = StoreTrait::new(world); + + // [Check] Team exists + let team = store.get_team(name); + team.assert_does_exist(); + + // [Check] Caller is at least admin + let callermate = store.get_teammate(name, team.time, caller_id); + callermate.assert_is_allowed(Role::Admin); + + // [Check] Teammate does not exist + let teammate = store.get_teammate(name, team.time, account_id); + teammate.assert_does_not_exist(); + + // [Effect] Create teammate + let teammate = TeammateTrait::new(name, team.time, account_id, role); + store.set_teammate(@teammate); + } + + fn remove( + self: @ComponentState, + world: WorldStorage, + caller_id: felt252, + name: felt252, + account_id: felt252, + ) { + // [Setup] Datastore + let mut store: Store = StoreTrait::new(world); + + // [Check] Team exists + let team = store.get_team(name); + team.assert_does_exist(); + + // [Check] Caller is at least admin + let callermate = store.get_teammate(name, team.time, caller_id); + callermate.assert_is_allowed(Role::Admin); + + // [Check] Teammate exists + let mut teammate = store.get_teammate(name, team.time, account_id); + teammate.assert_does_exist(); + + // [Check] Caller has greater role than teammate + callermate.assert_is_greater(teammate.role.into()); + + // [Effect] Delete teammate + teammate.nullify(); + store.delete_teammate(@teammate); + } + } +} diff --git a/packages/provider/src/lib.cairo b/packages/provider/src/lib.cairo index 8277531..3fbf36b 100644 --- a/packages/provider/src/lib.cairo +++ b/packages/provider/src/lib.cairo @@ -11,6 +11,7 @@ mod elements { } mod types { + mod role; mod tier; mod service; mod status; @@ -20,8 +21,11 @@ mod models { mod index; mod deployment; mod factory; + mod teammate; + mod team; } mod components { mod deployable; + mod groupable; } diff --git a/packages/provider/src/models/deployment.cairo b/packages/provider/src/models/deployment.cairo index d36653c..63f428b 100644 --- a/packages/provider/src/models/deployment.cairo +++ b/packages/provider/src/models/deployment.cairo @@ -1,4 +1,4 @@ -// Intenral imports +// Internal imports use provider::models::index::Deployment; use provider::types::service::Service; @@ -12,28 +12,22 @@ pub mod errors { pub const DEPLOYMENT_NOT_EXIST: felt252 = 'Deployment: does not exist'; pub const DEPLOYMENT_INVALID_SERVICE: felt252 = 'Deployment: invalid service'; pub const DEPLOYMENT_INVALID_PROJECT: felt252 = 'Deployment: invalid project'; - pub const DEPLOYMENT_INVALID_OWNER: felt252 = 'Deployment: invalid owner'; pub const DEPLOYMENT_INVALID_STATUS: felt252 = 'Deployment: invalid status'; pub const DEPLOYMENT_INVALID_TIER: felt252 = 'Deployment: invalid tier'; - pub const DEPLOYMENT_NOT_OWNER: felt252 = 'Deployment: caller is not owner'; } #[generate_trait] impl DeploymentImpl of DeploymentTrait { #[inline] - fn new( - service: Service, project: felt252, owner: felt252, tier: Tier, config: ByteArray, - ) -> Deployment { + fn new(service: Service, project: felt252, tier: Tier, config: ByteArray,) -> Deployment { // [Check] Inputs DeploymentAssert::assert_valid_service(service); DeploymentAssert::assert_valid_project(project); - DeploymentAssert::assert_valid_owner(owner); DeploymentAssert::assert_valid_tier(tier); // [Return] Deployment Deployment { service: service.into(), project: project, - owner: owner, status: Status::Disabled.into(), tier: tier.into(), config: config, @@ -42,7 +36,7 @@ impl DeploymentImpl of DeploymentTrait { #[inline] fn nullify(ref self: Deployment) { - self.owner = 0; + self.project = 0; } } @@ -50,12 +44,12 @@ impl DeploymentImpl of DeploymentTrait { impl DeploymentAssert of AssertTrait { #[inline] fn assert_does_not_exist(self: @Deployment) { - assert(self.owner == @0, errors::DEPLOYMENT_ALREADY_EXISTS); + assert(self.project == @0, errors::DEPLOYMENT_ALREADY_EXISTS); } #[inline] fn assert_does_exist(self: @Deployment) { - assert(self.owner != @0, errors::DEPLOYMENT_NOT_EXIST); + assert(self.project != @0, errors::DEPLOYMENT_NOT_EXIST); } #[inline] @@ -68,11 +62,6 @@ impl DeploymentAssert of AssertTrait { assert(project != 0, errors::DEPLOYMENT_INVALID_PROJECT); } - #[inline] - fn assert_valid_owner(owner: felt252) { - assert(owner != 0, errors::DEPLOYMENT_INVALID_OWNER); - } - #[inline] fn assert_valid_status(status: Status) { assert(status != Status::None, errors::DEPLOYMENT_INVALID_STATUS); @@ -82,11 +71,6 @@ impl DeploymentAssert of AssertTrait { fn assert_valid_tier(tier: Tier) { assert(tier != Tier::None, errors::DEPLOYMENT_INVALID_TIER); } - - #[inline] - fn assert_is_owner(self: @Deployment, caller: felt252) { - assert(@caller == self.owner, errors::DEPLOYMENT_NOT_OWNER); - } } #[cfg(test)] @@ -99,43 +83,27 @@ mod tests { const SERVICE: Service = Service::Katana; const PROJECT: felt252 = 'PROJECT'; - const OWNER: felt252 = 'OWNER'; const TIER: Tier = Tier::Basic; - const NOT_OWNER: felt252 = 'NOT_OWNER'; #[test] fn test_deployment_new() { - let deployment = DeploymentTrait::new(SERVICE, PROJECT, OWNER, TIER, ""); + let deployment = DeploymentTrait::new(SERVICE, PROJECT, TIER, ""); assert_eq!(deployment.service, SERVICE.into()); assert_eq!(deployment.project, PROJECT); - assert_eq!(deployment.owner, OWNER); assert_eq!(deployment.tier, TIER.into()); assert_eq!(deployment.config, ""); } #[test] fn test_deployment_assert_does_exist() { - let deployment = DeploymentTrait::new(SERVICE, PROJECT, OWNER, TIER, ""); + let deployment = DeploymentTrait::new(SERVICE, PROJECT, TIER, ""); deployment.assert_does_exist(); } #[test] #[should_panic(expected: 'Deployment: already exists')] fn test_deployment_revert_already_exists() { - let mut deployment = DeploymentTrait::new(SERVICE, PROJECT, OWNER, TIER, ""); + let mut deployment = DeploymentTrait::new(SERVICE, PROJECT, TIER, ""); deployment.assert_does_not_exist(); } - - #[test] - fn test_deployment_assert_is_owner() { - let deployment = DeploymentTrait::new(SERVICE, PROJECT, OWNER, TIER, ""); - deployment.assert_is_owner(OWNER); - } - - #[test] - #[should_panic(expected: 'Deployment: caller is not owner')] - fn test_deployment_revert_not_owner() { - let deployment = DeploymentTrait::new(SERVICE, PROJECT, OWNER, TIER, ""); - deployment.assert_is_owner(NOT_OWNER); - } } diff --git a/packages/provider/src/models/factory.cairo b/packages/provider/src/models/factory.cairo index 985e54f..fccbe0f 100644 --- a/packages/provider/src/models/factory.cairo +++ b/packages/provider/src/models/factory.cairo @@ -1,4 +1,4 @@ -// Intenral imports +// Internal imports use provider::models::index::Factory; use provider::types::service::Service; diff --git a/packages/provider/src/models/index.cairo b/packages/provider/src/models/index.cairo index f44d853..8bc95eb 100644 --- a/packages/provider/src/models/index.cairo +++ b/packages/provider/src/models/index.cairo @@ -7,7 +7,6 @@ pub struct Deployment { service: u8, #[key] project: felt252, - owner: felt252, status: u8, tier: u8, config: ByteArray, @@ -21,3 +20,26 @@ pub struct Factory { version: felt252, default_version: felt252, } + +#[derive(Clone, Drop, Serde)] +#[dojo::model] +pub struct Team { + #[key] + id: felt252, + deployment_count: u32, + time: u64, + name: felt252, + description: ByteArray, +} + +#[derive(Copy, Drop, Serde)] +#[dojo::model] +pub struct Teammate { + #[key] + team_id: felt252, + #[key] + time: u64, + #[key] + account_id: felt252, + role: u8, +} diff --git a/packages/controller/src/models/team.cairo b/packages/provider/src/models/team.cairo similarity index 50% rename from packages/controller/src/models/team.cairo rename to packages/provider/src/models/team.cairo index 68586ef..07c0d18 100644 --- a/packages/controller/src/models/team.cairo +++ b/packages/provider/src/models/team.cairo @@ -1,6 +1,7 @@ -// Intenral imports +// Internal imports -use controller::models::index::Team; +use provider::models::index::Team; +use provider::types::role::Role; // Errors @@ -8,18 +9,41 @@ pub mod errors { pub const TEAM_ALREADY_EXISTS: felt252 = 'Team: already exists'; pub const TEAM_NOT_EXIST: felt252 = 'Team: does not exist'; pub const TEAM_INVALID_IDENTIFIER: felt252 = 'Team: invalid identifier'; + pub const TEAM_INVALID_TIME: felt252 = 'Team: invalid time'; pub const TEAM_INVALID_NAME: felt252 = 'Team: invalid name'; } #[generate_trait] impl TeamImpl of TeamTrait { #[inline] - fn new(id: felt252, name: felt252, description: ByteArray) -> Team { + fn new(id: felt252, time: u64, name: felt252, description: ByteArray) -> Team { // [Check] Inputs TeamAssert::assert_valid_identifier(id); + TeamAssert::assert_valid_time(time); TeamAssert::assert_valid_name(name); // [Return] Team - Team { id: id, name: name, description: description, } + Team { id: id, deployment_count: 0, time: time, name: name, description: description } + } + + #[inline] + fn exists(self: @Team) -> bool { + self.name != @0 && self.time != @0 + } + + #[inline] + fn nullify(ref self: Team) { + self.time = 0; + self.name = 0; + } + + #[inline] + fn deploy(ref self: Team) { + self.deployment_count += 1; + } + + #[inline] + fn remove(ref self: Team) { + self.deployment_count -= 1; } } @@ -27,12 +51,12 @@ impl TeamImpl of TeamTrait { impl TeamAssert of AssertTrait { #[inline] fn assert_does_not_exist(self: @Team) { - assert(self.name == @0, errors::TEAM_ALREADY_EXISTS); + assert(!self.exists(), errors::TEAM_ALREADY_EXISTS); } #[inline] fn assert_does_exist(self: @Team) { - assert(self.name != @0, errors::TEAM_NOT_EXIST); + assert(self.exists(), errors::TEAM_NOT_EXIST); } #[inline] @@ -40,6 +64,11 @@ impl TeamAssert of AssertTrait { assert(identifier != 0, errors::TEAM_INVALID_IDENTIFIER); } + #[inline] + fn assert_valid_time(time: u64) { + assert(time != 0, errors::TEAM_INVALID_TIME); + } + #[inline] fn assert_valid_name(name: felt252) { assert(name != 0, errors::TEAM_INVALID_NAME); @@ -56,25 +85,27 @@ mod tests { const IDENTIFIER: felt252 = 'ID'; const NAME: felt252 = 'NAME'; + const TIME: u64 = 1; #[test] - fn test_deployment_new() { - let team = TeamTrait::new(IDENTIFIER, 'NAME', ""); + fn test_team_new() { + let team = TeamTrait::new(IDENTIFIER, TIME, NAME, ""); assert_eq!(team.id, IDENTIFIER); - assert_eq!(team.name, 'NAME'); + assert_eq!(team.time, TIME); + assert_eq!(team.name, NAME); assert_eq!(team.description, ""); } #[test] - fn test_deployment_assert_does_exist() { - let team = TeamTrait::new(IDENTIFIER, 'NAME', ""); + fn test_team_assert_does_exist() { + let team = TeamTrait::new(IDENTIFIER, TIME, NAME, ""); team.assert_does_exist(); } #[test] #[should_panic(expected: 'Team: already exists')] - fn test_deployment_revert_already_exists() { - let team = TeamTrait::new(IDENTIFIER, 'NAME', ""); + fn test_team_revert_already_exists() { + let team = TeamTrait::new(IDENTIFIER, TIME, NAME, ""); team.assert_does_not_exist(); } } diff --git a/packages/provider/src/models/teammate.cairo b/packages/provider/src/models/teammate.cairo new file mode 100644 index 0000000..9716acf --- /dev/null +++ b/packages/provider/src/models/teammate.cairo @@ -0,0 +1,120 @@ +// Internal imports + +use provider::models::index::Teammate; +use provider::types::role::Role; + +// Errors + +pub mod errors { + pub const TEAMMATE_ALREADY_EXISTS: felt252 = 'Teammate: already exists'; + pub const TEAMMATE_NOT_EXIST: felt252 = 'Teammate: does not exist'; + pub const TEAMMATE_INVALID_ACCOUNT_ID: felt252 = 'Teammate: invalid account id'; + pub const TEAMMATE_INVALID_TEAM_ID: felt252 = 'Teammate: invalid team id'; + pub const TEAMMATE_INVALID_TIME: felt252 = 'Teammate: invalid time'; + pub const TEAMMATE_INVALID_ROLE: felt252 = 'Teammate: invalid role'; + pub const TEAMMATE_NOT_ALLOWED: felt252 = 'Teammate: caller is not allowed'; + pub const TEAMMATE_NOT_ENOUGH_PERMISSION: felt252 = 'Teammate: not enough permission'; +} + +#[generate_trait] +impl TeammateImpl of TeammateTrait { + #[inline] + fn new(team_id: felt252, time: u64, account_id: felt252, role: Role) -> Teammate { + // [Check] Inputs + TeammateAssert::assert_valid_team_id(team_id); + TeammateAssert::assert_valid_time(time); + TeammateAssert::assert_valid_account_id(account_id); + TeammateAssert::assert_valid_role(role); + // [Return] Teammate + Teammate { team_id: team_id, time: time, account_id: account_id, role: role.into() } + } + + #[inline] + fn nullify(ref self: Teammate) { + self.role = Role::None.into(); + } +} + +#[generate_trait] +impl TeammateAssert of AssertTrait { + #[inline] + fn assert_does_not_exist(self: @Teammate) { + assert(self.role == @Role::None.into(), errors::TEAMMATE_ALREADY_EXISTS); + } + + #[inline] + fn assert_does_exist(self: @Teammate) { + assert(self.role != @Role::None.into(), errors::TEAMMATE_NOT_EXIST); + } + + #[inline] + fn assert_valid_account_id(account_id: felt252) { + assert(account_id != 0, errors::TEAMMATE_INVALID_ACCOUNT_ID); + } + + #[inline] + fn assert_valid_team_id(team_id: felt252) { + assert(team_id != 0, errors::TEAMMATE_INVALID_TEAM_ID); + } + + #[inline] + fn assert_valid_time(time: u64) { + assert(time != 0, errors::TEAMMATE_INVALID_TIME); + } + + #[inline] + fn assert_valid_role(role: Role) { + assert(role != Role::None, errors::TEAMMATE_INVALID_ROLE); + } + + #[inline] + fn assert_is_allowed(self: @Teammate, role: Role) { + assert(self.role >= @role.into(), errors::TEAMMATE_NOT_ALLOWED); + } + + #[inline] + fn assert_is_greater(self: @Teammate, role: Role) { + assert(self.role > @role.into(), errors::TEAMMATE_NOT_ENOUGH_PERMISSION); + } +} + +#[cfg(test)] +mod tests { + // Local imports + + use super::{Teammate, TeammateTrait, TeammateAssert, Role}; + + // Constants + + const TEAM_ID: felt252 = 'TEAM_ID'; + const TIME: u64 = 1; + const ACCOUNT_ID: felt252 = 'ACCOUNT_ID'; + const ROLE: Role = Role::Admin; + + #[test] + fn test_teammate_new() { + let member = TeammateTrait::new(TEAM_ID, TIME, ACCOUNT_ID, ROLE); + assert_eq!(member.account_id, ACCOUNT_ID); + assert_eq!(member.team_id, TEAM_ID); + assert_eq!(member.role, ROLE.into()); + } + + #[test] + fn test_teammate_assert_does_exist() { + let member = TeammateTrait::new(TEAM_ID, TIME, ACCOUNT_ID, ROLE); + member.assert_does_exist(); + } + + #[test] + #[should_panic(expected: 'Teammate: already exists')] + fn test_teammate_revert_already_exists() { + let member = TeammateTrait::new(TEAM_ID, TIME, ACCOUNT_ID, ROLE); + member.assert_does_not_exist(); + } + + #[test] + #[should_panic(expected: 'Teammate: invalid role')] + fn test_teammate_revert_invalid_role() { + TeammateTrait::new(TEAM_ID, TIME, ACCOUNT_ID, Role::None); + } +} diff --git a/packages/provider/src/store.cairo b/packages/provider/src/store.cairo index 0d53989..f6d2ba0 100644 --- a/packages/provider/src/store.cairo +++ b/packages/provider/src/store.cairo @@ -13,6 +13,8 @@ use dojo::model::ModelStorage; use provider::models::deployment::Deployment; use provider::models::factory::Factory; +use provider::models::index::Team; +use provider::models::index::Teammate; // Structs @@ -40,6 +42,16 @@ impl StoreImpl of StoreTrait { self.world.read_model(factory_id) } + #[inline] + fn get_team(self: Store, team_id: felt252) -> Team { + self.world.read_model(team_id) + } + + #[inline] + fn get_teammate(self: Store, team_id: felt252, time: u64, account_id: felt252) -> Teammate { + self.world.read_model((team_id, time, account_id)) + } + #[inline] fn set_deployment(ref self: Store, deployment: @Deployment) { self.world.write_model(deployment); @@ -50,8 +62,28 @@ impl StoreImpl of StoreTrait { self.world.write_model(factory); } + #[inline] + fn set_team(ref self: Store, team: @Team) { + self.world.write_model(team); + } + + #[inline] + fn set_teammate(ref self: Store, teammate: @Teammate) { + self.world.write_model(teammate); + } + #[inline] fn delete_deployment(ref self: Store, deployment: @Deployment) { self.world.erase_model(deployment); } + + #[inline] + fn delete_team(ref self: Store, team: @Team) { + self.world.erase_model(team); + } + + #[inline] + fn delete_teammate(ref self: Store, teammate: @Teammate) { + self.world.erase_model(teammate); + } } diff --git a/packages/controller/src/types/role.cairo b/packages/provider/src/types/role.cairo similarity index 80% rename from packages/controller/src/types/role.cairo rename to packages/provider/src/types/role.cairo index ae3b9c1..abf5492 100644 --- a/packages/controller/src/types/role.cairo +++ b/packages/provider/src/types/role.cairo @@ -1,9 +1,9 @@ #[derive(Copy, Drop, PartialEq)] pub enum Role { None, - Owner, - Admin, Member, + Admin, + Owner, } // Implementations @@ -13,9 +13,9 @@ impl IntoRoleU8 of core::Into { fn into(self: Role) -> u8 { match self { Role::None => 0, - Role::Owner => 1, + Role::Member => 1, Role::Admin => 2, - Role::Member => 3, + Role::Owner => 3, } } } @@ -25,9 +25,9 @@ impl IntoU8Role of core::Into { fn into(self: u8) -> Role { match self { 0 => Role::None, - 1 => Role::Owner, + 1 => Role::Member, 2 => Role::Admin, - 3 => Role::Member, + 3 => Role::Owner, _ => Role::None, } } diff --git a/packages/registry/src/components/registerable.cairo b/packages/registry/src/components/registerable.cairo index 56ed296..89e9521 100644 --- a/packages/registry/src/components/registerable.cairo +++ b/packages/registry/src/components/registerable.cairo @@ -31,6 +31,7 @@ mod RegisterableComponent { fn register( self: @ComponentState, world: WorldStorage, + caller_id: felt252, world_address: felt252, namespace: felt252, project: felt252, @@ -55,8 +56,9 @@ mod RegisterableComponent { // [Effect] Create game let metadata = MetadataTrait::new(color, name, description, image, banner); let socials = SocialsTrait::new(discord, telegram, twitter, youtube, website); - let owner: felt252 = starknet::get_caller_address().into(); - let game = GameTrait::new(world_address, namespace, project, metadata, socials, owner); + let game = GameTrait::new( + world_address, namespace, project, metadata, socials, caller_id + ); // [Effect] Store game store.set_game(@game); @@ -65,6 +67,7 @@ mod RegisterableComponent { fn update( self: @ComponentState, world: WorldStorage, + caller_id: felt252, world_address: felt252, namespace: felt252, color: Option, @@ -86,7 +89,7 @@ mod RegisterableComponent { game.assert_does_exist(); // [Check] Caller is owner - game.assert_is_owner(starknet::get_caller_address().into()); + game.assert_is_owner(caller_id); // [Effect] Update game let metadata = MetadataTrait::new(color, name, description, image, banner); @@ -100,6 +103,7 @@ mod RegisterableComponent { fn publish( self: @ComponentState, world: WorldStorage, + caller_id: felt252, world_address: felt252, namespace: felt252, ) { @@ -111,7 +115,7 @@ mod RegisterableComponent { game.assert_does_exist(); // [Check] Caller is owner - game.assert_is_owner(starknet::get_caller_address().into()); + game.assert_is_owner(caller_id); // [Effect] Publish game game.publish(); @@ -123,6 +127,7 @@ mod RegisterableComponent { fn hide( self: @ComponentState, world: WorldStorage, + caller_id: felt252, world_address: felt252, namespace: felt252, ) { @@ -134,7 +139,7 @@ mod RegisterableComponent { game.assert_does_exist(); // [Check] Caller is owner - game.assert_is_owner(starknet::get_caller_address().into()); + game.assert_is_owner(caller_id); // [Effect] Hide game game.hide(); @@ -146,6 +151,7 @@ mod RegisterableComponent { fn whitelist( self: @ComponentState, world: WorldStorage, + caller_id: felt252, world_address: felt252, namespace: felt252, ) { @@ -153,8 +159,7 @@ mod RegisterableComponent { let mut store: Store = StoreTrait::new(world); // [Check] Caller is allowed - let caller = starknet::get_caller_address().into(); - let access = store.get_access(caller); + let access = store.get_access(caller_id); access.assert_is_allowed(Role::Admin); // [Check] Game exists @@ -171,6 +176,7 @@ mod RegisterableComponent { fn blacklist( self: @ComponentState, world: WorldStorage, + caller_id: felt252, world_address: felt252, namespace: felt252, ) { @@ -178,8 +184,7 @@ mod RegisterableComponent { let mut store: Store = StoreTrait::new(world); // [Check] Caller is allowed - let caller = starknet::get_caller_address().into(); - let access = store.get_access(caller); + let access = store.get_access(caller_id); access.assert_is_allowed(Role::Admin); // [Check] Game exists @@ -196,6 +201,7 @@ mod RegisterableComponent { fn remove( self: @ComponentState, world: WorldStorage, + caller_id: felt252, world_address: felt252, namespace: felt252, ) { @@ -206,6 +212,9 @@ mod RegisterableComponent { let mut game = store.get_game(world_address, namespace); game.assert_does_exist(); + // [Check] Caller is owner + game.assert_is_owner(caller_id); + // [Effect] Remove game game.nullify(); diff --git a/packages/registry/src/components/trackable.cairo b/packages/registry/src/components/trackable.cairo index 3bb1ddd..65ddbc8 100644 --- a/packages/registry/src/components/trackable.cairo +++ b/packages/registry/src/components/trackable.cairo @@ -30,6 +30,7 @@ mod TrackableComponent { fn register( self: @ComponentState, world: WorldStorage, + caller_id: felt252, world_address: felt252, namespace: felt252, identifier: felt252, @@ -43,7 +44,7 @@ mod TrackableComponent { game.assert_does_exist(); // [Check] Caller is owner - game.assert_is_owner(starknet::get_caller_address().into()); + game.assert_is_owner(caller_id); // [Check] Achievement does not exist let achievement = store.get_achievement(world_address, namespace, identifier); @@ -63,6 +64,7 @@ mod TrackableComponent { fn update( self: @ComponentState, world: WorldStorage, + caller_id: felt252, world_address: felt252, namespace: felt252, identifier: felt252, @@ -76,7 +78,7 @@ mod TrackableComponent { game.assert_does_exist(); // [Check] Caller is owner - game.assert_is_owner(starknet::get_caller_address().into()); + game.assert_is_owner(caller_id); // [Check] Achievement exists let mut achievement = store.get_achievement(world_address, namespace, identifier); @@ -95,6 +97,7 @@ mod TrackableComponent { fn publish( self: @ComponentState, world: WorldStorage, + caller_id: felt252, world_address: felt252, namespace: felt252, identifier: felt252, @@ -107,7 +110,7 @@ mod TrackableComponent { game.assert_does_exist(); // [Check] Caller is owner - game.assert_is_owner(starknet::get_caller_address().into()); + game.assert_is_owner(caller_id); // [Check] Achievement exists let mut achievement = store.get_achievement(world_address, namespace, identifier); @@ -123,6 +126,7 @@ mod TrackableComponent { fn hide( self: @ComponentState, world: WorldStorage, + caller_id: felt252, world_address: felt252, namespace: felt252, identifier: felt252, @@ -135,7 +139,7 @@ mod TrackableComponent { game.assert_does_exist(); // [Check] Caller is owner - game.assert_is_owner(starknet::get_caller_address().into()); + game.assert_is_owner(caller_id); // [Check] Achievement exists let mut achievement = store.get_achievement(world_address, namespace, identifier); @@ -151,6 +155,7 @@ mod TrackableComponent { fn whitelist( self: @ComponentState, world: WorldStorage, + caller_id: felt252, world_address: felt252, namespace: felt252, identifier: felt252, @@ -159,8 +164,7 @@ mod TrackableComponent { let mut store: Store = StoreTrait::new(world); // [Check] Caller is allowed - let caller = starknet::get_caller_address().into(); - let access = store.get_access(caller); + let access = store.get_access(caller_id); access.assert_is_allowed(Role::Admin); // [Check] Game exists @@ -181,6 +185,7 @@ mod TrackableComponent { fn blacklist( self: @ComponentState, world: WorldStorage, + caller_id: felt252, world_address: felt252, namespace: felt252, identifier: felt252, @@ -189,8 +194,7 @@ mod TrackableComponent { let mut store: Store = StoreTrait::new(world); // [Check] Caller is allowed - let caller = starknet::get_caller_address().into(); - let access = store.get_access(caller); + let access = store.get_access(caller_id); access.assert_is_allowed(Role::Admin); // [Check] Game exists @@ -211,6 +215,7 @@ mod TrackableComponent { fn remove( self: @ComponentState, world: WorldStorage, + caller_id: felt252, world_address: felt252, namespace: felt252, identifier: felt252, @@ -222,6 +227,9 @@ mod TrackableComponent { let mut game = store.get_game(world_address, namespace); game.assert_does_exist(); + // [Check] Caller is owner + game.assert_is_owner(caller_id); + // [Check] Achievement exists let mut achievement = store.get_achievement(world_address, namespace, identifier); achievement.assert_does_exist(); diff --git a/packages/registry/src/models/access.cairo b/packages/registry/src/models/access.cairo index fb4c5ac..40e399b 100644 --- a/packages/registry/src/models/access.cairo +++ b/packages/registry/src/models/access.cairo @@ -1,4 +1,4 @@ -// Intenral imports +// Internal imports use registry::constants; use registry::models::index::Access; diff --git a/packages/registry/src/models/achievement.cairo b/packages/registry/src/models/achievement.cairo index 902c84d..09aca09 100644 --- a/packages/registry/src/models/achievement.cairo +++ b/packages/registry/src/models/achievement.cairo @@ -1,4 +1,4 @@ -// Intenral imports +// Internal imports use registry::models::index::Achievement; use registry::constants; diff --git a/packages/registry/src/models/game.cairo b/packages/registry/src/models/game.cairo index 6e87282..ae5c6d9 100644 --- a/packages/registry/src/models/game.cairo +++ b/packages/registry/src/models/game.cairo @@ -1,4 +1,4 @@ -// Intenral imports +// Internal imports use registry::constants; use registry::models::index::Game; diff --git a/packages/registry/src/tests/mocks/register.cairo b/packages/registry/src/tests/mocks/register.cairo index 32722fa..2dd246c 100644 --- a/packages/registry/src/tests/mocks/register.cairo +++ b/packages/registry/src/tests/mocks/register.cairo @@ -103,10 +103,13 @@ pub mod Register { youtube: Option, website: Option, ) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); self .registerable .register( - self.world_storage(), + world, + caller, world_address, namespace, project, @@ -119,7 +122,7 @@ pub mod Register { telegram, twitter, youtube, - website, + website ); } @@ -138,10 +141,13 @@ pub mod Register { youtube: Option, website: Option, ) { + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); self .registerable .update( - self.world_storage(), + world, + caller, world_address, namespace, color, @@ -158,19 +164,27 @@ pub mod Register { } fn publish(self: @ContractState, world_address: felt252, namespace: felt252) { - self.registerable.publish(self.world_storage(), world_address, namespace); + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.registerable.publish(world, caller, world_address, namespace); } fn hide(self: @ContractState, world_address: felt252, namespace: felt252) { - self.registerable.hide(self.world_storage(), world_address, namespace); + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.registerable.hide(world, caller, world_address, namespace); } fn whitelist(self: @ContractState, world_address: felt252, namespace: felt252) { - self.registerable.whitelist(self.world_storage(), world_address, namespace); + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.registerable.whitelist(world, caller, world_address, namespace); } fn blacklist(self: @ContractState, world_address: felt252, namespace: felt252) { - self.registerable.blacklist(self.world_storage(), world_address, namespace); + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.registerable.blacklist(world, caller, world_address, namespace); } } diff --git a/packages/registry/src/tests/mocks/tracker.cairo b/packages/registry/src/tests/mocks/tracker.cairo index cc6a57c..cc6c875 100644 --- a/packages/registry/src/tests/mocks/tracker.cairo +++ b/packages/registry/src/tests/mocks/tracker.cairo @@ -83,9 +83,9 @@ pub mod Tracker { identifier: felt252, karma: u16, ) { - self - .trackable - .register(self.world_storage(), world_address, namespace, identifier, karma); + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.trackable.register(world, caller, world_address, namespace, identifier, karma); } fn update( @@ -95,33 +95,41 @@ pub mod Tracker { identifier: felt252, karma: u16, ) { - self - .trackable - .update(self.world_storage(), world_address, namespace, identifier, karma); + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.trackable.update(world, caller, world_address, namespace, identifier, karma); } fn publish( self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 ) { - self.trackable.publish(self.world_storage(), world_address, namespace, identifier); + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.trackable.publish(world, caller, world_address, namespace, identifier); } fn hide( self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 ) { - self.trackable.hide(self.world_storage(), world_address, namespace, identifier); + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.trackable.hide(world, caller, world_address, namespace, identifier); } fn whitelist( self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 ) { - self.trackable.whitelist(self.world_storage(), world_address, namespace, identifier); + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.trackable.whitelist(world, caller, world_address, namespace, identifier); } fn blacklist( self: @ContractState, world_address: felt252, namespace: felt252, identifier: felt252 ) { - self.trackable.blacklist(self.world_storage(), world_address, namespace, identifier); + let world = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.trackable.blacklist(world, caller, world_address, namespace, identifier); } } diff --git a/packages/society/README.md b/packages/society/README.md new file mode 100644 index 0000000..e69de29 diff --git a/packages/society/Scarb.toml b/packages/society/Scarb.toml new file mode 100644 index 0000000..3a8e2fe --- /dev/null +++ b/packages/society/Scarb.toml @@ -0,0 +1,10 @@ +[package] +name = "society" +version.workspace = true + +[dependencies] +registry = { path = "../registry" } +dojo.workspace = true + +[dev-dependencies] +dojo_cairo_test.workspace = true diff --git a/packages/society/src/components/allianceable.cairo b/packages/society/src/components/allianceable.cairo new file mode 100644 index 0000000..41bd7f5 --- /dev/null +++ b/packages/society/src/components/allianceable.cairo @@ -0,0 +1,291 @@ +#[starknet::component] +mod AllianceableComponent { + // Dojo imports + + use dojo::world::WorldStorage; + use dojo::world::IWorldDispatcherTrait; + + // External imports + + use registry::types::metadata::MetadataTrait; + use registry::types::socials::SocialsTrait; + + // Internal imports + + use society::store::{Store, StoreTrait}; + use society::models::alliance::{Alliance, AllianceTrait, AllianceAssert}; + use society::models::guild::{Guild, GuildTrait, GuildAssert}; + use society::models::member::{Member, MemberTrait, MemberAssert}; + use society::types::role::Role; + + // Storage + + #[storage] + struct Storage {} + + // Events + + #[event] + #[derive(Drop, starknet::Event)] + enum Event {} + + #[generate_trait] + impl InternalImpl< + TContractState, +HasComponent + > of InternalTrait { + fn create( + self: @ComponentState, + world: WorldStorage, + player_id: felt252, + color: Option, + name: Option, + description: Option, + image: Option, + banner: Option, + discord: Option, + telegram: Option, + twitter: Option, + youtube: Option, + website: Option + ) { + // [Setup] Datastore + let mut store = StoreTrait::new(world); + + // [Check] Member is a guild master + let member = store.get_member(player_id); + member.assert_is_allowed(Role::Master); + + // [Check] Guild can join an alliance + let mut guild = store.get_guild(member.guild_id); + guild.assert_can_join(); + + // [Effect] Create an alliance + let alliance_id = world.dispatcher.uuid(); + let metadata = MetadataTrait::new(color, name, description, image, banner); + let socials = SocialsTrait::new(discord, telegram, twitter, youtube, website); + let mut alliance = AllianceTrait::new(alliance_id, metadata, socials); + + // [Effect] Guild joins alliance + guild.join(alliance_id); + alliance.hire(); + + // [Effect] Guild becomes alliance master + guild.crown(); + + // [Effect] Store entities + store.set_guild(@guild); + store.set_alliance(@alliance); + } + + fn open( + self: @ComponentState, + world: WorldStorage, + player_id: felt252, + free: bool + ) { + // [Setup] Datastore + let mut store = StoreTrait::new(world); + + // [Check] Caller exists and is allowed + let member = store.get_member(player_id); + member.assert_is_allowed(Role::Master); + + // [Check] Guild exists and is the alliance master + let guild = store.get_guild(member.guild_id); + guild.assert_is_allowed(Role::Master); + + // [Effect] Alliance opens + let mut alliance = store.get_alliance(guild.alliance_id); + alliance.open(free); + + // [Effect] Store entities + store.set_alliance(@alliance); + } + + fn close(self: @ComponentState, world: WorldStorage, player_id: felt252) { + // [Setup] Datastore + let mut store = StoreTrait::new(world); + + // [Check] Caller exists and is allowed + let member = store.get_member(player_id); + member.assert_is_allowed(Role::Master); + + // [Check] Guild exists and is the alliance master + let guild = store.get_guild(member.guild_id); + guild.assert_is_allowed(Role::Master); + + // [Effect] Alliance closes + let mut alliance = store.get_alliance(guild.alliance_id); + alliance.close(); + + // [Effect] Store entities + store.set_alliance(@alliance); + } + + fn crown( + self: @ComponentState, + world: WorldStorage, + player_id: felt252, + guild_id: u32 + ) { + // [Setup] Datastore + let mut store = StoreTrait::new(world); + + // [Check] Caller exists and is allowed + let member = store.get_member(player_id); + member.assert_is_allowed(Role::Master); + + // [Check] Guild exists and is the alliance master + let mut master = store.get_guild(member.guild_id); + master.assert_is_allowed(Role::Master); + + // [Check] Guild is in the same alliance + let mut guild = store.get_guild(guild_id); + guild.assert_same_alliance(master.alliance_id); + + // [Effect] Transfer the master role + master.uncrown(); + guild.crown(); + + // [Effect] Store entities + store.set_guild(@master); + store.set_guild(@guild); + } + + fn hire( + self: @ComponentState, + world: WorldStorage, + player_id: felt252, + guild_id: u32 + ) { + // [Setup] Datastore + let mut store = StoreTrait::new(world); + + // [Check] Caller exists and is allowed + let member = store.get_member(player_id); + member.assert_is_allowed(Role::Master); + + // [Check] Guild exists and is the alliance master + let master = store.get_guild(member.guild_id); + master.assert_is_allowed(Role::Master); + + // [Check] Alliance is open + let mut alliance = store.get_alliance(master.alliance_id); + alliance.assert_is_open(); + + // [Effect] Member joins the guild and guild hires a member + let mut guild = store.get_guild(guild_id); + guild.join(alliance.id); + alliance.hire(); + + // [Effect] Store entities + store.set_alliance(@alliance); + store.set_guild(@guild); + } + + fn fire( + self: @ComponentState, + world: WorldStorage, + player_id: felt252, + guild_id: u32 + ) { + // [Setup] Datastore + let mut store = StoreTrait::new(world); + + // [Check] Caller exists and is allowed + let member = store.get_member(player_id); + member.assert_is_allowed(Role::Master); + + // [Check] Guild exists and is the alliance master + let master = store.get_guild(member.guild_id); + master.assert_is_allowed(Role::Master); + + // [Check] Master has authority over the guild + let mut guild = store.get_guild(guild_id); + master.assert_has_authority(guild.role.into()); + + // [Check] Guilds are in the same alliance + guild.assert_same_alliance(master.alliance_id); + + // [Effect] Alliance fire a guild + let mut alliance = store.get_alliance(master.alliance_id); + alliance.fire(); + + // [Effect] Guild leaves the alliance + guild.leave(); + + // [Effect] Store entities + store.set_alliance(@alliance); + store.set_guild(@guild); + } + + fn request( + self: @ComponentState, + world: WorldStorage, + player_id: felt252, + alliance_id: u32 + ) { + // [Setup] Datastore + let mut store = StoreTrait::new(world); + + // [Check] Caller exists and is allowed + let member = store.get_member(player_id); + member.assert_is_allowed(Role::Master); + + // [Check] Alliance exists + let mut alliance = store.get_alliance(alliance_id); + alliance.assert_does_exist(); + + // [Check] Alliance is open + alliance.assert_is_open(); + + // [Effect] Guild requests to join the alliance + let mut guild = store.get_guild(member.guild_id); + guild.request(alliance_id); + + // [Effect] Guild joins the alliance if it is free + if alliance.free { + // [Effect] Guild joins the alliance + guild.join(alliance_id); + // [Effect] Alliance hires a guild + alliance.hire(); + store.set_alliance(@alliance); + }; + + // [Effect] Store entities + store.set_guild(@guild); + } + + fn cancel(self: @ComponentState, world: WorldStorage, player_id: felt252) { + // [Setup] Datastore + let mut store = StoreTrait::new(world); + + // [Check] Caller exists and is allowed + let member = store.get_member(player_id); + member.assert_is_allowed(Role::Master); + + // [Effect] Guild cancels the request + let mut guild = store.get_guild(member.guild_id); + guild.cancel(); + + // [Effect] Store entities + store.set_guild(@guild); + } + + fn leave(self: @ComponentState, world: WorldStorage, player_id: felt252) { + // [Setup] Datastore + let mut store = StoreTrait::new(world); + + // [Check] Caller exists and is allowed + let member = store.get_member(player_id); + member.assert_is_allowed(Role::Master); + + // [Effect] Guild leaves the alliance + let mut guild = store.get_guild(member.guild_id); + guild.leave(); + + // [Effect] Store entities + store.set_guild(@guild); + } + } +} diff --git a/packages/society/src/components/followable.cairo b/packages/society/src/components/followable.cairo new file mode 100644 index 0000000..e7930e3 --- /dev/null +++ b/packages/society/src/components/followable.cairo @@ -0,0 +1,53 @@ +#[starknet::component] +mod FollowableComponent { + // Dojo imports + + use dojo::world::WorldStorage; + + // Internal imports + + use society::store::{Store, StoreTrait}; + + // Storage + + #[storage] + struct Storage {} + + // Events + + #[event] + #[derive(Drop, starknet::Event)] + enum Event {} + + #[generate_trait] + impl InternalImpl< + TContractState, +HasComponent + > of InternalTrait { + fn follow( + self: @ComponentState, + world: WorldStorage, + player_id: felt252, + followed: felt252 + ) { + // [Setup] Datastore + let mut store = StoreTrait::new(world); + + // [Effect] Follow + let time = starknet::get_block_timestamp(); + store.follow(player_id, followed, time); + } + + fn unfollow( + self: @ComponentState, + world: WorldStorage, + player_id: felt252, + followed: felt252 + ) { + // [Setup] Datastore + let mut store = StoreTrait::new(world); + + // [Effect] Unfollow + store.unfollow(player_id, followed); + } + } +} diff --git a/packages/society/src/components/guildable.cairo b/packages/society/src/components/guildable.cairo new file mode 100644 index 0000000..3f8f452 --- /dev/null +++ b/packages/society/src/components/guildable.cairo @@ -0,0 +1,302 @@ +#[starknet::component] +mod GuildableComponent { + // Dojo imports + + use dojo::world::WorldStorage; + use dojo::world::IWorldDispatcherTrait; + + // External imports + + use registry::types::metadata::MetadataTrait; + use registry::types::socials::SocialsTrait; + + // Internal imports + + use society::store::{Store, StoreTrait}; + use society::models::guild::{Guild, GuildTrait, GuildAssert}; + use society::models::member::{Member, MemberTrait, MemberAssert}; + use society::types::role::Role; + + // Storage + + #[storage] + struct Storage {} + + // Events + + #[event] + #[derive(Drop, starknet::Event)] + enum Event {} + + #[generate_trait] + impl InternalImpl< + TContractState, +HasComponent + > of InternalTrait { + fn create( + self: @ComponentState, + world: WorldStorage, + player_id: felt252, + color: Option, + name: Option, + description: Option, + image: Option, + banner: Option, + discord: Option, + telegram: Option, + twitter: Option, + youtube: Option, + website: Option + ) { + // [Setup] Datastore + let mut store = StoreTrait::new(world); + + // [Check] Member does not belong to a guild + let mut member = store.get_member(player_id); + member.assert_can_join(); + + // [Effect] Create a guild + let guild_id = world.dispatcher.uuid(); + let metadata = MetadataTrait::new(color, name, description, image, banner); + let socials = SocialsTrait::new(discord, telegram, twitter, youtube, website); + let mut guild = GuildTrait::new(guild_id, metadata, socials); + + // [Effect] Member joins guild + member.join(guild_id); + guild.hire(); + + // [Effect] Member becomes guild master + member.crown(); + + // [Effect] Store entities + store.set_member(@member); + store.set_guild(@guild); + } + + fn open( + self: @ComponentState, + world: WorldStorage, + player_id: felt252, + free: bool + ) { + // [Setup] Datastore + let mut store = StoreTrait::new(world); + + // [Check] Caller exists and is allowed + let admin = store.get_member(player_id); + admin.assert_is_allowed(Role::Officer); + + // [Effect] Guild opens + let mut guild = store.get_guild(admin.guild_id); + guild.open(free); + + // [Effect] Store entities + store.set_guild(@guild); + } + + fn close(self: @ComponentState, world: WorldStorage, player_id: felt252) { + // [Setup] Datastore + let mut store = StoreTrait::new(world); + + // [Check] Caller exists and is allowed + let admin = store.get_member(player_id); + admin.assert_is_allowed(Role::Officer); + + // [Effect] Guild closes + let mut guild = store.get_guild(admin.guild_id); + guild.close(); + + // [Effect] Store entities + store.set_guild(@guild); + } + + fn crown( + self: @ComponentState, + world: WorldStorage, + player_id: felt252, + member_id: felt252 + ) { + // [Setup] Datastore + let mut store = StoreTrait::new(world); + + // [Check] Caller exists and is allowed + let mut master = store.get_member(player_id); + master.assert_is_allowed(Role::Master); + + // [Check] Member is in the same guild + let mut member = store.get_member(member_id); + member.assert_same_guild(master.guild_id); + + // [Effect] Transfer the master role + master.uncrown(); + member.crown(); + + // [Effect] Store entities + store.set_member(@master); + store.set_member(@member); + } + + fn promote( + self: @ComponentState, + world: WorldStorage, + player_id: felt252, + member_id: felt252 + ) { + // [Setup] Datastore + let mut store = StoreTrait::new(world); + + // [Check] Caller exists and is allowed + let admin = store.get_member(player_id); + admin.assert_is_allowed(Role::Officer); + + // [Check] Member is in the same guild + let mut member = store.get_member(member_id); + member.assert_same_guild(admin.guild_id); + + // [Effect] Guild promotes a member + member.promote(); + + // [Effect] Store entities + store.set_member(@member); + } + + fn demote( + self: @ComponentState, + world: WorldStorage, + player_id: felt252, + member_id: felt252 + ) { + // [Setup] Datastore + let mut store = StoreTrait::new(world); + + // [Check] Caller exists and is allowed + let master = store.get_member(player_id); + master.assert_is_allowed(Role::Master); + + // [Check] Member is in the same guild + let mut member = store.get_member(member_id); + member.assert_same_guild(master.guild_id); + + // [Effect] Guild demotes a member + member.demote(); + + // [Effect] Store entities + store.set_member(@member); + } + + fn hire( + self: @ComponentState, + world: WorldStorage, + player_id: felt252, + member_id: felt252 + ) { + // [Setup] Datastore + let mut store = StoreTrait::new(world); + + // [Check] Caller exists and is allowed + let admin = store.get_member(player_id); + admin.assert_is_allowed(Role::Officer); + + // [Check] Guild is open + let mut guild = store.get_guild(admin.guild_id); + guild.assert_is_open(); + + // [Effect] Member joins the guild and guild hires a member + let mut member = store.get_member(member_id); + member.join(guild.id); + guild.hire(); + + // [Effect] Store entities + store.set_guild(@guild); + store.set_member(@member); + } + + fn fire( + self: @ComponentState, + world: WorldStorage, + player_id: felt252, + member_id: felt252 + ) { + // [Setup] Datastore + let mut store = StoreTrait::new(world); + + // [Check] Caller exists and is allowed + let admin = store.get_member(player_id); + admin.assert_is_allowed(Role::Officer); + + // [Check] Admin has authority over the member + let mut member = store.get_member(member_id); + admin.assert_has_authority(member.role.into()); + + // [Check] Members are in the same guild + member.assert_same_guild(admin.guild_id); + + // [Effect] Guild fire a member + let mut guild = store.get_guild(admin.guild_id); + guild.fire(); + + // [Effect] Member leaves the guild + member.leave(); + + // [Effect] Store entities + store.set_guild(@guild); + store.set_member(@member); + } + + fn request( + self: @ComponentState, + world: WorldStorage, + player_id: felt252, + guild_id: u32 + ) { + // [Setup] Datastore + let mut store = StoreTrait::new(world); + + // [Check] Guild exists + let mut guild = store.get_guild(guild_id); + guild.assert_does_exist(); + + // [Check] Guild is open + guild.assert_is_open(); + + // [Effect] Member requests to join the guild + let mut member = store.get_member(player_id); + member.request(guild_id); + + // [Effect] Member joins the guild if it is free + if guild.free { + // [Effect] Member joins the guild + member.join(guild_id); + // [Effect] Guild hires a member + guild.hire(); + store.set_guild(@guild); + }; + + // [Effect] Store entities + store.set_member(@member); + } + + fn cancel(self: @ComponentState, world: WorldStorage, player_id: felt252) { + // [Setup] Datastore + let mut store = StoreTrait::new(world); + + // [Effect] Member cancels the request + let mut member = store.get_member(player_id); + member.cancel(); + + // [Effect] Store entities + store.set_member(@member); + } + + fn leave(self: @ComponentState, world: WorldStorage, player_id: felt252) { + // [Setup] Datastore + let mut store = StoreTrait::new(world); + + // [Effect] Member leaves the guild + let mut member = store.get_member(player_id); + member.leave(); + + // [Effect] Store entities + store.set_member(@member); + } + } +} diff --git a/packages/society/src/constants.cairo b/packages/society/src/constants.cairo new file mode 100644 index 0000000..3259fe6 --- /dev/null +++ b/packages/society/src/constants.cairo @@ -0,0 +1,4 @@ +// Guilds and Alliances + +pub const MAX_MEMBER_COUNT: u8 = 100; +pub const MAX_GUILD_COUNT: u8 = 10; diff --git a/packages/society/src/events/follow.cairo b/packages/society/src/events/follow.cairo new file mode 100644 index 0000000..08b414d --- /dev/null +++ b/packages/society/src/events/follow.cairo @@ -0,0 +1,72 @@ +// Internal imports + +use society::events::index::Follow; + +// Errors + +pub mod errors { + pub const FOLLOW_INVALID_FOLLOWER: felt252 = 'Follow: invalid follower'; + pub const FOLLOW_INVALID_FOLLOWED: felt252 = 'Follow: invalid followed'; +} + +// Implementations + +#[generate_trait] +impl FollowImpl of FollowTrait { + #[inline] + fn new(follower: felt252, followed: felt252, time: u64,) -> Follow { + // [Check] Inputs + // [Info] We don't check points here, leave free the game to decide + FollowAssert::assert_valid_follower(follower); + FollowAssert::assert_valid_followed(followed); + // [Return] Follow + Follow { follower, followed, time } + } +} + +#[generate_trait] +impl FollowAssert of AssertTrait { + #[inline] + fn assert_valid_follower(follower: felt252) { + assert(follower != 0, errors::FOLLOW_INVALID_FOLLOWER); + } + + #[inline] + fn assert_valid_followed(followed: felt252) { + assert(followed != 0, errors::FOLLOW_INVALID_FOLLOWED); + } +} + +#[cfg(test)] +mod tests { + // Local imports + + use super::{Follow, FollowTrait}; + + // Constants + + const FOLLOWER: felt252 = 'FOLLOWER'; + const FOLLOWED: felt252 = 'FOLLOWED'; + const TIME: u64 = 100; + + #[test] + fn test_follow_new() { + let follow = FollowTrait::new(FOLLOWER, FOLLOWED, TIME); + assert_eq!(follow.follower, FOLLOWER); + assert_eq!(follow.followed, FOLLOWED); + assert_eq!(follow.time, TIME); + } + + #[test] + #[should_panic(expected: ('Follow: invalid follower',))] + fn test_follow_new_invalid_follower() { + FollowTrait::new(0, FOLLOWED, TIME); + } + + #[test] + #[should_panic(expected: ('Follow: invalid followed',))] + fn test_follow_new_invalid_followed() { + FollowTrait::new(FOLLOWER, 0, TIME); + } +} + diff --git a/packages/society/src/events/index.cairo b/packages/society/src/events/index.cairo new file mode 100644 index 0000000..d44df24 --- /dev/null +++ b/packages/society/src/events/index.cairo @@ -0,0 +1,10 @@ +//! Events + +#[derive(Copy, Drop, Serde)] +#[dojo::event] +pub struct Follow { + #[key] + follower: felt252, + followed: felt252, + time: u64, +} diff --git a/packages/society/src/lib.cairo b/packages/society/src/lib.cairo new file mode 100644 index 0000000..c803c29 --- /dev/null +++ b/packages/society/src/lib.cairo @@ -0,0 +1,24 @@ +mod constants; +mod store; + +mod types { + mod role; +} + +mod events { + mod index; + mod follow; +} + +mod models { + mod index; + mod member; + mod guild; + mod alliance; +} + +mod components { + mod followable; + mod guildable; + mod allianceable; +} diff --git a/packages/society/src/models/alliance.cairo b/packages/society/src/models/alliance.cairo new file mode 100644 index 0000000..aae5456 --- /dev/null +++ b/packages/society/src/models/alliance.cairo @@ -0,0 +1,132 @@ +// External imports + +use registry::types::metadata::Metadata; +use registry::types::socials::Socials; +use registry::helpers::json::JsonifiableTrait; + +// Internal imports + +use society::constants::MAX_GUILD_COUNT; +use society::models::index::Alliance; +use society::types::role::Role; + +// Errors + +pub mod errors { + pub const ALLIANCE_ALREADY_EXISTS: felt252 = 'Alliance: already exists'; + pub const ALLIANCE_NOT_EXIST: felt252 = 'Alliance: does not exist'; + pub const ALLIANCE_CANNOT_HIRE: felt252 = 'Alliance: cannot hire'; + pub const ALLIANCE_CANNOT_FIRE: felt252 = 'Alliance: cannot fire'; + pub const ALLIANCE_IS_OPEN: felt252 = 'Alliance: is open'; + pub const ALLIANCE_IS_CLOSE: felt252 = 'Alliance: is close'; + pub const ALLIANCE_CANNOT_JOIN: felt252 = 'Alliance: cannot join'; + pub const ALLIANCE_CANNOT_LEAVE: felt252 = 'Alliance: cannot leave'; + pub const ALLIANCE_CANNOT_CROWN: felt252 = 'Alliance: cannot crown'; + pub const ALLIANCE_CANNOT_UNCROWN: felt252 = 'Alliance: cannot un-crown'; + pub const ALLIANCE_CANNOT_REQUEST: felt252 = 'Alliance: cannot request'; + pub const ALLIANCE_CANNOT_CANCEL: felt252 = 'Alliance: cannot cancel'; +} + +#[generate_trait] +impl AllianceImpl of AllianceTrait { + #[inline] + fn new(id: u32, metadata: Metadata, socials: Socials) -> Alliance { + Alliance { + id: id, + open: false, + free: false, + guild_count: 0, + metadata: metadata.jsonify(), + socials: socials.jsonify(), + } + } + + #[inline] + fn open(ref self: Alliance, free: bool) { + // [Check] Alliance can be opened + self.assert_is_close(); + // [Update] Alliance + self.open = true; + self.free = free; + } + + #[inline] + fn close(ref self: Alliance) { + // [Check] Alliance can be closed + self.assert_is_open(); + // [Update] Alliance + self.open = false; + self.free = false; + } + + #[inline] + fn hire(ref self: Alliance) { + // [Check] Alliance can be hired + self.assert_can_hire(); + // [Update] Alliance + self.guild_count += 1; + } + + #[inline] + fn fire(ref self: Alliance) { + // [Check] Alliance can be fired + self.assert_can_fire(); + // [Update] Alliance + self.guild_count -= 1; + } +} + +#[generate_trait] +impl AllianceAssert of AssertTrait { + #[inline] + fn assert_does_not_exist(self: @Alliance) { + assert(*self.guild_count == 0, errors::ALLIANCE_ALREADY_EXISTS); + } + + #[inline] + fn assert_does_exist(self: @Alliance) { + assert(*self.guild_count != 0, errors::ALLIANCE_NOT_EXIST); + } + + #[inline] + fn assert_is_open(self: @Alliance) { + assert(*self.open, errors::ALLIANCE_IS_CLOSE); + } + + #[inline] + fn assert_is_close(self: @Alliance) { + assert(!*self.open, errors::ALLIANCE_IS_OPEN); + } + + #[inline] + fn assert_can_hire(self: @Alliance) { + assert(*self.guild_count < MAX_GUILD_COUNT, errors::ALLIANCE_CANNOT_HIRE); + } + + #[inline] + fn assert_can_fire(self: @Alliance) { + assert(*self.guild_count > 0, errors::ALLIANCE_CANNOT_FIRE); + } +} + +#[cfg(test)] +mod tests { + // Local imports + + use super::{Alliance, AllianceTrait, AllianceAssert, Role, Metadata, Socials}; + + // Constants + + const ALLIANCE_ID: u32 = 42; + + #[test] + fn test_alliance_new() { + let metadata = core::Default::default(); + let socials = core::Default::default(); + let guild = AllianceTrait::new(ALLIANCE_ID, metadata, socials); + assert_eq!(guild.id, ALLIANCE_ID); + assert_eq!(guild.open, false); + assert_eq!(guild.free, false); + assert_eq!(guild.guild_count, 0); + } +} diff --git a/packages/society/src/models/guild.cairo b/packages/society/src/models/guild.cairo new file mode 100644 index 0000000..25ebeef --- /dev/null +++ b/packages/society/src/models/guild.cairo @@ -0,0 +1,244 @@ +// External imports + +use registry::types::metadata::Metadata; +use registry::types::socials::Socials; +use registry::helpers::json::JsonifiableTrait; + +// Internal imports + +use society::constants::MAX_MEMBER_COUNT; +use society::models::index::Guild; +use society::types::role::Role; + +// Errors + +pub mod errors { + pub const GUILD_ALREADY_EXISTS: felt252 = 'Guild: already exists'; + pub const GUILD_NOT_EXIST: felt252 = 'Guild: does not exist'; + pub const GUILD_CANNOT_HIRE: felt252 = 'Guild: cannot hire'; + pub const GUILD_CANNOT_FIRE: felt252 = 'Guild: cannot fire'; + pub const GUILD_IS_OPEN: felt252 = 'Guild: is open'; + pub const GUILD_IS_CLOSE: felt252 = 'Guild: is close'; + pub const GUILD_CANNOT_JOIN: felt252 = 'Guild: cannot join'; + pub const GUILD_CANNOT_LEAVE: felt252 = 'Guild: cannot leave'; + pub const GUILD_CANNOT_CROWN: felt252 = 'Guild: cannot crown'; + pub const GUILD_CANNOT_UNCROWN: felt252 = 'Guild: cannot un-crown'; + pub const GUILD_CANNOT_REQUEST: felt252 = 'Guild: cannot request'; + pub const GUILD_CANNOT_CANCEL: felt252 = 'Guild: cannot cancel'; + pub const GUILD_NOT_A_REQUESTER: felt252 = 'Guild: not a requester'; + pub const GUILD_NOT_IN_ALLIANCE: felt252 = 'Guild: not in alliance'; + pub const GUILD_NOT_ALLOWED: felt252 = 'Guild: not allowed'; + pub const GUILD_NOT_AUTHORIZED: felt252 = 'Guild: not authorized'; +} + +#[generate_trait] +impl GuildImpl of GuildTrait { + #[inline] + fn new(id: u32, metadata: Metadata, socials: Socials) -> Guild { + Guild { + id: id, + open: false, + free: false, + role: Role::None.into(), + member_count: 0, + alliance_id: 0, + pending_alliance_id: 0, + metadata: metadata.jsonify(), + socials: socials.jsonify(), + } + } + + #[inline] + fn open(ref self: Guild, free: bool) { + // [Check] Guild can be opened + self.assert_is_close(); + // [Update] Guild + self.open = true; + self.free = free; + } + + #[inline] + fn close(ref self: Guild) { + // [Check] Guild can be closed + self.assert_is_open(); + // [Update] Guild + self.open = false; + self.free = false; + } + + #[inline] + fn hire(ref self: Guild) { + // [Check] Guild can be hired + self.assert_can_hire(); + // [Update] Guild + self.member_count += 1; + } + + #[inline] + fn fire(ref self: Guild) { + // [Check] Guild can be fired + self.assert_can_fire(); + // [Update] Guild + self.member_count -= 1; + } + + #[inline] + fn join(ref self: Guild, alliance_id: u32) { + // [Check] Guild can join + self.assert_can_join(); + self.assert_is_requester(alliance_id); + // [Update] Guild + self.alliance_id = alliance_id; + self.pending_alliance_id = 0; + self.role = Role::Member.into(); + } + + #[inline] + fn leave(ref self: Guild) { + // [Check] Guild can leave + self.assert_can_leave(); + // [Update] Guild + self.alliance_id = 0; + self.role = Role::None.into(); + } + + #[inline] + fn crown(ref self: Guild) { + // [Check] Guild can be crowned + self.assert_is_crownable(); + // [Update] Guild + self.role = Role::Master.into(); + } + + #[inline] + fn uncrown(ref self: Guild) { + // [Check] Guild can be un-crowned + self.assert_is_uncrownable(); + // [Update] Guild + self.role = Role::Member.into(); + } + + #[inline] + fn request(ref self: Guild, alliance_id: u32) { + // [Check] Guild can request + self.assert_can_request(); + // [Update] Guild + self.pending_alliance_id = alliance_id; + } + + #[inline] + fn cancel(ref self: Guild) { + // [Check] Guild can cancel + self.assert_can_cancel(); + // [Update] Guild + self.pending_alliance_id = 0; + } +} + +#[generate_trait] +impl GuildAssert of AssertTrait { + #[inline] + fn assert_does_not_exist(self: @Guild) { + assert(*self.member_count == 0, errors::GUILD_ALREADY_EXISTS); + } + + #[inline] + fn assert_does_exist(self: @Guild) { + assert(*self.member_count != 0, errors::GUILD_NOT_EXIST); + } + + #[inline] + fn assert_is_open(self: @Guild) { + assert(*self.open, errors::GUILD_IS_CLOSE); + } + + #[inline] + fn assert_is_close(self: @Guild) { + assert(!*self.open, errors::GUILD_IS_OPEN); + } + + #[inline] + fn assert_can_hire(self: @Guild) { + assert(*self.member_count < MAX_MEMBER_COUNT, errors::GUILD_CANNOT_HIRE); + } + + #[inline] + fn assert_can_fire(self: @Guild) { + assert(*self.member_count > 0, errors::GUILD_CANNOT_FIRE); + } + + #[inline] + fn assert_can_join(self: @Guild) { + assert(self.alliance_id == @0, errors::GUILD_CANNOT_JOIN); + } + + #[inline] + fn assert_can_leave(self: @Guild) { + assert(self.alliance_id == @0, errors::GUILD_CANNOT_LEAVE); + } + + #[inline] + fn assert_is_crownable(self: @Guild) { + assert(self.role == @Role::Member.into(), errors::GUILD_CANNOT_CROWN); + } + + #[inline] + fn assert_is_uncrownable(self: @Guild) { + assert(self.role == @Role::Master.into(), errors::GUILD_CANNOT_UNCROWN); + } + + #[inline] + fn assert_can_request(self: @Guild) { + assert(*self.pending_alliance_id + *self.alliance_id == 0, errors::GUILD_CANNOT_REQUEST); + } + + #[inline] + fn assert_is_requester(self: @Guild, alliance_id: u32) { + assert(*self.pending_alliance_id == alliance_id, errors::GUILD_NOT_A_REQUESTER); + } + + #[inline] + fn assert_can_cancel(self: @Guild) { + assert(*self.pending_alliance_id != 0, errors::GUILD_CANNOT_CANCEL); + } + + #[inline] + fn assert_same_alliance(self: @Guild, alliance_id: u32) { + assert(*self.alliance_id == alliance_id, errors::GUILD_NOT_IN_ALLIANCE); + } + + #[inline] + fn assert_is_allowed(self: @Guild, role: Role) { + assert(*self.role >= role.into(), errors::GUILD_NOT_ALLOWED); + } + + #[inline] + fn assert_has_authority(self: @Guild, role: Role) { + assert(*self.role > role.into(), errors::GUILD_NOT_AUTHORIZED); + } +} + +#[cfg(test)] +mod tests { + // Local imports + + use super::{Guild, GuildTrait, GuildAssert, Role, Metadata, Socials}; + + // Constants + + const GUILD_ID: u32 = 42; + + #[test] + fn test_guild_new() { + let metadata = core::Default::default(); + let socials = core::Default::default(); + let guild = GuildTrait::new(GUILD_ID, metadata, socials); + assert_eq!(guild.id, GUILD_ID); + assert_eq!(guild.open, false); + assert_eq!(guild.free, false); + assert_eq!(guild.role, Role::None.into()); + assert_eq!(guild.member_count, 0); + assert_eq!(guild.alliance_id, 0); + assert_eq!(guild.pending_alliance_id, 0); + } +} diff --git a/packages/society/src/models/index.cairo b/packages/society/src/models/index.cairo new file mode 100644 index 0000000..bde920a --- /dev/null +++ b/packages/society/src/models/index.cairo @@ -0,0 +1,38 @@ +//! Models + +#[derive(Copy, Drop, Serde)] +#[dojo::model] +pub struct Member { + #[key] + id: felt252, + role: u8, + guild_id: u32, + pending_guild_id: u32, +} + +#[derive(Clone, Drop, Serde)] +#[dojo::model] +pub struct Guild { + #[key] + id: u32, + open: bool, + free: bool, + role: u8, + member_count: u8, + alliance_id: u32, + pending_alliance_id: u32, + metadata: ByteArray, + socials: ByteArray, +} + +#[derive(Clone, Drop, Serde)] +#[dojo::model] +pub struct Alliance { + #[key] + id: u32, + open: bool, + free: bool, + guild_count: u8, + metadata: ByteArray, + socials: ByteArray, +} diff --git a/packages/society/src/models/member.cairo b/packages/society/src/models/member.cairo new file mode 100644 index 0000000..e3bc9f7 --- /dev/null +++ b/packages/society/src/models/member.cairo @@ -0,0 +1,199 @@ +// Internal imports + +use society::models::index::Member; +use society::types::role::Role; + +// Errors + +pub mod errors { + pub const MEMBER_ALREADY_EXISTS: felt252 = 'Member: already exists'; + pub const MEMBER_NOT_EXIST: felt252 = 'Member: does not exist'; + pub const MEMBER_CANNOT_JOIN: felt252 = 'Member: cannot join'; + pub const MEMBER_CANNOT_LEAVE: felt252 = 'Member: cannot leave'; + pub const MEMBER_CANNOT_PROMOTE: felt252 = 'Member: cannot be promoted'; + pub const MEMBER_CANNOT_DEMOTE: felt252 = 'Member: cannot be demoted'; + pub const MEMBER_CANNOT_CROWN: felt252 = 'Member: cannot be crowned'; + pub const MEMBER_CANNOT_UNCROWN: felt252 = 'Member: cannot be un-crowned'; + pub const MEMBER_CANNOT_REQUEST: felt252 = 'Member: cannot request'; + pub const MEMBER_CANNOT_CANCEL: felt252 = 'Member: cannot cancel'; + pub const MEMBER_NOT_ALLOWED: felt252 = 'Member: not allowed'; + pub const MEMBER_NOT_A_REQUESTER: felt252 = 'Member: not a requester'; + pub const MEMBER_NOT_AUTHORIZED: felt252 = 'Member: not authorized'; + pub const MEMBER_NOT_IN_GUILD: felt252 = 'Member: not in guild'; +} + +#[generate_trait] +impl MemberImpl of MemberTrait { + #[inline] + fn new(id: felt252) -> Member { + // [Return] Member + let role = Role::None; + Member { id: id, role: role.into(), guild_id: 0, pending_guild_id: 0 } + } + + #[inline] + fn join(ref self: Member, guild_id: u32) { + // [Check] Member can join + self.assert_can_join(); + self.assert_is_requester(guild_id); + // [Update] Member + self.guild_id = guild_id; + self.pending_guild_id = 0; + self.role = Role::Member.into(); + } + + #[inline] + fn leave(ref self: Member) { + // [Check] Member can leave + self.assert_can_leave(); + // [Update] Member + self.guild_id = 0; + self.role = Role::None.into(); + } + + #[inline] + fn crown(ref self: Member) { + // [Check] Member can be crowned + self.assert_is_crownable(); + // [Update] Member + self.role = Role::Master.into(); + } + + #[inline] + fn uncrown(ref self: Member) { + // [Check] Member can be un-crowned + self.assert_is_uncrownable(); + // [Update] Member + self.role = Role::Officer.into(); + } + + #[inline] + fn promote(ref self: Member) { + // [Check] Member can be promoted + self.assert_is_promotable(); + // [Update] Member + self.role = Role::Officer.into(); + } + + #[inline] + fn demote(ref self: Member) { + // [Check] Member can be demoted + self.assert_is_demotable(); + // [Update] Member + self.role = Role::Member.into(); + } + + #[inline] + fn request(ref self: Member, guild_id: u32) { + // [Check] Member can request + self.assert_can_request(); + // [Update] Member + self.pending_guild_id = guild_id; + } + + #[inline] + fn cancel(ref self: Member) { + // [Check] Member can cancel + self.assert_can_cancel(); + // [Update] Member + self.pending_guild_id = 0; + } +} + +#[generate_trait] +impl MemberAssert of AssertTrait { + #[inline] + fn assert_does_not_exist(self: @Member) { + assert(*self.guild_id + *self.pending_guild_id == 0, errors::MEMBER_ALREADY_EXISTS); + } + + #[inline] + fn assert_does_exist(self: @Member) { + assert(*self.guild_id + *self.pending_guild_id != 0, errors::MEMBER_NOT_EXIST); + } + + #[inline] + fn assert_can_join(self: @Member) { + assert(*self.guild_id == 0 && *self.pending_guild_id != 0, errors::MEMBER_CANNOT_JOIN); + } + + #[inline] + fn assert_can_leave(self: @Member) { + assert( + *self.guild_id != 0 && *self.role != Role::Master.into(), errors::MEMBER_CANNOT_LEAVE + ); + } + + #[inline] + fn assert_is_promotable(self: @Member) { + assert(self.role == @Role::Member.into(), errors::MEMBER_CANNOT_PROMOTE); + } + + #[inline] + fn assert_is_demotable(self: @Member) { + assert(self.role == @Role::Officer.into(), errors::MEMBER_CANNOT_DEMOTE); + } + + #[inline] + fn assert_is_crownable(self: @Member) { + assert( + self.role == @Role::Member.into() || self.role == @Role::Officer.into(), + errors::MEMBER_CANNOT_CROWN + ); + } + + #[inline] + fn assert_is_uncrownable(self: @Member) { + assert(self.role == @Role::Master.into(), errors::MEMBER_CANNOT_UNCROWN); + } + + #[inline] + fn assert_can_request(self: @Member) { + assert(*self.pending_guild_id + *self.guild_id == 0, errors::MEMBER_CANNOT_REQUEST); + } + + #[inline] + fn assert_can_cancel(self: @Member) { + assert(*self.pending_guild_id != 0, errors::MEMBER_CANNOT_CANCEL); + } + + #[inline] + fn assert_is_allowed(self: @Member, role: Role) { + assert(*self.role >= role.into(), errors::MEMBER_NOT_ALLOWED); + } + + #[inline] + fn assert_is_requester(self: @Member, guild_id: u32) { + assert(*self.pending_guild_id != guild_id, errors::MEMBER_NOT_A_REQUESTER); + } + + #[inline] + fn assert_has_authority(self: @Member, role: Role) { + assert(*self.role > role.into(), errors::MEMBER_NOT_AUTHORIZED); + } + + #[inline] + fn assert_same_guild(self: @Member, guild_id: u32) { + assert(*self.guild_id == guild_id, errors::MEMBER_NOT_IN_GUILD); + } +} + +#[cfg(test)] +mod tests { + // Local imports + + use super::{Member, MemberTrait, MemberAssert, Role}; + + // Constants + + const ACCOUNT_ID: felt252 = 'ACCOUNT_ID'; + const GUILD_ID: u32 = 42; + + #[test] + fn test_member_new() { + let member = MemberTrait::new(ACCOUNT_ID); + assert_eq!(member.id, ACCOUNT_ID); + assert_eq!(member.guild_id, 0); + assert_eq!(member.role, Role::None.into()); + } +} diff --git a/packages/society/src/store.cairo b/packages/society/src/store.cairo new file mode 100644 index 0000000..4f74be0 --- /dev/null +++ b/packages/society/src/store.cairo @@ -0,0 +1,77 @@ +//! Store struct and component management methods. + +// Starknet imports + +use starknet::SyscallResultTrait; + +// Dojo imports + +use dojo::world::WorldStorage; +use dojo::model::ModelStorage; +use dojo::event::EventStorage; + +// Models imports + +use society::models::alliance::Alliance; +use society::models::guild::Guild; +use society::models::member::Member; +use society::events::follow::{Follow, FollowTrait}; + +// Structs + +#[derive(Copy, Drop)] +struct Store { + world: WorldStorage, +} + +// Implementations + +#[generate_trait] +impl StoreImpl of StoreTrait { + #[inline] + fn new(world: WorldStorage) -> Store { + Store { world: world } + } + + #[inline] + fn get_alliance(self: Store, alliance_id: u32) -> Alliance { + self.world.read_model(alliance_id) + } + + #[inline] + fn get_guild(self: Store, guild_id: u32) -> Guild { + self.world.read_model(guild_id) + } + + #[inline] + fn get_member(self: Store, member_id: felt252) -> Member { + self.world.read_model(member_id) + } + + #[inline] + fn set_alliance(ref self: Store, alliance: @Alliance) { + self.world.write_model(alliance); + } + + #[inline] + fn set_guild(ref self: Store, guild: @Guild) { + self.world.write_model(guild); + } + + #[inline] + fn set_member(ref self: Store, member: @Member) { + self.world.write_model(member); + } + + #[inline] + fn follow(ref self: Store, follower: felt252, followed: felt252, time: u64) { + let event = FollowTrait::new(follower, followed, time); + self.world.emit_event(@event); + } + + #[inline] + fn unfollow(ref self: Store, follower: felt252, followed: felt252) { + let event = FollowTrait::new(follower, followed, 0); + self.world.emit_event(@event); + } +} diff --git a/packages/society/src/types/role.cairo b/packages/society/src/types/role.cairo new file mode 100644 index 0000000..bd64fb6 --- /dev/null +++ b/packages/society/src/types/role.cairo @@ -0,0 +1,34 @@ +#[derive(Copy, Drop, PartialEq)] +pub enum Role { + None, + Member, + Officer, + Master, +} + +// Implementations + +impl IntoRoleU8 of core::Into { + #[inline] + fn into(self: Role) -> u8 { + match self { + Role::None => 0, + Role::Member => 1, + Role::Officer => 2, + Role::Master => 3, + } + } +} + +impl IntoU8Role of core::Into { + #[inline] + fn into(self: u8) -> Role { + match self { + 0 => Role::None, + 1 => Role::Member, + 2 => Role::Officer, + 3 => Role::Master, + _ => Role::None, + } + } +} From f1bafeb20d19ea1c3e159792f09e1ad3f03799cd Mon Sep 17 00:00:00 2001 From: bal7hazar Date: Mon, 25 Nov 2024 15:37:53 +0100 Subject: [PATCH 3/4] =?UTF-8?q?=E2=9C=A8=20Merge=20pinner=20into=20registr?= =?UTF-8?q?y=20system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/src/lib.cairo | 1 - contracts/src/systems/pinner.cairo | 74 ---------------------------- contracts/src/systems/registry.cairo | 21 ++++++++ contracts/src/tests/setup.cairo | 7 --- 4 files changed, 21 insertions(+), 82 deletions(-) delete mode 100644 contracts/src/systems/pinner.cairo diff --git a/contracts/src/lib.cairo b/contracts/src/lib.cairo index 7821122..450cb75 100644 --- a/contracts/src/lib.cairo +++ b/contracts/src/lib.cairo @@ -1,7 +1,6 @@ mod constants; mod systems { - mod pinner; mod registry; mod slot; mod society; diff --git a/contracts/src/systems/pinner.cairo b/contracts/src/systems/pinner.cairo deleted file mode 100644 index ed537ec..0000000 --- a/contracts/src/systems/pinner.cairo +++ /dev/null @@ -1,74 +0,0 @@ -// Interfaces - -#[starknet::interface] -trait IPinner { - fn pin(self: @TContractState, achievement_id: felt252); - fn unpin(self: @TContractState, achievement_id: felt252); -} - -// Contracts - -#[dojo::contract] -mod Pinner { - // Dojo imports - - use dojo::world::WorldStorage; - - // Component imports - - use achievement::components::pinnable::PinnableComponent; - - // Internal imports - - use arcade::constants::NAMESPACE; - - // Local imports - - use super::IPinner; - - // Components - - component!(path: PinnableComponent, storage: pinnable, event: PinnableEvent); - impl PinnableInternalImpl = PinnableComponent::InternalImpl; - - // Storage - - #[storage] - struct Storage { - #[substorage(v0)] - pinnable: PinnableComponent::Storage, - } - - // Events - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - PinnableEvent: PinnableComponent::Event, - } - - // Implementations - - #[abi(embed_v0)] - impl PinnerImpl of IPinner { - fn pin(self: @ContractState, achievement_id: felt252) { - let world: WorldStorage = self.world_storage(); - let caller: felt252 = starknet::get_caller_address().into(); - self.pinnable.pin(world, caller, achievement_id); - } - - fn unpin(self: @ContractState, achievement_id: felt252) { - let world: WorldStorage = self.world_storage(); - let caller: felt252 = starknet::get_caller_address().into(); - self.pinnable.unpin(world, caller, achievement_id); - } - } - - #[generate_trait] - impl Private of PrivateTrait { - fn world_storage(self: @ContractState) -> WorldStorage { - self.world(@NAMESPACE()) - } - } -} diff --git a/contracts/src/systems/registry.cairo b/contracts/src/systems/registry.cairo index 51b7dcb..6352edf 100644 --- a/contracts/src/systems/registry.cairo +++ b/contracts/src/systems/registry.cairo @@ -2,6 +2,8 @@ #[starknet::interface] trait IRegistry { + fn pin(self: @TContractState, achievement_id: felt252); + fn unpin(self: @TContractState, achievement_id: felt252); fn register_game( self: @TContractState, world_address: felt252, @@ -79,6 +81,7 @@ mod Registry { // Component imports + use achievement::components::pinnable::PinnableComponent; use registry::components::initializable::InitializableComponent; use registry::components::registerable::RegisterableComponent; use registry::components::trackable::TrackableComponent; @@ -99,6 +102,8 @@ mod Registry { impl RegisterableImpl = RegisterableComponent::InternalImpl; component!(path: TrackableComponent, storage: trackable, event: TrackableEvent); impl TrackableImpl = TrackableComponent::InternalImpl; + component!(path: PinnableComponent, storage: pinnable, event: PinnableEvent); + impl PinnableInternalImpl = PinnableComponent::InternalImpl; // Storage @@ -110,6 +115,8 @@ mod Registry { registerable: RegisterableComponent::Storage, #[substorage(v0)] trackable: TrackableComponent::Storage, + #[substorage(v0)] + pinnable: PinnableComponent::Storage, } // Events @@ -123,6 +130,8 @@ mod Registry { RegisterableEvent: RegisterableComponent::Event, #[flat] TrackableEvent: TrackableComponent::Event, + #[flat] + PinnableEvent: PinnableComponent::Event, } // Constructor @@ -304,6 +313,18 @@ mod Registry { let caller: felt252 = starknet::get_caller_address().into(); self.trackable.remove(world, caller, world_address, namespace, identifier); } + + fn pin(self: @ContractState, achievement_id: felt252) { + let world: WorldStorage = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.pinnable.pin(world, caller, achievement_id); + } + + fn unpin(self: @ContractState, achievement_id: felt252) { + let world: WorldStorage = self.world_storage(); + let caller: felt252 = starknet::get_caller_address().into(); + self.pinnable.unpin(world, caller, achievement_id); + } } #[generate_trait] diff --git a/contracts/src/tests/setup.cairo b/contracts/src/tests/setup.cairo index d86763c..468bb79 100644 --- a/contracts/src/tests/setup.cairo +++ b/contracts/src/tests/setup.cairo @@ -29,7 +29,6 @@ mod setup { // Internal imports use arcade::constants::NAMESPACE; - use arcade::systems::pinner::{Pinner, IPinnerDispatcher}; use arcade::systems::registry::{Registry, IRegistryDispatcher}; use arcade::systems::slot::{Slot, ISlotDispatcher}; use arcade::systems::society::{Society, ISocietyDispatcher}; @@ -47,7 +46,6 @@ mod setup { #[derive(Copy, Drop)] struct Systems { - pinner: IPinnerDispatcher, registry: IRegistryDispatcher, slot: ISlotDispatcher, society: ISocietyDispatcher, @@ -78,7 +76,6 @@ mod setup { TestResource::Model(society_models::m_Member::TEST_CLASS_HASH), TestResource::Event(society_events::e_Follow::TEST_CLASS_HASH), TestResource::Event(achievement_events::e_TrophyPinning::TEST_CLASS_HASH), - TestResource::Contract(Pinner::TEST_CLASS_HASH), TestResource::Contract(Registry::TEST_CLASS_HASH), TestResource::Contract(Slot::TEST_CLASS_HASH), TestResource::Contract(Society::TEST_CLASS_HASH), @@ -89,8 +86,6 @@ mod setup { fn setup_contracts() -> Span { [ - ContractDefTrait::new(@NAMESPACE(), @"Pinner") - .with_writer_of([dojo::utils::bytearray_hash(@NAMESPACE())].span()), ContractDefTrait::new(@NAMESPACE(), @"Registry") .with_writer_of([dojo::utils::bytearray_hash(@NAMESPACE())].span()) .with_init_calldata(array![OWNER().into()].span()), @@ -112,13 +107,11 @@ mod setup { let world = spawn_test_world([namespace_def].span()); world.sync_perms_and_inits(setup_contracts()); // [Setup] Systems - let (pinner_address, _) = world.dns(@"Pinner").unwrap(); let (registry_address, _) = world.dns(@"Registry").unwrap(); let (slot_address, _) = world.dns(@"Slot").unwrap(); let (society_address, _) = world.dns(@"Society").unwrap(); let (wallet_address, _) = world.dns(@"Wallet").unwrap(); let systems = Systems { - pinner: IPinnerDispatcher { contract_address: pinner_address }, registry: IRegistryDispatcher { contract_address: registry_address }, slot: ISlotDispatcher { contract_address: slot_address }, society: ISocietyDispatcher { contract_address: society_address }, From a63c83fe574d9738336ca38fca9f08a833db5531 Mon Sep 17 00:00:00 2001 From: bal7hazar Date: Mon, 25 Nov 2024 15:39:11 +0100 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=92=84=20Code=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/src/systems/registry.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/systems/registry.cairo b/contracts/src/systems/registry.cairo index 6352edf..16f6879 100644 --- a/contracts/src/systems/registry.cairo +++ b/contracts/src/systems/registry.cairo @@ -313,7 +313,7 @@ mod Registry { let caller: felt252 = starknet::get_caller_address().into(); self.trackable.remove(world, caller, world_address, namespace, identifier); } - + fn pin(self: @ContractState, achievement_id: felt252) { let world: WorldStorage = self.world_storage(); let caller: felt252 = starknet::get_caller_address().into();