Skip to content

Commit 0725228

Browse files
starknetdevclaude
andauthored
Re-architect Components (gas efficiency) (#9)
* feat: add metagame callback extension with SRC5 support - Add MetagameCallbackComponent with hooks pattern for receiving token state change callbacks (score update, game over, objectives completed) - Restructure tests directory from packages/test_starknet to tests/ - Add registry package with MinigameRegistryComponent - Various gas optimizations and code restructuring - Change from multi-objective to single objective model. - Add batch operations - Removed unnecessary event emissions (ScoreUpdate, GameOver, TokenMinted, TokenMetadataUpdate) --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 960c84a commit 0725228

119 files changed

Lines changed: 7013 additions & 2773 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.tool-versions

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
rust 1.89.0
2-
scarb 2.13.1
2+
scarb 2.14.0
33
starknet-foundry 0.53.0

Scarb.toml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
members = [
33
"packages/metagame",
44
"packages/minigame",
5+
"packages/registry",
56
"packages/token",
6-
# Test packages can be uncommented when needed
7-
"packages/test_starknet",
7+
# Test packages
8+
"tests",
89
"packages/utils",
910
"packages/leaderboard",
1011
"packages/presets",
@@ -32,6 +33,7 @@ keywords = [
3233
[workspace.dependencies]
3334
starknet = "2.13.1"
3435
snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.53.0" }
36+
openzeppelin_access = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v3.0.0-alpha.3" }
3537
openzeppelin_introspection = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v3.0.0-alpha.3" }
3638
openzeppelin_token = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v3.0.0-alpha.3" }
3739
openzeppelin_interfaces = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v3.0.0-alpha.3" }
@@ -41,10 +43,11 @@ starknet.workspace = true
4143
# Common packages
4244
game_components_metagame = { path = "packages/metagame" }
4345
game_components_minigame = { path = "packages/minigame" }
46+
game_components_registry = { path = "packages/registry" }
4447
game_components_token = { path = "packages/token" }
4548
# Test packages
4649
# game_components_test_dojo = { path = "packages/test_dojo" } # Excluded - must be built separately
47-
game_components_test_starknet = { path = "packages/test_starknet" }
50+
game_components_tests = { path = "tests" }
4851
game_components_utils = { path = "packages/utils" }
4952
game_components_leaderboard = { path = "packages/leaderboard" }
5053
game_components_presets = { path = "packages/presets" }

packages/metagame/Scarb.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ edition.workspace = true
88
[dependencies]
99
starknet.workspace = true
1010
game_components_minigame = { path = "../minigame" }
11+
game_components_registry = { path = "../registry" }
1112
game_components_token = { path = "../token" }
1213
game_components_utils = { path = "../utils" }
1314
openzeppelin_introspection.workspace = true
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
pub mod callback;
12
pub mod context;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod callback;
2+
pub mod interface;
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// ==============================================================================
2+
// METAGAME CALLBACK COMPONENT
3+
// ==============================================================================
4+
// A reusable component for receiving callbacks from token contracts when game
5+
// state changes (score updates, game over, objectives completed).
6+
//
7+
// Uses the hooks pattern: the component provides infrastructure and SRC5
8+
// registration, while implementations define actual callback behavior via traits.
9+
10+
#[starknet::component]
11+
pub mod MetagameCallbackComponent {
12+
use openzeppelin_introspection::src5::SRC5Component;
13+
use openzeppelin_introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait;
14+
use crate::extensions::callback::interface::{IMETAGAME_CALLBACK_ID, IMetagameCallback};
15+
16+
// ==========================================================================
17+
// STORAGE
18+
// ==========================================================================
19+
20+
#[storage]
21+
pub struct Storage {}
22+
23+
// ==========================================================================
24+
// HOOKS TRAIT
25+
// ==========================================================================
26+
// Allows the embedding contract to define custom callback behavior.
27+
// Implementers can aggregate scores, emit events, update leaderboards, etc.
28+
29+
pub trait MetagameCallbackHooksTrait<TContractState> {
30+
/// Called when a token's score is updated.
31+
/// @param token_id The token ID (packed u256)
32+
/// @param score The new score value
33+
fn on_score_update(ref self: TContractState, token_id: u256, score: u32);
34+
35+
/// Called when a game ends (game_over transitions to true).
36+
/// @param token_id The token ID (packed u256)
37+
/// @param final_score The final score when game ended
38+
fn on_game_over(ref self: TContractState, token_id: u256, final_score: u32);
39+
40+
/// Called when all objectives are completed.
41+
/// @param token_id The token ID (packed u256)
42+
fn on_objective_complete(ref self: TContractState, token_id: u256);
43+
}
44+
45+
// ==========================================================================
46+
// EMBEDDABLE IMPLEMENTATION
47+
// ==========================================================================
48+
// Implements IMetagameCallback by delegating to hooks.
49+
// Contracts must provide a MetagameCallbackHooksTrait implementation.
50+
51+
#[embeddable_as(MetagameCallbackImpl)]
52+
impl MetagameCallback<
53+
TContractState,
54+
+HasComponent<TContractState>,
55+
+Drop<TContractState>,
56+
+MetagameCallbackHooksTrait<TContractState>,
57+
> of IMetagameCallback<ComponentState<TContractState>> {
58+
fn on_score_update(ref self: ComponentState<TContractState>, token_id: u256, score: u32) {
59+
let mut contract = self.get_contract_mut();
60+
MetagameCallbackHooksTrait::on_score_update(ref contract, token_id, score);
61+
}
62+
63+
fn on_game_over(
64+
ref self: ComponentState<TContractState>, token_id: u256, final_score: u32,
65+
) {
66+
let mut contract = self.get_contract_mut();
67+
MetagameCallbackHooksTrait::on_game_over(ref contract, token_id, final_score);
68+
}
69+
70+
fn on_objective_complete(ref self: ComponentState<TContractState>, token_id: u256) {
71+
let mut contract = self.get_contract_mut();
72+
MetagameCallbackHooksTrait::on_objective_complete(ref contract, token_id);
73+
}
74+
}
75+
76+
// ==========================================================================
77+
// INTERNAL IMPLEMENTATION
78+
// ==========================================================================
79+
80+
#[generate_trait]
81+
pub impl InternalImpl<
82+
TContractState,
83+
+HasComponent<TContractState>,
84+
impl SRC5: SRC5Component::HasComponent<TContractState>,
85+
+Drop<TContractState>,
86+
> of InternalTrait<TContractState> {
87+
/// Initializes the component by registering the SRC5 interface.
88+
/// Should be called in the contract's constructor.
89+
fn initializer(ref self: ComponentState<TContractState>) {
90+
let mut src5_component = get_dep_component_mut!(ref self, SRC5);
91+
src5_component.register_interface(IMETAGAME_CALLBACK_ID);
92+
}
93+
}
94+
}
95+
96+
// ==============================================================================
97+
// EMPTY HOOKS IMPLEMENTATION
98+
// ==============================================================================
99+
// Provides a no-op implementation for contracts that don't need custom behavior.
100+
// Use this when you want to receive callbacks but don't need to process them.
101+
102+
pub impl MetagameCallbackHooksEmptyImpl<
103+
TContractState,
104+
> of MetagameCallbackComponent::MetagameCallbackHooksTrait<TContractState> {
105+
fn on_score_update(
106+
ref self: TContractState, token_id: u256, score: u32,
107+
) { // No-op: contracts can override for custom score handling
108+
}
109+
110+
fn on_game_over(
111+
ref self: TContractState, token_id: u256, final_score: u32,
112+
) { // No-op: contracts can override for custom game over handling
113+
}
114+
115+
fn on_objective_complete(
116+
ref self: TContractState, token_id: u256,
117+
) { // No-op: contracts can override for custom objectives handling
118+
}
119+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// ==============================================================================
2+
// METAGAME CALLBACK INTERFACE
3+
// ==============================================================================
4+
// Interface for metagame contracts to receive automatic callbacks from token
5+
// contracts when game state changes (score updates, game over, objectives
6+
// completed). This enables tournament score aggregation without manual sync.
7+
8+
/// Interface ID for IMetagameCallback (SRC5 compliant)
9+
/// Computed as: H(on_score_update) XOR H(on_game_over) XOR H(on_objective_complete)
10+
pub const IMETAGAME_CALLBACK_ID: felt252 =
11+
0x04d4f4758b99dcb4f1e2dc37c3a6e8c7a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6;
12+
13+
/// Interface for metagame contracts to receive callbacks from token contracts
14+
#[starknet::interface]
15+
pub trait IMetagameCallback<TState> {
16+
/// Called when a token's score is updated
17+
/// @param token_id The token ID (packed u256)
18+
/// @param score The new score value
19+
fn on_score_update(ref self: TState, token_id: u256, score: u32);
20+
21+
/// Called when a game ends (game_over transitions to true)
22+
/// @param token_id The token ID (packed u256)
23+
/// @param final_score The final score when game ended
24+
fn on_game_over(ref self: TState, token_id: u256, final_score: u32);
25+
26+
/// Called when the objective is completed
27+
/// @param token_id The token ID (packed u256)
28+
fn on_objective_complete(ref self: TState, token_id: u256);
29+
}

packages/metagame/src/interface.cairo

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,3 @@ pub trait IMetagame<TContractState> {
88
fn context_address(self: @TContractState) -> ContractAddress;
99
fn default_token_address(self: @TContractState) -> ContractAddress;
1010
}
11-

packages/metagame/src/lib.cairo

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ pub mod extensions;
22
pub mod interface;
33
pub mod libs;
44
pub mod metagame;
5+
pub mod structs;
56
pub mod ticket_booth;

packages/metagame/src/libs.cairo

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
use game_components_metagame::extensions::context::structs::GameContextDetails;
22
use game_components_minigame::interface::{IMinigameDispatcher, IMinigameDispatcherTrait};
3+
use game_components_registry::interface::{
4+
IMinigameRegistryDispatcher, IMinigameRegistryDispatcherTrait,
5+
};
36
use game_components_token::core::interface::{
47
IMinigameTokenDispatcher, IMinigameTokenDispatcherTrait,
58
};
6-
use game_components_token::examples::minigame_registry_contract::{
7-
IMinigameRegistryDispatcher, IMinigameRegistryDispatcherTrait,
8-
};
9+
use game_components_token::structs::MintParams;
910
use starknet::ContractAddress;
11+
use crate::structs::MintMetagameParams;
1012

1113
/// Asserts that a game is registered in the minigame token contract
1214
///
@@ -36,7 +38,7 @@ pub fn assert_game_registered(game_address: ContractAddress) {
3638
/// * `settings_id` - Optional settings ID
3739
/// * `start` - Optional start time
3840
/// * `end` - Optional end time
39-
/// * `objective_ids` - Optional objective IDs
41+
/// * `objective_id` - Optional objective ID
4042
/// * `context` - Optional context data
4143
/// * `client_url` - Optional client URL
4244
/// * `renderer_address` - Optional renderer contract address
@@ -52,7 +54,7 @@ pub fn mint(
5254
settings_id: Option<u32>,
5355
start: Option<u64>,
5456
end: Option<u64>,
55-
objective_ids: Option<Span<u32>>,
57+
objective_id: Option<u32>,
5658
context: Option<GameContextDetails>,
5759
client_url: Option<ByteArray>,
5860
renderer_address: Option<ContractAddress>,
@@ -75,7 +77,7 @@ pub fn mint(
7577
settings_id,
7678
start,
7779
end,
78-
objective_ids,
80+
objective_id,
7981
context,
8082
client_url,
8183
renderer_address,
@@ -96,7 +98,7 @@ pub fn mint(
9698
settings_id,
9799
start,
98100
end,
99-
objective_ids,
101+
objective_id,
100102
context,
101103
client_url,
102104
renderer_address,
@@ -106,3 +108,57 @@ pub fn mint(
106108
},
107109
}
108110
}
111+
112+
/// Mints multiple game tokens in batch through minigame token contracts
113+
///
114+
/// # Arguments
115+
/// * `default_token_address` - The default token address for minting when no game_address is
116+
/// provided * `mints` - Array of mint parameters for each token
117+
///
118+
/// # Returns
119+
/// * `Array<u64>` - Array of minted token IDs
120+
pub fn mint_batch(
121+
default_token_address: ContractAddress, mints: Array<MintMetagameParams>,
122+
) -> Array<u64> {
123+
let mut token_ids = array![];
124+
let mut index = 0;
125+
126+
loop {
127+
if index >= mints.len() {
128+
break;
129+
}
130+
131+
let mint_param = mints.at(index);
132+
133+
// Clone non-copyable Option types
134+
let context_clone = match mint_param.context {
135+
Option::Some(ctx) => Option::Some(ctx.clone()),
136+
Option::None => Option::None,
137+
};
138+
139+
let client_url_clone = match mint_param.client_url {
140+
Option::Some(url) => Option::Some(url.clone()),
141+
Option::None => Option::None,
142+
};
143+
144+
let token_id = mint(
145+
default_token_address,
146+
*mint_param.game_address,
147+
*mint_param.player_name,
148+
*mint_param.settings_id,
149+
*mint_param.start,
150+
*mint_param.end,
151+
*mint_param.objective_id,
152+
context_clone,
153+
client_url_clone,
154+
*mint_param.renderer_address,
155+
*mint_param.to,
156+
*mint_param.soulbound,
157+
);
158+
159+
token_ids.append(token_id);
160+
index += 1;
161+
}
162+
163+
token_ids
164+
}

0 commit comments

Comments
 (0)