From a81ec47726f4ebae226c4a92358fea9a0ce1d45c Mon Sep 17 00:00:00 2001 From: Henry Peters <96546584+henrypeters@users.noreply.github.com> Date: Mon, 1 Jun 2026 15:39:43 +0000 Subject: [PATCH] feat: add paginated layer --- contracts/escrow/src/lib.rs | 37 ++++++++++++++ contracts/escrow/src/tests/pagination.rs | 61 +++++++++++++++++++++--- 2 files changed, 92 insertions(+), 6 deletions(-) diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index 325efe4..1518f38 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -734,6 +734,8 @@ impl EscrowContract { } /// Return all match IDs for a given player (past and present). + /// + /// Deprecated: use `get_player_matches_paginated` to avoid unbounded return sizes. pub fn get_player_matches(env: Env, player: Address) -> Result, Error> { Ok(env .storage() @@ -741,6 +743,41 @@ impl EscrowContract { .get(&DataKey::PlayerMatches(player)) .unwrap_or_else(|| soroban_sdk::vec![&env])) } + + /// Return a page of match IDs for a given player. + pub fn get_player_matches_paginated( + env: Env, + player: Address, + offset: u32, + limit: u32, + ) -> Result, Error> { + let player_matches: soroban_sdk::Vec = env + .storage() + .persistent() + .get(&DataKey::PlayerMatches(player)) + .unwrap_or_else(|| soroban_sdk::vec![&env]); + + if limit == 0 { + return Ok(soroban_sdk::vec![&env]); + } + + let mut page = soroban_sdk::vec![&env]; + let mut skipped = 0u32; + let total = player_matches.len(); + + for i in 0..total { + if skipped < offset { + skipped = skipped.saturating_add(1); + continue; + } + page.push_back(player_matches.get(i).unwrap()); + if page.len() >= limit { + break; + } + } + + Ok(page) + } } #[cfg(test)] diff --git a/contracts/escrow/src/tests/pagination.rs b/contracts/escrow/src/tests/pagination.rs index f779031..cdc17aa 100644 --- a/contracts/escrow/src/tests/pagination.rs +++ b/contracts/escrow/src/tests/pagination.rs @@ -20,15 +20,32 @@ fn test_player_match_pagination_handles_empty_and_partial_pages() { match_ids.push(match_id); } - // Query player1's matches - let player1_matches = client.get_player_matches(&player1); - assert_eq!(player1_matches.len(), 25); + // Query player1's matches with paginated API + let player1_page_0 = client.get_player_matches_paginated(&player1, &0, &5); + assert_eq!(player1_page_0.len(), 5); + for (i, match_id) in player1_page_0.iter().enumerate() { + assert_eq!(*match_id, match_ids[i]); + } + + let player1_page_1 = client.get_player_matches_paginated(&player1, &5, &10); + assert_eq!(player1_page_1.len(), 10); + for (i, match_id) in player1_page_1.iter().enumerate() { + assert_eq!(*match_id, match_ids[5 + i]); + } - // Verify all match IDs are present in order - for (i, match_id) in match_ids.iter().enumerate() { - assert_eq!(player1_matches.get(i as u32).unwrap(), *match_id); + let player1_page_2 = client.get_player_matches_paginated(&player1, &20, &10); + assert_eq!(player1_page_2.len(), 5); + for (i, match_id) in player1_page_2.iter().enumerate() { + assert_eq!(*match_id, match_ids[20 + i]); } + let player1_page_3 = client.get_player_matches_paginated(&player1, &25, &10); + assert_eq!(player1_page_3.len(), 0); + + // Verify the existing getter still returns the full list for compatibility. + let player1_matches = client.get_player_matches(&player1); + assert_eq!(player1_matches.len(), 25); + // Query player2's matches (should have 25 as well) let player2_matches = client.get_player_matches(&player2); assert_eq!(player2_matches.len(), 25); @@ -39,6 +56,38 @@ fn test_player_match_pagination_handles_empty_and_partial_pages() { assert_eq!(player3_matches.len(), 0); } +/// Test #581: player match pagination returns empty page for zero limit and offset beyond end +#[test] +fn test_player_match_pagination_zero_limit_and_offset_beyond_end() { + let (env, contract_id, _oracle, player1, player2, token, _admin) = setup(); + let client = EscrowContractClient::new(&env, &contract_id); + + // Create 10 matches for player1 + let mut match_ids = Vec::new(); + for i in 0..10 { + let match_id = client.create_match( + &player1, + &player2, + &100, + &token, + &String::from_str(&env, &format!("game_{}", i)), + &Platform::Lichess, + ); + match_ids.push(match_id); + } + + let zero_limit = client.get_player_matches_paginated(&player1, &0, &0); + assert_eq!(zero_limit.len(), 0); + + let beyond_offset = client.get_player_matches_paginated(&player1, &15, &5); + assert_eq!(beyond_offset.len(), 0); + + let partial_page = client.get_player_matches_paginated(&player1, &8, &5); + assert_eq!(partial_page.len(), 2); + assert_eq!(partial_page.get(0).unwrap(), match_ids[8]); + assert_eq!(partial_page.get(1).unwrap(), match_ids[9]); +} + /// Test #579: player history index excludes unrelated matches for other players #[test] fn test_player_history_index_excludes_unrelated_matches() {