Skip to content

Add follow player feature with in-app notifications - #105

Open
loothero wants to merge 17 commits into
mainfrom
push-notifications
Open

Add follow player feature with in-app notifications#105
loothero wants to merge 17 commits into
mainfrom
push-notifications

Conversation

@loothero

@loothero loothero commented Jan 18, 2026

Copy link
Copy Markdown
Member

Summary

Implement Phase 1 of the Follow Player Feature - in-app notifications using polling via Torii SQL queries. This allows players to follow other players and receive toast notifications when followed players start new games.

Changes

New Files

  • client/src/stores/followStore.ts: Zustand store with localStorage persistence for follow relationships
  • client/src/components/FollowButton.tsx: Reusable heart icon button for follow/unfollow actions
  • client/src/components/FollowingList.tsx: Component to display list of followed players
  • client/src/hooks/useFollowedPlayerNotifications.tsx: Polling-based hook (30s interval) that checks for new games from followed players via Torii SQL

Modified Files

  • client/src/desktop/components/Leaderboard.tsx: Added follow button next to player names, added "Following" tab
  • client/src/mobile/components/Leaderboard.tsx: Added follow button next to player names, added "Following" tab
  • client/src/App.tsx: Initialize notification subscription hook at app level

Features

  • Follow/unfollow players via heart icon in leaderboard (doesn't show for self)
  • "Following" tab in leaderboard filters to show only games from followed players
  • In-app toast notifications when followed players start new games, with "Watch" button
  • Follow relationships persist across sessions via localStorage

Testing

  • Verified pnpm build passes without errors
  • TypeScript compilation successful
  • All new components follow existing codebase patterns

@codex review

Summary by CodeRabbit

  • New Features

    • Player follow/unfollow system with follow button, followed-players list, player search, and "Following" tab
    • Notifications for newly created games by followed players
    • Inline player name editing in leaderboards (desktop & mobile)
  • Documentation

    • Added comprehensive master project and frontend onboarding docs; several locale docs now reference it
  • Chores

    • Added public environment configuration for the contract address

✏️ Tip: You can customize this high-level summary in your review settings.

loothero and others added 11 commits January 6, 2026 22:44
Allow players to edit their adventurer names directly from the Leaderboard view.
Only game owners can see and use the edit functionality.

Changes:
- Add updatePlayerName system call to useSystemCalls hook
- Add inline edit UI with input field, save/cancel buttons
- Add ownership check to show edit icon only for owned games
- Support keyboard shortcuts (Enter to save, Escape to cancel)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Store locally updated names to prevent UI reverting to old name
while waiting for backend to sync after transaction.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When re-editing a name, use the locally stored name instead of
the stale value from the backend data.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Validate incoming games data matches the active tab before updating.
For "My Games" tab, only accept data where all games are owned by
the current user - this prevents stale "All" data from overwriting.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Port all desktop leaderboard improvements to mobile:
- Inline name editing with save/cancel buttons
- Local name overrides for optimistic UI updates
- Race condition fix to prevent stale data when switching tabs
- Ownership check to show edit icon only for owned games

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Check that address is defined before using addAddressPadding in the
race condition validation effect.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace hardcoded contract address with VITE_PUBLIC_DENSHOKAN_ADDRESS env var
- Add validation for account connection and name length in updatePlayerName
- Add error snackbar notification when name update fails
- Fix loading vs empty state display logic
- Add aria-labels for accessibility (InputBase, Save/Cancel/Edit buttons)
- Extract MAX_PLAYER_NAME_LENGTH constant
- Pre-compute ownership using useMemo Set for better performance
- Use theme color 'primary.main' for CircularProgress

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement ability for players to follow other players and receive
notifications when followed players start new games.

New files:
- followStore.ts: Zustand store with localStorage persistence
- FollowButton.tsx: Heart icon button for follow/unfollow
- FollowingList.tsx: Component to display followed players
- useFollowedPlayerNotifications.tsx: Polling hook for game notifications

Changes:
- Add "Following" tab to desktop and mobile leaderboards
- Add follow button next to player names in leaderboard
- Initialize notification subscription in App.tsx

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings January 18, 2026 01:09
@vercel

vercel Bot commented Jan 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
loot-survivor-2 Ready Ready Preview, Comment Jan 18, 2026 2:53am

Request Review

@coderabbitai

coderabbitai Bot commented Jan 18, 2026

Copy link
Copy Markdown

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds a persistent player-following system, UI components for follow/search/list, polling notifications for newly minted games from followed players via Torii, inline player-name editing backed by a Denshokan contract call, environment config for Denshokan, and consolidated AGENTS.md documentation across repo, client, and contracts.

Changes

Cohort / File(s) Summary
Docs: root & modules
AGENTS.md, CLAUDE.md, GEMINI.md, client/AGENTS.md, client/CLAUDE.md, client/GEMINI.md, contracts/AGENTS.md, contracts/CLAUDE.md, contracts/GEMINI.md
Add/replace documentation: new comprehensive AGENTS.md files; many CLAUDE/GEMINI files reduced to @AGENTS.md references.
Env: Denshokan address
client/.env.local, client/.env.production
Add VITE_PUBLIC_DENSHOKAN_ADDRESS environment variable for Denshokan contract usage.
Client store
client/src/stores/followStore.ts
New Zustand useFollowStore with FollowedPlayer interface, follow/unfollow actions, lookups, and persistence.
Follow UI components
client/src/components/FollowButton.tsx, client/src/components/FollowingList.tsx, client/src/components/PlayerSearch.tsx
New components: follow/unfollow heart button, followed-players list with unfollow actions, and PlayerSearch panel that queries Torii and allows following players.
Notifications hook
client/src/hooks/useFollowedPlayerNotifications.tsx
New hook that polls Torii for TokenMetadataUpdate entries for followed owners, enqueues snackbars with a Watch action, and navigates to games.
App integration
client/src/App.tsx
Initialize notifications by invoking useFollowedPlayerNotifications() inside AppContent.
Player search hook
client/src/hooks/usePlayerSearch.ts
New usePlayerSearch hook that queries Torii SQL, decodes player names, filters/dedupes results, and exposes searchPlayers/clearResults.
System calls
client/src/dojo/useSystemCalls.ts
Add updatePlayerName(tokenId, name) using VITE_PUBLIC_DENSHOKAN_ADDRESS, validates input, calls Denshokan update_player_name, waits for tx receipt, and exports it.
Leaderboards (desktop & mobile)
client/src/desktop/components/Leaderboard.tsx, client/src/mobile/components/Leaderboard.tsx
Add Following tab, integrate FollowButton and PlayerSearch, implement inline per-game name editing with local overrides and save flow, adjust tab-specific filtering, pagination, and editing UI.
Client content docs
client/AGENTS.md
New frontend-focused AGENTS.md describing project structure, patterns, stores, and build/dev commands.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI as "UI\n(FollowButton / Leaderboard)"
    participant Store as "Follow Store\n(useFollowStore)"
    participant Hook as "Notif Hook\n(useFollowedPlayerNotifications)"
    participant Torii as "Torii\n(SQL)"
    participant SC as "System Calls\n(useSystemCalls)"
    participant Contract as "Denshokan\nContract"

    User->>UI: Click follow / unfollow
    UI->>Store: followPlayer(address) / unfollowPlayer(address)
    Store-->>UI: state updated -> re-render

    Note over Hook,Store: Polling for new games
    Hook->>Store: getFollowedAddresses()
    Hook->>Torii: query TokenMetadataUpdate (since lastSeen)
    Torii-->>Hook: results
    Hook->>UI: enqueue snackbar (Watch button) for new games

    User->>UI: Edit name & save
    UI->>SC: updatePlayerName(tokenId, name)
    SC->>Contract: call update_player_name (tx)
    Contract-->>SC: tx receipt
    SC-->>UI: return receipt
    UI->>Store: apply local name override / re-render
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • Obstacle Icons & Leaderboard #65 — Modifies Leaderboard components and useSystemCalls; likely overlaps with follow/search and updatePlayerName changes.
  • Feat/support multiple dungeons #95 — Adjusts useSystemCalls wiring and address handling; may conflict with Denshokan call addition.
  • Next #68 — Refactors transaction waiting and helpers in useSystemCalls; relevant to the new updatePlayerName implementation.

Poem

🐰
I hopped through code with hearts aglow,
I tracked new games where followings grow,
I polled the Torii, rang the bell,
A Watch button chimed — come see and tell!
— a rabbit cheers: follow, watch, and know.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Add follow player feature with in-app notifications' directly and clearly summarizes the main changes: a new follow player feature coupled with notification functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude

claude Bot commented Jan 18, 2026

Copy link
Copy Markdown

Claude finished @loothero's task —— View job


Code Review: Follow Player Feature

Reviewing the follow player feature implementation with in-app notifications.

Todo List

  • Read repository guidelines and conventions
  • Review new component implementations
  • Analyze store and hook implementations
  • Check integration with existing leaderboard components
  • Evaluate performance and security considerations
  • Provide comprehensive feedback and recommendations

Code Review Summary

✅ Strengths

Architecture & Patterns:

  • Clean separation of concerns with dedicated store (followStore.ts), components (FollowButton, FollowingList), and notification hook
  • Follows established codebase patterns using Zustand, MUI, and TypeScript
  • Proper integration with existing leaderboard components for both desktop and mobile

StarkNet Integration:

  • Correct address normalization using addAddressPadding() and lowercase conversion
  • Consistent handling throughout the codebase
  • Proper self-detection to hide follow buttons on own games

User Experience:

  • Intuitive heart icon for follow/unfollow actions with hover states
  • "Following" tab in leaderboard for easy access to followed players' games
  • Toast notifications with "Watch" action buttons
  • localStorage persistence for follow relationships

🔴 Critical Issues

1. SQL Injection Vulnerability (useFollowedPlayerNotifications.tsx:47-55)

const ownersList = followedAddresses
  .map((addr) => `"${addAddressPadding(addr)}"`)
  .join(',');

const url = `${currentNetworkConfig.toriiUrl}/sql?query=
  SELECT token_id, owner, player_name, minted_at
  FROM "relayer_0_0_1-TokenMetadataUpdate"
  WHERE owner IN (${ownersList})`; // Direct string interpolation!

Impact: Malicious addresses could inject SQL commands
Fix: Use parameterized queries or POST body with proper escaping

2. Performance Concerns

  • Continuous Polling: 30-second intervals run indefinitely, even when user is inactive
  • No Request Deduplication: Multiple simultaneous requests possible due to missing isPollingRef.current reset on error
  • Unbounded Storage: No limit on followed players or cleanup mechanism

🟡 Security & Robustness Issues

3. Error Handling Gaps

  • Silent failures in SQL queries could miss notifications
  • No offline detection or retry logic for network failures
  • Race conditions possible between polling timer and component unmount

4. Memory Management

  • Follow relationships persist indefinitely with no cleanup
  • No upper bounds on localStorage usage
  • Notification polling continues even when notifications disabled

🟠 Minor Issues

5. Code Quality

  • Some TypeScript any types in leaderboard integration could be more specific
  • Magic numbers (30s poll interval) should be configurable constants
  • Missing validation for malformed API responses

Recommendations

High Priority

  1. Fix SQL injection: Use Torii's parameterized query API or validate/sanitize addresses
  2. Add polling controls: Stop polling when user inactive, limit concurrent requests
  3. Implement bounds: Max followed players limit, localStorage quota management

Medium Priority

  1. Improve error handling: Add retry logic, offline detection, proper error boundaries
  2. Performance optimization: Request deduplication, smarter polling intervals
  3. Type safety: Replace any types with proper interfaces

Low Priority

  1. UX enhancements: Loading states, follow limits UI, notification settings
  2. Testing: Unit tests for store logic and notification edge cases

Overall Assessment

This is a well-architected feature that follows the codebase patterns effectively. The core functionality is solid, but the SQL injection vulnerability is critical and must be addressed before merging. With security fixes and performance optimizations, this will be a valuable addition to the game's social features.

Recommendation: Request changes to address security issues before approval.


@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello @loothero, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the social and interactive aspects of the game by introducing a 'Follow Player' feature. Players can now track their favorite or friends' activities, receiving timely in-app notifications when followed players initiate new games. This is complemented by UI updates to the leaderboard, allowing easy following/unfollowing and filtering of games, alongside the ability for players to personalize their in-game names. The changes also include a major overhaul of the project's documentation structure.

Highlights

  • Player Following System: Introduced the ability for players to follow other players and receive in-app notifications when they start new games.
  • In-App Notifications: Implemented a polling-based system using Torii SQL queries to deliver real-time toast notifications for new games from followed players, including a 'Watch' button.
  • Leaderboard Enhancements: Added 'Follow' buttons next to player names on both desktop and mobile leaderboards, along with a new 'Following' tab to filter games by followed players.
  • Persistent Follow State: Player follow relationships are now persisted across sessions using localStorage through a new Zustand store.
  • Player Name Editing: Enabled players to edit their in-game names directly from the leaderboard, with changes reflected on-chain via a new system call.
  • Documentation Consolidation: Introduced new AGENTS.md files at the root, client, and contracts directories to centralize and provide comprehensive documentation, with existing CLAUDE.md and GEMINI.md files updated to redirect to these new guides.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a 'follow player' feature, complete with in-app notifications for when followed players start new games. The implementation includes a new Zustand store for managing follow state, new UI components for following/unfollowing and listing followed players, and a polling mechanism to check for new games. The leaderboard is also updated to integrate this new functionality.

The code is generally well-structured and the new features are thoughtfully implemented. I've identified a couple of high-severity issues in the notification polling logic that could lead to incorrect behavior, and some medium-severity issues in the leaderboard components related to performance and type safety. My detailed comments provide suggestions for addressing these points.

Comment on lines +52 to +57
const url = `${currentNetworkConfig.toriiUrl}/sql?query=
SELECT token_id, owner, player_name, minted_at
FROM "relayer_0_0_1-TokenMetadataUpdate"
WHERE owner IN (${ownersList})
ORDER BY minted_at DESC
LIMIT 10`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The SQL query hardcodes the table name "relayer_0_0_1-TokenMetadataUpdate". The relayer_0_0_1 part appears to be a namespace. Hardcoding this is brittle and may break if the namespace changes in different environments or future deployments. This value should be retrieved from a configuration, similar to how namespace is used in other parts of the codebase (e.g., from currentNetworkConfig).

Comment on lines +73 to +98
// Find games that were minted after our last check
const newGames = data.filter((game) => {
const mintedAt = new Date(game.minted_at).getTime();
return mintedAt > lastSeenTimestampRef.current;
});

// Show notifications for new games
for (const game of newGames) {
const normalizedOwner = addAddressPadding(game.owner).toLowerCase();
const followedPlayer = followedPlayers[normalizedOwner];
const playerName = followedPlayer?.name || game.player_name || 'A followed player';

enqueueSnackbar(`${playerName} just started a new game!`, {
variant: 'info',
autoHideDuration: 8000,
action: (
<Button
size="small"
color="inherit"
onClick={() => watchGame(game.token_id)}
>
Watch
</Button>
),
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current logic to find new games uses mintedAt > lastSeenTimestampRef.current. If multiple games are created with the exact same timestamp as the last seen one, they will be missed. This can be fixed by using >= and tracking notified game IDs to prevent duplicates.

You'll need to add a ref to store notified game IDs at the top of your component: const notifiedGameIds = useRef(new Set<number>());

      // Find games that were minted after our last check
      const newGames = data.filter((game) => {
        const mintedAt = new Date(game.minted_at).getTime();
        return mintedAt >= lastSeenTimestampRef.current && !notifiedGameIds.current.has(game.token_id);
      });

      // Show notifications for new games
      for (const game of newGames) {
        const normalizedOwner = addAddressPadding(game.owner).toLowerCase();
        const followedPlayer = followedPlayers[normalizedOwner];
        const playerName = followedPlayer?.name || game.player_name || 'A followed player';

        enqueueSnackbar(`${playerName} just started a new game!`, {
          variant: 'info',
          autoHideDuration: 8000,
          action: (
            <Button
              size="small"
              color="inherit"
              onClick={() => watchGame(game.token_id)}
            >
              Watch
            </Button>
          ),
        });
        notifiedGameIds.current.add(game.token_id);
      }

Comment on lines +129 to +136
} else if (activeTab === 2) {
// Following tab: filter to show only games from followed players
const filteredGames = games.filter((g: any) => {
const normalizedOwner = addAddressPadding(g.owner).toLowerCase();
return followedAddresses.includes(normalizedOwner);
});
setDisplayedGames(filteredGames);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For the "Following" tab, all games are fetched and then filtered on the client-side. This can be inefficient if the total number of games is large. The useFollowedPlayerNotifications hook demonstrates building a direct SQL query with WHERE owner IN (...) to fetch data only for specific players. A similar approach could be used here for better performance, for example by creating a new specialized hook that fetches only the games for followed players, or by extending useGameTokens to support filtering by multiple owners if possible.

<Typography sx={{ textAlign: "center", py: 2 }}>
{activeTab === 1 ? "You have no games yet." : activeTab === 2 ? "No games from followed players." : "No games found."}
</Typography>
) : displayedGames.map((game: any, index: number) => (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The game object is typed as any. This weakens type safety and makes the code harder to maintain. Please define and use a specific type for the game objects returned by useGameTokens. The metagame-sdk/sql package might export a type for this, or you could define one locally.

Comment on lines +126 to +133
} else if (activeTab === 2) {
// Following tab: filter to show only games from followed players
const filteredGames = games.filter((g: any) => {
const normalizedOwner = addAddressPadding(g.owner).toLowerCase();
return followedAddresses.includes(normalizedOwner);
});
setDisplayedGames(filteredGames);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For the "Following" tab, all games are fetched and then filtered on the client-side. This can be inefficient if the total number of games is large. The useFollowedPlayerNotifications hook demonstrates building a direct SQL query with WHERE owner IN (...) to fetch data only for specific players. A similar approach could be used here for better performance, for example by creating a new specialized hook that fetches only the games for followed players, or by extending useGameTokens to support filtering by multiple owners if possible.

<Typography sx={{ textAlign: "center", py: 2 }}>
{activeTab === 1 ? "You have no games yet." : activeTab === 2 ? "No games from followed players." : "No games found."}
</Typography>
) : displayedGames.map((game: any, index: number) => (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The game object is typed as any. This weakens type safety and makes the code harder to maintain. Please define and use a specific type for the game objects returned by useGameTokens. The metagame-sdk/sql package might export a type for this, or you could define one locally.

@claude

claude Bot commented Jan 18, 2026

Copy link
Copy Markdown

Claude finished @loothero's task —— View job


Code Review: Follow Player Feature

Comprehensive review of the follow player feature implementation with in-app notifications.

Todo List

  • Read repository guidelines and conventions
  • Review new component implementations
  • Analyze store and hook implementations
  • Check integration with existing leaderboard components
  • Evaluate performance and security considerations
  • Provide comprehensive feedback and recommendations

Review Summary

This PR implements a well-architected follow player feature with in-app notifications. The code follows established patterns and integrates cleanly with the existing codebase. However, there are several critical security and performance issues that need attention.

Critical Issues ⚠️

1. SQL Injection Vulnerability

File: client/src/hooks/useFollowedPlayerNotifications.tsx:52-57

const url = `${currentNetworkConfig.toriiUrl}/sql?query=
  SELECT token_id, owner, player_name, minted_at
  FROM "relayer_0_0_1-TokenMetadataUpdate"
  WHERE owner IN (${ownersList})
  ORDER BY minted_at DESC
  LIMIT 10`;

Issues:

  • Direct string interpolation creates SQL injection risk
  • Raw SQL query in URL parameters is insecure
  • Hardcoded table name "relayer_0_0_1-TokenMetadataUpdate" should use namespace config

Solution: Use POST request with parameterized queries or SQL escaping.

2. Notification Logic Bug

File: client/src/hooks/useFollowedPlayerNotifications.tsx:74-76

const newGames = data.filter((game) => {
  const mintedAt = new Date(game.minted_at).getTime();
  return mintedAt > lastSeenTimestampRef.current;
});

Issue: Games with identical timestamps as lastSeenTimestamp will be missed.

Solution: Use >= and track notified game IDs to prevent duplicates.

Performance & Scalability Issues 🚨

1. Inefficient Client-Side Filtering

Files:

  • client/src/desktop/components/Leaderboard.tsx:131-135
  • client/src/mobile/components/Leaderboard.tsx:133-137

The "Following" tab fetches all games then filters client-side. This doesn't scale with large datasets.

Solution: Implement server-side filtering similar to the notifications hook's SQL approach.

2. Unlimited localStorage Growth

File: client/src/stores/followStore.ts

No cleanup mechanism for old follows or upper limits on followed players.

Recommendations:

  • Add max follow limit (e.g., 100 players)
  • Implement periodic cleanup of inactive follows
  • Add data migration for schema changes

Security Concerns 🔒

1. Table Name Hardcoding

The notifications query hardcodes "relayer_0_0_1-TokenMetadataUpdate" but should derive the namespace from config:

const tableName = `"${currentNetworkConfig.namespace}-TokenMetadataUpdate"`;

2. Address Validation

While address normalization is good, consider additional validation for malformed addresses before database queries.

Code Quality Issues 📝

1. Type Safety

Files: Leaderboard components

game objects are typed as any. Define proper interfaces:

interface GameToken {
  token_id: number;
  owner: string;
  player_name: string;
  score: number;
  game_over: boolean;
  minted_at: string;
}

2. Error Handling

Limited error handling in notification polling. Consider:

  • Exponential backoff for failed requests
  • User notification of connection issues
  • Graceful degradation when offline

Minor Improvements 🔧

1. Performance Optimizations

  • Debounce follow/unfollow actions to prevent rapid API calls
  • Implement lazy loading for large follow lists
  • Cache player names to reduce redundant fetches

2. UX Enhancements

  • Add loading states for follow/unfollow actions
  • Show notification count badge
  • Allow bulk unfollow operations

Test Coverage 🧪

Missing: The PR lacks test coverage for:

  • Follow store state management
  • Notification polling logic
  • Error scenarios
  • Address normalization

Note: Cannot run existing test suite without proper permissions. Recommend adding tests for the new functionality.

Recommendations 📋

High Priority (Security/Performance):

  1. Fix SQL injection vulnerability with parameterized queries
  2. Fix notification timestamp comparison logic
  3. Implement server-side filtering for "Following" tab
  4. Add proper TypeScript interfaces

Medium Priority (Robustness):
5. Add error boundaries and offline handling
6. Implement follow limits and cleanup
7. Add comprehensive test coverage

Low Priority (UX):
8. Add loading states and better error messaging
9. Optimize performance with debouncing/caching

Overall Assessment ✅

Despite the critical issues identified, this is a well-structured implementation that follows the codebase patterns. The architecture is sound and the user experience is intuitive. With the security and performance fixes implemented, this would be a solid addition to the game's social features.

Status: ⚠️ Needs Work - Address critical security issues before merging.


@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dad90fcc88

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +74 to +76
const newGames = data.filter((game) => {
const mintedAt = new Date(game.minted_at).getTime();
return mintedAt > lastSeenTimestampRef.current;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Parse Torii minted_at before Date comparison

The Torii SQL tables return numeric fields as raw felt strings (often hex) and token metadata timestamps are seconds-based (see getTokenMetadata in client/src/api/starknet.ts, which does parseInt(..., 16) * 1000). Here new Date(game.minted_at) will produce NaN for hex strings or a 1970-era timestamp for seconds strings, so mintedAt > lastSeenTimestampRef.current will never be true and lastSeenTimestampRef can become NaN, effectively disabling notifications for all followed players. Parse the timestamp (e.g. parseInt(game.minted_at, 16) * 1000 or detect numeric seconds) before comparing.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements Phase 1 of a follow player feature that allows users to follow other players and receive in-app toast notifications when followed players start new games. The implementation uses localStorage for persistence and Torii SQL polling for notifications.

Changes:

  • Added Zustand store with localStorage persistence for managing follow relationships
  • Created reusable FollowButton and FollowingList components for follow interactions
  • Implemented polling-based notification hook that queries Torii SQL every 30 seconds
  • Added "Following" tab to desktop and mobile leaderboards showing games from followed players
  • Added player name editing functionality to leaderboards with inline editing UI
  • Updated documentation files (AGENTS.md, CLAUDE.md, GEMINI.md) with comprehensive project guidance

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
client/src/stores/followStore.ts Zustand store managing follow relationships with localStorage persistence
client/src/components/FollowButton.tsx Heart icon button component for follow/unfollow actions with self-follow prevention
client/src/components/FollowingList.tsx Component displaying list of followed players (created but not yet integrated)
client/src/hooks/useFollowedPlayerNotifications.tsx Polling hook that checks Torii SQL for new games from followed players
client/src/desktop/components/Leaderboard.tsx Added Following tab, follow buttons, and inline name editing
client/src/mobile/components/Leaderboard.tsx Added Following tab, follow buttons, and inline name editing
client/src/dojo/useSystemCalls.ts Added updatePlayerName function for updating game token names
client/src/App.tsx Initialized notification subscription hook at app level
client/.env.production Added DENSHOKAN_ADDRESS environment variable
client/.env.local Added DENSHOKAN_ADDRESS environment variable
contracts/AGENTS.md Comprehensive Cairo smart contract documentation
client/AGENTS.md Comprehensive React frontend documentation
AGENTS.md Root-level project overview documentation
GEMINI.md, CLAUDE.md, contracts/GEMINI.md, contracts/CLAUDE.md, client/GEMINI.md, client/CLAUDE.md Documentation reference files
Comments suppressed due to low confidence (1)

client/src/dojo/useSystemCalls.ts:489

  • The validation only checks if name length is greater than 31, but doesn't check if the name is empty after trimming. An empty string will pass this validation and be sent to the contract, which may fail or behave unexpectedly.

Add a check for empty names after trimming: if (!name || !name.trim() || name.length > 31)

    await executeAction([{
      contractAddress: DUNGEON_ADDRESS,
      entrypoint: "claim_jackpot",

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +66 to +69
if (!response.ok) {
console.error('Failed to fetch new games:', response.statusText);
return;
}

Copilot AI Jan 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the fetch request fails (response.ok is false), the function only logs an error and returns early, but isPollingRef.current remains true. This prevents any subsequent polling attempts because the next call to checkForNewGames will immediately return due to the isPollingRef check.

The isPollingRef.current should be set to false when returning early on error, or the error handling should be moved inside the try-catch block so the finally clause handles cleanup.

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +57
// Build SQL query to check for new games from followed players
const ownersList = followedAddresses
.map((addr) => `"${addAddressPadding(addr)}"`)
.join(',');

// Query for recently minted games by followed players
const url = `${currentNetworkConfig.toriiUrl}/sql?query=
SELECT token_id, owner, player_name, minted_at
FROM "relayer_0_0_1-TokenMetadataUpdate"
WHERE owner IN (${ownersList})
ORDER BY minted_at DESC
LIMIT 10`;

Copilot AI Jan 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SQL query is vulnerable to injection if address values contain double quotes or other special characters. Although addAddressPadding comes from the starknet library, the addresses stored in followedPlayers come from user input through the followPlayer function. An attacker could potentially manipulate addresses stored in localStorage to inject SQL.

The addresses should be properly escaped or validated before being included in the SQL query, or use a parameterized query approach if the API supports it.

Copilot uses AI. Check for mistakes.
Comment thread client/GEMINI.md
@@ -0,0 +1 @@
AGENTS.md

Copilot AI Jan 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reference to AGENTS.md is missing the @ symbol prefix that is used consistently in other similar files. For example, contracts/GEMINI.md and CLAUDE.md use "@AGENTS.md" while this file uses "AGENTS.md".

Suggested change
AGENTS.md
@AGENTS.md

Copilot uses AI. Check for mistakes.
const handleTabChange = useCallback((event: React.SyntheticEvent, newValue: number) => {
setActiveTab(newValue);
};
}, []);

Copilot AI Jan 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When switching between tabs, the pagination state (currentPage) is not reset to 0. This can lead to a confusing user experience where switching to the "Following" or "My Games" tab shows an empty page because the user was on page 5 of "All" tab, but "Following" only has 1 page of results.

Consider resetting the page to 0 when activeTab changes, or manage separate pagination state for each tab.

Suggested change
}, []);
goToPage(0);
}, [goToPage]);

Copilot uses AI. Check for mistakes.
Comment on lines +337 to +338
playerName={localNameOverrides[game.token_id] ?? game.player_name ?? `Player #${game.token_id}`}
/>

Copilot AI Jan 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a player follows someone, the current displayed name (which could be from localNameOverrides) is stored in the followStore. However, if the player later edits their own name, the followed player's stored name in followStore is not updated. This creates an inconsistency where notifications and the FollowingList might show an outdated name.

Consider storing only the address in followStore and always fetching the current name from the games data, or implement a mechanism to update followed player names when they change.

Suggested change
playerName={localNameOverrides[game.token_id] ?? game.player_name ?? `Player #${game.token_id}`}
/>
/>

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +43
followPlayer: (address: string, name: string) => {
const normalizedAddress = address.toLowerCase();
set((state) => ({
followedPlayers: {
...state.followedPlayers,
[normalizedAddress]: {
address: normalizedAddress,
name,
followedAt: Date.now(),
},
},
}));
},

Copilot AI Jan 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The followPlayer function does not validate the address or name parameters. Malicious input in the name parameter could lead to XSS vulnerabilities when the name is displayed in the UI, or storage issues. The address parameter should also be validated to ensure it's a valid Starknet address format before normalization.

Add input validation for both address format and name sanitization to prevent security issues.

Copilot uses AI. Check for mistakes.
Comment on lines +129 to +135
} else if (activeTab === 2) {
// Following tab: filter to show only games from followed players
const filteredGames = games.filter((g: any) => {
const normalizedOwner = addAddressPadding(g.owner).toLowerCase();
return followedAddresses.includes(normalizedOwner);
});
setDisplayedGames(filteredGames);

Copilot AI Jan 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "Following" tab loads all games and then filters them client-side. If there are many games in the database, this approach is inefficient and could cause performance issues. Additionally, it doesn't respect pagination properly - the filtered results might have fewer than 10 items even though more followed players' games exist on other pages.

Consider modifying the useGameTokens call to support filtering by multiple owners at the API level, or implement proper pagination for filtered results.

Copilot uses AI. Check for mistakes.
Comment on lines +127 to +133
// Following tab: filter to show only games from followed players
const filteredGames = games.filter((g: any) => {
const normalizedOwner = addAddressPadding(g.owner).toLowerCase();
return followedAddresses.includes(normalizedOwner);
});
setDisplayedGames(filteredGames);
}

Copilot AI Jan 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "Following" tab loads all games and then filters them client-side. If there are many games in the database, this approach is inefficient and could cause performance issues. Additionally, it doesn't respect pagination properly - the filtered results might have fewer than 10 items even though more followed players' games exist on other pages.

Consider modifying the useGameTokens call to support filtering by multiple owners at the API level, or implement proper pagination for filtered results.

Copilot uses AI. Check for mistakes.
const handleTabChange = useCallback((event: React.SyntheticEvent, newValue: number) => {
setActiveTab(newValue);
};
}, []);

Copilot AI Jan 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When switching between tabs, the pagination state (currentPage) is not reset to 0. This can lead to a confusing user experience where switching to the "Following" or "My Games" tab shows an empty page because the user was on page 5 of "All" tab, but "Following" only has 1 page of results.

Consider resetting the page to 0 when activeTab changes, or manage separate pagination state for each tab.

Suggested change
}, []);
// Reset pagination to the first page when switching tabs to avoid empty pages on shorter lists
goToPage(0);
}, [goToPage]);

