-
Notifications
You must be signed in to change notification settings - Fork 95
feat: implement admin game settlement and staking logic #661
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -693,6 +693,42 @@ impl GameContract { | |||||||
| Ok(()) | ||||||||
| } | ||||||||
|
|
||||||||
| /// Settle the game gracefully by the admin/backend (e.g. checkmate validated off-chain). | ||||||||
| /// This sets the game to Completed and automatically processes the payout. | ||||||||
| pub fn resolve_game(env: Env, game_id: u64, winner: Address) -> Result<(), ContractError> { | ||||||||
| let admin: Address = env | ||||||||
| .storage() | ||||||||
| .instance() | ||||||||
| .get(&CONTRACT_ADMIN) | ||||||||
| .expect("Not initialized"); | ||||||||
| admin.require_auth(); | ||||||||
|
|
||||||||
| let mut games: Map<u64, Game> = env | ||||||||
| .storage() | ||||||||
| .instance() | ||||||||
| .get(&GAMES) | ||||||||
| .ok_or(ContractError::GameNotFound)?; | ||||||||
|
|
||||||||
| let mut game = games.get(game_id).ok_or(ContractError::GameNotFound)?; | ||||||||
|
|
||||||||
| if game.state != GameState::InProgress { | ||||||||
| return Err(ContractError::GameNotInProgress); | ||||||||
| } | ||||||||
|
|
||||||||
| if game.player1 != winner && Some(winner.clone()) != game.player2 { | ||||||||
| return Err(ContractError::NotPlayer); | ||||||||
| } | ||||||||
|
|
||||||||
| game.state = GameState::Completed; | ||||||||
| game.winner = Some(winner.clone()); | ||||||||
| Self::process_payout(&env, &game, &winner)?; | ||||||||
|
|
||||||||
| games.set(game_id, game); | ||||||||
| env.storage().instance().set(&GAMES, &games); | ||||||||
|
|
||||||||
| Ok(()) | ||||||||
| } | ||||||||
|
|
||||||||
| pub fn payout_tournament( | ||||||||
| env: Env, | ||||||||
| game_id: u64, | ||||||||
|
|
@@ -3008,3 +3044,4 @@ mod tests { | |||||||
| assert_eq!(second, Err(Ok(ContractError::AlreadySettled))); | ||||||||
| } | ||||||||
| } | ||||||||
| mod test; | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Inspect the unguarded Rust test module and its imports.
fd '^lib\.rs$' | xargs rg -n -C2 '^\s*mod test;\s*$|^\s*#\[cfg\(test\)\]'
fd '^test\.rs$' | xargs sed -n '1,120p'Repository: NOVUS-X/XLMate Length of output: 5612 Add The external test module Proposed fix-mod test;
+#[cfg(test)]
+mod test;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,7 @@ extern crate std; | |
| use super::*; | ||
| use ed25519_dalek::{Signer, SigningKey}; | ||
| use rand::rngs::OsRng; | ||
| use soroban_sdk::token::StellarAssetClient; | ||
| use soroban_sdk::token::{StellarAssetClient, TokenClient}; | ||
| use soroban_sdk::{Address, Bytes, BytesN, Env, Map, Vec, testutils::Address as _}; | ||
|
|
||
| /// Helper: seed a completed game directly into contract storage, bypassing | ||
|
|
@@ -765,3 +765,44 @@ fn test_submit_move_player1_cannot_move_twice() { | |
| let res = client.try_submit_move(&game_id, &player1, &Vec::from_array(&env, [2u32])); | ||
| assert_eq!(res, Err(Ok(ContractError::NotYourTurn))); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_resolve_game_by_admin() { | ||
| let env = Env::default(); | ||
| env.mock_all_auths(); | ||
|
|
||
| let contract_id = env.register_contract(None, GameContract); | ||
| let client = GameContractClient::new(&env, &contract_id); | ||
|
|
||
| let admin = Address::generate(&env); | ||
| let issuer = Address::generate(&env); | ||
| let player1 = Address::generate(&env); | ||
| let player2 = Address::generate(&env); | ||
| let treasury_addr = Address::generate(&env); | ||
|
|
||
| let stellar_token = env.register_stellar_asset_contract_v2(issuer); | ||
| let token_address = stellar_token.address(); | ||
| let stellar_asset_client = StellarAssetClient::new(&env, &token_address); | ||
| let token_client = TokenClient::new(&env, &token_address); | ||
|
|
||
| client.initialize_token(&admin, &token_address); | ||
|
|
||
| let admin_key = Bytes::from_slice(&env, &[0u8; 32]); | ||
| client.initialize_puzzle_rewards(&admin, &admin_key, &0i128, &20u32, &treasury_addr); | ||
|
|
||
| let wager = 500; | ||
| stellar_asset_client.mint(&player1, &wager); | ||
| stellar_asset_client.mint(&player2, &wager); | ||
|
|
||
| let game_id = client.create_game(&player1, &wager); | ||
| client.join_game(&game_id, &player2); | ||
|
|
||
| // Let the admin resolve the game, meaning player1 won | ||
| client.resolve_game(&game_id, &player1); | ||
|
|
||
| // Check balances | ||
| // Pool = 1000. Fee = 1000 * 20 / 1000 = 20. Payout = 980. | ||
| assert_eq!(token_client.balance(&player1), 980); | ||
| assert_eq!(token_client.balance(&treasury_addr), 20); | ||
| assert_eq!(token_client.balance(&player2), 0); | ||
| } | ||
|
Comment on lines
+769
to
+808
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test does not actually verify admin-only authorization or final game state. Two gaps in a test that the PR summary claims verifies "authorization scoping" and settlement:
Suggested additions// After resolve_game:
env.as_contract(&contract_id, || {
let games: Map<u64, Game> = env.storage().instance().get(&GAMES).unwrap();
let game = games.get(game_id).unwrap();
assert_eq!(game.state, GameState::Completed);
assert_eq!(game.winner, Some(player1.clone()));
});
// And a separate test:
#[test]
fn test_resolve_game_rejects_non_admin() {
// ...setup identical up to create_game/join_game...
let stranger = Address::generate(&env);
// Use per-address auth mocks instead of mock_all_auths so the admin
// requirement is actually checked.
let res = client
.mock_auths(&[/* stranger only */])
.try_resolve_game(&game_id, &player1);
assert!(res.is_err());
}🤖 Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prevent replay payout after admin resolution.
resolve_gamepays immediately, then persists aCompletedgame with a winner. The publicpayoutpath accepts that same state/winner and can callprocess_payoutagain, draining other funds if the contract has enough token balance. Add an explicit paid/settled guard, or makeprocess_payoutreject when both player escrow deposits are no longer present.🛡️ One minimal guard in
process_payout// Deduct both stakes first (clean state, prevents double-spend) let player1_escrow = escrow.get(game.player1.clone()).unwrap_or(0); + if player1_escrow < game.wager_amount { + return Err(ContractError::InsufficientFunds); + } escrow.set(game.player1.clone(), player1_escrow - game.wager_amount); let player2 = game.player2.as_ref().ok_or(ContractError::GameFull)?; let player2_escrow = escrow.get(player2.clone()).unwrap_or(0); + if player2_escrow < game.wager_amount { + return Err(ContractError::InsufficientFunds); + } escrow.set(player2.clone(), player2_escrow - game.wager_amount);🤖 Prompt for AI Agents