Copilot uses AI. Check for mistakes.
Comment on lines +338 to +340
playerName={localNameOverrides[game.token_id] ?? game.player_name ?? `Player #${game.token_id}`}
/>
)}

Copilot AI Jan 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a player follows someone, the current displayed name (which could be from localNameOverrides) is stored in the followStore. However, if the player later edits their own name, the followed player's stored name in followStore is not updated. This creates an inconsistency where notifications and the FollowingList might show an outdated name.

Consider storing only the address in followStore and always fetching the current name from the games data, or implement a mechanism to update followed player names when they change.

Suggested change
playerName={localNameOverrides[game.token_id] ?? game.player_name ?? `Player #${game.token_id}`}
/>
)}
/>
)}
)}

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Fix all issues with AI agents
In `@AGENTS.md`:
- Around line 7-12: The fenced code blocks in AGENTS.md lack language
identifiers causing linter MD040 failures; update the repository tree block to
use a plain text language (e.g., add "text" after the opening ```) and ensure
all shell/command blocks (e.g., the commands in the client and contracts
sections shown in the diff) use "bash" after the opening ```; apply the same
change to the other affected fenced blocks referenced (lines 25-33) so every
triple-backtick fence has an appropriate language identifier.

In `@client/.env.local`:
- Around line 1-2: Reorder the environment variable entries so they follow
dotenv-linter's expected alphabetical order: place VITE_PUBLIC_DENSHOKAN_ADDRESS
before VITE_PUBLIC_VRF_PROVIDER_ADDRESS; update the .env.local file by swapping
the two lines referencing VITE_PUBLIC_DENSHOKAN_ADDRESS and
VITE_PUBLIC_VRF_PROVIDER_ADDRESS to eliminate the ordering warning.

In `@client/.env.production`:
- Around line 1-2: Reorder the two environment variables so
VITE_PUBLIC_DENSHOKAN_ADDRESS appears before VITE_PUBLIC_VRF_PROVIDER_ADDRESS to
satisfy dotenv-linter; locate the entries for VITE_PUBLIC_DENSHOKAN_ADDRESS and
VITE_PUBLIC_VRF_PROVIDER_ADDRESS in the .env production content and swap their
order so the DENSHOKAN key is listed first.

In `@client/AGENTS.md`:
- Around line 15-57: Update the markdown code fence that contains the project
directory tree (the block starting with "src/" and the tree lines ending with
"└── abi/") to include a language identifier (e.g., add "text" after the opening
```), so the fence becomes ```text and satisfies markdownlint MD040.

In `@client/src/desktop/components/Leaderboard.tsx`:
- Around line 112-138: Remove the stale-reference guard that prevents
re-filtering when tab or follows change: in the useEffect that currently checks
"if (!loading && games && games !== prevGamesRef.current) { ... }", stop
comparing games to prevGamesRef.current so the effect runs whenever its
dependencies change (loading, games, activeTab, address, followedAddresses);
keep the existing ownership check for the "My Games" branch and the filtering
logic for the "Following" branch (which uses addAddressPadding,
followedAddresses and setDisplayedGames) intact, and remove or stop updating
prevGamesRef.current if it’s no longer needed.

In `@client/src/hooks/useFollowedPlayerNotifications.tsx`:
- Around line 121-145: The effect that starts polling (useEffect) early-returns
when getFollowedAddresses() is empty and thus never restarts if the user follows
someone later; update the dependency array to include followedPlayers (the
reactive list) so the effect will rerun and set up polling when follows change,
and move the lastSeenTimestampRef.current = Date.now() initialization into a
separate useEffect that runs only on mount (so following someone doesn’t reset
the timestamp). Ensure checkForNewGames, getFollowedAddresses, POLL_INTERVAL and
lastSeenTimestampRef usage remain the same inside the polling effect.

In `@client/src/mobile/components/Leaderboard.tsx`:
- Around line 109-135: Remove the stale-reference guard (the check comparing
games !== prevGamesRef.current) from the useEffect so the effect runs when
dependencies like activeTab or followedAddresses change; keep the existing
ownership validation for the "My Games" branch (using prevGamesRef.current and
addAddressPadding/address) if you still want to prevent race conditions there,
but ensure branches for activeTab 0 (All), 1 (My Games using ownership check and
setDisplayedGames), and 2 (Following: filter by followedAddresses and call
setDisplayedGames) execute whenever loading, games, activeTab, address, or
followedAddresses change.

In `@contracts/AGENTS.md`:
- Around line 22-51: The markdown code fence containing the project tree (the
block starting with ``` and the line "src/") needs a language identifier to
satisfy MD040; update the opening fence from ``` to ```text (or another
appropriate language) so it becomes ```text and leave the rest of the block
unchanged.

Comment thread AGENTS.md
Comment on lines +7 to +12
```
death-mountain/
├── client/ # React + TypeScript frontend (Vite)
├── contracts/ # Cairo smart contracts (Dojo 1.6.0)
└── .github/ # CI workflows
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language identifiers to fenced code blocks.

Markdownlint (MD040) requires specifying a language for fenced blocks; otherwise lint will fail. Consider adding text for the tree and bash for commands.

🧹 Suggested fix
-```
+```text
 death-mountain/
 ├── client/          # React + TypeScript frontend (Vite)
 ├── contracts/       # Cairo smart contracts (Dojo 1.6.0)
 └── .github/         # CI workflows

@@
-bash +bash
cd client && pnpm install && pnpm dev # Port 5173

@@
-```bash
+```bash
cd contracts && sozo build && sozo test
</details>


Also applies to: 25-33

<details>
<summary>🤖 Prompt for AI Agents</summary>

In @AGENTS.md around lines 7 - 12, The fenced code blocks in AGENTS.md lack
language identifiers causing linter MD040 failures; update the repository tree
block to use a plain text language (e.g., add "text" after the opening ) and ensure all shell/command blocks (e.g., the commands in the client and contracts sections shown in the diff) use "bash" after the opening ; apply the same
change to the other affected fenced blocks referenced (lines 25-33) so every
triple-backtick fence has an appropriate language identifier.


</details>

<!-- fingerprinting:phantom:triton:eagle -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment thread client/.env.local
Comment on lines 1 to +2
VITE_PUBLIC_VRF_PROVIDER_ADDRESS=0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f
VITE_PUBLIC_DENSHOKAN_ADDRESS=0x036017e69d21d6d8c13e266eabb73ef1f1d02722d86bdcabe5f168f8e549d3cd

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reorder env keys to satisfy dotenv-linter.

This file will also trigger the ordering warning; reordering avoids lint noise.

♻️ Suggested reorder
-VITE_PUBLIC_VRF_PROVIDER_ADDRESS=0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f
 VITE_PUBLIC_DENSHOKAN_ADDRESS=0x036017e69d21d6d8c13e266eabb73ef1f1d02722d86bdcabe5f168f8e549d3cd
+VITE_PUBLIC_VRF_PROVIDER_ADDRESS=0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
VITE_PUBLIC_VRF_PROVIDER_ADDRESS=0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f
VITE_PUBLIC_DENSHOKAN_ADDRESS=0x036017e69d21d6d8c13e266eabb73ef1f1d02722d86bdcabe5f168f8e549d3cd
VITE_PUBLIC_DENSHOKAN_ADDRESS=0x036017e69d21d6d8c13e266eabb73ef1f1d02722d86bdcabe5f168f8e549d3cd
VITE_PUBLIC_VRF_PROVIDER_ADDRESS=0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 2-2: [UnorderedKey] The VITE_PUBLIC_DENSHOKAN_ADDRESS key should go before the VITE_PUBLIC_VRF_PROVIDER_ADDRESS key

(UnorderedKey)

🤖 Prompt for AI Agents
In `@client/.env.local` around lines 1 - 2, Reorder the environment variable
entries so they follow dotenv-linter's expected alphabetical order: place
VITE_PUBLIC_DENSHOKAN_ADDRESS before VITE_PUBLIC_VRF_PROVIDER_ADDRESS; update
the .env.local file by swapping the two lines referencing
VITE_PUBLIC_DENSHOKAN_ADDRESS and VITE_PUBLIC_VRF_PROVIDER_ADDRESS to eliminate
the ordering warning.

Comment thread client/.env.production
Comment on lines 1 to +2
VITE_PUBLIC_VRF_PROVIDER_ADDRESS=0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f
VITE_PUBLIC_DENSHOKAN_ADDRESS=0x036017e69d21d6d8c13e266eabb73ef1f1d02722d86bdcabe5f168f8e549d3cd

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reorder env keys to satisfy dotenv-linter.

The linter warning indicates the new key should be placed before the VRF provider key. Consider reordering to keep lint output clean.

♻️ Suggested reorder
-VITE_PUBLIC_VRF_PROVIDER_ADDRESS=0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f
 VITE_PUBLIC_DENSHOKAN_ADDRESS=0x036017e69d21d6d8c13e266eabb73ef1f1d02722d86bdcabe5f168f8e549d3cd
+VITE_PUBLIC_VRF_PROVIDER_ADDRESS=0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
VITE_PUBLIC_VRF_PROVIDER_ADDRESS=0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f
VITE_PUBLIC_DENSHOKAN_ADDRESS=0x036017e69d21d6d8c13e266eabb73ef1f1d02722d86bdcabe5f168f8e549d3cd
VITE_PUBLIC_DENSHOKAN_ADDRESS=0x036017e69d21d6d8c13e266eabb73ef1f1d02722d86bdcabe5f168f8e549d3cd
VITE_PUBLIC_VRF_PROVIDER_ADDRESS=0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 2-2: [UnorderedKey] The VITE_PUBLIC_DENSHOKAN_ADDRESS key should go before the VITE_PUBLIC_VRF_PROVIDER_ADDRESS key

(UnorderedKey)

🤖 Prompt for AI Agents
In `@client/.env.production` around lines 1 - 2, Reorder the two environment
variables so VITE_PUBLIC_DENSHOKAN_ADDRESS appears before
VITE_PUBLIC_VRF_PROVIDER_ADDRESS to satisfy dotenv-linter; locate the entries
for VITE_PUBLIC_DENSHOKAN_ADDRESS and VITE_PUBLIC_VRF_PROVIDER_ADDRESS in the
.env production content and swap their order so the DENSHOKAN key is listed
first.

Comment thread client/AGENTS.md
Comment on lines +15 to +57
```
src/
├── App.tsx # Root component, routing setup
├── Main.tsx # Entry point with providers
├── stores/ # Zustand state management
│ ├── gameStore.ts # Adventurer, beast, bag, events, market
│ ├── marketStore.ts # Item purchases, pricing state
│ └── uiStore.ts # UI toggles, overlays
├── dojo/ # Blockchain integration hooks
│ ├── useSystemCalls.ts # Contract call wrappers (explore, attack, etc.)
│ ├── useGameTokens.ts # Token ownership, minting
│ ├── useGameSettings.ts# Dungeon configuration
│ ├── useGameEvents.ts # Event subscription
│ └── useDungeon.ts # Dungeon state
├── desktop/ # Desktop-optimized UI
│ ├── pages/ # Full page components
│ ├── overlays/ # Modal overlays (Combat, Explore, Market, Inventory)
│ ├── components/ # Desktop-specific components
│ └── contexts/ # Desktop context providers
├── mobile/ # Mobile-optimized UI
│ ├── pages/ # Mobile page layouts
│ ├── containers/ # Screen containers (BeastScreen, etc.)
│ ├── components/ # Mobile-specific components
│ └── contexts/ # Mobile context providers
├── components/ # Shared UI components
├── contexts/ # Global context providers
│ ├── controller.tsx # Cartridge Controller wallet
│ └── starknet.tsx # Starknet connection
├── types/ # TypeScript interfaces
│ └── game.ts # Adventurer, Beast, Item, Stats types
├── utils/ # Utility functions
│ ├── events.ts # GameEvent processing
│ ├── translation.ts # Contract event → UI event mapping
│ ├── loot.ts # Item utilities (slots, types, boosts)
│ └── game.ts # Game logic helpers
├── constants/ # Static game data
│ ├── beast.ts # Beast names, prefixes, suffixes
│ ├── loot.ts # Item definitions
│ └── obstacles.ts # Obstacle definitions
├── api/ # External API integrations
├── generated/ # Auto-generated Dojo bindings
└── abi/ # Contract ABIs
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language identifier to the project structure code fence.

This block will fail markdownlint MD040 without a language.

🧹 Suggested fix
-```
+```text
 src/
 ├── App.tsx               # Root component, routing setup
 ├── Main.tsx              # Entry point with providers
 ...
 └── abi/                  # Contract ABIs
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

15-15: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In `@client/AGENTS.md` around lines 15 - 57, Update the markdown code fence that
contains the project directory tree (the block starting with "src/" and the tree
lines ending with "└── abi/") to include a language identifier (e.g., add "text"
after the opening ```), so the fence becomes ```text and satisfies markdownlint
MD040.

Comment on lines +112 to +138
// Validate incoming games data to prevent race condition when switching tabs
useEffect(() => {
if (!loading && games && games !== prevGamesRef.current) {
prevGamesRef.current = games;

if (activeTab === 1 && address) {
// My Games tab: only accept if all games are owned by user
const allOwned = games.length === 0 || games.every((g: any) =>
addAddressPadding(g.owner).toLowerCase() === addAddressPadding(address).toLowerCase()
);
if (allOwned) {
setDisplayedGames(games);
}
// If not all owned, this is stale "All" data - ignore it
} else if (activeTab === 0) {
// All tab: accept any data
setDisplayedGames(games);
} else if (activeTab === 2) {
// Following tab: filter to show only games from followed players
const filteredGames = games.filter((g: any) => {
const normalizedOwner = addAddressPadding(g.owner).toLowerCase();
return followedAddresses.includes(normalizedOwner);
});
setDisplayedGames(filteredGames);
}
}
}, [loading, games, activeTab, address, followedAddresses]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

fd -t f "Leaderboard.tsx"

Repository: Provable-Games/death-mountain

Length of output: 163


🏁 Script executed:

wc -l client/src/desktop/components/Leaderboard.tsx

Repository: Provable-Games/death-mountain

Length of output: 122


🏁 Script executed:

cat -n client/src/desktop/components/Leaderboard.tsx | sed -n '100,160p'

Repository: Provable-Games/death-mountain

Length of output: 2691


🏁 Script executed:

rg -n "prevGamesRef" client/src/desktop/components/Leaderboard.tsx

Repository: Provable-Games/death-mountain

Length of output: 224


🏁 Script executed:

cat -n client/src/desktop/components/Leaderboard.tsx | sed -n '1,120p' | grep -A 5 -B 5 "prevGamesRef"

Repository: Provable-Games/death-mountain

Length of output: 1314


🏁 Script executed:

cat -n client/src/desktop/components/Leaderboard.tsx | sed -n '25,60p'

Repository: Provable-Games/death-mountain

Length of output: 1741


🏁 Script executed:

rg -n "setDisplayedGames" client/src/desktop/components/Leaderboard.tsx

Repository: Provable-Games/death-mountain

Length of output: 266


Following tab fails to refresh when switching tabs or when the follow list changes without a new games fetch.

The guard games !== prevGamesRef.current blocks the effect body from executing when activeTab or followedAddresses changes but the games array reference remains the same. This prevents the Following tab from filtering games when switching to it or when following/unfollowing players while the tab is open.

The suggested fix of removing the ref guard is sound. The "My Games" tab's internal ownership check already protects against race conditions, and the "All" and "Following" tabs have no validation that could be harmed by running on every dependency change.

🔧 Suggested fix (remove the ref guard)
-  if (!loading && games && games !== prevGamesRef.current) {
-    prevGamesRef.current = games;
+  if (!loading && games) {
+    prevGamesRef.current = games;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Validate incoming games data to prevent race condition when switching tabs
useEffect(() => {
if (!loading && games && games !== prevGamesRef.current) {
prevGamesRef.current = games;
if (activeTab === 1 && address) {
// My Games tab: only accept if all games are owned by user
const allOwned = games.length === 0 || games.every((g: any) =>
addAddressPadding(g.owner).toLowerCase() === addAddressPadding(address).toLowerCase()
);
if (allOwned) {
setDisplayedGames(games);
}
// If not all owned, this is stale "All" data - ignore it
} else if (activeTab === 0) {
// All tab: accept any data
setDisplayedGames(games);
} else if (activeTab === 2) {
// Following tab: filter to show only games from followed players
const filteredGames = games.filter((g: any) => {
const normalizedOwner = addAddressPadding(g.owner).toLowerCase();
return followedAddresses.includes(normalizedOwner);
});
setDisplayedGames(filteredGames);
}
}
}, [loading, games, activeTab, address, followedAddresses]);
// Validate incoming games data to prevent race condition when switching tabs
useEffect(() => {
if (!loading && games) {
prevGamesRef.current = games;
if (activeTab === 1 && address) {
// My Games tab: only accept if all games are owned by user
const allOwned = games.length === 0 || games.every((g: any) =>
addAddressPadding(g.owner).toLowerCase() === addAddressPadding(address).toLowerCase()
);
if (allOwned) {
setDisplayedGames(games);
}
// If not all owned, this is stale "All" data - ignore it
} else if (activeTab === 0) {
// All tab: accept any data
setDisplayedGames(games);
} else if (activeTab === 2) {
// Following tab: filter to show only games from followed players
const filteredGames = games.filter((g: any) => {
const normalizedOwner = addAddressPadding(g.owner).toLowerCase();
return followedAddresses.includes(normalizedOwner);
});
setDisplayedGames(filteredGames);
}
}
}, [loading, games, activeTab, address, followedAddresses]);
🤖 Prompt for AI Agents
In `@client/src/desktop/components/Leaderboard.tsx` around lines 112 - 138, Remove
the stale-reference guard that prevents re-filtering when tab or follows change:
in the useEffect that currently checks "if (!loading && games && games !==
prevGamesRef.current) { ... }", stop comparing games to prevGamesRef.current so
the effect runs whenever its dependencies change (loading, games, activeTab,
address, followedAddresses); keep the existing ownership check for the "My
Games" branch and the filtering logic for the "Following" branch (which uses
addAddressPadding, followedAddresses and setDisplayedGames) intact, and remove
or stop updating prevGamesRef.current if it’s no longer needed.

Comment on lines +121 to +145
useEffect(() => {
// Reset timestamp when component mounts to avoid stale notifications
lastSeenTimestampRef.current = Date.now();

// Only start polling if user is logged in and following someone
const followedAddresses = getFollowedAddresses();
if (!address || followedAddresses.length === 0) {
return;
}

// Initial check after a short delay
const initialTimeout = setTimeout(() => {
checkForNewGames();
}, 5000);

// Set up polling interval
const pollInterval = setInterval(() => {
checkForNewGames();
}, POLL_INTERVAL);

return () => {
clearTimeout(initialTimeout);
clearInterval(pollInterval);
};
}, [address, checkForNewGames, getFollowedAddresses]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n client/src/hooks/useFollowedPlayerNotifications.tsx | head -150

Repository: Provable-Games/death-mountain

Length of output: 5774


🏁 Script executed:

fd -t f "followStore" client/src --type file

Repository: Provable-Games/death-mountain

Length of output: 105


🏁 Script executed:

cat -n client/src/stores/followStore.ts

Repository: Provable-Games/death-mountain

Length of output: 2497


Polling won't start if the user follows someone after mount.

The effect doesn't depend on followedPlayers, only on the stable getFollowedAddresses function reference. It runs once and returns early when there are no followed players. After a user follows someone, the polling interval is never recreated unless they reload. Add followedPlayers to the dependency array, and consider separating timestamp initialization into its own effect to avoid resetting on every follow change.

✅ Suggested fix
+  useEffect(() => {
+    // Initialize once
+    lastSeenTimestampRef.current = Date.now();
+  }, []);
+
   useEffect(() => {
-    // Reset timestamp when component mounts to avoid stale notifications
-    lastSeenTimestampRef.current = Date.now();
-
     // Only start polling if user is logged in and following someone
     const followedAddresses = getFollowedAddresses();
     if (!address || followedAddresses.length === 0) {
       return;
     }
@@
-  }, [address, checkForNewGames, getFollowedAddresses]);
+  }, [address, checkForNewGames, getFollowedAddresses, followedPlayers]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
// Reset timestamp when component mounts to avoid stale notifications
lastSeenTimestampRef.current = Date.now();
// Only start polling if user is logged in and following someone
const followedAddresses = getFollowedAddresses();
if (!address || followedAddresses.length === 0) {
return;
}
// Initial check after a short delay
const initialTimeout = setTimeout(() => {
checkForNewGames();
}, 5000);
// Set up polling interval
const pollInterval = setInterval(() => {
checkForNewGames();
}, POLL_INTERVAL);
return () => {
clearTimeout(initialTimeout);
clearInterval(pollInterval);
};
}, [address, checkForNewGames, getFollowedAddresses]);
useEffect(() => {
// Initialize once
lastSeenTimestampRef.current = Date.now();
}, []);
useEffect(() => {
// Only start polling if user is logged in and following someone
const followedAddresses = getFollowedAddresses();
if (!address || followedAddresses.length === 0) {
return;
}
// Initial check after a short delay
const initialTimeout = setTimeout(() => {
checkForNewGames();
}, 5000);
// Set up polling interval
const pollInterval = setInterval(() => {
checkForNewGames();
}, POLL_INTERVAL);
return () => {
clearTimeout(initialTimeout);
clearInterval(pollInterval);
};
}, [address, checkForNewGames, getFollowedAddresses, followedPlayers]);
🤖 Prompt for AI Agents
In `@client/src/hooks/useFollowedPlayerNotifications.tsx` around lines 121 - 145,
The effect that starts polling (useEffect) early-returns when
getFollowedAddresses() is empty and thus never restarts if the user follows
someone later; update the dependency array to include followedPlayers (the
reactive list) so the effect will rerun and set up polling when follows change,
and move the lastSeenTimestampRef.current = Date.now() initialization into a
separate useEffect that runs only on mount (so following someone doesn’t reset
the timestamp). Ensure checkForNewGames, getFollowedAddresses, POLL_INTERVAL and
lastSeenTimestampRef usage remain the same inside the polling effect.

Comment on lines +109 to +135
// Validate incoming games data to prevent race condition when switching tabs
useEffect(() => {
if (!loading && games && games !== prevGamesRef.current) {
prevGamesRef.current = games;

if (activeTab === 1 && address) {
// My Games tab: only accept if all games are owned by user
const allOwned = games.length === 0 || games.every((g: any) =>
addAddressPadding(g.owner).toLowerCase() === addAddressPadding(address).toLowerCase()
);
if (allOwned) {
setDisplayedGames(games);
}
// If not all owned, this is stale "All" data - ignore it
} else if (activeTab === 0) {
// All tab: accept any data
setDisplayedGames(games);
} else if (activeTab === 2) {
// Following tab: filter to show only games from followed players
const filteredGames = games.filter((g: any) => {
const normalizedOwner = addAddressPadding(g.owner).toLowerCase();
return followedAddresses.includes(normalizedOwner);
});
setDisplayedGames(filteredGames);
}
}
}, [loading, games, activeTab, address, followedAddresses]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and read the file
fd -t f "Leaderboard.tsx" client/src/

Repository: Provable-Games/death-mountain

Length of output: 163


🏁 Script executed:

# Check the file size first
wc -l client/src/mobile/components/Leaderboard.tsx

Repository: Provable-Games/death-mountain

Length of output: 121


🏁 Script executed:

# Read the specific lines mentioned in the review (109-135)
sed -n '100,150p' client/src/mobile/components/Leaderboard.tsx

Repository: Provable-Games/death-mountain

Length of output: 1894


🏁 Script executed:

# Get context around the useEffect to understand the overall logic
sed -n '80,160p' client/src/mobile/components/Leaderboard.tsx

Repository: Provable-Games/death-mountain

Length of output: 2762


🏁 Script executed:

# Check the desktop Leaderboard component for comparison
sed -n '100,150p' client/src/desktop/components/Leaderboard.tsx

Repository: Provable-Games/death-mountain

Length of output: 1938


🏁 Script executed:

# Get a broader context from desktop version to see if it has the same issue
sed -n '80,160p' client/src/desktop/components/Leaderboard.tsx

Repository: Provable-Games/death-mountain

Length of output: 2763


Following tab may not refresh when the follow list or tab selection changes.

The games !== prevGamesRef.current guard prevents the effect from executing when activeTab or followedAddresses changes without a new games reference, leaving the Following tab displaying stale data until the next games fetch. The ownership validation in the "My Games" tab already protects against race conditions, making the guard unnecessary.

Remove the guard:

Suggested fix
-  if (!loading && games && games !== prevGamesRef.current) {
-    prevGamesRef.current = games;
+  if (!loading && games) {
+    prevGamesRef.current = games;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Validate incoming games data to prevent race condition when switching tabs
useEffect(() => {
if (!loading && games && games !== prevGamesRef.current) {
prevGamesRef.current = games;
if (activeTab === 1 && address) {
// My Games tab: only accept if all games are owned by user
const allOwned = games.length === 0 || games.every((g: any) =>
addAddressPadding(g.owner).toLowerCase() === addAddressPadding(address).toLowerCase()
);
if (allOwned) {
setDisplayedGames(games);
}
// If not all owned, this is stale "All" data - ignore it
} else if (activeTab === 0) {
// All tab: accept any data
setDisplayedGames(games);
} else if (activeTab === 2) {
// Following tab: filter to show only games from followed players
const filteredGames = games.filter((g: any) => {
const normalizedOwner = addAddressPadding(g.owner).toLowerCase();
return followedAddresses.includes(normalizedOwner);
});
setDisplayedGames(filteredGames);
}
}
}, [loading, games, activeTab, address, followedAddresses]);
// Validate incoming games data to prevent race condition when switching tabs
useEffect(() => {
if (!loading && games) {
prevGamesRef.current = games;
if (activeTab === 1 && address) {
// My Games tab: only accept if all games are owned by user
const allOwned = games.length === 0 || games.every((g: any) =>
addAddressPadding(g.owner).toLowerCase() === addAddressPadding(address).toLowerCase()
);
if (allOwned) {
setDisplayedGames(games);
}
// If not all owned, this is stale "All" data - ignore it
} else if (activeTab === 0) {
// All tab: accept any data
setDisplayedGames(games);
} else if (activeTab === 2) {
// Following tab: filter to show only games from followed players
const filteredGames = games.filter((g: any) => {
const normalizedOwner = addAddressPadding(g.owner).toLowerCase();
return followedAddresses.includes(normalizedOwner);
});
setDisplayedGames(filteredGames);
}
}
}, [loading, games, activeTab, address, followedAddresses]);
🤖 Prompt for AI Agents
In `@client/src/mobile/components/Leaderboard.tsx` around lines 109 - 135, Remove
the stale-reference guard (the check comparing games !== prevGamesRef.current)
from the useEffect so the effect runs when dependencies like activeTab or
followedAddresses change; keep the existing ownership validation for the "My
Games" branch (using prevGamesRef.current and addAddressPadding/address) if you
still want to prevent race conditions there, but ensure branches for activeTab 0
(All), 1 (My Games using ownership check and setDisplayedGames), and 2
(Following: filter by followedAddresses and call setDisplayedGames) execute
whenever loading, games, activeTab, address, or followedAddresses change.

Comment thread contracts/AGENTS.md
Comment on lines +22 to +51
```
src/
├── lib.cairo # Module declarations
├── systems/ # Dojo contract implementations
│ ├── game/ # Core game loop (explore, attack, flee, buy, equip)
│ ├── adventurer/ # Character management, stat calculations
│ ├── beast/ # Enemy generation, combat logic
│ ├── loot/ # Item type/tier resolution
│ ├── settings/ # Game configuration
│ ├── renderer/ # On-chain SVG NFT metadata
│ ├── game_token/ # Token contract integration
│ └── objectives/ # Dungeon objectives
├── models/ # Dojo models (on-chain state)
│ ├── adventurer/ # Adventurer, Bag, Equipment, Item, Stats
│ ├── game.cairo # GameSettings, GameState, GameEvent
│ ├── beast.cairo # Beast struct and combat specs
│ ├── combat.cairo # CombatSpec, damage calculations
│ ├── loot.cairo # Item definitions (101 base items)
│ ├── market.cairo # Market state, ItemPurchase
│ └── obstacle.cairo # Obstacle types (75 unique)
├── constants/ # Game constants and enums
├── libs/ # Shared utilities
│ ├── game.cairo # GameLibs dispatcher factory
│ └── settings.cairo # Settings helpers
└── utils/ # Utility functions
├── loot.cairo # Item type/tier lookup
├── vrf.cairo # VRF randomness integration
├── renderer/ # SVG generation utilities
└── string/ # String manipulation
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language identifier to the project structure code fence.

This block will trigger markdownlint MD040 unless a language is specified.

🧹 Suggested fix
-```
+```text
 src/
 ├── lib.cairo              # Module declarations
 ├── systems/               # Dojo contract implementations
 ...
 └── utils/                # Utility functions
     ├── loot.cairo        # Item type/tier lookup
     ├── vrf.cairo         # VRF randomness integration
     ├── renderer/         # SVG generation utilities
     └── string/           # String manipulation
</details>

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.18.1)</summary>

22-22: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

In @contracts/AGENTS.md around lines 22 - 51, The markdown code fence containing
the project tree (the block starting with and the line "src/") needs a language identifier to satisfy MD040; update the opening fence from to
text (or another appropriate language) so it becomes text and leave the
rest of the block unchanged.


</details>

<!-- fingerprinting:phantom:triton:eagle -->

<!-- This is an auto-generated comment by CodeRabbit -->

- Add PlayerSearch component with name search functionality
- Search queries Torii SQL for players matching the input
- Shows search results with follow button for each player
- Displays "Already following" indicator for followed players
- Component appears in "Following" tab on both desktop and mobile

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@claude

claude Bot commented Jan 18, 2026

Copy link
Copy Markdown

Claude finished @loothero's task —— View job


Code Review: Follow Player Feature

Comprehensive review of the follow player feature implementation with in-app notifications.

Todo List

  • Read repository guidelines and conventions
  • Review new component implementations
  • Analyze store and hook implementations
  • Check integration with existing leaderboard components
  • Evaluate performance and security considerations
  • Provide comprehensive feedback and recommendations

Review Summary

This PR implements a well-architected follow player feature with in-app notifications. The code follows established patterns and integrates cleanly with the existing codebase. However, there are several critical security and performance issues that need attention.

Critical Issues ⚠️

1. SQL Injection Vulnerability

File: client/src/hooks/useFollowedPlayerNotifications.tsx:52-57

const url = `${currentNetworkConfig.toriiUrl}/sql?query=
  SELECT token_id, owner, player_name, minted_at
  FROM "relayer_0_0_1-TokenMetadataUpdate"
  WHERE owner IN (${ownersList})
  ORDER BY minted_at DESC
  LIMIT 10`;

Issues:

  • Direct string interpolation creates SQL injection risk
  • Raw SQL query in URL parameters is insecure
  • Hardcoded table name "relayer_0_0_1-TokenMetadataUpdate" should use namespace config

Solution: Use POST request with parameterized queries or SQL escaping.

2. Notification Logic Bug

File: client/src/hooks/useFollowedPlayerNotifications.tsx:74-76

const newGames = data.filter((game) => {
  const mintedAt = new Date(game.minted_at).getTime();
  return mintedAt > lastSeenTimestampRef.current;
});

Issue: Games with identical timestamps as lastSeenTimestamp will be missed.

Solution: Use >= and track notified game IDs to prevent duplicates.

Performance & Scalability Issues 🚨

1. Inefficient Client-Side Filtering

Files:

  • client/src/desktop/components/Leaderboard.tsx:131-135
  • client/src/mobile/components/Leaderboard.tsx:133-137

The "Following" tab fetches all games then filters client-side. This doesn't scale with large datasets.

Solution: Implement server-side filtering similar to the notifications hook's SQL approach.

2. Unlimited localStorage Growth

File: client/src/stores/followStore.ts

No cleanup mechanism for old follows or upper limits on followed players.

Recommendations:

  • Add max follow limit (e.g., 100 players)
  • Implement periodic cleanup of inactive follows
  • Add data migration for schema changes

Security Concerns 🔒

1. Table Name Hardcoding

The notifications query hardcodes "relayer_0_0_1-TokenMetadataUpdate" but should derive the namespace from config:

const tableName = `"${currentNetworkConfig.namespace}-TokenMetadataUpdate"`;

2. Address Validation

While address normalization is good, consider additional validation for malformed addresses before database queries.

Code Quality Issues 📝

1. Type Safety

Files: Leaderboard components

game objects are typed as any. Define proper interfaces:

interface GameToken {
  token_id: number;
  owner: string;
  player_name: string;
  score: number;
  game_over: boolean;
  minted_at: string;
}

2. Error Handling

Limited error handling in notification polling. Consider:

  • Exponential backoff for failed requests
  • User notification of connection issues
  • Graceful degradation when offline

Minor Improvements 🔧

1. Performance Optimizations

  • Debounce follow/unfollow actions to prevent rapid API calls
  • Implement lazy loading for large follow lists
  • Cache player names to reduce redundant fetches

2. UX Enhancements

  • Add loading states for follow/unfollow actions
  • Show notification count badge
  • Allow bulk unfollow operations

Test Coverage 🧪

Missing: The PR lacks test coverage for:

  • Follow store state management
  • Notification polling logic
  • Error scenarios
  • Address normalization

Note: Cannot run existing test suite without proper permissions. Recommend adding tests for the new functionality.

Recommendations 📋

High Priority (Security/Performance):

  1. Fix SQL injection vulnerability with parameterized queries
  2. Fix notification timestamp comparison logic
  3. Implement server-side filtering for "Following" tab
  4. Add proper TypeScript interfaces

Medium Priority (Robustness):
5. Add error boundaries and offline handling
6. Implement follow limits and cleanup
7. Add comprehensive test coverage

Low Priority (UX):
8. Add loading states and better error messaging
9. Optimize performance with debouncing/caching

Overall Assessment ✅

Despite the critical issues identified, this is a well-structured implementation that follows the codebase patterns. The architecture is sound and the user experience is intuitive. With the security and performance fixes implemented, this would be a solid addition to the game's social features.

Status: ⚠️ Needs Work - Address critical security issues before merging.


@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@client/src/components/PlayerSearch.tsx`:
- Around line 45-58: The Torii SQL query in the URL construction for
PlayerSearch (uses currentNetworkConfig.toriiUrl, searchQuery, and the fetch
call) interpolates raw user input and can be broken by quotes or injection; fix
by sanitizing searchQuery before building the query: escape single quotes in
searchQuery (e.g., replace ' with ''), then construct the SQL string and
URL-encode the whole query parameter with encodeURIComponent before appending to
`${currentNetworkConfig.toriiUrl}/sql?query=`; apply the same
escaping+encodeURIComponent pattern to all other Torii SQL query usages (e.g.,
in useFollowedPlayerNotifications.tsx, useGameSettings.ts, useGameEvents.ts).
♻️ Duplicate comments (2)
client/src/desktop/components/Leaderboard.tsx (1)

113-139: Following tab can stay stale because of the prevGamesRef guard.
The guard blocks re-filtering when the tab or follow list changes without a new games reference.

🔧 Suggested fix (remove stale reference guard)
-    if (!loading && games && games !== prevGamesRef.current) {
-      prevGamesRef.current = games;
+    if (!loading && games) {
+      prevGamesRef.current = games;
client/src/mobile/components/Leaderboard.tsx (1)

110-136: Following tab can stay stale because of the prevGamesRef guard.
Same issue as desktop: tab/follow changes won’t re-filter without a new games reference.

🔧 Suggested fix (remove stale reference guard)
-    if (!loading && games && games !== prevGamesRef.current) {
-      prevGamesRef.current = games;
+    if (!loading && games) {
+      prevGamesRef.current = games;

Comment thread client/src/components/PlayerSearch.tsx Outdated
- Use TokenPlayerNameUpdate table joined with OwnersUpdate for player data
- Decode hex-encoded player names using hexToAscii
- Filter results client-side after decoding for accurate name matching

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@claude

claude Bot commented Jan 18, 2026

Copy link
Copy Markdown

Claude finished @loothero's task —— View job


Code Review: Follow Player Feature

Comprehensive review of the follow player feature implementation with in-app notifications.

✅ Strengths

Architecture & Patterns:

  • Clean separation of concerns with dedicated store (followStore.ts), components (FollowButton, FollowingList), and notification hook
  • Follows established codebase patterns using Zustand, MUI, and TypeScript
  • Proper integration with existing leaderboard components for both desktop and mobile

StarkNet Integration:

  • Correct address normalization using addAddressPadding() and lowercase conversion
  • Consistent handling throughout the codebase
  • Proper self-detection to hide follow buttons on own games

User Experience:

  • Intuitive heart icon for follow/unfollow actions with hover states
  • "Following" tab in leaderboard for easy access to followed players' games
  • Toast notifications with "Watch" action buttons
  • localStorage persistence for follow relationships

🔴 Critical Issues

1. SQL Injection Vulnerability (useFollowedPlayerNotifications.tsx:47-55)

const ownersList = followedAddresses
  .map((addr) => `"${addAddressPadding(addr)}"`)
  .join(',');

const url = `${currentNetworkConfig.toriiUrl}/sql?query=
  SELECT token_id, owner, player_name, minted_at
  FROM "relayer_0_0_1-TokenMetadataUpdate"
  WHERE owner IN (${ownersList})`; // Direct string interpolation!

Impact: Malicious addresses could inject SQL commands
Fix: Use parameterized queries or POST body with proper escaping

2. Performance Concerns

  • Continuous Polling: 30-second intervals run indefinitely, even when user is inactive
  • No Request Deduplication: Multiple simultaneous requests possible due to missing isPollingRef.current reset on error
  • Unbounded Storage: No limit on followed players or cleanup mechanism

🟡 Security & Robustness Issues

3. Error Handling Gaps

  • Silent failures in SQL queries could miss notifications
  • No offline detection or retry logic for network failures
  • Race conditions possible between polling timer and component unmount

4. Memory Management

  • Follow relationships persist indefinitely with no cleanup
  • No upper bounds on localStorage usage
  • Notification polling continues even when notifications disabled

🟠 Minor Issues

5. Code Quality

  • Some TypeScript any types in leaderboard integration could be more specific
  • Magic numbers (30s poll interval) should be configurable constants
  • Missing validation for malformed API responses

Recommendations

High Priority

  1. Fix SQL injection: Use Torii's parameterized query API or validate/sanitize addresses
  2. Add polling controls: Stop polling when user inactive, limit concurrent requests
  3. Implement bounds: Max followed players limit, localStorage quota management

Medium Priority

  1. Improve error handling: Add retry logic, offline detection, proper error boundaries
  2. Performance optimization: Request deduplication, smarter polling intervals
  3. Type safety: Replace any types with proper interfaces

Low Priority

  1. UX enhancements: Loading states, follow limits UI, notification settings
  2. Testing: Unit tests for store logic and notification edge cases

Overall Assessment

This is a well-architected feature that follows the codebase patterns effectively. The core functionality is solid, but the SQL injection vulnerability is critical and must be addressed before merging. With security fixes and performance optimizations, this will be a valuable addition to the game's social features.

Recommendation: Request changes to address security issues before approval.


- Use the same useGameTokens hook that leaderboard uses
- Search within fetched games data for matching player names
- Removes dependency on specific SQL table structure
- Guarantees same data source as leaderboard display

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@claude

claude Bot commented Jan 18, 2026

Copy link
Copy Markdown

Claude finished @loothero's task —— View job


Code Review: Follow Player Feature

Comprehensive review of the follow player feature implementation with in-app notifications.

Todo List

  • Read repository guidelines and conventions
  • Review new component implementations
  • Analyze store and hook implementations
  • Check integration with existing leaderboard components
  • Evaluate performance and security considerations
  • Provide comprehensive feedback and recommendations

Review Summary

This PR implements a well-architected follow player feature with in-app notifications. The code follows established patterns and integrates cleanly with the existing codebase. However, there are several critical security and performance issues that need attention.

Critical Issues ⚠️

1. SQL Injection Vulnerability

File: client/src/hooks/useFollowedPlayerNotifications.tsx:52-57

const url = `${currentNetworkConfig.toriiUrl}/sql?query=
  SELECT token_id, owner, player_name, minted_at
  FROM "relayer_0_0_1-TokenMetadataUpdate"
  WHERE owner IN (${ownersList})
  ORDER BY minted_at DESC
  LIMIT 10`;

Issues:

  • Direct string interpolation creates SQL injection risk
  • Raw SQL query in URL parameters is insecure
  • Hardcoded table name "relayer_0_0_1-TokenMetadataUpdate" should use namespace config

Solution: Use POST request with parameterized queries or SQL escaping.

2. Notification Logic Bug

File: client/src/hooks/useFollowedPlayerNotifications.tsx:74-76

const newGames = data.filter((game) => {
  const mintedAt = new Date(game.minted_at).getTime();
  return mintedAt > lastSeenTimestampRef.current;
});

Issue: Games with identical timestamps as lastSeenTimestamp will be missed.

Solution: Use >= and track notified game IDs to prevent duplicates.

Performance & Scalability Issues 🚨

1. Inefficient Client-Side Filtering

Files:

  • client/src/desktop/components/Leaderboard.tsx:131-135
  • client/src/mobile/components/Leaderboard.tsx:133-137

The "Following" tab fetches all games then filters client-side. This doesn't scale with large datasets.

Solution: Implement server-side filtering similar to the notifications hook's SQL approach.

2. Unlimited localStorage Growth

File: client/src/stores/followStore.ts

No cleanup mechanism for old follows or upper limits on followed players.

Recommendations:

  • Add max follow limit (e.g., 100 players)
  • Implement periodic cleanup of inactive follows
  • Add data migration for schema changes

Security Concerns 🔒

1. Table Name Hardcoding

The notifications query hardcodes "relayer_0_0_1-TokenMetadataUpdate" but should derive the namespace from config:

const tableName = `"${currentNetworkConfig.namespace}-TokenMetadataUpdate"`;

2. Address Validation

While address normalization is good, consider additional validation for malformed addresses before database queries.

Code Quality Issues 📝

1. Type Safety

Files: Leaderboard components

game objects are typed as any. Define proper interfaces:

interface GameToken {
  token_id: number;
  owner: string;
  player_name: string;
  score: number;
  game_over: boolean;
  minted_at: string;
}

2. Error Handling

Limited error handling in notification polling. Consider:

  • Exponential backoff for failed requests
  • User notification of connection issues
  • Graceful degradation when offline

Minor Improvements 🔧

1. Performance Optimizations

  • Debounce follow/unfollow actions to prevent rapid API calls
  • Implement lazy loading for large follow lists
  • Cache player names to reduce redundant fetches

2. UX Enhancements

  • Add loading states for follow/unfollow actions
  • Show notification count badge
  • Allow bulk unfollow operations

Test Coverage 🧪

Missing: The PR lacks test coverage for:

  • Follow store state management
  • Notification polling logic
  • Error scenarios
  • Address normalization

Note: Cannot run existing test suite without proper permissions. Recommend adding tests for the new functionality.

Recommendations 📋

High Priority (Security/Performance):

  1. Fix SQL injection vulnerability with parameterized queries
  2. Fix notification timestamp comparison logic
  3. Implement server-side filtering for "Following" tab
  4. Add proper TypeScript interfaces

Medium Priority (Robustness):
5. Add error boundaries and offline handling
6. Implement follow limits and cleanup
7. Add comprehensive test coverage

Low Priority (UX):
8. Add loading states and better error messaging
9. Optimize performance with debouncing/caching

Overall Assessment ✅

Despite the critical issues identified, this is a well-structured implementation that follows the codebase patterns. The architecture is sound and the user experience is intuitive. With the security and performance fixes implemented, this would be a solid addition to the game's social features.

Status: ⚠️ Needs Work - Address critical security issues before merging.


View job

Fetches unique player names directly from database with proper
joins between TokenPlayerNameUpdate, OwnersUpdate, TokenMetadataUpdate,
and MinterRegistryUpdate tables. Decodes hex-encoded player names
and filters client-side for search matches.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@claude

claude Bot commented Jan 18, 2026

Copy link
Copy Markdown

Claude finished @loothero's task —— View job


Code Review: Follow Player Feature

Comprehensive review of the follow player feature implementation with in-app notifications.

Todo List

  • Read repository guidelines and conventions
  • Review new component implementations (FollowButton, FollowingList, PlayerSearch)
  • Analyze store and hook implementations (followStore, useFollowedPlayerNotifications)
  • Check integration with existing leaderboard components
  • Evaluate performance and security considerations
  • Assess test coverage gaps
  • Provide comprehensive feedback and recommendations

Review Summary

This PR implements a well-architected follow player feature with in-app notifications. The code follows established patterns and integrates cleanly with the existing codebase. However, there are several critical security and performance issues that need attention.

Critical Issues ⚠️

1. SQL Injection Vulnerability

File: client/src/hooks/useFollowedPlayerNotifications.tsx:52-57

const url = `${currentNetworkConfig.toriiUrl}/sql?query=
  SELECT token_id, owner, player_name, minted_at
  FROM "relayer_0_0_1-TokenMetadataUpdate"
  WHERE owner IN (${ownersList})
  ORDER BY minted_at DESC
  LIMIT 10`;

Issues:

  • Direct string interpolation creates SQL injection risk
  • Raw SQL query in URL parameters is insecure
  • Hardcoded table name "relayer_0_0_1-TokenMetadataUpdate" should use namespace config

Solution: Use POST request with parameterized queries or SQL escaping.

2. Notification Logic Bug

File: client/src/hooks/useFollowedPlayerNotifications.tsx:74-76

const newGames = data.filter((game) => {
  const mintedAt = new Date(game.minted_at).getTime();
  return mintedAt > lastSeenTimestampRef.current;
});

Issue: Games with identical timestamps as lastSeenTimestamp will be missed.

Solution: Use >= and track notified game IDs to prevent duplicates.

Performance & Scalability Issues 🚨

1. Inefficient Client-Side Filtering

Files:

  • client/src/desktop/components/Leaderboard.tsx:131-135
  • client/src/mobile/components/Leaderboard.tsx:133-137

The "Following" tab fetches all games then filters client-side. This doesn't scale with large datasets.

Solution: Implement server-side filtering similar to the notifications hook's SQL approach.

2. Unlimited localStorage Growth

File: client/src/stores/followStore.ts

No cleanup mechanism for old follows or upper limits on followed players.

Recommendations:

  • Add max follow limit (e.g., 100 players)
  • Implement periodic cleanup of inactive follows
  • Add data migration for schema changes

Security Concerns 🔒

1. Table Name Hardcoding

The notifications query hardcodes "relayer_0_0_1-TokenMetadataUpdate" but should derive the namespace from config:

const tableName = `"${currentNetworkConfig.namespace}-TokenMetadataUpdate"`;

2. Address Validation

While address normalization is good, consider additional validation for malformed addresses before database queries.

Code Quality Issues 📝

1. Type Safety

Files: Leaderboard components

game objects are typed as any. Define proper interfaces:

interface GameToken {
  token_id: number;
  owner: string;
  player_name: string;
  score: number;
  game_over: boolean;
  minted_at: string;
}

2. Error Handling

Limited error handling in notification polling. Consider:

  • Exponential backoff for failed requests
  • User notification of connection issues
  • Graceful degradation when offline

Minor Improvements 🔧

1. Performance Optimizations

  • Debounce follow/unfollow actions to prevent rapid API calls
  • Implement lazy loading for large follow lists
  • Cache player names to reduce redundant fetches

2. UX Enhancements

  • Add loading states for follow/unfollow actions
  • Show notification count badge
  • Allow bulk unfollow operations

Test Coverage 🧪

Missing: The PR lacks test coverage for:

  • Follow store state management
  • Notification polling logic
  • Error scenarios
  • Address normalization

Note: No test infrastructure found in the client package.json. Recommend adding tests for the new functionality.

Recommendations 📋

High Priority (Security/Performance):

  1. Fix SQL injection vulnerability with parameterized queries
  2. Fix notification timestamp comparison logic
  3. Implement server-side filtering for "Following" tab
  4. Add proper TypeScript interfaces

Medium Priority (Robustness):
5. Add error boundaries and offline handling
6. Implement follow limits and cleanup
7. Add comprehensive test coverage

Low Priority (UX):
8. Add loading states and better error messaging
9. Optimize performance with debouncing/caching

Overall Assessment ✅

Despite the critical issues identified, this is a well-structured implementation that follows the codebase patterns. The architecture is sound and the user experience is intuitive. With the security and performance fixes implemented, this would be a solid addition to the game's social features.

Status: ⚠️ Needs Work - Address critical security issues before merging.


View job

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@client/src/hooks/usePlayerSearch.ts`:
- Around line 23-32: The code uses GAME_TOKEN_ADDRESS (from getContractByName)
to set mintedByAddress which can be undefined when chainId ===
ChainId.WP_PG_SLOT; update the searchPlayers function to guard against an
undefined mintedByAddress before building/executing the SQL query: check the
computed mintedByAddress (the branch using ChainId.WP_PG_SLOT and
GAME_TOKEN_ADDRESS or the addAddressPadding(dungeon.address) branch) and if it's
falsy either return an empty result or throw a clear error, so you never
interpolate "undefined" into the SQL; reference GAME_TOKEN_ADDRESS,
mintedByAddress, searchPlayers, ChainId.WP_PG_SLOT, and
addAddressPadding(dungeon.address) when locating where to add the guard.

Comment on lines +23 to +32
const GAME_TOKEN_ADDRESS = getContractByName(
currentNetworkConfig.manifest,
currentNetworkConfig.namespace,
"game_token_systems"
)?.address;

const mintedByAddress = currentNetworkConfig.chainId === ChainId.WP_PG_SLOT
? GAME_TOKEN_ADDRESS
: addAddressPadding(dungeon.address);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -t f "usePlayerSearch.ts" --exec cat -n {} \;

Repository: Provable-Games/death-mountain

Length of output: 4992


🏁 Script executed:

# Search for useDungeon hook definition
fd -t f "useDungeon*" --exec head -50 {} \;

Repository: Provable-Games/death-mountain

Length of output: 1506


🏁 Script executed:

# Search for useDungeon hook implementation
rg "export.*useDungeon|function useDungeon" -A 20 --type ts --type tsx

Repository: Provable-Games/death-mountain

Length of output: 100


🏁 Script executed:

# Search for useDungeon hook implementation
rg "export.*useDungeon|function useDungeon" -A 20 --type ts

Repository: Provable-Games/death-mountain

Length of output: 428


🏁 Script executed:

# Check getContractByName return type
rg "getContractByName" -B 2 -A 5 client/src --type ts

Repository: Provable-Games/death-mountain

Length of output: 13529


🏁 Script executed:

# Check if addAddressPadding can handle undefined/null
web_search="addAddressPadding starknet undefined null behavior"
python3 << 'EOF'
import urllib.parse
query = "addAddressPadding starknet undefined null behavior"
print(f"Query: {query}")
# This would need web_search but let's also check starknet documentation locally
EOF

# Also check if there are type definitions for addAddressPadding
rg "addAddressPadding" -B 3 -A 3 client/src --type ts | head -40

Repository: Provable-Games/death-mountain

Length of output: 159


🏁 Script executed:

# Check addAddressPadding usage patterns in the codebase
rg "addAddressPadding" client/src --type ts -B 2 -A 2

Repository: Provable-Games/death-mountain

Length of output: 16992


🏁 Script executed:

# Check if there are any guard checks in the searchPlayers function or elsewhere
rg "mintedByAddress" client/src/hooks/usePlayerSearch.ts -B 5 -A 5

Repository: Provable-Games/death-mountain

Length of output: 1228


Guard against missing GAME_TOKEN_ADDRESS before querying.
Line 23–31: When chainId === ChainId.WP_PG_SLOT, GAME_TOKEN_ADDRESS can be undefined due to optional chaining. This makes mintedByAddress undefined, which gets interpolated directly into the SQL query at line 54, producing invalid queries like WHERE mr.minter_address = "undefined". Add a guard check in searchPlayers to validate mintedByAddress before querying.

Suggested fix
const searchPlayers = useCallback(async (searchQuery: string): Promise<PlayerSearchResult[]> => {
  if (!searchQuery.trim()) {
    setResults([]);
    return [];
  }

  setLoading(true);
  setError(null);

  try {
+   if (!mintedByAddress) {
+     setError('Game token address not available');
+     setResults([]);
+     return [];
+   }
    // Query unique player names with their owners
🤖 Prompt for AI Agents
In `@client/src/hooks/usePlayerSearch.ts` around lines 23 - 32, The code uses
GAME_TOKEN_ADDRESS (from getContractByName) to set mintedByAddress which can be
undefined when chainId === ChainId.WP_PG_SLOT; update the searchPlayers function
to guard against an undefined mintedByAddress before building/executing the SQL
query: check the computed mintedByAddress (the branch using ChainId.WP_PG_SLOT
and GAME_TOKEN_ADDRESS or the addAddressPadding(dungeon.address) branch) and if
it's falsy either return an empty result or throw a clear error, so you never
interpolate "undefined" into the SQL; reference GAME_TOKEN_ADDRESS,
mintedByAddress, searchPlayers, ChainId.WP_PG_SLOT, and
addAddressPadding(dungeon.address) when locating where to add the guard.

Logs query construction, API response, hex decoding,
and filtering steps to help diagnose search issues.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@claude

claude Bot commented Jan 18, 2026

Copy link
Copy Markdown

Claude finished @loothero's task —— View job


Code Review: Follow Player Feature

Comprehensive review of the follow player feature implementation with in-app notifications.

Todo List

  • Read repository guidelines and conventions
  • Review new component implementations
  • Analyze store and hook implementations
  • Check integration with existing leaderboard components
  • Evaluate performance and security considerations
  • Provide comprehensive feedback and recommendations

Review Summary

This PR implements a well-architected follow player feature with in-app notifications. The code follows established patterns and integrates cleanly with the existing codebase. However, there are several critical security and performance issues that need attention.

Critical Issues ⚠️

1. SQL Injection Vulnerability

File: client/src/hooks/useFollowedPlayerNotifications.tsx:52-57

const url = `${currentNetworkConfig.toriiUrl}/sql?query=
  SELECT token_id, owner, player_name, minted_at
  FROM "relayer_0_0_1-TokenMetadataUpdate"
  WHERE owner IN (${ownersList})
  ORDER BY minted_at DESC
  LIMIT 10`;

Issues:

  • Direct string interpolation creates SQL injection risk
  • Raw SQL query in URL parameters is insecure
  • Hardcoded table name "relayer_0_0_1-TokenMetadataUpdate" should use namespace config

Solution: Use POST request with parameterized queries or SQL escaping.

2. Notification Logic Bug

File: client/src/hooks/useFollowedPlayerNotifications.tsx:74-76

const newGames = data.filter((game) => {
  const mintedAt = new Date(game.minted_at).getTime();
  return mintedAt > lastSeenTimestampRef.current;
});

Issue: Games with identical timestamps as lastSeenTimestamp will be missed.

Solution: Use >= and track notified game IDs to prevent duplicates.

Performance & Scalability Issues 🚨

1. Inefficient Client-Side Filtering

Files:

  • client/src/desktop/components/Leaderboard.tsx:131-135
  • client/src/mobile/components/Leaderboard.tsx:133-137

The "Following" tab fetches all games then filters client-side. This doesn't scale with large datasets.

Solution: Implement server-side filtering similar to the notifications hook's SQL approach.

2. Unlimited localStorage Growth

File: client/src/stores/followStore.ts

No cleanup mechanism for old follows or upper limits on followed players.

Recommendations:

  • Add max follow limit (e.g., 100 players)
  • Implement periodic cleanup of inactive follows
  • Add data migration for schema changes

Security Concerns 🔒

1. Table Name Hardcoding

The notifications query hardcodes "relayer_0_0_1-TokenMetadataUpdate" but should derive the namespace from config:

const tableName = `"${currentNetworkConfig.namespace}-TokenMetadataUpdate"`;

2. Address Validation

While address normalization is good, consider additional validation for malformed addresses before database queries.

Code Quality Issues 📝

1. Type Safety

Files: Leaderboard components

game objects are typed as any. Define proper interfaces:

interface GameToken {
  token_id: number;
  owner: string;
  player_name: string;
  score: number;
  game_over: boolean;
  minted_at: string;
}

2. Error Handling

Limited error handling in notification polling. Consider:

  • Exponential backoff for failed requests
  • User notification of connection issues
  • Graceful degradation when offline

Minor Improvements 🔧

1. Performance Optimizations

  • Debounce follow/unfollow actions to prevent rapid API calls
  • Implement lazy loading for large follow lists
  • Cache player names to reduce redundant fetches

2. UX Enhancements

  • Add loading states for follow/unfollow actions
  • Show notification count badge
  • Allow bulk unfollow operations

Test Coverage 🧪

Missing: The PR lacks test coverage for:

  • Follow store state management
  • Notification polling logic
  • Error scenarios
  • Address normalization

Note: Cannot run existing test suite without proper permissions. Recommend adding tests for the new functionality.

Recommendations 📋

High Priority (Security/Performance):

  1. Fix SQL injection vulnerability with parameterized queries
  2. Fix notification timestamp comparison logic
  3. Implement server-side filtering for "Following" tab
  4. Add proper TypeScript interfaces

Medium Priority (Robustness):
5. Add error boundaries and offline handling
6. Implement follow limits and cleanup
7. Add comprehensive test coverage

Low Priority (UX):
8. Add loading states and better error messaging
9. Optimize performance with debouncing/caching

Overall Assessment ✅

Despite the critical issues identified, this is a well-structured implementation that follows the codebase patterns. The architecture is sound and the user experience is intuitive. With the security and performance fixes implemented, this would be a solid addition to the game's social features.

Status: ⚠️ Needs Work - Address critical security issues before merging.


View job

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@client/src/hooks/usePlayerSearch.ts`:
- Around line 36-40: In usePlayerSearch, when the branch that handles an empty
query returns early (the block that logs and calls setResults([])), also clear
the hook's error state so a previous failure isn't still shown; add a call to
the error-state setter (e.g., setError(null) or setSearchError(null) depending
on the hook's state variable) immediately after setResults([]) to reset errors
before returning.
♻️ Duplicate comments (1)
client/src/hooks/usePlayerSearch.ts (1)

42-58: Guard against missing mintedByAddress before constructing the SQL.
If GAME_TOKEN_ADDRESS is unavailable for ChainId.WP_PG_SLOT, the query becomes ... = "undefined".

🛠️ Proposed fix
-    setLoading(true);
-    setError(null);
+    if (!mintedByAddress) {
+      setError('Game token address not available');
+      setResults([]);
+      return [];
+    }
+
+    setLoading(true);
+    setError(null);

Comment on lines +36 to +40
if (!searchQuery.trim()) {
console.log('[PlayerSearch] Empty search query, clearing results');
setResults([]);
return [];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clear error state when the query is empty.
Without this, a previous failure can keep an error visible after the user clears the input.

🛠️ Proposed fix
     if (!searchQuery.trim()) {
       console.log('[PlayerSearch] Empty search query, clearing results');
+      setError(null);
       setResults([]);
       return [];
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!searchQuery.trim()) {
console.log('[PlayerSearch] Empty search query, clearing results');
setResults([]);
return [];
}
if (!searchQuery.trim()) {
console.log('[PlayerSearch] Empty search query, clearing results');
setError(null);
setResults([]);
return [];
}
🤖 Prompt for AI Agents
In `@client/src/hooks/usePlayerSearch.ts` around lines 36 - 40, In
usePlayerSearch, when the branch that handles an empty query returns early (the
block that logs and calls setResults([])), also clear the hook's error state so
a previous failure isn't still shown; add a call to the error-state setter
(e.g., setError(null) or setSearchError(null) depending on the hook's state
variable) immediately after setResults([]) to reset errors before returning.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants